bmad-module-skill-forge 3.0.0 → 4.0.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/.claude-plugin/marketplace.json +1 -1
- package/docs/_data/pinned.yaml +1 -1
- package/docs/examples.md +1 -1
- package/docs/troubleshooting.md +8 -0
- package/docs/verifying-a-skill.md +7 -3
- package/docs/workflows.md +3 -3
- package/package.json +1 -1
- package/src/knowledge/qmd-registry.md +1 -1
- package/src/knowledge/version-paths.md +1 -1
- package/src/shared/health-check.md +2 -50
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +2 -2
- package/src/shared/scripts/skf-atomic-write.py +212 -59
- package/src/shared/scripts/skf-build-change-manifest.py +593 -47
- package/src/shared/scripts/skf-classify-changed-files.py +78 -0
- package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
- package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
- package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
- package/src/shared/scripts/skf-extract-public-api.py +1309 -157
- package/src/shared/scripts/skf-extraction-inventory.py +580 -46
- package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
- package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
- package/src/shared/scripts/skf-github-probe.py +47 -8
- package/src/shared/scripts/skf-hash-content.py +14 -8
- package/src/shared/scripts/skf-load-provenance.py +3 -1
- package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
- package/src/shared/scripts/skf-preflight.py +15 -1
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
- package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
- package/src/shared/scripts/skf-structural-diff.py +133 -18
- package/src/shared/scripts/skf-update-active-symlink.py +115 -62
- package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
- package/src/shared/scripts/skf-verify-provenance-completeness.py +977 -49
- package/src/shared/scripts/skf-write-skill-brief.py +41 -8
- package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
- package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
- package/src/skf-audit-skill/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +2 -1
- package/src/skf-audit-skill/references/re-index.md +14 -9
- package/src/skf-audit-skill/references/report.md +2 -2
- package/src/skf-audit-skill/references/semantic-diff.md +15 -15
- package/src/skf-audit-skill/references/structural-diff.md +10 -5
- package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
- package/src/skf-brief-skill/SKILL.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -9
- package/src/skf-brief-skill/references/analyze-target.md +6 -6
- package/src/skf-brief-skill/references/confirm-brief.md +1 -1
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
- package/src/skf-brief-skill/references/version-resolution.md +5 -9
- package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
- package/src/skf-create-skill/assets/skill-sections.md +6 -6
- package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
- package/src/skf-create-skill/references/compile.md +31 -60
- package/src/skf-create-skill/references/component-extraction.md +3 -3
- package/src/skf-create-skill/references/enrich.md +2 -2
- package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
- package/src/skf-create-skill/references/extract.md +22 -19
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
- package/src/skf-create-skill/references/extraction-patterns.md +8 -9
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +16 -5
- package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
- package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
- package/src/skf-drop-skill/references/execute.md +5 -1
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +2 -2
- package/src/skf-drop-skill/references/select.md +7 -3
- package/src/skf-test-skill/references/coherence-check.md +1 -1
- package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
- package/src/skf-test-skill/references/coverage-check.md +39 -57
- package/src/skf-test-skill/references/init.md +3 -5
- package/src/skf-test-skill/references/report.md +10 -8
- package/src/skf-test-skill/references/scoring-rules.md +6 -5
- package/src/skf-test-skill/references/source-access-protocol.md +14 -17
- package/src/skf-test-skill/scripts/build-result-context.py +114 -21
- package/src/skf-test-skill/scripts/gap-ledger.py +315 -30
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +534 -96
- package/src/skf-test-skill/scripts/reconcile-coverage.py +276 -2
- package/src/skf-test-skill/scripts/score-signatures.py +36 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
- package/src/skf-update-skill/references/detect-changes.md +32 -13
- package/src/skf-update-skill/references/gap-driven.md +3 -3
- package/src/skf-update-skill/references/init.md +6 -2
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +2 -2
- package/src/skf-update-skill/references/re-extract.md +2 -2
- package/src/skf-update-skill/references/report.md +3 -1
- package/src/skf-update-skill/references/write.md +43 -23
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
|
@@ -19,7 +19,7 @@ It covers the four branches §2c enumerates:
|
|
|
19
19
|
Missing = barrel_set − documented_set, Stale = documented_set − barrel_set,
|
|
20
20
|
Export Coverage = |Documented| / |barrel_set| * 100.
|
|
21
21
|
|
|
22
|
-
* "scalar" (§2b
|
|
22
|
+
* "scalar" (§2b's scalar branch, stats.effective_denominator, no enumerated name set):
|
|
23
23
|
look each documented name up in SKILL.md ∪ references/*.md;
|
|
24
24
|
Documented = count present, denominator = effective_denominator,
|
|
25
25
|
Missing = max(0, denominator − Documented), Stale not enumerable (empty),
|
|
@@ -98,6 +98,7 @@ Output (stdout, one object):
|
|
|
98
98
|
"denominatorSource": "barrel" | "scalar" | "stack" | "docsOnly",
|
|
99
99
|
"denominator": <int>,
|
|
100
100
|
"documented": <int>,
|
|
101
|
+
"documentedNames": [names], # enumerated only: the documented names the denominator counts
|
|
101
102
|
"missing": [names], # enumerated: source names not documented; others: []
|
|
102
103
|
"missingCount": <int>, # always present; never negative
|
|
103
104
|
"stale": [names], # enumerated: documented names not in source; others: []
|
|
@@ -115,6 +116,43 @@ Exit codes:
|
|
|
115
116
|
0 reconciliation emitted successfully
|
|
116
117
|
1 no input, input that does not parse as JSON, or an input file that cannot be read
|
|
117
118
|
2 input parsed but schema/semantics invalid (error object emitted as JSON)
|
|
119
|
+
|
|
120
|
+
Subcommand render (coverage-check.md §5 writes its output unchanged):
|
|
121
|
+
uv run reconcile-coverage.py render --coverage <coverage.json>
|
|
122
|
+
[--surface <surface.json>] [--signatures <signatures.json>]
|
|
123
|
+
|
|
124
|
+
prints the Markdown of the Coverage Analysis section that follows its
|
|
125
|
+
header lines, from the run files:
|
|
126
|
+
**Files Analyzed:** the distinct files the --surface export records name
|
|
127
|
+
(N/A without --surface: a docs-only run has none)
|
|
128
|
+
### Export Coverage on the enumerated branch, one row per name the
|
|
129
|
+
denominator counts, per stale name and per name whose
|
|
130
|
+
signature mismatches, by name:
|
|
131
|
+
Type the first kind the surface records for the name, else -
|
|
132
|
+
Documented no for a missing name, else yes
|
|
133
|
+
Signature no for a mismatch, yes for a compared name that
|
|
134
|
+
matches, unverified for a documented name when no
|
|
135
|
+
--signatures was given, - otherwise
|
|
136
|
+
Source Location the file:line the surface records for the name, a
|
|
137
|
+
record with a line preferred (gap-ledger.py's rule
|
|
138
|
+
for a missing export's Source), else -
|
|
139
|
+
Status FAIL for a signature mismatch, WARN for a missing or
|
|
140
|
+
stale name, PASS otherwise
|
|
141
|
+
The scalar and stack branches count without naming
|
|
142
|
+
the exports, and a docs-only run has no source
|
|
143
|
+
surface: one line says so instead (NO_TABLE).
|
|
144
|
+
### Coverage Summary the --coverage counts, the --signatures mismatch count
|
|
145
|
+
and warnings, the numerator surplus when the ratio
|
|
146
|
+
was capped, and the names the --surface leaves out
|
|
147
|
+
or puts back: `excluded.outsideScope` (scoped out
|
|
148
|
+
by the brief), `excluded.nestedEntries` (a nested
|
|
149
|
+
Python entry point's names) and
|
|
150
|
+
`guards.staleScope.restored`, each line only when
|
|
151
|
+
it names one
|
|
152
|
+
### Category Scores Export Coverage, and Signature Accuracy and Type
|
|
153
|
+
Coverage from --signatures (N/A without it)
|
|
154
|
+
Exit 0 when it printed; 1 when an input cannot be read or is not the
|
|
155
|
+
result of the script that writes it (one line on stderr); 2 a usage error.
|
|
118
156
|
"""
|
|
119
157
|
|
|
120
158
|
from __future__ import annotations
|
|
@@ -286,6 +324,7 @@ def reconcile(inp, doc_text=None):
|
|
|
286
324
|
"denominatorSource": source,
|
|
287
325
|
"denominator": denominator,
|
|
288
326
|
"documented": documented,
|
|
327
|
+
"documentedNames": sorted(documented_names),
|
|
289
328
|
"missing": missing,
|
|
290
329
|
"missingCount": len(missing),
|
|
291
330
|
"stale": stale,
|
|
@@ -350,6 +389,235 @@ def reconcile(inp, doc_text=None):
|
|
|
350
389
|
return result
|
|
351
390
|
|
|
352
391
|
|
|
392
|
+
# --- The Coverage Analysis section (render) -----------------------------------
|
|
393
|
+
|
|
394
|
+
BRANCHES = ("enumerated", "scalar", "stack", "docsOnly")
|
|
395
|
+
# Why a branch names no export: the line it writes in place of the table, and its stale count.
|
|
396
|
+
NO_NAMES = {
|
|
397
|
+
"scalar": "the scalar denominator (`stats.effective_denominator`) counts the exports without naming them",
|
|
398
|
+
"stack": "the stack denominator counts the composition surface without naming each export",
|
|
399
|
+
"docsOnly": "a docs-only run has no source surface",
|
|
400
|
+
}
|
|
401
|
+
NO_TABLE = {
|
|
402
|
+
"scalar": f"No per-export table: {NO_NAMES['scalar']}. The summary below gives the counts.",
|
|
403
|
+
"stack": f"No per-export table: {NO_NAMES['stack']}. The summary below gives the counts.",
|
|
404
|
+
"docsOnly": f"No per-export table: {NO_NAMES['docsOnly']} to compare against. The summary below gives its "
|
|
405
|
+
"documentation completeness.",
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
_GAP_LEDGER_MODULE = None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class RenderError(Exception):
|
|
412
|
+
"""A render input that is not the result of the script that writes it."""
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _gap_ledger():
|
|
416
|
+
"""gap-ledger.py beside this script: its rule for a missing export's Source gives the Source Location."""
|
|
417
|
+
global _GAP_LEDGER_MODULE
|
|
418
|
+
if _GAP_LEDGER_MODULE is None:
|
|
419
|
+
sibling = Path(__file__).resolve().parent / "gap-ledger.py"
|
|
420
|
+
spec = importlib.util.spec_from_file_location("skf_gap_ledger_render", sibling)
|
|
421
|
+
if spec is None or spec.loader is None or not sibling.is_file():
|
|
422
|
+
raise RenderError(f"gap-ledger.py not found beside {Path(__file__).name}")
|
|
423
|
+
module = importlib.util.module_from_spec(spec)
|
|
424
|
+
spec.loader.exec_module(module)
|
|
425
|
+
_GAP_LEDGER_MODULE = module
|
|
426
|
+
return _GAP_LEDGER_MODULE
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _pct(value):
|
|
430
|
+
"""A score as the report writes it.
|
|
431
|
+
|
|
432
|
+
>>> _pct(60.0), _pct(66.67), _pct(None)
|
|
433
|
+
('60%', '66.67%', 'N/A')
|
|
434
|
+
"""
|
|
435
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
436
|
+
return "N/A"
|
|
437
|
+
return f"{int(value)}%" if float(value).is_integer() else f"{value}%"
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _cell(text):
|
|
441
|
+
"""A table cell: a `|` would end it."""
|
|
442
|
+
return str(text).replace("|", "\\|")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _string_list(data, key, label):
|
|
446
|
+
value = data.get(key)
|
|
447
|
+
if value is None:
|
|
448
|
+
return []
|
|
449
|
+
if not isinstance(value, list):
|
|
450
|
+
raise RenderError(f"{label}: `{key}` is not a list")
|
|
451
|
+
return [v for v in value if isinstance(v, str) and v]
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _incomplete(item):
|
|
455
|
+
"""A docs-only item and the fields it lacks.
|
|
456
|
+
|
|
457
|
+
>>> _incomplete({"name": "load", "kind": "function", "missing": ["params", "return_type"]})
|
|
458
|
+
'`load` (function) lacks `params`, `return_type`'
|
|
459
|
+
"""
|
|
460
|
+
fields = ", ".join(f"`{field}`" for field in item.get("missing") or [])
|
|
461
|
+
return f"`{item.get('name')}` ({item.get('kind')}) lacks {fields}"
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _left_out(surface):
|
|
465
|
+
"""The Coverage Summary lines for the names the surface leaves out or puts back, each only when it names one.
|
|
466
|
+
|
|
467
|
+
>>> for line in _left_out({
|
|
468
|
+
... "excluded": {"outsideScope": [{"name": "gen", "file": "src/gen.ts"}],
|
|
469
|
+
... "nestedEntries": [{"entry": "pkg/api/__init__.py", "within": "pkg/__init__.py",
|
|
470
|
+
... "names": ["router"]}]},
|
|
471
|
+
... "guards": {"staleScope": {"restored": [{"name": "old", "file": "src/old.ts", "line": 4}]}}}):
|
|
472
|
+
... print(line)
|
|
473
|
+
- **Scoped Out by the Brief:** `gen` (src/gen.ts)
|
|
474
|
+
- **Nested Entry Points:** `pkg/api/__init__.py` inside `pkg/__init__.py`: `router`
|
|
475
|
+
- **Restored by the Stale-Scope Guard:** `old` (src/old.ts:4)
|
|
476
|
+
>>> _left_out({"excluded": {"outsideScope": [], "nestedEntries": []}, "guards": None})
|
|
477
|
+
[]
|
|
478
|
+
"""
|
|
479
|
+
def items(value):
|
|
480
|
+
return [i for i in value if isinstance(i, dict)] if isinstance(value, list) else []
|
|
481
|
+
|
|
482
|
+
def where(item):
|
|
483
|
+
file, line = item.get("file"), item.get("line")
|
|
484
|
+
if not isinstance(file, str) or not file:
|
|
485
|
+
return ""
|
|
486
|
+
return f" ({file}:{line})" if isinstance(line, int) and not isinstance(line, bool) else f" ({file})"
|
|
487
|
+
|
|
488
|
+
excluded = surface.get("excluded") if isinstance(surface.get("excluded"), dict) else {}
|
|
489
|
+
guards = surface.get("guards") if isinstance(surface.get("guards"), dict) else {}
|
|
490
|
+
stale_scope = guards.get("staleScope") if isinstance(guards.get("staleScope"), dict) else {}
|
|
491
|
+
lines = []
|
|
492
|
+
scoped = [f"`{i['name']}`{where(i)}" for i in items(excluded.get("outsideScope"))
|
|
493
|
+
if isinstance(i.get("name"), str)]
|
|
494
|
+
if scoped:
|
|
495
|
+
lines.append(f"- **Scoped Out by the Brief:** {', '.join(scoped)}")
|
|
496
|
+
nested = [f"`{i['entry']}` inside `{i.get('within')}`: "
|
|
497
|
+
+ ", ".join(f"`{n}`" for n in i.get("names") or [] if isinstance(n, str))
|
|
498
|
+
for i in items(excluded.get("nestedEntries")) if isinstance(i.get("entry"), str)]
|
|
499
|
+
if nested:
|
|
500
|
+
lines.append(f"- **Nested Entry Points:** {'; '.join(nested)}")
|
|
501
|
+
restored = [f"`{i['name']}`{where(i)}" for i in items(stale_scope.get("restored"))
|
|
502
|
+
if isinstance(i.get("name"), str)]
|
|
503
|
+
if restored:
|
|
504
|
+
lines.append(f"- **Restored by the Stale-Scope Guard:** {', '.join(restored)}")
|
|
505
|
+
return lines
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _rows(coverage, surface, signatures):
|
|
509
|
+
"""The Export Coverage table of the enumerated branch."""
|
|
510
|
+
if "documentedNames" not in coverage:
|
|
511
|
+
raise RenderError("--coverage has no `documentedNames`: write it again with this reconcile-coverage.py")
|
|
512
|
+
documented = set(_string_list(coverage, "documentedNames", "--coverage"))
|
|
513
|
+
missing = set(_string_list(coverage, "missing", "--coverage"))
|
|
514
|
+
stale = set(_string_list(coverage, "stale", "--coverage"))
|
|
515
|
+
mismatched, compared = set(), set()
|
|
516
|
+
if signatures is not None:
|
|
517
|
+
mismatched = {m["name"] for m in signatures.get("mismatches") or []
|
|
518
|
+
if isinstance(m, dict) and isinstance(m.get("name"), str)}
|
|
519
|
+
compared = set(_string_list(signatures, "comparedNames", "--signatures"))
|
|
520
|
+
kinds, sources = {}, {}
|
|
521
|
+
if surface is not None:
|
|
522
|
+
for record in surface["exports"]:
|
|
523
|
+
if isinstance(record, dict) and isinstance(record.get("name"), str) and record.get("kind"):
|
|
524
|
+
kinds.setdefault(record["name"], str(record["kind"]))
|
|
525
|
+
sources = _gap_ledger()._surface_sources(surface)
|
|
526
|
+
rows = ["| Export | Type | Documented | Signature | Source Location | Status |",
|
|
527
|
+
"|--------|------|------------|-----------|-----------------|--------|"]
|
|
528
|
+
for name in sorted(documented | missing | stale | mismatched):
|
|
529
|
+
if name in mismatched:
|
|
530
|
+
signature = "no"
|
|
531
|
+
elif name in compared:
|
|
532
|
+
signature = "yes"
|
|
533
|
+
elif signatures is None and name not in missing:
|
|
534
|
+
signature = "unverified"
|
|
535
|
+
else:
|
|
536
|
+
signature = "-"
|
|
537
|
+
status = "FAIL" if name in mismatched else "WARN" if name in missing or name in stale else "PASS"
|
|
538
|
+
rows.append(f"| `{_cell(name)}` | {_cell(kinds.get(name, '-'))} | {'no' if name in missing else 'yes'} "
|
|
539
|
+
f"| {signature} | {_cell(sources.get(name, '-'))} | {status} |")
|
|
540
|
+
return rows
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def render(coverage, surface=None, signatures=None):
|
|
544
|
+
"""The Coverage Analysis section after its header lines, as Markdown (see the module docstring)."""
|
|
545
|
+
if not isinstance(coverage, dict) or coverage.get("branch") not in BRANCHES:
|
|
546
|
+
raise RenderError("--coverage is not reconcile-coverage.py's result (no known `branch`)")
|
|
547
|
+
if surface is not None and not (isinstance(surface, dict) and isinstance(surface.get("exports"), list)):
|
|
548
|
+
raise RenderError("--surface is not load-coverage-inputs.py surface's result (no `exports` list)")
|
|
549
|
+
if signatures is not None and not (isinstance(signatures, dict) and signatures.get("valid") is True):
|
|
550
|
+
raise RenderError("--signatures is not score-signatures.py score's result (no `valid: true`)")
|
|
551
|
+
branch = coverage["branch"]
|
|
552
|
+
docs_only = branch == "docsOnly"
|
|
553
|
+
|
|
554
|
+
if surface is None:
|
|
555
|
+
files = "N/A: no source surface"
|
|
556
|
+
else:
|
|
557
|
+
files = len({r["file"] for r in surface["exports"]
|
|
558
|
+
if isinstance(r, dict) and isinstance(r.get("file"), str) and r["file"]})
|
|
559
|
+
lines = [f"**Files Analyzed:** {files}", "", "### Export Coverage", ""]
|
|
560
|
+
lines += _rows(coverage, surface, signatures) if branch == "enumerated" else [NO_TABLE[branch]]
|
|
561
|
+
|
|
562
|
+
denominator, documented = coverage.get("denominator"), coverage.get("documented")
|
|
563
|
+
missing_count, coverage_pct = coverage.get("missingCount"), _pct(coverage.get("exportCoverage"))
|
|
564
|
+
lines += ["", "### Coverage Summary", ""]
|
|
565
|
+
if docs_only:
|
|
566
|
+
incomplete = [_incomplete(item) for item in coverage.get("incomplete") or [] if isinstance(item, dict)]
|
|
567
|
+
lines += [f"- **Exports Found:** {denominator} documented items",
|
|
568
|
+
f"- **Documented:** {documented} complete ({coverage_pct})",
|
|
569
|
+
f"- **Missing Documentation:** {missing_count} incomplete"
|
|
570
|
+
+ (f": {'; '.join(incomplete)}" if incomplete else "")]
|
|
571
|
+
else:
|
|
572
|
+
capped = coverage.get("coverageCapped")
|
|
573
|
+
floored = " (a floored residual: the numerator exceeds the denominator)" if capped else ""
|
|
574
|
+
lines += [f"- **Exports Found:** {denominator}",
|
|
575
|
+
f"- **Documented:** {documented} ({coverage_pct})",
|
|
576
|
+
f"- **Missing Documentation:** {missing_count}{floored}"]
|
|
577
|
+
mismatches = signatures.get("mismatches") if signatures is not None else None
|
|
578
|
+
lines.append(f"- **Signature Mismatches:** {len(mismatches) if isinstance(mismatches, list) else 'N/A'}")
|
|
579
|
+
if coverage.get("staleApplicable"):
|
|
580
|
+
lines.append(f"- **Stale Documentation:** {coverage.get('staleCount')}, documented extras included")
|
|
581
|
+
else:
|
|
582
|
+
lines.append(f"- **Stale Documentation:** N/A: {NO_NAMES.get(branch, 'the denominator names no stale export')}")
|
|
583
|
+
warnings = _string_list(signatures, "warnings", "--signatures") if signatures is not None else []
|
|
584
|
+
if warnings:
|
|
585
|
+
lines.append("- **Scoring Warnings:**")
|
|
586
|
+
lines += [f" - {w}" for w in warnings]
|
|
587
|
+
if coverage.get("coverageCapped"):
|
|
588
|
+
lines.append(f"- **Numerator Surplus:** {coverage.get('numeratorSurplus')}, uncapped coverage "
|
|
589
|
+
f"{_pct(coverage.get('coverageUncapped'))}")
|
|
590
|
+
if surface is not None:
|
|
591
|
+
lines += _left_out(surface)
|
|
592
|
+
|
|
593
|
+
scores = signatures or {}
|
|
594
|
+
lines += ["", "### Category Scores", "", "| Category | Score |", "|----------|-------|",
|
|
595
|
+
f"| Export Coverage | {coverage_pct} |",
|
|
596
|
+
f"| Signature Accuracy | {_pct(scores.get('signatureAccuracy'))} |",
|
|
597
|
+
f"| Type Coverage | {_pct(scores.get('typeCoverage'))} |"]
|
|
598
|
+
return "\n".join(lines) + "\n"
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _render_main(argv):
|
|
602
|
+
parser = argparse.ArgumentParser(
|
|
603
|
+
prog="reconcile-coverage render",
|
|
604
|
+
description="Print the Coverage Analysis section's Markdown from the run files (coverage-check.md §5).",
|
|
605
|
+
)
|
|
606
|
+
parser.add_argument("--coverage", required=True, metavar="PATH", help="this script's result (coverage.json)")
|
|
607
|
+
parser.add_argument("--surface", metavar="PATH", help="load-coverage-inputs.py surface output (surface.json)")
|
|
608
|
+
parser.add_argument("--signatures", metavar="PATH", help="score-signatures.py score output (signatures.json)")
|
|
609
|
+
args = parser.parse_args(argv)
|
|
610
|
+
try:
|
|
611
|
+
text = render(_read_json_file(args.coverage, "--coverage"),
|
|
612
|
+
_read_json_file(args.surface, "--surface") if args.surface else None,
|
|
613
|
+
_read_json_file(args.signatures, "--signatures") if args.signatures else None)
|
|
614
|
+
except (InputFileError, RenderError) as exc:
|
|
615
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
616
|
+
return 1
|
|
617
|
+
sys.stdout.write(text)
|
|
618
|
+
return 0
|
|
619
|
+
|
|
620
|
+
|
|
353
621
|
# --- Run-folder inputs --------------------------------------------------------
|
|
354
622
|
|
|
355
623
|
|
|
@@ -432,7 +700,10 @@ def _build_parser():
|
|
|
432
700
|
epilog=(
|
|
433
701
|
"Example (barrel branch):\n"
|
|
434
702
|
" uv run reconcile-coverage.py --denominator-source barrel "
|
|
435
|
-
"--inventory run/inventory.json --surface run/surface.json --output run/coverage.json"
|
|
703
|
+
"--inventory run/inventory.json --surface run/surface.json --output run/coverage.json\n"
|
|
704
|
+
"Render the Coverage Analysis section (see `render --help`):\n"
|
|
705
|
+
" uv run reconcile-coverage.py render --coverage run/coverage.json "
|
|
706
|
+
"[--surface run/surface.json] [--signatures run/signatures.json]"
|
|
436
707
|
),
|
|
437
708
|
)
|
|
438
709
|
src = parser.add_mutually_exclusive_group()
|
|
@@ -492,6 +763,9 @@ def _write_output(path, result):
|
|
|
492
763
|
|
|
493
764
|
|
|
494
765
|
def main(argv=None):
|
|
766
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
767
|
+
if argv[:1] == ["render"]:
|
|
768
|
+
return _render_main(argv[1:])
|
|
495
769
|
parser = _build_parser()
|
|
496
770
|
args = parser.parse_args(argv)
|
|
497
771
|
raw = _resolve_input(args)
|
|
@@ -15,18 +15,21 @@ ratios and the gap records of the wrong signatures.
|
|
|
15
15
|
|
|
16
16
|
plan the comparisons to make. For each documented export with a
|
|
17
17
|
signature (an inventory entry of any kind but `method` that carries
|
|
18
|
-
`params`, `return_type` or `usage_signature`)
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
`params`, `return_type` or `usage_signature`) in the signature
|
|
19
|
+
universe, grouped by source file, with the line that defines it in
|
|
20
|
+
the source and the documented signature. The signature universe is
|
|
21
|
+
the surface's `all` set plus every name on its export records, so
|
|
22
|
+
the names of a Python sub-package load-coverage-inputs.py keeps out
|
|
23
|
+
of `all` (excluded.nestedEntries) keep their checks. Also the whole
|
|
21
24
|
documented-signature map, for the fallback scan that has no surface
|
|
22
25
|
lines yet.
|
|
23
26
|
score the categories. Reads every subagent result, checks its schema,
|
|
24
27
|
and computes:
|
|
25
28
|
Signature Accuracy = matching / compared * 100, where compared is
|
|
26
|
-
the documented signatures the
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
the documented signatures of the signature universe (with a
|
|
30
|
+
fallback scan's exports_found) and matching those no result
|
|
31
|
+
names in signature_mismatches[]: a wrong signature is wrong
|
|
32
|
+
whichever set the denominator counts
|
|
30
33
|
Type Coverage = documented types / total types * 100, where
|
|
31
34
|
total types counts the names of the --surface-set set (the
|
|
32
35
|
denominator set coverage-check section 2b picked) whose kind is
|
|
@@ -45,8 +48,9 @@ Inputs, all files in the run folder:
|
|
|
45
48
|
--inventory the validated inventory validate-inventory.py wrote (--output)
|
|
46
49
|
--surface load-coverage-inputs.py surface output: `exports[]` (name,
|
|
47
50
|
kind, file, line, signatureLine) and the name `sets`;
|
|
48
|
-
--surface-set picks the set (default "all")
|
|
49
|
-
|
|
51
|
+
--surface-set picks the set (default "all") score's Type
|
|
52
|
+
Coverage counts, while plan and Signature Accuracy compare
|
|
53
|
+
the signature universe whichever set it picks
|
|
50
54
|
--results a subagent result, as the subagent returned it (a wrapping
|
|
51
55
|
markdown fence is stripped), repeatable. One object, or a
|
|
52
56
|
list of them:
|
|
@@ -67,7 +71,8 @@ Output (stdout; --output also writes it, --gaps-output writes the records):
|
|
|
67
71
|
plan: {"compared": N, "files": [{"file", "checks": [{"name", "line",
|
|
68
72
|
"signatureLine", "documented": {...}}]}], "documentedSignatures": {...}}
|
|
69
73
|
score: {"valid": true, "signatureAccuracy", "matchingSignatures",
|
|
70
|
-
"totalDocumented", "
|
|
74
|
+
"totalDocumented", "comparedNames": [the names compared],
|
|
75
|
+
"typeCoverage", "documentedTypes", "totalTypes",
|
|
71
76
|
"missingTypes": [...], "mismatches": [...], "gapRecords": [...],
|
|
72
77
|
"warnings": [...]}
|
|
73
78
|
or {"valid": false, "violations": [...]} when a result breaks the schema
|
|
@@ -259,13 +264,23 @@ def _records_by_name(exports: list[dict]) -> dict[str, list[dict]]:
|
|
|
259
264
|
return by_name
|
|
260
265
|
|
|
261
266
|
|
|
267
|
+
def signature_universe(names: list[str], exports: list[dict]) -> set[str]:
|
|
268
|
+
"""The names whose documented signature is compared: the `all` set
|
|
269
|
+
(`names`) plus every name on the surface's export records. A nested
|
|
270
|
+
Python top's names leave `all` but keep their records, so a documented
|
|
271
|
+
one still has its signature checked."""
|
|
272
|
+
return set(names) | set(_records_by_name(exports))
|
|
273
|
+
|
|
274
|
+
|
|
262
275
|
def plan(inventory: list[dict], names: list[str] | None, exports: list[dict]) -> dict:
|
|
276
|
+
"""The comparisons over the signature universe of `names` (the `all`
|
|
277
|
+
set) and `exports`; none without a surface (`names` None)."""
|
|
263
278
|
signatures = documented_signatures(inventory)
|
|
264
279
|
files: dict[str, list[dict]] = {}
|
|
265
280
|
compared = 0
|
|
266
281
|
if names is not None:
|
|
267
282
|
by_name = _records_by_name(exports)
|
|
268
|
-
for name in sorted(
|
|
283
|
+
for name in sorted(signature_universe(names, exports) & set(signatures)):
|
|
269
284
|
record = (by_name.get(name) or [{}])[0]
|
|
270
285
|
compared += 1
|
|
271
286
|
files.setdefault(str(record.get("file") or ""), []).append({
|
|
@@ -297,15 +312,17 @@ def _gap_record(m: dict) -> dict:
|
|
|
297
312
|
|
|
298
313
|
def score(inventory: list[dict], names: list[str], exports: list[dict], results: list[dict],
|
|
299
314
|
type_names: list[str] | None = None) -> dict:
|
|
300
|
-
"""The two categories: Signature Accuracy over
|
|
301
|
-
|
|
315
|
+
"""The two categories: Signature Accuracy over the signature universe of
|
|
316
|
+
`names` (the `all` set) and `exports`, Type Coverage over `type_names`
|
|
317
|
+
when given, else over `names`. A fallback scan's exports_found join the
|
|
318
|
+
universe, and `names` when no `type_names` is given."""
|
|
302
319
|
by_name = _records_by_name(exports)
|
|
303
320
|
# A fallback scan reports its own names and types.
|
|
304
321
|
fallback_types = {n for r in results for n in r.get("types_found") or []}
|
|
305
322
|
surface = set(names) | {n for r in results for n in r.get("exports_found") or []}
|
|
306
323
|
documented = documented_names(inventory)
|
|
307
324
|
signatures = documented_signatures(inventory)
|
|
308
|
-
compared = surface & set(signatures)
|
|
325
|
+
compared = (surface | signature_universe(names, exports)) & set(signatures)
|
|
309
326
|
warnings = []
|
|
310
327
|
|
|
311
328
|
mismatches = []
|
|
@@ -353,6 +370,7 @@ def score(inventory: list[dict], names: list[str], exports: list[dict], results:
|
|
|
353
370
|
"signatureAccuracy": signature_accuracy,
|
|
354
371
|
"matchingSignatures": len(compared) - len(wrong),
|
|
355
372
|
"totalDocumented": len(compared),
|
|
373
|
+
"comparedNames": sorted(compared),
|
|
356
374
|
"typeCoverage": type_coverage,
|
|
357
375
|
"documentedTypes": len(documented_types),
|
|
358
376
|
"totalTypes": len(types),
|
|
@@ -378,7 +396,7 @@ def _cmd_plan(args: argparse.Namespace) -> tuple[dict, int]:
|
|
|
378
396
|
inventory = load_inventory(args.inventory)
|
|
379
397
|
names, exports = (None, [])
|
|
380
398
|
if args.surface is not None:
|
|
381
|
-
|
|
399
|
+
_, exports, names = load_surface(args.surface, args.surface_set)
|
|
382
400
|
out = plan(inventory, names, exports)
|
|
383
401
|
if args.output:
|
|
384
402
|
_write_json(args.output, out)
|
|
@@ -412,7 +430,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
412
430
|
p = sub.add_parser("plan", help="the signature comparisons for the subagents to make")
|
|
413
431
|
p.add_argument("--inventory", required=True, help="the validated inventory file")
|
|
414
432
|
p.add_argument("--surface", help="load-coverage-inputs.py surface output")
|
|
415
|
-
p.add_argument("--surface-set", default="all",
|
|
433
|
+
p.add_argument("--surface-set", default="all",
|
|
434
|
+
help="a surface name set, which must exist (default: all); the plan compares the signature "
|
|
435
|
+
"universe whichever set it names")
|
|
416
436
|
p.add_argument("--output", help="also write the plan to this file")
|
|
417
437
|
p.set_defaults(func=_cmd_plan)
|
|
418
438
|
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
Lookup-and-count helper for the SKF test-skill workflow (coverage-check.md
|
|
9
9
|
§2b, its "Numerator ground truth" paragraph; §4b records the gap). It fires
|
|
10
10
|
only on the inflation signature
|
|
11
|
-
(`stats.exports_documented == effective_denominator` exactly
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
(`stats.exports_documented == effective_denominator` exactly, or, for a
|
|
12
|
+
provenance map with no `::` entry, its distinct entry names number
|
|
13
|
+
`effective_denominator`): the numerator equals the denominator, the tell of a
|
|
14
|
+
documented count padded to force 100% coverage. On that signature the declared
|
|
15
|
+
count is not trusted: every declared export name is looked up in the skill's
|
|
16
|
+
documentation surface and only the names that actually appear count toward
|
|
17
|
+
coverage.
|
|
16
18
|
|
|
17
19
|
Distinct from reconcile-coverage.py (whose scalar/stack branches look up the §1
|
|
18
20
|
inventory names and return only a residual missing *count*): this script looks
|
|
@@ -7,8 +7,8 @@ hashContentProbeOrder:
|
|
|
7
7
|
- '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
|
|
8
8
|
- '{project-root}/src/shared/scripts/skf-hash-content.py'
|
|
9
9
|
# `{buildChangeManifestHelper}`: §3's `build` (and §1's for a docs-only
|
|
10
|
-
# skill), §2.2's `deletion-ratio
|
|
11
|
-
# if neither exists.
|
|
10
|
+
# skill), §2.2's `deletion-ratio`, Category B's `baseline-gaps` and Category
|
|
11
|
+
# C's `rename-candidates`. HALT if neither exists.
|
|
12
12
|
buildChangeManifestProbeOrder:
|
|
13
13
|
- '{project-root}/_bmad/skf/shared/scripts/skf-build-change-manifest.py'
|
|
14
14
|
- '{project-root}/src/shared/scripts/skf-build-change-manifest.py'
|
|
@@ -17,13 +17,13 @@ buildChangeManifestProbeOrder:
|
|
|
17
17
|
provenanceGapDispatchProbeOrder:
|
|
18
18
|
- '{project-root}/_bmad/skf/shared/scripts/skf-provenance-gap-dispatch.py'
|
|
19
19
|
- '{project-root}/src/shared/scripts/skf-provenance-gap-dispatch.py'
|
|
20
|
-
# `{detectScriptsAssetsHelper}`: Category D's script and asset walk,
|
|
21
|
-
# create-skill
|
|
20
|
+
# `{detectScriptsAssetsHelper}`: Category D's script and asset walk, the
|
|
21
|
+
# detector create-skill runs, over the whole source. HALT if neither exists.
|
|
22
22
|
detectScriptsAssetsProbeOrder:
|
|
23
23
|
- '{project-root}/_bmad/skf/shared/scripts/skf-detect-scripts-assets.py'
|
|
24
24
|
- '{project-root}/src/shared/scripts/skf-detect-scripts-assets.py'
|
|
25
|
-
# `{newFileDiffHelper}`: Category D's NEW_FILE set difference,
|
|
26
|
-
# this skill.
|
|
25
|
+
# `{newFileDiffHelper}`: Category D's NEW_FILE set difference, kept to the
|
|
26
|
+
# skill brief's scope, bundled with this skill.
|
|
27
27
|
newFileDiffHelper: 'scripts/skf-new-file-diff.py'
|
|
28
28
|
# `{resolveAuthoritativeFilesHelper}`: §1b's mirror. HALT if neither exists.
|
|
29
29
|
resolveAuthoritativeFilesProbeOrder:
|
|
@@ -103,6 +103,8 @@ It writes `{"mode": "docs-only", "no_changes", "changed_urls", "fetch_failed", "
|
|
|
103
103
|
|
|
104
104
|
**The brief this run reads.** Bind `{brief_path}` ← `{forge_data_folder}/{skill_name}/skill-brief.yaml`. When `detect_only_mode` or `dry_run_mode` is true and that file exists, copy it to `{run_dir}/skill-brief.yaml` and bind `{brief_path}` to the copy: §1b and §1c write each decision there, Category A reads the scope from there, and the brief itself stays as it was. Each amendment a read-only run writes to the copy is a proposed one: add `proposed-amendment: {action} {path} ({category}); not written: {--dry-run or --detect-only}` to `warnings[]` as you write it, and step 6 lists them with the change manifest.
|
|
105
105
|
|
|
106
|
+
**The scope type this run reads.** Bind `{scope_type}` ← the `scope_type` metadata.json records (init.md §2), else the `scope.type` of `{brief_path}` when that file exists and names one: a skill whose metadata.json records no scope type updates under its brief's. Category B's runner takes it as `--scope-type` and writes it to `{run_dir}/extraction.json` as `scope.type`, where step 3's `records` and step 5's `apply` read it: under `public-api`, a normal-mode update neither documents nor adds to the provenance map a new export off the runner's public surface (re-extract.md §4, write.md §3).
|
|
107
|
+
|
|
106
108
|
### 1b. Discovered Authoritative Files Protocol (Mirror)
|
|
107
109
|
|
|
108
110
|
**Purpose:** mirror `skf-create-skill` §2a into update-skill. `skf-create-skill` §2a catches authoritative AI documentation files (`llms.txt`, `AGENTS.md`, `.cursorrules`, etc.) during **creation**, but a project may add these files *after* the skill was created. Without this mirror, update-skill would either miss the new file entirely (if it doesn't match the provenance map's file patterns) or classify it as a generic ADDED file in §2 Category A with no authoritative-file treatment. The mirror surfaces the discovery with the same P/S/U prompt create-skill uses, honoring any prior amendments.
|
|
@@ -309,9 +311,22 @@ Pass `--provenance-map` whenever init.md §4 found one; `--brief` (as §1c left
|
|
|
309
311
|
-o "{run_dir}/extraction.json"
|
|
310
312
|
```
|
|
311
313
|
|
|
312
|
-
Pass `--language` and `--scope-type` when
|
|
314
|
+
Pass `--language` when metadata.json records it, and `--scope-type` when §1 bound `{scope_type}`. It takes no `--brief`: Category A applied the scope, and a tracked file the brief's scope leaves out (gap-driven.md §1's rule R1 excludes a rescoped export's file) keeps its exports. `--head-cap 0` keeps every match, since a dropped one would read as a deleted export. Step 3 reads this file and never runs the runner again. **Exit 1** (`incomplete`, its JSON written): run it once more; when still incomplete, step 2 below reads by eye each listed file with no export in `exports[]` or that an `errors[]` item lists in `unread`, and tells the user which files were read by eye. **Exit 2** (an input error, such as a file list it cannot read, or a failure it did not foresee, named in one stderr line with no JSON; `uv` failing to start it exits 2 too): HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-b"`, the stderr line that holds `error:` as `reason`). No by-eye read stands in for it: the skill is not written yet, and reading every file by eye would hide the error. **Exit 3 (`no-ast-grep`), no JSON on another exit, no candidate resolves, or Quick tier:** write `{"exports": []}` to `{run_dir}/extraction.json`, with `"scope": {"type": "{scope_type}"}` when §1 bound `{scope_type}`, so step 5's `apply` can warn that a `public-api` surface was not applied; step 2 reads every listed file by eye (at Quick tier, by text pattern). Tell the user which files were read by eye.
|
|
315
|
+
|
|
316
|
+
Then, at every tier (in degraded mode there is no map: skip it), list the map's entries the runner left out that their file still declares: the runner never reports a module, an alias that renames, a dunder such as `__version__` or another underscore name, and step 3's diff would read each as a deleted export. From `{project-root}`, run:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
uv run {buildChangeManifestHelper} baseline-gaps \
|
|
320
|
+
--provenance-map "{provenance_map_path}" \
|
|
321
|
+
--extraction "{run_dir}/extraction.json" \
|
|
322
|
+
--files "{run_dir}/modified-files.json" \
|
|
323
|
+
--source-root "{source_root}" \
|
|
324
|
+
-o "{run_dir}/baseline-gaps.json"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
It writes `{"gaps": [{name, file, line, entry, export_type}], "unchecked": [{name, file, export_type}]}`: in `gaps[]` each entry of a modified file that no export of `{run_dir}/extraction.json` holds and whose file still declares it, at the line that declares it, by the verifier's rule (audit-skill's extraction snapshot applies the same one), and in `unchecked[]` each such entry no rule can check (its file neither Python nor TS/JS, a file the rule will not read, such as a test file or one with a symlink on its path, or a dotted module name), which step 2 reads by eye. Never add a gap or drop one by eye. On exit 1 (an input it cannot read, or a `skf-verify-provenance-completeness.py` beside it that cannot be loaded), no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-b"`, its stderr as `reason`): without it, every name the runner leaves out would read as deleted.
|
|
313
328
|
|
|
314
|
-
2. **What the recipes do not record.** Workers, in parallel batches (Pattern 4) when the list is long, read the modified and added files and each return ONLY `{"exports": [...]}`, with no prose and no fences. For each function export the runner found whose `params` it left null (it could not read the signature): `export_name` and `source_file` as it wrote them, with `params` (each parameter as the source writes it, `name: type`, the form the provenance map's `params` hold) and `return_type` (null when there is none), read at its `source_line`. For each export it could not find (a name `entry_point_diff.extraction_gaps[]` lists, a form Known Limitation #11 in `{extractionPatternsData}` lists, an export of a file `file_issues[]` names or that was read by eye): the same, plus `export_type`, `source_line`, `confidence: T1-low` and `extraction_method: source-read`. Write the union of their `exports` arrays to `{run_dir}/export-details.json`.
|
|
329
|
+
2. **What the recipes do not record.** Workers, in parallel batches (Pattern 4) when the list is long, read the modified and added files and each return ONLY `{"exports": [...]}`, with no prose and no fences. For each function export the runner found whose `params` it left null (it could not read the signature): `export_name` and `source_file` as it wrote them, with `params` (each parameter as the source writes it, `name: type`, the form the provenance map's `params` hold) and `return_type` (null when there is none), read at its `source_line`. For each export it could not find (a name `entry_point_diff.extraction_gaps[]` lists, a name `{run_dir}/baseline-gaps.json` lists in `gaps[]`, a form Known Limitation #11 in `{extractionPatternsData}` lists, an export of a file `file_issues[]` names or that was read by eye): the same, plus `export_type`, `source_line`, `confidence: T1-low` and `extraction_method: source-read`. Record a baseline gap at its `file` and `line` (`source_line`), with, as `export_type`, its `export_type` while the declaration there still has that kind, else the kind it has now (a `variable` now declared by a `def` is a `function`, which step 3 reports as a changed kind), and the declaration's kind when the gap gives none; `params` and `return_type` as the declaration writes them, null when it has none (a module, an alias or a variable). For each `unchecked[]` entry, read its file by eye and record it the same way, at the line that declares it, only when the file still declares it. Record each name and file once: when a file read by eye whole (at Quick tier, or when the runner could not run or left the file unread) holds a name a gap or an `unchecked[]` entry lists, that read's record stands and the gap adds none. Write the union of their `exports` arrays to `{run_dir}/export-details.json`.
|
|
315
330
|
|
|
316
331
|
3. **The diff.** Resolve `{structuralDiffHelper}` ← first existing path in `{structuralDiffProbeOrder}` and, from `{project-root}`, run:
|
|
317
332
|
|
|
@@ -368,15 +383,19 @@ The helper emits:
|
|
|
368
383
|
}
|
|
369
384
|
```
|
|
370
385
|
|
|
371
|
-
|
|
386
|
+
A map with no `file_entries` field, or a null one (a skill with no script, asset or promoted document, or a map written before the field existed), tracks no file: the comparison lists no row and the set difference below subtracts nothing.
|
|
387
|
+
|
|
388
|
+
The compare helper reports only tracked files; NEW_FILE detection (a file present in source but absent from the provenance map) is a set-difference, so it runs through a script rather than the prompt. **When `{brief_path}` does not exist** (a skill built without a brief, such as a quick skill), skip it: with no scope to keep to, the walk would read every script and asset of the repository as new. Add `new-files-not-checked: no skill brief, so no new script or asset was looked for` to `warnings[]`, write no `{run_dir}/new-files.json`, and leave `--new-files` out of §3's `build` and step 5's `apply`. Otherwise pipe the detector create-skill step 3 §4c runs (resolved via `detectScriptsAssetsProbeOrder`), which walks the whole source, into `{newFileDiffHelper}`, which keeps what the brief takes and subtracts what the map tracks:
|
|
372
389
|
|
|
373
390
|
```bash
|
|
374
391
|
uv run {detectScriptsAssetsHelper} detect "{source_root}" \
|
|
375
|
-
| uv run {newFileDiffHelper} "{provenance_map_path}" \
|
|
392
|
+
| uv run {newFileDiffHelper} "{provenance_map_path}" [--brief "{brief_path}"] \
|
|
376
393
|
> "{run_dir}/new-files.json"
|
|
377
394
|
```
|
|
378
395
|
|
|
379
|
-
|
|
396
|
+
Pass `--brief` as Category A does, `{brief_path}` as §1c left it (in a read-only run, its copy in `{run_dir}`). On exit 2 (the detector's JSON, the map or the brief it cannot read, named in its JSON `error` on stderr, which also catches a detector that printed nothing), no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-d"`, its `error` as `reason`).
|
|
397
|
+
|
|
398
|
+
It writes `{"new_files":[{source_file, kind}], "skipped_manual":[...], "already_tracked":[...], "tracked_code":[...], "out_of_scope":[...], "intent_none":[...], "stats":{...}}`. §3's `build` reads both files, typing each compare row by its `file_entries[]` row's `file_type` and each new file by its `kind`: sort no row by hand. `skipped_manual[]` are user-authored files under `scripts/[MANUAL]/` or `assets/[MANUAL]/`, preserved and not touched; `already_tracked[]` were handled by the compare above; `tracked_code[]` are code files the map's `entries[]` cite, which Categories A and B compare; `out_of_scope[]` are paths the brief's scope leaves out, by Category A's in-scope test; `intent_none[]` are paths of a kind whose `scripts_intent` or `assets_intent` is `none` (an absent or free-text intent detects). A path in these lists is no new file and no `stats` count, so step 6's report counts each of the three lists from this file, where it shows Category D: a brief scoped wrongly shows as many paths out of scope. A new file Category A lists too stays a new file: Category A lists every changed in-scope file, a genuine script included.
|
|
380
399
|
|
|
381
400
|
**Write the category JSON** to `{run_dir}/categories.json`, where §2.2 and §3 read it beside the helper files: the two flags, `degraded_mode` and `update_mode: "normal"`. Category C stays in the files its steps wrote, which §2.2, §3 and step 5's `apply` take as `--category-c` and `--ccc-pairs`:
|
|
382
401
|
|
|
@@ -488,13 +507,13 @@ The helper emits the unified manifest envelope:
|
|
|
488
507
|
"total_export_changes": N,
|
|
489
508
|
"per_file": [
|
|
490
509
|
{"file_path": "...", "status": "MODIFIED|ADDED|DELETED|MOVED",
|
|
491
|
-
"exports_affected": [{name, change_type, old_line, new_line}, ...]}
|
|
510
|
+
"exports_affected": [{name, change_type, old_line, new_line, old_file?}, ...]}
|
|
492
511
|
],
|
|
493
512
|
"category_d": {"scripts_modified": [...], ..., "docs_deleted": [...]}
|
|
494
513
|
}
|
|
495
514
|
```
|
|
496
515
|
|
|
497
|
-
`per_file` entries are sorted MODIFIED → ADDED → DELETED → MOVED, then alphabetically within each status group, so downstream stages can rely on stable ordering. MOVED entries include an extra `old_path` field. `category_d` lists the paths behind the `scripts_*`, `assets_*` and `docs_*` counts: a `docs_*` path is a tracked document, which only Category D detects, so a change to one alone is a change. `{run_dir}/change-manifest.json` is the change manifest: step 3, step 6 and step 5's `apply` read it there, so it survives a compacted context.
|
|
516
|
+
`per_file` entries are sorted MODIFIED → ADDED → DELETED → MOVED, then alphabetically within each status group, so downstream stages can rely on stable ordering. MOVED entries include an extra `old_path` field, and a `MOVED_EXPORT` across files an `old_file`, the file it left, where `apply` finds its entry. `category_d` lists the paths behind the `scripts_*`, `assets_*` and `docs_*` counts: a `docs_*` path is a tracked document, which only Category D detects, so a change to one alone is a change. `{run_dir}/change-manifest.json` is the change manifest: step 3, step 6 and step 5's `apply` read it there, so it survives a compacted context.
|
|
498
517
|
|
|
499
518
|
### 4. Check for No-Change Shortcut
|
|
500
519
|
|
|
@@ -88,14 +88,14 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
|
|
|
88
88
|
|------------|-------------------------------------------|-----------------|
|
|
89
89
|
| `missing-export`, `missing-type` | Missing export, type or interface documentation | NEW_EXPORT (an undocumented public export or type), unless the remediation says the export is internal or out of scope: then DELETED_EXPORT (rescope), see rule R1 |
|
|
90
90
|
| `split-body-mismatch` | Split-body inconsistency: the SKILL.md body and a `references/*.md` file document one export differently, with a `Source:` inside the skill package | STRUCTURAL_FIX, see rule R2 (for an older report, this row takes precedence over the Signature mismatch row below) |
|
|
91
|
-
| `signature-mismatch`, `fabricated-signature`, `stale-documentation` | Signature mismatch; stale documentation (the skill documents an export the source
|
|
91
|
+
| `signature-mismatch`, `fabricated-signature`, `stale-documentation` | Signature mismatch; stale documentation (the skill documents an export the source's enumerated surface lacks and that is not a documented extra, an Info `observation` this table never routes) | MODIFIED_EXPORT (the documentation needs updating from source) |
|
|
92
92
|
| `structural`, `broken-reference`, `inaccurate-reference`, `reference-escape`, `integration-pattern`, `migration-section`, `scripts-assets` | Structural or coherence drift in the output files only, no source change | STRUCTURAL_FIX, see rule R2 |
|
|
93
93
|
| `provenance-completeness` | Export documented in SKILL.md/references but **missing from the provenance-map** | NEW_EXPORT (provenance-completeness), see rule R3 |
|
|
94
94
|
| `provenance-line` | Provenance line is not the export's definition | MOVED_EXPORT (provenance line), see rule R5 |
|
|
95
95
|
| `metadata-drift`, `metadata` | Divergent export counts in the metadata; missing metadata or examples | metadata update, see rule R4 |
|
|
96
96
|
| any other | any other gap | not routed |
|
|
97
97
|
|
|
98
|
-
**Not routed:** update-skill has no repair for the other categories. `provenance-unverified` asks for a check by hand, `denominator-inflation` and `multi-denominator` concern the brief's scope and the count design, and `numerator-inflation`, `scripts-assets-provenance`, `observation`, `discovery`, `description` and `external-validator` need a person or another workflow. Such a gap takes no manifest entry (bullet 3 lists it).
|
|
98
|
+
**Not routed:** update-skill has no repair for the other categories. `provenance-unverified` asks for a check by hand, `denominator-inflation`, `brief-scope-stale` and `multi-denominator` concern the brief's scope and the count design, and `numerator-inflation`, `scripts-assets-provenance`, `observation`, `discovery`, `description` and `external-validator` need a person or another workflow. Such a gap takes no manifest entry (bullet 3 lists it).
|
|
99
99
|
|
|
100
100
|
**Translation rules (referenced by the table above):**
|
|
101
101
|
|
|
@@ -310,7 +310,7 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
|
|
|
310
310
|
-o "{run_dir}/remediation-exports.json"
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
-
Pass `--scope-type` when step 1 read a `scope_type` from metadata.json, so a component library runs its recipe set. It takes no `--brief`: a test report can name a file the brief's scope leaves out as an export's home, and the runner would skip it. On exit 1 (`incomplete
|
|
313
|
+
Pass `--scope-type` when step 1 read a `scope_type` from metadata.json, so a component library runs its recipe set. It takes no `--brief`: a test report can name a file the brief's scope leaves out as an export's home, and the runner would skip it. On exit 1 (`incomplete`, its JSON written) run it once more. When it is still incomplete, read by eye each file of the file set that has no export in `exports[]` or that an `errors[]` item lists in `unread`, and tell the user which files were read by eye. On exit 2 (an input error, such as a file list it cannot read, or a failure it did not foresee, named in one stderr line with no JSON; `uv` failing to start it exits 2 too), HALT with status `blocked` (halt procedure: `phase: "re-extract:records"`, the stderr line that holds `error:` as `reason`): no by-eye read stands in for it, since this step has written nothing to the skill and reading the files by eye would hide the error. On exit 3, no JSON on another exit, or no candidate resolves, or at Quick tier, read the files by eye (by text pattern). Read by eye only what the recipes leave out: an export a file defines in a form Known Limitation #11 in `{extractionPatternsData}` lists, a file `file_issues[]` names, a name `entry_point_diff.extraction_gaps[]` lists and a file the rule above reads after a still-incomplete run. Write each export read by eye, and the `params` and `return_type` of each function export whose `params` the runner left null, in the shape detect-changes.md §2.1 Category B step 2 gives them, to `{run_dir}/remediation-details.json` as `{"exports": [...]}`. Launch subprocesses in parallel (Pattern 4) when available; sequential fallback otherwise. Follow the tier-degradation rules in `{tierDegradationRulesData}` when AST tools fail on individual files. Then, from `{project-root}`, build the file set's records:
|
|
314
314
|
|
|
315
315
|
```bash
|
|
316
316
|
uv run {buildChangeManifestHelper} records \
|