Skip to content

Record project stack facts after creation, with detection as evidence - #699

Merged
PavloSEO merged 2 commits into
mainfrom
feat/648-stack-facts
Sep 9, 2026
Merged

PavloSEO merged 2 commits into
mainfrom
feat/648-stack-facts

Conversation

@PavloSEO

@PavloSEO PavloSEO commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Advances #648 (child C: priorities raised by what the site is actually built on, detected by tech-detect and recorded in project.json). The epic stays open.

The gap

seohead/projects/priorities.py already raises work from a project's framework or cms fact, but nothing could write a fact after project new: grep -rn tech_detect seohead/projects/ returned nothing, and prepare_project ran crawl/sitemap/competitors/plan only. A stack had to be typed in by hand at creation.

What this adds

project-facts / seo_project_facts, registered through the shared handler in both interfaces:

seohead project facts --directory ./example-project --input '{"facts":[...]}' --apply
seohead project facts --directory ./example-project --detect --apply
  • Operator decisions outrank detected evidence. A supplied fact is a decision and always wins. A detection never overwrites an operator-entered fact: it reports what it saw as detected_value with the action kept_operator and leaves the record alone. Which is which is carried by the fields the schema already had — a detected fact's provenance starts with detected by tech-detect and names the marker that matched and the URL it was seen on, and observed_at is that observation's time. A supplied fact claiming that provenance is refused.
  • --detect is the only thing that makes a request, and never implicit. It reads robots.txt first and fetches the project's own target once — two requests at most, through the same guarded single-page tools tech-detect and robots-check use, so it inherits their pinning transport. A disallowed path is not fetched; rules that could not be read are a refusal, not permission. project-prepare still runs no detection.
  • An unavailable detection is not a clean result. The detection block always names a state (run / partial / not_run / unavailable) and a reason. A failed fetch, a disallowed target, a category with no matching signature, and a category where two candidates matched each leave the fact absent with its own reason. Nothing is guessed.
  • Preview is the default; --apply writes project.json through the existing locked, backed-up atomic document writer. Recording a fact does not reorder work by itself — project-priorities --apply still does that, explicitly.

Detection covers the cms and framework facts the packaged policy consults; the fact name is the tech-detect category name, so there is no translation table to drift. site_type and anything else remain operator-entered.

The core stays free of cli and servers: the detection tools are injected, the same way prepare_project takes them.

Tests

tests/test_project_facts.py (14) and tests/test_project_facts_interfaces.py (4). Every detection is injected; no test makes a request. Positive, failure, unavailable and partial paths are covered: unreadable robots, disallowed target, failed detection, no signature, ambiguous candidates, a generator version label that is not a second candidate, and forged detection provenance. On origin/main the new files fail — ModuleNotFoundError: seohead.projects.facts on collection, and 4 failed in the interface module.

Gates

  • ruff check ., ruff format --check . — clean.
  • pytest tests/test_registration.py tests/test_docs_drift.py tests/test_doc_counts.py -q — passed.
  • pytest -q4543 passed, 2 skipped in 6:49.

Docs: docs/PROJECTS.md gets a "Recording stack facts" section; docs/TOOLS.md, docs/INPUTS.md and docs/TOOL_REFERENCE.md are updated (the last two regenerated), with the command counts in PROVENANCE.md, docs/COMPARISON.md, docs/SKILLS.md, docs/USAGE.md and docs/scenarios/README.md moved to 93/98. changelog.d/648.md added.

…#648)

The priority policy already raised work from a project's `framework` or `cms`
fact, but nothing could write one after `project new`: a stack had to be typed
in by hand, and `tech-detect` never reached `project.json`.

`project-facts` closes that link. Supplied facts are operator decisions and
always win; `--detect` runs one `tech-detect` pass over the project's own target
and records what it saw as evidence under its own provenance, so the record says
which is which. A detection never overwrites an operator-entered fact. It reads
robots.txt first and fetches at most twice, through the same guarded single-page
tools the standalone commands use, and it runs only where it was asked for --
`project-prepare` does not run it. A failed, disallowed or ambiguous detection
leaves the fact absent with its reason instead of guessing.
@PavloSEO
PavloSEO merged commit 62f7531 into main Sep 9, 2026
9 checks passed
@PavloSEO
PavloSEO deleted the feat/648-stack-facts branch September 9, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant