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
|
@@ -8,6 +8,10 @@ skillInventoryProbeOrder:
|
|
|
8
8
|
- '{project-root}/src/shared/scripts/skf-skill-inventory.py'
|
|
9
9
|
# {dirSizesHelper}: the §9b size of the folders in scope.
|
|
10
10
|
dirSizesHelper: 'scripts/dir-sizes.py'
|
|
11
|
+
# {atomicWriteHelper}: the §7 recovery that points `active` at the version to keep.
|
|
12
|
+
atomicWriteProbeOrder:
|
|
13
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
|
|
14
|
+
- '{project-root}/src/shared/scripts/skf-atomic-write.py'
|
|
11
15
|
---
|
|
12
16
|
|
|
13
17
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -167,11 +171,11 @@ Set `target_versions = "all"` and `is_skill_level = true`.
|
|
|
167
171
|
"**Cannot drop the active version `{version}`.**
|
|
168
172
|
Other non-deprecated versions of `{target_skill}` still exist. To proceed, either:
|
|
169
173
|
|
|
170
|
-
**(a)** Make the version you keep active first: point the `active` link at it (`
|
|
174
|
+
**(a)** Make the version you keep active first: point the `active` link at it (`uv run {atomicWriteHelper} flip-link --link "{skills_output_folder}/{target_skill}/active" --target <version to keep>`), run `[EX] Export Skill` for `{target_skill}`, which exports that version and archives `{version}`, then return here to drop `{version}`, OR
|
|
171
175
|
|
|
172
176
|
**(b)** Use the `[A] All versions` option to drop every version of `{target_skill}` at once."
|
|
173
177
|
|
|
174
|
-
In (a), `<version to keep>` is the first `{version_rows}` entry other than `{version}` whose `in_manifest` and `on_disk` are true and whose `status` is not `"deprecated"` (the rows are newest first); with no such entry, show `<version to keep>` as written.
|
|
178
|
+
In (a), `<version to keep>` is the first `{version_rows}` entry other than `{version}` whose `in_manifest` and `on_disk` are true and whose `status` is not `"deprecated"` (the rows are newest first); with no such entry, show `<version to keep>` as written. `{atomicWriteHelper}` is the first existing path in `{atomicWriteProbeOrder}`, or its first entry when neither exists.
|
|
175
179
|
|
|
176
180
|
HALT (exit code 5, `halt_reason: "active-version-guard-refused"`, phase `select:active-version-guard`), with `skill: "{target_skill}"` and `versions_affected: ["{version}"]` in the §1 halt envelope. Do not proceed.
|
|
177
181
|
|
|
@@ -305,7 +309,7 @@ When `{headless_mode}` is true, stage the preview as `{run_dir}/result-context.j
|
|
|
305
309
|
uv run {emitEnvelopeHelper} emit --workflow skf-drop-skill --run-dir "{run_dir}" < "{run_dir}/result-context.json"
|
|
306
310
|
```
|
|
307
311
|
|
|
308
|
-
|
|
312
|
+
A dry run passes no `--result-dir`, so it writes no result file. Then, in either mode, delete the run folder (`rm -rf "{run_dir}"`) and HALT (exit code 0). In a headless run, display the line the emitter printed verbatim (the `message` of its stderr JSON when it exits non-zero) after the delete and before the HALT, with nothing after it, so a `claude -p` caller reads it as the final message. The manifest, filesystem, and context files are untouched.
|
|
309
313
|
|
|
310
314
|
**Otherwise (not a dry-run), GATE [default: Y]:** If `{headless_mode}`: auto-proceed with [Y], record `confirm_source = "headless-auto"`, stage `{run_dir}/decision.json` as `{"gate": "select.confirm", "default_action": "Y", "taken_action": "Y", "reason": "headless auto-confirm"}` and run the §8 `record` command, and log: "headless: auto-confirmed drop of {target_skill}"
|
|
311
315
|
|
|
@@ -136,7 +136,7 @@ SKILL.md's cross-references come in two kinds, and each has one owner:
|
|
|
136
136
|
uv run {scanSkillMdStructureHelper} reference-check "{resolved_skill_package}/SKILL.md" [--source-root "{source_path}"] [--skills-root "{skills_output_folder}"] > "{run_dir}/references.json"
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
Pass `--source-root` when metadata.json records the extraction tree (`{source_path}`), and `--skills-root` for a stack skill: a stack's constituent cross-references legitimately resolve to `skills/{name}/active` under `{skills_output_folder}`, which lies outside `{skillDir}`, and a stack's metadata.json records no
|
|
139
|
+
Pass `--source-root` when metadata.json records the extraction tree (`{source_path}`), and `--skills-root` for a stack skill: a stack's constituent cross-references legitimately resolve to `skills/{name}/active` under `{skills_output_folder}`, which lies outside `{skillDir}`, and a stack's metadata.json records no `source_root` to anchor them. The scanner resolves each target against the SKILL.md folder through every symlink (`os.path.realpath`) before the root check, so a symlink that points outside every root is still caught, and gives it a `status`: `ok` (inside a root, present), `missing` (inside a root, nothing there) or `escapes` (outside every root: `../../../etc/passwd`, an absolute path to an unrelated folder, a symlink redirection outside the skill, its source or a stack's skills tree). Do not check these targets again by hand or through subagents, and do not validate an escaping target's contents: the escape itself is the finding.
|
|
140
140
|
- **Skill references, type imports and integration-pattern references** (`See SKILL.md for {other-skill}`, `Integrates with {package}`, `import { Type } from './module'`, middleware chains, plugin hooks, shared state): whether one is accurate takes judgment. Delegate to a subagent that reads SKILL.md and returns only this JSON shape (no prose, no commentary, no markdown fences): `{"references_found": [{"line": N, "type": "skill|type-import|integration-pattern", "target": "..."}]}`. Parent strips wrapping markdown fences (if present) before parsing. If subagent unavailable, scan in main thread.
|
|
141
141
|
|
|
142
142
|
### 4. Contextual Mode: Validate Each Reference
|
|
@@ -4,7 +4,7 @@ Load this file only from coverage-check.md §2, and follow the one section §2 n
|
|
|
4
4
|
|
|
5
5
|
## Quick Tier
|
|
6
6
|
|
|
7
|
-
- **A skill quick-skill built** (`metadata.json` `generated_by` is `quick-skill`), **from local source** (State 1):
|
|
7
|
+
- **A skill quick-skill built** (`metadata.json` `generated_by` is `quick-skill`), or **any skill in Python, JavaScript or TypeScript**, **from local source** (State 1): parse the entry points with `{extractPublicApiHelper}` `--mode quick`. quick-skill built its export list with that parser, so both sides count one surface, and in those three languages it gives each name the file that defines it (`defined_in`), so the brief's globs place the name as the recipe runner does. Resolve it ← first existing path in `{extractPublicApiProbeOrder}` and run it once per package in scope, with its manifest and one `--entry-file` for each entry-point file the Source API Surface Definition makes its surface, each path relative to `{source_path}` (`<n>` counts the packages from 1):
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
uv run {extractPublicApiHelper} --mode quick --language <language> --source-root "{source_path}" --manifest-file <manifest path> --entry-file <entry path> > "{run_dir}/quick-<n>.json"
|
|
@@ -13,10 +13,10 @@ Load this file only from coverage-check.md §2, and follow the one section §2 n
|
|
|
13
13
|
Its `warnings` name each statement whose names the entry file alone cannot give. Read by eye only the statements its warnings name, save the names they give as a per-file result for the entry file that holds the statement, `{"file": "<entry path>", "exports_found": [<each name>], "signature_mismatches": []}`, and list those names in the Coverage Analysis section as read by eye. Give `surface` one `--quick` per package, one `--per-file` per saved result, the brief when the skill has one (its globs give the `scope.include` and `tier_a_include` sets), the metadata and, when init.md §2 bound one, the provenance map (the baselines of the §2b candidates and guards; they add no name):
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
uv run {coverageInputsScript} surface --quick "{run_dir}/quick-<n>.json" [--per-file "{run_dir}/per-file-<n>.json"] [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] --output "{run_dir}/surface.json"
|
|
16
|
+
uv run {coverageInputsScript} surface --quick "{run_dir}/quick-<n>.json" [--per-file "{run_dir}/per-file-<n>.json"] [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] [--monorepo] [--pattern-reference-intent] [--curated-subset] [--barrel <state>] --output "{run_dir}/surface.json"
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
- **Any other skill** (create-skill's Quick tier reads the entry points as source text), a source that is not local, or a helper that does not resolve or exits non-zero (it exits 1 on a language it does not parse): identify the exports by reading the entry points per the Source API Surface Definition, save one per-file result per entry file, run the `surface` command above with no `--quick`, and note in the Coverage Analysis section that the Quick-tier scan read them by eye.
|
|
19
|
+
- **Any other skill** (create-skill's Quick tier reads the entry points of another language as source text), a source that is not local, or a helper that does not resolve or exits non-zero (it exits 1 on a language it does not parse): identify the exports by reading the entry points per the Source API Surface Definition, save one per-file result per entry file, run the `surface` command above with no `--quick`, and note in the Coverage Analysis section that the Quick-tier scan read them by eye.
|
|
20
20
|
|
|
21
21
|
## Fallback Per-File Scan
|
|
22
22
|
|
|
@@ -26,10 +26,10 @@ The recipe runner cannot be used, so subagents read the files in scope one at a
|
|
|
26
26
|
uv run {scoreSignaturesScript} plan --inventory "{run_dir}/inventory.json" --output "{run_dir}/signature-plan.json"
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
For each source file that defines public API exports, delegate a subagent that gets that map with the file. It extracts the file
|
|
29
|
+
For each source file that defines public API exports, delegate a subagent that gets that map with the file. It extracts the symbols the file exports, with their full signatures, with ast-grep (or reads the file when `extraction.fallback.reason` says no ast-grep can run; list those files in the Coverage Analysis section as read by eye), leaves out a name the file only passes on from another module of the source being tested (`export { x } from './x'`, `export * from`, an `__init__.py` import, a Rust `pub use`), which that module's own result counts, keeps a name it passes on from another package (`export { z } from 'zod'`), which no other result counts, compares each with the map's entry (params, return type), gives each mismatch the `line` that defines the export, and returns only `{"file", "exports_found": [...], "types_found": [<the exported interfaces, type aliases, enums and classes>], "signature_mismatches": [...]}`, each mismatch in the shape §2's signature comparison returns. Save each response to `{run_dir}/per-file-<n>.txt` with the Write tool and build the surface from them, one `--per-file` each and `--fallback-scan` (each names the file that defines its exports, so a `scope.tier_a_include` list that counts none of them does not turn the guards off), with the same baselines as at Quick tier, and `--extraction` when the runner's JSON sent you here (`extraction.fallback.needed`): it adds no name, only its scope and the globs it lists as matching no file, so the guards still run:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
uv run {coverageInputsScript} surface --per-file "{run_dir}/per-file-<n>.txt" [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] --output "{run_dir}/surface.json"
|
|
32
|
+
uv run {coverageInputsScript} surface --per-file "{run_dir}/per-file-<n>.txt" --fallback-scan [--extraction "{run_dir}/extract-full.json"] [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] [--monorepo] [--pattern-reference-intent] [--curated-subset] [--barrel <state>] --output "{run_dir}/surface.json"
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
coverage-check.md §2b scores these responses once it has picked the denominator set.
|
|
@@ -55,7 +55,7 @@ uv run {coverageInputsScript} census --skill-dir "{resolved_skill_package}" --ou
|
|
|
55
55
|
|
|
56
56
|
Bind `{docs_only_mode}` ← `docsOnly`. Exit 1 printed its error on stderr: HALT with it (`halt_reason: "helper-failed"`, phase `coverage-check:census`). §0b sets `{docs_only_mode}` too when source access reaches State 5.
|
|
57
57
|
|
|
58
|
-
**A docs-only run** measures documentation completeness as its Export Coverage, with `analysis_confidence` `docs-only`. It runs only §1, §1a and §1b, the docs-only guard of §2b, §2c's `docsOnly` branch, §4's
|
|
58
|
+
**A docs-only run** measures documentation completeness as its Export Coverage, with `analysis_confidence` `docs-only`. It runs only §1, §1a and §1b, the docs-only guard of §2b, §2c's `docsOnly` branch, §4's `Denominator: docs-only completeness` annotation, §5, §5b and §6. Step 5 passes `docsOnly: true` to the scoring script, which skips Signature Accuracy and Type Coverage at any tier.
|
|
59
59
|
|
|
60
60
|
"**Docs-only skill detected.** Coverage check evaluates documentation completeness rather than source code coverage."
|
|
61
61
|
|
|
@@ -73,6 +73,8 @@ uv run {stageHelperPayloadScript} detect-workspaces --source-root "{source_path}
|
|
|
73
73
|
|
|
74
74
|
Bind `{source_is_monorepo}` ← `is_monorepo` and `{source_workspace_kind}` ← `manifest_kind`. At States 2 to 5, or when the helper does not resolve or the command exits non-zero, the layout is unknown: bind `{source_is_monorepo}` ← false. §5 records the layout.
|
|
75
75
|
|
|
76
|
+
**Clause flags.** Every `surface` command in §2 takes `--monorepo` when `{source_is_monorepo}` is true, and the flags the Source API Surface Definition leaves to you.
|
|
77
|
+
|
|
76
78
|
### 1. Extract Documented Exports from SKILL.md
|
|
77
79
|
|
|
78
80
|
Delegate the reading of the skill under test to a subagent, given the path to SKILL.md and to its `references/` directory when one exists. The subagent reads SKILL.md, and every `references/*.md` file as well when SKILL.md's `## Full` headings are absent or stubs. For each export documented both in the SKILL.md body and in a reference file, it compares the two: parameters (name, type, order, optionality), the return type, and descriptions that contradict (brief against detailed is fine). It returns only this JSON, with no prose and no commentary:
|
|
@@ -118,7 +120,7 @@ Identify the public API surface from the package entry points (see 0b), at the d
|
|
|
118
120
|
|
|
119
121
|
**Exits.** Every `{coverageInputsScript}` and `{scoreSignaturesScript}` command in §2 and §2b exits 0, or exits 1 with its error on stderr: HALT with it (`halt_reason: "helper-failed"`, phase `coverage-check:surface`). `surface` with a `--per-file`, and `score`, also exit 2 when a subagent response breaks the schema: re-dispatch that subagent once with its `violations[]` appended (or correct a result the main thread wrote), save the new response over the old one and run the command again; a second failure HALTs the same way.
|
|
120
122
|
|
|
121
|
-
**Quick Tier (no AST tools):** load `{coverageTiersFile}` and follow its **Quick Tier** section. Signatures cannot be verified at this tier
|
|
123
|
+
**Quick Tier (no AST tools):** load `{coverageTiersFile}` and follow its **Quick Tier** section. Signatures cannot be verified at this tier (§5's render marks them unverified).
|
|
122
124
|
|
|
123
125
|
**Forge Tier (ast-grep available):**
|
|
124
126
|
|
|
@@ -128,15 +130,17 @@ At State 1, run the recipe runner once over the source. Resolve `{extractPublicA
|
|
|
128
130
|
uv run {extractPublicApiHelper} --mode full --source-root "{source_path}" --brief "{forge_data_folder}/{skill_name}/skill-brief.yaml" --tier {detected_tier} --head-cap 0 --output "{run_dir}/extract-full.json"
|
|
129
131
|
```
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
Exit 2 is an input error (a brief the runner cannot read, say) or a failure the runner did not foresee, named in one line on stderr with no JSON: HALT with that line (`halt_reason: "helper-failed"`, phase `coverage-check:surface`). No Fallback Per-File Scan stands in for it: every flag here is fixed, so the error is a defect to surface.
|
|
134
|
+
|
|
135
|
+
Build the surface from it, with the metadata, the brief when the skill has one and, when init.md §2 bound one, the provenance map (the baselines of the §2b candidates and guards; they add no name):
|
|
132
136
|
|
|
133
137
|
```bash
|
|
134
|
-
uv run {coverageInputsScript} surface --extraction "{run_dir}/extract-full.json" --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] --output "{run_dir}/surface.json"
|
|
138
|
+
uv run {coverageInputsScript} surface --extraction "{run_dir}/extract-full.json" --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] [--monorepo] [--pattern-reference-intent] [--curated-subset] --output "{run_dir}/surface.json"
|
|
135
139
|
```
|
|
136
140
|
|
|
137
|
-
The surface holds the names the entry points export, less those defined in a file the brief scopes out (`excluded.outsideScope
|
|
141
|
+
The surface holds the names the entry points export, less those defined in a file the brief scopes out (`excluded.outsideScope`); the recipes' internal matches are never part of it. A Python `__init__.py` nested in the folder of another package that exports a name is no barrel: its names (`excluded.nestedEntries`) leave only the `all` and `root` sets and the umbrella ratio, and `plan` still compares their signatures. When a `scope.include` glob matches no file (`guards.staleScope.fires`), the root exports defined in a file no glob covers and the brief does not exclude are back in it (`guards.staleScope.restored`). A stale `scope.tier_a_include` glob fires the guard too (`guards.staleScope.unmatchedTierAInclude`) and counts no name. §5's render lists `excluded.outsideScope`, `excluded.nestedEntries` and the restored names. Decide what to do from the JSON, not from the exit code:
|
|
138
142
|
|
|
139
|
-
- `extraction.fallback.needed` is true, the runner
|
|
143
|
+
- `extraction.fallback.needed` is true, or the runner printed no JSON on an exit other than 2: load `{coverageTiersFile}` and run its **Fallback Per-File Scan** in place of the rest of this tier.
|
|
140
144
|
- `extraction.status` is `incomplete` (exit 1): keep the surface, read by eye only the files `extraction.readByEye` names, save a per-file result for each, and run `surface` again with them.
|
|
141
145
|
- `extractionGaps[]` names exports no recipe found: they are on the surface, and their signature is read by eye at their `file` and `line`.
|
|
142
146
|
|
|
@@ -160,12 +164,12 @@ Save each response to `{run_dir}/signatures-<n>.txt` with the Write tool. Withou
|
|
|
160
164
|
- **State 2** (`{forge_provenance_map}`, the provenance map init.md §2 bound): the union of its named exports and the `metadata.json` `exports[]` names, with `--fold`, `--keep` and `--fold-prefix` as the Source API Surface Definition's canonicalization says:
|
|
161
165
|
|
|
162
166
|
```bash
|
|
163
|
-
uv run {coverageInputsScript} surface --provenance "{forge_provenance_map}" --metadata "{resolved_skill_package}/metadata.json" [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] [--fold] [--keep <variant>] [--fold-prefix <prefix>] --output "{run_dir}/surface.json"
|
|
167
|
+
uv run {coverageInputsScript} surface --provenance "{forge_provenance_map}" --metadata "{resolved_skill_package}/metadata.json" [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] [--fold] [--keep <variant>] [--fold-prefix <prefix>] [--monorepo] [--pattern-reference-intent] [--curated-subset] --output "{run_dir}/surface.json"
|
|
164
168
|
```
|
|
165
169
|
|
|
166
170
|
Step 5's scoring script reads its `state2` counts, and the report records its fold summary, `canonical.summary`.
|
|
167
|
-
- **State 3**: the `metadata.json` `exports[]` names: `surface --metadata "{resolved_skill_package}/metadata.json" --output "{run_dir}/surface.json"`.
|
|
168
|
-
- **State 4**: the names read remotely by eye, as per-file results: `surface --per-file "{run_dir}/per-file-<n>.json" [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" --output "{run_dir}/surface.json"`.
|
|
171
|
+
- **State 3**: the `metadata.json` `exports[]` names: `surface --metadata "{resolved_skill_package}/metadata.json" [--monorepo] [--pattern-reference-intent] [--curated-subset] --output "{run_dir}/surface.json"`.
|
|
172
|
+
- **State 4**: the names read remotely by eye, as per-file results: `surface --per-file "{run_dir}/per-file-<n>.json" [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--monorepo] [--pattern-reference-intent] [--curated-subset] [--barrel <state>] --output "{run_dir}/surface.json"`.
|
|
169
173
|
|
|
170
174
|
### 2b. Resolve the Denominator and Guard Zero Exports
|
|
171
175
|
|
|
@@ -175,11 +179,9 @@ Load the metadata and provenance-map counts once, for §2c, §4b and the numerat
|
|
|
175
179
|
uv run {coverageInputsScript} metadata --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] --output "{run_dir}/coverage-inputs.json"
|
|
176
180
|
```
|
|
177
181
|
|
|
178
|
-
**Pick the denominator.**
|
|
182
|
+
**Pick the denominator.** `surface.json` `denominator` holds the Source API Surface Definition clause (§0b) `surface` picked: its §2c `branch` (`barrel`, `scalar` or `stack`), `surfaceSet` (the set the barrel branch counts and the signature score reads) and `ladderStep`. When `absentSet` names a set the surface lacks (the scope sets need the brief's globs and names that carry their files, `subpaths` and `root` an extraction), `surfaceSet` is `all` and the `Denominator:` line (§4) adds `({absentSet} set absent: all set used)`. When `rebuild` is `provenance-fold`, rerun State 2's command with no `--metadata`, with `--fold` and `--pattern-reference-intent`, and read its `denominator`.
|
|
179
183
|
|
|
180
|
-
- **
|
|
181
|
-
- **Stratified-scope priority 1**: the scalar branch over `stats.effective_denominator` (`candidates.statsEffectiveDenominator`), unless `guards.deflation.fires`: then the barrel branch over the `scope.include` set (`all` when the brief has no `scope.include`).
|
|
182
|
-
- **A stack skill** (`metadata.json.skill_type == "stack"`): the stack branch, over `coverage-inputs.json` `stack.denominator`. Its own barrel is empty by design, and its `[from skill: …]` citations never make it docs-only. **If `stack.denominator` is 0**: HALT (`halt_reason: "indeterminate-surface"`, phase `coverage-check:denominator`) with `Error: stack composition surface empty: {skill_name} cites no contracts, libraries, or integration pairs, so Export Coverage is undefined. Verify the stack was compiled from at least one constituent skill.` Do not write the Coverage Analysis section; this is an indeterminate state, not a FAIL.
|
|
184
|
+
- **A stack skill** (`branch` `stack`): `coverage-inputs.json` `stack.denominator`. Its own barrel is empty by design, and its `[from skill: …]` citations never make it docs-only. **If `stack.denominator` is 0**: HALT (`halt_reason: "indeterminate-surface"`, phase `coverage-check:denominator`) with `Error: stack composition surface empty: {skill_name} cites no contracts, libraries, or integration pairs, so Export Coverage is undefined. Verify the stack was compiled from at least one constituent skill.` Do not write the Coverage Analysis section; this is an indeterminate state, not a FAIL.
|
|
183
185
|
- **A docs-only skill**: the `docsOnly` branch, over the inventory.
|
|
184
186
|
|
|
185
187
|
**Numerator ground truth.** A declared count equal to the denominator (`stats.exports_documented == stats.effective_denominator`, the inflation signature) may be padded to a tautological 100%. Run the check on every source-based skill; without the signature (a stack and a reference app carry none) it reports `skipped`:
|
|
@@ -198,18 +200,18 @@ Error: indeterminate API surface: 0 exports discovered in source for {skill_name
|
|
|
198
200
|
Export Coverage is undefined, and scoring would yield a vacuous PASS. Fix one of:
|
|
199
201
|
- Set `scope.include` in the brief to point at the package's entry point(s)
|
|
200
202
|
- Add `[EXT:]` citations if this is actually a docs-only skill
|
|
201
|
-
- Verify the skill's
|
|
203
|
+
- Verify the skill's source_root / source_ref resolve to the intended tree
|
|
202
204
|
```
|
|
203
205
|
|
|
204
206
|
Do not write the Coverage Analysis section or attach a score: this is an indeterminate state, not a FAIL. **If `docs_only_mode == true` and the validated inventory documents no item:** HALT the same way with "docs-only skill declares zero items: no API surface to test".
|
|
205
207
|
|
|
206
|
-
**Score the signatures** (Forge, Forge+ and Deep tier at State 1, once §2 compared them). Give `{scoreSignaturesScript}`
|
|
208
|
+
**Score the signatures** (Forge, Forge+ and Deep tier at State 1, once §2 compared them). Give `{scoreSignaturesScript}` `denominator.surfaceSet` as `--surface-set`. Type Coverage then counts that set's types alone, so a type the clause leaves out (a stratified skill's deferred Tier B) lowers neither Export Coverage nor Type Coverage. Give it one `--results` per saved response (the fallback scan's `per-file-<n>.txt`, else the `signatures-<n>.txt`):
|
|
207
209
|
|
|
208
210
|
```bash
|
|
209
211
|
uv run {scoreSignaturesScript} score --inventory "{run_dir}/inventory.json" --surface "{run_dir}/surface.json" --surface-set <set> --results "{run_dir}/signatures-<n>.txt" --output "{run_dir}/signatures.json" --gaps-output "{run_dir}/signature-gaps.json"
|
|
210
212
|
```
|
|
211
213
|
|
|
212
|
-
It writes each mismatch of a compared name to `signature-gaps.json` as a Critical `signature-mismatch` gap titled `Signature mismatch: {name}`, with `export` `{name}`, its Source at `{file}:{line}` and both signatures in its Issue; §5b appends that file to the ledger.
|
|
214
|
+
It writes each mismatch of a compared name to `signature-gaps.json` as a Critical `signature-mismatch` gap titled `Signature mismatch: {name}`, with `export` `{name}`, its Source at `{file}:{line}` and both signatures in its Issue; §5b appends that file to the ledger. §5's render lists its `warnings` (a mismatch for a name never compared, a category with nothing to compare).
|
|
213
215
|
|
|
214
216
|
### 2c. Reconcile Documented vs Source Surface (Deterministic Intersection)
|
|
215
217
|
|
|
@@ -241,7 +243,7 @@ It writes each mismatch of a compared name to `signature-gaps.json` as a Critica
|
|
|
241
243
|
|
|
242
244
|
Exit 2 printed an `INVALID_INPUT` envelope: correct the input it names and run it again. Exit 1: HALT with its stderr (`halt_reason: "helper-failed"`, phase `coverage-check:reconcile`).
|
|
243
245
|
|
|
244
|
-
**A surplus is a signal, not a pass.** The scalar and stack branches count the numerator and the denominator from independent sets, so `documented` can exceed `denominator`: the script then reports `numeratorSurplus` above 0 and `coverageCapped: true`, holds `exportCoverage` at 100 and keeps the raw ratio in `coverageUncapped
|
|
246
|
+
**A surplus is a signal, not a pass.** The scalar and stack branches count the numerator and the denominator from independent sets, so `documented` can exceed `denominator`: the script then reports `numeratorSurplus` above 0 and `coverageCapped: true`, holds `exportCoverage` at 100 and keeps the raw ratio in `coverageUncapped`, and §5's render states the surplus beside both counts. On a skill with no `scope.tier_a_include` glob that matches a file, a large surplus is the deflated-denominator signature: check `guards.deflation` before accepting the 100.
|
|
245
247
|
|
|
246
248
|
**Classify what the script found.** §5b records each of these gaps from the run files:
|
|
247
249
|
|
|
@@ -250,31 +252,25 @@ Exit 2 printed an `INVALID_INPUT` envelope: correct the input it names and run i
|
|
|
250
252
|
- **Stale names** (barrel branch: `stale`): each is a documented name the enumerated source surface lacks. Where ast-grep read the source at the pinned commit (Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` in the `{outputFile}` frontmatter) and init.md §2 bound `{forge_provenance_map}`, check the lines the skill cites for them: resolve `{verifyProvenanceCompletenessHelper}` ← first existing path in `{verifyProvenanceCompletenessProbeOrder}` and run once:
|
|
251
253
|
|
|
252
254
|
```bash
|
|
253
|
-
uv run {verifyProvenanceCompletenessHelper} classify-stale --names "{run_dir}/coverage.json" --provenance "{forge_provenance_map}" --source-root "{source_path}" -o "{run_dir}/stale.json"
|
|
255
|
+
uv run {verifyProvenanceCompletenessHelper} classify-stale --names "{run_dir}/coverage.json" --provenance "{forge_provenance_map}" --source-root "{source_path}" --inventory "{run_dir}/inventory.json" -o "{run_dir}/stale.json"
|
|
254
256
|
```
|
|
255
257
|
|
|
256
|
-
A name it marks `fabricated: true` (every map entry of the name cites a missing file or one that does not define it) is a Critical `fabricated-signature` gap at its `source`. Any other stale name (no
|
|
257
|
-
|
|
258
|
-
### 4. Category Scores and the Denominator Record
|
|
258
|
+
A name it marks `fabricated: true` (every map entry of the name cites a missing file or one that does not define it) is a Critical `fabricated-signature` gap at its `source`. A name with a `defined_at` (the source still declares it there, an import never counting, a module so named counting unless the name's inventory kind rules it out), or whose `declared_in` lists several files of which the skill's `[AST:]`/`[SRC:]` citations on lines naming it cite exactly one, is a documented extra: an Info `observation` gap at that declaration, when `surface.json` has an `extraction` and its `excluded.outsideScope` does not list the name (a brief scoped its file out). Any other stale name (no `defined_at` and no single cited declaring file, one scoped out, no `extraction`, a dotted name, the conditions above not met, a helper that does not resolve or exits non-zero) is a Medium `stale-documentation` gap.
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
### 4. The Denominator Record
|
|
261
261
|
|
|
262
|
-
|
|
263
|
-
- **Signature Accuracy:** `signatures.json` `signatureAccuracy` (Forge, Forge+ and Deep tier at State 1; "N/A" otherwise)
|
|
264
|
-
- **Type Coverage:** `signatures.json` `typeCoverage`, over the types of the set §2b scored; "N/A" where Signature Accuracy is
|
|
265
|
-
|
|
266
|
-
Record the denominator source in the Coverage Analysis section with the annotation the matching clause of `{sourceAccessProtocol}` specifies: `Denominator: {barrel | stratified (…) | multi-entry (…) | specific-modules (…) | pattern-reference (…) | stack composition (…) | pattern-surface (…) | docs-only completeness}`, and the `canonical.summary` line when `surface.json` has a fold. When stratified-scope or multi-entry resolution picked the denominator, append this block right after the `Denominator:` line, with every count from `surface.json` `candidates` (`absent` for a null one):
|
|
262
|
+
Record the denominator source in the Coverage Analysis section with the annotation `{sourceAccessProtocol}` gives `denominator.clause` (§4b gives a stack's and a reference app's): `Denominator: {barrel | empty barrel (…) | stratified (…) | multi-entry (…) | specific-modules (…) | pattern-reference (…) | stack composition (…) | pattern-surface (…) | docs-only completeness}`, and the `canonical.summary` line when `surface.json` has a fold. When `denominator.clause` is `stratified-scope` or `multi-entry`, append this block after the `Denominator:` line, with every count from `surface.json` `candidates` (`absent` for a null one):
|
|
267
263
|
|
|
268
264
|
```markdown
|
|
269
265
|
**Denominator Candidates** (stratified-scope audit trail):
|
|
270
|
-
- `stats.effective_denominator`: {statsEffectiveDenominator | absent} {← chosen
|
|
271
|
-
- `scope.tier_a_include` union: {tierAIncludeUnion | absent} {← chosen if
|
|
272
|
-
- `scope.include` union: {scopeIncludeUnion | absent} {← chosen if
|
|
273
|
-
- exports-map subpath union: {subpathUnion | absent} {← chosen if the
|
|
266
|
+
- `stats.effective_denominator`: {statsEffectiveDenominator | absent} {← chosen on the scalar branch}
|
|
267
|
+
- `scope.tier_a_include` union: {tierAIncludeUnion | absent} {← chosen if the barrel branch's set}
|
|
268
|
+
- `scope.include` union: {scopeIncludeUnion | absent} {← chosen if the barrel branch's set}
|
|
269
|
+
- exports-map subpath union: {subpathUnion | absent} {← chosen if the barrel branch's set}
|
|
274
270
|
- root barrel: {rootBarrel | absent} {secondary candidate: root-barrel-vs-subpath-union audit}
|
|
275
271
|
```
|
|
276
272
|
|
|
277
|
-
The guards raise coverage gaps too, which §5b records from `surface.json`: `guards.deflation.fires` is a Medium `metadata-drift` gap
|
|
273
|
+
The guards raise coverage gaps too, which §5b records from `surface.json`: `guards.deflation.fires` is a Medium `metadata-drift` gap, `guards.inflation.fires` a Medium `denominator-inflation` gap and `guards.staleScope.fires` a Medium `brief-scope-stale` gap at the brief.
|
|
278
274
|
|
|
279
275
|
### 4b. Metadata Export-Count Coherence Cross-Check
|
|
280
276
|
|
|
@@ -308,7 +304,13 @@ This section reports none of the helper's other findings, and neither gap blocks
|
|
|
308
304
|
|
|
309
305
|
### 5. Write the Coverage Analysis Section
|
|
310
306
|
|
|
311
|
-
Write the **Coverage Analysis** section in place of the template's `## Coverage Analysis` heading and the placeholder comment under it, in `{outputFile}`.
|
|
307
|
+
Write the **Coverage Analysis** section in place of the template's `## Coverage Analysis` heading and the placeholder comment under it, in `{outputFile}`. `{reconcileScript}` renders its counts, per-export table and category scores from the run files (leave out `--surface` on a docs-only run and `--signatures` when §2b scored none):
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
uv run {reconcileScript} render --coverage "{run_dir}/coverage.json" [--surface "{run_dir}/surface.json"] [--signatures "{run_dir}/signatures.json"]
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Exit 0: write its output unchanged where the section below places it. Exit 2 is a usage error: correct the command and run it again. Exit 1 printed its error on stderr: HALT with it (`halt_reason: "helper-failed"`, phase `coverage-check:render`).
|
|
312
314
|
|
|
313
315
|
```markdown
|
|
314
316
|
## Coverage Analysis
|
|
@@ -317,32 +319,12 @@ Write the **Coverage Analysis** section in place of the template's `## Coverage
|
|
|
317
319
|
**Source Access:** {analysis_confidence} (full | provenance-map | metadata-only | remote-only | docs-only)
|
|
318
320
|
**Source Path:** {source_path}
|
|
319
321
|
**Workspace Layout:** {source_workspace_kind | single package | unknown (no local source, or the §0b helper failed)}
|
|
320
|
-
**Files Analyzed:** {count}
|
|
321
322
|
**Denominator:** {the §4 annotation}
|
|
323
|
+
{the Denominator Candidates block, when §4 adds one}
|
|
322
324
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
| Export | Type | Documented | Signature | Source Location | Status |
|
|
326
|
-
|--------|------|-----------|-----------|-----------------|--------|
|
|
327
|
-
| {name} | {kind} | yes/no | yes/no/unverified | {file}:{line} | PASS/FAIL/WARN |
|
|
328
|
-
|
|
329
|
-
### Coverage Summary
|
|
330
|
-
|
|
331
|
-
- **Exports Found:** {`denominator`; docs-only: the documented items}
|
|
332
|
-
- **Documented:** {`documented`} ({`exportCoverage`}%)
|
|
333
|
-
- **Missing Documentation:** {`missingCount`; docs-only: the incomplete items, each with the fields it lacks}
|
|
334
|
-
- **Signature Mismatches:** {the length of `signatures.json` `mismatches`}
|
|
335
|
-
- **Stale Documentation:** {`staleCount`}
|
|
336
|
-
- **Scoring Warnings:** each entry of `signatures.json` `warnings`; omit this row when there is none
|
|
337
|
-
- **Numerator Surplus:** {`numeratorSurplus`}, uncapped coverage {`coverageUncapped`}%; only when `coverageCapped: true` (then `Missing Documentation: 0` is a floored residual)
|
|
338
|
-
|
|
339
|
-
### Category Scores
|
|
325
|
+
{the render output}
|
|
340
326
|
|
|
341
|
-
|
|
342
|
-
|----------|-------|
|
|
343
|
-
| Export Coverage | {N}% |
|
|
344
|
-
| Signature Accuracy | {N}% or N/A |
|
|
345
|
-
| Type Coverage | {N}% or N/A |
|
|
327
|
+
{the notes §2 to §4b add to this section}
|
|
346
328
|
```
|
|
347
329
|
|
|
348
330
|
### 5b. Record the Coverage Gaps
|
|
@@ -374,6 +356,6 @@ Rely on the JSON of each command:
|
|
|
374
356
|
|
|
375
357
|
### 6. Report Coverage Results
|
|
376
358
|
|
|
377
|
-
Report the coverage result to the user: the {forge_tier}
|
|
359
|
+
Report the coverage result to the user from §5's render: the {forge_tier} tier, its **Files Analyzed** count, the three category scores and the issue count. Full details are in the Coverage Analysis section.
|
|
378
360
|
|
|
379
361
|
Append `'coverage-check'` to `stepsCompleted` in the `{outputFile}` frontmatter, then load and execute {nextStepFile}.
|
|
@@ -243,18 +243,16 @@ Read `{sidecar_path}/preferences.yaml`. If `tier_override` is set and is a valid
|
|
|
243
243
|
Read `metadata.json` to extract:
|
|
244
244
|
- `name` — display name
|
|
245
245
|
- `skill_type` — single or stack (needed for mode detection)
|
|
246
|
-
- `source_path`
|
|
246
|
+
- `source_root`: the source tree the skill was extracted from; bind `{source_path}` ← `source_root` (null or absent for a docs-only skill and a stack skill)
|
|
247
247
|
- `source_commit` — pinned commit the skill was extracted against (may be null for docs-only skills, `"local"` for non-git sources, or a per-repo map for stack skills)
|
|
248
248
|
- `source_ref` — pinned ref (tag/branch/`HEAD`) used at extraction time
|
|
249
249
|
- `generation_date` — when skill was generated
|
|
250
250
|
- `confidence_tier` — tier used during creation
|
|
251
251
|
- `generated_by`: the SKF workflow that built the skill (coverage-check §2 reads it at Quick tier)
|
|
252
252
|
|
|
253
|
-
If source path override was provided as optional input, use that instead.
|
|
254
|
-
|
|
255
253
|
### 5b. Verify Workspace HEAD Matches Pinned Commit
|
|
256
254
|
|
|
257
|
-
Test-skill reads `source_path` during coverage and coherence analysis. If the local workspace has drifted from `metadata.source_commit`, gap and signature-mismatch findings silently reflect the drifted tree, not the skill's pinned source: false positives that downstream update-skill runs may then "repair" by corrupting correct documentation.
|
|
255
|
+
Test-skill reads `{source_path}` during coverage and coherence analysis. If the local workspace has drifted from `metadata.source_commit`, gap and signature-mismatch findings silently reflect the drifted tree, not the skill's pinned source: false positives that downstream update-skill runs may then "repair" by corrupting correct documentation.
|
|
258
256
|
|
|
259
257
|
The guard runs through `{checkWorkspaceDriftHelper}`, never through `git` commands run by hand. Resolve it ← first existing path in `{checkWorkspaceDriftProbeOrder}`. If neither path exists, HALT (`halt_reason: "helper-missing"`, phase `init:workspace-drift`) with: "Error: cannot locate skf-check-workspace-drift.py at `{project-root}/_bmad/skf/shared/scripts/skf-check-workspace-drift.py` or `{project-root}/src/shared/scripts/skf-check-workspace-drift.py`. Install the SKF module or run from a development checkout with src/ present." No test report is written.
|
|
260
258
|
|
|
@@ -264,7 +262,7 @@ Run it once per source tree, from `{project-root}`, passing `--allow-drift` only
|
|
|
264
262
|
uv run {checkWorkspaceDriftHelper} "{tree}" --pinned-commit "{pinned_commit}" [--source-ref "{source_ref}"] [--allow-drift] --workflow test-skill
|
|
265
263
|
```
|
|
266
264
|
|
|
267
|
-
- **One tree:** `{tree}` is `{source_path}`, `{pinned_commit}` is `metadata.source_commit` (an empty string when it is null), and `{source_ref}` is `metadata.source_ref` when it is set.
|
|
265
|
+
- **One tree:** `{tree}` is `{source_path}`, `{pinned_commit}` is `metadata.source_commit` (an empty string when it is null), and `{source_ref}` is `metadata.source_ref` when it is set. When metadata.json records no `source_root` (null, empty or absent), make no call and log `workspace_drift_check: skipped (metadata.json records no source_root)`.
|
|
268
266
|
- **A stack skill** (`metadata.source_commit` is a `{repo_path: commit}` map): one call per entry, with that repo path as `{tree}`, its commit as `{pinned_commit}`, and the same key's value of `metadata.source_ref` as `{source_ref}` when that is a map too. Check every repo: do not skip stack skills.
|
|
269
267
|
- Make no call for a tree that is not on disk (coverage then reads the provenance map, State 2 of the source access protocol), and log `workspace_drift_check: skipped (no local source at {tree})`. The helper itself skips a tree with no pinned commit (`""` or `"local"`) and one that is not a git working tree.
|
|
270
268
|
|
|
@@ -65,7 +65,7 @@ Load `{outputFormatsFile}` for the Ledger Record Format and the remediation qual
|
|
|
65
65
|
Every run that reaches this step, a blocked one included, ends through one sequence, and a check that fails stops it before anything is published:
|
|
66
66
|
|
|
67
67
|
1. §4b records the discovery outcome in the ledger (a blocked run skips it).
|
|
68
|
-
2. §4c renders the Gap Report from the ledger, checks that every stage ran and wrote its section and that the health check is installed, then publishes the report, writes the result contract and runs the on_complete hook.
|
|
68
|
+
2. §4c renders the Gap Report from the ledger, checks that every stage ran and wrote its section and that the health check is installed, then builds the result payload, publishes the report, writes the result contract and runs the on_complete hook.
|
|
69
69
|
3. §6 presents the result, §6b keeps the headless envelope for the run's last line, and §7 releases the run lock and hands over to the health check.
|
|
70
70
|
|
|
71
71
|
### 4b. Discovery Testing
|
|
@@ -213,27 +213,29 @@ uv run {runLockHelper} acquire --lock "{forge_version}/.test-skill.lock" --owner
|
|
|
213
213
|
|
|
214
214
|
The run lock renewed here stays held until §7 releases it, so no other run publishes over this run's report or result files meanwhile.
|
|
215
215
|
|
|
216
|
-
**
|
|
216
|
+
**Build the result payload** before the report takes its public name. Every value in it comes from the run's own records: `{resultContextScript}` (it resolves relative to the skill root) reads the report frontmatter, the gap ledger and the scoring output, checks the verdict fields score.md §7 wrote (`testResult`, `score`, `threshold` and the fallback fields) against the scoring output, derives `verdict`, `exit_code` and `next_workflow` from `testResult`, and writes the shared emitter's payload, which names the report by its public name. Pass `--score` for a scored run (a run the hard gate blocked has no scoring output), `--no-health-check` when that flag was given, and `--warning` when SKILL.md On Activation step 3 kept a `{customization_resolver_unavailable}` reason: first write `customization_resolver_unavailable: {customization_resolver_unavailable}` to `{run_dir}/resolver-warning.txt` with a file write, never `echo` (the reason can hold quotes, backticks or `$( )`), and pass that file's text (the emitter adds the warnings a stage recorded in the run's sink):
|
|
217
217
|
|
|
218
218
|
```bash
|
|
219
|
-
|
|
219
|
+
uv run {resultContextScript} --report "{report_file}" --report-path "{publishedReportFile}" --ledger "{ledgerFile}" [--score "{run_dir}/score.json"] [--no-health-check] [--warning "$(cat "{run_dir}/resolver-warning.txt")"] --output "{run_dir}/result-context.json"
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
Bind `{emit_target}` ← `target` from its JSON (`stderr` for a run the hard gate blocked, else `stdout`). Exit 2: correct the command and run it again. Exit 1: HALT with its `error` (`halt_reason: "helper-failed"`, phase `report:result-contract`); the report keeps its in-progress name, so export-skill never reads a verdict the scoring output did not settle.
|
|
223
223
|
|
|
224
|
-
**
|
|
224
|
+
**Publish the report.** Set `health_check_dispatched` in the `{outputFile}` frontmatter: `false` when `no_health_check` is true (the `--no-health-check` flag), else `true`, since the health check now runs with no menu before it. The checks passed, so the report takes its public name, the one export-skill and update-skill read, a blocked run's included. From `{project-root}`, run:
|
|
225
225
|
|
|
226
226
|
```bash
|
|
227
|
-
|
|
227
|
+
mv "{report_file}" "{publishedReportFile}"
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
and bind `{report_file}` ← `{publishedReportFile}`. If the move fails, HALT (`halt_reason: "write-failed"`, phase `report:publish`, `"path": "{publishedReportFile}"`): no result file has been written, so the previous run's verdict still stands.
|
|
231
|
+
|
|
232
|
+
**Write the result contract.** The shared emitter writes it, per `{outputContractSchema}`. In every mode, run:
|
|
231
233
|
|
|
232
234
|
```bash
|
|
233
235
|
uv run {emitEnvelopeHelper} emit --workflow skf-test-skill --run-dir "{run_dir}" --result-dir "{forge_version}" --target {emit_target} < "{run_dir}/result-context.json"
|
|
234
236
|
```
|
|
235
237
|
|
|
236
|
-
It writes `{forge_version}/skf-test-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name) and then its `skf-test-skill-result-latest.json` copy (stable path for pipeline consumers: a copy, not a symlink), each atomically, so a partial write is never observable; stamps the timestamp, `run_id`, `headless_decisions` and `warnings` into both; and prints the `SKF_TEST_RESULT_JSON:` line. Bind `{result_line}` ← that line and `{result_path}` ← its `result_path`. A run the hard gate blocked writes the record too, as a FAIL (`status: "error"`, `verdict: "FAIL"`, `halt_reason: "hard-gate-blocked"`, `exit_code` 2), so `skf-test-skill-result-latest.json` never holds an earlier run's verdict. A result file that could not be written leaves `result_path` null and a `result_file_write_failed` warning in the line, and the run still finishes. If the emitter exits non-zero, run
|
|
238
|
+
It writes `{forge_version}/skf-test-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name) and then its `skf-test-skill-result-latest.json` copy (stable path for pipeline consumers: a copy, not a symlink), each atomically, so a partial write is never observable; stamps the timestamp, `run_id`, `headless_decisions` and `warnings` into both; and prints the `SKF_TEST_RESULT_JSON:` line. Bind `{result_line}` ← that line and `{result_path}` ← its `result_path`. A run the hard gate blocked writes the record too, as a FAIL (`status: "error"`, `verdict: "FAIL"`, `halt_reason: "hard-gate-blocked"`, `exit_code` 2), so `skf-test-skill-result-latest.json` never holds an earlier run's verdict. A result file that could not be written leaves `result_path` null and a `result_file_write_failed` warning in the line, and the run still finishes. If the emitter exits non-zero, run it once more; if it fails again, HALT (`halt_reason: "write-failed"`, phase `report:result-contract`) with its stderr: the run's result contract could not be written. `references/invocation-contract.md` lists the record's fields.
|
|
237
239
|
|
|
238
240
|
Once the result files are written, append `'report'` to `stepsCompleted` in the `{outputFile}` frontmatter (it stays out of the expected set above).
|
|
239
241
|
|
|
@@ -24,15 +24,16 @@ This table is the single source of truth for classifying a gap. Every stage reco
|
|
|
24
24
|
| High | `inaccurate-reference` | Inaccurate reference (contextual mode): the target exists but does not match, such as a type the cited module does not export, a signature that differs, or an integration pattern that does not match the implementation (coherence-check §4) |
|
|
25
25
|
| High | `reference-escape` | A reference whose real path leaves the skill, its source and, for a stack, the skills output folder (coherence-check §4) |
|
|
26
26
|
| High | `split-body-mismatch` | The SKILL.md body and a `references/` file document one export differently (coverage-check §1b) |
|
|
27
|
-
| High | `numerator-inflation` | `stats.exports_documented
|
|
27
|
+
| High | `numerator-inflation` | A declared count (`stats.exports_documented`, or the provenance map's distinct entry names) equals the denominator while declared exports are absent from SKILL.md and `references/` (coverage-check §4b) |
|
|
28
28
|
| High | `structural` | Naive mode: a missing required section, an unbalanced code fence, or a documented async or sync behaviour its example contradicts (coherence-check §2.1, §2.2, §2.5) |
|
|
29
29
|
| High | `discovery` | Discovery test: at most 1 of 3 realistic prompts routed to the skill (report §4b.3) |
|
|
30
30
|
| Medium | `missing-export` | Missing export: an exported function or class the skill does not document. It lowers Export Coverage, and the threshold decides the verdict |
|
|
31
31
|
| Medium | `missing-type` | Missing type or interface documentation |
|
|
32
|
-
| Medium | `stale-documentation` | A documented export the enumerated source surface lacks that is not a fabricated signature:
|
|
32
|
+
| Medium | `stale-documentation` | A documented export the enumerated source surface lacks that is not a fabricated signature and not a documented extra: classify-stale found no single file that declares it (an import, a re-export or an indented local does not count, nor does a module so named when the inventory gives the name a `function`, `hook`, `class`, `interface`, `enum` or `type` kind, as the Info `observation` row says) and the skill's citations on the lines that name it cite no single one of the files that do, it is a dotted name, the brief scoped its file out (`excluded.outsideScope`), or the run could not look: the source is not Python or TS/JS, classify-stale did not run (it needs the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` and a bound provenance map), or `surface.json` has no complete `extraction` (coverage-check §2c) |
|
|
33
33
|
| Medium | `migration-section` | Migration section present/absent mismatch with T2-future annotation data (Deep tier; Case 1 and Case 3 of `migration-section-rules.md`) |
|
|
34
|
-
| Medium | `metadata-drift` | Metadata drift: intra-cluster export counts diverge by more than 10% (barrel: `stats.exports_public_api` vs `exports[].length`; documented surface: `stats.exports_documented` vs the provenance-map named-export count), or denominator deflation: the re-derived source barrel exceeds `effective_denominator` by more than 25% and the brief has no `scope.tier_a_include` (coverage-check §4 and §4b) |
|
|
35
|
-
| Medium | `denominator-inflation` | Denominator inflation: the stratified-scope `scope.include` union exceeds the provenance-map entry count by more than 25% (brief
|
|
34
|
+
| Medium | `metadata-drift` | Metadata drift: intra-cluster export counts diverge by more than 10% (barrel: `stats.exports_public_api` vs `exports[].length`; documented surface: `stats.exports_documented` vs the provenance-map named-export count), or denominator deflation: the re-derived source barrel exceeds `effective_denominator` by more than 25% and the brief has no `scope.tier_a_include` glob that counts a public name (coverage-check §4 and §4b) |
|
|
35
|
+
| Medium | `denominator-inflation` | Denominator inflation: the stratified-scope `scope.include` union exceeds the provenance-map entry count by more than 25% (the brief has no `scope.tier_a_include` glob that counts a public name) |
|
|
36
|
+
| Medium | `brief-scope-stale` | Stale brief scope: a `scope.include` glob matches no file in the source tested, so the surface restores the root exports defined in a file no glob covers that the brief does not exclude, or a `scope.tier_a_include` glob matches no file in the source tested, so it counts no name (coverage-check §4) |
|
|
36
37
|
| Medium | `scripts-assets` | A `scripts/` or `assets/` directory exists but SKILL.md has no Scripts & Assets section (coherence-check §2.7 and §4) |
|
|
37
38
|
| Medium | `integration-pattern` | Incomplete integration pattern (contextual mode): one entry of coherence-check §5's `incomplete_patterns` |
|
|
38
39
|
| Medium | `structural` | Naive mode: an opening code fence without a language tag, an exported function or method no usage-family section or reference file names, or a table row whose column count differs from its header (coherence-check §2.3, §2.4, §2.6) |
|
|
@@ -43,7 +44,7 @@ This table is the single source of truth for classifying a gap. Every stage reco
|
|
|
43
44
|
| Low | `metadata` | Missing optional metadata or examples |
|
|
44
45
|
| Low | `description` | Description trigger optimization recommended (third-person voice, negative triggers, or keyword coverage gaps), a Tessl Review description suggestion included |
|
|
45
46
|
| Low | `external-validator` | A skill-check diagnostic or a Tessl Review validation finding the run left unresolved (external-validators §5b) |
|
|
46
|
-
| Info | `observation` | Style suggestions, non-blocking observations |
|
|
47
|
+
| Info | `observation` | Style suggestions, non-blocking observations, and a documented extra: a documented name outside the enumerated surface that is not a fabricated signature and that the source still declares at column 0 (a module so named counts, unless the inventory gives the name a `function`, `hook`, `class`, `interface`, `enum` or `type` kind, no provenance map entry of the name types it a `module` or `package` and the module is not a TS/JS file that is itself the export, with an `export default` or a `module.exports =`; among several declaring files, the one the skill cites on a line naming it), found only in a Python or TS/JS source where classify-stale ran (the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` and a bound provenance map) and `surface.json` has a complete `extraction`: `Documented extra: {name}`, at that line, which update-skill never routes (coverage-check §2c) |
|
|
47
48
|
| Info | `provenance-unverified` | Provenance line not verified: the line-check rules found no definition line (coverage-check §4c) |
|
|
48
49
|
| Info | `migration-section` | Historical migration content in Section 4b with no T2-future annotation (Case 2 of `migration-section-rules.md`) |
|
|
49
50
|
| Info | `multi-denominator` | Multi-denominator reporting: barrel and documented-surface clusters diverge by design (>10% cross-cluster) |
|
|
@@ -6,32 +6,32 @@
|
|
|
6
6
|
|
|
7
7
|
**Source API surface** = the package's top-level public exports. These are the symbols reachable from the primary entry point without importing internal modules:
|
|
8
8
|
|
|
9
|
-
- **Python:** symbols exported in `__init__.py` (including re-exports)
|
|
9
|
+
- **Python:** symbols exported in the package root's `__init__.py` (including re-exports), excluding private (`_prefixed`) names. A sub-package behind a folder with no `__init__.py` (a FastAPI `routers/__init__.py`, say) is not a second package root when the folder of another package root that exports a name holds it, unless `scope.include` or `scope.tier_a_include` lists that `__init__.py`: its names stay out of the `all` and `root` sets (`excluded.nestedEntries`), while the `scope.include` and `tier_a_include` sets still count them and their signatures are still compared
|
|
10
10
|
- **TypeScript/JavaScript:** named exports from `index.ts` / `index.js` — exclude unexported locals
|
|
11
11
|
- **Go:** exported identifiers (capitalized) from the package's public-facing files
|
|
12
12
|
- **Rust:** items in `pub use` from `lib.rs` or `mod.rs`
|
|
13
13
|
|
|
14
|
-
The clauses below decide which part of that surface is the coverage denominator.
|
|
14
|
+
The clauses below decide which part of that surface is the coverage denominator. `load-coverage-inputs.py surface` picks the clause from the brief (`{forge_data_folder}/{skill_name}/skill-brief.yaml`), `{source_is_monorepo}` (coverage-check.md §0b, passed as `--monorepo`), the extraction and `metadata.json`, and writes it to `surface.json` `denominator` with the branch, the name set, the rung it takes (coverage-check.md §2b) and the entry points' state it read (`barrel`), beside the counts each clause names (`sets`, `candidates` and `guards`). Three of its inputs are yours to pass as flags, the last two only where no file gives them:
|
|
15
15
|
|
|
16
|
-
- **
|
|
16
|
+
- **Pattern-reference intent** (`--pattern-reference-intent`): `scope.notes` says the skill demonstrates an integration pattern rather than a library API ("Reference app, not a library", "pattern-reference", "embedded-pattern skill", "skill value is the … pattern"; the author's words are authoritative).
|
|
17
|
+
- **A curated monorepo subset** (`--curated-subset`), where the extraction has no `counts.curated_subset` (Quick tier, the fallback per-file scan, States 2 to 4): `{source_is_monorepo}` is true and the brief's `scope.include` lists a curated subset rather than the full workspace.
|
|
18
|
+
- **The entry points' state** (`--barrel`), where no complete extraction or quick output gives it (the fallback per-file scan when the runner listed entry points, Quick tier read by eye, State 4): `none` when no barrel file is at any recognized entry-point path (`__init__.py`, `index.ts`/`index.js`, `lib.rs`, `mod.rs`), `empty` when the primary entry point exports nothing (`export {};`, an empty `__init__.py`, a `lib.rs` with no `pub use`), else `barrel`.
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
The recipe runner already re-derives each barrel (multi-line re-export blocks, star re-exports and a Rust `pub use` chain included) and counts each name once, at the file that defines it, so a barrel that only re-exports adds nothing of its own. Without the runner's names (Quick tier, the fallback per-file scan, States 2 to 4) the script builds the `scope.include` and `tier_a_include` sets from the files the names carry and the brief's globs (the runner's JSON the fallback scan passes adds only its scope and stale globs), and there is no `subpaths` or `root` set (coverage-check.md §2b). Never re-count a barrel by hand. A `scope.tier_a_include` glob counts a name only in a file a `scope.include` glob also matches, and one that matches no file (`guards.staleScope.unmatchedTierAInclude`, a brief older than the source) counts none: when the tier A globs count no name, `surface.json` has no `tier_a_include` set. Each clause, by its `denominator.clause`, with what stays yours:
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
2. Otherwise the `tier_a_include` set when the brief has `scope.tier_a_include`, the narrow list of the surface the brief intends to document.
|
|
22
|
-
3. Otherwise the `scope.include` set, **subject to the inflation guard**: `guards.inflation` compares it with the provenance-map entry count, and when `guards.inflation.fires` (above `guards.thresholds.inflationPct` percent) the coarse globs sweep in internal infrastructure the brief did not mean to document, a Medium `denominator-inflation` gap pointing at `scope.tier_a_include`. When `guards.umbrella.umbrella` is true (the root barrel is mostly re-exports), recommend `stats.effective_denominator` as the remediation instead.
|
|
22
|
+
- **Empty-barrel packages (`empty-barrel`; copy-paste or subpath-only distribution):** the primary entry point is empty, so the denominator is the `scope.include` set. The brief's `scope.notes` should say the distribution model is by design; when it does, treat the empty barrel as intended rather than a bug. With no brief and an empty barrel, set `analysis_confidence: docs-only` and report that the source API surface could not be determined. Annotate: `Empty barrel, denominator: scope.include union ({N} exports)`.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
- **Stratified-scope monorepo packages (`stratified-scope`; curated subsets of multi-package repos):** the denominator reflects only the authored surface, not the monorepo's global export count: the `stats.effective_denominator` Create Skill stored, unless `guards.deflation.fires` (a Medium `metadata-drift` gap, coverage-check.md §4: a legitimate scope reduction goes in the brief, through `scope.tier_a_include` or `scope.exclude`, never into `metadata.stats`), then the `tier_a_include` and `scope.include` sets. With no surface read from the source (`guards.deflation.applicable` false), annotate the report `effective_denominator used unverified: no source access to re-derive the barrel`. When `guards.inflation.fires` (the coarse globs sweep in internal infrastructure, a Medium `denominator-inflation` gap) and `guards.umbrella.umbrella` is true (the root barrel is mostly re-exports), recommend `stats.effective_denominator` rather than `scope.tier_a_include`. Leave `analysis_confidence` unchanged. Annotate the report: `Stratified scope, denominator: {effective_denominator | tier_a_include union | scope.include union} ({N} files matched, {M} exports union)`. `scope.notes` should document the stratification (for example "Tier A: fully documented; Tier B: deferred to references; Tier C: excluded"); when it does, treat the curated subset as intended, not a scope gap.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
- **Standard barrel (`standard-barrel`, the `all` set):** `full-library` skills, single-package repositories and briefs where the full monorepo is intentionally in scope. A `scope.include` glob that matches no file (a brief written for an older layout of the source) does not shrink that barrel: `guards.staleScope` restores the root exports defined in a file no glob covers, unless `scope.exclude` or a `scope-expansion` amendment whose latest action is `skipped` or `demoted-include` matches the file. The stale glob is a Medium `brief-scope-stale` gap (coverage-check.md §4), which also names each `scope.tier_a_include` glob that matches no file.
|
|
27
27
|
|
|
28
|
-
- **Pattern-reference apps (non-library source):** a single-package repo that demonstrates an integration pattern rather than distributing a library API.
|
|
28
|
+
- **Pattern-reference apps (`pattern-reference`; non-library source):** a single-package repo that demonstrates an integration pattern rather than distributing a library API. The denominator is the `tier_a_include` set when `surface.json` has it, else the canonicalized provenance map, since `skf-create-skill`'s extraction already curated the map to the authored pattern surface. Leave `analysis_confidence` unchanged. Annotate: `Pattern-reference, denominator: {tier_a_include union | canonicalized provenance-map count} ({N} pattern surfaces)`.
|
|
29
29
|
|
|
30
|
-
- **Single-crate curated subset (`
|
|
30
|
+
- **Single-crate curated subset (`specific-modules`):** a single-package repo whose brief carves a subset of the crate's public surface with `scope.include` and `scope.exclude`: the in-scope reachable barrel, or the `stats.effective_denominator` Create Skill stored for it, under the same deflation guard. Annotate: `Specific-modules subset, denominator: in-scope reachable barrel ({N} items from {M} modules, after scope.include/exclude filtering)`.
|
|
31
31
|
|
|
32
|
-
- **Multi-entry (exports-map) packages:** an in-scope `package.json` declares an `exports` map with more than one non-root subpath
|
|
32
|
+
- **Multi-entry (exports-map) packages (`multi-entry`):** an in-scope `package.json` declares an `exports` map with more than one non-root subpath (`extraction.arms.multi_subpath_exports`). Installers reach every published subpath, so the denominator is the `subpaths` set (wildcard subpaths are left out: their surface is unbounded), or a stored count or `tier_a_include` set ahead of it. The root-barrel count (`candidates.rootBarrel`) is the secondary candidate in coverage-check.md §4's Denominator Candidates block. Annotate: `Multi-entry (exports-map), denominator: {effective_denominator | tier_a_include union | subpath union} ({N} subpaths resolved, {M} exports union; root barrel: {R})`.
|
|
33
33
|
|
|
34
|
-
Internal module symbols are **excluded** from the coverage denominator unless they are explicitly documented in SKILL.md (in which case they count as documented extras, not missing coverage): the surface holds only the names the entry points export, and the extraction's `internal` names never reach it.
|
|
34
|
+
Internal module symbols are **excluded** from the coverage denominator unless they are explicitly documented in SKILL.md (in which case they count as documented extras, not missing coverage): the surface holds only the names the entry points export, and the extraction's `internal` names never reach it. A documented extra reaches coverage-check.md §2c as a stale name. In a Python or TS/JS source, on a run where §2c runs `classify-stale` (the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` and a bound provenance map) and whose `surface.json` came from a complete extraction, it is an Info `observation` at the line where `classify-stale` finds the source still declares it (`defined_at`, or among several declaring files the one the skill cites on a line naming it), which update-skill never routes. Otherwise, and for a name a brief scopes out (`excluded.outsideScope`), it stays a Medium `stale-documentation` gap.
|
|
35
35
|
|
|
36
36
|
This matches the extraction-patterns.md convention used during skill creation: coverage measures how well SKILL.md documents what users actually import, not the entire internal codebase.
|
|
37
37
|
|
|
@@ -39,10 +39,7 @@ This matches the extraction-patterns.md convention used during skill creation: c
|
|
|
39
39
|
|
|
40
40
|
A provenance map may hold **bookkeeping variants** of one export, artifacts of how the library structures its registry (Storybook's component-plus-story decomposition, accessibility renderer shadowing, exact-match renderer disambiguation). Counting them as separate exports inflates the denominator with names that are structural duplicates of a documented base export. When a clause takes its denominator from the provenance map, pass `--fold` to `surface`: it folds `X_def`, `X_exact` and `a11y_X` back to `X`, and `<prefix>X` back to `X` for each renderer prefix you pass as `--fold-prefix` (`mobile_`, `web_`, `ssr_`, only folded when `X` is an entry too), and reports the fold summary in `canonical.summary` for the report. This matches the convention `skf-create-skill` records in `metadata.json.stats.effective_denominator_source`: the base form is authoritative, the variant a sibling record.
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- A variant the library's public surface genuinely distinguishes (an `a11y_Checkbox` documented and installed on its own) is a real export: pass it as `--keep`. Check SKILL.md for explicit documentation of the variant first; when in doubt, keep it and report both forms.
|
|
45
|
-
- When `stats.effective_denominator` is present and the raw entry count matches it, the map is already canonical: do not fold. When drift remains after folding (raw 222, canonical 215, `effective_denominator` 216), record the residual as an unexplained-reconciliation note; do not invent fold rules to close it.
|
|
42
|
+
A map whose raw count equals `stats.effective_denominator` is already canonical: `surface` folds nothing (`canonical.skipped`). A non-zero `canonical.residual` after folding (raw 222, canonical 215, `effective_denominator` 216) is an unexplained-reconciliation note; do not invent fold rules to close it. One judgment stays here: a variant the library's public surface genuinely distinguishes (an `a11y_Checkbox` documented and installed on its own) is a real export: pass it as `--keep`. Check SKILL.md for explicit documentation of the variant first; when in doubt, keep it and report both forms.
|
|
46
43
|
|
|
47
44
|
## Source Access Resolution
|
|
48
45
|
|