diff --git a/launchpad/docs/corpus/AGENTS.md b/launchpad/docs/corpus/AGENTS.md new file mode 100644 index 00000000000..e42228305d9 --- /dev/null +++ b/launchpad/docs/corpus/AGENTS.md @@ -0,0 +1,468 @@ +--- +id: corpus-agents +type: agent +status: active +origin: launchpad +audiences: + - agent + - reviewer +evidence: + - statement: "This node was authored and checked against repository revision 0052f5a7820ca4ca261efa233feb8bb53858ade6." + entry_class: FACT + evidence: + - "commit 0052f5a7820ca4ca261efa233feb8bb53858ade6" + - statement: "Markdown with YAML front matter is the one canonical authored representation of a corpus node; every other serialization is a generated derived view." + entry_class: FACT + evidence: + - "launchpad/decisions/ADR-0028-corpus-canonical-representation.md" + - statement: "A node's front matter is validated against node.schema.json, which requires id, type, status, origin, audiences and evidence, additionally permits relationships, and rejects any field beyond those seven." + entry_class: FACT + evidence: + - "launchpad/docs/corpus/schema/node.schema.json" + - statement: "The corpus root is launchpad/docs/corpus, and validate.py is the deterministic check that governs it." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - "Justfile" + - statement: "The schema/ subtree is excluded from validation because it is the schema's own testing infrastructure rather than corpus content." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - statement: "Evidence precedence is contextual by claim type, and two authoritative sources of the same claim type in conflict leave the node flagged for a human rather than silently resolved." + entry_class: FACT + evidence: + - "launchpad/decisions/ADR-0029-corpus-evidence-precedence.md" + - statement: "Citations take six shapes and only three of them name a file that can be opened." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/CONTRACT.md" + - statement: "Citation checking is structural: the validator confirms a cited path resolves to a real file inside the repository, never that the file supports the statement it sits under." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - statement: "A relationship whose target matches no loaded node's id is a hard validation error." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - statement: "This file is also resolved as the nearest AGENTS.md for every change under launchpad/docs/corpus, so it is read as governing instructions and not only as a corpus node." + entry_class: FACT + evidence: + - "launchpad/scripts/preflight_core.py" + - statement: "Changes under launchpad/docs/corpus are validated in CI on pull requests and on pushes to the launchpad branch." + entry_class: FACT + evidence: + - ".github/workflows/launchpad-corpus-validate.yml" + - statement: "Evidence entries are classified FACT, INFERENCE or TEAM_KNOWLEDGE, and the class chosen decides which further fields the schema requires or forbids." + entry_class: FACT + evidence: + - "launchpad/docs/corpus/schema/node.schema.json" + - "launchpad/project-intelligence/CONTRACT.md" + - statement: "supersedes is a typed relationship whose declared directionality is that the source replaces the target and the target becomes historical." + entry_class: FACT + evidence: + - "launchpad/docs/corpus/schema/relationships.schema.json" + - statement: "A non-GitHub external URL is reported UNVERIFIED, because the validator can neither pin it to a commit nor open it." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - statement: "A GitHub file link is checked for a full-SHA pin and a non-empty path segment only; the validator never establishes that the named file exists." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - statement: "Deleting a node breaks every relationship targeting it, while retiring it by status change leaves those relationships resolving." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - statement: "Retirement is therefore a status change that keeps the file and spends the id permanently, rather than a deletion." + entry_class: INFERENCE + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - "launchpad/decisions/ADR-0028-corpus-canonical-representation.md" + confidence: 0.8 + - statement: "Issue #636 requires that the draft is checked against the repository revision recorded in provenance." + entry_class: TEAM_KNOWLEDGE + provided_by: "launchpad-26/buzz#636 definition of done" + - statement: "Every non-.md file under the corpus root is rejected today, including one placed under generated/, because no generator exists to reproduce it from canonical Markdown." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - statement: "A second or subsequent FACT resting only on a commit citation produces another non-fatal UNVERIFIED notice and does not fail the run." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - statement: "A citation naming a line or line range is checked for the path only; the line number is never compared against the file's length." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" +--- + +# Working with the documentation corpus + +Instructions for creating, updating and retiring one node in +`launchpad/docs/corpus/`. Look up the section you need; this is a reference, not a +tutorial. + +**Authoritative sources — this file duplicates none of them:** + +| For | Read | +|---|---| +| The front-matter contract (fields, enums, conditional rules) | `launchpad/docs/corpus/schema/node.schema.json` | +| Prose explanation of those fields | `launchpad/docs/corpus/schema/README.md` | +| Adding a value to a closed enum | `launchpad/docs/corpus/schema/COMPATIBILITY.md` | +| Relationship types and their directionality | `launchpad/docs/corpus/schema/relationships.schema.json` | +| Why Markdown + front matter is canonical | `launchpad/decisions/ADR-0028-corpus-canonical-representation.md` | +| How to rank conflicting evidence | `launchpad/decisions/ADR-0029-corpus-evidence-precedence.md` | +| The six citation shapes | `launchpad/project-intelligence/CONTRACT.md` §3 | +| What the checker actually enforces | `launchpad/project-intelligence/corpus/validate.py` | + +If this file and any of those disagree, **they win** — this one has drifted and +should be fixed. + +## What a corpus node is + +**One file is one node.** A node is a Markdown file with YAML front matter, and that +is the single canonical authored representation; anything else — JSON, an index, a +graph serialization — is a generated derived view, never hand-authored. + +**One node is one independently maintainable idea.** If a second concept, contract or +procedure turns up while you are writing, it does not get folded in. File it as its +own task and link to it. + +**Where it goes.** Anywhere under `launchpad/docs/corpus/`, except `schema/` — that +subtree is the schema's own testing infrastructure and is deliberately skipped by the +checker, so a node placed there is never validated at all. + +**Front matter.** Validated against `node.schema.json`. Field names, which fields are +required, the closed enums and the conditional rules between fields all live in that +file and in `launchpad/docs/corpus/schema/README.md`; they are not repeated here, +because a second copy drifts silently — the checker never reads this document's +prose, so a stale copy would stay green forever. + +**`id` is permanent.** Kebab-case, assigned once, never renamed. Generated views +derive from it reproducibly, so renaming an id is a migration, not an edit. + +**Relationships are optional and must resolve.** A `relationships[].target` naming an +id no node in the corpus carries is a hard error, so an edge may only name a node that +is already merged. Declaring none is always valid — but **check before you justify it**. +"There was nothing to point at" was true when this was the corpus's only node and stops +being true the moment a second one merges. Enumerate what exists +(`ls launchpad/docs/corpus/**/*.md`) and give the real reason, which may simply be that +the edges are being added in one pass later. Two independent agents authoring sibling +nodes copied an earlier version of this paragraph and produced a **false** justification +from it, because it read as a general rule rather than a fact about one moment. + +**Authored versus generated.** Every non-`.md` file under the corpus root must live +in a `generated/` directory. Today the checker rejects such files even there, because +no generator exists yet to reproduce them from canonical Markdown, and a hand-written +file in `generated/` is indistinguishable from a real projection. That contract is +owned by #1316; until it lands, a corpus change adds Markdown only. + +## Evidence, citations, and what validation proves + +Every substantive claim in a node's body needs an entry in its front-matter +`evidence` array. That array is also the node's **provenance ledger** — there is no +separate provenance field, so the revision a node was written against belongs in +there too, as a commit citation. + +### Choosing a class + +Three classes exist: `FACT`, `INFERENCE`, `TEAM_KNOWLEDGE`. Which one you choose +decides which additional fields the schema then requires or forbids — those rules are +in `node.schema.json` and `launchpad/docs/corpus/schema/README.md`, and are not +restated here. + +What the classes are *for* is the part that is easy to get wrong: + +- **`FACT`** — you opened the cited source and it says so. Not "a source exists that + probably says so." +- **`INFERENCE`** — you reasoned to it from evidence. Reasoning is not fact, however + good it is. +- **`TEAM_KNOWLEDGE`** — something told to the corpus that no source corroborates, with + `provided_by` naming who or what said it: a person, an issue, a decision record. It + is the class for uncorroborated statements, and using it honestly beats promoting a + recollection to `FACT`. It is **not** a place to park a decision you made yourself — + attributing an extrapolation to the thing it started from does not make it something + you were told. + +When two sources disagree, do not average them and do not pick the newer one. For how +the system **currently behaves**, executable evidence — code, config, schema, passing +tests — outranks documentation and history. For **intended or authorized** behaviour, +accepted decisions outrank code that has drifted from them. When two sources of the +*same* claim type conflict, stop: record the conflict and leave the node flagged for a +human rather than resolving it yourself. `ADR-0029` is the full rule. + +### What the checker does with each citation shape + +`CONTRACT.md` §3 enumerates six shapes — file range, file line, bare path, graph edge, +tool result, commit. It contains **no URL form at all** (grep it for `http`: zero hits). +The two URL rows below are forms `validate.py` recognises and §3 does not enumerate, so +this table is **seven** rows and is not a summary of §3. An earlier version of this +sentence claimed it was, and an agent authoring a sibling node built a scope argument on +the miscount before their plan review caught it. What `validate.py` does with any of them +is not +documented anywhere else, so it is here — provisionally. This table is reference +material rather than instruction, and belongs in the evidence standard once that +lands (#1314); when it moves, this section links to it instead. + +Read the middle column carefully: only two rows involve opening anything. + +| Shape | Checker's verdict | Does it prove the target exists? | +|---|---|---| +| Bare repository path | Opened on disk; must be a real **file** inside the repo. A directory fails. | **Yes** | +| Path with a line or line range | The path is opened. The line number is **not** checked at all. | File yes, line **no** | +| GitHub file link | **Syntax only.** Must be pinned to a full 40-character SHA and have a non-empty path segment after it. | **No** | +| External (non-GitHub) URL | Reported `UNVERIFIED`. Nothing to pin, nothing to open. | No | +| Commit reference | Reported `UNVERIFIED`. Nothing on disk to open. | No | +| Graph edge | Reported `UNVERIFIED`. | No | +| Tool result | Reported `UNVERIFIED`. | No | + +Anything matching **no** known shape is a hard error, not an `UNVERIFIED` notice. + +**The GitHub row is the trap.** The checker never contacts GitHub. It reads the URL as +a string, and a link pinned to a real commit but naming a file that has never existed +passes as cleanly as a correct one: + +``` +https://github.com/launchpad-26/buzz/blob//does-not-exist.md -> ok +``` + +So a typo in a remote path ships silently. A repo-relative path is checked against the +filesystem and a GitHub link is not — prefer the former for anything in this +repository, and treat a GitHub link as a *pin*, not as evidence the target is there. + +### Three things a passing run does not mean + +**1. It does not mean a citation supports its claim.** Checking is *structural*. The +checker confirms a path resolves to a real file; it never opens that file and compares +it against your `statement`. A `FACT` citing a real file that says nothing on the +subject passes cleanly. Only a human reading the source establishes a `FACT`. + +**2. `UNVERIFIED` is not a pass.** Those notices are printed, never fatal, and they +mean the checker recognised the shape and could not open it. A `FACT` resting only on +`UNVERIFIED` citations has not been checked by anything — open the source and keep the +class, or change the class. + +One conventional exception: the **provenance entry recording the revision** cites a +commit id, which no file can corroborate because the citation *is* the claim. It is +still checkable, just not by this checker: + +```bash +git cat-file -e # exit 0 means that revision exists in this repository +``` + +Run that, and the entry is a `FACT`. Every other claim needs a source you opened. A +commit citation attached to a claim *about repository content* is not covered — that +claim needs the file, at that revision. + +**When the only source is an issue, a PR or a discussion**, you have no openable file and +no way to pin one: the validator's repository-link check matches only file and tree views, +so an issue URL is an external URL and lands on `UNVERIFIED`. Do not force it into a +`FACT` on a tool-result or URL citation. Use `TEAM_KNOWLEDGE` with `provided_by` naming +the issue — that is what the class is for, and ADR-0029 requires GitHub history to stay +attributed rather than be promoted to fact. An earlier draft of this section left that +case with no honest class at all, which an agent authoring a sibling node hit directly. + +**Nothing enforces this.** The checker treats every commit citation identically: a +second, third or tenth `FACT` resting only on `commit ` produces nothing but +extra non-fatal `UNVERIFIED` notices and still exits 0. Verified by adding one and +watching the run pass. So this is a convention a **reviewer** has to hold, not a rule +the tooling holds — if a node's ledger shows more than one commit-only `FACT`, that is +the signal, and no check will raise it for you. + +**3. A line number is not verified.** `Justfile:999999` is accepted against a +1005-line file (#1459). Prefer a bare path until that is fixed; a position that has +silently drifted is worse than no position, because it looks precise. + +### Pinning + +A GitHub link to a repository file must use the full 40-character commit SHA. +`blob/main` is rejected, and correctly: evidence that can change underneath a green +validation run is the exact staleness provenance exists to catch. A link pinned but +naming no file is also rejected — it cites a repository at a commit, not the source +of the claim. + +## Running the check + +All three procedures below end with the same command, run from the repository root: + +```bash +python3 launchpad/project-intelligence/corpus/validate.py +``` + +Exit status 0 is a pass; 1 means at least one error, and every error names the node it +came from. `just corpus-validate` runs exactly this, but needs the Hermit environment +activated first (`. ./bin/activate-hermit`) — the direct form above does not. The same +command runs in CI on every change under `launchpad/docs/corpus/`, so a local failure +is a CI failure. + +To check a corpus tree somewhere other than the real one, pass `--root `. Without +it the command always validates `launchpad/docs/corpus/`, whatever directory you are +standing in. + +## Creating a node + +1. **Confirm it is one idea.** If you are describing two contracts, or a concept and + the procedure that uses it, that is two nodes. File the second as its own task now. +2. **Check nothing already covers it.** Read the existing nodes under + `launchpad/docs/corpus/`. If one is close, you are updating, not creating. +3. **Record what you inspected, before drafting.** The repository revision + (`git rev-parse HEAD`), the source paths and symbols you read, the tests, + specifications and configuration you consulted, and — explicitly — anything you + expected to verify and could not. Working notes need not be committed, but every + category has a destination in the finished node, and they are not the same one: + + | What you recorded | Where it ends up | + |---|---| + | The revision | A commit citation in the `evidence` ledger (step 6) | + | Paths, symbols, tests, specs, configuration you read | Citations on the `evidence` entries whose claims they support (step 7) | + | Expected but could not verify | The body's scope-and-omissions section (step 8), named as a gap | + + Anything that reaches none of those three was not needed. If you inspected a source + that backs no claim, you have either a missing claim or a stale note — decide which + rather than leaving it in a file nobody reads. +4. **Choose the `id`.** Kebab-case, and permanent from this moment. Pick something that + describes the idea, not where the file currently sits. +5. **Create the file** anywhere under `launchpad/docs/corpus/` except `schema/`. +6. **Write the front matter** against `node.schema.json`. Include a commit citation for + the revision from step 3 — the ledger is the only schema-legal place for it. +7. **Write one `evidence` entry per substantive claim** you intend to make. Classify + honestly; open every source you call a `FACT`. +8. **Write the body**, structured for lookup, with a scope section carrying **two + distinct things**: what the node does not cover and who owns it, and — separately — + what you expected to verify from step 3 and could not. A boundary and a confidence + disclosure are different, and an earlier version of this step named only the first, + leaving step 3's third category with nowhere to go. +9. **Add relationships only to nodes that exist on the branch you are merging INTO.** + Not the branch you are working on — that distinction is the whole trap. The checker + loads whatever is present where it runs, so a target that resolves in your worktree + can be a hard error in CI: an agent branched off an unmerged node targeted it, + validated clean locally, and would have broken the run on `launchpad` where that node + does not exist yet. Check against the merge base + (`git ls-tree -r --name-only origin/launchpad -- launchpad/docs/corpus`), not your own + tree. None is a valid answer, and while the corpus is being built out it is usually + the right one. +10. **Run the check.** Fix what it names, and re-run until it exits 0. + +## Updating a node + +**What the recorded revision means — working practice, not settled policy.** #636's +definition of done requires that "the draft is checked against the repository revision +recorded in provenance", so the revision is at minimum the one the claims were checked +against. Everything beyond that — whether it may stay put across edits, what to do when +only some claims are re-verified — is **#1321's** to settle and is not established here. +Until it does, this document works to the rule below, and says so rather than dressing +it up as a corpus-wide standard. + +1. **Confirm the change belongs in this node.** New idea, not new detail about the + existing one? That is a new node. +2. **Re-verify the claims you are touching**, against those sources at current `HEAD` + (`git rev-parse HEAD`). A claim whose source moved is not still a `FACT` because it + used to be. +3. **Update the ledger in the same edit as the body.** A new claim without an entry, or + an entry left behind by a deleted claim, are the two ways these drift apart. +4. **Decide whether the recorded revision moves.** A node carries one snapshot, so + moving it makes a statement about the whole ledger, not just the claims you touched. + - Re-verified every claim at `HEAD` → move it. + - Re-verified some → move it only if the rest still hold at `HEAD` too. Check, do + not assume. + - Re-verified nothing → leave it. + - **Every cited source byte-identical between the recorded revision and `HEAD`** → + leave it. Checking a claim at either point was the same act, so moving the + revision would assert a re-check that added nothing. + `git diff --name-only -- ` returning empty is + the test. This case is why the rule is stated as four branches and not as "bump on + every edit": that shorter rule was in an earlier draft and contradicted what this + very node does. +5. **Leave the `id` alone.** Always. +6. **Run the check.** + +## Retiring a node + +Retiring is a **status change, not a deletion**. The file stays, so the checker keeps +loading the node and inbound relationships keep resolving. Nothing here is enforced by +tooling — a retired node with stale inbound edges validates exactly like a healthy one. + +1. **Set `status` to the retired value** defined in `node.schema.json`. Do not delete + the file. Deleting it is what breaks inbound relationships: a + `relationships[].target` naming an id nothing carries is a hard error, and every + node pointing at the deleted one starts failing. +2. **Find what points at it.** Search the corpus for the node's `id`. Those edges will + still resolve — that is the problem, not the safety net. Readers and generated + views will keep being sent to a node that has stopped being current, and no check + will ever mention it. +3. **Decide what replaces it, and say so in the vocabulary.** If another node takes + over the subject, that node declares `supersedes` targeting the retired id — the + type exists in `relationships.schema.json` for exactly this. Repointing the inbound + edges from step 2 at the replacement is a judgement call: repoint the ones that + wanted the subject, leave the ones that genuinely meant the retired node. +4. **If nothing replaces it**, say that in the retired node's body. A reader arriving + from an old link needs to be told the subject is gone, not left guessing. +5. **Never reuse or rename the `id`.** A retired id stays spent — renaming breaks + generated views that resolve through it, and reuse silently points old references + at new content. +6. **Record why**, in the body and in the ledger, at the revision you checked. +7. **Run the check.** It will pass whether or not you did steps 2-4 correctly. That is + the point of doing them deliberately. + +## Scope and omissions + +**This document covers** how to create, update and retire one corpus node, what the +front-matter contract is and where it lives, how to classify and cite evidence, and +what the deterministic check does and does not establish. + +**It does not cover, and these are gaps rather than silence:** + +| Not covered here | Owned by | +|---|---| +| Per-type standards — naming, identifiers, linking, provenance, status, taxonomy, diagrams, evidence | somewhere in #1307–#1351 | +| Templates for each node type — concept, component, capability, interface, flow, policy, procedure, runbook, reference, specification, and the rest | somewhere in #1307–#1351 | + +**That range is a range, not a mapping.** It is 45 issues across those subjects, and this +table does **not** say which number owns which subject. Look the subject up +(`gh issue list --repo launchpad-26/buzz --search "corpus standard for "`) rather +than citing this table for a subject-to-issue pairing — an agent authoring a sibling node +did exactly that and produced nine invented mappings, which is the "FACT cited to a file +that does not discuss the claim" failure this document warns about two sections up. +| How generated artifacts prove their provenance, and the exception process for them | #1316 | +| Encoding ADR-0029's claim-type classification and the flagged state in the schema and checker | #1410 | +| The human-facing entry point to the corpus | #639 | +| Line numbers in citations not being verified against file length | #1459 | + +Until the standards land there is no per-type template to follow: write the node +against `node.schema.json` and the rules above, and expect a later task to reshape it. + +**No `relationships` in this node's own front matter.** There is no other node to point +at, and a `relationships[].target` naming an id no node carries is a hard error. The +absence is deliberate; the first sibling node is the moment to revisit it. + +**How to check this node's own recorded revision.** Run +`git diff --name-only -- `. Empty output means +no cited source has moved since the revision was recorded, so every claim still stands +where it was checked. Do not take that on this document's word — the command is the +check. + +**What the corpus has NOT settled about revisions.** Whether a recorded revision may +stay put while a node is edited, and what an author must do when only some claims are +re-verified, is **#1321's** to decide (`document corpus standard for provenance`, +unlanded). Until it lands, *Updating a node* below states this document's working +practice, not a corpus-wide rule — three independent review passes rejected earlier +attempts to present it as one, on the grounds that no authorized source establishes it. +When #1321 lands, that section defers to it. + +**Expected but not verified when this node was written**, per the rule in *Creating a +node* step 3: + +- **No agent harness was tested reading this file as its resolved `AGENTS.md`.** The + front matter is harmless to the checker, and to `preflight_core.py`, which resolves + the path without parsing content. Whether it degrades the file for a harness that + *reads* it as instructions is unknown. +- **The relationship enums in `node.schema.json` and `relationships.schema.json` were + not checked against each other.** `relationships.schema.json` was read for the + `supersedes` directionality this node cites. `node.schema.json` states that a test + guards the two enum lists against drifting apart; that test was not run, so a reader + relying on the two agreeing is relying on that guard rather than on a check made here. + +**This file is read twice.** It is a corpus node, validated like any other; it is also +resolved as the nearest `AGENTS.md` for every change under `launchpad/docs/corpus/`, +so an agent working anywhere in this subtree is handed it as governing instructions. +Write it to be followed, not merely to be accurate. diff --git a/launchpad/docs/corpus/standards/confidence.md b/launchpad/docs/corpus/standards/confidence.md new file mode 100644 index 00000000000..820fbe4e159 --- /dev/null +++ b/launchpad/docs/corpus/standards/confidence.md @@ -0,0 +1,407 @@ +--- +id: corpus-standard-confidence +type: governance +status: active +origin: launchpad +audiences: + - agent + - reviewer +evidence: + - statement: "This node was authored and checked against repository revision 60d4947b7145a6ef25f185b9c25d43e43d99de3c." + entry_class: FACT + evidence: + - "commit 60d4947b7145a6ef25f185b9c25d43e43d99de3c" + - statement: "confidence is a number between 0.0 and 1.0, required for INFERENCE entries only, and forbidden on FACT and TEAM_KNOWLEDGE entries." + entry_class: FACT + evidence: + - "launchpad/docs/corpus/schema/node.schema.json" + - statement: "Both bounds are inclusive, an integer is accepted as a number, and a boolean or a quoted string is rejected on type before the range is ever considered." + entry_class: FACT + evidence: + - "launchpad/docs/corpus/schema/node.schema.json" + - statement: "memory.py's __post_init__ raises when confidence is missing from an INFERENCE, present on any other class, not a real number, or outside the closed interval." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/memory.py" + - statement: "The corpus check enforces the confidence rule through node.schema.json alone; validate.py never imports memory.py, so the two are independent enforcement paths rather than one calling the other." + entry_class: FACT + evidence: + - "launchpad/project-intelligence/corpus/validate.py" + - "launchpad/docs/corpus/schema/node.schema.json" + - statement: "A confidence of NaN satisfies node.schema.json and passes corpus validation, while memory.py rejects the same value as outside the interval." + entry_class: FACT + evidence: + - "launchpad/docs/corpus/schema/node.schema.json" + - "launchpad/project-intelligence/memory.py" + - statement: "Inference and team knowledge may supply context but are never treated as fact on their own, and stay attributed to their source and distinguishable from FACT claims." + entry_class: FACT + evidence: + - "launchpad/decisions/ADR-0029-corpus-evidence-precedence.md" + - statement: "The flagged status names two same-claim-type authoritative sources contradicting each other with no human resolution yet, and is explicitly not a generic low-confidence marker." + entry_class: FACT + evidence: + - "launchpad/docs/corpus/schema/node.schema.json" + - "launchpad/decisions/ADR-0029-corpus-evidence-precedence.md" + - statement: "Citation checking is structural: the check confirms a cited path resolves to a real file and never that the file supports the statement it sits under." + entry_class: FACT + evidence: + - "launchpad/docs/corpus/AGENTS.md" + - "launchpad/project-intelligence/corpus/validate.py" + - statement: "AGENTS.md carries an INFERENCE at confidence 0.8 claiming that retirement is a status change rather than a deletion, and it is the only INFERENCE in the validated corpus at the recorded revision because validate.py excludes the schema/ subtree, whose valid fixture carries one too." + entry_class: FACT + evidence: + - "launchpad/docs/corpus/AGENTS.md" + - "launchpad/project-intelligence/corpus/validate.py" + - "launchpad/docs/corpus/schema/fixtures/valid/node-full.md" + - statement: "No corpus mechanism records whether a past inference turned out to be correct, so a confidence value can only express the author's assessed strength of reasoning and never an observed frequency." + entry_class: INFERENCE + evidence: + - "launchpad/docs/corpus/schema/node.schema.json" + - "launchpad/project-intelligence/corpus/validate.py" + - "launchpad/project-intelligence/memory.py" + confidence: 0.6 + - statement: "Because the schema accepts any in-range value without recording how it was chosen, two entries carrying the same number need not represent comparable strength, so values are not safely comparable between authors or nodes." + entry_class: INFERENCE + evidence: + - "launchpad/docs/corpus/schema/node.schema.json" + confidence: 0.8 + - statement: "An INFERENCE whose citation supports the subject of the claim but not the choice the claim makes is a decision in disguise, and belongs in TEAM_KNOWLEDGE attributed to whoever decided it." + entry_class: TEAM_KNOWLEDGE + provided_by: "launchpad-26/buzz#636 cross-model final review, relayed in the #1309 task brief: a second INFERENCE on AGENTS.md was rejected as laundering an unsourced policy choice into a class that made it look derived, and was reclassified to TEAM_KNOWLEDGE attributed to the issue's definition of done" +--- + +# Standard: `confidence` + +What the `confidence` number on an evidence entry means, when it is required, how an +author picks one, and what a reader may conclude from it. + +This is a policy node. Look up the section you need. + +| For | Read | +|---|---| +| The field's machine contract — type, range, which class requires or forbids it | `launchpad/docs/corpus/schema/node.schema.json` | +| Prose walkthrough of the front-matter fields | `launchpad/docs/corpus/schema/README.md` | +| The same rule enforced at runtime on the in-process store | `launchpad/project-intelligence/memory.py` | +| How to rank conflicting evidence, and when to stop and escalate | `launchpad/decisions/ADR-0029-corpus-evidence-precedence.md` | +| Creating, updating and retiring a node | `launchpad/docs/corpus/AGENTS.md` | + +Those files are authoritative. Where this document and any of them disagree, **they +win** — this one has drifted and should be fixed. + +**This document restates the schema only where its own subject forces it to.** A standard +about `confidence` cannot omit the rule for `confidence`, so Requirement 1 states that one +row of the field-combination matrix and Requirements 2 and 3 describe how the value itself +is bounded. What is *not* restated: the literal bound values, the rules for the other +entry fields, and every enum's member list. The check never reads body prose, so a copy +that goes stale stays green forever — which makes the copies that do exist here named +drift surfaces rather than accidents. + +**If the schema's rules change, these are the places in this document that must change +with them**, and there is nowhere else: + +| Restated here | Tracks | +|---|---| +| Requirement 1 | The `confidence` row of the field-combination matrix | +| Requirements 2 and 3, and the enforcement table in *Enforcement, and where it stops* | What the schema does and does not catch on the value itself — the NaN gap in particular, which #1463 is filed to close | +| The description of `flagged`, in *What the number is for* and in *Exceptions and escalation* | The schema's `status` description, which #1410 is filed to encode further | + +## Scope and authority + +**This standard governs** the `confidence` key on an entry in a corpus node's +`evidence` ledger — the one place the field exists. + +**Its authority is derived, not original.** The structural half of this standard is +already law: `node.schema.json` enforces it, `validate.py` runs that schema, and CI runs +`validate.py`. This document does not create those rules and cannot relax them. What it +adds is the half no schema can hold — what the number is *for*, how to choose one +honestly, and what a reader is entitled to conclude. That half is enforced by review. + +**On the interpretation of an INFERENCE, ADR-0029 outranks this document.** It is the +accepted decision; this is a standard written under it. + +## What the number is for + +An INFERENCE is a claim you reasoned to rather than read. `confidence` is **the +author's own rating of how strongly the cited evidence supports the statement** — a +declared strength of reasoning, published so a reader can weigh the claim and a reviewer +can challenge it. + +That is the whole of it. In particular: + +- It is **not a probability**, and not a frequency. Nothing in the corpus records + whether a past inference turned out to be right, so no number here has ever been + scored against an outcome. There is no calibration behind it and none is collected. +- It is **not a quality score** for the node, the author, or the citation. +- It is **not a promotion path**. A confidence of 0.99 does not make an INFERENCE into a + FACT. Class is decided by *how you came to know the thing*, never by how sure you feel + about it, and ADR-0029 is explicit that inference is never treated as fact on its own. +- It is **not a conflict marker**. Two authoritative sources of the same claim type + saying different things is `status: flagged`, not a low number. See *Exceptions and + escalation*. + +**The failure this field exists to prevent.** A number with no stated meaning is +decoration. The specific way that goes wrong: an author writes `0.8` because +it feels about right, and a reader — or a generated view, or a later agent — treats it +as calibrated and reasons onward from it. The number then carries more weight than +anything that produced it. Everything below is aimed at that. + +## Requirements + +These are MUSTs. The first two are enforced mechanically. Requirement 3 is a MUST that +no check reaches — see *Enforcement, and where it stops*. The rest are enforced by +review, and a reviewer who lets one through has approved a defect. + +1. **Every INFERENCE entry MUST carry a `confidence`, and no FACT or TEAM_KNOWLEDGE + entry may carry one.** This is structural and there is no exception to seek — the + schema rejects the node either way. +2. **The value MUST be a number within the closed interval the schema defines.** Both + bounds are inclusive. A quoted string is not a number and is rejected on type. +3. **The value MUST be a real number, not NaN.** The infinities are already caught on + range — `.inf` exceeds the maximum and `-.inf` falls below the minimum — so NaN is + the single value that escapes, because every comparison against it is false and the + range assertions therefore never apply. See *Enforcement, and where it stops*. This + one is on the author and the reviewer alone. +4. **The reasoning the number rates MUST be visible.** The reader has to be able to see + what was reasoned from what: in the `statement`, in the body section the entry + supports, or in both. A number attached to reasoning nobody can inspect cannot be + challenged, and an unchallengeable claim is the thing this corpus exists to avoid. +5. **An entry MUST NOT be an INFERENCE if its citation supports the subject of the claim + but not the choice the claim makes.** That is a decision, not a derivation. Reclassify + it to TEAM_KNOWLEDGE and name who decided. See *Reasoning versus deciding*. +6. **A number MUST NOT be moved because someone pushed back on it.** Re-verify the claim + or reclassify the entry. Adjusting the number to settle an argument records agreement + where there was none. Re-encoding an existing value onto the band values in *Guidance* + is not a move under this rule, but do not tell yourself nothing changed: snapping to a + peg preserves the assessment while it can and does alter the entry's **rank** against + its neighbours, which is the one thing the number legitimately does. So the record must + name the band's "Means" row the assessment matches and why — not merely report that a + re-encoding happened. A value equidistant from two pegs has no tie-break rule here; it + is decided by the "Means" column and by nothing else. +7. **When a claim is re-verified at a new revision, its confidence MUST be re-considered + in the same edit.** A number that outlived the reasoning it rated is worse than no + number, because it still looks current. + +## Guidance + +These are SHOULDs. Depart from them with a reason. + +**Use a coarse scale.** Three bands carry everything this field can honestly express: + +| Band | Value | Means (this column decides the band) | Illustration, not a test | +|---|---|---|---| +| High | `0.8` | The cited sources constrain the conclusion; a competent reader given only them would reach the same one, and the step to it is short. | Sources that agree and leave little room to land elsewhere. | +| Medium | `0.6` | The reasoning is sound but rests on a step the sources do not fully close. | A general principle bridging a gap. An absence-of-evidence argument over a scope you actually checked. | +| Low | `0.4` | You believe it, and you can see how it could be wrong. | A single weak source, or a long inferential chain. | + +**Read the "Means" column, not the illustrations.** The fourth column is there to make the +bands concrete and is not criterial — a one-source claim can be High if that source really +does close the question, and a three-source claim can be Medium if all three leave the same +step open. Count what the sources *settle*, never how many there are. + +**Two decimal places are not warranted.** The scale has no calibration behind it, so +`0.83` claims a precision that nothing supports. Use the band's value and nothing else. +The three values are evenly spaced, carry one decimal, and start at `0.8` because that is +what the only INFERENCE in the validated corpus already used — the convention continues +practice rather than inventing a fresh scale beside it. + +**This node's own ledger follows the convention**, and is the nearest worked example: + +| This node's INFERENCE | Band | Why | +|---|---|---| +| Values are not comparable between authors or nodes | High, `0.8` | One citation, but it settles the question: a schema that accepts any in-range value and records nothing about how it was chosen leaves no other conclusion available. | +| No mechanism records whether a past inference was correct, so the number is assessed strength and not observed frequency | Medium, `0.6` | Three citations, all of them silences. An absence-of-evidence argument over a scope that was checked — which is exactly as far as it can be pushed, because the scope is three files rather than the whole repository. | + +**Prefer removing the inference to rating it.** If a source would settle the claim, open +the source and make it a FACT. High confidence is not a substitute for five minutes of +reading, and it is the cheaper-looking of the two only until someone relies on it. + +**Split compound claims rather than averaging.** If one half is solid and the other is a +guess, a middling number describes neither. Two entries, two numbers. + +**Do not publish a claim you would rate very low.** At that point the honest artefact is +not a weak INFERENCE but a named gap in the node's scope-and-omissions section — "this +was expected and could not be verified." A gap tells a reader to go and find out; a +low-confidence claim invites them to use it anyway. + +**Keep the number stable.** It moves when the reasoning or the evidence moves, and at no +other time. + +## Reasoning versus deciding + +**This is the distinction most worth getting right, and the one nothing will catch for +you.** + +Two entries can look identical in front matter — an `INFERENCE`, some citations, a +number — while being completely different objects: + +- **Reasoning from evidence.** The sources constrain the conclusion. Someone else, + handed only those sources, could get there. +- **Dressing up a decision.** Someone chose something. A citation was then attached that + is *about the same subject* but does not compel the choice. The class makes it look + derived. It was not derived; it was decided. + +The second is the more dangerous artefact, because `INFERENCE` implies the claim came +from the evidence, and a reader who trusts that will not go looking for the person who +actually made the call. The choice becomes unattributable — and an unattributable +decision cannot be revisited, because nobody can be asked why. + +**The test.** Read the statement and the citations, and nothing else. *Could a competent +reader, without knowing what the team wanted, arrive at this statement from these +sources?* + +- Yes → INFERENCE. Rate it. +- No, because the statement contains a choice the sources leave open → TEAM_KNOWLEDGE, + with `provided_by` naming the person, issue or decision that made it. + +**The tell** is a citation that supports the *topic* of the claim while saying nothing +about the *specific* thing being asserted. Read your own citation as an adversary would: +does it say this, or does it merely concern this? + +**Worked example — a legitimate INFERENCE.** `AGENTS.md` claims that retiring a node is +a status change rather than a deletion, at confidence 0.8, citing the checker and +ADR-0028. Both citations do real work: the checker's behaviour establishes that deleting +a node breaks every relationship targeting it, and ADR-0028's requirement that generated +projections derive reproducibly from a stable id establishes that the id cannot be +released. The conclusion follows from the two together. Neither source says "retirement +is a status change" — that step is the author's, which is exactly why it is an INFERENCE +and not a FACT, and why 0.8 rather than higher. + +**Worked example — the move to avoid.** That same node originally carried a second +INFERENCE, which a cross-model review rejected: it stated a policy choice and cited a +file that did not discuss the policy. It was reclassified to TEAM_KNOWLEDGE and +attributed to the issue's definition of done, which is where the choice had actually come +from. Nothing about the front matter had been invalid. The schema was satisfied, the +citation resolved, the check passed. Only a reader comparing the statement against the +source caught it. + +**This is the load-bearing point of the whole standard.** No number, however carefully +chosen, repairs a misclassified entry — a decision at confidence 0.4 is still a decision +wearing the wrong clothes. Get the class right first; the number is the smaller question. + +## What a reader may conclude + +**You may** read it as the author's rating of how strongly the cited evidence supports +the statement, and use it to decide what to check first. + +**You may not:** + +- Treat it as a probability that the claim is true. +- **Compare it across entries, nodes or authors.** The schema accepts any in-range value + and records nothing about how it was chosen, so one author's high band and another's + need not mean the same thing. Two entries at the same number are not thereby equally + strong. +- Aggregate, average or multiply values. They are not measurements and the arithmetic + means nothing. +- Read a high value as "nearly a FACT". The class is the load-bearing signal; the number + ranks entries within a class, and even that only against the same author's other work. +- Rely on it having been reviewed. A number that no reviewer questioned is a number + nobody objected to, which is not the same as a number anyone confirmed. + +**What a passing validation tells you about a confidence value:** that it is present +where required, absent where forbidden, and numerically in range. Nothing else. Checking +is structural — a citation is confirmed to resolve to a real file, never to support the +statement it sits under. + +## Enforcement, and where it stops + +**Enforced mechanically**, by `node.schema.json` through `validate.py`, and in CI on +every change under the corpus root: presence on INFERENCE, absence on the other two +classes, numeric type, and the closed range. A node violating any of these does not +merge. + +**Enforced separately at runtime**, by `memory.py`'s `__post_init__`, for the in-process +store. These are two independent paths to the same rule, not one calling the other — +`validate.py` does not import `memory.py`. Both must be satisfied by anything that +travels between them, and where they differ the stricter one is the safe assumption. + +**Not enforced by anything:** + +| Gap | Consequence | +|---|---| +| Whether the number is justified, or was reasoned at all | An arbitrary value passes cleanly. This standard's judgement rules are review-enforced only. | +| Whether the citation supports the claim | Checking is structural. A FACT or an INFERENCE citing a real file that says nothing on the subject passes. | +| Whether an INFERENCE is really a decision | The move described in *Reasoning versus deciding* is invisible to every check that exists. | +| **A NaN value — and only NaN** | `confidence: .nan` satisfies the schema and passes corpus validation, while `memory.py` rejects the identical value as out of range. The infinities do not escape: `.inf` and `-.inf` are both caught on the bounds. NaN slips through because every comparison against it is false, so the range assertions never fire rather than failing. The schema's own description points at `memory.py` as the enforced rule, so this is a real divergence between the two paths and not a deliberate relaxation. Requirement 3 covers it until #1463 closes it. | + +The pattern across that table: everything a schema can hold is held, and everything that +requires reading is not. Reviewing a confidence value means reading the sources. There +is no cheaper check, and a green run is not one. + +## Exceptions and escalation + +**There is no exception process for the structural requirements.** They are enforced +before merge and cannot be waived by agreement. Changing them means changing +`node.schema.json` under `launchpad/docs/corpus/schema/COMPATIBILITY.md`, which is a +schema change, not an exception. + +**When you cannot pick a number honestly**, that is a signal about the entry, not a +reason to guess. In order: + +1. **Can a source settle it?** Open it. The entry becomes a FACT and the question goes + away. +2. **Is it two claims?** Split it, and rate each. +3. **Is it actually a decision?** Reclassify to TEAM_KNOWLEDGE and attribute it. +4. **Is it none of those, and still not something you would stand behind?** Do not + publish it as a claim. Record it as a gap in the node's scope-and-omissions section. + +**When two authoritative sources of the same claim type contradict each other**, do not +express the disagreement as a middling confidence. That hides a conflict inside a number +and produces a node that looks merely tentative when it is actually unresolved. Record +the contradiction, set the node's `status` to `flagged`, and leave it for a human. +ADR-0029 is the governing rule and its escalation is deliberate: a flagged node is the +accepted safer failure mode, not a defect to be tidied away. + +**When a reviewer disputes a value**, the author re-verifies and then either moves the +number with a stated reason or reclassifies the entry. Requirement 6 forbids splitting +the difference. If it stays unresolved, it escalates as a conflict rather than settling +at an average nobody believes. + +## Scope and omissions + +**This document covers** what `confidence` means, its requirements and guidance, how to +choose a value, how to tell reasoning from a disguised decision, what a reader may +conclude, and what enforcement does and does not reach. + +**It does not cover, and these are gaps rather than silence:** + +| Not covered here | Owned by | +|---|---| +| The other evidence-entry fields — `entry_class`, `evidence`, `provided_by` — and the citation shapes | The evidence standard, #1314 | +| The field-combination matrix and the enum members themselves | `launchpad/docs/corpus/schema/node.schema.json` | +| Encoding ADR-0029's claim-type classification and the flagged state in the schema and checker | #1410 | +| Whether a node's classification is per-node or per-claim | Settled as per-entry by the schema; the wider question sits with #605 | +| Any numeric scale with real calibration behind it | Nothing. No such thing exists here, and this document does not invent one. | + +**No `relationships` in this node's front matter, and the reason is narrower than it +looks.** Every sibling standard is unmerged at the recorded revision, so no edge to one +would resolve — a `relationships[].target` naming an id no loaded node carries is a hard +validation error. But that is not the same as *nothing* being linkable: `AGENTS.md` +carries `id: corpus-agents`, is a loaded node, and a `references` edge to it would +validate today. The absence is a deliberate choice to add the whole edge set in one pass +once the siblings land, not a claim that the corpus offers nothing to point at. The +first sibling standard to merge is the moment to revisit it, and `corpus-agents` is the +edge to make first. + +**Expected but not verified when this node was written:** + +- **The bands in *Guidance*, and the values `0.8` / `0.6` / `0.4`, are not derived from + anything.** They are a proposed convention for keeping values coarse, offered because + unbounded precision is the observed failure. No study, no sample, and one authored + INFERENCE in the corpus to look at. `0.8` was chosen to match that entry and the other + two spaced evenly from it — which is a reason, but not evidence. Treat them as a + starting point that practice should correct, and do not read the spacing as measuring + anything. +- **Adopting the pegs does not make values comparable, and this document does not claim + it does.** The prohibition in *What a reader may conclude* is unconditional and stands: + the schema guarantees nothing about how any value was chosen, so no reader may compare + across authors or nodes. What a shared convention can do is give *future* authors who + followed it a common vocabulary — which is a hope about practice, not a property a + reader may rely on, and certainly not licence to sort. Until something records that an + author followed the pegs, a reader cannot tell whether they did. +- **No generated view was tested consuming a `confidence` value.** No generator exists + yet, so how a projection renders or ranks these numbers is unknown, and the + "not comparable" rule above has not been tested against a consumer that might assume + otherwise. +- **The NaN divergence was measured against the current schema and `memory.py` only.** + Whether any other consumer of the field shares the schema's permissiveness or + `memory.py`'s strictness was not established. diff --git a/launchpad/plans/2026-08-26-issue-1309-corpus-standard-confidence.md b/launchpad/plans/2026-08-26-issue-1309-corpus-standard-confidence.md new file mode 100644 index 00000000000..7971a732906 --- /dev/null +++ b/launchpad/plans/2026-08-26-issue-1309-corpus-standard-confidence.md @@ -0,0 +1,125 @@ +Issue #1309 — task: document corpus standard for confidence +Stated size: no `Size` line → cap: 5 steps (set by the feature #605 task brief, not asked per-issue) + +ALREADY TRUE (verified against git, not notes) + Worktree `__worktrees/task-1309-corpus-standard-confidence` is on branch + `task/1309-corpus-standard-confidence`, based on `origin/task/636-corpus-agents-md`, + HEAD `60d4947b7145a6ef25f185b9c25d43e43d99de3c`, working tree clean. + `launchpad/docs/corpus/AGENTS.md` exists on this base (the instruction node, #636, unmerged as PR #1462). + `launchpad/docs/corpus/standards/` does NOT exist — no sibling standard has landed. + The only authored corpus node today is `AGENTS.md`; everything else under + `launchpad/docs/corpus/` is `schema/`, which `validate.py` deliberately skips. + `node.schema.json` already encodes the confidence rule in three `allOf` branches: + INFERENCE requires `evidence` + `confidence`; FACT forbids `confidence` and `provided_by`; + TEAM_KNOWLEDGE requires `provided_by` and forbids `confidence`. + `launchpad/project-intelligence/memory.py` `__post_init__` enforces the same rule at runtime, + plus two checks the schema does not have (bool rejection, NaN rejection). + `validate.py` imports `jsonschema` and `yaml` only — it never imports `memory.py`. + The corpus path is enforced by the schema, not by `memory.py`. + Measured by running `Draft202012Validator` over synthesised nodes (recorded in STEP 1): + `0.0` and `1.0` both pass (bounds inclusive); integer `1` passes; `1.1` and `-0.1` fail; + `true` and `"0.8"` fail on type; **`.nan` PASSES the schema** but `memory.py` rejects it. + ADR-0029 is accepted and states inference is "never treated as fact on their own". + No open or closed issue on `launchpad-26/buzz` matches "confidence NaN schema". + +STEP 1 [independent] Record the measured constraint set as a reproducible probe script + under the session scratchpad, covering: the three `allOf` branches, both inclusive + bounds, integer acceptance, bool/string rejection, and the NaN divergence between + `node.schema.json` and `memory.py`. This is evidence-gathering, not a + deliverable — nothing under `launchpad/` changes in this step. + done when: the probe script runs and prints one PASS/FAIL line per case, and its + output shows `INFERENCE conf nan -> PASS` against the schema while + `memory.py` raises `confidence must be within [0.0, 1.0]` for the same value. + +STEP 2 [needs 1] Create `launchpad/docs/corpus/standards/confidence.md` with schema-valid + front matter only (`id: corpus-standard-confidence`, `type: governance`, + `status: active`, `origin: launchpad`, `audiences`, an `evidence` ledger whose + first entry is the `commit 60d4947b…` provenance citation, and **no** + `relationships` key). + done when: `python3 launchpad/project-intelligence/corpus/validate.py` exits 0 with + the new file present, and a YAML parse of the front matter (not a grep — a grep + confuses "prints 0" with "exits 1") reports `relationships` absent from the + top-level keys and `id == corpus-standard-confidence`. + +STEP 3 [needs 2] ← RUNS HERE Write the body: scope and authority, what the number + means, MUST vs SHOULD split, how an author picks one, what a reader may and may + not conclude, the reasoning-from-evidence versus dressing-up-a-decision + distinction, enforcement and its gaps, and the exception/escalation route to + `status: flagged` per ADR-0029. Link the schema and ADR-0029 rather than + restating enum lists or the field-combination matrix. + done when: `validate.py` exits 0; every `##` section named in this step is present + in the file; and the body links `launchpad/docs/corpus/schema/node.schema.json` + and `launchpad/decisions/ADR-0029-corpus-evidence-precedence.md` at least once each. + NOTE: "does not restate the field-combination matrix" is deliberately NOT a + done-when here. A one-line grep cannot detect a matrix restated as a table or a + bulleted list — review-plan proved that by constructing one that slipped through. + It is a reading judgement, so it belongs to STEP 4's audit, not to a regex. + +STEP 4 [needs 3] Audit the finished node against its own ledger: every `##`-level + substantive claim has an `evidence` entry, every `FACT` cites a source that was + actually opened, exactly one commit-only FACT exists, every INFERENCE carries a + confidence the document's own rules would justify, and the body nowhere restates + the schema's field-combination matrix or an enum member list (the judgement STEP 3 + deliberately did not delegate to a regex). + done when: `validate.py` exits 0; a YAML parse of the ledger — not a grep, so + indentation and quote style cannot skew the count — reports exactly one entry + whose every citation matches `^commit [0-9a-f]{40}$`; every INFERENCE entry has a + `confidence` key; and a written audit note maps each ledger entry to the body claim + it supports, naming any entry that supports none. + +STEP 5 [needs 4] Run the full corpus test suite and the validator as the last command of + their own invocations (verify-gate stamp), file the NaN-divergence finding as a + GitHub issue on `launchpad-26/buzz` linked to parent #605, then commit with `-s` + via an `-F` message file. + done when: `python3 -m unittest discover -s launchpad/project-intelligence/corpus/tests -p "test_*.py"` + reports OK; `validate.py` exits 0; `gh issue view --repo launchpad-26/buzz` + resolves and its body references #605; and `git log --format=%B -1` shows a + `Signed-off-by:` trailer. + +PARALLEL None. Steps 2, 3 and 4 all edit the same single file + (`launchpad/docs/corpus/standards/confidence.md`), so they are strictly + sequential regardless of how separable the content looks. STEP 1 touches no + repository file but produces the measurements STEP 2's ledger cites, so it + still precedes them. No step may be dispatched as a parallel subagent. + +GATES `review-plan` on this plan before STEP 1 (self-review, not independent). + `review-code` after STEP 5. `review-tests` does NOT apply — the diff adds no + test file and changes none; if that stops being true, it applies. + `review-adjudicate` over every finding both raise. A mandatory Codex + cross-model final pass after adjudication. + `qa` explore mode does **not** apply: the deliverable is a Markdown document + with no runtime interface to exercise. The only executable surface touched is + `validate.py`, which this change calls but does not modify. + +BUDGET STEP 3. The hard part is not length, it is saying what a confidence number + licenses a reader to conclude without either overclaiming calibration the + repository cannot support or hedging into uselessness. Expect the + reasoning-versus-decision distinction to need more than one pass. + +OPEN Whether `developer` belongs in `audiences`. #636 carries `agent` and + `reviewer`, and the brief calls the question deliberately unsettled. Planned + choice: `agent`, `reviewer` — an author picking a number is acting as agent, + a person checking it is acting as reviewer, and a developer reading corpus + prose is not addressed by this document in any way the other two do not + already cover. Stated so a reviewer can overturn it cheaply. + Whether the NaN divergence is a schema defect or an accepted limit of + structural validation. Planned handling: report it, do not fix it here — + `schema/` is out of this issue's scope and its DoD forbids a second authored + node. It becomes a filed issue, not a silent edit. + +LEFT OUT Any `relationships` edge. Every sibling standard (#1307–#1351) is unmerged, and + a `relationships[].target` naming an id no loaded node carries is a hard + validation error, so no edge to a sibling would resolve. CORRECTION, from + review-final: that does not mean nothing is linkable — `AGENTS.md` carries + `id: corpus-agents`, is a loaded node, and a `references` edge to it would + validate today. Declaring none is a deliberate choice to add the whole edge set + in one pass once the siblings land, and the body must say that rather than + claiming nothing is legal to point at. + Editing `launchpad/docs/corpus/AGENTS.md`, even where this work suggests it + could be clearer — the brief forbids it and the DoD scopes this task to one + authored document. + Restating the enum members or the field-combination matrix in body prose. + `validate.py` never reads body prose, so a second copy would stay green + forever after it went stale. + Fixing `#1459` (line numbers unverified) or the NaN gap. Both are pre-existing + and owned elsewhere. diff --git a/launchpad/plans/2026-08-26-issue-636-corpus-agents-md.md b/launchpad/plans/2026-08-26-issue-636-corpus-agents-md.md new file mode 100644 index 00000000000..65d3570b82e --- /dev/null +++ b/launchpad/plans/2026-08-26-issue-636-corpus-agents-md.md @@ -0,0 +1,259 @@ +Issue #636 — task: document AGENTS.md +Stated size: none given (no `Size` line/label) → asked Serina; she chose 30–60 minutes → cap: 8 steps + +Revised 2026-08-26 after an independent cross-model review-plan pass (Codex, gpt-5.6-sol, +high effort) against the first draft. Eleven findings; all applied. That pass also refuted +one finding from the author's own self-review — see OPEN item 4 for what was withdrawn and +why, because the withdrawn reasoning is the kind that comes back. + +ALREADY TRUE (verified against git at 0052f5a7820ca4ca261efa233feb8bb53858ade6) + #622 is merged: launchpad/docs/corpus/schema/node.schema.json is the frontmatter + contract this document must describe rather than restate. Required fields are + id, type, status, origin, audiences, evidence; relationships is optional; and + `additionalProperties` is false, so the frontmatter has no field outside that set. + The schema's own description of `evidence` calls it "The node's provenance ledger." + There is no separate `provenance` property and adding one is a schema violation — + verified: injecting `provenance:` yields "Additional properties are not allowed + ('provenance' was unexpected)". The ledger IS the provenance mechanism. + #623 is merged (PR #1422): launchpad/project-intelligence/corpus/validate.py exists + and `just corpus-validate` runs it (Justfile:1004-1005). DEFAULT_ROOT is + "launchpad/docs/corpus". `just corpus-validate` passes NO `--root`, so it always + validates the real corpus and can never be pointed at a scratch tree. + The scan excludes exactly one top-level directory by name, `schema/`. A new + launchpad/docs/corpus/AGENTS.md WILL therefore be scanned and must carry + schema-valid frontmatter. Not optional for this file. + launchpad/docs/corpus/ today contains only `schema/` — ZERO real corpus content nodes. + AGENTS.md is the first, so it can cite no sibling node as an example and can declare + no `relationships` target: find_unresolved_relationship_targets rejects any target + no loaded node's id matches, and today no such id exists. + The validator does not read the Markdown body at all — `_, frontmatter, _body = + text.split("---\n", 2)`. Every claim about body content, links, scope or one-concept + discipline is enforced by a human or not at all. + Citation checking is STRUCTURAL, not evidential. A bare path is checked to resolve to a + real file inside the repo (is_file(), so a directory is rejected). A `path:line` + citation is NOT checked against the file's length — `Justfile:999999` returns `ok` + against a 1005-line file; filed as #1459, and this task avoids the positional forms + because of it. A GitHub blob/raw URL must be pinned to a full 40-char SHA, but a + pinned link to a real file proves only that the URL is well-formed. A non-GitHub URL + and a `commit ` reference both land on the non-fatal UNVERIFIED channel. + Nothing the validator reports means a citation SUPPORTS the statement it sits under. + find_ownership_violations fails closed on every non-.md file under the corpus root, + including files under `generated/`, until #1316 defines the provenance contract. It + says nothing about how MANY .md nodes exist — five valid nodes validate together — + so the DoD's "exactly one hand-authored document" is not a property any tool checks. + .github/workflows/launchpad-corpus-validate.yml triggers on `launchpad/docs/corpus/**` + for pull_request and push-to-launchpad. This change is CI-gated with no workflow edit. + This file will not only be a corpus node. `launchpad/scripts/preflight_core.py` sets + `RULES_FILENAMES = ("AGENTS.md", "CLAUDE.md")` (line 164) and resolves the NEAREST one + per changed path into `nearest_rules`, which is a FATAL_FIELD. Creating + launchpad/docs/corpus/AGENTS.md therefore makes it the governing rules file surfaced + for every future change under launchpad/docs/corpus/. That is what #605's acceptance + criterion wants, but it means the document is read as instructions as well as + validated as a node — so it must actually instruct, not merely describe. + There is no mkdocs.yml anywhere in the repository — checked, so that despite + launchpad/AGENTS.md §3 describing launchpad/docs/ as "MkDocs knowledge layer" there is + no navigation to register and no docs build this file could break. The only workflows + touching launchpad/docs/ are the two corpus ones above. + `just` is NOT on PATH without `. ./bin/activate-hermit`, which must run in the SAME + command as the recipe. Every done-when below therefore calls the interpreter directly. + The 45 standards/template issues (#1307–#1351) are all OPEN, as are #639, #1316, #1410. + Baseline before this plan existed: no branch, worktree, PR or plan for #636 or #639. + That is a statement about the pre-plan baseline, not about the worktree now — this + plan file is itself untracked in it. + +STEP 1 Record the evidence base the issue demands before drafting. [independent] + The issue requires it BEFORE drafting: repository ref, inspected source paths and + symbols, tests, specifications, migrations and configuration, relevant Git + history/PRs/issues, and anything expected but NOT verified. Write it to the + session scratchpad as a structured checklist with one section per required + category — not a flat path list, which is satisfiable by writing nothing. + Every category gets either at least one inspected item or an explicit + "not verified, because …" line. Path candidates are checked with `test -f`, not + `test -e`: the validator requires is_file(), so a directory passes a `-e` gate + and fails two steps later as a citation error. + done when: the note names commit 0052f5a7820ca4ca261efa233feb8bb53858ade6; + every required category has an entry or an explicit not-verified reason; and + every filesystem path it lists satisfies `test -f` from the worktree root. + +STEP 2 Create AGENTS.md with schema-valid frontmatter and a skeleton. [needs 1] + id: `corpus-agents` — mirrors the file's own name so a reader maps node id to + path without a lookup table. Chosen HERE rather than left to the builder because + ids are never renamed: an id picked ad hoc mid-build is permanent. #1317 (the + identifier standard) is still open, so there is no scheme to conform to yet; if + it later prescribes a different one, that is a migration, not an edit. + type: `agent` — decided by Serina 2026-08-26, and precedent for the remaining 45 + nodes. Reasoning: the enum's values are PRD #602's in-scope surfaces, this + document's audience and subject are both agents, and it becomes the resolved + AGENTS.md for the subtree (see ALREADY TRUE), which is an agent-facing surface + in the most literal sense. `governance` was the rejected alternative — it would + eventually absorb every process document and stop discriminating. + status: active. origin: launchpad. + audiences: [agent] at minimum. evidence: one entry per substantive claim. No + `relationships` block — no other node exists to target, and inventing one is a + hard validator error. + The repository revision goes HERE, in the evidence ledger, because the ledger is + the schema's provenance mechanism and no other schema-legal slot exists. It lands + on the UNVERIFIED channel (a commit reference names no openable file), which is + correct and non-fatal — but it means nothing enforces it, so STEP 8 checks it. + done when: `python3` validates the file's frontmatter against + launchpad/docs/corpus/schema/node.schema.json with zero errors — the schema + itself, not a YAML parse, which would accept frontmatter with no `evidence` key + at all; AND the ledger contains an entry recording + 0052f5a7820ca4ca261efa233feb8bb53858ade6. + +STEP 3 Prove the validator actually scans the new node. [needs 2] ← RUNS HERE + A pass that skipped the file looks identical to a pass that checked it — the + failure this step rules out, and the class of defect (`schema/` exclusion, + symlink escape) that cost #623 two review rounds. + done when: `python3 launchpad/project-intelligence/corpus/validate.py` exits 0 + with the file present; AND after temporarily setting `type:` to a value outside + the schema enum the same command exits non-zero AND its output names this node's + id; AND the file is restored and the command exits 0 again. Asserting on the + named id, not merely on a non-zero status: a non-zero status is produced by any + failure, including the command not existing. + +STEP 4 Write the node contract section — what a corpus node IS. [needs 3] + Where the file goes, the one-node-per-document rule, id stability, the closed + enums, and the canonical-vs-generated boundary (non-.md files fail closed today; + #1316 owns the contract that will change that). LINK to + launchpad/docs/corpus/schema/README.md and node.schema.json rather than restating + field lists — a second copy of an enum is a second thing to drift, and the + validator never reads the body, so a stale copy stays green forever. + done when: the section names the schema files by path, reproduces no enum member + list and no field-combination matrix, and the validator still exits 0. + +STEP 5 Write the evidence section — the ledger, and what validation proves. [needs 4] + NOT a restatement of the three classes and their field rules: those are canonical + in node.schema.json and schema/README.md, and copying them here contradicts both + STEP 4's anti-drift rule and the DoD's "without duplicating canonical content". + Link them. What this section adds is the part living nowhere else: which citation + forms the validator can check, that checking is STRUCTURAL only, that a `PASS` + never means a citation supports its statement, that a GitHub file link needs a + full 40-char SHA or CI fails, and that the positional `path:line` form does not + verify the line exists (#1459) so a bare path is the safer citation today. + done when: the section reproduces no field matrix or enum list; states the + structural-vs-evidential distinction explicitly; states the full-SHA pin rule; + and cites launchpad/project-intelligence/CONTRACT.md and validate.py by path. + +STEP 6 Write the create / update / retire procedures. [needs 5] + Three numbered procedures, each ending in the validator command. Retire is the + one most likely to be got wrong: `status: retired`, id NEVER reused or renamed, + inbound relationships from other nodes considered before retiring. #605's + acceptance criterion for this file is exactly "create/update/retire one node + without oral guidance" — this step is that criterion, and STEP 8's QA charter is + what tests it. + done when: all three procedures are present; each is a numbered sequence a reader + could follow without asking a question; and each terminates in a command whose + exit status decides success. + +STEP 7 Write the scope-and-omissions section and the outbound links. [needs 6] + State what this document does NOT cover and who owns it: the 45 standards and + templates (#1307–#1351), generated-content provenance (#1316), claim-type and + flagged state (#1410), the human-facing entry point (#639). Link ADR-0028 and + ADR-0029 by repo path. Say plainly that no sibling corpus node exists yet, so the + frontmatter carries no `relationships` — an absence with a reason, not an + oversight a reviewer has to guess at. + done when: the section names each deferred area with its owning issue number, and + every repo path it cites satisfies `test -f`. Body links are checked by hand + here because the validator never reads the body. + +STEP 8 Audit the ledger, the scope, and the changed-file set. [needs 7] + Four checks, none of which any tool performs: + (a) Every substantive body claim maps to a ledger entry of the right class, and + for each FACT the cited source is OPENED and read to confirm it actually + supports the statement. A citation that only passes structurally, or that + lands on the UNVERIFIED channel, does not support a FACT — either open the + source and keep the class, or reclassify to INFERENCE with a confidence, or + to TEAM_KNOWLEDGE with provided_by. + (b) The revision recorded in STEP 2's ledger matches the revision actually + inspected in STEP 1. + (c) Scope: the document still represents ONE independently maintainable concept. + Any second concept found while drafting is removed and filed as its own task, + per the DoD — it is not folded in because it happened to be well-sourced. + (d) Changed-file inventory against the merge base: `launchpad/docs/corpus/` + contains exactly one added hand-authored .md file. Five valid nodes validate + together, so "exactly one document" is a property only this check enforces. + done when: all four pass; the validator exits 0; and the build report lists every + UNVERIFIED notice with the classification decision taken for it, or states there + were none. + +PARALLEL Nothing here may fan out. STEPs 2 and 4–8 all edit the single file + launchpad/docs/corpus/AGENTS.md, which makes them strictly sequential + regardless of how independent their subject matter looks. STEP 1 is the only + step touching no repository file, and everything else needs its output. + #639 (README.md) is genuinely independent and could run as a parallel worktree + — but it is a different issue and a different PR, not a step of this plan. + +GATES review-plan: DONE — an independent Codex pass reviewed the first draft and its + eleven findings are applied above. review-code after STEP 8: applies despite + this being docs-only, because the frontmatter is machine-validated input to a + checked-in validator, not prose. review-tests does NOT apply — no tests added, + and #623's suite already covers the validator. review-a11y does NOT apply — no + UI. review-adjudicate then review-final before merge, and review-final MUST be + cross-model: three same-model passes on #120 missed what one Codex pass caught + immediately, and this plan is a fresh data point for the same rule. + qa explore mode APPLIES. Charter: in a scratch corpus root OUTSIDE the repo, + follow STEP 6's procedures literally, as written, supplying nothing the document + did not state. Exercise ALL THREE — create, update and retire — because #605's + criterion names all three and a create-only charter cannot detect an update + procedure that forgets provenance. Use at least two scratch nodes so retirement + has an inbound relationship to handle. Validate with + `python3 launchpad/project-intelligence/corpus/validate.py --root "$SCRATCH"`, + never `just corpus-validate`, which takes no `--root` and would validate the + real corpus while appearing to test the scratch one. Include a negative control: + a deliberately invalid scratch node the run must reject, proving that root was + the one scanned. Nothing from the scratch root is committed. + +BUDGET STEP 8(a). Opening every cited source and confirming it supports its statement + is the only step whose cost scales with the ledger's length, and it is the one + with no tool behind it — the validator reports structure and stops. It is also + the step most likely to be quietly downgraded to "the validator passed", which + is precisely the substitution it exists to prevent. + +OPEN 1. RESOLVED 2026-08-26 by Serina: `type: agent`, and `id: corpus-agents`. Both + are now stated in STEP 2 with their reasoning. They were listed here in an + earlier revision, which was a defect: STEP 2 cannot write frontmatter without + them, so a plan that both required them and forbade the builder deciding them + blocked its own second step. An OPEN item that a step must consume is not an + open question — it is a missing input. + 2. Forward references. The standards this document would defer to (#1307–#1351) + do not exist. Naming them as "owned by #NNNN, not yet written" is honest but + ages into a stale list; omitting them leaves a cold-start agent unable to + tell a gap from an omission. STEP 7 assumes naming them is right. + 3. Whether YAML frontmatter at the top of a file that IS a rules document + degrades it as instructions. No longer hypothetical: preflight_core.py will + resolve this file as the nearest AGENTS.md for the subtree (see ALREADY + TRUE), so it is handed to agents as governing rules, frontmatter first. + Harmless to the validator and to preflight, which resolves the path without + parsing content. Unverified against any agent harness actually reading it. + 4. WITHDRAWN, recorded because the reasoning will recur. The first draft claimed + STEP 3's mutation clause could be satisfied by `just: command not found` + (exit 127). The Codex pass refuted it: STEP 3's first and third clauses + require exit 0, which 127 fails, so a missing `just` is fail-CLOSED. The + underlying operational fact is real and kept — `just` needs Hermit activated + in the same command — which is why every done-when now calls `python3` + directly. The severity claim was wrong; the PATH observation was not. + +LEFT OUT launchpad/docs/corpus/README.md — that is #639, a separate PR; #636's own + out-of-scope list forbids materially editing a second hand-authored node here. + The generated-content provenance contract (#1316) — the validator deliberately + refuses to guess at it and so does this document. ADR-0029's claim-type + classification and flagged state (#1410) — that issue's work; `status: flagged` + is simply not used. Fixing #1459 (positional citations not checked against file + length) — a defect in merged code, filed separately; this task routes around it + by preferring bare-path citations rather than changing the validator. Any change + to the schema or the validator — both are merged and this task consumes them. + + AMENDED during build, 2026-08-26, authorised by Serina. ONE exception to the + line above: `test_real_corpus_root_currently_has_no_content_outside_schema` in + launchpad/project-intelligence/corpus/tests/test_validate.py asserted the corpus + root is EMPTY. #636 authors the first node, so that test cannot survive this + issue — it fails in CI via launchpad-corpus-validate.yml, not merely locally. + It is replaced (not deleted) by an assertion of what its author's own comment + says it was for: the root holds authored content AND none of it comes from + schema/. Both halves are needed; the second alone is satisfied by an exclusion + that rejects everything. Neither the review-plan self-pass nor the independent + Codex pass caught this — the plan asserted "#623's suite already covers the + validator" without running it against a corpus containing a node. + CONSEQUENCE: the diff now touches tests, so review-tests JOINS the roster that + the GATES line above says does not apply. Treat the GATES line as amended. diff --git a/launchpad/project-intelligence/corpus/tests/test_validate.py b/launchpad/project-intelligence/corpus/tests/test_validate.py index 2d08fcc504d..ffdff6ffc01 100644 --- a/launchpad/project-intelligence/corpus/tests/test_validate.py +++ b/launchpad/project-intelligence/corpus/tests/test_validate.py @@ -1,11 +1,28 @@ """Unit tests for the deterministic corpus validator -- issue #623. -Run: python3 -m unittest launchpad.project_intelligence.corpus.tests.test_validate - or: python3 -m unittest discover -s launchpad/project-intelligence/corpus/tests -p "test_*.py" - -These tests only ever point --root at fixtures under this directory, never at the -real launchpad/docs/corpus/ -- that root's own content (or lack of it) must never -change what this suite asserts. +Run: python3 -m unittest discover -s launchpad/project-intelligence/corpus/tests -p "test_*.py" + +(The dotted-module form this docstring used to advertise never worked: the directory is +`project-intelligence`, with a hyphen, which is not a legal Python package name, so it +raised ModuleNotFoundError. Broken since #623; found by a cross-model review pass.) + +Almost every test points --root at fixtures under this directory, so the real +launchpad/docs/corpus/ cannot change what they assert. That is the rule, and new +tests should follow it. + +ONE test is deliberately outside it: +`SchemaDirExclusionTest.test_real_corpus_root_discovery_matches_an_independent_walk` +walks the real corpus root and requires it to be non-empty. It exists to catch a +discovery regression against real committed content, which no fixture can stand in +for, and it will legitimately fail if the corpus is ever emptied or relocated -- at +which point it is reporting that, not a discovery bug. Its own docstring says what it +can and cannot catch. + +An earlier revision of this docstring claimed the never-touch-the-real-root rule was +absolute. It was not true even then: the test this one replaced also read the real +root, asserting it was EMPTY. Two independent reviews (an adjudicator and a +cross-model final pass) flagged the contradiction separately, which is why the rule is +now stated with its exception rather than as an absolute nobody was keeping. """ from __future__ import annotations @@ -982,11 +999,13 @@ class SchemaDirExclusionTest(unittest.TestCase): """schema/ is #622's own infrastructure, never scanned as corpus content. Proven against a purpose-built fixture tree containing BOTH a schema/ file - and a real sibling, not merely against today's real launchpad/docs/corpus/ - root -- that root currently has zero non-schema content, so a test asserting - only "no file under schema/ leaked" against it would pass vacuously even if - exclusion were broadened to reject everything. An independent review-tests - pass found this. + and a real sibling, rather than against the real launchpad/docs/corpus/ root + alone. When this suite was written that root held zero non-schema content, so + a test asserting only "no file under schema/ leaked" against it would have + passed vacuously even if exclusion were broadened to reject everything. An + independent review-tests pass found that. The fixture test below stays the + primary proof for the same reason: it controls both sides of the comparison, + where the real root only ever shows whatever happens to be committed. """ def test_sibling_discovered_schema_dir_excluded(self) -> None: @@ -1003,13 +1022,64 @@ def test_sibling_discovered_schema_dir_excluded(self) -> None: self.assertIn(real_sibling, files) self.assertNotIn(inside_schema, files) - def test_real_corpus_root_currently_has_no_content_outside_schema(self) -> None: - # A documentation-style sanity check on today's real state, not the - # primary proof of exclusion (see test_sibling_discovered_schema_dir_ - # excluded above, which is the one that can actually fail on regression). + def test_real_corpus_root_discovery_matches_an_independent_walk(self) -> None: + """Discovery over the real root returns EXACTLY the nodes that are there. + + This replaces `test_real_corpus_root_currently_has_no_content_outside_ + schema`, which asserted the root was EMPTY. That assertion was true when + #623 wrote it and had a shelf life ending at the first authored node -- + #636's launchpad/docs/corpus/AGENTS.md, which is what broke it. A test + encoding a temporary state as a permanent assertion fails on the change + it was supposed to permit, and says nothing about the behaviour under + test when it does. + + The first replacement asserted only "non-empty AND nothing from schema/", + which an independent review-tests pass defeated immediately: replacing + discover_markdown_files with a hardcoded `return [root / "AGENTS.md"]` + -- a constant that never touches the filesystem -- satisfied both halves + while proving neither discovery nor exclusion. Asserting that a check CAN + fail is not the same as asserting it can only pass for the right reason. + + So the expectation is now derived from the filesystem independently of + the function under test, and compared for equality. Measured against + mutants of discover_markdown_files, this test catches: + + returns nothing -> FAIL (caught) + exclusion disabled -> FAIL (caught) + hardcoded constant -> PASS (NOT caught today) + + The constant is not caught, and cannot be by any assertion made here + while the real corpus holds exactly ONE node: `[root / "AGENTS.md"]` is + the correct answer today, so a constant and a real walk are + indistinguishable from outside. That is a property of the real tree, not + of this assertion -- and it resolves itself the moment a second node + lands, at which point the equality form catches constants and partial + walks for free. `test_sibling_discovered_schema_dir_excluded` catches the + constant NOW, because its fixture tree holds names no constant predicts. + That test, not this one, is the proof of discovery behaviour. + + `assertNotEqual(expected, [])` is the guard against inheriting the + original sin: with an empty corpus both sides would be [] and equality + would pass vacuously. If that ever fires, the corpus is empty and this + test should be read as reporting that, not as a discovery bug. + + The walk mirrors the canonical-location rule as well as the schema/ + exclusion, because discover_markdown_files drops symlinks resolving + outside the root. A node symlinked in from elsewhere therefore fails + here loudly rather than silently widening what counts as corpus content. + """ root = validate.repo_root() / validate.DEFAULT_ROOT - files = validate.discover_markdown_files(root) - self.assertEqual(files, []) + resolved_root = root.resolve() + + expected = sorted( + path + for path in root.rglob("*.md") + if path.relative_to(root).parts[0] != "schema" + and path.resolve().is_relative_to(resolved_root) + ) + + self.assertNotEqual(expected, []) + self.assertEqual(validate.discover_markdown_files(root), expected) if __name__ == "__main__":