@antoneeo/kb-agentic-skill 1.0.1 → 1.1.1

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/README.md CHANGED
@@ -8,9 +8,9 @@ The sibling of [`@antoneeo/agentic-sdlc-skill`](https://www.npmjs.com/package/@a
8
8
 
9
9
  Two axes are kept apart on purpose — **abstraction** (a topic made of topics: the graph edges) and **certainty** (how strongly the corpus supports a statement: the claim rows).
10
10
 
11
- 1. **Intake — the source becomes untouchable.** Every document enters verbatim under `corpus/given/`, content-addressed by a raw-byte digest. A new version is appended with `supersedes:`, never overwritten. Non-text files get a **stored canonical extraction** (`.txt`, registered extractor) — those are the bytes locators point at, so an id stays valid across re-ingestion. Spoken input is a note with `origin: elicited`; a synthesis carries `derived_from:`; your decision carries `basis:`. A note with none of the three is model knowledge dressed as a source, and the validator rejects it.
11
+ 1. **Intake — the source becomes untouchable.** Every document enters verbatim under `corpus/given/`, content-addressed by a raw-byte digest. A new version is appended with `supersedes:`, never overwritten. Non-text files get a **stored canonical extraction** (`.txt`, registered extractor) — those are the bytes locators point at, so an id stays valid across re-ingestion. On a large binary corpus the extraction **is** the artifact: the digest moves onto it (immutability still enforced, on the bytes that actually matter) and the original stays where it lives, recorded as `original_path:` + `original_sha256:`. 233 MB of manuals do not enter your repository to protect bytes no locator addresses — and the two recorded fields are honest about their limit: they let a human re-verify, they check nothing on their own. Spoken input is a note with `origin: elicited`; a synthesis carries `derived_from:`; your decision carries `basis:`. A note with none of the three is model knowledge dressed as a source, and the validator rejects it.
12
12
 
13
- 2. **Extraction — the unit is the claim.** Rows of `id | claim | valid | qty | about | source | prov | state`. The **id hashes the location and the quantity, never the text**, so an LLM rephrasing mints no new identity. The locator (`p=17@412-509`) is verified: the validator opens the extraction and checks the span exists. Validity scopes are half-open ("until March" and "from March" do not conflict). Quantities are typed — mixed kinds or currencies **refuse to sum**. What the source does not assert becomes a `gaps:` line, never a claim.
13
+ 2. **Extraction — the unit is the claim.** Rows of `id | claim | valid | qty | about | source | prov | state`. The **id hashes the location and the quantity, never the text**, so an LLM rephrasing mints no new identity. The locator (`p=17@412-509`) is verified: the validator opens the extraction and checks the span exists — and `anchor <path> <phrase>` produces it for you, matching spaces as `\s+` because a PDF extraction breaks phrases mid-line. **Gates are extracted alongside powers**: for every row saying what something can do, the source is asked what must hold first — default-off, licence tier, version floor, dependency — because "yes, supported" without the gate is a plan that fails on site. The rule is *ask*, never *produce*: a source that states no gate yields no row. Validity scopes are half-open ("until March" and "from March" do not conflict). Quantities are typed — mixed kinds or currencies **refuse to sum**. What the source does not assert becomes a `gaps:` line, never a claim.
14
14
 
15
15
  3. **Placement — five verdicts, after querying the graph.** Descent through the generated index following every parent (polyhierarchy). EXISTS → reconcile; INADEQUATE → child; **MISSING only after the graph was actually asked**; GENERALIZES → escalate (a new root stops at you); UNPLACED → quarantine. Similar-but-maybe-different becomes a sibling **with the distinguishing line written** — if you cannot write it, it is the same concept. Cycles are refused at write time; merged nodes leave a tombstone with `redirect_to:`, never a deletion.
16
16
 
@@ -22,12 +22,12 @@ Deliberately absent: any per-node coverage or completion state. `gaps:` says wha
22
22
 
23
23
  ## Key features
24
24
 
25
- - **Risk-proportional triage**: L1/L2/L3/Spike (noteSOPcorpus), so a quick fact never pays for heavyweight process, with a **Write Triggers** table mapping each knowledge event to exactly one destination.
25
+ - **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. A **Write Triggers** table maps each knowledge event to exactly one destination.
26
26
  - **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.
27
27
  - **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".
28
28
  - **Question discipline**: a question is legal only when the agent searched first, names the search with its result, and names the decision it unblocks; otherwise it proceeds on a declared assumption, batched.
29
29
  - **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.
30
- - **Mechanical checks**: `check`, `validate`, `index`, `graph`, `corpus`, `claim-id`, plus the spine's `stale`/`mark`/`gate`/`plan`/`orient`/`migrate`. The graph and corpus checks verify spans against the stored extraction, recompute every id, refuse cycles and unreachable nodes, and enforce `CONTESTED` symmetry.
30
+ - **Mechanical checks**: `check`, `validate`, `index`, `graph`, `corpus`, `claim-id`, `anchor`, plus the spine's `stale`/`mark`/`gate`/`plan`/`orient`/`migrate`. The graph and corpus checks verify spans against the stored extraction, recompute every id, refuse cycles and unreachable nodes, and enforce `CONTESTED` symmetry.
31
31
  - **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`, and the validator's two files, `scripts/sdlc_check.py` + `scripts/sdlc_core.py` (the core is the family's shared spine — copy both, or neither).
32
32
 
33
33
  ## Installation
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kb-agentic-skill",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
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.0.1",
3
+ "version": "1.1.1",
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",
@@ -21,7 +21,7 @@ Support files in the skill directory:
21
21
  - `review.md`: the review discipline — when a review is due, how to request one, how to receive findings, how to review.
22
22
  - `dispatch.md`: opt-in subagent execution of an approved plan.
23
23
  - `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.
24
- - `scripts/sdlc_check.py` + `scripts/sdlc_core.py`: the mechanical validator for the docs root (`check`, `validate`, `index`, `stale`, `mark`, `gate`, `plan`, `orient`, `migrate`, and the knowledge overlay: `graph`, `corpus`, `claim-id`). Two files: the core is the family's shared spine; the entry point IS the knowledge overlay — the claim ledger and topic-graph checks live inside it, so the core alone runs none of them. Copy both, or neither.
24
+ - `scripts/sdlc_check.py` + `scripts/sdlc_core.py`: the mechanical validator for the docs root (`check`, `validate`, `index`, `stale`, `mark`, `gate`, `plan`, `orient`, `migrate`, and the knowledge overlay: `graph`, `corpus`, `claim-id`, `anchor`). Two files: the core is the family's shared spine; the entry point IS the knowledge overlay — the claim ledger and topic-graph checks live inside it, so the core alone runs none of them. Copy both, or neither.
25
25
  - `ENFORCEMENT.md`: optional setup for CI and hooks.
26
26
 
27
27
  Read these files only when needed. `SKILL.md` is the operating contract; the support files are progressive resources.
@@ -43,12 +43,19 @@ Always classify the request before choosing the process. Declare the chosen leve
43
43
 
44
44
  | Level | Criteria | Required process |
45
45
  |---|---|---|
46
- | **L1 - Quick Fact / Snippet** | Small update to an existing note, typo fix in docs, quick preference update (at most 1-2 files). | Implement edit directly in existing note. No new documents. |
47
- | **L2 - Local Note / SOP Update** | Specific SOP update, local research note, or small document addition (at most 1-2 files). Low risk. | Mini-analysis in message: objective, impact, sources, validation. Update existing document or create single SOP/note. |
48
- | **L3 - Major Knowledge Unit / Corpus** | Ingesting large document sets, complex multi-topic research, multi-part guide creation, or restructuring the KB. | Full workflow: Vision Gate, Spec Elicitation, Taxonomy Pass, Knowledge Analysis, Distillation, Review, Indexing. |
46
+ | **L1 - Quick Fact / Snippet** | One claim row added to an existing topic; a typo; a preference update. No node created, no source entering the corpus, no frontmatter change. | Implement edit directly in existing note. No new documents. |
47
+ | **L2 - Propagation of settled knowledge** | The fact is **already settled** in the corpus and the work carries it into existing documents — restating, correcting a stale copy, updating an SOP that quotes it. No node created or superseded, no hierarchy change, no node frontmatter change, no new source ingested. | Mini-analysis in message: objective, impact, sources, validation. Update existing document or create single SOP/note. |
48
+ | **L3 - New knowledge unit / Corpus** | A source enters the corpus; a topic node is created or superseded; the hierarchy moves; a conflict must be reconciled; or what a claim asserts changes. | Full workflow: Vision Gate, Spec Elicitation, Taxonomy Pass, Knowledge Analysis, Distillation, Review, Indexing. |
49
49
  | **Spike - Exploration** | Time-boxed exploratory research or draft without merging into official KB. | Outcome in `ai_docs/solutions/SPIKE_[topic].md`. |
50
50
 
51
51
  Cross-cutting rules:
52
+ - **The unit of measure here is knowledge, never file count.** In this domain risk is
53
+ knowledge-shaped: carrying one settled fact into eight documents is small, and one
54
+ claim that re-parents a node is not. Do not import the code lens's file thresholds —
55
+ a level is chosen by what the change does to the corpus and the graph. What keeps
56
+ this from becoming an escape hatch is the trigger list below, which overrides the
57
+ level whatever the size, plus one limit: **propagation that changes what a claim
58
+ asserts is not propagation** — it is a new knowledge unit, so L3.
52
59
  - **Domain routing (multi-lens installs only).** After the level is set, and only when a sibling lens skill of this family is installed (`agentic-sdlc`, `mkt-agentic-sdlc`), run the router in `routing.md` for every L2, L3 and Spike: it decides which lens's method and validation rules govern this unit of work. L1 never reaches it, and a single-lens install never reads the file — detection fails open. In such a project, never refer to a document whose meaning differs by lens ("threat model", "vision", `principles.md`, `handoff.md`) by its bare name: qualify it with its domain, or name its path.
53
60
  - Personal data, credentials, security-sensitive processes, authN/authZ specs are high-risk: never L1.
54
61
  - **Escalation triggers — ANY of these makes it L3, whatever the file count:** the change touches the topic hierarchy (`parents:`, a `GENERALIZES` verdict, a re-parent); it touches more than one node's frontmatter; it creates or supersedes a node other nodes reference. Re-shaping the graph is a unit of change, never a side effect of placing one claim.
@@ -76,7 +83,7 @@ Triage decides IF documentation is due; this table decides WHICH document each e
76
83
  | `strategic/architecture.md`, `strategic/existing_features.md` | Bootstrap; update at closure when the knowledge catalog actually changed. | 1 / 5 |
77
84
  | `vision/project_vision.md`, `roadmap.md`, `principles.md` | Bootstrap as `Status: DRAFT`; promoted to APPROVED only by explicit user confirmation. | 1 / 2 |
78
85
  | `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 |
79
- | `corpus/given/*` + sidecar | A source arrives: verbatim copy, content-addressed, sidecar with digest/date/`supersedes:`; non-text originals also get their stored canonical extraction (`distillation.md` §1). Never edited after ingest — the digest check is what enforces it. | 4 |
86
+ | `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. 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 edited after ingest — the digest check on whatever `given/` holds is what enforces it. | 4 |
80
87
  | `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 |
81
88
  | `INDEX.md`, `reference/INDEX.md`, `topics/INDEX.md`, `corpus/INDEX.md` | Regenerated by `sdlc_check.py index` at closure — never by hand; `validate` fails on a hand-edited one. | 5 |
82
89
 
@@ -14,14 +14,32 @@ from the corpus, so everything is re-derivable when the rules improve.
14
14
  | What arrives | Where it lands |
15
15
  |---|---|
16
16
  | 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 |
17
- | a non-text original (PDF, docx, xlsx) | additionally, 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 |
17
+ | 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) |
18
18
  | something the practitioner says | a transcription in `corpus/notes/`, frontmatter `origin: elicited`, dated |
19
19
  | 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 |
20
20
  | a practitioner ruling | a note with `basis:` (`reconciliation.md`) |
21
21
 
22
- Every sidecar (`<original>.meta.md`) carries: the digest, the date, provenance, and
22
+ Every sidecar (`<artifact>.meta.md`) carries: the digest, the date, provenance, and
23
23
  `supersedes:` when it replaces an earlier version. `corpus/INDEX.md` is generated.
24
24
 
25
+ **Extraction-as-artifact — the variant for a large binary corpus.** Copying gigabytes
26
+ of PDFs into the docs root buys nothing: what the digest protects is *the bytes a
27
+ locator addresses*, and those are the extraction's. So the extraction may be the
28
+ corpus artifact on its own. Then `sha256:` is the extraction's digest — immutability is
29
+ enforced exactly as before — and the original is recorded, not copied:
30
+
31
+ ```yaml
32
+ sha256: <digest of this extraction> # enforced: the bytes locators address
33
+ original_path: /vault/manuals/xyz.pdf # recorded: where the original lives
34
+ original_sha256: <digest at ingest> # recorded, NOT verified — we do not hold it
35
+ ```
36
+
37
+ Say the limit out loud, because a field that looks like a guarantee and is not is worse
38
+ than an absent one: **`original_sha256` is never checked.** It lets a human re-verify by
39
+ hand and it dates the ingest; it detects nothing on its own. The digest that bites is
40
+ `sha256:`. Same honesty as a guide's `source_hash`, which proves the source is unchanged
41
+ and never that the guide described it correctly.
42
+
25
43
  ## 2. The claim — one falsifiable assertion
26
44
 
27
45
  The unit of knowledge is the **claim**, not the document. "Doc A and doc B disagree" is
@@ -63,6 +81,33 @@ the offset span it came from. The extractor **invents nothing**: no labels, no
63
81
  summaries-as-claims, no filling of gaps from model knowledge. What the source does not
64
82
  assert does not become a row — it may become a `gaps:` entry on the topic.
65
83
 
84
+ **Cover the gates, not only the powers.** "One row per assertion" is obeyable and still
85
+ leaves the ledger optimistic: reading a capability paragraph, an extractor emits the
86
+ capability and moves on, because that is what the paragraph asserts. So for every row
87
+ saying what the subject **can do**, ask the source what must hold first — a documented
88
+ default-off state, a licence or edition gate, a firmware or version floor, something
89
+ that must be enabled elsewhere, a dependency on another construct. Each one the source
90
+ states is **its own row, with its own span**.
91
+
92
+ The failure this prevents is an answer that is true and unusable: *"yes, supported —
93
+ three verified rows"* is a plan that fails on site when the function ships disabled. On
94
+ a corpus whose purpose is to deflate over-promising, an extraction axis that records
95
+ powers and skips gates inverts the point.
96
+
97
+ **The rule is _ask_, never _produce_.** A source stating no precondition yields no
98
+ precondition row — the paragraph above still governs. An invented gate is the same
99
+ defect as an invented capability, and a gate you suspect but cannot locate is a `gaps:`
100
+ entry, not a claim. Nothing mechanical can enforce this (no validator detects an
101
+ assertion that was never extracted), which is why it carries no column and no check:
102
+ it is verified at the ingestion review (`review.md`), and it is stated here so the
103
+ extractor knows it before the reviewer does.
104
+
105
+ **Anchor across line wraps.** A PDF extraction breaks phrases mid-line, so a multi-word
106
+ anchor must match a literal space as `\s+` or it silently finds nothing — and a probe
107
+ that pretty-prints collapsed whitespace will show you the phrase intact while the
108
+ checker sees the break. `sdlc_check.py anchor` does this for you; hand-authoring a
109
+ locator without it is how two generation rounds get lost.
110
+
66
111
  Extraction is read-only on the corpus and blind to the graph: rows go to placement
67
112
  (`taxonomy.md`) afterwards. Ingesting a document set is **L3 by Rule Zero**; the
68
113
  ingestion plan derives from the ANALYSIS Action Plan, one task per source, each task's
@@ -435,6 +435,65 @@ def kb_check_locator(target, loc, where, errors):
435
435
  "(p=<n>@<a>-<b> / L<a>-<b> / Sheet<s>!<cell>)" % (where, loc))
436
436
 
437
437
 
438
+ def kb_anchor_pattern(phrase, ignore_case=False):
439
+ """A phrase, compiled so it survives the line wraps an extraction introduces.
440
+
441
+ Every run of whitespace in the phrase becomes `\\s+`: a PDF extraction breaks
442
+ phrases mid-line, so a literal space matches nothing while a probe that
443
+ pretty-prints collapsed whitespace shows the phrase intact. That gap cost a
444
+ field user two full generation rounds."""
445
+ tokens = [t for t in re.split(r"\s+", phrase.strip()) if t]
446
+ if not tokens:
447
+ return None
448
+ body = r"\s+".join(re.escape(t) for t in tokens)
449
+ return re.compile(body, re.IGNORECASE if ignore_case else 0)
450
+
451
+
452
+ def kb_resolve_anchor(target, phrase, ignore_case=False, page=None):
453
+ """Locate a phrase in stored bytes and return [(locator, context), ...].
454
+
455
+ The stored form decides the locator form: form-feeds mean paged bytes and
456
+ offsets are counted inside the page, exactly as `kb_check_locator` reads
457
+ them; otherwise line numbers. Every locator produced here is re-verified with
458
+ that same checker before it is returned, so this can never emit a span its
459
+ own validator would reject."""
460
+ rx = kb_anchor_pattern(phrase, ignore_case)
461
+ if rx is None:
462
+ return []
463
+ # Read what the checker will read: for a non-text original the stored
464
+ # extraction beside it holds the bytes offsets address. `target` stays as
465
+ # given, because that is the path the claim's `source` cell will carry and
466
+ # therefore the path the round-trip below must verify.
467
+ ext = target if target.suffix == ".txt" else target.with_suffix(".txt")
468
+ text = sdlc_core.read_text(ext if ext.is_file() else target)
469
+ hits = []
470
+ if "\f" in text:
471
+ for idx, body in enumerate(text.split("\f"), start=1):
472
+ if page is not None and idx != page:
473
+ continue
474
+ for m in rx.finditer(body):
475
+ hits.append(("p=%d@%d-%d" % (idx, m.start(), m.end()),
476
+ body[max(0, m.start() - 40):m.end() + 40]))
477
+ else:
478
+ lines = text.splitlines()
479
+ starts, pos = [], 0
480
+ for ln in lines:
481
+ starts.append(pos)
482
+ pos += len(ln) + 1
483
+ for m in rx.finditer(text):
484
+ a = sum(1 for s in starts if s <= m.start())
485
+ b = sum(1 for s in starts if s <= m.end() - 1)
486
+ hits.append(("L%d-%d" % (a, b),
487
+ text[max(0, m.start() - 40):m.end() + 40]))
488
+ verified = []
489
+ for loc, ctx in hits:
490
+ errs = []
491
+ kb_check_locator(target, loc, "anchor", errs)
492
+ if not errs:
493
+ verified.append((loc, ctx))
494
+ return verified
495
+
496
+
438
497
  # ---------------------------------------------------------------- topic graph
439
498
  # F-024. Findings only; the graph is held in memory, rebuilt per run.
440
499
 
@@ -677,7 +736,8 @@ def kb_sha256_bytes(path):
677
736
 
678
737
  # ------------------------------------------------------------------ commands
679
738
 
680
- INTERCEPTED = {"index", "validate", "check", "graph", "corpus", "claim-id"}
739
+ INTERCEPTED = {"index", "validate", "check", "graph", "corpus", "claim-id",
740
+ "anchor"}
681
741
 
682
742
 
683
743
  def _kb_root(args):
@@ -798,8 +858,80 @@ def kb_cmd_claim_id(args):
798
858
  return 0
799
859
 
800
860
 
861
+ def kb_cmd_anchor(args):
862
+ """Prose citation -> a verified span. The half `claim-id` never had.
863
+
864
+ The path may be given as it appears in a claim's `source` cell
865
+ (`corpus/given/x-ab12cd34.txt`): when it does not resolve from the current
866
+ directory it is retried under the docs root, so the command works from
867
+ anywhere in the project instead of only from inside `ai_docs/`. Reported
868
+ from the field as an asymmetry with `graph`/`corpus`/`check`, which take
869
+ `--root`; those scan a tree, this one takes a path, and the fix is to make
870
+ the path resolve rather than to document where to stand."""
871
+ p = Path(args.path)
872
+ if not p.is_file():
873
+ try:
874
+ _, docs = _kb_root(args)
875
+ if (docs / args.path).is_file():
876
+ p = docs / args.path
877
+ except sdlc_core.AmbiguousDocsRoot:
878
+ pass
879
+ if not p.is_file():
880
+ print("[ERROR] no such file: %s" % args.path)
881
+ print(" looked from the current directory and under the docs "
882
+ "root; give the path as the claim's `source` cell carries it.")
883
+ return 2
884
+ hits = kb_resolve_anchor(p, args.phrase, ignore_case=args.ignore_case,
885
+ page=args.page)
886
+ if not hits:
887
+ print("[ERROR] phrase not found in the stored bytes of %s" % p)
888
+ print(" nothing written: a locator you cannot verify is worse "
889
+ "than no locator.")
890
+ return 2
891
+ if len(hits) > 1 and not args.all:
892
+ print("[ERROR] %d matches -- ambiguous, so nothing is emitted. Narrow "
893
+ "the phrase, add --page, or pass --all to see them:" % len(hits))
894
+ for loc, ctx in hits:
895
+ print(" %s" % loc)
896
+ return 2
897
+ for loc, ctx in hits:
898
+ print(loc)
899
+ print(" ...%s..." % " ".join(ctx.split()))
900
+ return 0
901
+
902
+
903
+ def kb_cmd_help():
904
+ """The spine's usage, then the overlay's own commands.
905
+
906
+ Forward-by-default sends everything the overlay does not intercept to the
907
+ spine -- including `--help`, whose usage line then lists the spine's nine
908
+ commands and nothing else. A reader concludes the knowledge overlay is not
909
+ installed; it is, and every command below works. Only `-h/--help` AT argv[0]
910
+ lands here, so dispatch is otherwise untouched and a future spine command
911
+ still reaches the spine."""
912
+ try:
913
+ sdlc_core.main(["--help"])
914
+ except SystemExit:
915
+ pass
916
+ print("""
917
+ knowledge overlay (kb-agentic) -- also available:
918
+ graph topic-graph integrity: placement, edges, cycles
919
+ corpus corpus integrity: digests, supersession, notes
920
+ claim-id <path> <locator> compute a claim id (--fill to fill a whole table)
921
+ anchor <path> <phrase> resolve a phrase to a verified locator span
922
+
923
+ index / validate / check the spine's behaviour PLUS the claim ledger and
924
+ the topic graph""")
925
+ return 0
926
+
927
+
801
928
  def main(argv=None):
802
929
  argv = list(sys.argv[1:] if argv is None else argv)
930
+ # `--help` is the one flag the overlay must answer for itself: forwarded, it
931
+ # renders the spine's usage and hides every command above (field report,
932
+ # 2026-08-02). Intercepted at argv[0] ONLY.
933
+ if argv and argv[0] in ("-h", "--help"):
934
+ return kb_cmd_help()
803
935
  # Forward-by-default: anything not intercepted goes to the spine untouched.
804
936
  # Never a hand-copied command tuple - that is how a spine command gets
805
937
  # silently dropped (mkt_check.py ships that exact defect with `migrate`).
@@ -822,9 +954,19 @@ def main(argv=None):
822
954
  p.add_argument("locator", nargs="?")
823
955
  p.add_argument("--qty")
824
956
  p.add_argument("--fill", action="store_true")
957
+ p = sub.add_parser("anchor")
958
+ p.add_argument("path")
959
+ p.add_argument("phrase")
960
+ p.add_argument("--root")
961
+ p.add_argument("--docs-dir")
962
+ p.add_argument("--page", type=int)
963
+ p.add_argument("--ignore-case", action="store_true")
964
+ p.add_argument("--all", action="store_true")
825
965
  args = ap.parse_args(argv)
826
966
  if args.cmd == "claim-id":
827
967
  return kb_cmd_claim_id(args)
968
+ if args.cmd == "anchor":
969
+ return kb_cmd_anchor(args)
828
970
  try:
829
971
  root, docs = _kb_root(args)
830
972
  except sdlc_core.AmbiguousDocsRoot as e:
@@ -253,10 +253,10 @@ end_date:
253
253
  what makes a verdict falsifiable. A question, not a form: when every concept
254
254
  plainly has its owner, one line under this heading answers it. -->
255
255
 
256
- | Capability | Verdict | Component / gap | Evidence |
256
+ | Capability | Verdict | Owning node / gap | Evidence |
257
257
  |---|---|---|---|
258
- | persist an order | EXISTS | `path/to/store.py#OrderStore` | re-read `save()`: durable, returns the id |
259
- | notify the customer | MISSING | — | grep notify/alert/dispatch + send, over src/ and legacy/; no owner |
258
+ | scoping authorizations per company | EXISTS | `topics/multi-company.md`, `owns: [multi-company/scoping]` | descended the index from `access-control`; re-read `owns:` — the concept is claimed there, not in `topics/operators.md` |
259
+ | the licence tiers that gate it | MISSING | — | descended `pricing`, `licensing`, `editions`; synonyms tried: edition, tier, SKU. No node owns it and no source asserts it → a `gaps:` entry, not a claim |
260
260
 
261
261
  ## Impact
262
262
  <!-- existing files touched, APIs/contracts, performance, new dependencies.
@@ -548,7 +548,7 @@ status: CURRENT
548
548
 
549
549
  ```markdown
550
550
  ---
551
- sha256: <raw-byte digest of the original — NOT the LF-normalized text digest>
551
+ sha256: <raw-byte digest of the artifact this sidecar names — NOT the LF-normalized text digest>
552
552
  date: 2026-08-01
553
553
  provenance: GIVEN
554
554
  supersedes: contract-1a2b3c4d.pdf
@@ -561,6 +561,24 @@ Handed over by <who>, <context in one line>.
561
561
  without it the two content-addressed files are unrelated. `extractor:` pins the stored
562
562
  canonical extraction (`<name>-<hash8>.txt`) that offset locators address.
563
563
 
564
+ **Extraction-as-artifact** (`distillation.md` §1 — the variant for a large binary
565
+ corpus): when the extraction IS the artifact and the original was never copied in, two
566
+ more fields record where it came from.
567
+
568
+ ```markdown
569
+ ---
570
+ sha256: <digest of THIS extraction — enforced, these are the bytes locators address>
571
+ date: 2026-08-02
572
+ provenance: GIVEN
573
+ extractor: pdftotext 24.02, form-feed page breaks, whitespace collapsed
574
+ original_path: /vault/manuals/xyz.pdf
575
+ original_sha256: <digest at ingest — RECORDED, never checked: we do not hold the file>
576
+ ---
577
+ ```
578
+
579
+ Keep `original_sha256`'s limit visible wherever it is written: it lets a human
580
+ re-verify by hand and it dates the ingest, and it detects nothing on its own.
581
+
564
582
  ## ai_docs/corpus/notes/RULING_[topic]_[date].md (practitioner ruling)
565
583
 
566
584
  ```markdown