@antoneeo/kb-agentic-skill 1.4.7 → 1.5.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 +140 -0
- package/README.md +1 -1
- package/gemini-extension.json +6 -6
- package/package.json +51 -51
- package/scripts/init.js +123 -6
- package/scripts/lib.js +409 -202
- package/skills/kb-agentic-skill/ENFORCEMENT.md +33 -2
- package/skills/kb-agentic-skill/SKILL.md +148 -148
- package/skills/kb-agentic-skill/distillation.md +39 -7
- package/skills/kb-agentic-skill/scripts/sdlc_check.py +189 -15
- package/skills/kb-agentic-skill/templates.md +28 -2
|
@@ -31,6 +31,8 @@ Pure stdlib. ASCII output only (Windows-console safe).
|
|
|
31
31
|
"""
|
|
32
32
|
import argparse
|
|
33
33
|
import hashlib
|
|
34
|
+
import ntpath
|
|
35
|
+
import posixpath
|
|
34
36
|
import re
|
|
35
37
|
import sys
|
|
36
38
|
from pathlib import Path
|
|
@@ -272,10 +274,94 @@ def kb_fill_ids(text):
|
|
|
272
274
|
|
|
273
275
|
|
|
274
276
|
def _note_frontmatter(root, rel):
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
"""The frontmatter that DECLARES the cited file, and the path it came from.
|
|
278
|
+
|
|
279
|
+
A corpus artifact carries none of its own: `given/x.txt` is bytes, and what
|
|
280
|
+
says how those bytes were obtained is the `x.txt.meta.md` sidecar beside it.
|
|
281
|
+
Resolving the cited path alone returned `{}` for such a file -- which the
|
|
282
|
+
caller cannot tell apart from "resolved, field absent" -- and that made
|
|
283
|
+
DERIVED, RULING and IMPORTED impossible for every claim citing `given/`.
|
|
284
|
+
|
|
285
|
+
Sidecar FIRST when one exists, the cited file otherwise. Not "non-.md ->
|
|
286
|
+
sidecar": a verbatim `.md` source stored in `given/` is declared by its
|
|
287
|
+
sidecar exactly like a `.txt` extraction, and reading its own frontmatter
|
|
288
|
+
would read the SOURCE's, which says nothing about how it was extracted. A
|
|
289
|
+
`corpus/notes/*.md` note has no sidecar and still resolves to its own.
|
|
290
|
+
|
|
291
|
+
Returns `(meta, label)`. The label names the file actually read, so a
|
|
292
|
+
finding can say where it looked instead of sending the reader to the wrong
|
|
293
|
+
file -- the same defect this release fixes in the duplicate-id message.
|
|
294
|
+
"""
|
|
295
|
+
base = sdlc_core.confine_under(root, rel)
|
|
296
|
+
if base is None or base == root.resolve():
|
|
297
|
+
# None: the path escapes the docs root, and the source loop said so.
|
|
298
|
+
# Equal to the root: an empty or dot-only path cell (a source written
|
|
299
|
+
# `#L1-2`, with no file before the locator). Forming a sidecar name
|
|
300
|
+
# from that would step back OUT of the root through `base.parent` and
|
|
301
|
+
# read `<docs-root>.meta.md` -- outside the tree this helper confines.
|
|
302
|
+
return None, rel
|
|
303
|
+
if not base.is_file():
|
|
304
|
+
# The cited file is gone. Its sidecar may still be lying there, but a
|
|
305
|
+
# claim on a missing artifact is already an error from the source loop,
|
|
306
|
+
# and answering from an orphan sidecar would add a second finding about
|
|
307
|
+
# a file that is not there.
|
|
308
|
+
return None, rel
|
|
309
|
+
side = base.parent / (base.name + ".meta.md")
|
|
310
|
+
if side.is_file():
|
|
311
|
+
return (sdlc_core.load_frontmatter(
|
|
312
|
+
sdlc_core.read_text(side).splitlines()) or {}, rel + ".meta.md")
|
|
313
|
+
return (sdlc_core.load_frontmatter(
|
|
314
|
+
sdlc_core.read_text(base).splitlines()) or {}, rel)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _kb_pointer_resolves(p):
|
|
318
|
+
"""A recorded pointer resolves when it names a FILE we can stat.
|
|
319
|
+
|
|
320
|
+
`is_file()` and not `exists()`: `original_path` names a document, so a
|
|
321
|
+
directory that happens to sit at that path is not the original. Any OSError
|
|
322
|
+
is "does not resolve" and never a traceback -- the field points OUTSIDE the
|
|
323
|
+
docs root by design, so the validator has to survive whatever lives there
|
|
324
|
+
(an unreadable parent on a network vault, a name too long, a reparse point).
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
return p.is_file()
|
|
328
|
+
except OSError:
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _kb_original_candidates(op, root):
|
|
333
|
+
"""Every place a recorded `original_path` could legitimately be, in order.
|
|
334
|
+
|
|
335
|
+
`Path.is_absolute()` is NOT the test, and using it was a real defect: on
|
|
336
|
+
Windows a rooted-but-driveless path -- `/vault/manuals/xyz.pdf`, the exact
|
|
337
|
+
form this project's own templates print -- is not absolute, so it was
|
|
338
|
+
joined under the docs root and silently rewritten onto the docs root's
|
|
339
|
+
DRIVE. That produced a warning quoting a path nobody wrote, and could hide
|
|
340
|
+
a genuinely dangling pointer behind a file that happened to exist there.
|
|
341
|
+
Anything EITHER platform calls rooted is now taken as written; only a
|
|
342
|
+
genuinely relative pointer is joined.
|
|
343
|
+
|
|
344
|
+
A relative pointer is tried against the docs root's parent (the project
|
|
345
|
+
root in the standard layout) and against the docs root itself, because
|
|
346
|
+
`--root` and `migrate` both allow a docs root that does not sit directly
|
|
347
|
+
under the project root.
|
|
348
|
+
"""
|
|
349
|
+
raw = (op or "").strip()
|
|
350
|
+
forms = [raw]
|
|
351
|
+
if "\\" in raw:
|
|
352
|
+
# A path authored on Windows and read anywhere. Tried as a SECOND form,
|
|
353
|
+
# never instead of the first: a backslash is a legal character in a
|
|
354
|
+
# POSIX filename, and rewriting it unconditionally invented a path.
|
|
355
|
+
forms.append(raw.replace("\\", "/"))
|
|
356
|
+
out, seen = [], set()
|
|
357
|
+
for f in forms:
|
|
358
|
+
cands = ([Path(f)] if (ntpath.isabs(f) or posixpath.isabs(f))
|
|
359
|
+
else [root.parent / f, root / f])
|
|
360
|
+
for c in cands:
|
|
361
|
+
if str(c) not in seen:
|
|
362
|
+
seen.add(str(c))
|
|
363
|
+
out.append(c)
|
|
364
|
+
return out
|
|
279
365
|
|
|
280
366
|
|
|
281
367
|
def kb_check_claims(root):
|
|
@@ -287,6 +373,15 @@ def kb_check_claims(root):
|
|
|
287
373
|
return errors, warnings, notes
|
|
288
374
|
all_ids = {} # id -> "file:line"
|
|
289
375
|
all_rows = {} # id -> (row, rel)
|
|
376
|
+
# Frontmatter is now resolved for every row, GIVEN included, so a ledger
|
|
377
|
+
# citing one artifact from 80 rows would otherwise stat and decode that
|
|
378
|
+
# artifact's sidecar 80 times. Keyed by the cited path, per run.
|
|
379
|
+
fm_cache = {}
|
|
380
|
+
|
|
381
|
+
def _declaring_frontmatter(rel):
|
|
382
|
+
if rel not in fm_cache:
|
|
383
|
+
fm_cache[rel] = _note_frontmatter(root, rel)
|
|
384
|
+
return fm_cache[rel]
|
|
290
385
|
per_file_rows = []
|
|
291
386
|
for p in sorted(topics.glob("*.md")):
|
|
292
387
|
rel = "topics/" + p.name
|
|
@@ -320,25 +415,46 @@ def kb_check_claims(root):
|
|
|
320
415
|
kb_check_locator(target, loc, where, errors)
|
|
321
416
|
# --- provenance ---
|
|
322
417
|
prov = row["prov"]
|
|
418
|
+
# Resolved ONCE, for every provenance: the non-GIVEN classes read it
|
|
419
|
+
# for their required field, and GIVEN reads it to notice that the
|
|
420
|
+
# artifact declares a weaker chain than the row claims.
|
|
421
|
+
meta, meta_where = (None, "")
|
|
422
|
+
if "#" in first:
|
|
423
|
+
meta, meta_where = _declaring_frontmatter(first.rsplit("#", 1)[0])
|
|
323
424
|
if prov not in PROVENANCES:
|
|
324
425
|
errors.append("%s: prov %r not in %s" % (where, prov, "/".join(PROVENANCES)))
|
|
325
426
|
elif prov in ("DERIVED", "RULING", "ELICITED", "IMPORTED"):
|
|
326
|
-
meta = _note_frontmatter(root, first.rsplit("#", 1)[0])
|
|
327
427
|
if meta is None:
|
|
328
428
|
pass # unresolvable source already reported
|
|
329
429
|
elif prov == "DERIVED" and not meta.get("derived_from"):
|
|
330
|
-
errors.append("%s: DERIVED claim's
|
|
331
|
-
"— model knowledge disguised as
|
|
430
|
+
errors.append("%s: DERIVED claim's source (%s) carries no "
|
|
431
|
+
"'derived_from:' — model knowledge disguised as "
|
|
432
|
+
"a source" % (where, meta_where))
|
|
332
433
|
elif prov == "RULING" and not meta.get("basis"):
|
|
333
|
-
errors.append("%s: RULING
|
|
334
|
-
"is not a fact; no basis, no ruling"
|
|
434
|
+
errors.append("%s: RULING source (%s) carries no 'basis:' — a "
|
|
435
|
+
"preference is not a fact; no basis, no ruling"
|
|
436
|
+
% (where, meta_where))
|
|
335
437
|
elif prov == "IMPORTED" and not meta.get("imported_from"):
|
|
336
438
|
# F-030: IMPORTED exists so a foreign decision cannot pass for
|
|
337
439
|
# a local one. Without the origin the class says nothing and
|
|
338
440
|
# the row is a RULING with the label filed off.
|
|
339
|
-
errors.append("%s: IMPORTED
|
|
340
|
-
"the class exists to name
|
|
341
|
-
"was; unnamed, it is a
|
|
441
|
+
errors.append("%s: IMPORTED source (%s) carries no "
|
|
442
|
+
"'imported_from:' — the class exists to name "
|
|
443
|
+
"whose decision this was; unnamed, it is a "
|
|
444
|
+
"RULING in disguise" % (where, meta_where))
|
|
445
|
+
elif prov == "GIVEN":
|
|
446
|
+
# F-035: a row resting on an OCR, a transcription or a
|
|
447
|
+
# translation is not the same evidence as one resting on a
|
|
448
|
+
# deterministic text layer. The sidecar could say so only in
|
|
449
|
+
# prose, and prose is not a check -- which is how three rows
|
|
450
|
+
# whose evidence was a reading of an image passed as GIVEN.
|
|
451
|
+
declared = ((meta or {}).get("provenance") or "").strip()
|
|
452
|
+
if declared and declared.upper() != "GIVEN":
|
|
453
|
+
warnings.append(
|
|
454
|
+
"%s: prov GIVEN, but %s declares 'provenance: %s' — the "
|
|
455
|
+
"row reads as first-hand evidence and its artifact does "
|
|
456
|
+
"not. File the row at the provenance the chain actually "
|
|
457
|
+
"has, or correct the sidecar" % (where, meta_where, declared))
|
|
342
458
|
# --- grammar cells ---
|
|
343
459
|
try:
|
|
344
460
|
kb_parse_scope(row["valid"])
|
|
@@ -368,9 +484,49 @@ def kb_check_claims(root):
|
|
|
368
484
|
"moved silently" % (where, row["id"], expect))
|
|
369
485
|
if row["id"]:
|
|
370
486
|
if row["id"] in all_ids:
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
487
|
+
# F-035: one message served two different defects. Same id
|
|
488
|
+
# with the SAME text is a copied row. Same id with DIFFERENT
|
|
489
|
+
# text is a collision: kb_claim_id hashes path#locator#qty
|
|
490
|
+
# and excludes the text on purpose, so two distinct
|
|
491
|
+
# assertions about one span cannot be told apart. The source
|
|
492
|
+
# and qty do NOT discriminate -- the id already implies them.
|
|
493
|
+
prev_row = all_rows[row["id"]][0]
|
|
494
|
+
|
|
495
|
+
def _cell(r, k):
|
|
496
|
+
return (r[k] or "").strip()
|
|
497
|
+
|
|
498
|
+
def _first_src(r):
|
|
499
|
+
return (r["source"] or "").split(";")[0].strip()
|
|
500
|
+
|
|
501
|
+
same_text = _cell(prev_row, "claim") == _cell(row, "claim")
|
|
502
|
+
same_span = (_first_src(prev_row) == _first_src(row)
|
|
503
|
+
and _cell(prev_row, "qty") == _cell(row, "qty"))
|
|
504
|
+
if same_text:
|
|
505
|
+
errors.append("%s: duplicate id %s (also at %s) — uniqueness "
|
|
506
|
+
"is global across topics/" % (where, row["id"],
|
|
507
|
+
all_ids[row["id"]]))
|
|
508
|
+
elif same_span:
|
|
509
|
+
errors.append(
|
|
510
|
+
"%s: id %s collides with the row at %s — two "
|
|
511
|
+
"DIFFERENT rows cite the same span with the same "
|
|
512
|
+
"qty, and the id function cannot separate them (it "
|
|
513
|
+
"hashes path#locator#qty and excludes the text on "
|
|
514
|
+
"purpose). Widen one locator to the span that "
|
|
515
|
+
"actually carries its assertion, or merge the two "
|
|
516
|
+
"rows — do not edit the qty to break the tie"
|
|
517
|
+
% (where, row["id"], all_ids[row["id"]]))
|
|
518
|
+
else:
|
|
519
|
+
# Same id, different text AND different span: the id
|
|
520
|
+
# cannot have been computed from both rows, so it was
|
|
521
|
+
# hand-typed or left stale after a source was repointed.
|
|
522
|
+
# Saying "they cite the same span" here would name a
|
|
523
|
+
# cause that is provably not this one.
|
|
524
|
+
errors.append(
|
|
525
|
+
"%s: id %s is also on the row at %s, which cites a "
|
|
526
|
+
"different span — the id was not computed from this "
|
|
527
|
+
"row (hand-typed, or left stale after its source "
|
|
528
|
+
"moved). Run 'sdlc_check.py claim-id --fill %s'"
|
|
529
|
+
% (where, row["id"], all_ids[row["id"]], rel))
|
|
374
530
|
else:
|
|
375
531
|
all_ids[row["id"]] = where
|
|
376
532
|
all_rows[row["id"]] = (row, rel)
|
|
@@ -889,6 +1045,24 @@ def kb_corpus_check(root):
|
|
|
889
1045
|
errors.append("%s: raw-byte digest changed since ingest — "
|
|
890
1046
|
"given/ is never edited; this is the check, "
|
|
891
1047
|
"not a convention" % rel)
|
|
1048
|
+
# F-035: `original_sha256` is not verified because we do not hold
|
|
1049
|
+
# the bytes -- a limit stated out loud in distillation.md. That
|
|
1050
|
+
# reason does not extend to the PATH, which costs one exists(). A
|
|
1051
|
+
# corpus whose premise is "every provenance is a real file" cannot
|
|
1052
|
+
# let 16 sidecars go dangling behind a green run.
|
|
1053
|
+
op = (meta.get("original_path") or "").strip()
|
|
1054
|
+
if op:
|
|
1055
|
+
cands = _kb_original_candidates(op, root)
|
|
1056
|
+
tried = [str(c) for c in cands]
|
|
1057
|
+
resolved = any(_kb_pointer_resolves(c) for c in cands)
|
|
1058
|
+
if not resolved:
|
|
1059
|
+
warnings.append(
|
|
1060
|
+
"%s: original_path %r does not resolve (tried %s) — the "
|
|
1061
|
+
"extraction is intact, the pointer to the original is "
|
|
1062
|
+
"not. A warning and not an error: a bundle carries "
|
|
1063
|
+
"artifacts and sidecars, never the originals, so after "
|
|
1064
|
+
"an import this dangles legitimately"
|
|
1065
|
+
% (rel, op, ", ".join(tried)))
|
|
892
1066
|
sup = (meta.get("supersedes") or "").strip()
|
|
893
1067
|
if sup:
|
|
894
1068
|
superseded.add(sup)
|
|
@@ -668,8 +668,34 @@ original_sha256: <digest at ingest — RECORDED, never checked: we do not hold t
|
|
|
668
668
|
---
|
|
669
669
|
```
|
|
670
670
|
|
|
671
|
-
|
|
672
|
-
|
|
671
|
+
The two fields are **not** in the same position, and writing them as if they were is
|
|
672
|
+
how a dangling pointer survives a green run:
|
|
673
|
+
|
|
674
|
+
- `original_sha256` detects nothing on its own — we do not hold the bytes. It lets a
|
|
675
|
+
human re-verify by hand and it dates the ingest. Keep that limit visible wherever the
|
|
676
|
+
field is written.
|
|
677
|
+
- `original_path` **is** checked for resolution, and warns when it does not resolve
|
|
678
|
+
(never errors — an imported bundle carries no originals). **Write it absolute, or
|
|
679
|
+
relative to the project root**; the validator tries the docs root's parent first and
|
|
680
|
+
the docs root second, and names both in the warning. The convention was implicit
|
|
681
|
+
until F-035 and is stated here because a pointer nobody can resolve is worth less
|
|
682
|
+
than no pointer at all.
|
|
683
|
+
|
|
684
|
+
Write these two values with **no trailing `# comment`**: the frontmatter reader does
|
|
685
|
+
not strip inline comments, so the comment lands inside the value.
|
|
686
|
+
|
|
687
|
+
`provenance:` has a consumer too: a claim row filed `prov: GIVEN` whose artifact's
|
|
688
|
+
sidecar declares anything else warns. Declare the chain the artifact actually has —
|
|
689
|
+
an OCR, a transcription from an image, a translation are not first-hand evidence, and
|
|
690
|
+
saying so in the sidecar's prose is not a check.
|
|
691
|
+
|
|
692
|
+
Two limits of that warning, stated so it is not mistaken for more than it is. It reads
|
|
693
|
+
the row's **first** source, so a weak artifact cited second is not compared. And it can
|
|
694
|
+
only read the **field**: a sidecar that says `provenance: GIVEN` while its prose says
|
|
695
|
+
"transcribed from a photograph" is silent, because prose is not machine-readable — the
|
|
696
|
+
warning catches the author who declared the chain honestly and then filed the row too
|
|
697
|
+
strongly, never the author who declared it wrongly. Nothing requires a `given/` sidecar
|
|
698
|
+
to carry `provenance:` at all; adding that requirement is a new gate, not this one.
|
|
673
699
|
|
|
674
700
|
## ai_docs/corpus/notes/RULING_[topic]_[date].md (practitioner ruling)
|
|
675
701
|
|