bmad-module-skill-forge 2.0.0 → 2.0.1

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 (235) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.nvmrc +1 -1
  3. package/docs/_data/pinned.yaml +1 -1
  4. package/package.json +2 -2
  5. package/src/knowledge/skill-lifecycle.md +1 -1
  6. package/src/module-help.csv +2 -2
  7. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  8. package/src/shared/scripts/skf-count-tokens.py +222 -0
  9. package/src/shared/scripts/skf-detect-docs.py +232 -1
  10. package/src/shared/scripts/skf-detect-language.py +77 -0
  11. package/src/shared/scripts/skf-detect-tools.py +105 -2
  12. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  13. package/src/shared/scripts/skf-find-cycles.py +246 -0
  14. package/src/shared/scripts/skf-hash-content.py +487 -2
  15. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  16. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  17. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  18. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  19. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  20. package/src/shared/scripts/skf-shard-body.py +461 -0
  21. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  22. package/src/shared/scripts/skf-structural-diff.py +307 -67
  23. package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
  24. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  25. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  26. package/src/shared/scripts/skf-validate-output.py +443 -10
  27. package/src/shared/scripts/skf-validate-pins.py +1 -1
  28. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  29. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  30. package/src/skf-analyze-source/SKILL.md +17 -32
  31. package/src/skf-analyze-source/customize.toml +2 -0
  32. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  33. package/src/skf-analyze-source/references/continue.md +8 -13
  34. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  35. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  36. package/src/skf-analyze-source/references/health-check.md +1 -2
  37. package/src/skf-analyze-source/references/identify-units.md +25 -36
  38. package/src/skf-analyze-source/references/init.md +12 -25
  39. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  40. package/src/skf-analyze-source/references/recommend.md +4 -14
  41. package/src/skf-analyze-source/references/scan-project.md +3 -11
  42. package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
  43. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  44. package/src/skf-audit-skill/SKILL.md +9 -6
  45. package/src/skf-audit-skill/customize.toml +2 -0
  46. package/src/skf-audit-skill/references/init.md +58 -32
  47. package/src/skf-audit-skill/references/re-index.md +15 -34
  48. package/src/skf-audit-skill/references/report.md +11 -58
  49. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  50. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  51. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  52. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  53. package/src/skf-brief-skill/SKILL.md +7 -36
  54. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  55. package/src/skf-brief-skill/customize.toml +14 -0
  56. package/src/skf-brief-skill/references/analyze-target.md +6 -14
  57. package/src/skf-brief-skill/references/confirm-brief.md +2 -14
  58. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  59. package/src/skf-brief-skill/references/gather-intent.md +19 -22
  60. package/src/skf-brief-skill/references/headless-args.md +1 -1
  61. package/src/skf-brief-skill/references/health-check.md +1 -22
  62. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  63. package/src/skf-brief-skill/references/scope-definition.md +4 -14
  64. package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
  65. package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
  66. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  67. package/src/skf-brief-skill/references/write-brief.md +17 -10
  68. package/src/skf-campaign/SKILL.md +15 -84
  69. package/src/skf-campaign/customize.toml +2 -0
  70. package/src/skf-campaign/manifest.yaml +4 -0
  71. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  72. package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
  73. package/src/skf-campaign/references/step-01-setup.md +4 -5
  74. package/src/skf-campaign/references/step-02-strategy.md +8 -3
  75. package/src/skf-campaign/references/step-03-pins.md +1 -1
  76. package/src/skf-campaign/references/step-04-provenance.md +1 -1
  77. package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
  78. package/src/skf-campaign/references/step-06-batch.md +7 -6
  79. package/src/skf-campaign/references/step-11-maintenance.md +2 -2
  80. package/src/skf-campaign/references/step-resume.md +9 -8
  81. package/src/skf-campaign/scripts/campaign-deps.py +10 -1
  82. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  83. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  84. package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
  85. package/src/skf-create-skill/SKILL.md +14 -3
  86. package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
  87. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  88. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  89. package/src/skf-create-skill/customize.toml +13 -0
  90. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  91. package/src/skf-create-skill/references/compile.md +34 -47
  92. package/src/skf-create-skill/references/component-extraction.md +8 -35
  93. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  94. package/src/skf-create-skill/references/enrich.md +4 -17
  95. package/src/skf-create-skill/references/extract.md +14 -29
  96. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  97. package/src/skf-create-skill/references/extraction-patterns.md +7 -7
  98. package/src/skf-create-skill/references/generate-artifacts.md +9 -23
  99. package/src/skf-create-skill/references/health-check.md +1 -1
  100. package/src/skf-create-skill/references/load-brief.md +16 -18
  101. package/src/skf-create-skill/references/report.md +12 -15
  102. package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
  103. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  104. package/src/skf-create-skill/references/step-doc-rot.md +30 -12
  105. package/src/skf-create-skill/references/step-doc-sources.md +2 -2
  106. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  107. package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
  108. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  109. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  110. package/src/skf-create-skill/references/validate.md +59 -28
  111. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  112. package/src/skf-create-stack-skill/SKILL.md +15 -9
  113. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  114. package/src/skf-create-stack-skill/customize.toml +10 -0
  115. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  116. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  117. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  118. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  119. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  120. package/src/skf-create-stack-skill/references/init.md +9 -81
  121. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  122. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  123. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  124. package/src/skf-create-stack-skill/references/report.md +27 -62
  125. package/src/skf-create-stack-skill/references/validate.md +35 -31
  126. package/src/skf-drop-skill/SKILL.md +20 -23
  127. package/src/skf-drop-skill/customize.toml +10 -0
  128. package/src/skf-drop-skill/references/execute.md +51 -31
  129. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  130. package/src/skf-drop-skill/references/health-check.md +1 -13
  131. package/src/skf-drop-skill/references/report.md +42 -10
  132. package/src/skf-drop-skill/references/select.md +88 -48
  133. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  134. package/src/skf-export-skill/SKILL.md +17 -14
  135. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  136. package/src/skf-export-skill/customize.toml +10 -6
  137. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  138. package/src/skf-export-skill/references/load-skill.md +44 -42
  139. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  140. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  141. package/src/skf-export-skill/references/package.md +27 -33
  142. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  143. package/src/skf-export-skill/references/result-envelope.md +28 -0
  144. package/src/skf-export-skill/references/summary.md +11 -5
  145. package/src/skf-export-skill/references/token-report.md +42 -24
  146. package/src/skf-export-skill/references/update-context.md +32 -33
  147. package/src/skf-forger/SKILL.md +27 -39
  148. package/src/skf-forger/references/pipeline-mode.md +34 -0
  149. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  150. package/src/skf-quick-skill/SKILL.md +10 -60
  151. package/src/skf-quick-skill/customize.toml +9 -0
  152. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  153. package/src/skf-quick-skill/references/compile.md +15 -52
  154. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  155. package/src/skf-quick-skill/references/finalize.md +12 -6
  156. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  157. package/src/skf-quick-skill/references/health-check.md +3 -4
  158. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  159. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  160. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  161. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  162. package/src/skf-refine-architecture/SKILL.md +14 -7
  163. package/src/skf-refine-architecture/customize.toml +9 -0
  164. package/src/skf-refine-architecture/references/compile.md +9 -7
  165. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  166. package/src/skf-refine-architecture/references/health-check.md +1 -1
  167. package/src/skf-refine-architecture/references/improvements.md +9 -25
  168. package/src/skf-refine-architecture/references/init.md +9 -16
  169. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  170. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  171. package/src/skf-refine-architecture/references/report.md +13 -10
  172. package/src/skf-rename-skill/SKILL.md +11 -21
  173. package/src/skf-rename-skill/customize.toml +10 -0
  174. package/src/skf-rename-skill/references/execute.md +107 -78
  175. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  176. package/src/skf-rename-skill/references/report.md +16 -8
  177. package/src/skf-rename-skill/references/select.md +69 -42
  178. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  179. package/src/skf-setup/SKILL.md +20 -12
  180. package/src/skf-setup/customize.toml +10 -0
  181. package/src/skf-setup/references/auto-index.md +9 -10
  182. package/src/skf-setup/references/ccc-index.md +14 -15
  183. package/src/skf-setup/references/detect-and-tier.md +6 -8
  184. package/src/skf-setup/references/report.md +22 -31
  185. package/src/skf-setup/references/tier-rules.md +2 -32
  186. package/src/skf-setup/references/write-config.md +9 -10
  187. package/src/skf-test-skill/SKILL.md +10 -8
  188. package/src/skf-test-skill/customize.toml +2 -0
  189. package/src/skf-test-skill/references/coherence-check.md +19 -31
  190. package/src/skf-test-skill/references/coverage-check.md +109 -75
  191. package/src/skf-test-skill/references/detect-mode.md +10 -41
  192. package/src/skf-test-skill/references/external-validators.md +11 -19
  193. package/src/skf-test-skill/references/init.md +38 -20
  194. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  195. package/src/skf-test-skill/references/report.md +60 -40
  196. package/src/skf-test-skill/references/score.md +32 -64
  197. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  198. package/src/skf-test-skill/references/source-access-protocol.md +6 -6
  199. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  200. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  201. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  202. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  203. package/src/skf-test-skill/scripts/compute-score.py +131 -1
  204. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  205. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  206. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  207. package/src/skf-update-skill/SKILL.md +17 -5
  208. package/src/skf-update-skill/customize.toml +14 -0
  209. package/src/skf-update-skill/references/detect-changes.md +19 -37
  210. package/src/skf-update-skill/references/health-check.md +3 -9
  211. package/src/skf-update-skill/references/init.md +39 -32
  212. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  213. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  214. package/src/skf-update-skill/references/merge.md +13 -57
  215. package/src/skf-update-skill/references/re-extract.md +12 -24
  216. package/src/skf-update-skill/references/report.md +14 -7
  217. package/src/skf-update-skill/references/validate.md +32 -30
  218. package/src/skf-update-skill/references/write.md +86 -51
  219. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  220. package/src/skf-verify-stack/SKILL.md +11 -21
  221. package/src/skf-verify-stack/customize.toml +12 -3
  222. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  223. package/src/skf-verify-stack/references/coverage.md +21 -10
  224. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  225. package/src/skf-verify-stack/references/health-check.md +1 -1
  226. package/src/skf-verify-stack/references/init.md +20 -25
  227. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  228. package/src/skf-verify-stack/references/integrations.md +44 -49
  229. package/src/skf-verify-stack/references/report.md +28 -34
  230. package/src/skf-verify-stack/references/requirements.md +3 -3
  231. package/src/skf-verify-stack/references/synthesize.md +36 -39
  232. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  233. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  234. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  235. package/src/skf-rename-skill/references/rebuild-context.md +0 -110
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env python3
1
2
  # /// script
2
3
  # requires-python = ">=3.10"
3
4
  # dependencies = []
@@ -25,9 +26,19 @@ Input schema (one object):
25
26
  "externalValidation": <0-100>
26
27
  },
27
28
  "threshold": <0-100, optional, default 80>,
28
- "evidenceCount": <int, optional, used for INCONCLUSIVE floor>
29
+ "evidenceCount": <int, optional, used for INCONCLUSIVE floor>,
30
+ "analysisConfidence": "<optional string; 'degraded' fires the tooling cap>",
31
+ "toolingStatus": "<optional string; a '*-missing' marker fires the tooling cap>"
29
32
  }
30
33
 
34
+ Verdict override (post-score caps + threshold fallback — see compute_score):
35
+ When a cap or the threshold fallback engages, the output additionally carries
36
+ `effectiveResult` (the final verdict after caps/fallback), `capReason`
37
+ (string or null), `thresholdFallback` (bool), and `originalThreshold` (the
38
+ pre-fallback threshold, or null). When neither engages, those keys are omitted
39
+ and `result` is the final verdict. `result` itself is never mutated — it is
40
+ always the pre-cap/pre-fallback score-vs-threshold (or evidence-floor) verdict.
41
+
31
42
  Exit codes:
32
43
  0 — input was parsed; either a score was computed (verdict may be
33
44
  PASS/FAIL/INCONCLUSIVE) or a schema/validation error object
@@ -71,6 +82,59 @@ CATEGORIES = [
71
82
  DEFAULT_THRESHOLD = 80
72
83
 
73
84
 
85
+ # --- Redistribution equivalence classes (moved here from scoring-rules.md) ---
86
+ #
87
+ # Determinism note for maintainers — NOT reader-model context. The redistributed
88
+ # `weights` / `activeCategories` / `skippedCategories` output depends ONLY on
89
+ # three things: the base table (contextual vs naive), whether Signature Accuracy
90
+ # + Type Coverage are skipped (Quick tier OR docsOnly OR state2 OR stackSkill OR
91
+ # referenceApp), and whether External Validation is skipped (externalValidation
92
+ # is null). Every (mode × tier × docsOnly × state2 × stackSkill × referenceApp)
93
+ # cell with an identical skip set + base table therefore reduces to the same
94
+ # equivalence class below and emits identical final weights for identical input
95
+ # scores — only the `skipReasons` string differs. The prompt (score.md) never
96
+ # names a class or fixture; it sets the flags and reads the script's output back.
97
+ # The rightmost column pins each class to a representative fixture in
98
+ # test/fixtures/compute-score-contract.json (see test-compute-score-contract.py);
99
+ # "(equiv.)" cells reduce algebraically to a listed representative.
100
+ #
101
+ # | # | mode | tier | docsOnly | state2 | base | sig/type skip | ext skip | class | representative fixture |
102
+ # |----|------------|--------|----------|--------|------------|------------------------|----------|-------|----------------------------------|
103
+ # | 1 | contextual | Deep | F | F | contextual | — | if null | A | suite_a_all_active |
104
+ # | 2 | contextual | Forge+ | F | F | contextual | — | if null | A | suite_k_forge_plus |
105
+ # | 3 | contextual | Forge | F | F | contextual | — | if null | A | suite_p_contextual_forge |
106
+ # | 4 | contextual | Quick | F | F | contextual | Quick tier | if null | B | suite_c_quick_tier |
107
+ # | 5 | contextual | Deep | T | F | contextual | docs-only | if null | B | suite_r_contextual_deep_docsonly |
108
+ # | 6 | contextual | Forge+ | T | F | contextual | docs-only | if null | B | (equiv.) |
109
+ # | 7 | contextual | Forge | T | F | contextual | docs-only | if null | B | (equiv.) |
110
+ # | 8 | contextual | Quick | T | F | contextual | Quick tier + docs-only | if null | B | suite_f_docs_only |
111
+ # | 9 | contextual | Deep | F | T | contextual | State 2 | if null | B | suite_g_state2 |
112
+ # | 10 | contextual | Forge+ | F | T | contextual | State 2 | if null | B | (equiv.) |
113
+ # | 11 | contextual | Forge | F | T | contextual | State 2 | if null | B | (equiv.) |
114
+ # | 12 | contextual | Quick | F | T | contextual | Quick + State 2 | if null | B | (equiv.) |
115
+ # | 13 | contextual | * | T | T | contextual | docs-only + State 2 | if null | B | (equiv.) |
116
+ # | 14 | naive | Deep | F | F | naive | — | if null | C | suite_q_naive_deep |
117
+ # | 15 | naive | Forge+ | F | F | naive | — | if null | C | (equiv.) |
118
+ # | 16 | naive | Forge | F | F | naive | — | if null | C | suite_b_naive |
119
+ # | 17 | naive | Quick | F | F | naive | Quick tier | if null | D | suite_e_triple_skip |
120
+ # | 18 | naive | Deep | T | F | naive | docs-only | if null | D | suite_s_naive_deep_docsonly |
121
+ # | 19 | naive | Forge+ | T | F | naive | docs-only | if null | D | (equiv.) |
122
+ # | 20 | naive | Forge | T | F | naive | docs-only | if null | D | (equiv.) |
123
+ # | 21 | naive | Quick | T | F | naive | Quick + docs-only | if null | D | suite_o_input_echo |
124
+ # | 22 | naive | Deep | F | T | naive | State 2 | if null | D | suite_t_naive_state2 |
125
+ # | 23 | naive | Forge+ | F | T | naive | State 2 | if null | D | (equiv.) |
126
+ # | 24 | naive | Forge | F | T | naive | State 2 | if null | D | (equiv.) |
127
+ # | 25 | naive | Quick | F | T | naive | Quick + State 2 | if null | D | (equiv.) |
128
+ # | 26 | naive | * | T | T | naive | docs-only + State 2 | if null | D | (equiv.) |
129
+ #
130
+ # Stack skills (stackSkill) and reference-app skills (referenceApp) share the
131
+ # skip set of the docsOnly/state2 rows: contextual → class B, naive → class D;
132
+ # only skipReasons differs (fixtures suite_u_stack_skill_deep,
133
+ # suite_v_reference_app_deep). Quick-tier rows (4, 8, 12, 17, 21, 25) then feed
134
+ # the minimum-evidence floor in the result block below (§7 of compute_score),
135
+ # which can force INCONCLUSIVE after this pre-floor redistribution.
136
+
137
+
74
138
  # --- Helpers ---
75
139
 
76
140
 
@@ -130,6 +194,11 @@ def validate_input(inp):
130
194
  if not isinstance(threshold, (int, float)) or threshold < 0 or threshold > 100:
131
195
  return "threshold must be a number between 0 and 100"
132
196
 
197
+ for str_field in ("analysisConfidence", "toolingStatus"):
198
+ val = inp.get(str_field)
199
+ if val is not None and not isinstance(val, str):
200
+ return f"{str_field} must be a string or null, got: {type(val).__name__}"
201
+
133
202
  for cat in CATEGORIES:
134
203
  score = inp["scores"].get(cat)
135
204
  if score is not None:
@@ -277,6 +346,58 @@ def compute_score(inp):
277
346
  else:
278
347
  result = "PASS" if total_score >= threshold else "FAIL"
279
348
 
349
+ # 7b. Post-score caps + threshold fallback — deterministic verdict override.
350
+ # Lifted from score.md §3d/§4b so the cap<->fallback interaction is centralized
351
+ # and unit-tested here rather than re-derived in the prompt. The minimum-
352
+ # evidence floor (INCONCLUSIVE) is a gate that NO cap or fallback may override,
353
+ # so all of this is skipped when result == "INCONCLUSIVE".
354
+ analysis_confidence = inp.get("analysisConfidence")
355
+ tooling_status = inp.get("toolingStatus")
356
+
357
+ effective_result = result
358
+ cap_reason = None
359
+ threshold_fallback = False
360
+ original_threshold = None
361
+ cap_fired = False
362
+
363
+ if result != "INCONCLUSIVE":
364
+ cap_reasons = []
365
+ # Cap 1 — tooling degraded: analysis confidence is "degraded", or a
366
+ # missing-helper marker (e.g. python3-missing, frontmatter-validator-missing).
367
+ tooling_degraded = analysis_confidence == "degraded" or (
368
+ isinstance(tooling_status, str) and "missing" in tooling_status
369
+ )
370
+ if tooling_degraded:
371
+ cap_fired = True
372
+ cap_reasons.append(
373
+ "tooling degraded — capped below threshold until helper restored"
374
+ )
375
+ # Cap 2 — docs-only mode with no external validators available.
376
+ if docs_only and scores.get("externalValidation") is None:
377
+ cap_fired = True
378
+ cap_reasons.append(
379
+ "docs-only without external validators — capped below threshold"
380
+ )
381
+ if cap_fired:
382
+ cap_reason = "; ".join(cap_reasons)
383
+ # A cap only flips a PASS into FAIL; a pre-existing FAIL stays FAIL.
384
+ if result == "PASS":
385
+ effective_result = "FAIL"
386
+
387
+ # Threshold fallback — convert a FAIL into PASS at the 80 floor when the
388
+ # RAW totalScore clears 80 and the effective threshold was above 80. The
389
+ # fallback reads the raw totalScore (matching score.md §4b's documented
390
+ # `totalScore >= 80`), not the capped value; because it also requires
391
+ # threshold > 80 (so threshold - 1 >= 80), min(totalScore, threshold - 1)
392
+ # is itself >= 80 whenever totalScore >= 80, so raw-vs-capped is the same
393
+ # decision — raw is used for spec fidelity and testability.
394
+ if effective_result == "FAIL" and total_score >= 80 and threshold > 80:
395
+ threshold_fallback = True
396
+ original_threshold = threshold
397
+ effective_result = "PASS"
398
+
399
+ overrides_engaged = cap_fired or threshold_fallback
400
+
280
401
  # Build scores echo with null preservation
281
402
  scores_echo = {}
282
403
  for cat in CATEGORIES:
@@ -303,6 +424,15 @@ def compute_score(inp):
303
424
  "weightSum": weight_sum,
304
425
  }
305
426
 
427
+ # Verdict-override fields — emitted as an atomic group only when a post-score
428
+ # cap or the threshold fallback engaged (mirrors the conditional `warnings` /
429
+ # `inconclusiveReasons` fields). When absent, `result` is the final verdict.
430
+ if overrides_engaged:
431
+ output["effectiveResult"] = effective_result
432
+ output["capReason"] = cap_reason
433
+ output["thresholdFallback"] = threshold_fallback
434
+ output["originalThreshold"] = original_threshold
435
+
306
436
  if warnings:
307
437
  output["warnings"] = warnings
308
438
 
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # ///
5
+ """Deterministic Documented-vs-Source coverage reconciliation.
6
+
7
+ Set-reconciliation helper for the SKF test-skill workflow (coverage-check.md
8
+ §2c). Distinct from compute-score.py (which owns weight tables, redistribution,
9
+ and the INCONCLUSIVE floor): this script performs the coverage *numerator*
10
+ arithmetic that §2c calls the "Deterministic Intersection" — turning two
11
+ already-extracted name lists (the §1 documented inventory and the §2 source
12
+ barrel) into a reproducible {documented, missing, stale, exportCoverage}
13
+ result, so the numerator no longer swings between runs on split-body skills.
14
+
15
+ It covers the three branches §2c enumerates:
16
+
17
+ * "barrel" (enumerated path) — intersect documented_set against the source
18
+ barrel_set: Documented = |documented_set ∩ barrel_set|,
19
+ Missing = barrel_set − documented_set, Stale = documented_set − barrel_set,
20
+ Export Coverage = |Documented| / |barrel_set| * 100.
21
+
22
+ * "scalar" (§4 priority-1 effective_denominator, no enumerated name set) —
23
+ grep each documented name across SKILL.md ∪ references/*.md;
24
+ Documented = count present, denominator = effective_denominator,
25
+ Missing = denominator − Documented, Stale not enumerable (empty),
26
+ Export Coverage = Documented / denominator * 100.
27
+
28
+ * "stack" (skill_type == "stack", empty source barrel) — grep each
29
+ composition-surface name (provenance-map cited contracts, ::-excluded, or
30
+ libraries + integration_pairs — resolved upstream) across
31
+ SKILL.md ∪ references/*.md; denominator = stack_denominator,
32
+ Missing = denominator − Documented, Stale not enumerable,
33
+ Export Coverage = Documented / stack_denominator * 100.
34
+
35
+ CLI usage:
36
+ uv run reconcile-coverage.py '<JSON>' # JSON literal positional
37
+ uv run reconcile-coverage.py --json-input '<JSON>' # explicit flag form
38
+ cat input.json | uv run reconcile-coverage.py --stdin # piped input
39
+
40
+ Input schema (one object):
41
+ {
42
+ "denominatorSource": "barrel" | "scalar" | "stack", # required
43
+ "exports": [ {"name": "...", "kind": "..."}, ... ], # §1 inventory
44
+ "barrelSet": ["a", "b", ...], # barrel: resolved name set
45
+ "perFileResults": [ {"exports_found": ["a", ...]} ], # barrel: union'd if no barrelSet
46
+ "denominatorValue": <int>, # scalar/stack: resolved denominator
47
+ "compositionNames": ["lib::x", ...], # stack: names to grep
48
+ "skillPackagePath": "/path/to/skill" # scalar/stack: SKILL.md ∪ references
49
+ }
50
+
51
+ Output (stdout, one object):
52
+ {
53
+ "branch": "enumerated" | "scalar" | "stack",
54
+ "denominatorSource": "barrel" | "scalar" | "stack",
55
+ "denominator": <int>,
56
+ "documented": <int>,
57
+ "missing": [names], # enumerated: source names not documented; scalar/stack: []
58
+ "missingCount": <int>, # always present
59
+ "stale": [names], # enumerated: documented names not in source; scalar/stack: []
60
+ "staleCount": <int>, # always present
61
+ "staleApplicable": <bool>, # false for scalar/stack (no barrel to enumerate)
62
+ "exportCoverage": <float>
63
+ }
64
+
65
+ Exit codes:
66
+ 0 — reconciliation emitted successfully
67
+ 1 — no input / input could not be parsed as JSON
68
+ 2 — input parsed but schema/semantics invalid (error object emitted as JSON)
69
+ """
70
+
71
+ from __future__ import annotations
72
+
73
+ import argparse
74
+ import json
75
+ import math
76
+ import sys
77
+ from pathlib import Path
78
+
79
+ VALID_SOURCES = ("barrel", "scalar", "stack")
80
+
81
+
82
+ def round2(value):
83
+ """Round to 2 decimals with JS-compatible half-up rounding (matches compute-score.py)."""
84
+ return math.floor(value * 100 + 0.5) / 100
85
+
86
+
87
+ def make_error(message):
88
+ return {"error": message, "code": "INVALID_INPUT"}
89
+
90
+
91
+ # --- Documented-set derivation ---------------------------------------------
92
+
93
+
94
+ def build_documented_set(exports):
95
+ """De-duplicated set of `name` from the §1 inventory, excluding kind:"method".
96
+
97
+ Methods are members of an already-counted class/type, not top-level barrel
98
+ exports (coverage-check.md §2c step 1).
99
+ """
100
+ names = set()
101
+ for entry in exports or []:
102
+ if not isinstance(entry, dict):
103
+ continue
104
+ if entry.get("kind") == "method":
105
+ continue
106
+ name = entry.get("name")
107
+ if isinstance(name, str) and name:
108
+ names.add(name)
109
+ return names
110
+
111
+
112
+ def build_barrel_set(inp):
113
+ """barrel_set = explicit `barrelSet` if given, else union of exports_found[]."""
114
+ if inp.get("barrelSet") is not None:
115
+ return {n for n in inp["barrelSet"] if isinstance(n, str) and n}
116
+ barrel = set()
117
+ for result in inp.get("perFileResults") or []:
118
+ if not isinstance(result, dict):
119
+ continue
120
+ for name in result.get("exports_found") or []:
121
+ if isinstance(name, str) and name:
122
+ barrel.add(name)
123
+ return barrel
124
+
125
+
126
+ # --- Grep (documented-name presence) ---------------------------------------
127
+
128
+
129
+ def load_doc_text(skill_package_path):
130
+ """Concatenate SKILL.md ∪ references/*.md text for name-presence grepping.
131
+
132
+ Deterministic: references are read in sorted order. Reads as UTF-8 so
133
+ non-ASCII exports do not mojibake on Windows (cp1252 default).
134
+ """
135
+ root = Path(skill_package_path)
136
+ parts = []
137
+ skill_md = root / "SKILL.md"
138
+ if skill_md.is_file():
139
+ parts.append(skill_md.read_text(encoding="utf-8"))
140
+ refs_dir = root / "references"
141
+ if refs_dir.is_dir():
142
+ for ref in sorted(refs_dir.glob("*.md")):
143
+ parts.append(ref.read_text(encoding="utf-8"))
144
+ return "\n".join(parts)
145
+
146
+
147
+ def names_present(names, doc_text):
148
+ """Return the sorted, de-duplicated set of `names` that appear in doc_text.
149
+
150
+ Substring containment, case-sensitive — mirrors `grep "{name}"`.
151
+ """
152
+ present = {n for n in names if isinstance(n, str) and n and n in doc_text}
153
+ return sorted(present)
154
+
155
+
156
+ # --- Core reconciliation ----------------------------------------------------
157
+
158
+
159
+ def _validate(inp):
160
+ if inp is None or not isinstance(inp, dict):
161
+ return "Input must be a JSON object"
162
+ source = inp.get("denominatorSource")
163
+ if source not in VALID_SOURCES:
164
+ return (
165
+ "Missing or invalid required field: denominatorSource "
166
+ f"(must be one of: {', '.join(VALID_SOURCES)})"
167
+ )
168
+ if source == "barrel":
169
+ if inp.get("barrelSet") is None and inp.get("perFileResults") is None:
170
+ return "barrel branch requires either `barrelSet` or `perFileResults`"
171
+ else: # scalar / stack
172
+ dv = inp.get("denominatorValue")
173
+ if not isinstance(dv, int) or isinstance(dv, bool) or dv < 0:
174
+ return f"{source} branch requires integer `denominatorValue` >= 0"
175
+ if not inp.get("skillPackagePath"):
176
+ return f"{source} branch requires `skillPackagePath` to grep SKILL.md ∪ references"
177
+ if source == "stack" and inp.get("compositionNames") is None:
178
+ return "stack branch requires `compositionNames` (composition-surface names to grep)"
179
+ return None
180
+
181
+
182
+ def reconcile(inp, doc_text=None):
183
+ """Pure reconciliation. `doc_text` may be injected (tests); else loaded from disk."""
184
+ err = _validate(inp)
185
+ if err:
186
+ return make_error(err)
187
+
188
+ source = inp["denominatorSource"]
189
+
190
+ if source == "barrel":
191
+ documented_set = build_documented_set(inp.get("exports"))
192
+ barrel_set = build_barrel_set(inp)
193
+ if not barrel_set:
194
+ return make_error(
195
+ "barrel branch: barrel_set is empty (denominator 0) — "
196
+ "Export Coverage is undefined; upstream §2b zero-exports guard should HALT"
197
+ )
198
+ documented_names = documented_set & barrel_set
199
+ missing = sorted(barrel_set - documented_set)
200
+ stale = sorted(documented_set - barrel_set)
201
+ denominator = len(barrel_set)
202
+ documented = len(documented_names)
203
+ return {
204
+ "branch": "enumerated",
205
+ "denominatorSource": source,
206
+ "denominator": denominator,
207
+ "documented": documented,
208
+ "missing": missing,
209
+ "missingCount": len(missing),
210
+ "stale": stale,
211
+ "staleCount": len(stale),
212
+ "staleApplicable": True,
213
+ "exportCoverage": round2(documented / denominator * 100),
214
+ }
215
+
216
+ # scalar / stack — grep-based numerator against SKILL.md ∪ references
217
+ denominator = inp["denominatorValue"]
218
+ if doc_text is None:
219
+ doc_text = load_doc_text(inp["skillPackagePath"])
220
+
221
+ if source == "scalar":
222
+ candidates = sorted(build_documented_set(inp.get("exports")))
223
+ else: # stack
224
+ candidates = [n for n in inp["compositionNames"] if isinstance(n, str) and n]
225
+
226
+ present = names_present(candidates, doc_text)
227
+ documented = len(present)
228
+
229
+ if denominator == 0:
230
+ return make_error(
231
+ f"{source} branch: denominatorValue is 0 — Export Coverage is undefined; "
232
+ "upstream §2b guard should HALT before reconciliation"
233
+ )
234
+
235
+ missing_count = denominator - documented
236
+ return {
237
+ "branch": source,
238
+ "denominatorSource": source,
239
+ "denominator": denominator,
240
+ "documented": documented,
241
+ "missing": [],
242
+ "missingCount": missing_count,
243
+ "stale": [],
244
+ "staleCount": 0,
245
+ "staleApplicable": False,
246
+ "exportCoverage": round2(documented / denominator * 100),
247
+ }
248
+
249
+
250
+ # --- CLI --------------------------------------------------------------------
251
+
252
+
253
+ def _build_parser():
254
+ parser = argparse.ArgumentParser(
255
+ prog="reconcile-coverage",
256
+ description=(
257
+ "Deterministic Documented-vs-Source coverage reconciliation "
258
+ "(coverage-check.md §2c). Consumes the §1 documented inventory and "
259
+ "the §2 source barrel (or a resolved scalar/stack denominator + the "
260
+ "skill package path) and emits {documented, missing, stale, "
261
+ "exportCoverage} so the coverage numerator is reproducible."
262
+ ),
263
+ formatter_class=argparse.RawDescriptionHelpFormatter,
264
+ epilog=(
265
+ "Example (barrel branch):\n"
266
+ " uv run reconcile-coverage.py "
267
+ "'{\"denominatorSource\":\"barrel\","
268
+ "\"exports\":[{\"name\":\"a\",\"kind\":\"function\"}],"
269
+ "\"barrelSet\":[\"a\",\"b\"]}'"
270
+ ),
271
+ )
272
+ src = parser.add_mutually_exclusive_group()
273
+ src.add_argument(
274
+ "json_input",
275
+ nargs="?",
276
+ help="JSON object as a positional argument (single-quote it on the shell).",
277
+ )
278
+ src.add_argument(
279
+ "--json-input",
280
+ dest="json_input_flag",
281
+ help="JSON object passed via flag (overrides positional).",
282
+ )
283
+ src.add_argument(
284
+ "--stdin",
285
+ action="store_true",
286
+ help="Read the JSON object from stdin.",
287
+ )
288
+ return parser
289
+
290
+
291
+ def _resolve_input(args):
292
+ if args.stdin:
293
+ return sys.stdin.read()
294
+ if args.json_input_flag is not None:
295
+ return args.json_input_flag
296
+ if args.json_input is not None:
297
+ return args.json_input
298
+ return ""
299
+
300
+
301
+ def main(argv=None):
302
+ parser = _build_parser()
303
+ args = parser.parse_args(argv)
304
+ raw = _resolve_input(args)
305
+ if not raw.strip():
306
+ parser.print_usage(file=sys.stderr)
307
+ print(
308
+ "error: no input provided (positional arg, --json-input, or --stdin)",
309
+ file=sys.stderr,
310
+ )
311
+ return 1
312
+
313
+ try:
314
+ data = json.loads(raw)
315
+ except json.JSONDecodeError as exc:
316
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
317
+ return 1
318
+
319
+ result = reconcile(data)
320
+ print(json.dumps(result, indent=2))
321
+ if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
322
+ return 2
323
+ return 0
324
+
325
+
326
+ if __name__ == "__main__":
327
+ raise SystemExit(main())