@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 +4 -4
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/skills/kb-agentic-skill/SKILL.md +12 -5
- package/skills/kb-agentic-skill/distillation.md +47 -2
- package/skills/kb-agentic-skill/scripts/sdlc_check.py +143 -1
- package/skills/kb-agentic-skill/templates.md +22 -4
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**:
|
|
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
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kb-agentic-skill",
|
|
3
|
-
"version": "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.
|
|
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** |
|
|
47
|
-
| **L2 -
|
|
48
|
-
| **L3 -
|
|
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:
|
|
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) |
|
|
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 (`<
|
|
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 |
|
|
256
|
+
| Capability | Verdict | Owning node / gap | Evidence |
|
|
257
257
|
|---|---|---|---|
|
|
258
|
-
|
|
|
259
|
-
|
|
|
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
|
|
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
|