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
|
@@ -180,7 +180,7 @@ If `confidence` is `low` (or `unknown` is returned for `language`) and no `langu
|
|
|
180
180
|
|
|
181
181
|
**Resolve `{extractPublicApiHelper}`** from `{extractPublicApiProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
182
182
|
|
|
183
|
-
Identify the public API surface. **Delegate the parsing to `{extractPublicApiHelper}` whenever the detected language is supported:** it finds the entry files, parses the manifest
|
|
183
|
+
Identify the public API surface. **Delegate the parsing to `{extractPublicApiHelper}` whenever the detected language is supported:** it finds the entry files, parses the manifest, reads the exports and finds the version, so none of them is derived in prose.
|
|
184
184
|
|
|
185
185
|
**Script-supported languages** (use the script): `js`, `ts`, `javascript`, `typescript`, `python`, `rust`, `go`, `java`, `kotlin`.
|
|
186
186
|
|
|
@@ -203,14 +203,14 @@ Display `Listing the exports...`. This section runs exactly one of §4.1 (script
|
|
|
203
203
|
2. **Parse them and follow their re-exports** by path: the `manifest` (no `--manifest-file` when it is null) and one `--entry-file` per entry file (for a GitHub source, each one the fetch `fetched`):
|
|
204
204
|
|
|
205
205
|
```bash
|
|
206
|
-
uv run {extractPublicApiHelper} --mode quick --language "<language>" --source-root "<source root>" --tree-file "{run_dir}/tree.json" --manifest-file "<manifest path>" --entry-file "<entry path>" --follow [--repo "{owner}/{repo}" --ref "{analysis_ref}"] > "{run_dir}/extract.json"
|
|
206
|
+
uv run {extractPublicApiHelper} --mode quick --language "<language>" --source-root "<source root>" --tree-file "{run_dir}/tree.json" --manifest-file "<manifest path>" --entry-file "<entry path>" --follow --resolve-version [--repo "{owner}/{repo}" --ref "{analysis_ref}"] > "{run_dir}/extract.json"
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
`--follow` reads each module an `export *` or a star import passes names on from, round after round, so a chain is read to its end, and lists them in `followed`; for a GitHub source, `--repo` and `--ref` fetch each into `{run_dir}/files` first. A module it could not read stays in `unlisted[]`, and a warning names it.
|
|
209
|
+
`--follow` reads each module an `export *` or a star import passes names on from, round after round, so a chain is read to its end, and lists them in `followed`; for a GitHub source, `--repo` and `--ref` fetch each into `{run_dir}/files` first. A module it could not read stays in `unlisted[]`, and a warning names it. `--resolve-version` looks up a version the manifest does not state.
|
|
210
210
|
|
|
211
211
|
3. On a non-zero exit of the extractor (codes 1 or 2 per its docstring), log its stderr and fall through to §4.2 (the prose-fallback path), which writes over `{run_dir}/extract.json`: never HALT just because the script choked on an unusual manifest.
|
|
212
212
|
|
|
213
|
-
4. Keep `{run_dir}/extract.json`'s `package_name`, `exports` (each entry's `name`/`type`/`source_file`), `dependencies` and `warnings` for §5, which shows the package, its dependency count, the exports and the warnings. Its `version` feeds §4b
|
|
213
|
+
4. Keep `{run_dir}/extract.json`'s `package_name`, `exports` (each entry's `name`/`type`/`source_file`), `dependencies` and `warnings` for §5, which shows the package, its dependency count, the exports and the warnings. Its `version` feeds §4b, and step 3 counts its exports.
|
|
214
214
|
|
|
215
215
|
#### 4.2 Procedure — fallback (not script-supported)
|
|
216
216
|
|
|
@@ -251,9 +251,9 @@ If CCC is unavailable or returns no results: skip this subsection silently.
|
|
|
251
251
|
|
|
252
252
|
### 4b. Detect Source Version
|
|
253
253
|
|
|
254
|
-
**When
|
|
254
|
+
**When §4.1 ran:** `{run_dir}/extract.json`'s `version` is the detected version, null when the extractor found none. Do not derive it again or load `{versionResolutionFile}`.
|
|
255
255
|
|
|
256
|
-
**When
|
|
256
|
+
**When §4.2 ran:** load `{versionResolutionFile}` and run its Detection Algorithm.
|
|
257
257
|
|
|
258
258
|
Whichever path produced it, keep the detected version for §5, which shows it beside `target_version`. The actual write happens in step 05.
|
|
259
259
|
|
|
@@ -108,7 +108,7 @@ Glosses (substitute the matching one-liner for `{tier_gloss}`, `{scripts_gloss}`
|
|
|
108
108
|
- `Deep`: full pipeline — AST + ccc + qmd portfolio search + LLM re-ranking
|
|
109
109
|
|
|
110
110
|
- **`scripts_intent` / `assets_intent` glosses** —
|
|
111
|
-
- `detect`: SKF will scan source for the standard `scripts/`/`bin/`/`tools/`/`cli/` (or `assets/`/`templates/`/`schemas/`/`configs/`) directories during create-skill and decide automatically
|
|
111
|
+
- `detect`: SKF will scan source for the standard `scripts/`/`bin/`/`tools/`/`cli/` (or `assets/`/`templates/`/`schemas/`/`configs/`) directories during create-skill and decide automatically (when one of the four script folders is a Python package, holding `__init__.py`, only its `__main__.py` and the modules with a shebang or a `__main__` block count as scripts)
|
|
112
112
|
- `none`: no script/asset packaging — create-skill will skip the detection pass
|
|
113
113
|
- free-text (anything else): a description of what to package; create-skill treats it as the user's spec
|
|
114
114
|
|
|
@@ -173,10 +173,10 @@ Using the boundary definitions from `{scopeTemplatesPath}`, present the appropri
|
|
|
173
173
|
|
|
174
174
|
- **A module include** is `<path>/**`, where `<path>` is the module's `path` in `{run_dir}/snapshot.json` (its `workspaces` or `module_candidates` entry, or the entry of `{run_dir}/package-snapshot.json` when §4.3 picked from it): repo-relative, with any workspace prefix already in it. For Maven and Gradle it is the §4.1 `modules` entry, prefixed with `{monorepo_workspace}/` when step 2 picked a workspace. A `named_module_subset` name maps to the §4.3 module it names. With no module picked, the include is `{monorepo_workspace}/**` when step 2 picked a workspace (§3b), else `**`.
|
|
175
175
|
- **The default exclusions** are the Full Library template's test globs (`**/*.test.*`, `**/*.spec.*`, `**/test/**`, `**/tests/**`) and build globs (`**/dist/**`, `**/build/**`, `**/target/**`). The template's configuration and documentation exclusions name no glob, and the module includes already leave the root configuration files and the docs folders out.
|
|
176
|
-
- **The public API files** are the files that define the names the package's entry points export. Run §3c's recipe runner (its helper, source folder, clone and clone removal) with the `full-library` default as its globs and `-o "{run_dir}/public-api.json"`, then print the file of each public name, an entry point's own definitions
|
|
176
|
+
- **The public API files** are the files that define the names the package's entry points export. Run §3c's recipe runner (its helper, source folder, clone and clone removal) with the `full-library` default as its globs and `-o "{run_dir}/public-api.json"`, then print the file of each public name, an entry point's own definitions, the files its re-exports lead to and the files that define a namespace's `members` (the names a submodule the package exports passes on):
|
|
177
177
|
|
|
178
178
|
```bash
|
|
179
|
-
uv run python -c 'import json, sys; d = json.load(open(sys.argv[1], encoding="utf-8")); print("\n".join(sorted({
|
|
179
|
+
uv run python -c 'import json, sys; d = json.load(open(sys.argv[1], encoding="utf-8")); print("\n".join(sorted({f for p in d["entry_point_diff"]["public"] for f in [p.get("file")] + [m.get("file") for m in p.get("members") or []] if f})))' "{run_dir}/public-api.json"
|
|
180
180
|
```
|
|
181
181
|
|
|
182
182
|
At Quick tier, when the runner exits 1 to 3 or the clone fails, or when it prints no file, take the `full-library` default instead and give the reason in the warn line: `warn: headless boundary default public-api: full-library boundaries ({the reason})`.
|
|
@@ -215,10 +215,10 @@ Head this off by capturing the authoring surface as **`scope.tier_a_include`**:
|
|
|
215
215
|
uv run {extractPublicApiHelper} --mode full --source-root "<source folder>" --include "<include glob>" --exclude "<exclude glob>" --language "{language}" -o "{run_dir}/tier-a.json"
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
The candidates are the files that define a name an entry point passes on (`reexport_targets`), less the entry points themselves (`entry_points.files[].file`). Print them, one per line:
|
|
218
|
+
The candidates are the files that define a name an entry point passes on (`reexport_targets`, and the `members` of a namespace in `entry_point_diff.public`), less the entry points themselves (`entry_points.files[].file`) and the files of the names the runner lists outside the scope (`entry_point_diff.outside_scope`), since a tier A name counts only in a file `scope.include` matches. A file outside `scope.include` that a named re-export (`export { x } from`) or a barrel reaches has no `outside_scope` row and stays a candidate, which Create Skill's runner then warns about. Print them, one per line:
|
|
219
219
|
|
|
220
220
|
```bash
|
|
221
|
-
uv run python -c 'import json, sys; d = json.load(open(sys.argv[1], encoding="utf-8")); entries = {f["file"] for f in d["entry_points"]["files"]}; print("\n".join(sorted({t["file"] for t in d["reexport_targets"]
|
|
221
|
+
uv run python -c 'import json, sys; d = json.load(open(sys.argv[1], encoding="utf-8")); entries = {f["file"] for f in d["entry_points"]["files"]}; diff = d.get("entry_point_diff") or {}; members = {m.get("file") for p in diff.get("public") or [] for m in p.get("members") or []}; outside = {o.get("file") for o in diff.get("outside_scope") or []}; print("\n".join(sorted(({t["file"] for t in d["reexport_targets"]} | members) - {None} - entries - outside)))' "{run_dir}/tier-a.json"
|
|
222
222
|
```
|
|
223
223
|
|
|
224
224
|
Once the runner has run, whatever its exit, remove a clone this section made (`rm -rf "{run_dir}/clone"`).
|
|
@@ -60,50 +60,31 @@ Nothing the upstream brief carries is rebuilt or dropped: §4's writer starts fr
|
|
|
60
60
|
|
|
61
61
|
**Resolve `{detectDocsHelper}`** from `{detectDocsProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
62
62
|
|
|
63
|
-
Invoke doc detection to discover documentation URLs for the source repo:
|
|
63
|
+
Invoke doc detection to discover documentation URLs for the source repo, keeping its JSON in the run folder:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
uv run {detectDocsHelper} --repo-url {source_repo}
|
|
66
|
+
uv run {detectDocsHelper} --repo-url "{source_repo}" > "{run_dir}/detected.json"
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
`--repo-url` is always required (the script uses it for GitHub API calls). If a local clone is also available at `{local_clone_path}`, add `--local-path {local_clone_path}` to enable docs-folder scanning in addition to API-based detection.
|
|
69
|
+
`--repo-url` is always required (the script uses it for GitHub API calls). If a local clone is also available at `{local_clone_path}`, add `--local-path "{local_clone_path}"` to enable docs-folder scanning in addition to API-based detection.
|
|
70
70
|
|
|
71
71
|
**Handle exit codes:**
|
|
72
72
|
|
|
73
|
-
- **Exit 0 (found docs):**
|
|
73
|
+
- **Exit 0 (found docs):** Proceed to §3, which reads `{run_dir}/detected.json`.
|
|
74
74
|
- **Exit 1 (none found):** Log: "No external documentation found — brief generated from source analysis only." Proceed to §4 with no doc enrichment.
|
|
75
75
|
- **Exit 2 (error):** Log the warning "Doc detection failed: proceeding without doc enrichment." and add it to `workflow_warnings[]`. Do NOT halt: doc enrichment is best-effort. Proceed to §4 with no doc enrichment.
|
|
76
76
|
|
|
77
77
|
### 3. Enrich Brief with Detected Docs
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- `url` ← `url` (direct copy)
|
|
82
|
-
- `label` ← derive from `content_type` if available:
|
|
83
|
-
- `"api-docs"` → `"API Documentation"`
|
|
84
|
-
- `"guide"` → `"Guide"`
|
|
85
|
-
- `"reference"` → `"Reference"`
|
|
86
|
-
- Otherwise derive from `detected_via`:
|
|
87
|
-
- `"homepageUrl"` → `"Homepage"`
|
|
88
|
-
- `"readme_link"` → `"README Link"`
|
|
89
|
-
- `"pages_api"` → `"GitHub Pages"`
|
|
90
|
-
- `"docs_folder"` → `"Docs Folder"`
|
|
91
|
-
- `source` ← coarse provenance derived from `detected_via` (per the `skill-brief.v1.json` `doc_urls[].source` enum): `homepageUrl` → `homepage`, `readme_link` → `readme-detection`, `pages_api` → `pages-api`, `docs_folder` → `docs-folder`. This marks the entry as opportunistically detected, distinct from a registry-guaranteed corpus.
|
|
92
|
-
|
|
93
|
-
**Merge via the canonical helper.** Resolve `{mergeDocUrlsHelper}` from `{mergeDocUrlsProbeOrder}` (first existing path wins; HALT if neither exists). Stage its input in the run folder, the upstream brief's `doc_urls` as `existing` and the entries just mapped above as `detected`, then run it on the file:
|
|
79
|
+
**Merge via the canonical helper.** Resolve `{mergeDocUrlsHelper}` from `{mergeDocUrlsProbeOrder}` (first existing path wins; HALT if neither exists). It maps each detected entry to the brief's `{url, label, source}`, reads the upstream brief's `doc_urls` itself and merges the two, so no entry is typed here:
|
|
94
80
|
|
|
95
81
|
```bash
|
|
96
|
-
|
|
97
|
-
{"scope_type": "<scope_type>", "existing": <the upstream brief's doc_urls, [] if none>, "detected": <the mapped detected entries>}
|
|
98
|
-
SKF_JSON
|
|
99
|
-
uv run {mergeDocUrlsHelper} < "{run_dir}/doc-urls.json" > "{run_dir}/doc-urls-merged.json"
|
|
82
|
+
uv run {mergeDocUrlsHelper} --scope-type "{scope_type}" --detected-file "{run_dir}/detected.json" --existing-brief "{brief_path}" > "{run_dir}/doc-urls-merged.json"
|
|
100
83
|
```
|
|
101
84
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
The helper returns `{"doc_urls": [...], "suppressed": [...]}`. It deduplicates by **normalized** URL (lowercase host, strip a trailing `/index.html` and any trailing `/`), so a seeded `…/book/` and a README's `…/book/index.html` collapse to one entry; existing/corpora-seeded entries always win and keep their `source: language-registry`, so the registry-vs-detected distinction survives the merge. For a **whole-language reference** (`scope_type == "full-library"` AND ≥1 `existing` entry has `source: language-registry`) it additionally suppresses README noise on a corpus host: non-corpus path segments (`/whatsnew/`, `/contribute`, `/wiki/`) and non-primary-locale duplicates of a kept page (`/ja/master/` when `/en/master/` is kept). Ordinary skills (any other `scope_type`, or no registry corpora) pass through with dedup only — no suppression. Use the returned `doc_urls` as the brief's merged list.
|
|
85
|
+
§4 hands the writer that file: its `doc_urls` are the merged list, every upstream entry kept with its `source`. On a non-zero exit, add `Doc merge failed: proceeding without doc enrichment.` to `workflow_warnings[]` and proceed to §4 with no doc enrichment.
|
|
105
86
|
|
|
106
|
-
**Log suppressed entries.** When `suppressed` is non-empty, log one line per entry
|
|
87
|
+
**Log suppressed entries.** When its `suppressed` is non-empty, log one line per entry, `"info: suppressed {url} ({reason})"`, so the operator sees each entry the merge dropped and why (never drop silently).
|
|
107
88
|
|
|
108
89
|
### 4. Write Enriched Brief
|
|
109
90
|
|
|
@@ -115,10 +96,11 @@ Write the enriched brief through the canonical writer. It starts from the upstre
|
|
|
115
96
|
uv run {writeSkillBriefHelper} write --target {forge_data_folder}/{skill_name}/skill-brief.yaml --base-brief "{brief_path}" --doc-urls-file "{run_dir}/doc-urls-merged.json"
|
|
116
97
|
```
|
|
117
98
|
|
|
118
|
-
Leave `--doc-urls-file` out when §2 and §3 were skipped or found nothing.
|
|
99
|
+
Leave `--doc-urls-file` out when §2 and §3 were skipped or found nothing, or the merge failed.
|
|
119
100
|
|
|
120
101
|
**On script failure (non-zero exit):**
|
|
121
|
-
- Exit 1 (
|
|
102
|
+
- Exit 1 of a call with `--doc-urls-file` whose stderr `field` is `doc_urls` (a merged entry the brief schema refuses): doc enrichment is best-effort, so add `Doc enrichment dropped: {its message}` to `workflow_warnings[]`, run the call once more without `--doc-urls-file`, and handle that result.
|
|
103
|
+
- Any other exit 1 (validation/invariant): emit the halt envelope first, `uv run {emitBriefEnvelopeHelper} emit --target stderr` with `halt_reason: "input-invalid"` (SKILL.md Halt Contract), then HARD HALT (exit code 2).
|
|
122
104
|
- Exit 2 (I/O failure): emit the halt envelope first, `uv run {emitBriefEnvelopeHelper} emit --target stderr` with `halt_reason: "write-failed"` (SKILL.md Halt Contract), then HARD HALT (exit code 4).
|
|
123
105
|
|
|
124
106
|
**On success:** Capture `brief_path` and `version` from the response envelope for step-auto-validate's envelope emission, and add each entry of its `warnings[]` to `workflow_warnings[]`.
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
# Version Resolution
|
|
2
2
|
|
|
3
|
-
Single source of truth for how brief-skill resolves the `version` field of `skill-brief.yaml`. Loaded by step 2 §4b (auto-detect,
|
|
4
|
-
|
|
5
|
-
**Aligned with** `assets/skill-brief-schema.md` "Version Detection" section. If you change one, change the other.
|
|
3
|
+
Single source of truth for how brief-skill resolves the `version` field of `skill-brief.yaml`. Loaded by step 2 §4b (auto-detect, when §4.2's fallback read the source) and step 5 §3 (resolve & write) so both operate on the same precedence rules and invariant. Step-01 §3b references this file in prose for human-readable rationale but does not load it: that step only collects `target_version` and checks its shape with `skf-validate-brief-inputs.py --only target_version`.
|
|
6
4
|
|
|
7
5
|
## Detection Algorithm
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
Step 2 §4.1's extractor detects the version of the languages it reads, with `--resolve-version`: the manifest's literal version, then a version file or a Cargo workspace root, then the release tag (`skf-extract-public-api.py --help` gives the order). When §4.2 read the source instead, attempt these lookups in order and stop at the first match:
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- **Rust:** `Cargo.toml` `[package] version` (static). If `version = { workspace = true }`, resolve from workspace root `Cargo.toml` → `git describe --tags --abbrev=0`.
|
|
14
|
-
- **Go:** version tag from `go.mod`, or `git describe --tags --abbrev=0`.
|
|
9
|
+
1. The version the manifest or a version file writes as a literal: `package.json`'s `"version"`, `pyproject.toml`'s `[project] version` or a package's `__version__`, a `.gemspec` or `version.rb` `VERSION`, a `.csproj` `<Version>`, `composer.json`'s `"version"`.
|
|
10
|
+
2. The tag lookup. For a GitHub source, `uv run {githubProbeHelper} tags --repo "{owner}/{repo}" --ref "{analysis_ref}" --limit 1`: its `release.version` is the version (the newest release tag at `HEAD`, the pinned tag itself at a tag, none at a branch or a commit), and a null `release` or a non-zero exit gives none. For a local source, `git -C "{source_path}" describe --tags --abbrev=0`: a tag whose name is a version gives it, a leading `v` dropped.
|
|
15
11
|
|
|
16
12
|
For remote GitHub sources, read version-bearing files from `{run_dir}/files/`, where step 2 stages the files it fetches; fetch one that is not there yet with step 2's fetch helper, naming it: `uv run {githubFetchHelper} --repo "{owner}/{repo}" --ref "{analysis_ref}" --tree-file "{run_dir}/tree.json" --dest "{run_dir}/files" "{file}"`. `{analysis_ref}` is the ref resolved in step 02 §1, defaulting to `HEAD` when no `target_ref`/`target_version` was pinned; reading at the pinned ref keeps the "Detected version" consistent with the version being skilled. For local sources, read the file directly.
|
|
17
13
|
|
|
@@ -43,5 +39,5 @@ Step-05 §3 enforces this by setting both fields to the same string when `target
|
|
|
43
39
|
|------|----------------|
|
|
44
40
|
| 01 §3b | Collect `target_version` (interactive prompt, or headless arg). Do not auto-detect — that is step 02's job. |
|
|
45
41
|
| 02 §1 | Resolve `target_version` to the git tag step 2 reads, through `skf-validate-pins.py`. With no match, analyze `HEAD` and warn. |
|
|
46
|
-
| 02 §4b |
|
|
42
|
+
| 02 §4b | Detect the version (§4.1's extractor, or the detection algorithm after §4.2) regardless of whether `target_version` is set. If `target_version` is set and the detected version differs, surface the disagreement to the user; the precedence above is unchanged, so `target_version` wins. |
|
|
47
43
|
| 05 §3 | Apply the precedence rules and write `version`. If `target_version` is set, also write the `target_version` field with the identical value. Enforce the invariant. |
|
|
@@ -29,6 +29,8 @@ description: >
|
|
|
29
29
|
|
|
30
30
|
**Two-tier assembly.** SKILL.md must retain actionable inline content that survives `split-body` extraction. Assemble Tier 1 sections first (always inline), then Tier 2 sections (reference-eligible, may be extracted by split-body).
|
|
31
31
|
|
|
32
|
+
**Public surface (`public-api` scope).** An export the extraction inventory marks `public: false` is off the package's public surface: no section of SKILL.md or `references/` names or documents it, as the provenance map and `metadata.json`'s `exports[]` hold no entry for it. Every other export, and every export of any other scope type, is assembled as below.
|
|
33
|
+
|
|
32
34
|
### Tier 1 — Always Inline (must survive split-body)
|
|
33
35
|
|
|
34
36
|
These sections form the essential standalone body. Target: **under 300 lines total** for Tier 1. An agent loading only SKILL.md (without references) must get enough to act.
|
|
@@ -57,7 +59,7 @@ These sections form the essential standalone body. Target: **under 300 lines tot
|
|
|
57
59
|
```
|
|
58
60
|
|
|
59
61
|
**Section 4 — Key API Summary (~20 lines):**
|
|
60
|
-
- Table of top 10-15 functions: name, purpose, key parameters
|
|
62
|
+
- Table of top 10-15 functions: name, purpose, key parameters, none marked `public: false`
|
|
61
63
|
- One row per function — no full signatures, just enough for discovery
|
|
62
64
|
- Provenance citation per function
|
|
63
65
|
|
|
@@ -104,7 +106,7 @@ These sections form the essential standalone body. Target: **under 300 lines tot
|
|
|
104
106
|
|
|
105
107
|
### Tier 2 — Reference-Eligible (can be extracted by split-body)
|
|
106
108
|
|
|
107
|
-
Assemble Sections 9-11 (Full API Reference, Full Type Definitions, Full Integration Patterns) as defined in the skill-sections data file. These contain full detail and are split into `references/` when the body exceeds 500 lines. Include T2 annotations from enrichment in the Full API Reference (Deep tier only).
|
|
109
|
+
Assemble Sections 9-11 (Full API Reference, Full Type Definitions, Full Integration Patterns) as defined in the skill-sections data file, for the exports the provenance map holds (never one marked `public: false`). These contain full detail and are split into `references/` when the body exceeds 500 lines. Include T2 annotations from enrichment in the Full API Reference (Deep tier only).
|
|
108
110
|
|
|
109
111
|
**Tier 2 differentiation from Tier 1:** Tier 2 Full API Reference must contain content that is not present in Tier 1's Key API Summary. Specifically:
|
|
110
112
|
|
|
@@ -270,7 +272,7 @@ Replace per-function subsections with `references/pattern-*.md` groupings: one r
|
|
|
270
272
|
- `stats.exports_documented` MAY be set to the Pattern Surface row count as a proxy, but its semantics change: it counts authored pattern surfaces, not public exports. Emit an adjacent `stats.pattern_surfaces_documented` with the same integer so downstream consumers (test-skill, feasibility, discovery) can discriminate.
|
|
271
273
|
- `exports_public_api` / `exports_internal` / `exports_total` are not meaningful for a reference app — omit them, or set all three to the Pattern Surface count with a note in `stats.notes`: `"reference-app: export counts are pattern-surface proxies, not library exports"`.
|
|
272
274
|
- Do not fabricate signature / type-coverage data from pattern surfaces. skf-test-skill skips those categories when metadata flags a reference-app (`referenceApp`, the same skip path as `stackSkill`; its `scripts/compute-score.py` redistributes their weights).
|
|
273
|
-
- Do not emit `effective_denominator` for reference-app scope
|
|
275
|
+
- Do not emit `effective_denominator` for reference-app scope (its `effective_denominator_decision` never stores one): `effective_denominator` counts public exports from the authoring-surface barrel, a library concept that `pattern_surfaces_documented` already replaces here: pattern-surface coverage is the reference-app basis, and skf-test-skill skips library-export coverage for it.
|
|
274
276
|
|
|
275
277
|
**Language / spec-reference sub-shape:** A reference app that documents an engine- or spec-versioned **language** — a query language, grammar, or DSL (e.g. SurrealQL @ SurrealDB) whose value is construct idioms rather than wiring or exports — is a recognized reference-app sub-shape. Keep `scope.type: "reference-app"` (there is no separate enum value), so every carve-out above applies unchanged (`pattern_surfaces_documented` proxy, no `effective_denominator`, test-skill signature/type skip). Adapt the three overrides to the language's surface instead of app wiring:
|
|
276
278
|
|
|
@@ -117,7 +117,7 @@ Indexed pipe-delimited format for CLAUDE.md managed section (~80-120 tokens per
|
|
|
117
117
|
"tool_versions": {
|
|
118
118
|
"ast_grep": "{version-or-null}",
|
|
119
119
|
"qmd": "{version-or-null}",
|
|
120
|
-
"skf": "{
|
|
120
|
+
"skf": "{installed-skf-version-or-unknown}"
|
|
121
121
|
},
|
|
122
122
|
"confidence_distribution": {
|
|
123
123
|
"t1": 0,
|
|
@@ -134,7 +134,7 @@ Indexed pipe-delimited format for CLAUDE.md managed section (~80-120 tokens per
|
|
|
134
134
|
"total_coverage": 0.0,
|
|
135
135
|
"scripts_count": 0,
|
|
136
136
|
"assets_count": 0
|
|
137
|
-
// "effective_denominator": 0 // optional
|
|
137
|
+
// "effective_denominator": 0 // optional: emitted when compile.md §4 passes it (counts.effective_denominator_decision.store and a stratified scope.notes); test-skill uses this as the coverage denominator when present.
|
|
138
138
|
},
|
|
139
139
|
// scripts[] and assets[] — include ONLY when inventories are non-empty; omit entirely otherwise
|
|
140
140
|
// "scripts": [{ "file": "scripts/{name}", "purpose": "{description}", "source_file": "{source-path}", "confidence": "T1-low" }],
|
|
@@ -144,7 +144,7 @@ Indexed pipe-delimited format for CLAUDE.md managed section (~80-120 tokens per
|
|
|
144
144
|
// detected_via "brief_doc_urls" = docs-only skills: the brief's doc_urls (plus subpages step 3c fetched), hashed by `skf-detect-docs.py hash-urls`
|
|
145
145
|
"generated_by": "{quick-skill|create-skill}",
|
|
146
146
|
"dependencies": [],
|
|
147
|
-
"compatibility": "
|
|
147
|
+
"compatibility": ""
|
|
148
148
|
}
|
|
149
149
|
```
|
|
150
150
|
|
|
@@ -270,9 +270,9 @@ t2_future_count: {N}
|
|
|
270
270
|
**Source:** {repo} @ {commit}
|
|
271
271
|
|
|
272
272
|
## Tool Versions
|
|
273
|
-
- ast-grep: {
|
|
274
|
-
- QMD: {
|
|
275
|
-
- SKF: {
|
|
273
|
+
- ast-grep: {tool_versions.ast_grep}
|
|
274
|
+
- QMD: {tool_versions.qmd}
|
|
275
|
+
- SKF: {tool_versions.skf}
|
|
276
276
|
|
|
277
277
|
## Extraction Summary
|
|
278
278
|
- Files scanned: {count}
|
|
@@ -12,11 +12,9 @@ Once §2b has resolved source access, scan the resolved source tree (the local s
|
|
|
12
12
|
|
|
13
13
|
This protocol detects such files, prompts the user, and records the decision in the brief so future runs (re-create, update, audit) honor it.
|
|
14
14
|
|
|
15
|
-
**Heuristic scan list (handled by the helper — listed here for reference):** case-insensitive basename match, any directory depth, on `llms.txt`, `llms-full.txt`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `COPILOT.md`, `.cursorrules`, `.windsurfrules`, `.clinerules`.
|
|
16
|
-
|
|
17
15
|
## Procedure
|
|
18
16
|
|
|
19
|
-
1. **Resolve, scan, classify, and load previews in one call.** Resolve `{resolveAuthoritativeFilesHelper}` ← first existing path in `{resolveAuthoritativeFilesProbeOrder}` (step 3's frontmatter); if neither exists, **HARD HALT** (exit code 3, `helper-missing`, phase `extract`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`): "Cannot scan for authoritative files: skf-resolve-authoritative-files.py is missing. Re-install SKF, then re-run create-skill." The helper
|
|
17
|
+
1. **Resolve, scan, classify, and load previews in one call.** Resolve `{resolveAuthoritativeFilesHelper}` ← first existing path in `{resolveAuthoritativeFilesProbeOrder}` (step 3's frontmatter); if neither exists, **HARD HALT** (exit code 3, `helper-missing`, phase `extract`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`): "Cannot scan for authoritative files: skf-resolve-authoritative-files.py is missing. Re-install SKF, then re-run create-skill." The helper walks the tree, classifies each candidate and hashes it:
|
|
20
18
|
|
|
21
19
|
```bash
|
|
22
20
|
uv run {resolveAuthoritativeFilesHelper} resolve \
|
|
@@ -57,7 +55,7 @@ This protocol detects such files, prompts the user, and records the decision in
|
|
|
57
55
|
|
|
58
56
|
2. **Apply the helper's classification:**
|
|
59
57
|
|
|
60
|
-
- **`already_in_scope[]`**: the file is in scope and not skipped. Append the record to in-context `promoted_docs[]`, and remove the path from §2's filtered file list when §2 built one
|
|
58
|
+
- **`already_in_scope[]`**: the file is in scope and not skipped. Append the record to in-context `promoted_docs[]`, and remove the path from §2's filtered file list when §2 built one. No prompt: authoritative docs must never reach §4 code extraction even when scope.include matches.
|
|
61
59
|
- **`pre_decided[]` with `prior_action: "promoted"`** — amendment says promoted; **append to `promoted_docs[]`** using the helper-supplied hash/size/lines. Deterministic replay path.
|
|
62
60
|
- **`pre_decided[]` with `prior_action: "skipped"`** — user previously declined. Do nothing. Move on.
|
|
63
61
|
- **`unresolved[]`**: proceed to step 3 below (user prompt). A `prior_action` of `deferred-headless` means an earlier headless run met the file and left the decision to the next interactive run.
|
|
@@ -120,22 +118,3 @@ This protocol detects such files, prompts the user, and records the decision in
|
|
|
120
118
|
**Record for evidence report:** `authoritative_files_scan: {candidates: N, promoted: P, skipped: S, deferred: D, pre_decided: A, decisions: [{path, action, heuristic, reason}]}`. Step 3 §5 writes it and `promoted_docs[]` into the extraction inventory, and step 5 writes it into `evidence-report.md`.
|
|
121
119
|
|
|
122
120
|
When step 3 §2a's remote source guard skipped the scan, the record is `authoritative_files_scan: {not_scanned: "remote source not cloned"}` instead (its own key, apart from the `skipped` count of candidates above), and step 6 lists that skip under the evidence report's Remaining Warnings.
|
|
123
|
-
|
|
124
|
-
## How promoted docs reach the provenance map
|
|
125
|
-
|
|
126
|
-
Promoted docs do not flow through §4 code extraction. Instead:
|
|
127
|
-
|
|
128
|
-
1. §2a populates the `promoted_docs[]` list with content hashes, which step 3 §5 writes into the extraction inventory.
|
|
129
|
-
2. **Step-05 §6** (provenance-map assembly) reads `promoted_docs[]` from the inventory and emits one `file_entries[]` entry per promoted doc with `file_type: "doc"`, `extraction_method: "promoted-authoritative"`, `confidence: "T1-low"`, and the pre-computed `content_hash`.
|
|
130
|
-
3. **Step 7's promotion** does not copy doc files into the skill package (unlike scripts and assets). The source file remains at its original path; only the provenance map tracks it. Future audit and update workflows compare against this tracking entry via content hash. No file copy is required, because the intent is drift detection on the *source*, not bundling documentation into the skill output.
|
|
131
|
-
|
|
132
|
-
**Re-running `skf-create-skill`** reads the amended brief. Files with `action: "promoted"` amendments already appear in `scope.include`, but §2a still runs — it detects the file is in scope AND has an existing amendment, and takes the "pre-decided" silent path. The `promoted_docs[]` list is rebuilt on each run by scanning amendments with `action: "promoted"` (this is the deterministic replay path).
|
|
133
|
-
|
|
134
|
-
## Downstream workflow consumption
|
|
135
|
-
|
|
136
|
-
Zero code changes required in consumer workflows:
|
|
137
|
-
|
|
138
|
-
- **`skf-update-skill`** reads `provenance-map.json`. Promoted docs appear as `file_entries[]` entries. Update-skill Category D (script/asset file changes) iterates `file_entries` and compares content hashes — this works identically for `file_type: "doc"` entries, giving drift detection for free.
|
|
139
|
-
- **`skf-audit-skill`** scans files from `provenance-map.json`. The re-index builds its list from `entries[].source_file ∪ file_entries[].source_file`, so promoted doc paths are naturally included in the audit scan.
|
|
140
|
-
|
|
141
|
-
The brief is the single source of truth for authored scope intent. The provenance map is the single source of truth for extracted state. `scope.amendments[]` is the bridge that records when those two intentionally diverged. `promoted_docs[]` is the handoff from §2a to step 5 §6, through the extraction inventory; the published form is the `file_entries[]` list in provenance-map.json.
|
|
@@ -20,9 +20,9 @@ verifyProvenanceCompletenessProbeOrder:
|
|
|
20
20
|
- '{project-root}/_bmad/skf/shared/scripts/skf-verify-provenance-completeness.py'
|
|
21
21
|
- '{project-root}/src/shared/scripts/skf-verify-provenance-completeness.py'
|
|
22
22
|
# Resolve `{extractionInventoryHelper}` to the first existing path. §1 reads
|
|
23
|
-
# the inventory's counts through its `summary`,
|
|
24
|
-
#
|
|
25
|
-
# neither path exists.
|
|
23
|
+
# the inventory's counts through its `summary`, §6 writes the provenance
|
|
24
|
+
# map through its `provenance` and §4 metadata.json through its
|
|
25
|
+
# `metadata`; step 3 §5 already halted when neither path exists.
|
|
26
26
|
extractionInventoryProbeOrder:
|
|
27
27
|
- '{project-root}/_bmad/skf/shared/scripts/skf-extraction-inventory.py'
|
|
28
28
|
- '{project-root}/src/shared/scripts/skf-extraction-inventory.py'
|
|
@@ -63,7 +63,7 @@ To assemble the complete skill content from the extraction inventory and enrichm
|
|
|
63
63
|
|
|
64
64
|
Load `{skillSectionsData}` and `{assemblyRulesData}` completely. These define the agentskills.io-compliant format and detailed assembly rules for all output artifacts.
|
|
65
65
|
|
|
66
|
-
Then load `{extraction_inventory}` (`{project-root}/_bmad-output/.skf-stage/{skill-name}.inventory.json`) completely: the extraction inventory step 3 §5 wrote, with the T3 items step 3c and the T2 annotations step 4 added. Compile from this file, not from what context still holds of steps 3 and 4: its `exports[]` (
|
|
66
|
+
Then load `{extraction_inventory}` (`{project-root}/_bmad-output/.skf-stage/{skill-name}.inventory.json`) completely: the extraction inventory step 3 §5 wrote, with the T3 items step 3c and the T2 annotations step 4 added. Compile from this file, not from what context still holds of steps 3 and 4: its `exports[]` (one marked `public: false`, off a `public-api` skill's public surface, stays out of SKILL.md, `references/` and the provenance map), `top_exports`, `t2_annotations`, `t3_items`, `scripts_inventory`, `assets_inventory`, `intent_mapping`, `promoted_docs`, `authoritative_files_scan`, `counts`, `arms`, `aggregates`, `co_imports` and `warnings` (a component library's `component_catalog` too) are every input below. Resolve `{extractionInventoryHelper}` ← first existing path in `{extractionInventoryProbeOrder}` and, from `{project-root}`, run `uv run {extractionInventoryHelper} summary --inventory "{extraction_inventory}"`: bind `{inventory_counts}` ← its `counts`, which §7 reads.
|
|
67
67
|
|
|
68
68
|
For a whole-language reference, also load `{project-root}/_bmad-output/.skf-stage/{skill-name}.language-guide.json`, the index step 3c §4a wrote, and build `language_guide[]` from it: for each of its `language_guide` entries, in order, `{url, label, prose}`, where `prose` is the substantive body (narrative, idioms, usage examples, conceptual reference) of the pages its `pages` lists in its `docs_folder`, lightly trimmed of navigation and boilerplate, each block cited `[EXT:{url}]`, and `null` when `pages` is empty (the fetch failed). When the file is missing, or a page it lists is gone, display "The Language Guide from step 3c is missing ({the file or page}): assembling without it." and leave `language_guide[]` empty: `{assemblyRulesData}`'s empty-guide guard then assembles a standard library skill.
|
|
69
69
|
|
|
@@ -133,77 +133,46 @@ Vercel-aligned indexed format for CLAUDE.md managed section (~80-120 tokens):
|
|
|
133
133
|
**Derivation rules:**
|
|
134
134
|
|
|
135
135
|
- **version**: From source detection (reconciled in step 3), not brief default
|
|
136
|
-
- **api**: Top 10 exports from extraction inventory, append `()` to function names
|
|
136
|
+
- **api**: Top 10 exports from extraction inventory, none marked `public: false`, append `()` to function names
|
|
137
137
|
- **key-types**: Inline summary of most important enum/type values from Key Types section
|
|
138
|
-
- **gotchas**: Derived from T2-future annotations (breaking changes)
|
|
138
|
+
- **gotchas**: Derived from T2-future annotations (breaking changes) of exports not marked `public: false`, async requirements, version-specific behavior changes. If no gotchas available, omit the gotchas line.
|
|
139
139
|
- **Section anchors** (`#quick-start`, `#key-types`): Must match actual heading slugs in the assembled SKILL.md
|
|
140
140
|
|
|
141
141
|
### 4. Build metadata.json Content
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
- Populate all fields from brief_data, extraction inventory, and tier
|
|
145
|
-
- Set `generation_date` to current ISO-8601 timestamp
|
|
146
|
-
- Set `source_commit` from resolved source (if available)
|
|
147
|
-
- Set `source_ref` from resolved source ref (tag name, branch, or `HEAD`; null if unavailable)
|
|
148
|
-
- Set `scope_type` from the brief's `scope.type` value verbatim (`full-library`, `specific-modules`, `public-api`, `component-library`, `reference-app`, or `docs-only`). **Always emit this field.** `skf-test-skill` keys reference-app handling on `metadata.json.scope_type == "reference-app"` — both the scoring redistribution (Signature Accuracy / Type Coverage marked N/A) and the coverage-check §4b count-coherence skip. Omitting it silently mis-scores a reference-app skill as a library (zero-export barrel HALT or false metadata-drift findings). The value is informational for the other scope types (consumers only branch on `reference-app`), but emit it for all so the producer/consumer contract holds.
|
|
143
|
+
`{extractionInventoryHelper}`'s `metadata` writes `metadata.json` in `{skillSectionsData}`'s structure, from the brief, the inventory and the stats computed first: never type a field it writes.
|
|
149
144
|
- **Compute the `stats` block and `confidence_distribution` deterministically** with `{renderMetadataStatsHelper}` (resolve from `{renderMetadataStatsProbeOrder}`; first existing path wins; if neither exists, **HARD HALT** (exit code 3, `helper-missing`, phase `compile`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`): "Cannot compute the metadata stats: skf-render-metadata-stats.py is missing. Re-install SKF, then re-run create-skill."). The helper owns all the arithmetic (binning each provenance entry once by its `signature_source` tier, summing the bins, and computing every coverage ratio) and returns the finished `stats` + `confidence_distribution` objects to write into `metadata.json` verbatim. Run `uv run {renderMetadataStatsHelper} --help` for the full contract. You supply only the judgment payload below.
|
|
150
145
|
|
|
151
|
-
**Judgment payload (passed as JSON on stdin):** the counts come from the inventory's `counts`, which step 3 §4b took from the recipe runner's `counts` (or
|
|
152
|
-
- `exports_public_api`: `counts.exports_public_api`, the public names the package's entry points export (`outside_scope` ones included), never the provenance-map entry count, which misses the exports extraction did not find
|
|
146
|
+
**Judgment payload (passed as JSON on stdin):** the counts come from the inventory's `counts`, which step 3 §4b took from the recipe runner's `counts` (or from `--mode denominator`'s, for names counted without the runner's diff); what you decide is whether the brief documents the stratification `effective_denominator` needs.
|
|
147
|
+
- `exports_public_api`: `counts.exports_public_api`, the public names the package's entry points export (`outside_scope` ones included, and a namespace counted as its `members`, the names its module passes on), never the provenance-map entry count, which misses the exports extraction did not find
|
|
153
148
|
- `exports_internal`: `counts.exports_internal`, the exports the recipes found that no entry point exports or makes reachable
|
|
154
149
|
- `scripts` / `assets`: the `scripts_inventory` / `assets_inventory` arrays (or `[]` when empty) — the helper sets `stats.scripts_count` / `stats.assets_count` from their lengths
|
|
155
|
-
- `effective_denominator` (**optional
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
- **(b) single-crate curated subset:** `arms.specific_modules` (a `specific-modules` scope, not a monorepo);
|
|
159
|
-
- **(c) multi-entry (exports-map):** `arms.multi_subpath_exports` (an in-scope `package.json` whose `exports` map has more than one non-root subpath without `*` whose target is a JS/TS file, not a monorepo).
|
|
160
|
-
|
|
161
|
-
Arms (b) and (c) are not a widening for its own sake — both consumer clauses already say "prefer `metadata.json.stats.effective_denominator`", and a monorepo-only emit condition makes those two rungs unreachable, so a non-monorepo curated scope has no way to lock its denominator.
|
|
162
|
-
2. `scope.type` is not `full-library` (and not `reference-app` — see carve-out below), AND the resolved include list (`tier_a_include` if present, else `scope.include`) lists a curated file/directory subset rather than the full workspace.
|
|
163
|
-
3. `scope.notes` is present and documents the stratification strategy (e.g., a tiered A/B/C plan) — this serves as the intent marker confirming the subset is by design.
|
|
164
|
-
|
|
165
|
-
Otherwise omit it from the payload entirely — when absent, `skf-test-skill` falls back to `exports_public_api`. See `skf-test-skill` `references/source-access-protocol.md` §Source API Surface Definition ("Stratified-scope monorepo packages", "Single-crate curated subset", "Multi-entry (exports-map) packages") for the test-side consumption rules.
|
|
150
|
+
- `effective_denominator` (**optional**): `counts.effective_denominator`, only when `counts.effective_denominator_decision.store` is true and the brief's `scope.notes` documents the stratification strategy (e.g., a tiered A/B/C plan), the intent marker that the curated subset is by design. `skf-extract-public-api.py` decides `store` with the count (the recipe runner, or `--mode denominator` for names counted without its diff). Otherwise omit it from the payload: `skf-test-skill` then measures coverage over the surface it re-derives (its `references/source-access-protocol.md` §Source API Surface Definition).
|
|
151
|
+
- **Shape:** pass `--shape reference-app` when the reference-app assembly-rules override applies (default `library`), and also put `pattern_surfaces_documented` (the Pattern Surface row count) in the payload: the helper uses it as `exports_documented`, emits `stats.pattern_surfaces_documented`, and refuses to emit `effective_denominator`; the distribution then sums to the per-citation count, not to `exports_documented`.
|
|
152
|
+
- **Invoke**: stage `provenance-map.json` (§6) first, since the helper reads its `entries[]`, and bind `{stats_json}` ← `{project-root}/_bmad-output/.skf-stage/{skill-name}.stats.json`, beside the staging folder:
|
|
166
153
|
|
|
167
|
-
|
|
154
|
+
```bash
|
|
155
|
+
rm -f "{stats_json}"
|
|
156
|
+
echo '{"exports_public_api": {N}, "exports_internal": {M}, "scripts": {scripts_inventory-or-[]}, "assets": {assets_inventory-or-[]}}' \
|
|
157
|
+
| uv run {renderMetadataStatsHelper} <staging-skill-dir>/provenance-map.json -o "{stats_json}"
|
|
158
|
+
```
|
|
168
159
|
|
|
169
|
-
|
|
160
|
+
It writes its JSON to `{stats_json}`: read its `coherence` there, and the `metadata` call below copies its `stats` and `confidence_distribution` verbatim. The helper derives `exports_documented` (the documented-export count = provenance `entries[]` count, which holds no export marked `public: false`), `exports_total` = `exports_public_api` + `exports_internal`, `public_api_coverage` = documented / public_api (`null` if public_api is 0), `total_coverage` = documented / total (`null` if total is 0), and bins `confidence_distribution.{t1, t1_low, t2, t3}` by each entry's `signature_source`, **each entry counted exactly once**, so the four bins provably sum to the documented-export count. This structurally prevents the recurring miscount where binning ~8 T2 annotations + ~80 T3 doc items on top of 59 exports produced a distribution summing to 147 ≠ 59 (`skf-test-skill` coverage-check §4b flags that sum as an internal-consistency defect). If the helper reports `coherence.ok: false`, fix the provenance map and re-run the helper; never hand-edit the stats. A `confidence_distribution` violation means some provenance entries carry a missing or unrecognized `signature_source`, and a `stats.scripts_count` / `stats.assets_count` violation means a file-entry count disagrees.
|
|
170
161
|
|
|
171
|
-
|
|
172
|
-
- **
|
|
173
|
-
- **Invoke** — stage `provenance-map.json` (§6) first, since the helper reads its `entries[]`:
|
|
162
|
+
**Label violations.** A violation whose `field` is `provenance.entries[<i>].<field>` means entry `<i>` (its `export_name` is in the violation) carries a label its `extraction_method` does not allow: relabel it by the Relabel Rule in `{relabelRuleData}` (labels follow the method, a node kind comes from the recipe that matched, never invented, and a violation the rule cannot settle stays as a WARN), resolving `{verifyProvenanceCompletenessHelper}` ← first existing path in `{verifyProvenanceCompletenessProbeOrder}` for its `kind-at` lookup. Write the relabeled map back to `<staging-skill-dir>/provenance-map.json`, the copy step 7 promotes, and re-run the helper, whose `{stats_json}` the `metadata` call reads: a relabeled `signature_source` moves the distribution, and a violation left as a WARN does not block the write. Note the relabeled export names, and each WARN left, in the evidence report's Remaining Warnings (§7).
|
|
163
|
+
- **Write metadata.json** once the stats helper's last run wrote `{stats_json}`. From `{project-root}`, send the fields you decide on stdin:
|
|
174
164
|
|
|
175
165
|
```bash
|
|
176
|
-
|
|
177
|
-
|
|
166
|
+
uv run {extractionInventoryHelper} metadata --inventory "{extraction_inventory}" --brief "{brief_path}" --stats "{stats_json}" --skf-root "{project-root}/_bmad/skf" --target <staging-skill-dir>/metadata.json <<'SKF_JSON'
|
|
167
|
+
{"version": "<version>", "language": "<language>", "source_root": "<source_root>", "source_commit": "<source_commit>", "source_ref": "<source_ref>", "qmd_version": null}
|
|
168
|
+
SKF_JSON
|
|
178
169
|
```
|
|
179
170
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
**Label violations.** A violation whose `field` is `provenance.entries[<i>].<field>` means entry `<i>` (its `export_name` is in the violation) carries a label its `extraction_method` does not allow: relabel it by the Relabel Rule in `{relabelRuleData}` (labels follow the method, a node kind comes from the recipe that matched, never invented, and a violation the rule cannot settle stays as a WARN), resolving `{verifyProvenanceCompletenessHelper}` ← first existing path in `{verifyProvenanceCompletenessProbeOrder}` for its `kind-at` lookup. Write the relabeled map back to `<staging-skill-dir>/provenance-map.json`, the copy step 7 promotes, re-run the helper, and write the `stats` and `confidence_distribution` it returns: a relabeled `signature_source` moves the distribution, and a violation left as a WARN does not block the write. Note the relabeled export names, and each WARN left, in the evidence report's Remaining Warnings (§7).
|
|
183
|
-
- Set `description` from the SKILL.md frontmatter `description` field (already assembled in section 2)
|
|
184
|
-
- Set `language` from source analysis (e.g., `"typescript"`, `"python"`) — use the primary language of the entry point file
|
|
185
|
-
- Set `ast_node_count` to the number of exports ast-grep matched (the recipe runner's `aggregates.exports`) when ast-grep ran, otherwise omit
|
|
186
|
-
- Set `tool_versions` based on tier and available tools. Resolve `{skf_version}` using this resolution chain (try each in order, use the first that succeeds):
|
|
187
|
-
1. `{project-root}/_bmad/skf/package.json` → read `.version` field
|
|
188
|
-
2. `node -p "require('./node_modules/bmad-module-skill-forge/package.json').version"`
|
|
189
|
-
3. `{project-root}/_bmad/skf/VERSION` → read plain text file (single line containing version string, written by the SKF installer)
|
|
190
|
-
4. `"unknown"` (final fallback — add a warning to the evidence report)
|
|
191
|
-
Never hardcode the version.
|
|
192
|
-
- Resolve `{ast_grep_version}` using this resolution chain (try each in order, use the first that succeeds):
|
|
193
|
-
1. `ast-grep --version` → parse version string from output (e.g., `ast-grep 0.41.1` → `"0.41.1"`)
|
|
194
|
-
2. `mcp__ast-grep__find_code` tool metadata (if version is exposed by the MCP server)
|
|
195
|
-
3. `"unknown"` (final fallback — add a warning to the evidence report)
|
|
196
|
-
- Resolve `{qmd_version}` using this resolution chain (try each in order, use the first that succeeds):
|
|
197
|
-
1. `qmd --version` → parse version string from output (e.g., `qmd 2.0.1` → `"2.0.1"`)
|
|
198
|
-
2. `mcp__plugin_qmd-plugin_qmd__status` → parse version if exposed in status output
|
|
199
|
-
3. `"unknown"` (final fallback — add a warning to the evidence report)
|
|
200
|
-
Note: QMD is a Bun/Node package (`@tobilu/qmd`). Install via `bun install -g @tobilu/qmd`.
|
|
201
|
-
- Store `commit_short` = first 8 characters of `source_commit` (or `"unknown"` if unavailable) for use in step 8 report.
|
|
202
|
-
- If `scripts_inventory` is non-empty, populate the `scripts[]` array (per-file `{file, purpose, source_file, confidence}` rows). If `assets_inventory` is non-empty, populate the `assets[]` array. Omit the `scripts[]` / `assets[]` arrays entirely when their inventory is empty — `stats.scripts_count` / `stats.assets_count` were already set by `{renderMetadataStatsHelper}` from the `scripts` / `assets` payload you passed it (0 when empty).
|
|
171
|
+
`version` is the source version step 3 reconciled, not the brief's default; `language` is the entry-point file's primary language; `source_root` is `{resolved-source-path}`; `source_commit` and `source_ref` (a tag, a branch or `HEAD`) are the resolved source's; `qmd_version` is what `qmd --version` prints (`qmd 2.0.1` gives `"2.0.1"`) when step 4 queried QMD; each is `null` when unavailable. When step 3 ran the recipes without the runner (extraction-patterns.md's **When the Runner Cannot Run**), add `ast_grep_version`, what `ast-grep --version` prints, `null` when only the MCP server ran them. For a component library, add `stats_extra`, the fields `{assemblyRulesData}`'s Component Library stats block adds; for a reference app, only its `notes`, when you write one (the stats helper writes `pattern_surfaces_documented`). The helper writes every other field from the brief, the inventory, `{stats_json}`, the staged SKILL.md (its `description`), the clock and the installed SKF (`--help` lists each). Bind `{commit_short}` ← its `commit_short` (step 8) and `{tool_versions}` ← its `tool_versions` (§7), and list its `warnings` in the evidence report. If it exits non-zero, fix what its `message` names and run it again; when it fails twice, **HARD HALT** (exit code 4, `write-failed`, phase `compile`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`), with `"path"` the staged `metadata.json`: "Cannot write metadata.json: {the helper's message}."
|
|
203
172
|
|
|
204
173
|
### 5. Build references/ Content
|
|
205
174
|
|
|
206
|
-
Create one reference file per major function group or type:
|
|
175
|
+
Create one reference file per major function group or type, of the exports the provenance map holds (never one the inventory marks `public: false`):
|
|
207
176
|
- Full function signatures with detailed parameter descriptions
|
|
208
177
|
- Complete usage examples (from source only)
|
|
209
178
|
- Related functions cross-references
|
|
@@ -213,7 +182,7 @@ Group functions logically by module, file, or functional area.
|
|
|
213
182
|
|
|
214
183
|
### 6. Build provenance-map.json Content
|
|
215
184
|
|
|
216
|
-
The map holds one entry per export of the inventory's `exports[]` (export_name, export_type, params[] as typed strings, return_type, source_file, source_line, confidence, extraction_method, ast_node_type, and signature_source, the tier that contributed the structural signature: "T1", "T1-low", "T2" or "T3"), then the entries and file entries the inventory does not hold. `{extractionInventoryHelper}` writes it: never type an entry the inventory holds. From `{project-root}`, run it with the source
|
|
185
|
+
The map holds one entry per export of the inventory's `exports[]` but one marked `public: false` (export_name, export_type, params[] as typed strings, return_type, source_file, source_line, confidence, extraction_method, ast_node_type, and signature_source, the tier that contributed the structural signature: "T1", "T1-low", "T2" or "T3"), then the entries and file entries the inventory does not hold. `{extractionInventoryHelper}` writes it: never type an entry the inventory holds. From `{project-root}`, run it with the brief's `source_repo` and the resolved source's `source_commit` and `source_ref`, the values §4's `metadata` call sends (`null` for one that is unavailable):
|
|
217
186
|
|
|
218
187
|
```bash
|
|
219
188
|
uv run {extractionInventoryHelper} provenance --inventory "{extraction_inventory}" --target "{project-root}/_bmad-output/.skf-stage/{skill-name}/provenance-map.json" <<'SKF_JSON'
|
|
@@ -226,6 +195,8 @@ It writes the map's header (`provenance_version` `"2.0"`, the skill's name, `ski
|
|
|
226
195
|
- `extraction_method: "ast-grep"` (an ast-grep rule matched the export): `confidence: "T1"`, `signature_source: "T1"`, an `[AST:...]` citation, and `ast_node_type` the `kind` the matching recipe declares in extraction-patterns.md, copied from the recipe, never inferred from the source (validate.md §7a asks ast-grep whether it is a real kind).
|
|
227
196
|
- `extraction_method: "source-read"` (the export was read by eye): `confidence: "T1-low"`, `signature_source: "T1-low"`, a `[SRC:...]` citation and `ast_node_type: null`.
|
|
228
197
|
|
|
198
|
+
For a `public-api` skill it writes no entry for an export the inventory marks `public: false`, off the package's public surface (step 3 §5's `init` or `add` marked it, and one of the inventory's `warnings` names each such export), and SKILL.md and `references/` leave it out too. When the runner gave no surface the rule could read, nothing is marked: every export has its entry, and an inventory warning says why.
|
|
199
|
+
|
|
229
200
|
Its `unsigned` lists the functions whose signature nobody read, which extract.md §5's `patch` left: read each one's parameters and return type from the source at its `source_line`, send them through that `patch` call as extract.md §5 says, and run the command above again, which rewrites the map.
|
|
230
201
|
|
|
231
202
|
**Then add what the inventory does not hold**, in one call, leaving out a list that is empty and the call when both are: the T2 and T3 entries (a docs-only skill's exports, from the inventory's `t3_items`), each with the fields above, and the file entries below.
|
|
@@ -236,7 +207,7 @@ uv run {extractionInventoryHelper} provenance --inventory "{extraction_inventory
|
|
|
236
207
|
SKF_JSON
|
|
237
208
|
```
|
|
238
209
|
|
|
239
|
-
The helper appends them and skips an entry whose `export_name` and `source_file` the map already holds (its `duplicates`): T2 and T3 content never replaces an inventory export's entry. If either call exits non-zero, fix what its `message` names and run it again; when it fails twice, **HARD HALT** (exit code 4, `write-failed`, phase `compile`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`), with `"path"` the staged `provenance-map.json`: "Cannot write the provenance map: {the helper's message}. Check permissions and disk space."
|
|
210
|
+
The helper appends them and skips an entry whose `export_name` and `source_file` the map already holds (its `duplicates`): T2 and T3 content never replaces an inventory export's entry. Send no entry for an export the inventory marks `public: false`: the helper skips one and lists it in its `not_public`. If either call exits non-zero, fix what its `message` names and run it again; when it fails twice, **HARD HALT** (exit code 4, `write-failed`, phase `compile`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`), with `"path"` the staged `provenance-map.json`: "Cannot write the provenance map: {the helper's message}. Check permissions and disk space."
|
|
240
211
|
|
|
241
212
|
`{renderMetadataStatsHelper}` (§4) checks these pairings on every T1 or T1-low entry, and the `signature_source` of an entry read by eye at any other confidence too (T2, T3 or none): the distribution bins every entry by its `signature_source`, so a `source-read` entry that claims `"T1"` is a violation whatever its `confidence`. Besides the two values above it accepts only the stack-map values `ast_bridge` (pairs with T1), `source_reading` (pairs with T1-low and a `signature_source` other than T1), `qmd_bridge` and `compose-from-skill` (no pairing); any other `extraction_method` on a T1 or T1-low entry is a violation.
|
|
242
213
|
|
|
@@ -250,7 +221,7 @@ Each `file_entries[]` row has the same shape regardless of `file_type`: `{file_n
|
|
|
250
221
|
|
|
251
222
|
### 7. Build evidence-report.md Content
|
|
252
223
|
|
|
253
|
-
Compilation audit trail: generation date, forge tier, source info, tool versions, extraction summary (files/exports/confidence), warnings. For validation-specific fields (Schema, Frontmatter, Body, Security, Description angle brackets, Tessl Review, Metadata, Provenance labels, Provenance lines), insert the placeholder text `[PENDING: populated by step 6]`. Step-06 will replace these placeholders with actual results. See `{skillSectionsData}` for full template.
|
|
224
|
+
Compilation audit trail: generation date, forge tier, source info, tool versions, extraction summary (files/exports/confidence), warnings. For validation-specific fields (Schema, Frontmatter, Body, Security, Description angle brackets, Tessl Review, Metadata, Provenance labels, Provenance lines), insert the placeholder text `[PENDING: populated by step 6]`. Step-06 will replace these placeholders with actual results. See `{skillSectionsData}` for full template. Fill its Tool Versions block from `{tool_versions}` (§4), with `not used` for a null one.
|
|
254
225
|
|
|
255
226
|
**Frontmatter, pinned fields:** emit YAML frontmatter at the top of `evidence-report.md` with at minimum `skill_name`, `generated`, `forge_tier`, and `t2_future_count`. Take `t2_future_count` from `{inventory_counts}.t2_future` (§1): the T2 annotations step 4 classified `T2-future`, counted by the helper. **Emit `t2_future_count: 0` when no T2-future annotations exist**: omission is indistinguishable from "no data" for downstream consumers and would silently flip the skf-test-skill §2b/§5b migration-section gate into Case 2/3 for a Case-1 skill. This frontmatter is the authoritative detection contract: `migration-section-rules.md` Case Rules parse it deterministically rather than grepping prose.
|
|
256
227
|
|
|
@@ -264,7 +235,7 @@ Compilation audit trail: generation date, forge tier, source info, tool versions
|
|
|
264
235
|
| {step} | {gate} | {decision}{value?} | {rationale} | {timestamp} |
|
|
265
236
|
```
|
|
266
237
|
|
|
267
|
-
Also write the inventory's `authoritative_files_scan` record into the evidence report. When the inventory's `intent_mapping` is not empty (a free-text `scripts_intent` or `assets_intent`, step 3 §4c), write one line per kind into the evidence report, so a reader sees which detected files the intent left out: `{kind} intent "{intent}": kept {each kept source file, or none}; left out {each left_out source file, or none}`. This step is the first point `evidence-report.md` is written to disk (staging dir, §8 below).
|
|
238
|
+
Also write the inventory's `authoritative_files_scan` record into the evidence report. When the inventory's `intent_mapping` is not empty (a free-text `scripts_intent` or `assets_intent`, step 3 §4c), write one line per kind into the evidence report, so a reader sees which detected files the intent left out: `{kind} intent "{intent}": kept {each kept source file, or none}; left out {each left_out source file, or none}`. When the inventory marks exports `public: false`, list each one the inventory `summary` call (§1's, or the same call run again) gives in its `not_public` in the evidence report, one line each as `{name} ({file})`, under the line `Off the public surface (left out of the provenance map, SKILL.md, references/, the context snippet and metadata.json's exports[]):`. This step is the first point `evidence-report.md` is written to disk (staging dir, §8 below).
|
|
268
239
|
|
|
269
240
|
### 8. Auto-Proceed
|
|
270
241
|
|