bmad-module-skill-forge 1.5.1 → 1.7.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.
Files changed (87) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/_data/pinned.yaml +1 -1
  3. package/docs/workflows.md +1 -1
  4. package/package.json +2 -2
  5. package/src/knowledge/version-paths.md +4 -3
  6. package/src/shared/health-check.md +1 -1
  7. package/src/shared/scripts/schemas/skill-brief.v1.json +5 -0
  8. package/src/shared/scripts/skf-atomic-write.py +4 -1
  9. package/src/shared/scripts/skf-description-guard.py +33 -80
  10. package/src/shared/scripts/skf-detect-language.py +34 -0
  11. package/src/shared/scripts/skf-detect-workspaces.py +54 -0
  12. package/src/shared/scripts/skf-enumerate-stack-skills.py +107 -16
  13. package/src/shared/scripts/skf-forge-tier-rw.py +4 -1
  14. package/src/shared/scripts/skf-manifest-ops.py +8 -4
  15. package/src/shared/scripts/skf-merge-ccc-exclusions.py +4 -1
  16. package/src/shared/scripts/skf-qmd-classify-collections.py +29 -3
  17. package/src/shared/scripts/skf-rebuild-managed-sections.py +72 -7
  18. package/src/shared/scripts/skf-scan-skill-md-structure.py +13 -3
  19. package/src/shared/scripts/skf-validate-brief-inputs.py +79 -18
  20. package/src/shared/scripts/skf-validate-brief-schema.py +47 -1
  21. package/src/shared/scripts/skf-validate-frontmatter.py +60 -2
  22. package/src/shared/scripts/skf-write-skill-brief.py +97 -17
  23. package/src/skf-analyze-source/assets/skill-brief-schema.md +10 -2
  24. package/src/skf-analyze-source/references/continue.md +3 -1
  25. package/src/skf-analyze-source/references/generate-briefs.md +30 -9
  26. package/src/skf-analyze-source/references/recommend.md +1 -1
  27. package/src/skf-brief-skill/SKILL.md +4 -2
  28. package/src/skf-brief-skill/assets/skill-brief-schema.md +6 -1
  29. package/src/skf-brief-skill/references/analyze-target.md +22 -6
  30. package/src/skf-brief-skill/references/confirm-brief.md +4 -0
  31. package/src/skf-brief-skill/references/gather-intent.md +72 -3
  32. package/src/skf-brief-skill/references/headless-args.md +3 -2
  33. package/src/skf-brief-skill/references/portfolio-similarity-check.md +16 -8
  34. package/src/skf-brief-skill/references/scope-definition.md +20 -2
  35. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  36. package/src/skf-brief-skill/references/write-brief.md +12 -2
  37. package/src/skf-create-skill/assets/compile-assembly-rules.md +20 -1
  38. package/src/skf-create-skill/assets/skill-sections.md +2 -1
  39. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +18 -5
  40. package/src/skf-create-skill/references/compile.md +5 -2
  41. package/src/skf-create-skill/references/extract.md +8 -3
  42. package/src/skf-create-skill/references/extraction-patterns.md +14 -5
  43. package/src/skf-create-skill/references/generate-artifacts.md +5 -1
  44. package/src/skf-create-skill/references/source-resolution-protocols.md +5 -2
  45. package/src/skf-create-skill/references/validate.md +4 -3
  46. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +3 -3
  47. package/src/skf-create-stack-skill/assets/stack-skill-template.md +56 -0
  48. package/src/skf-create-stack-skill/references/compile-stack.md +13 -2
  49. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -0
  50. package/src/skf-create-stack-skill/references/detect-integrations.md +5 -2
  51. package/src/skf-create-stack-skill/references/generate-output.md +49 -9
  52. package/src/skf-create-stack-skill/references/validate.md +8 -5
  53. package/src/skf-export-skill/references/load-skill.md +5 -4
  54. package/src/skf-export-skill/references/manifest-rebuild.md +1 -1
  55. package/src/skf-export-skill/references/token-report.md +2 -2
  56. package/src/skf-export-skill/references/update-context.md +25 -11
  57. package/src/skf-quick-skill/assets/skill-template.md +2 -2
  58. package/src/skf-quick-skill/references/compile.md +4 -2
  59. package/src/skf-quick-skill/references/write-and-validate.md +2 -2
  60. package/src/skf-refine-architecture/SKILL.md +1 -1
  61. package/src/skf-refine-architecture/references/gap-analysis.md +30 -4
  62. package/src/skf-refine-architecture/references/init.md +2 -0
  63. package/src/skf-refine-architecture/references/issue-detection.md +8 -3
  64. package/src/skf-test-skill/SKILL.md +1 -1
  65. package/src/skf-test-skill/references/coherence-check.md +4 -4
  66. package/src/skf-test-skill/references/coverage-check.md +58 -16
  67. package/src/skf-test-skill/references/external-validators.md +7 -7
  68. package/src/skf-test-skill/references/init.md +5 -4
  69. package/src/skf-test-skill/references/score.md +6 -3
  70. package/src/skf-test-skill/references/scoring-rules.md +12 -0
  71. package/src/skf-test-skill/references/source-access-protocol.md +19 -3
  72. package/src/skf-test-skill/scripts/compute-score.py +4 -1
  73. package/src/skf-update-skill/references/detect-changes.md +15 -2
  74. package/src/skf-update-skill/references/init.md +8 -1
  75. package/src/skf-update-skill/references/merge.md +22 -0
  76. package/src/skf-update-skill/references/re-extract.md +19 -7
  77. package/src/skf-update-skill/references/write.md +3 -0
  78. package/src/skf-verify-stack/SKILL.md +2 -2
  79. package/src/skf-verify-stack/assets/feasibility-report-template.md +1 -1
  80. package/src/skf-verify-stack/references/coverage.md +35 -6
  81. package/src/skf-verify-stack/references/init.md +6 -2
  82. package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
  83. package/src/skf-verify-stack/references/integrations.md +7 -3
  84. package/src/skf-verify-stack/references/report.md +5 -1
  85. package/src/skf-verify-stack/references/requirements.md +3 -1
  86. package/src/skf-verify-stack/references/synthesize.md +11 -3
  87. package/tools/cli/lib/installer.js +1 -0
@@ -49,7 +49,7 @@ These rules apply to every step in this workflow:
49
49
  | Aspect | Detail |
50
50
  |--------|--------|
51
51
  | **Inputs** | architecture_doc_path [required], vs_report_path [optional] |
52
- | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--architecture-doc <path>` (skip step 1 prompt for the required input); `--vs-report-path <path>` (skip step 1 prompt for the optional VS report) |
52
+ | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--architecture-doc <path>` (skip step 1 prompt for the required input); `--vs-report-path <path>` (skip step 1 prompt for the optional VS report); `--scope-skills <names>` (comma-separated in-scope skill names; overrides scope derivation in gap analysis) |
53
53
  | **Gates** | step 1: Input Gate [use args] | step 5: Review Gate [C] |
54
54
  | **Outputs** | `refined-architecture-{project_name}.md` at `{outputFolderPath}`, plus `refine-architecture-result-{timestamp}.json` and `refine-architecture-result-latest.json` |
55
55
  | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Per-flag args (`--architecture-doc`, `--vs-report-path`) consumed at the gates that would otherwise prompt. |
@@ -42,6 +42,21 @@ Parse the architecture document for statements describing two or more technologi
42
42
  - Each pair: `{library_a, library_b, architectural_context}`
43
43
  - `architectural_context`: the quoted text or paraphrased description of their relationship
44
44
 
45
+ ### 2b. Establish Document Scope
46
+
47
+ The skill inventory (Step 01 §2) can span a wider product surface than the architecture document under refinement. Pairs drawn from a different surface are NOT actionable gaps for THIS document — surfacing them injects irrelevant integration recommendations (e.g. wiring real-time A/V libraries into an admin-dashboard architecture).
48
+
49
+ Resolve the in-scope skill set:
50
+
51
+ - **If `{scope_skills}` was provided** (via `--scope-skills`, resolved in Step 01 §1): use it verbatim as `{in_scope_skills}` — it is authoritative.
52
+ - **Otherwise derive:** `{in_scope_skills}` = every inventory skill whose name or primary technology is referenced anywhere in the architecture document (reuse the technology references parsed in §2; match on skill name and library/technology keywords, case-insensitive, word-boundary). Be conservative — when a skill's relevance is ambiguous, treat it as **in-scope**. Surfacing a borderline gap is safer than burying a real one.
53
+
54
+ `{out_of_scope_skills}` = inventory skills not in `{in_scope_skills}`. A library pair is **out-of-scope** when either of its libraries is in `{out_of_scope_skills}`.
55
+
56
+ **Safe default:** If scope cannot be derived (e.g. the architecture references no inventory skill by name) and no `{scope_skills}` was provided, treat ALL skills as in-scope and note: "Could not derive document scope — analyzing all skill pairs." This preserves prior behavior rather than hiding gaps.
57
+
58
+ Store `{in_scope_skills}` and `{out_of_scope_skills}` as workflow state — Step 03 (issue detection) reuses them.
59
+
45
60
  ### 3. Generate All Possible Library Pairs
46
61
 
47
62
  From the skill inventory, generate all unique combinations of library pairs.
@@ -89,7 +104,9 @@ For each possible library pair NOT already documented in the architecture:
89
104
  - Do both libraries share a compatible protocol or data format?
90
105
  - Are they in the same language or is there a bridge mechanism available?
91
106
 
92
- **If compatible APIs exist but NO architecture mention:**
107
+ **Scope routing (from §2b):** Before classifying, check the pair's scope. If the pair is **out-of-scope** (either library is in `{out_of_scope_skills}`), do NOT add it to the gap list even when its APIs are compatible record it in the informational **Out-of-Scope** bucket instead (a compatible pair that belongs to a different product surface than this architecture). Only **in-scope** pairs proceed to gap classification below.
108
+
109
+ **If compatible APIs exist (in-scope pair) but NO architecture mention:**
93
110
  - Classify the gap type (Missing Integration Path, Undocumented Data Flow, or Absent Bridge Layer)
94
111
  - Document the connecting APIs from both skills
95
112
  - Propose a brief architecture section describing the integration
@@ -112,7 +129,7 @@ Suggestion: {proposed architecture section content}
112
129
 
113
130
  "**Pass 1: Gap Analysis — Undocumented Integration Paths**
114
131
 
115
- **Gaps Found:** {count}
132
+ **Gaps Found (in-scope):** {count}
116
133
 
117
134
  {IF gaps found:}
118
135
  | # | Library A | Library B | Gap Type | Connecting APIs |
@@ -121,15 +138,24 @@ Suggestion: {proposed architecture section content}
121
138
 
122
139
  {For each gap, display the full citation with evidence and suggestion}
123
140
 
141
+ {IF out-of-scope compatible pairs exist (from §2b/§5):}
142
+ **Out of scope for this document:** {oos_count} compatible pair(s) involve skills outside this architecture's surface — `{out_of_scope_skills}` — and were NOT counted as gaps. Listed for awareness only:
143
+
144
+ | Library A | Library B | Reason |
145
+ |-----------|-----------|--------|
146
+ | {lib_a} | {lib_b} | out-of-scope — not referenced in this architecture |
147
+
148
+ If any of these belongs in this architecture, re-run with `--scope-skills` naming the skills to include.
149
+
124
150
  {IF no gaps found AND N > 1:}
125
- **No undocumented integration paths detected.** The architecture document covers all compatible library pairs.
151
+ **No undocumented integration paths detected.** The architecture document covers all compatible in-scope library pairs.
126
152
 
127
153
  {IF no gaps found AND N == 1:}
128
154
  **⚠️ Gap analysis skipped — only 1 skill loaded.** Pairwise integration analysis requires at least 2 skills. If the architecture references multiple libraries, those without a matching skill are invisible to gap analysis. **Recommendation:** Generate skills for all architecture libraries with [CS] or [QS] before running [RA] for comprehensive gap detection.
129
155
 
130
156
  **Proceeding to issue detection (still produces value with 1 skill)...**"
131
157
 
132
- Store all gap findings as workflow state for Step 05. To ensure durability across long runs, also append a `<!-- [RA-GAPS] ... -->` comment block to `{forge_data_folder}/ra-state-{project_name}.md` containing the **complete formatted gap findings** (full citation blocks with evidence and suggestions, not just counts) — Step 05 can read this back if context degrades. **Do NOT write to `{output_folder}/refined-architecture-{project_name}.md` — that file is created only in step 5.**
158
+ Store all **in-scope** gap findings as workflow state for Step 05. To ensure durability across long runs, also append a `<!-- [RA-GAPS] ... -->` comment block to `{forge_data_folder}/ra-state-{project_name}.md` containing the **complete formatted gap findings** (full citation blocks with evidence and suggestions, not just counts) — Step 05 can read this back if context degrades. Record out-of-scope pairs under a separate `<!-- [RA-OUT-OF-SCOPE] ... -->` marker (NOT `[RA-GAPS]`) so Step 05 does not compile them into the refined document — they are informational only. **Do NOT write to `{output_folder}/refined-architecture-{project_name}.md` — that file is created only in step 5.**
133
159
 
134
160
  ### 7. Auto-Proceed to Next Step
135
161
 
@@ -51,6 +51,8 @@ Wait for user input. Store the validated architecture document path as `architec
51
51
  - If missing at user-provided path: attempt auto-probe (below) before giving up
52
52
  - Store VS report availability as `vs_report_available: true|false` and `vs_report_path`
53
53
 
54
+ **Scope hint (optional, `--scope-skills`):** If `--scope-skills <names>` was provided, store the parsed comma-separated list as `{scope_skills}` — gap analysis (Step 02 §2b) uses it as the authoritative in-scope skill set. If absent, leave `{scope_skills}` empty; Step 02 derives scope from the architecture document instead.
55
+
54
56
  ### 2. Scan Skills Folder
55
57
 
56
58
  **Resolve `{enumerateStackSkillsHelper}`** from `{enumerateStackSkillsProbeOrder}`; first existing path wins.
@@ -66,6 +66,8 @@ For each extracted claim, load the relevant skill(s) and check:
66
66
 
67
67
  If `vs_report_available` is true:
68
68
 
69
+ **Scope filter (reuse `{out_of_scope_skills}` from Step 02 §2b):** The VS report carries verdicts across the entire skill set, which may exceed this architecture's surface. Before promoting any verdict, check the pair's scope — if a verdict is for an **out-of-scope** pair (either library in `{out_of_scope_skills}`), do NOT promote it to an issue for THIS architecture; record it under the informational Out-of-Scope bucket instead. Only **in-scope** pairs' verdicts are promoted by the rules below.
70
+
69
71
  **Load the VS feasibility report and extract verdicts:**
70
72
  - **Risky verdicts** (match case-insensitively: "Risky", "RISKY", "risky"): Promote to confirmed issues with the VS evidence as additional citation
71
73
  - **Blocked verdicts** (match case-insensitively: "Blocked", "BLOCKED", "blocked"): Promote to critical issues requiring architecture redesign
@@ -100,7 +102,7 @@ Suggestion: {specific correction with API evidence}
100
102
 
101
103
  "**Pass 2: Issue Detection — Architecture vs. API Reality**
102
104
 
103
- **Issues Found:** {count} ({critical_count} critical, {major_count} major, {minor_count} minor)
105
+ **Issues Found (in-scope):** {count} ({critical_count} critical, {major_count} major, {minor_count} minor)
104
106
 
105
107
  {IF issues found:}
106
108
  | # | Libraries | Issue Type | Severity | Summary |
@@ -109,12 +111,15 @@ Suggestion: {specific correction with API evidence}
109
111
 
110
112
  {For each issue, display the full citation with evidence and suggestion}
111
113
 
114
+ {IF out-of-scope VS verdicts were set aside (from §4):}
115
+ **Out of scope for this document:** {oos_issue_count} VS verdict(s) involve skills outside this architecture's surface (`{out_of_scope_skills}`) and were NOT counted as issues. Listed for awareness only — re-run with `--scope-skills` if any belongs in scope.
116
+
112
117
  {IF no issues found:}
113
- **No contradictions detected.** Architecture claims align with verified skill API surfaces.
118
+ **No contradictions detected.** Architecture claims align with verified in-scope skill API surfaces.
114
119
 
115
120
  **Proceeding to improvement detection...**"
116
121
 
117
- Store all issue findings as workflow state for Step 05. To ensure durability across long runs, also append a `<!-- [RA-ISSUES] ... -->` comment block to `{forge_data_folder}/ra-state-{project_name}.md` containing the **complete formatted issue findings** (full citation blocks with architecture claims, skill evidence, VS verdicts, severity, and suggestions — not just counts) — Step 05 can read this back if context degrades. **Do NOT write to `{output_folder}/refined-architecture-{project_name}.md` — that file is created only in step 5.**
122
+ Store all **in-scope** issue findings as workflow state for Step 05. To ensure durability across long runs, also append a `<!-- [RA-ISSUES] ... -->` comment block to `{forge_data_folder}/ra-state-{project_name}.md` containing the **complete formatted issue findings** (full citation blocks with architecture claims, skill evidence, VS verdicts, severity, and suggestions — not just counts) — Step 05 can read this back if context degrades. Record any out-of-scope VS verdicts under the shared `<!-- [RA-OUT-OF-SCOPE] ... -->` marker (NOT `[RA-ISSUES]`) so Step 05 does not compile them — they are informational only. **Do NOT write to `{output_folder}/refined-architecture-{project_name}.md` — that file is created only in step 5.**
118
123
 
119
124
  ### 7. Auto-Proceed to Next Step
120
125
 
@@ -51,7 +51,7 @@ These rules apply to every step in this workflow:
51
51
  |--------|--------|
52
52
  | **Inputs** | skill_name [required]; optional flags: `--allow-workspace-drift`, `--no-discovery` (skip §4b Discovery Testing), `--no-health-check` (skip §7 health-check dispatch), `--tier=<Quick\|Forge\|Forge+\|Deep>` (bypass forge-tier.yaml sidecar requirement), `--threshold=<N>` (override pass threshold; CLI wins over `workflow.default_threshold` scalar) |
53
53
  | **Gates** | step 6: Confirm Gate [C] |
54
- | **Outputs** | test-report-{skill_name}.md with completeness score and result (PASS/FAIL); per-run `skf-test-skill-result-{run_id}.json` and `skf-test-skill-result-latest.json` written atomically under `{forge_version}/` |
54
+ | **Outputs** | per-run `test-report-{skill_name}-{run_id}.md` with completeness score and result (PASS/FAIL); per-run `skf-test-skill-result-{run_id}.json` and `skf-test-skill-result-latest.json` written atomically under `{forge_version}/` — downstream consumers (export-skill, update-skill `--from-test-report`) glob `test-report-{skill_name}-*.md` and pick newest by parsed ISO timestamp |
55
55
  | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true |
56
56
  | **Exit codes** | See "Exit Codes" below |
57
57
 
@@ -43,9 +43,9 @@ The first call returns `{ description: {satisfied, matched_synonym, tried[]}, us
43
43
 
44
44
  - `satisfied: true` → no finding.
45
45
  - `satisfied: false` AND family is `description` AND the SKILL.md frontmatter has a non-empty `description` field → no finding (the frontmatter alternative satisfies the family per the original rule).
46
- - Otherwise → **High severity** finding: `naive-coherence — missing required section: {family}` (the `tried[]` list from the JSON identifies which synonyms were checked: `Description`/`Overview`/`Purpose`/`Summary` for description; `Usage`/`Examples`/`How to use`/`Quickstart`/`Quick Start`/`Getting Started`/`Common Workflows` for usage; `API`/`API Surface`/`Exports`/`Public API`/`Interface`/`Reference`/`Key API Summary` for api_surface).
46
+ - Otherwise → **High severity** finding: `naive-coherence — missing required section: {family}` (the `tried[]` list from the JSON identifies which synonyms were checked: `Description`/`Overview`/`Purpose`/`Summary` for description; `Usage`/`Usage Patterns`/`Examples`/`How to use`/`Quickstart`/`Quick Start`/`Getting Started`/`Common Workflows`/`Adoption Steps` for usage; `API`/`API Surface`/`Exports`/`Key Exports`/`Public API`/`Interface`/`Reference`/`Key API Summary`/`Pattern Surface` for api_surface).
47
47
 
48
- The script matches case-insensitively and tolerates `##`/`###` heading levels. SKF-template skills' `## Quick Start`, `## Common Workflows`, and `## Key API Summary` are first-class synonyms baked into the script — they will surface with `satisfied: true` and the corresponding `matched_synonym` field.
48
+ The script matches case-insensitively and tolerates `##`/`###` heading levels. SKF-template skills' headings are first-class synonyms baked into the script — the Deep/create-skill `## Quick Start`, `## Common Workflows`, and `## Key API Summary`, the quick-skill `## Usage Patterns` and `## Key Exports`, and the reference-app overrides `## Adoption Steps` (usage) and `## Pattern Surface` (api_surface) so they all surface with `satisfied: true` and the corresponding `matched_synonym` field.
49
49
 
50
50
  **2.2 Code fence balance.** Read `unbalanced_fences` from the second JSON blob. **`true` → High severity** finding: `naive-coherence — unbalanced code fence (unclosed block)` (the JSON's `fence_count` may be cited in the detail).
51
51
 
@@ -54,7 +54,7 @@ The script matches case-insensitively and tolerates `##`/`###` heading levels. S
54
54
  **2.4 Exports cross-used in a usage-family section.** For each function name reported in the step 3 subagent inventory (`exports[].name` where `kind == "function"` or `kind == "method"`):
55
55
  - Determine the usage-family search scope:
56
56
  - **Single-body skill** (no `references/` directory, or `## Full*` sections carry real content): the span from §2.1's `matched_synonym` anchor to the next `^## ` anchor.
57
- - **Split-body skill** (a `references/` directory exists alongside SKILL.md AND the SKILL.md `## Full*` sections are stubs/pointers): the union of EVERY usage-family heading present in SKILL.md (`Usage`/`Examples`/`How to use`/`Quickstart`/`Quick Start`/`Getting Started`/`Common Workflows`/`Key API Summary`), each from its anchor to the next `^## ` anchor, PLUS the full text of every file under `references/`.
57
+ - **Split-body skill** (a `references/` directory exists alongside SKILL.md AND the SKILL.md `## Full*` sections are stubs/pointers): the union of EVERY usage-family heading present in SKILL.md (`Usage`/`Usage Patterns`/`Examples`/`How to use`/`Quickstart`/`Quick Start`/`Getting Started`/`Common Workflows`/`Adoption Steps`/`Key API Summary`/`Pattern Surface`/`Key Exports`), each from its anchor to the next `^## ` anchor, PLUS the full text of every file under `references/`.
58
58
  - `grep -c "{export.name}"` across that scope and sum the counts.
59
59
  - **Zero occurrences across the entire scope → High severity** finding: `naive-coherence — exported {kind} \`{name}\` is not referenced in any usage-family section or reference file`. This catches the "documented but unused" failure mode that trivially fails discovery testing. A method referenced in any usage-family section OR any `references/` file satisfies the check.
60
60
 
@@ -123,7 +123,7 @@ Parent strips wrapping markdown fences (if present) before parsing. If subagent
123
123
 
124
124
  4. **Scripts/assets directory check:** If a `scripts/` or `assets/` directory exists alongside SKILL.md, verify that a "Scripts & Assets" section (Section 7b) is present in SKILL.md. This directory-level check applies in both modes (naive mode performs it in Section 2; contextual mode performs it here alongside per-reference validation). Flag absence as Medium severity gap per `{scoringRulesFile}`.
125
125
 
126
- 5. **Path containment:** for every resolved reference target, compute its canonical path (`os.path.realpath`) and require that it lives inside `{skillDir}` OR inside `{source_path}` (the extraction tree recorded in metadata.json). References whose canonical path escapes both roots (e.g. `../../../etc/passwd`, absolute paths to unrelated dirs, symlink redirections outside the skill or its source) are **High severity** findings: `coherence — reference escapes skill/source sandbox: {raw_ref} → {canonical_path}`. Do NOT validate the target's contents for escaping references — the escape itself is the finding.
126
+ 5. **Path containment:** for every resolved reference target, compute its canonical path (`os.path.realpath`) and require that it lives inside `{skillDir}`, inside `{source_path}` (the extraction tree recorded in metadata.json), OR — for stack skills — inside `{skills_output_folder}`. The third root applies only here in contextual mode: 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 single `source_path` to anchor them. References whose canonical path escapes all applicable roots (e.g. `../../../etc/passwd`, absolute paths to unrelated dirs, symlink redirections outside the skill, its source, or for a stack — the skills output tree) are **High severity** findings: `coherence — reference escapes skill/source sandbox: {raw_ref} → {canonical_path}`. Canonicalization happens before the root check, so a symlink that points outside every applicable root is still caught. Do NOT validate the target's contents for escaping references — the escape itself is the finding.
127
127
 
128
128
  ### 5. Contextual Mode: Check Integration Pattern Completeness
129
129
 
@@ -73,15 +73,15 @@ test-skill is a quality gate — it MUST NOT trust subagent output blindly. Befo
73
73
 
74
74
  1. Strip wrapping markdown fences before parsing. Subagents frequently return JSON wrapped in a code fence — a line of three backticks (optionally followed by a language tag like `json`) preceding the JSON and a closing line of three backticks after it — despite prompt instructions to return raw JSON. When the first non-empty line of the response is three backticks (optionally with a language tag) and the last non-empty line is three backticks, remove those two fence lines before parsing. Then parse the remaining content as JSON. On parse failure of the inner content → HALT "coverage-check: subagent response not valid JSON".
75
75
  2. Required keys present: `exports` (list), `cross_check_mismatches` (list — may be empty). Missing key or wrong type → HALT "coverage-check: subagent JSON schema invalid — missing/typo: {key}". Note: the parent already knows the skill name from workflow context (`{resolved_skill_package}` from step 1) — the subagent is not required to echo it back, and doing so introduces a contract-drift surface without improving verification.
76
- 3. Each `exports[]` entry must be a dict with at minimum `name` (non-empty string) and `kind` (one of `function|class|type|constant|hook|interface|method`). Reject entries violating this; if >0 rejections, HALT "coverage-check: subagent returned malformed export entries — {count} entries do not match schema".
76
+ 3. Each `exports[]` entry must be a dict with at minimum `name` (non-empty string) and `kind` (one of `function|class|type|constant|hook|interface|method|struct|enum|trait|macro|adapter`). The enum spans the constructs SKF actually documents across languages and skill types: JS/TS (`function`/`class`/`type`/`constant`/`hook`/`interface`/`method`), Rust public-API items (`struct`/`enum`/`trait`/`macro` — alongside the shared `type`/`constant`/`function`), and stack-composition scaffolds (`adapter`). Reject entries violating this; if >0 rejections, HALT "coverage-check: subagent returned malformed export entries — {count} entries do not match schema".
77
77
  4. `cross_check_mismatches[]` entries (when non-empty) must carry `export`, `skill_md_line`, `reference_file`, `reference_line`, `issue`. Missing fields → HALT.
78
78
 
79
79
  **Spot-check (ground-truth verification, zero-hallucination guard):**
80
80
 
81
81
  1. If `len(exports) == 0`: skip the spot-check (no names to verify). Zero-exports policy is handled in the §2b zero-exports guard.
82
82
  2. Otherwise, sample `min(3, len(exports))` exports deterministically — by default take indices `[0, len//2, len-1]` (first, middle, last) from the `exports` array after a stable sort by `name`.
83
- 3. For each sampled export, run: `grep -n "{export.name}" {resolved_skill_package}/SKILL.md` in the parent context. The name MUST appear at least once.
84
- 4. If ANY sampled name returns zero matches, HALT "coverage-check: subagent inventory failed ground-truth spot-check — `{name}` claimed as export but absent from SKILL.md".
83
+ 3. For each sampled export, grep for the name across SKILL.md **and every reference file the subagent listed in its `references[]` array** (the documented surface of a split-body skill spans both): `grep -n "{export.name}" {resolved_skill_package}/SKILL.md {resolved_skill_package}/{each references[] path}` in the parent context. The name MUST appear at least once somewhere in that file set. Greping SKILL.md alone would false-HALT a split-body skill whose sampled export is documented only in a `references/*.md` file (a legitimate placement per §1 step 2 and the split-body note below).
84
+ 4. If a sampled name returns zero matches across SKILL.md **and** all listed reference files, HALT "coverage-check: subagent inventory failed ground-truth spot-check — `{name}` claimed as export but absent from SKILL.md and the listed reference files".
85
85
 
86
86
  These checks catch two hallucination classes: schema-shape drift (subagent paraphrased or dropped the contract) and fabricated exports (subagent invented names not in the document). Both are disqualifying for a grader skill — do not downgrade to a warning.
87
87
 
@@ -130,9 +130,12 @@ Start from the package entry point (see 0b) and identify the public API surface.
130
130
  - Cannot verify signatures — note as "unverified" in report
131
131
 
132
132
  **Forge Tier (ast-grep available):**
133
+
134
+ **Before delegating, the parent builds a `documented_signatures` map** from the §1 inventory: `{ "{name}": {"params": "...", "return_type": "..."} }` for every documented export that carries a signature. Pass this map as a structured input alongside the per-file ast-grep instructions. Without it the subagent has only the bare source — it cannot diff against the documented signatures, so `signature_mismatches[]` collapses to empty and Signature Accuracy defaults to 100% on a name-only pass (a silent false positive). The subagent must populate `documented_sig` from this map, not invent it.
135
+
133
136
  For EACH source file that defines public API exports, delegate to a subagent that:
134
- 1. Uses ast-grep to extract all exported symbols with their full signatures
135
- 2. Matches each export against the documented inventory
137
+ 1. Uses ast-grep to extract all exported symbols with their full signatures (the `source_sig`)
138
+ 2. Matches each export against the `documented_signatures` map supplied by the parent, comparing params (name, type, order, optionality) and return type
136
139
  3. Returns ONLY the JSON object below — no prose, no commentary, no markdown fences:
137
140
 
138
141
  ```json
@@ -164,7 +167,16 @@ Parent strips wrapping markdown fences (if present) before parsing, same as §1a
164
167
 
165
168
  After the source-code analysis (§2) completes, compute `total_exports` — the count of exports discovered in the source / provenance-map / metadata.json, per the stratified-scope and State 2 rules resolved in §4.
166
169
 
167
- **If `total_exports == 0` AND `docs_only_mode == false`:** HALT with:
170
+ **Stack-skill branch (`metadata.json.skill_type == "stack"`):** A stack skill's own barrel is empty by design — it composes constituent skills rather than exporting a proprietary surface — so `total_exports` derived from its own barrel is `0` for a *correctly* built stack, and its `[from skill: …]` citations never trip §0's `[EXT:…]`-only docs-only trigger. The zero-exports HALT below targets individual source-based skills and must NOT fire for stacks. Derive the stack's coverage denominator (`stack_denominator`) from its composition surface, in priority order, and use it as `total_exports` for the rest of coverage scoring:
171
+
172
+ 1. Provenance-map cited-contract count — when `{forge_data_folder}/{skill_name}/provenance-map.json` exists **and its `entries[]` is non-empty**: count the named cited contracts, **excluding entries whose `export_name` contains `::`** (impl-block methods roll up under an already-counted type). Use the same exclusion as §4b's named-export rule so the §2b and §4b stack denominators agree.
173
+ 2. Otherwise the composition surface from `metadata.json`: `len(libraries) + len(integration_pairs)`.
174
+
175
+ **If `stack_denominator == 0`** (no provenance-map or empty `entries[]`, AND `libraries` and `integration_pairs` are both empty): HALT 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.
176
+
177
+ Otherwise carry `stack_denominator` forward as `total_exports`, skip the HALT below, and continue — §2c and §4b consume this same denominator via their own stack-skill branches. (Stacks route to contextual mode per `detect-mode.md` and have a dedicated section in `scoring-rules.md`, so they are first-class — the indeterminate-surface HALT is an individual-skill guard only.)
178
+
179
+ **If `total_exports == 0` AND `docs_only_mode == false` AND `metadata.json.skill_type != "stack"`:** HALT with:
168
180
 
169
181
  ```
170
182
  Error: indeterminate API surface — 0 exports discovered in source for {skill_name}.
@@ -183,6 +195,24 @@ Do not write the Coverage Analysis section. Do not proceed to scoring. This is a
183
195
 
184
196
  **If `docs_only_mode == true` and the documented inventory is empty:** HALT with the analogous docs-only message ("docs-only skill declares zero items — no API surface to test").
185
197
 
198
+ ### 2c. Reconcile Documented vs Source Surface (Deterministic Intersection)
199
+
200
+ On a split-body skill the §1 inventory (documented surface) and the §2 AST output (source barrel) are two independent lists, so the `Documented` count must be their **intersection**, not a parent estimate. Compute three sets deterministically so the Export Coverage numerator is reproducible across runs and reviewers:
201
+
202
+ **Stack-skill branch (`metadata.json.skill_type == "stack"`):** A stack's source barrel is empty by design, so `barrel_set` is `{}` and the `|documented_set ∩ barrel_set|` / `|barrel_set|` formulas in steps 3–4 would divide by zero. Do NOT use the source barrel for a stack. Instead use the §2b `stack_denominator` as the denominator and compute the numerator by full-grep verification (same mechanism as the scalar-denominator branch below): enumerate the stack's composition-surface names — the provenance-map cited-contract names (`::`-excluded) when the map exists and is non-empty, else the `libraries` and `integration_pairs` names — and for each, grep across `SKILL.md ∪ references/*.md`; `Documented` := the count that appear at least once. Set `Missing` := `stack_denominator − Documented` and omit `Stale` (no source barrel to enumerate against). Carry these into §3/§4 with `Export Coverage = Documented / stack_denominator * 100`, and skip steps 1–4 below.
203
+
204
+ 1. **`documented_set`** := the de-duplicated set of `name` from the §1 inventory `exports[]`, excluding `kind: "method"` (methods are members of an already-counted class/type, not top-level barrel exports).
205
+ 2. **`barrel_set`** := the union of `exports_found[]` across every §2 per-file result (the actual source public surface). When a stratified-scope or State-2 denominator applies (see §4) **and it resolves to an enumerated name set** — the priority-2/3 re-derivation from `scope.tier_a_include` / `scope.include` globs — `barrel_set` is that resolved denominator's name set instead of the raw union.
206
+
207
+ **Scalar-denominator branch (§4 priority 1):** when the resolved denominator is the scalar `metadata.json.stats.effective_denominator` (a count with **no enumerated name set**), there is no `barrel_set` to intersect against — the `documented_set ∩ barrel_set` formula in step 3 does not apply. Instead compute the numerator by full-grep verification: for each name in `documented_set`, grep it across `SKILL.md ∪ references/*.md`; `Documented` := the count of `documented_set` names that appear at least once. Use `effective_denominator` directly as the denominator, set `Missing` := `effective_denominator − Documented`, and omit `Stale` (not enumerable without a barrel name set). If §4b's numerator-ground-truth arm fires (it triggers only when `exports_documented == effective_denominator`), its verified count is authoritative and **overrides** this numerator — do not apply both.
208
+ 3. Compute (**enumerated path only** — skip when the scalar-denominator branch above applies):
209
+ - `Documented` := `|documented_set ∩ barrel_set|`
210
+ - `Missing` := `barrel_set − documented_set` (in source, not documented)
211
+ - `Stale` := `documented_set − barrel_set` (documented, not in source)
212
+ 4. Carry the resulting counts into §3's table and §4's Export Coverage: `Export Coverage = |Documented| / |barrel_set| * 100` (enumerated path) or `Documented / effective_denominator * 100` (scalar-denominator branch). Record the counts in the Coverage Analysis section so the numerator is auditable.
213
+
214
+ This removes the parent-side guess that otherwise swings the documented count between runs (e.g., "85 from the AST agent" vs "~120 from a hand intersection") and can cross the PASS threshold on split-body skills.
215
+
186
216
  ### 3. Build Coverage Results
187
217
 
188
218
  Aggregate findings across all source files:
@@ -193,12 +223,12 @@ Aggregate findings across all source files:
193
223
  |--------|------|-----------|-----------------|-----------|--------|
194
224
  | {name} | function/class/type | yes/no | yes/no/unverified | src/file.ts:42 | PASS/FAIL/WARN |
195
225
 
196
- **Summary counts:**
197
- - Total exports in source: {N}
198
- - Documented in SKILL.md: {N}
199
- - Missing documentation: {N}
226
+ **Summary counts** (from the §2c reconciliation — not re-estimated here):
227
+ - Total exports in source: `|barrel_set|`
228
+ - Documented in SKILL.md: `Documented` (`|documented_set ∩ barrel_set|`)
229
+ - Missing documentation: `|Missing|`
200
230
  - Signature mismatches: {N}
201
- - Undocumented in SKILL.md but not in source (stale docs): {N}
231
+ - Undocumented in SKILL.md but not in source (stale docs): `|Stale|`
202
232
 
203
233
  ### 4. Load Scoring Rules
204
234
 
@@ -210,8 +240,8 @@ Load `{scoringRulesFile}` to determine category scores:
210
240
 
211
241
  **Stratified-scope denominator (monorepo curated subsets):** Before computing Export Coverage, check whether the Source Access Protocol's stratified-scope clause applies to this skill (see `{sourceAccessProtocol}` §Source API Surface Definition — "Stratified-scope monorepo packages"). When it applies:
212
242
 
213
- 1. **Prefer `metadata.json.stats.effective_denominator`** when present. Use it directly as `total_exports`.
214
- 2. **Otherwise re-derive at test time** from the brief's scope globs per the protocol. When the brief supplies `scope.tier_a_include`, re-derive from that narrower list; otherwise re-derive from `scope.include`. Use the resulting union count as `total_exports`.
243
+ 1. **Prefer `metadata.json.stats.effective_denominator`** when present. Use it directly as `total_exports` — but apply the **denominator deflation guard** from `{sourceAccessProtocol}` stratified-scope resolution step 1: when source is readable, re-derive the source barrel and, if it exceeds `effective_denominator` by >25% with no `scope.tier_a_include`, treat the stored value as deflated, use the re-derived count, and emit the `denominator deflation` gap.
244
+ 2. **Otherwise re-derive at test time** from the brief's scope globs per the protocol. When the brief supplies `scope.tier_a_include`, re-derive from that narrower list (excluding umbrella barrel files per the protocol's umbrella-barrel note); otherwise re-derive from `scope.include`. Use the resulting union count as `total_exports`.
215
245
  3. **Run the denominator inflation check** defined in `{sourceAccessProtocol}` stratified-scope resolution step 3 whenever re-derivation fell back to `scope.include`. If the `scope.include` union exceeds the provenance-map entry count by more than 25%, emit the Medium-severity `denominator inflation — coarse scope.include union exceeds authored surface` gap and append it to the Coverage Analysis gap list.
216
246
  4. **Apply provenance-map canonicalization** before intersecting documented exports against the raw provenance-map entry list — see `{sourceAccessProtocol}` §Source API Surface Definition → "Provenance-map canonicalization" for the folding rules (`_def`/`_exact` suffix, `a11y_` prefix, renderer-prefix disambiguation). Skip folding when `metadata.json.stats.effective_denominator` is present and already equals the raw provenance-map entry count. Record the fold summary in the Coverage Analysis section so it's auditable.
217
247
 
@@ -242,6 +272,10 @@ suspects denominator gaming has the evidence inline.
242
272
 
243
273
  After the denominator has been resolved (standard, stratified, or State 2), cross-check export counts *within each semantic cluster* to detect extraction drift without false-positiving on intentional multi-denominator reporting. Picking the denominator silently when sources disagree is a known friction — the tester cannot tell whether to trust the pick, ignore the drift, or report it. Make it explicit, but only for counts that are authored to measure the *same* surface.
244
274
 
275
+ **Stack-skill branch (`metadata.json.skill_type == "stack"`):** Skip the intra-cluster and cross-cluster count comparisons, the `confidence_distribution` sum check, AND the numerator ground-truth check below — none apply to a stack. For a stack the three counts measure intentionally *different* surfaces, so comparing them yields only false drift: `exports_documented` / `exports[]` measure the stack's own barrel (empty by design → `0`), the provenance-map enumerates the cited *constituent* contracts, and `confidence_distribution` bins those constituents — so for a stack treat `confidence_distribution` as a per-constituent count (it sums to the constituent count, not to `exports_documented`) and do not assert it against `exports_documented`. The numerator-inflation arm below targets an *individual* skill whose `exports_documented` was padded to equal `effective_denominator`; a stack's numerator is instead computed by full-grep in §2c's stack-skill branch and stack `metadata.json.stats` carries no `effective_denominator`, so the arm is not run. Record the denominator using the §2b source — `Denominator: stack composition ({N} cited contracts)` when the provenance-map supplied it, or `Denominator: stack composition ({N} libraries + integration pairs)` when the composition surface did — then proceed.
276
+
277
+ **Reference-app branch (`metadata.json.scope_type == "reference-app"`):** Skip the intra-cluster and cross-cluster count comparisons, the `confidence_distribution` sum check, AND the numerator ground-truth check below — none apply to a reference app, whose counts measure intentionally *different* surfaces. A reference app documents wiring/construct **pattern surfaces**, not a library export barrel, so `metadata.json.exports[]` is empty by design (`0`), `stats.exports_public_api` / `stats.pattern_surfaces_documented` count the documented pattern surfaces, and `confidence_distribution` bins the per-citation provenance entries (it sums to the citation count, not to `pattern_surfaces_documented`). Comparing these yields only false drift: a spurious Cluster-A "barrel drift" (`exports_public_api` vs `exports[].length == 0`) and a Cluster-B "documented-surface drift" (`pattern_surfaces_documented` vs the larger `confidence_distribution` sum). The numerator-inflation arm also does not apply — a reference app carries no `effective_denominator` (see the `skf-create-skill` reference-app carve-out), so the `exports_documented == effective_denominator` signature never fires. Record the denominator as `Denominator: pattern-surface ({pattern_surfaces_documented})` and proceed. (`referenceApp` and a stack skill are distinct signals — a skill is one or the other, never both, so only one of these two branches applies.)
278
+
245
279
  **Collect available counts (skip any that are absent) and bin them into two clusters:**
246
280
 
247
281
  **Cluster A — public-barrel surface** (what `__init__.py` / `index.ts` / `lib.rs` re-exports):
@@ -252,11 +286,12 @@ After the denominator has been resolved (standard, stratified, or State 2), cros
252
286
  **Cluster B — documented surface** (what was extracted and documented, including methods and submodule members):
253
287
 
254
288
  3. `metadata.json.stats.exports_documented` — the declared documented count
255
- 4. Provenance-map entry count (if `{forge_data_folder}/{skill_name}/provenance-map.json` exists)
289
+ 4. Provenance-map **named-export count** (if `{forge_data_folder}/{skill_name}/provenance-map.json` exists) — count only top-level named exports: **exclude entries whose `export_name` contains `::`** (impl-block methods like `Type::method`, which roll up under an already-counted type and are not separate barrel exports). Comparing the raw entry count instead false-positives on any method-enumerating provenance map (common for Rust/TS type-heavy skills): e.g. a map with 88 named exports + 48 `Type::method` entries reports 136 against `exports_documented` ≈ 92 → a spurious ~32% Cluster-B drift, while the comparable named-export count (88) agrees within ~4%.
290
+ 5. `confidence_distribution` sum (`t1 + t1_low + t2 + t3`, when present in `metadata.json.stats`) — every extracted/documented export is binned into exactly one confidence tier, so the distribution must sum to the documented-surface total; a divergence (e.g., distribution sums to 91 while `exports_documented` is 85) is an internal-consistency defect even when the two clusters look fine
256
291
 
257
292
  Cluster assignment is canonical: `skf-create-skill` step 5 derives `exports_public_api` from entry-point validation and writes the `exports[]` array from the same barrel surface (see `skf-create-skill/references/compile.md:105`), while `exports_documented` tracks the broader documented surface that the provenance-map also enumerates.
258
293
 
259
- **Intra-cluster divergence (Medium):** For each cluster, if two counts are present and disagree by more than 10% of the larger, emit a **Medium**-severity gap titled `metadata drift — {cluster} export counts diverge` (substitute `barrel` for Cluster A, `documented-surface` for Cluster B). Enumerate the offending counts in the gap body (e.g., `stats.exports_public_api=55, exports[].length=48` → 13% drift). This is the real drift signal — the two sources should mirror the same surface and they don't, so upstream extraction or compilation produced inconsistent output that a re-compile should reconcile. Classify under structural/metadata coherence regardless of naive/contextual mode.
294
+ **Intra-cluster divergence (Medium):** For each cluster, if two or more counts are present and the largest and smallest disagree by more than 10% of the larger, emit a **Medium**-severity gap titled `metadata drift — {cluster} export counts diverge` (substitute `barrel` for Cluster A, `documented-surface` for Cluster B). Enumerate the offending counts in the gap body (e.g., `stats.exports_public_api=55, exports[].length=48` → 13% drift). This is the real drift signal — the two sources should mirror the same surface and they don't, so upstream extraction or compilation produced inconsistent output that a re-compile should reconcile. Classify under structural/metadata coherence regardless of naive/contextual mode.
260
295
 
261
296
  **Cross-cluster divergence (Info):** After intra-cluster checks, if both clusters resolved to a representative count (pick the higher of each cluster's available counts) and the two cluster values differ by more than 10%, append a single **Info**-severity note titled `multi-denominator reporting — barrel vs documented surface` with both values (e.g., `barrel=55, documented=114`). This is expected for skills whose documented surface intentionally exceeds the barrel (methods, submodule members, re-exported classes) — it is not drift. The note exists so the test report makes the dual-denominator design visible and auditable without demanding action.
262
297
 
@@ -266,7 +301,14 @@ Cluster assignment is canonical: `skf-create-skill` step 5 derives `exports_publ
266
301
 
267
302
  **When only one count is available across both clusters:** Skip silently — there is nothing to cross-check.
268
303
 
269
- Append any findings (Medium gaps and/or the Info note) to the Coverage Analysis section's gap list (built in section 5) so they surface in the final test report alongside coverage and signature findings. Findings are informational about data quality they do not change the denominator chosen above.
304
+ **Numerator ground-truth force a full grep on the inflation signature:** The intra/cross-cluster checks above only compare *counts*; they cannot tell whether the declared documented exports actually appear in the skill. When `metadata.json.stats.exports_documented == effective_denominator` exactly (the numerator equals the denominator — the signature of a numerator inflated to match the full surface), do **not** trust the documented count. Grep every declared export name (the full `metadata.exports[]` / provenance-map declared set — not the §1a 3-sample) against `SKILL.md ∪ references/*.md`. The count of declared names that actually appear is the **verified numerator**:
305
+
306
+ - If verified == declared, the skill is genuinely fully documented — no finding; coverage stands.
307
+ - If verified < declared, emit a **High**-severity gap `numerator inflation — {declared − verified} of {declared} declared exports absent from SKILL.md/references` listing the absent names, and use the verified count as the Export Coverage numerator (overriding `exports_documented`). A numerator padded to equal the denominator otherwise produces a tautological 100% that passes the gate.
308
+
309
+ The full grep runs only on the exact equality signature, so it adds no cost to the common case where the numerator is already below the denominator. Unlike the count-coherence findings above, this arm is authoritative — it changes the numerator used for scoring.
310
+
311
+ Append any findings (Medium gaps, the Info note, and/or the High numerator-inflation gap) to the Coverage Analysis section's gap list (built in section 5) so they surface in the final test report alongside coverage and signature findings. The count-coherence findings are informational about data quality and do not change the denominator chosen above; the numerator ground-truth arm is the one exception that overrides the numerator.
270
312
 
271
313
  ### 5. Append Coverage Analysis to Output
272
314
 
@@ -24,10 +24,10 @@ Before running external validators, check if `{forge_data_folder}/{skill_name}/e
24
24
  **Pre-check (untracked or staged-only file):** Run `git ls-files --error-unmatch {skillDir}/SKILL.md 2>/dev/null`.
25
25
  - If the command fails (exit code non-zero) or git is not available, the file is either **untracked** (new, never committed) or we're in a **non-git environment**:
26
26
  - Check if `{skillDir}/metadata.json` exists and has a `generation_date` field
27
- - Compare `metadata.json` `generation_date` against the evidence report's generation date (from its frontmatter or `## Validation Results` timestamp)
28
- - If timestamps match within the same minute (same workflow session): auto-reuse is safe — the evidence report was generated from the same SKILL.md content
29
- - If timestamps differ or metadata.json is missing: treat as stale, proceed to section 2 for a fresh run
30
- - Note: "Staleness check: SKILL.md is untracked/non-git — using metadata.json timestamp comparison."
27
+ - Compare `metadata.json` `generation_date` against the evidence report's generation date (from its frontmatter `generated` field or the `## Validation Results` timestamp)
28
+ - **Precision guard (mirror of the git-path Primary-cross check):** date-granularity equality is NOT proof of same-session generation. A same-day `update-skill` that regenerates SKILL.md *after* the cached evidence report was produced yields the same calendar date (e.g. `metadata.generation_date: 2026-05-23T00:00:00Z` vs evidence `generated: 2026-05-23`), so reusing on date-equality alone would publish pre-update scores for post-update content. Auto-reuse is safe **only** when BOTH timestamps carry a real time-of-day component neither a date-only string (`2026-05-23`) nor a midnight-coerced `…T00:00:00Z` — AND they match to the minute. In that case auto-reuse: the evidence report was generated from the same SKILL.md content.
29
+ - Otherwise if either timestamp is date-only or midnight-coerced, if they differ, or if `metadata.json` is missing or has no `generation_date` — treat as stale and proceed to section 2 for a fresh run. Forcing a fresh run on ambiguous precision matches the git path's bias toward freshness over reusing possibly-stale scores.
30
+ - Note: "Staleness check: SKILL.md is untracked/non-git — using metadata.json timestamp comparison (date-only/midnight timestamps force a fresh run)."
31
31
  - If the command succeeds (file is tracked by git), continue to Primary check below.
32
32
 
33
33
  **Primary (git-tracked):** Run `git log -1 --format=%cI -- {skillDir}/SKILL.md` to get the last commit date of SKILL.md. Compare against the evidence report's generation date (from its frontmatter or the `## Validation Results` timestamp). If SKILL.md's last commit is newer, results are stale.
@@ -66,11 +66,11 @@ timeout 120s npx skill-check check {skillDir} --format json --no-security-scan
66
66
  If the command exits non-zero AND the exit code is `124` (GNU timeout's signal for the 120s wall-clock expiring), record `skill_check_score: N/A` with reason `timeout-120s`, log a warning, and skip to section 3. Other non-zero exits fall through to the regular JSON-parse path per the note below.
67
67
 
68
68
  **Parse JSON output** to extract:
69
- - `qualityScore` — overall score (0-100)
69
+ - `scores[].score` — overall score (0-100); match the entry by `relativePath` (or `skillId`) to the validated skill dir. Older skill-check builds exposed this as a top-level `qualityScore` — fall back to that if `scores[]` is absent.
70
70
  - `diagnostics[]` — any remaining issues
71
- - `errorCount` and `warningCount`
71
+ - `summary.errorCount` and `summary.warningCount` — issue counts (the counts live under `summary`, not at the top level)
72
72
 
73
- **Note:** `skill-check` may return a non-zero exit code even when `errorCount` is 0. Always rely on the parsed JSON output, not the shell exit code.
73
+ **Note:** `skill-check` may return a non-zero exit code even when `summary.errorCount` is 0. Always rely on the parsed JSON output, not the shell exit code.
74
74
 
75
75
  Store in context: `skill_check_score`, `skill_check_diagnostics`
76
76
 
@@ -45,10 +45,11 @@ Provide the skill path or name. I'll search in `{skillsOutputFolder}`.
45
45
  Resolve the skill path using version-aware resolution (see `{versionPathsKnowledge}`):
46
46
 
47
47
  1. Read `{skillsOutputFolder}/.export-manifest.json` and look up the skill name in `exports` to get `active_version`
48
- 2. If found: resolve to `{skill_package}` = `{skillsOutputFolder}/{skill_name}/{active_version}/{skill_name}/`
49
- 3. If not in manifest: check for `active` symlink at `{skillsOutputFolder}/{skill_name}/active` — resolve to `{skill_group}/active/{skill_name}/`
50
- 4. If neither: fall back to flat path `{skillsOutputFolder}/{skill_name}/`. If SKILL.md exists at the flat path, auto-migrate per `{versionPathsKnowledge}` migration rules
51
- 5. Store the resolved path as `{resolved_skill_package}`
48
+ 2. **Manifest-lag guard.** If the skill is in the manifest, also read the `active` symlink target at `{skillsOutputFolder}/{skill_name}/active`. If that symlink resolves to a *different* version than `active_version`, prefer the **symlink target** as `{resolved_version}` and emit an Info note: "manifest active_version {M} lags the active symlink {N} — testing the symlink target (the just-forged version); run export-skill to reconcile the manifest." This is the canonical SS→TS→EX case: create-stack-skill flipped `active` to the new version, but the manifest only advances when export-skill runs — so a bare manifest-first resolution would test the *previously exported* version and report a PASS for the wrong version (silent false confidence). When the symlink matches `active_version` (or no `active` symlink exists), use `active_version`. See `{versionPathsKnowledge}` "Reading Workflows".
49
+ 3. If found: resolve to `{skill_package}` = `{skillsOutputFolder}/{skill_name}/{resolved_version}/{skill_name}/`
50
+ 4. If not in manifest: check for `active` symlink at `{skillsOutputFolder}/{skill_name}/active` resolve to `{skill_group}/active/{skill_name}/`
51
+ 5. If neither: fall back to flat path `{skillsOutputFolder}/{skill_name}/`. If SKILL.md exists at the flat path, auto-migrate per `{versionPathsKnowledge}` migration rules
52
+ 6. Store the resolved path as `{resolved_skill_package}`
52
53
 
53
54
  Check that the skill package contains required files:
54
55
 
@@ -53,7 +53,7 @@ Read `{outputFile}` and extract the category scores calculated in previous steps
53
53
 
54
54
  #### 2b. Apply State 2 Undercount Deduction (pre-script)
55
55
 
56
- If `analysis_confidence == 'provenance-map'` (State 2) AND step 3 recorded a provenance vs metadata divergence > 5% (see §4b in step 3), apply a 10-point deduction to `exportCoverage` BEFORE building the scoring input:
56
+ If `analysis_confidence == 'provenance-map'` (State 2) AND `metadata.json.skill_type != "stack"` AND step 3 recorded a provenance vs metadata divergence > 5% (see §4b in step 3), apply a 10-point deduction to `exportCoverage` BEFORE building the scoring input:
57
57
 
58
58
  ```
59
59
  exportCoverage_adjusted = max(0, exportCoverage - 10)
@@ -61,6 +61,8 @@ exportCoverage_adjusted = max(0, exportCoverage - 10)
61
61
 
62
62
  Record in the report: `scoring_notes: State 2 undercount risk acknowledged — 10% deduction applied to Export Coverage (raw: {N}%, adjusted: {M}%)`. Use the adjusted value as the `exportCoverage` field in §3a below. The deduction is deterministic and does not change category weights or active-category counting.
63
63
 
64
+ Stack skills are exempt (the `metadata.json.skill_type != "stack"` guard above; `skill_type` is loaded in step 01 and also surfaces as `stackSkill` in §3a): a stack's own barrel is empty by design, so a provenance-vs-metadata divergence on a stack reflects the barrel-vs-constituent surface difference (suppressed by §4b's stack-skill branch in step 3), not extraction undercount — deducting for it would penalize a correctly built stack.
65
+
64
66
  #### 3a. Construct Scoring Input JSON
65
67
 
66
68
  Build a JSON object from the data gathered in steps 1-2:
@@ -72,6 +74,7 @@ Build a JSON object from the data gathered in steps 1-2:
72
74
  "docsOnly": "{true if docs_only_mode detected in step 03, else false}",
73
75
  "state2": "{true if analysis_confidence is provenance-map, else false}",
74
76
  "stackSkill": "{true if metadata.json.skill_type == 'stack', else false}",
77
+ "referenceApp": "{true if metadata.json.scope_type == 'reference-app', else false}",
75
78
  "scores": {
76
79
  "exportCoverage": "{export_coverage_percentage}",
77
80
  "signatureAccuracy": "{signature_accuracy_percentage or null if N/A}",
@@ -83,7 +86,7 @@ Build a JSON object from the data gathered in steps 1-2:
83
86
  }
84
87
  ```
85
88
 
86
- **Important:** Score values must be numbers (not strings). Use `null` (not `"N/A"`) for categories that were not scored. Read `metadata.json.skill_type` from `{resolved_skill_package}/metadata.json`; if the value is `"stack"`, set `stackSkill: true` and pass `null` for `signatureAccuracy` and `typeCoverage` (the categories will be redistributed per `{scoringRulesFile}` Stack Skills rule).
89
+ **Important:** Score values must be numbers (not strings). Use `null` (not `"N/A"`) for categories that were not scored. Read `metadata.json.skill_type` from `{resolved_skill_package}/metadata.json`; if the value is `"stack"`, set `stackSkill: true` and pass `null` for `signatureAccuracy` and `typeCoverage` (the categories will be redistributed per `{scoringRulesFile}` Stack Skills rule). Likewise read `metadata.json.scope_type`; if the value is `"reference-app"`, set `referenceApp: true` and pass `null` for `signatureAccuracy` and `typeCoverage` (redistributed per `{scoringRulesFile}` Reference-App rule — a reference app documents wiring patterns, not library export signatures).
87
90
 
88
91
  #### 3b. Run the Scoring Script
89
92
 
@@ -112,7 +115,7 @@ Use these values for Section 4 (pass/fail/inconclusive) and Section 6 (output fo
112
115
  If the script is unavailable or returns an error, fall back to manual calculation:
113
116
 
114
117
  1. Select the weight table from `{scoringRulesFile}` for the detected mode (naive or contextual)
115
- 2. Determine skip conditions: Quick tier/docsOnly/state2/stackSkill skip Signature Accuracy + Type Coverage; naive mode coherence is already 0; null external validation means skip it
118
+ 2. Determine skip conditions: Quick tier/docsOnly/state2/stackSkill/referenceApp skip Signature Accuracy + Type Coverage; naive mode coherence is already 0; null external validation means skip it
116
119
  3. For each skipped category, set its weight to 0
117
120
  4. Compute sum of active category weights
118
121
  5. For each active category: `new_weight = old_weight / sum_active * 100`
@@ -70,6 +70,18 @@ When `metadata.json.skill_type == "stack"` (set `stackSkill: true` in the scorin
70
70
 
71
71
  Equivalence-class note: stack skills with `docsOnly:false` / `state2:false` map to the same equivalence class as State 2 contextual rows (class B) or State 2 naive rows (class D) — the redistribution math is identical; only the `skipReasons` string changes.
72
72
 
73
+ ### Reference-App Skills (Any Tier)
74
+
75
+ When `metadata.json.scope_type == "reference-app"` (set `referenceApp: true` in the scoring input):
76
+
77
+ - **Signature Accuracy:** N/A — a reference app documents wiring patterns (how surfaces are composed), not a library export surface the skill authors. There are no public-export signatures to grade against; the Pattern Surface replaces the Key API Summary (see `skf-create-skill` Reference-App Assembly Overrides).
78
+ - **Type Coverage:** N/A — same rationale; a reference app has no library type surface to cover. Coverage is measured as pattern-surface coverage (`stats.pattern_surfaces_documented`), not export/type coverage.
79
+ - **Weight redistribution:** Same as Quick tier / docs-only / State 2 / stack — Signature Accuracy (22%) and Type Coverage (14%) weights redistributed proportionally to remaining active categories (Export Coverage, Coherence, External Validation).
80
+ - **Applies regardless of detected tier** (Quick, Forge, Forge+, Deep) and is independent of `docsOnly` and `state2`; skip reasons combine additively. `referenceApp` and `stackSkill` are distinct scope/type signals and should not both be set for the same skill.
81
+ - **Detection:** step 5 reads `metadata.json.scope_type` from the skill package. If the value is `"reference-app"`, set `referenceApp: true` in the scoring input JSON. The skip reason recorded is `"reference-app (no library export signatures)"`.
82
+
83
+ Equivalence-class note: reference-app skills map to the same redistribution equivalence class as stack skills — the math is identical; only the `skipReasons` string changes.
84
+
73
85
  ### State 2 Source Access (Any Tier, Provenance-Map Only)
74
86
 
75
87
  When source is not locally available and analysis resolves to State 2 (provenance-map baseline per source-access-protocol.md):
@@ -16,12 +16,28 @@
16
16
 
17
17
  **Resolution order:**
18
18
 
19
- 1. **Prefer `metadata.json.stats.effective_denominator`** when present. `skf-create-skill` step 5 §4 writes this field for stratified-scope skills. When set, use it directly as the `exports_public_api` count for coverage scoring.
19
+ 1. **Prefer `metadata.json.stats.effective_denominator`** when present. `skf-create-skill` step 5 §4 writes this field for stratified-scope skills. When set, use it directly as the `exports_public_api` count for coverage scoring — **subject to the deflation guard below**.
20
+
21
+ **Denominator deflation guard (when `effective_denominator` is used).** `effective_denominator` is a stored count, and a gap-driven `skf-update-skill` run can lower it — legitimately (a real rescope expressed in the brief) or by gaming (deleting in-scope public exports from `metadata`/provenance and setting `effective_denominator` to the documented total to force 100% coverage). When source is readable (State 1, or State ≥ 2 with remote tools), validate it: re-derive the public surface from `scope.include` filtered by `scope.exclude` — the same union step 2 computes — and compare. If the re-derived source barrel exceeds `effective_denominator` by **more than 25%** AND the brief carries no `scope.tier_a_include` (the only legitimate narrowing mechanism), treat `effective_denominator` as **deflated**: use the re-derived source-barrel count as `total_exports` instead, and emit a **Medium**-severity gap `denominator deflation — effective_denominator below source public surface without tier_a_include` reporting both counts (`effective_denominator: {N}`, `re-derived source barrel: {M}`, `{percent}% below source`). A legitimate scope reduction is expressed in the brief — via `scope.tier_a_include`, or by adding the removed exports to `scope.exclude` so the re-derived barrel shrinks to match (the mechanism `skf-update-skill` gap-driven rescope uses) — **never** by editing `metadata.stats` to equal the documented count. This is the directional counterpart to the inflation check in step 3: inflation fires when the coarse surface is too large, deflation when the stored denominator is suspiciously small. When source is unavailable, skip the guard and annotate the report: `effective_denominator used unverified — no source access to re-derive the barrel`.
22
+
23
+ **Rust barrel re-derivation (when ast-grep `pub`-fn extraction is unreliable).** The Rust-skill condition documents the ast-grep `pub-fn` extractor as unreliable, so re-deriving a crate's barrel surface — for this guard or for the live fallback below — falls back to grep. A naive `pub`-token sweep over-counts badly: it pulls in `pub(crate)`/`pub(super)` internals, impl-block associated consts and methods, test modules, and foreign re-export files (such sweeps routinely land 30–40% above the true barrel). Enumerate deterministically instead:
24
+
25
+ - Trace the `pub use` re-export chain from the crate root (`lib.rs` / `mod.rs`) to find which modules the barrel actually re-exports; count from those, not from every file containing a `pub` token.
26
+ - Count only **unrestricted, module-level (column-0) `pub`** item declarations: `pub struct` / `pub enum` / `pub trait` / `pub type` / `pub const` / `pub fn` (free functions) that start at column 0.
27
+ - Exclude `pub(crate)` / `pub(super)` / `pub(in …)` (crate-internal, not barrel surface); anything indented (impl-block associated consts and methods are not free barrel items); `*tests.rs` and `#[cfg(test)]` modules; and files listed under `scope.exclude` (e.g. foreign re-export files).
28
+
29
+ **TypeScript / JavaScript barrel re-derivation (multi-line re-export blocks).** When re-deriving a TS/JS barrel — for the deflation guard above or the live fallback below — a line-oriented `^export` regex undercounts: a named re-export block spans continuation lines (`export type {\n A,\n B,\n} from "./x"`), and a single-line match captures only the opening line, dropping every member listed on the continuation lines. Enumerate the full block instead:
30
+
31
+ - For each `export { … }` / `export type { … }` (including the `import`-then-`export` re-export form), accumulate named members across continuation lines until the closing `}` — count every identifier in the brace list, not only those on the opening line. A renamed re-export (`export { A as B }`) counts once under its public name (`B`).
32
+ - Resolve `export * from "…"` to the target module's own public names — a star re-export contributes that module's surface, not a single entry.
33
+ - Prefer a `tsc` / `ts-morph` / AST entry-point resolution over the line regex when a TS toolchain is reachable; it resolves multi-line blocks and star re-exports natively. Fall back to the brace-accumulation grep only when no AST tool is available — the same grep-fallback posture the Rust note above takes.
20
34
  2. **Fall back to live re-derivation** when `effective_denominator` is absent (older skills, quick-tier output, or skills compiled before this rule existed). Read the brief's scope globs from `forge-data/{skill_name}/skill-brief.yaml`, resolve them against `source_path`, filter out files matching `scope.exclude`, and compute the source API surface as the **union of named exports across the matched files only**. The skill brief's `scope.notes` field should document the stratification strategy (e.g., "Tier A: fully documented; Tier B: deferred to references; Tier C: excluded") — when present, treat it as confirmation that the curated subset is by design, not a scope gap.
21
35
 
22
36
  **Honor `scope.tier_a_include` when present.** When re-deriving, prefer the brief-level `scope.tier_a_include` narrow include list over the coarse `scope.include`. `tier_a_include` is an optional brief field that lists only the authoring surface the brief actually intends to document (tier A), letting the denominator match the brief's authoring-vs-installing intent even when `scope.include` uses coarse globs that also match internal infrastructure. When `tier_a_include` is present, resolve its globs (still filtered by `scope.exclude`), compute the union across those files, and use that count as the denominator. When absent, fall back to resolving `scope.include`.
23
37
 
24
- 3. **Denominator inflation check (absent `tier_a_include`).** If re-derivation used `scope.include` because no `tier_a_include` was provided, compare the resulting union count against the provenance-map entry count (when provenance-map exists). If the `scope.include` union is more than 25% larger than the provenance-map entry count, the coarse globs are almost certainly sweeping in internal infrastructure that the brief did not intend to document. Emit a **Medium**-severity gap titled `denominator inflation — coarse scope.include union exceeds authored surface` that points the user at the brief for rescoping via `scope.tier_a_include`. Report both counts (`scope.include union: {N}`, `provenance-map entries: {M}`, `{percent}% inflation`) and state that the coverage score it produced is driven by denominator inflation rather than documentation gaps. The check is skipped when provenance-map is unavailable (there is no baseline to compare against).
38
+ **Exclude umbrella barrel files from a `tier_a_include` re-derivation.** A barrel file (`lib.rs`, `mod.rs`, `index.ts`/`index.js`, `__init__.py`) whose public surface is dominated by `pub use` / re-export leaves re-exports the *entire* crate/package surface. Including such a file in the resolved `tier_a_include` set makes the union **grow** rather than shrink defeating the narrowing and producing a denominator larger than `scope.include` would have. Detect an umbrella barrel as a file whose exported names are mostly (>50%) re-exports of symbols defined in other files. When a `tier_a_include` file set resolves to include an umbrella barrel, drop the barrel and count only the concrete-definition files; if dropping it leaves no meaningful surface, prefer `effective_denominator` (priority 1) instead of the inflated `tier_a_include` union.
39
+
40
+ 3. **Denominator inflation check (absent `tier_a_include`).** If re-derivation used `scope.include` because no `tier_a_include` was provided, compare the resulting union count against the provenance-map entry count (when provenance-map exists). If the `scope.include` union is more than 25% larger than the provenance-map entry count, the coarse globs are almost certainly sweeping in internal infrastructure that the brief did not intend to document. Emit a **Medium**-severity gap titled `denominator inflation — coarse scope.include union exceeds authored surface` that points the user at the brief for rescoping via `scope.tier_a_include`. **When the package's barrel is an umbrella re-export file** (public surface mostly `pub use` / re-export leaves — see the umbrella-barrel note in step 2), recommend `stats.effective_denominator` (priority 1) as the **primary** remediation instead of `tier_a_include`: a `tier_a_include` that lists the umbrella barrel would recount *larger* than `scope.include`, not smaller, so it cannot clear the inflation. Report both counts (`scope.include union: {N}`, `provenance-map entries: {M}`, `{percent}% inflation`) and state that the coverage score it produced is driven by denominator inflation rather than documentation gaps. The check is skipped when provenance-map is unavailable (there is no baseline to compare against).
25
41
 
26
42
  Leave `analysis_confidence` unchanged (still `full` or `provenance-map` per the waterfall) — stratified scope does not degrade confidence, only the denominator. Annotate the coverage report with: `Stratified scope — denominator: {effective_denominator | tier_a_include union | scope.include union} ({N} files matched, {M} exports union)`.
27
43
 
@@ -40,7 +56,7 @@
40
56
 
41
57
  **Confidence:** leave `analysis_confidence` unchanged (still `full` or `provenance-map` per the waterfall). Pattern-reference does not degrade confidence — the surface is smaller than a library barrel, not lower quality. Annotate the coverage report with: `Pattern-reference — denominator: {tier_a_include union | canonicalized provenance-map count} ({N} pattern surfaces)`.
42
58
 
43
- **When this clause does NOT apply:** any repo with a non-empty barrel file, any monorepo (use the stratified-scope clause), or any single-package repo whose `scope.type` is explicitly `public-api` / `specific-modules` / `component-library` / `docs-only` (those scope types have their own denominator semantics). Also does NOT apply if `scope.type: "reference-app"` exists in the enum (pending upgrade in `skf-create-skill/references/extract.md`) in that case the brief speaks for itself and this clause's filesystem trigger is moot.
59
+ **When this clause does NOT apply:** any repo with a non-empty barrel file, any monorepo (use the stratified-scope clause), or any single-package repo whose `scope.type` is explicitly `public-api` / `specific-modules` / `component-library` / `docs-only` (those scope types have their own denominator semantics). Also does NOT apply when `scope.type: "reference-app"` that scope type carries its own pattern-surface denominator semantics (the brief speaks for itself), so this clause's filesystem trigger is moot.
44
60
 
45
61
  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).
46
62
 
@@ -153,6 +153,7 @@ def compute_score(inp):
153
153
  docs_only = inp.get("docsOnly") is True
154
154
  state2 = inp.get("state2") is True
155
155
  stack_skill = inp.get("stackSkill") is True
156
+ reference_app = inp.get("referenceApp") is True
156
157
  threshold = inp.get("threshold") if inp.get("threshold") is not None else DEFAULT_THRESHOLD
157
158
  scores = inp["scores"]
158
159
 
@@ -161,7 +162,7 @@ def compute_score(inp):
161
162
 
162
163
  # 3. Determine skip set
163
164
  skip_reasons = {}
164
- skip_sig_type = tier == "Quick" or docs_only or state2 or stack_skill
165
+ skip_sig_type = tier == "Quick" or docs_only or state2 or stack_skill or reference_app
165
166
 
166
167
  if skip_sig_type:
167
168
  reasons = []
@@ -173,6 +174,8 @@ def compute_score(inp):
173
174
  reasons.append("State 2 (provenance-map)")
174
175
  if stack_skill:
175
176
  reasons.append("stack skill (external type surface)")
177
+ if reference_app:
178
+ reasons.append("reference-app (no library export signatures)")
176
179
  reason = " + ".join(reasons)
177
180
  skip_reasons["signatureAccuracy"] = reason
178
181
  skip_reasons["typeCoverage"] = reason