@antoneeo/kb-agentic-skill 1.31.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  Every significant change to this skill is recorded here.
4
4
 
5
+ ## [1.32.0] - 2026-10-27
6
+
7
+ ### Added
8
+ - `inventory <path>`: every project file under a path (`.` for all), each with its state against the corpus, compared by content — `INGESTED` (its bytes are a sidecar's digest, wherever it lives, or its `original_path:` with no digest kept), `INGESTED_EARLIER` (the original changed since ingest), `EXCLUDED` (a row of the approved registry `audit/INVENTORY_exclusions.md`; never read), `NOT_EXAMINED` (the default; links, junctions and cloud-only files are never read), and once per built-in skipped directory `SKIPPED`. No totals, no filter by state; it writes nothing (`vision/rulings.md` r37).
9
+ - Orient prints, in a project with a corpus, an upper bound of the project files never examined (`Corpus reach: <= N …`); Topic Recall runs `inventory` on the plausible path before answering `kb: no coverage` and says what it found there. `stale` names an ingested artifact whose local original changed since ingest, until a successor supersedes it.
10
+ - Documents the project sends out: the version sent enters `corpus/given/` with `provenance: DECLARED`, `recipient:` and the send date; its claims are `DECLARED` ("declared to X on D"), never evidence about the product. A declaration that contradicts the ledger is held as `DIVERGES <id>` on the declaration only; the ledger row is untouched. `export` leaves declarations behind and says so; `import` refuses them (r38).
11
+ - `check` (every lens) now refuses a non-DECLARED row citing a sent document in any source position, a DECLARED row without `recipient:`, `SUPERSEDED`/`CONTESTED` across a declaration, a malformed exclusion-registry row, and a `DIVERGES` on a non-DECLARED row or at a missing, DECLARED or superseded target. A GIVEN row on a `provenance: DECLARED` sidecar used to warn; it is now an error. The docs site shows the `declared` origin, the `diverges` state, ground resting on a sent document, and "document sent" in the chronology.
12
+ - `constraints --unit <id>` and `constraints --file <path>` (shared core): the constraints an ANALYSIS states in extractable form, each with its line, ending with the sections they could not extract.
13
+ - `benefit` and the docs site recognize a REVIEW_LOG row with tier `post-merge` (written by the code lens: the owner's report of a wrong behaviour in a merged change): `benefit` keeps it out of every review figure, the docs site shows it as "defect reported after merge". `review.md`: a reviewer derives callers and behaviours before comparing with the constraint view.
14
+ - `guides.md`: a code guide may carry a `## Preservation constraints` section copied from a closed unit; it survives regeneration. Nothing else changes in this lens: `validate` adds no note.
15
+
16
+ ### Fixed
17
+ - `import` now does what `portability.md` §4 promised: a ruling from another project arrives as `prov: IMPORTED` and settles nothing until you ratify it. It used to arrive unchanged as `RULING`, so another project's decision was in force here with `check` clean. Its note arrives as a renamed copy, `corpus/notes/imported_<project>_<name>`, carrying `imported_from:` and `origin: imported`; the note is never edited under its own name, because an inserted line would move the line every locator cites. The rows the ruling superseded and the ruling itself arrive as one `CONTESTED` set. Ratifying is an ordinary ruling: your own note with your own `basis:`, a `RULING` row on it, and the contested rows `SUPERSEDED` by it. `--dry-run` reports the conversions too.
18
+ - `import` refuses, writing nothing and naming the row, a bundle whose conflict sets would arrive incomplete (a pointer resolving to no row, or to a row of this project that does not name it back), a row settled by a ruling the import does not write or by a row held as `IMPORTED`, and a ruling it cannot carry foreign (not on a note cited by line; on a note with no frontmatter or one already naming a foreign origin; superseded both by a ruling and by another row). `portability.md` §3 already listed an incomplete conflict set as a refusal; it is now enforced. §3 no longer lists a dangling `supersedes:` as a refusal: the import never checked it, and `check` reports it after the import.
19
+ - Any bundle note that declares a `basis:` lands as the renamed foreign copy, even when no ruling row travels with it, so a later import of its ruling cannot find it under its own name. A note in UTF-8 with a BOM (Windows editors) converts and keeps its BOM.
20
+ - `check` refuses a `RULING` row on a note that names a foreign origin (`imported_from:`): relabelling an imported row is not ratifying it.
21
+ - A project imported with kb 1.31.0 or earlier holds a foreign ruling as `RULING`: delete the imported topic and its files and import again, or convert it by hand (`portability.md` §4).
22
+ - Guide-verdict check (`verdict --hook`, shared core): a level quoted inside a sentence (`Level: L1` in prose) no longer counts as a declaration: only a line that opens with the level word does, the rule the constraint recall already uses (level 1 alone, `L1` or `E1`; levels 2 and 3 with `router` on the same line). The check now judges the project, and its docs-root name, that holds the edited file, not the one around the session's working directory; after a `cd` out of the project it let every edit through. A router verdict follows the same rule: only a line that opens with `router` or declares the level with it counts, never `router: no match` quoted in a sentence; `Level: Spike · router: …` and markdown openers (`##`, `1.`, `•`) count. A quote that itself opens a line (blockquote, code fence) still counts. The protected-path gate (`gate --hook`, `gate --file`) also judges the edited file's project, falls back to the session's project when no docs root sits above the file, and stays silent instead of exiting with an error when two docs roots sit side by side. Its `--protected` prefixes are relative to the nearest docs root above the edited file: with nested docs roots (a monorepo package with its own `ai_docs/`, or a docs folder inside a protected path) rewrite them relative to that root, or the path is no longer protected.
23
+
5
24
  ## [1.31.0] - 2026-10-23
6
25
 
7
26
  ### Changed
package/README.md CHANGED
@@ -47,14 +47,14 @@ Deliberately absent: any per-node coverage or completion state. `gaps:` says wha
47
47
  ## Key features
48
48
 
49
49
  - **Risk-proportional triage, measured in knowledge and never in files**: one claim row → propagating a fact already settled → a new knowledge unit (a source ingested, a node created or superseded, the hierarchy moved). Carrying one settled fact into eight documents is small; one claim that re-parents a node is not. On a live front, a new source or statement within an open unit's declared scope is an **ingestion step** of that unit — recorded on the ledger at once — while the rewrite of dependent documents, the registry and the full check run once, at a closure the practitioner calls. A **Write Triggers** table maps each knowledge event to exactly one destination.
50
- - **Portable knowledge**: `export` bundles a subgraph together with the bytes its claims cite — a closure, not a selection, because a claim whose source cannot be reopened is model knowledge arriving by another route. `import` merges it into another project **additively**: it never overwrites a node and never deletes, and claims already present are recognised by id rather than by comparing text, since the same artifact cited at the same span mints the same id in every project. Knowledge crosses the project boundary; **authority does not** — an imported ruling arrives as `prov: IMPORTED`, keeps its original `basis:`, and cannot settle a local disagreement until you re-ratify it.
50
+ - **Portable knowledge**: `export` bundles a subgraph together with the bytes its claims cite — a closure, not a selection, because a claim whose source cannot be reopened is model knowledge arriving by another route. `import` merges it into another project **additively**: it never overwrites a node and never deletes, and claims already present are recognised by id rather than by comparing text, since the same artifact cited at the same span mints the same id in every project. Knowledge crosses the project boundary; **authority does not** — an imported ruling arrives as `prov: IMPORTED` on a renamed copy of its note that names its origin, keeps its original `basis:`, and the question it had closed is open again (`CONTESTED`) until you ratify it with a ruling of your own.
51
51
  - **Several people, one project**: the workstream registry (`audit/handoff.md`) is **generated** from one file per open workstream, so two people opening or closing two workstreams on two branches edit two different files and their merge is clean. Row-per-workstream alone was not enough — a file-global `Date:` header defeats row-level ownership — so the header is derived from the sources and no writer touches it. The generated view can still conflict; that conflict is resolved by re-running `index`, never by hand, and `validate` refuses CLEAN until the file matches its sources. The append-only review log gets `merge=union` (a built-in driver, no per-clone configuration). It all works with no VCS at all: it is files and a generator.
52
52
  - **Vision-guided governance**: Standalone projects use `ai_docs/vision/`; Hybrid projects use devPNT `M-VISION` as the milestone north star. `DRAFT` informs, `APPROVED` binds, promotion is the user's alone. Every Vision, the M-VISION included, is written only after a six-question **owner interview** and your explicit approval of a one- or two-sentence north star in your own words — the agent never writes one alone.
53
53
  - **Independent review, twice**: the design before it is implemented, the result before it is declared done — fresh-context subagent > one-shot run > a declared self-pass, 3 rounds max, one log line each, and a PASS is invalid on "found nothing".
54
54
  - **Question discipline**: a real doubt is asked when it emerges, before its answer is written, with the pros and cons of each option; a choice whose pros clearly win is taken with the weighing written next to it. A question is legal only when the agent searched first, names the search with its result, and names the decision it unblocks.
55
55
  - **Verbatim request record**: every L3 starts `audit/requests/REQUEST_[topic].md` before the Vision Gate — the practitioner's own words and every later decision (answers, approvals, costs, rulings), append-only and redacted for secrets — so analyses cite what was asked instead of paraphrasing it, and reviews check owner statements against it.
56
56
  - **Operative guides + agent-global KB**: distil user-provided indications into source-faithful `GUIDE_*.md` (`source_kind: document`) — verbatim snapshot plus hash, so drift is detected mechanically.
57
- - **Mechanical checks**: `check`, `validate`, `index`, `graph`, `corpus`, `claim-id`, `anchor`, `export`, `import`, `adopt` (dry run: document guides on snapshots and what moving them into the corpus involves), `stale` (spine staleness plus `## claims` -- superseded-cited documents, expired `valid` windows, superseded derivation chains), plus the spine's `mark`/`benefit`/`gate`/`plan`/`orient`/`migrate`. `benefit` reports what the review gates have already caught, beside what the doctrine costs to read; it always exits 0 and carries no verdict. The graph and corpus checks verify spans against the stored extraction, recompute every id, refuse cycles and unreachable nodes, and enforce `CONTESTED` symmetry.
57
+ - **Mechanical checks**: `check`, `validate`, `index`, `graph`, `corpus`, `claim-id`, `anchor`, `export`, `import`, `adopt` (dry run: document guides on snapshots and what moving them into the corpus involves), `inventory <path>` (each project file under a path against the corpus: `INGESTED`, `INGESTED_EARLIER`, `EXCLUDED`, `NOT_EXAMINED`, `SKIPPED`; orient prints an upper bound of the files never examined), `stale` (spine staleness plus `## claims` -- superseded-cited documents, expired `valid` windows, superseded derivation chains), plus the spine's `mark`/`benefit`/`gate`/`plan`/`orient`/`migrate`. `benefit` reports what the review gates have already caught, beside what the doctrine costs to read; it always exits 0 and carries no verdict. The graph and corpus checks verify spans against the stored extraction, recompute every id, refuse cycles and unreachable nodes, and enforce `CONTESTED` symmetry.
58
58
  - **Installed support files**: Claude, Codex, Gemini and Google Antigravity receive the full skill folder — `SKILL.md`, `templates.md`, `taxonomy.md`, `distillation.md`, `reconciliation.md`, `guides.md`, `vision.md`, `elicitation.md`, `review.md`, `dispatch.md`, `routing.md`, `ENFORCEMENT.md`, `memory.md`, and the validator's three files, `scripts/sdlc_check.py` + `scripts/sdlc_core.py` + `scripts/knowledge.py` (copy all three) — plus the docs site generator `scripts/docs_site.py` and its page `docs_site/`, and the `LICENSE` and `NOTICE` it ships under.
59
59
  - **Docs site** (`scripts/docs_site.py --root <project> [--open] [--zip <file>]`): a static, offline, read-only website of what the agent reads in the project — reading guide, Vision, rules, documents, open work with each next step, analyses and, with a knowledge base, the topic map, each fact with its exact source passage, and the source documents. Written to `.docs-site/` (added to `.gitignore` on the first run), outside `ai_docs/`; nothing is ranked or recommended.
60
60
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kb-agentic-skill",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "description": "Knowledge-Base & Document-First protocol with risk triage, Vision governance, signal distillation and optional devPNT integration.",
5
5
  "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antoneeo/kb-agentic-skill",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "description": "Knowledge-Base & Document-First protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, signal distillation and optional devPNT integration.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -99,15 +99,15 @@ Semantics: exit code 2 + message on stderr ⇒ the write is blocked and the mess
99
99
 
100
100
  **Usage warnings:**
101
101
  - The gate is deliberately coarse: applied to all of `src/` it would also block the legitimate L1/L2 tasks foreseen by the Triage. Use it **only on security-critical directories** (`--protected "src/auth;src/crypto"`), where "never without analysis" is the desired policy.
102
- - The paths in `--protected` are prefixes relative to the project root, separated by `;`.
102
+ - The paths in `--protected` are prefixes relative to the root of the project the hook judges (below), separated by `;`. With nested docs roots (a monorepo whose package has its own), that is the nearest one above the edited file: write the prefixes relative to it.
103
103
  - `ai_docs/`, `tests/` and `test/` are always excluded from blocking.
104
- - The hook assumes the working directory is the project root (standard behavior of Claude Code hooks).
104
+ - The hook judges the project, and the docs root, holding the edited file: the session's working directory can move with a `cd`, so it is never the reference. With no docs root above the file, it is the session's own project, as before. `--root <path>` overrides it; `gate --file` resolves the same way, and two docs roots side by side leave the gate silent.
105
105
  - **Hybrid/devPNT projects**: add `--hybrid` to the gate command. Governed designs live in the devPNT DB, so the gate also unlocks when an approved E-TDD shadow (`ai_docs/solutions/SHADOW_*tdd*.md`, exported before implementation — in this package that clause is the whole statement of the duty; the sibling lenses keep it in their `hybrid.md`, which kb does not have) is present. Without the flag the gate would block legitimate governed work. The flag is deliberately explicit: never auto-detected.
106
106
 
107
107
  ### Verdict check: `verdict` (PreToolUse -- manual for this lens)
108
108
 
109
109
  `sdlc_check.py verdict --hook` refuses an Edit, Write, MultiEdit or NotebookEdit when the
110
- session transcript shows no guide-router verdict (`router: …`, or a level-1 declaration)
110
+ session transcript shows no guide-router verdict (a line that opens with `router: …` or with the level, `Level: L2 · router: …`, `Level: Spike · router: …`; or one that opens with a level-1 declaration)
111
111
  or a guide the verdict names was never read; it is silent in projects without guides and
112
112
  whenever it cannot tell. The code package (`agentic-sdlc`) wires it machine-wide on
113
113
  Claude Code; this package does not. To wire it by hand, add a `PreToolUse` entry with
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: kb-agentic
3
- version: 1.31.0
3
+ version: 1.32.0
4
4
  description: Knowledge-Base & Document-First protocol with risk-proportional triage, Vision as a guide, Signal Distillation, a complete Standalone mode and optional symbiosis with devPNT. Use for user documentation, knowledge extraction, SOPs, research notes, decision logs, knowledge management — and for answering project questions from the claim ledger and topic graph.
5
5
  author: Antonio Pinto (https://github.com/Antoneeo)
6
6
  copyright: (c) 2026 Antonio Pinto
@@ -24,7 +24,7 @@ Support files in the skill directory:
24
24
  - `review.md`: the review discipline — when a review is due, how to request one, how to receive findings, how to review.
25
25
  - `dispatch.md`: opt-in subagent execution of an approved plan, plus the family-wide delegation boundary (what may be delegated at all) — that part applies at any level.
26
26
  - `routing.md`: which lens owns this unit of work. Read ONLY when a sibling lens skill is installed alongside this one; a single-lens install never reads it.
27
- - `scripts/sdlc_check.py` + `scripts/sdlc_core.py`: the mechanical validator for the docs root (`check`, `validate`, `index`, `mark`, `benefit`, `gate`, `plan`, `orient`, `migrate`, and the knowledge overlay: `graph`, `corpus`, `claim-id`, `anchor`, `export`, `import`, `adopt` (dry run of an adoption), `stale` (spine staleness plus `## claims` -- the time cycle's consolidated view), plus `remind` — the opt-in per-turn one-line reminder, ENFORCEMENT §4). Keep the entry point, `scripts/sdlc_core.py` and `scripts/knowledge.py` together; the last supplies common memory and KB integrity.
27
+ - `scripts/sdlc_check.py` + `scripts/sdlc_core.py`: the mechanical validator for the docs root (`check`, `validate`, `index`, `mark`, `benefit`, `gate`, `plan`, `orient`, `migrate`, and the knowledge overlay: `graph`, `corpus`, `claim-id`, `anchor`, `export`, `import`, `adopt` (dry run of an adoption), `inventory <path>` (the project files under a path, each with its state against the corpus), `stale` (spine staleness plus `## claims` -- the time cycle's consolidated view), plus `remind` — the opt-in per-turn one-line reminder, ENFORCEMENT §4). Keep the entry point, `scripts/sdlc_core.py` and `scripts/knowledge.py` together; the last supplies common memory and KB integrity.
28
28
  - `scripts/docs_site.py --root <project> --open` (`--zip <file>` to share): opens in the user's browser a read-only site of what the agent reads; use it to show the project state.
29
29
  It is what users mean by "docs site", "minisito", "documentation site" or "project site": run it, never build a page by hand.
30
30
  - `ENFORCEMENT.md`: optional setup for CI and hooks.
@@ -102,7 +102,9 @@ Triage decides IF documentation is due; this table decides WHICH document each e
102
102
  | `topics/<slug>.md` | A placement verdict creates it (MISSING/INADEQUATE-child, `taxonomy.md`); reconciliation updates its claim rows. One node per topic — a similar-but-distinct concept is a sibling with `related:` + a written distinction, never a merge and never a duplicate. Merged/renamed nodes become tombstones (`status: SUPERSEDED` + `redirect_to:`), never deleted. | 4 |
103
103
  | a KB bundle (`export`) | Knowledge must leave this project. Export is L1 — it writes nothing into the corpus. **Importing one is L3**: a source enters the corpus and nodes are created, and a bundle is external input, so never L1 whatever its size (`portability.md`). | — |
104
104
  | `corpus/given/*` + sidecar | A source arrives: it becomes a content-addressed artifact with a sidecar carrying digest/date/`supersedes:`. A text source is copied verbatim, except that credentials and unneeded personal or confidential data are redacted first (`redacted_at_intake:`). A non-text source yields its stored canonical extraction — copied **beside** the original when that is small enough to keep, or **instead of it** on a large binary corpus, where the original stays where it lives and is recorded as `original_path:`/`original_sha256:` (`distillation.md` §1, which owns this rule) — never copied beside, and no `original_sha256:`, when it was redacted. Never edited after ingest — the digest check on whatever `given/` holds is what enforces it; the one removal is withdrawing an artifact that holds a secret (`distillation.md` §1). | 4 |
105
+ | `corpus/given/*` + sidecar `provenance: DECLARED` | A document leaves the project — to a client, a supplier, another function: the version sent is ingested like any source (redaction included), its sidecar carrying `provenance: DECLARED`, `recipient:` (an organization or role) and `date:` = the day it was sent; its claims are `DECLARED`. A resend is a new artifact that `supersedes:` the earlier one. Drafts, internal working documents and the files a sent document is generated from do not enter (`distillation.md` §1). | 4 |
105
106
  | `corpus/notes/*` | Something is said (`origin: elicited`), synthesised (`derived_from:`), or ruled (`basis:`). A note with none of the three is refused by the validator. | 4 / 5 |
107
+ | `audit/INVENTORY_exclusions.md` | The practitioner approves that some project files never enter the corpus (code, copies, generated files): one row `pattern \| reason \| approved`, written only after the approval, dated that day. `validate`/`check` refuse a malformed row. Read by `inventory`; never a list of what to ingest next (`vision/rulings.md` r37). | 4 |
106
108
  | the day's decisions, at a user-signed closing (the Capture Moment sweep) | Verification that the phase-4 notes/claims exist; NEW `corpus/notes/*` + claim rows only for what the sweep surfaces un-captured. The event is the CLOSING, disjoint from the row above (which fires when something is SAID). | 5 / session end |
107
109
  | `solutions/PLAN_[topic].md` + its ledger | A source too long for one context is ingested: **one task per reading window** (30 pages by default; the plan states the window used), each ending at a declared `extracted_through:`. That ledger is the register the ingestion resumes from across sessions — ingestion never builds a second one (`distillation.md` §3, `templates.md`). | 4 |
108
110
  | `corpus/given/*.meta.md` — `extracted_through:` | Every reading window closes by advancing it (`p=<n>`, `L<n>`, `complete`). Required once any claim cites the artifact: unstated, "I am finished" cannot be falsified, which is how a sampled 200-page manual passes for an ingested one. | 4 |
@@ -201,6 +203,14 @@ twin of the guide consult above.
201
203
  - `kb: <slug>[, <slug> …] → node matched, no claims` — surface the node's `gaps:` when
202
204
  it answers the question: "the KB knows it doesn't know" is a first-class answer.
203
205
  - `kb: no coverage` — index read, nothing fits.
206
+ Before declaring `no coverage` or `node matched, no claims`, look where the KB has
207
+ not: run `inventory <path>` on the project paths whose names plausibly hold the
208
+ subject, count its `NOT_EXAMINED` lines, and say it in the verdict —
209
+ `kb: no coverage; not examined under <path>: N` (N = 0 stated), naming the file when
210
+ one plainly holds the subject. When no path plausibly holds it, never run
211
+ `inventory .`: cite orient's bound (`not examined in the project: <= N`). When
212
+ `inventory` fails, the clause says so. A corpus is a subset of the project; a
213
+ verdict that hides the residue certifies a lookup over what was never read.
204
214
  - `kb: index absent — regenerate (sdlc_check.py index)` — the INDEX is missing, not
205
215
  necessarily the graph (nodes routinely exist while the index lags); regenerate and
206
216
  rescan.
@@ -216,7 +226,11 @@ twin of the guide consult above.
216
226
  decision-grounding** — surfaced, never silently cited. Per-provenance stance: GIVEN's
217
227
  source column is already the direct citation; ELICITED/RULING carry the practitioner's
218
228
  own authority; an IMPORTED claim grounding a decision is named as un-re-ratified
219
- foreign authority.
229
+ foreign authority. A **DECLARED** claim is what this project told a recipient: cite it
230
+ as "declared to <recipient> on <date>", never as a fact about the product, and a
231
+ `DIVERGES` row together with the ledger row it diverges from. A DECLARED artifact is
232
+ **not ground** for a product decision — a chain that ends on one rests on our own
233
+ statement: say so and treat the claim as unverified for decision-grounding.
220
234
  - **Citation scope — the row's test is scope, not fidelity.** Before citing a claim in
221
235
  an answer or a document, verify every part of the row belongs to the question: a row
222
236
  can be atomic as provenance and plural as meaning (`distillation.md` §3), and a
@@ -29,6 +29,7 @@ from the corpus, so everything is re-derivable when the rules improve.
29
29
  | a file from the practitioner | `corpus/given/<name>-<hash8>.<ext>` — verbatim, **content-addressed with a raw-byte sha256**; a newer version is a new file whose sidecar says `supersedes: <old>` — never an overwrite |
30
30
  | a non-text original (PDF, docx, xlsx) | its **canonical extraction**: `corpus/given/<name>-<hash8>.txt`, pages separated by form-feed; extractor id, version and normalization recorded in the sidecar. Offsets address THIS file — stored bytes, not a runtime step. The original is copied beside it when it is small enough to be worth keeping; **on a large or binary-heavy corpus the extraction is the artifact and the original stays where it is** (below) |
31
31
  | something the practitioner says | a transcription in `corpus/notes/`, frontmatter `origin: elicited`, dated |
32
+ | a document the project sends out | the version sent, in `corpus/given/` like any source (redacted at intake), sidecar `provenance: DECLARED`, `recipient:` (an organization or role, never a private person's name) and `date:` = the day sent; when the file also lives in the project, `original_path:` names it. Its claims are `DECLARED`: what we said, to whom, when — not facts about the product. A resend `supersedes:` the earlier send; drafts and the files a document is generated from never enter |
32
33
  | an agent synthesis | a note in `corpus/notes/` with `derived_from:` listing its sources — a note with neither `origin:` nor `derived_from:` nor `basis:` is **model knowledge disguised as a source**, and the validator refuses it |
33
34
  | a practitioner ruling | a note with `basis:` (`reconciliation.md`) |
34
35
 
@@ -88,8 +89,13 @@ Say the limit out loud, because a field that looks like a guarantee and is not i
88
89
  than an absent one — and say it **per field**, because the two are not in the same
89
90
  position:
90
91
 
91
- - **`original_sha256` is never checked.** We do not hold the bytes, so nothing can. It
92
- lets a human re-verify by hand and it dates the ingest; it detects nothing on its own.
92
+ - **`original_sha256` is checked only where the original is local.** `stale` and
93
+ `inventory` hash the file `original_path:` resolves to, when it is a local regular
94
+ file, and report an original that changed since ingest (`stale`'s `## claims`, until
95
+ a successor sidecar `supersedes:` this one). `check` never does: it runs on every
96
+ change, and re-reading gigabytes there is not proportional. Elsewhere — a remote, a
97
+ cloud-only file, no path — it detects nothing; it lets a human re-verify by hand and
98
+ it dates the ingest.
93
99
  Same honesty as a guide's `source_hash`, which proves the source is unchanged and
94
100
  never that the guide described it correctly.
95
101
  - **`original_path` IS checked — for resolution only.** "We do not hold it" was never a
@@ -167,10 +173,14 @@ Claims live in the owning topic's `## Claims` table (`templates.md` has the temp
167
173
  `p=<n>@<start>-<end>` (character offsets into page n of the stored extraction),
168
174
  `L<a>-<b>` (line files), `Sheet<s>!<cell>`. The span must exist — the validator opens
169
175
  the file and checks.
170
- - **prov** — `GIVEN | ELICITED | DERIVED | RULING | IMPORTED`. `IMPORTED` is a ruling that came from another project (`portability.md`): it keeps its text and original `basis:`, its note must say `imported_from:`, and it may not supersede a local row until you re-ratify it. Information for whoever resolves a
176
+ - **prov** — `GIVEN | ELICITED | DERIVED | RULING | IMPORTED | DECLARED`. `DECLARED`
177
+ rests only on a document this project sent (its sidecar says so and names the
178
+ `recipient:`), and no other provenance may cite one, in any source position — a
179
+ declaration is never corroboration (`reconciliation.md` §1). `IMPORTED` is a ruling that came from another project (`portability.md`): it keeps its text and original `basis:`, it rests on a renamed copy of its note that says `imported_from:`, `import` reopens the set it had settled, and nothing is settled by it until you ratify it with a ruling of your own. Information for whoever resolves a
171
180
  conflict; never a rank.
172
- - **state** — `OK`, `CONTESTED <id>[,..]`, `SUPERSEDED <id>[,..]` (`reconciliation.md`
173
- owns the transitions; a split bundle names ALL its successors).
181
+ - **state** — `OK`, `CONTESTED <id>[,..]`, `SUPERSEDED <id>[,..]`, and on a DECLARED row
182
+ only `DIVERGES <id>[,..]` (`reconciliation.md` owns the transitions; a split bundle
183
+ names ALL its successors).
174
184
 
175
185
  ## 3. Extraction discipline
176
186
 
@@ -12,7 +12,7 @@
12
12
  var D = null; // documents, loaded on first use
13
13
  var K = null; // knowledge base, loaded on first use
14
14
  var app = document.getElementById("app");
15
- var ORIGINS = ["given", "elicited", "derived", "ruling", "imported"];
15
+ var ORIGINS = ["given", "elicited", "derived", "ruling", "imported", "declared"];
16
16
 
17
17
  // ------------------------------------------------------------- helpers
18
18
 
@@ -483,6 +483,7 @@
483
483
  if (f.statement) marks.push(el("span", { "class": "tag statement", title: t("statement_notice"), text: t("statement_mark") }));
484
484
  if (f.state === "superseded") marks.push(el("span", { "class": "tag replaced", title: t("state_superseded_why"), text: t("state_superseded") }));
485
485
  if (f.state === "contested") marks.push(el("span", { "class": "tag conflict", title: t("state_contested_why"), text: t("state_contested") }));
486
+ if (f.state === "diverges") marks.push(el("span", { "class": "tag conflict", title: t("state_diverges_why"), text: t("state_diverges") }));
486
487
  if (opts && opts.topic) marks.push(link("#/topic/" + encodeURIComponent(f.topic), f.topic, "chip"));
487
488
  return el("li", { "class": "fact" + (f.state === "superseded" ? " replaced" : ""), onclick: function () { location.hash = "#/fact/" + encodeURIComponent(id); } },
488
489
  el("span", { "class": "dot bg-" + oc }),
@@ -597,6 +598,11 @@
597
598
  el("p", { "class": "why-line", text: t("state_contested_why") }),
598
599
  el("ul", { "class": "facts" }, (f.with || []).map(function (s) { return factLine(s, { topic: true }); })));
599
600
  }
601
+ if (f.state === "diverges") {
602
+ parts.push(el("h3", null, el("span", { "class": "tag conflict", text: t("state_diverges") }), " ", t("fact_diverges_from")),
603
+ el("p", { "class": "why-line", text: t("state_diverges_why") }),
604
+ el("ul", { "class": "facts" }, (f.diverges_from || []).map(function (s) { return factLine(s, { topic: true }); })));
605
+ }
600
606
  if (f.grounds) {
601
607
  parts.push(el("h3", { text: t("fact_rests_on") }),
602
608
  el("p", { "class": "why-line", text: t("ground_class_" + f.ground_class) }),
@@ -56,7 +56,9 @@ window.DOCS_SITE_STRINGS = {
56
56
  event_request: "request record",
57
57
  event_note: "note",
58
58
  event_source: "source document received",
59
+ event_sent: "document sent",
59
60
  event_review: "review",
61
+ event_defect: "defect reported after merge",
60
62
  event_decision: "decision record",
61
63
  event_project_note: "project note",
62
64
  event_spike: "exploration outcome",
@@ -231,6 +233,8 @@ window.DOCS_SITE_STRINGS = {
231
233
  origin_ruling_why: "Taken to settle a conflict.",
232
234
  origin_imported: "from another knowledge base",
233
235
  origin_imported_why: "Brought in from another knowledge base, where its own sources are.",
236
+ origin_declared: "declared by this project",
237
+ origin_declared_why: "What this project told a recipient, on the day it was sent: a statement of ours, not a fact about the product.",
234
238
  origin_other: "origin as written: {v}",
235
239
  origin_other_why: "An origin this site does not know.",
236
240
 
@@ -239,6 +243,8 @@ window.DOCS_SITE_STRINGS = {
239
243
  state_superseded_why: "A newer fact takes its place.",
240
244
  state_contested: "in conflict",
241
245
  state_contested_why: "Facts that disagree, not yet settled.",
246
+ state_diverges: "diverges",
247
+ state_diverges_why: "What we declared contradicts what the project decided. It is corrected by a new send, or the decision changes.",
242
248
 
243
249
  statement_mark: "statement",
244
250
  statement_prefix: "[statement] {text}",
@@ -254,6 +260,7 @@ window.DOCS_SITE_STRINGS = {
254
260
  fact_replaced_by: "Replaced by",
255
261
  fact_current_at_end: "Current fact at the end of each chain",
256
262
  fact_in_conflict_with: "In conflict with",
263
+ fact_diverges_from: "Diverges from",
257
264
  fact_rests_on: "What it rests on",
258
265
  fact_untraced: "This part could not be traced.",
259
266
  fact_sources: "Sources",
@@ -266,6 +273,7 @@ window.DOCS_SITE_STRINGS = {
266
273
  fact_unknown: "This fact is not in this knowledge base.",
267
274
 
268
275
  ground_document: "a document",
276
+ ground_declared: "a document this project sent",
269
277
  ground_statement: "a person's statement",
270
278
  ground_decision: "a decision",
271
279
  ground_kb: "another knowledge base",
@@ -273,6 +281,7 @@ window.DOCS_SITE_STRINGS = {
273
281
  ground_class_statement: "Rests, at least in part, on a person's statement.",
274
282
  ground_class_untraceable: "Part of what it rests on could not be traced.",
275
283
  ground_class_imported: "Rests on another knowledge base.",
284
+ ground_class_declared: "Rests on our own declaration to a recipient, not on evidence.",
276
285
  ground_class_documents: "Rests on documents or decisions only.",
277
286
 
278
287
  chain_loop: "The chain of replacements loops back to {id}: it stops here.",
@@ -18,6 +18,7 @@
18
18
  --o-derived: #a99be0;
19
19
  --o-ruling: #7fa7e6;
20
20
  --o-imported: #9cc96b;
21
+ --o-declared: #d98fb0;
21
22
  --conflict: #e0605a;
22
23
  --display: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Charter, Georgia, serif;
23
24
  --body: "Segoe UI Variable Text", "Avenir Next", Avenir, "Gill Sans Nova", "Gill Sans", "Trebuchet MS", sans-serif;
@@ -179,6 +180,7 @@ a.factid code { color: var(--link); }
179
180
  .o-derived { color: var(--o-derived); } .bg-derived { background: var(--o-derived); }
180
181
  .o-ruling { color: var(--o-ruling); } .bg-ruling { background: var(--o-ruling); }
181
182
  .o-imported { color: var(--o-imported); } .bg-imported { background: var(--o-imported); }
183
+ .o-declared { color: var(--o-declared); } .bg-declared { background: var(--o-declared); }
182
184
  .o-other { color: var(--paper-3); } .bg-other { background: var(--paper-3); }
183
185
 
184
186
  .panel {
@@ -262,6 +262,12 @@ in the snapshot, never to your assumption about what the code does.
262
262
  (source-anchored), not an elaborated version of it.
263
263
  - When in doubt about whether something is "supported" by the source, treat it
264
264
  as not covered rather than stretching the marker to fit.
265
+ - **Declared exception: `## Preservation constraints`.** A code guide may carry
266
+ this section, copied at an L3 closure from the ANALYSIS's table (code lens,
267
+ `SKILL.md` Phase 5). The section's marker cites each row it holds as
268
+ `[source: solutions/ANALYSIS_<name>.md#P<n>]` instead of a snapshot anchor: each
269
+ row records what the code must keep doing, as that unit enumerated and probed it,
270
+ not an excerpt of the code.
265
271
 
266
272
  ## 4. Ingestion bound (T9)
267
273
 
@@ -293,5 +299,7 @@ state it explicitly when handing off a newly created guide.
293
299
  closure (docs travel with the code); when you merely READ one, check it against
294
300
  the code before trusting it. Too large to absorb → mark it `SUPERSEDED` rather
295
301
  than leave a confident-wrong map badged CURRENT, which is worse than none.
302
+ - **A regenerated guide keeps `## Preservation constraints` unchanged**: the
303
+ section does not come from the snapshot (§3), so regeneration carries it over.
296
304
  - **Agent-global KB guides** use the same pipeline and validator via
297
305
  `--root ~/.agentic-sdlc`; freshness via the same `stale` engine.
@@ -39,7 +39,8 @@ sdlc_check.py check # always, right after
39
39
  and, if a topic was skipped, *"a topic with the same name already exists; I have not
40
40
  merged them, because they may not be the same subject. Do you want me to look?"*
41
41
  4. If an imported ruling arrived: *"one decision came from the other project. I have kept
42
- it, marked as theirs, and it cannot settle anything here until you confirm it."*
42
+ it, marked as theirs, and it cannot settle anything here until you confirm it. The
43
+ question it had closed is open again here, with their decision beside the sources."*
43
44
 
44
45
  Do not explain the closure, the id hashing or the provenance model unless asked. The
45
46
  user asked to move their knowledge, not to learn how it is stored.
@@ -74,6 +75,11 @@ You choose topics; the export decides what must travel with them.
74
75
  When a partner row lives in an unselected topic, that topic is **added and reported** —
75
76
  never dropped, never silently.
76
77
  - **Not the `not-yet-honored-by` markers.** They describe this project's code, so the export drops them and says so, without a number.
78
+ - **Not the declarations.** A `DECLARED` row is this project's commitment to its own
79
+ recipient; carried elsewhere it would read as the importer's. The export leaves the
80
+ rows and the artifacts only they cite behind and says so in one line, without a
81
+ number. Nothing else is lost: no other row may point at a declaration, and the export
82
+ refuses one that does (run `check`).
77
83
 
78
84
  If a conflict partner resolves to no row at all, the export **refuses**: exporting a
79
85
  broken set is worse than exporting nothing.
@@ -83,7 +89,8 @@ tombstone sidecar stays home.
83
89
 
84
90
  ## 3. Import is additive, and decides nothing
85
91
 
86
- `import` writes files. It does not place concepts, merge bodies, or settle disagreements.
92
+ `import` writes files. It does not place concepts, merge bodies, or settle disagreements —
93
+ and it refuses to let the bundle settle them either: a ruling arrives foreign (§4).
87
94
 
88
95
  - **Never overwrites a topic.** A slug that already exists is reported and skipped: two
89
96
  projects using the word `pricing` may mean two different things, and that judgement is
@@ -96,7 +103,12 @@ tombstone sidecar stays home.
96
103
  It refuses on: a missing or unmarked `MANIFEST.md`; a path that escapes the docs root; an
97
104
  artifact whose name matches an existing one **with different bytes** (content-addressed
98
105
  names must mean equal content — a mismatch means one of the two is lying about its
99
- origin); an incomplete conflict set; a dangling `supersedes:`.
106
+ origin); an incomplete conflict set — a `CONTESTED` or `SUPERSEDED` pointer that would
107
+ resolve to no row, or to a row of this project that does not name it back (an import
108
+ never edits an existing topic); a row settled by a ruling the import does not write, or
109
+ by a row this project holds as `IMPORTED`; a ruling it cannot carry foreign (§4); a
110
+ `DECLARED` row (an export never ships one, so the bundle was edited by hand). Each
111
+ refusal names the row and writes nothing.
100
112
 
101
113
  After importing, run `check`. The import is deliberately not a validator.
102
114
 
@@ -108,19 +120,43 @@ another owner's decision, and importing it unchanged would make that decision bi
108
120
  here without anyone here granting it — the machine deciding, which reconciliation
109
121
  refuses everywhere else.
110
122
 
111
- So an imported ruling arrives as **`prov: IMPORTED`** (owner ruling, 2026-08-03):
112
-
113
- - its text, span and original `basis:` travel **verbatim** — the knowledge is not lost,
114
- and pretending it is a `DERIVED` synthesis would make the row lie about where it came
115
- from;
116
- - its note carries `imported_from:`, and the validator refuses an `IMPORTED` row without
117
- it: unnamed, the class says nothing and the row is a `RULING` with the label filed off;
118
- - **it cannot supersede a local row.** The validator errors on `SUPERSEDED <id>` when
119
- that id is `IMPORTED`.
120
-
121
- **Re-ratification** is one act: read it, write your own note with your own `basis:`, and
122
- set the row's `prov` to `RULING`. That is you deciding, which is the only thing that ever
123
- settles anything here.
123
+ So `import` makes every `RULING` row of a topic it writes arrive as **`prov: IMPORTED`**
124
+ (owner rulings 2026-08-03 and 2026-10-23):
125
+
126
+ - **its note arrives as a renamed copy**, `corpus/notes/imported_<source project>_<name>`,
127
+ whose frontmatter adds `imported_from: <source project> — <original path>` and reads
128
+ `origin: imported`. Its text and original `basis:` travel **verbatim** — the knowledge
129
+ is not lost, and pretending it is a `DERIVED` synthesis would make the row lie about
130
+ where it came from. The note is never edited under its own name: a line inserted into
131
+ it would move the line every locator cites. The row cites the copy at the same text
132
+ (its locator and id move with it), and a bundle note `derived_from:` the ruling names
133
+ the copy. The original name is never written, so a row copied by hand out of a skipped
134
+ topic cannot cite a foreign note as local;
135
+ - **the question it had closed reopens.** The rows it superseded and the ruling itself
136
+ arrive as one `CONTESTED` set; a ruling already contested with a newer source keeps
137
+ that partner, one that superseded nothing arrives `OK`;
138
+ - the validator refuses an `IMPORTED` row whose note lacks `imported_from:` (unnamed,
139
+ the class says nothing), a `SUPERSEDED <id>` pointing at an `IMPORTED` row, and a
140
+ `RULING` row on a note that names a foreign origin.
141
+
142
+ **Ratification** is the ordinary ruling of `reconciliation.md`, and it is you deciding,
143
+ which is the only thing that ever settles anything here: write your own note with your
144
+ own `basis:`, add a `RULING` row on it, and mark every row `CONTESTED` with the imported
145
+ one — following the sets transitively — `SUPERSEDED <your id>`, the `IMPORTED` row
146
+ included. Relabelling the `IMPORTED` row `RULING` is refused: another project's note
147
+ cannot carry your decision.
148
+
149
+ Any bundle note that declares a `basis:` lands this way, renamed, even when its ruling row
150
+ does not travel with it — otherwise it would land first under its own name and a later
151
+ import of its ruling would find it there. A ruling `import` cannot carry this way is
152
+ refused, never copied: one not resting on a note cited by line, on a note with no
153
+ frontmatter, or on a note that already names a foreign origin (the source relabelled an
154
+ imported row); a row superseded both by a ruling and by a row that is not one; a ruling
155
+ another row superseded while rows still depend on it. A tree imported before this
156
+ conversion existed (≤ kb 1.31.0) holds the foreign ruling as `RULING` on its original
157
+ note: delete the imported topic and its files and import again, or convert it by hand as
158
+ above. The origin is named after the exporting folder (`source_project`), so two clones
159
+ of one project in differently named folders name it differently.
124
160
 
125
161
  ## 5. Triage
126
162
 
@@ -24,6 +24,15 @@ Subject-sameness is semantic judgement; no query performs it. Classify:
24
24
  | **coexistence** | contradictory only if scopes overlapped — and by the half-open rule they do not | both rows stay `OK`. "12k until March" and "15k from March" are two truths, not a conflict |
25
25
  | **conflict** | contradictory, scopes overlap | **every** row in the set becomes `CONTESTED` listing all counterparts. Nothing is picked |
26
26
 
27
+ **A declaration is not a source.** A `DECLARED` row (`distillation.md` §1: what this
28
+ project sent) never joins the five outcomes with a fact: it never corroborates a row,
29
+ never refines or supersedes one, never contests one. Against the ledger it is either
30
+ consistent (`OK`) or it **diverges**: its assertion contradicts a non-superseded,
31
+ non-DECLARED row, scopes overlapping → the DECLARED row alone becomes
32
+ `DIVERGES <id>[, ..]`; the ledger row is not touched, because our own wrong statement
33
+ puts nothing about the product in doubt. Two declarations that contradict each other
34
+ are `CONTESTED` — both are still held by their recipients.
35
+
27
36
  The machine then verifies (the `graph` check): ids recompute, sources resolve and their
28
37
  spans exist, `CONTESTED` is **symmetric** (editing one cell to `OK` fails the check —
29
38
  the cheapest laundering), a `CONTESTED` pointer at a `SUPERSEDED` row is an error,
@@ -73,6 +82,9 @@ premise: signed amendments arrive later.
73
82
  | newer source vs a ruling | new set {new row, ruling row}; escalation shows the `basis:` |
74
83
  | member refined | the refining row inherits the member's contested relations (re-judged); counterparts rewritten to the new id |
75
84
  | corroboration of a contested member | source appended; the set untouched — more evidence is not new information, and the escalation form shows source counts |
85
+ | a declaration contradicts the ledger | the DECLARED row → `DIVERGES <ledger ids>`; the ledger rows stay as they are; escalation names it |
86
+ | a new send corrects it | the new DECLARED row; the divergent one → `SUPERSEDED <new-id>` (a declaration is superseded only by a later declaration) |
87
+ | a ruling changes the decision | the ledger row → `SUPERSEDED <ruling-id>`; `check` then refuses the stale `DIVERGES` until the declaration is re-judged against the ruling: `OK` if it now agrees, `DIVERGES <ruling-id>` if not |
76
88
  | bundled row split (`distillation.md` §3) | N atomic rows `OK`, each on its narrowed span; the bundle → `SUPERSEDED <id1>[, <id2> ..]`, text intact; contested relations re-judged per successor |
77
89
 
78
90
  A superseded row keeps its `not-yet-honored-by` marker with its text. Copy the marker to each successor that still states behavior the code does not have; the analysis or shadow citing the predecessor's id satisfies the successor.
@@ -103,7 +115,11 @@ record like any claim-table edit.
103
115
  Escalations are presented **once, at the end of a run** — the pipeline never stops
104
116
  mid-ingest to ask. Each escalation names: the claims in the set, each one's source
105
117
  (reopenable), date and provenance, and one line on why the machine cannot decide
106
- ("same subject, overlapping validity, GIVEN vs GIVEN, no newer source"). **A question
118
+ ("same subject, overlapping validity, GIVEN vs GIVEN, no newer source"). A divergence
119
+ is escalated the same way, naming the recipient and the day it was sent: the
120
+ practitioner corrects it with a new send or changes the decision. One limit stated
121
+ out loud: `DIVERGES` is one-way, so unlike `CONTESTED` a hand edit back to `OK` has
122
+ no counterpart to fail on — the diff review is what catches it. **A question
107
123
  that cannot fill this form is not askable** — it is the symptom that the answer is in
108
124
  the corpus and was not searched. Generic confirmations are not questions.
109
125
 
@@ -122,10 +138,15 @@ An `IMPORTED` row is a ruling made by the owner of a different project
122
138
 
123
139
  - It keeps its text, its span and its original `basis:` verbatim — the knowledge is not
124
140
  lost, and relabelling it `DERIVED` would make the row lie about its origin.
125
- - Its note must carry `imported_from:`. Unnamed, the class says nothing and the row is a
126
- `RULING` with the label filed off; the validator refuses it.
127
- - **It cannot settle anything here.** `SUPERSEDED <id>` pointing at an `IMPORTED` row is
128
- an error: nobody in this project granted that decision its authority.
129
-
130
- Re-ratification is one act, and it is you deciding: read it, write your own note with
131
- your own `basis:`, set `prov` to `RULING`.
141
+ - It rests on a renamed copy of its note (`imported_<project>_<name>`) that carries
142
+ `imported_from:`. Unnamed, the class says nothing and the row is a `RULING` with the
143
+ label filed off; the validator refuses it.
144
+ - **It cannot settle anything here.** `import` reopens the set it had settled: its former
145
+ members and the ruling arrive `CONTESTED` with each other. `SUPERSEDED <id>` pointing at
146
+ an `IMPORTED` row is an error: nobody in this project granted that decision its
147
+ authority.
148
+
149
+ Ratification is the ruling procedure above, and it is you deciding: your own note with
150
+ your own `basis:`, a `RULING` row on it, and every row `CONTESTED` with the imported one —
151
+ transitively, when it sits in more than one set — `SUPERSEDED <your id>`, the imported row
152
+ included. A `RULING` on a note that names a foreign origin is refused.
@@ -187,7 +187,10 @@ can block forever gets removed. **Log one row per completed review, PASS or FAIL
187
187
  logging only passes would erase exactly that evidence. The row goes in
188
188
  `ai_docs/audit/reviews/REVIEW_LOG.md` (create it if absent — `templates.md`):
189
189
  `| date | doc_key | tier | model | reviewer | findings_raised | findings_real | verdict | revise_rounds |`,
190
- with `tier` = `design` or `closure` in Standalone. **`model` records the capability
190
+ with `tier` = `design` or `closure` in Standalone. In the code lens, a wrong behaviour
191
+ the owner reports in a merged L3 change is one row too, tier `post-merge`, `model`
192
+ `none (owner report)`, reviewer `owner`, the owner quoted in the notes: a defect
193
+ record, not a review, so `benefit` keeps it out of every review figure. **`model` records the capability
191
194
  tier that actually ran** — `deep`, `light`, `economy`, `single (client exposes no
192
195
  choice)`, `below floor: <reason>`, or `none (not due)` on a NOT DUE row — never a provider name in doctrine (a project
193
196
  may of course write its own model names in its own log). Without it the floor above
@@ -285,6 +288,11 @@ The binding inputs, by review type:
285
288
  clause reads both), and the **probe harness** (`ai_docs/solutions/harness_[feature]/`).
286
289
  Hand these *in addition to* the design artifact — the reviewer checks the artifact
287
290
  **against** them, not only for internal consistency.
291
+ - **The constraint view comes last.** Where the ANALYSIS carries ids, the reviewer
292
+ derives the callers and behaviours first, then compares with
293
+ `sdlc_check.py constraints --unit <id>`: a constraint the design states and the
294
+ diff breaks is a finding. The view never replaces the ANALYSIS or the diff in the
295
+ request.
288
296
  - **For a closure review, the diff and the test evidence**: the run output that
289
297
  backs every completion claim, dated after the final relevant change.
290
298
  - **For a design or closure review of an L3/E3 unit, the request record**
@@ -615,6 +615,11 @@ class Grounds:
615
615
  return [{"kind": "untraceable", "path": rel}]
616
616
  seen = seen | {rel}
617
617
  if rel.startswith("corpus/given/"):
618
+ # F-084: a document this project SENT is our own declaration, not
619
+ # ground for a fact about the product.
620
+ side = self._meta(rel + ".meta.md")
621
+ if _v(side, "provenance").upper() == "DECLARED":
622
+ return [{"kind": "declared", "path": rel}]
618
623
  return [{"kind": "document", "path": rel}]
619
624
  if not _inside(self.docs, rel) or not (self.docs / rel).is_file():
620
625
  return [{"kind": "untraceable", "path": rel}]
@@ -643,6 +648,8 @@ def ground_class(leaves):
643
648
  return "statement"
644
649
  if "untraceable" in kinds:
645
650
  return "untraceable"
651
+ if "declared" in kinds:
652
+ return "declared"
646
653
  if "kb" in kinds:
647
654
  return "imported"
648
655
  return "documents"
@@ -707,12 +714,15 @@ def build_kb(docs):
707
714
  kind, rest = _kind(r)
708
715
  f = {"text": r["claim"], "valid": r["valid"], "qty": r["qty"], "about": r["about"],
709
716
  "origin": (r["prov"] or "").strip().lower(), "topic": slug,
710
- "state": {"SUPERSEDED": "superseded", "CONTESTED": "contested"}.get(kind, "ok")}
717
+ "state": {"SUPERSEDED": "superseded", "CONTESTED": "contested",
718
+ "DIVERGES": "diverges"}.get(kind, "ok")}
711
719
  if kind == "SUPERSEDED":
712
720
  f["successors"] = _ids(rest)
713
721
  f["chain_ends"] = chain_ends(r["id"], by_id)
714
722
  if kind == "CONTESTED":
715
723
  f["with"] = _ids(rest)
724
+ if kind == "DIVERGES":
725
+ f["diverges_from"] = _ids(rest)
716
726
  srcs = []
717
727
  for src in r["source"].split(";"):
718
728
  src = src.strip()
@@ -1246,7 +1256,9 @@ def chronology(docs, pages):
1246
1256
  text = _text(docs / rel) if (docs / rel).is_file() else None
1247
1257
  if text is not None:
1248
1258
  for row in sdlc_core.parse_review_log(text)[0]:
1249
- add(row["date"], "review", "%s — %s: %s" % (
1259
+ # F-085: a `post-merge` row is the owner's defect report, not a review
1260
+ kind = "defect" if row["tier"].lower().startswith("post-merge") else "review"
1261
+ add(row["date"], kind, "%s — %s: %s" % (
1250
1262
  row["doc_key"], row["tier"], row["verdict"]), rel)
1251
1263
  for p in sorted(docs.glob("architecture/ADR_*.md")):
1252
1264
  rel = "architecture/" + p.name
@@ -1269,7 +1281,9 @@ def chronology(docs, pages):
1269
1281
  continue
1270
1282
  if kind == "source":
1271
1283
  name = p.name[:-len(suffix)]
1272
- add(date, kind, name, rel, source=name)
1284
+ # F-084: a DECLARED sidecar's date is the day it was SENT
1285
+ sent = _v(fm, "provenance").upper() == "DECLARED"
1286
+ add(date, "sent" if sent else kind, name, rel, source=name)
1273
1287
  else:
1274
1288
  add(date, kind, title_of(rel, text), rel)
1275
1289
  # Most recent first. Within one day a document's later entry is the more recent