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
@@ -11,6 +11,9 @@ feasibilitySchemaProbeOrder:
11
11
  atomicWriteProbeOrder:
12
12
  - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
13
13
  - '{project-root}/src/shared/scripts/skf-atomic-write.py'
14
+ validateFeasibilityReportProbeOrder:
15
+ - '{project-root}/_bmad/skf/shared/scripts/skf-validate-feasibility-report.py'
16
+ - '{project-root}/src/shared/scripts/skf-validate-feasibility-report.py'
14
17
  nextStepFile: 'health-check.md'
15
18
  ---
16
19
 
@@ -25,7 +28,7 @@ Present the complete feasibility report to the user. Display the overall verdict
25
28
  ## Rules
26
29
 
27
30
  - Focus only on presenting the completed report — no new analysis or changes to verdicts
28
- - Chains to the local health-check step via `{nextStepFile}` after completion — the user-facing report is NOT the terminal step
31
+ - Chains to the local health-check step via `{nextStepFile}` after completion — the user-facing report is not the terminal step
29
32
 
30
33
  ## MANDATORY SEQUENCE
31
34
 
@@ -35,11 +38,24 @@ Read the entire `{outputFile}` to have all data available for presentation.
35
38
 
36
39
  **Resolve `{feasibilitySchemaRef}`** from `{feasibilitySchemaProbeOrder}`; first existing path wins (installed SKF module path first, dev-checkout `src/` fallback).
37
40
 
38
- Verify all expected sections are present in order per `{feasibilitySchemaRef}`: `## Executive Summary`, `## Coverage Analysis`, `## Integration Verdicts`, `## Recommendations`, `## Evidence Sources`. If any section is missing or out of order, HALT (exit code 5, `halt_reason: "schema-violation"`) and report the schema violation — do not display partial results. In headless, emit the error envelope per SKILL.md "Result Contract (Headless)" with `report_path: "{outputFile}"`, `overall_verdict: null`.
41
+ **Validate report structure and schema version (deterministic gate).** Resolve `{validateFeasibilityReportHelper}` from `{validateFeasibilityReportProbeOrder}`; first existing path wins (installed SKF module path first, dev-checkout `src/` fallback). Then run:
39
42
 
40
- **Extract metrics from `{outputFile}` frontmatter** (per shared schema in `{feasibilitySchemaRef}`): `skillsAnalyzed`, `coveragePercentage`, `pairsVerified` (as `verified_count`), `pairsPlausible` (as `plausible_count`), `pairsRisky` (as `risky_count`), `pairsBlocked` (as `blocked_count`), `requirementsFulfilled` (as `fulfilled_count`), `requirementsPartial` (as `partial_count`), `requirementsNotAddressed` (as `not_addressed_count`), `requirementsPass`, `overallVerdict`, and `recommendationCount`. Use these mapped display names in the summary table and next steps below.
43
+ ```bash
44
+ python3 {validateFeasibilityReportHelper} {outputFile}
45
+ ```
46
+
47
+ The script (see `--help`) deterministically confirms the five required body sections — `## Executive Summary`, `## Coverage Analysis`, `## Integration Verdicts`, `## Recommendations`, `## Evidence Sources` — are all present and in canonical order per `{feasibilitySchemaRef}`, **and** that frontmatter `schemaVersion == "1.0"`. It emits a JSON verdict on stdout (`headingsOk`, `missingHeadings`, `orderViolations`, `schemaVersionOk`, `schemaVersionFound`, `violation`) and exits `0` when valid, `1` on a schema violation, `2` on an IO/parse error.
48
+
49
+ **Graceful degradation:** if no `{validateFeasibilityReportProbeOrder}` candidate exists (e.g. partial installation, or `python3` unavailable), perform the equivalent structural check inline — confirm the five headings above are all present and in that exact order, and that frontmatter `schemaVersion` is `"1.0"` — and apply the same halt semantics below. The report is already on disk from steps 1-5, so a missing validator degrades to the inline check rather than blocking presentation.
50
+
51
+ On any non-zero exit (or an inline check that fails), HALT (exit code 5, `halt_reason: "schema-violation"`) — do not display partial results. Report the specific violation from the JSON:
41
52
 
42
- **Schema guard:** Verify `schemaVersion == "1.0"` in the frontmatter. If mismatched, HALT (exit code 5, `halt_reason: "schema-violation"`) with "Report frontmatter schemaVersion `{value}` does not match producer schema `1.0` — report was corrupted between steps. Re-run [VS]." (Producer never proceeds past a schema mismatch.) In headless, emit the error envelope.
53
+ - a missing or out-of-order section (`missingHeadings` / `orderViolations`); or
54
+ - a schemaVersion mismatch — "Report frontmatter schemaVersion `{schemaVersionFound}` does not match producer schema `1.0` — report was corrupted between steps. Re-run [VS]." (Producer never proceeds past a schema mismatch.)
55
+
56
+ In headless, emit the error envelope per SKILL.md "Result Contract (Headless)" with `report_path: "{outputFile}"`, `overall_verdict: null`.
57
+
58
+ With the deterministic gate passed (sections present + in order, `schemaVersion == "1.0"`), **extract metrics from `{outputFile}` frontmatter** (per shared schema in `{feasibilitySchemaRef}`): `skillsAnalyzed`, `coveragePercentage`, `pairsVerified` (as `verified_count`), `pairsPlausible` (as `plausible_count`), `pairsRisky` (as `risky_count`), `pairsBlocked` (as `blocked_count`), `requirementsFulfilled` (as `fulfilled_count`), `requirementsPartial` (as `partial_count`), `requirementsNotAddressed` (as `not_addressed_count`), `requirementsPass`, `overallVerdict`, and `recommendationCount`. Use these mapped display names in the summary table and next steps below.
43
59
 
44
60
  ### 2. Present Summary
45
61
 
@@ -76,37 +92,17 @@ Walk through the highlights — coverage gaps, risky/blocked integrations, and p
76
92
 
77
93
  ### 4. Present Next Steps
78
94
 
79
- Based on the overall verdict, present the appropriate recommendation:
80
-
81
- **IF `overallVerdict == "FEASIBLE"`:**
82
- "**Your stack is verified.** All technologies are covered, integrations are compatible, and requirements are all fulfilled (or requirements pass was skipped).
83
-
84
- **Recommended next steps:**
85
- 1. **[RA] Refine Architecture** — Produce an implementation-ready architecture document enriched with skill-backed API details
86
- 2. **[SS] Create Stack Skill** — compose your individual skills into a unified stack skill, providing the refined architecture doc when prompted
87
- 3. **[TS] Test Skill** → **[EX] Export Skill** — Verify completeness and package for distribution"
88
-
89
- **IF `overallVerdict == "CONDITIONALLY_FEASIBLE"`:**
90
- "**Your stack is conditionally feasible.** There are {recommendationCount} items to address before proceeding.
91
-
92
- **Required actions:**
93
- {List the specific recommendations from Step 05 synthesis}
94
-
95
- **After addressing these items:** Re-run **[VS] Verify Stack** to confirm resolution, then proceed to **[RA]**."
96
-
97
- **IF `overallVerdict == "NOT_FEASIBLE"`:**
98
- "**Critical blockers must be resolved.** The stack cannot support the architecture as described.
99
-
100
- **Critical actions:**
101
- {List the blocked integration recommendations and missing skill actions from Step 05}
95
+ Step 05 already wrote a **Suggested next workflow** block (keyed on the case-sensitive `overallVerdict` token) at the end of `## Recommendations`. Surface that block from the §1 load rather than re-deriving it, prefixed with the one-line verdict-specific framing:
102
96
 
103
- **After resolving blockers:** Re-run **[VS] Verify Stack**. Repeat until verdict improves to FEASIBLE or CONDITIONALLY FEASIBLE."
97
+ - **`FEASIBLE`:** "**Your stack is verified.** All technologies are covered, integrations are compatible, and requirements are all fulfilled (or requirements pass was skipped)."
98
+ - **`CONDITIONALLY_FEASIBLE`:** "**Your stack is conditionally feasible.** There are {recommendationCount} items to address before proceeding." — then list the specific recommendations from the report's `## Recommendations` section.
99
+ - **`NOT_FEASIBLE`:** "**Critical blockers must be resolved.** The stack cannot support the architecture as described." — then list the blocked-integration and missing-skill recommendations from the report's `## Recommendations` section.
104
100
 
105
101
  ### 4b. Result Contract
106
102
 
107
- **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. HALT if no candidate exists.
103
+ **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. If no candidate exists: HALT (exit code 3, `halt_reason: "resolution-failure"`); in headless, emit the error envelope.
108
104
 
109
- Write the result contract per `shared/references/output-contract-schema.md` (this path resolves relative to the SKF module root — `{project-root}/_bmad/skf/` when installed, `{project-root}/src/` during development — NOT relative to this step file): the per-run record at `{forge_data_folder}/verify-stack-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{forge_data_folder}/verify-stack-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include the feasibility report path (both `{outputFile}` and `{outputFileLatest}`) in `outputs`; include `overallVerdict` (`FEASIBLE` / `CONDITIONALLY_FEASIBLE` / `NOT_FEASIBLE`), `coveragePercentage`, and `recommendationCount` in `summary` — use the case-sensitive schema tokens.
105
+ Write the result contract per `shared/references/output-contract-schema.md` (this path resolves relative to the SKF module root — `{project-root}/_bmad/skf/` when installed, `{project-root}/src/` during development — not relative to this step file): the per-run record at `{forge_data_folder}/verify-stack-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{forge_data_folder}/verify-stack-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include the feasibility report path (both `{outputFile}` and `{outputFileLatest}`) in `outputs`; include `overallVerdict` (`FEASIBLE` / `CONDITIONALLY_FEASIBLE` / `NOT_FEASIBLE`), `coveragePercentage`, and `recommendationCount` in `summary` — use the case-sensitive schema tokens.
110
106
 
111
107
  Write both JSON files through `python3 {atomicWriteHelper} write --target ...` to avoid partial-write corruption. On any non-zero exit: HALT (exit code 4, `halt_reason: "write-failed"`) and emit the error envelope.
112
108
 
@@ -118,7 +114,7 @@ SKF_VERIFY_STACK_RESULT_JSON: {"status":"success","report_path":"{outputFile}","
118
114
 
119
115
  `{overallVerdict}` uses the schema tokens (`FEASIBLE` / `CONDITIONALLY_FEASIBLE` / `NOT_FEASIBLE`).
120
116
 
121
- **Result-contract ordering:** The result contract is written exactly once on the first entry to step 6 (the `[X] Exit verification` path). Re-walks of the report via the `[R] Review full report` menu option do NOT regenerate it — the contract captures the run, not the presentation loop. If the user selects `[R]` repeatedly before exiting, the single on-disk contract written on first entry remains authoritative.
117
+ **Result-contract ordering:** The result contract is written exactly once on the first entry to step 6 (the `[X] Exit verification` path). Re-walks of the report via the `[R] Review full report` menu option do not regenerate it — the contract captures the run, not the presentation loop. If the user selects `[R]` repeatedly before exiting, the single on-disk contract written on first entry remains authoritative.
122
118
 
123
119
  ### 5. Present Menu
124
120
 
@@ -133,13 +129,11 @@ Re-run **[VS] Verify Stack** anytime after making changes to your skills or arch
133
129
 
134
130
  **Verification workflow complete.**"
135
131
 
136
- Then load, read the full file, and execute `{nextStepFile}` — the health-check step is the true terminal step of this workflow.
132
+ If `{workflow.on_complete}` is non-empty, execute it now (e.g. route the verdict onward or trigger a downstream step); in headless, log the action. Then load, read the full file, and execute `{nextStepFile}` — the health-check step is the true terminal step of this workflow.
137
133
 
138
134
  #### EXECUTION RULES:
139
135
 
140
- - ALWAYS halt and wait for user input after presenting the menu
141
136
  - **GATE [default: X]** — If `{headless_mode}`: auto-proceed with [X] Exit verification, log: "headless: auto-exit past report menu"
142
137
  - R may be selected multiple times — always walk through the full report
143
- - X triggers the health check, which is the true workflow exit
144
138
 
145
139
 
@@ -28,7 +28,7 @@ If a PRD or vision document was provided in Step 01, verify that the combined ca
28
28
 
29
29
  ### 1. Check PRD Availability
30
30
 
31
- **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. HALT if no candidate exists.
31
+ **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. If no candidate exists: HALT (exit code 3, `halt_reason: "resolution-failure"`); in headless, emit the error envelope.
32
32
 
33
33
  **Read `prdAvailable` from `{outputFile}` frontmatter (set in Step 01). If `prdAvailable` is false (no PRD/vision document was provided):**
34
34
 
@@ -42,7 +42,7 @@ To include this pass, re-run **[VS]** with a PRD or vision document path.
42
42
 
43
43
  Update `{outputFile}` frontmatter: append `'requirements'` to `stepsCompleted`; set `requirementsPass: "skipped"`. Pipe the updated content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`.
44
44
 
45
- Load, read the full file and then execute `{nextStepFile}`. **STOP HERE do not execute sections 2-6.**
45
+ Load, read the full file and then execute `{nextStepFile}`. The no-PRD path ends here sections 2-6 are the PRD-present branch and do not run.
46
46
 
47
47
  **If PRD/vision document was provided:** Continue to section 2.
48
48
 
@@ -78,7 +78,7 @@ For each requirement, evaluate whether the combined capabilities of the generate
78
78
  - **Partially Fulfilled** — skills provide related capability but gaps remain (specify what is covered and what is not)
79
79
  - **Not Addressed** — no skill in the stack provides capability relevant to this requirement
80
80
 
81
- **Each verdict MUST include:**
81
+ **Each verdict includes:**
82
82
  - Which skills contribute (if any)
83
83
  - Specific exports or capabilities from those skills that are relevant
84
84
  - For Partially Fulfilled: what gap remains
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  nextStepFile: 'report.md'
3
+ verdictRollupScript: 'scripts/skf-verdict-rollup.py'
4
+ reportDeltaScript: 'scripts/skf-report-delta.py'
3
5
  feasibilitySchemaProbeOrder:
4
6
  - '{project-root}/_bmad/skf/shared/references/feasibility-report-schema.md'
5
7
  - '{project-root}/src/shared/references/feasibility-report-schema.md'
@@ -27,33 +29,29 @@ Calculate the overall feasibility verdict based on all three analysis passes, ge
27
29
 
28
30
  ### 1. Calculate Overall Verdict
29
31
 
30
- **Zero-coverage short-circuit (evaluate before anything else):** Read `coveragePercentage` from `{outputFile}` frontmatter. If `coveragePercentage == 0`, force `overallVerdict: NOT_FEASIBLE` with rationale "no coverage analysis vacuous: zero generated skills match the architecture's referenced technologies, so integration and requirements verdicts cannot produce meaningful evidence." Skip the remainder of the verdict ladder; proceed directly to section 2 to generate recommendations for the Missing and/or Replaced technologies surfaced by Step 02.
32
+ **The verdict token is deterministic do not walk the ladder in prose.** All three passes have already persisted their counts (`coveragePercentage`, `pairsBlocked`/`pairsRisky`/`pairsPlausible`/`pairsVerified`, `requirementsPass` + `requirementsNotAddressed`/`requirementsPartial` in `{outputFile}` frontmatter; the Missing technology count in the Coverage Analysis table). Rolling those already-decided counts up into one token has a single correct answer per input, so delegate it. Count Missing rows directly from the Coverage table half-up rounding can leave `coveragePercentage == 100` with one technology still Missing assemble the counts, and run:
31
33
 
32
- Apply the following decision logic using findings from all completed passes:
34
+ ```bash
35
+ echo '<counts JSON>' | uv run {verdictRollupScript} --stdin
36
+ ```
33
37
 
34
- **Evaluate in order the first matching condition wins. Do not continue once a verdict is determined.**
38
+ Input keys: `coveragePercentage`, `missingCount`, `pairsBlocked`, `pairsRisky`, `pairsPlausible`, `pairsVerified`; plus, only when the requirements pass ran (`requirementsPass == "completed"`), `requirementsEvaluated: true` with `requirementsNotAddressed`/`requirementsPartial`; plus `continuedPastZeroState: true` if the user pressed `[C] Continue anyway` past a step-2 zero-state gate (all-Replaced or 0%-coverage). The script (run `uv run {verdictRollupScript} --help` for the contract) returns `overallVerdict` (one of `FEASIBLE`/`CONDITIONALLY_FEASIBLE`/`NOT_FEASIBLE`), `matchedConditions` (the condition codes that fired), and `zeroPairsGuardFired`. If `uv` is unavailable (e.g. claude.ai web), apply the ladder below inline.
35
39
 
36
- **NOT_FEASIBLE (evaluate first):**
37
- - Any integration is **Blocked** overall verdict is `NOT_FEASIBLE`
38
- - Rationale: a blocked integration represents a fundamental architectural incompatibility. Also note in the rationale any co-occurring Missing skills or Risky integrations so the user understands the full set of problems
40
+ **The ladder it applies (documented so the rationale can cite it — the script is the executor; evaluate top-to-bottom, first match wins):**
41
+ - `coveragePercentage == 0``NOT_FEASIBLE` (short-circuit: no live coverage, analysis vacuous).
42
+ - Any Blocked integration `NOT_FEASIBLE` (fundamental architectural incompatibility).
43
+ - Any Missing technology, any Risky integration, or — when requirements ran — any Not Addressed / Partially Fulfilled requirement → `CONDITIONALLY_FEASIBLE`.
44
+ - Otherwise → `FEASIBLE`, but any pair capped at `Plausible` (including Check-4-missing caps) downgrades to `CONDITIONALLY_FEASIBLE`.
45
+ - Post-verdict guard: when all four integration counts are 0 and the user continued past a step-2 zero-state gate, `zeroPairsGuardFired` is true — a `FEASIBLE` verdict is overridden to `CONDITIONALLY_FEASIBLE`.
39
46
 
40
- **CONDITIONALLY_FEASIBLE (evaluate second):**
41
- If ANY of the following apply, the verdict is `CONDITIONALLY_FEASIBLE`. Include ALL matching conditions in the rationale:
42
- - Any technology is **Missing** from coverage (no skill exists). Technologies marked **Replaced** in Step 02 are intentionally being removed and do NOT count as Missing — they never trigger CONDITIONALLY_FEASIBLE or a [CS]/[QS] recommendation.
43
- - Any integration is **Risky** (but none Blocked)
44
- - Requirements have any **Not Addressed** items
45
- - Requirements have any **Partially Fulfilled** items
46
- - Rationale: the stack can work but has gaps, risks, or unverified assumptions that must be addressed
47
+ Technologies marked **Replaced** in Step 02 are intentionally being removed and are already excluded from `missingCount` and the coverage denominator — they never trigger `CONDITIONALLY_FEASIBLE` or a [CS]/[QS] recommendation.
47
48
 
48
- **FEASIBLE (evaluate last):**
49
- - Coverage is 100% (no Missing skills) AND all integrations are `Verified` AND requirements are all Fulfilled (or requirements pass was skipped) AND no Blocked or Risky integrations AND zero pairs were capped at `Plausible` due to missing Check 4 evidence overall verdict is `FEASIBLE`
50
- - If any pair sits at `Plausible` (including Check-4-missing caps), downgrade to `CONDITIONALLY_FEASIBLE`
51
- - Rationale: {IF requirements pass completed:} the stack can support the architecture as described all requirements fully fulfilled, every integration pair has a literal cross-reference. {IF requirements pass was skipped:} the stack can support the architecture as described — requirements were not evaluated (no PRD provided)
52
-
53
- **Post-verdict: Zero integration pairs guard (apply after ANY verdict):**
54
- If zero integration pairs were extracted (all four integration counts are 0), fire the guard whenever the user continued past the step 2 `[C] Continue anyway` prompt (i.e., any `[C]` continuation from 0% coverage or any other zero-state) — regardless of how many technologies the architecture document references:
55
- - If the verdict was `FEASIBLE`, override to `CONDITIONALLY_FEASIBLE`
56
- - Regardless of verdict, append this note to the rationale: "No integration claims were found in the architecture document prose. Manual review recommended to confirm that technology relationships are not documented exclusively in diagrams or implied without explicit co-mention."
49
+ **Write the rationale** from `matchedConditions`, naming the specific findings behind each code:
50
+ - `zero-coverage` "no coverage analysis vacuous: zero generated skills match the architecture's referenced technologies, so integration and requirements verdicts cannot produce meaningful evidence." Then proceed directly to section 2 to generate recommendations for the Missing and/or Replaced technologies surfaced by Step 02.
51
+ - `blocked-integration` a blocked integration is a fundamental architectural incompatibility; name each Blocked pair and note any co-occurring `missing-coverage`/`risky-integration` codes so the user sees the full set of problems.
52
+ - `missing-coverage` / `risky-integration` / `requirements-not-addressed` / `requirements-partial` / `plausible-cap` the stack can work but has gaps, risks, or unverified assumptions that must be addressed; name the specific items behind each code.
53
+ - No codes (`FEASIBLE`) → {IF requirements pass completed:} the stack can support the architecture as described — all requirements fully fulfilled, every integration pair has a literal cross-reference. {IF requirements pass was skipped:} the stack can support the architecture as described — requirements were not evaluated (no PRD provided).
54
+ - `zero-integration-pairs` (present whenever the guard fired, regardless of verdict) → append: "No integration claims were found in the architecture document prose. Manual review recommended to confirm that technology relationships are not documented exclusively in diagrams or implied without explicit co-mention."
57
55
 
58
56
  Store the verdict for use in the report.
59
57
 
@@ -66,7 +64,7 @@ For each non-verified finding across all passes, generate an actionable next ste
66
64
 
67
65
  **Replaced / being-removed technology (from Step 02):**
68
66
  - "`{library_name}` is marked for removal/replacement in the architecture document — no skill is needed. Remove it from the architecture document (or, if it is in fact staying, correct the document to drop the removal marker), then re-run **[VS]**."
69
- - Do NOT emit a [CS]/[QS] recommendation for a Replaced technology — forging a skill for a technology that is being deleted is exactly the misfire this category prevents.
67
+ - Do not emit a [CS]/[QS] recommendation for a Replaced technology — forging a skill for a technology that is being deleted is exactly the misfire this category prevents.
70
68
 
71
69
  **Risky integration (from Step 03):**
72
70
  - If protocol mismatch → "Consider adding a bridge layer between `{lib_a}` and `{lib_b}` (e.g., HTTP adapter, message queue). Document the bridge in the architecture."
@@ -89,18 +87,20 @@ For each non-verified finding across all passes, generate an actionable next ste
89
87
 
90
88
  ### 3. Check for Previous Report
91
89
 
92
- Read `previousReport` from `{outputFile}` frontmatter (set in Step 01). Since the current workflow run overwrites the report starting in Step 01, the delta feature requires the user to have saved a copy before re-running [VS].
90
+ Read `previousReport` from `{outputFile}` frontmatter (set in Step 01). Each run writes a new timestamped `feasibility-report-{projectSlug}-{timestamp}.md`, so prior reports persist on disk automatically — Step 01 auto-discovers the most recent one for delta comparison when no path is supplied. `previousReport` holds the resolved path, or is empty when no prior report exists or the user skipped the comparison.
93
91
 
94
- **Note:** The delta feature is only available when the user has manually backed up a prior report and provided the path. To enable delta comparisons, instruct the user to copy their feasibility report (e.g., `feasibility-report-{projectSlug}-v1.md`) before re-running [VS], then provide the backup path when prompted in Step 01.
92
+ **Note:** A manual backup is only needed to compare against a *specific older* snapshot rather than the most recent prior run; provide that backup path when prompted in Step 01.
95
93
 
96
94
  **If a previous report is found:**
97
- - Load its verdict, coverage percentage, integration verdicts, and per-skill `confidence_tier` values (if captured in the previous report's inventory block)
98
- - Generate a delta comparison:
99
- - **Improved items:** findings that were Risky/Blocked/Missing and are now Verified/Covered
100
- - **Regressed items:** findings that were Verified/Covered and are now Risky/Blocked/Missing
101
- - **Tier downgrades (regression):** for each skill present in both runs, compare current `confidence_tier` against previous. A downgrade (e.g., Tier 1 → Tier 2, or T1 → T1-low) is a regression — flag explicitly in the delta section with rationale "skill `{skill_name}` regressed from `{prev_tier}` to `{curr_tier}` — re-extract with [CS] at the prior tier level".
102
- - **New items:** findings not present in the previous report
103
- - **Unchanged items:** count of findings with the same verdict
95
+ - Extract from both reports (current run + the previous report's tables/inventory block): the coverage findings (`{technology, verdict}`), the integration findings (`{libA, libB, verdict}`), and each skill's `confidence_tier`. Reading the tables is judgment; classifying the difference is not — so hand the two extracted finding sets to the delta helper rather than diffing in prose (matching pair keys and applying the verdict ranking by hand drifts between runs).
96
+ - Serialize as `{"previous": {"coverage": […], "integration": […]}, "current": {…}, "previousTiers": {skill: tier}, "currentTiers": {…}}` and run:
97
+
98
+ ```bash
99
+ echo '<delta JSON>' | uv run {reportDeltaScript} --stdin
100
+ ```
101
+
102
+ The script (run `uv run {reportDeltaScript} --help` for the contract and rankings) returns `improved`/`regressed`/`unchanged`/`new`/`dropped`/`replaced` label lists with counts, plus `tierDowngrades` (each `{skill, from, to}`) — a tier drop (Tier 1 → Tier 2, or T1 → T1-low) counts as a regression. If `uv` is unavailable, apply the ranking from `--help` inline (coverage Missing<Covered; integration Blocked<Risky<Plausible<Verified; tier T2<T1-low<T1; Replaced findings bucketed, not scored).
103
+ - Render the delta section from those results. For each tier downgrade, flag: "skill `{skill}` regressed from `{from}` to `{to}` — re-extract with [CS] at the prior tier level".
104
104
 
105
105
  **If no previous report found:**
106
106
  - Note: "First verification run — no delta available."
@@ -129,7 +129,7 @@ Assemble the following for the report:
129
129
 
130
130
  ### 5. Append to Report
131
131
 
132
- **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. HALT if no candidate exists.
132
+ **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. If no candidate exists: HALT (exit code 3, `halt_reason: "resolution-failure"`); in headless, emit the error envelope.
133
133
 
134
134
  **Resolve `{feasibilitySchemaRef}`** from `{feasibilitySchemaProbeOrder}`; first existing path wins (installed SKF module path first, dev-checkout `src/` fallback).
135
135
 
@@ -141,14 +141,11 @@ Write the **Recommendations** and **Evidence Sources** sections to `{outputFile}
141
141
  - Populate `## Evidence Sources` with per-skill citations (SKILL.md path, `metadata_schema_version`, `confidence_tier`, stack manifest if any) and architecture/PRD doc paths
142
142
  - Update frontmatter (shared-schema keys):
143
143
  - Append `'synthesize'` to `stepsCompleted`
144
- - Set `overallVerdict` to one of `FEASIBLE`, `CONDITIONALLY_FEASIBLE`, `NOT_FEASIBLE` (case-sensitive, underscores NOT spaces)
144
+ - Set `overallVerdict` to one of `FEASIBLE`, `CONDITIONALLY_FEASIBLE`, `NOT_FEASIBLE` (case-sensitive, underscores not spaces)
145
145
  - Set `recommendationCount` to the total number of recommendations
146
- - If delta was computed (section 3), set `deltaImproved`, `deltaRegressed`, `deltaNew`, `deltaUnchanged`
146
+ - If delta was computed (section 3), set `deltaImproved`, `deltaRegressed`, `deltaNew`, `deltaUnchanged` from the delta helper's `improvedCount` / `regressedCount` / `newCount` / `unchangedCount`
147
147
  - Verify that `pairsVerified`, `pairsPlausible`, `pairsRisky`, `pairsBlocked` match the counts from Step 03 (these were set in Step 03). If a discrepancy is found, overwrite the frontmatter counts with the values from Step 03 — the report file is the system of record
148
- - **Overall verdict enforcement (schema producer obligation):**
149
- - If any pair has Check 4 missing/weak AND was capped at `Plausible`, that alone does NOT force `NOT_FEASIBLE`, but `FEASIBLE` requires zero such pairs
150
- - `FEASIBLE` requires 100% coverage AND zero Blocked pairs AND zero Check-4-missing pairs — otherwise downgrade to `CONDITIONALLY_FEASIBLE`
151
- - `coveragePercentage == 0` forces `NOT_FEASIBLE` (per section 1 short-circuit)
148
+ - **Overall verdict enforcement (schema producer obligation):** write the `overallVerdict` the §1 rollup script returned verbatim — do not re-derive the ladder here. §1 (via `{verdictRollupScript}`) is its single source of truth (the 100%-coverage + zero-Blocked + zero-Check-4-missing bar for `FEASIBLE`, and the `coveragePercentage == 0` → `NOT_FEASIBLE` short-circuit included).
152
149
  - Pipe the updated full content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`
153
150
 
154
151
  ### 6. Auto-Proceed to Next Step
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """Deterministic technology-coverage tally for skf-verify-stack (coverage.md §6).
7
+
8
+ The coverage verdicts themselves (Covered / Missing / Replaced) are LLM judgment
9
+ — matching an architecture technology to a generated skill involves aliases and
10
+ prose. This script takes that already-decided coverage matrix and does only the
11
+ part with one correct answer: counting each verdict class and turning the counts
12
+ into `coveragePercentage`.
13
+
14
+ Two gotchas make the in-prose version drift between runs, so they live here
15
+ instead:
16
+
17
+ * The denominator excludes Replaced. `live_count = Covered + Missing`;
18
+ technologies flagged Replaced (intentionally being removed) are not a gap and
19
+ must not dilute the percentage.
20
+ * Rounding is pinned to half-up to the nearest integer, so the same matrix
21
+ always yields the same `coveragePercentage` (the shared schema declares
22
+ `coveragePercentage: <0..100 integer>` but not the rounding rule).
23
+
24
+ CLI usage:
25
+ uv run skf-coverage-tally.py '<JSON>' # JSON literal positional
26
+ uv run skf-coverage-tally.py --json-input '<JSON>' # explicit flag form
27
+ cat input.json | uv run skf-coverage-tally.py --stdin # piped input
28
+
29
+ Input schema (one object):
30
+ {
31
+ "rows": [
32
+ {"technology": "react", "verdict": "Covered"},
33
+ {"technology": "postgres","verdict": "Missing"},
34
+ {"technology": "old-orm", "verdict": "Replaced"}
35
+ ]
36
+ }
37
+
38
+ Output (stdout, one object):
39
+ {
40
+ "covered_count": <int>,
41
+ "missing_count": <int>,
42
+ "replaced_count": <int>,
43
+ "live_count": <int>, # Covered + Missing (denominator)
44
+ "total_referenced": <int>, # all rows after dedup
45
+ "coverage_percentage": <int> # round-half-up(covered/live*100); 0 when live==0
46
+ }
47
+
48
+ Exit codes:
49
+ 0 — tally emitted successfully
50
+ 1 — no input / input could not be parsed as JSON
51
+ 2 — input parsed but schema/semantics invalid (error object emitted as JSON)
52
+ """
53
+
54
+ from __future__ import annotations
55
+
56
+ import argparse
57
+ import json
58
+ import math
59
+ import sys
60
+
61
+ VALID_VERDICTS = ("Covered", "Missing", "Replaced")
62
+
63
+
64
+ def make_error(message):
65
+ return {"error": message, "code": "INVALID_INPUT"}
66
+
67
+
68
+ def _validate(inp):
69
+ if inp is None or not isinstance(inp, dict):
70
+ return "Input must be a JSON object"
71
+ rows = inp.get("rows")
72
+ if not isinstance(rows, list):
73
+ return "Missing or invalid required field: rows (must be a list)"
74
+ seen = set()
75
+ for i, row in enumerate(rows):
76
+ if not isinstance(row, dict):
77
+ return f"rows[{i}] must be an object"
78
+ tech = row.get("technology")
79
+ if not isinstance(tech, str) or not tech.strip():
80
+ return f"rows[{i}] requires a non-empty string `technology`"
81
+ verdict = row.get("verdict")
82
+ if verdict not in VALID_VERDICTS:
83
+ return (
84
+ f"rows[{i}] verdict {verdict!r} is not one of: "
85
+ f"{', '.join(VALID_VERDICTS)}"
86
+ )
87
+ norm = tech.strip().lower()
88
+ if norm in seen:
89
+ return f"duplicate technology {tech!r} — the coverage list must be deduplicated first"
90
+ seen.add(norm)
91
+ return None
92
+
93
+
94
+ def tally(inp):
95
+ """Pure tally. Counts each verdict class and computes coverage_percentage."""
96
+ err = _validate(inp)
97
+ if err:
98
+ return make_error(err)
99
+
100
+ covered = missing = replaced = 0
101
+ for row in inp["rows"]:
102
+ verdict = row["verdict"]
103
+ if verdict == "Covered":
104
+ covered += 1
105
+ elif verdict == "Missing":
106
+ missing += 1
107
+ else: # Replaced
108
+ replaced += 1
109
+
110
+ live = covered + missing
111
+ percentage = math.floor(covered / live * 100 + 0.5) if live > 0 else 0
112
+ return {
113
+ "covered_count": covered,
114
+ "missing_count": missing,
115
+ "replaced_count": replaced,
116
+ "live_count": live,
117
+ "total_referenced": covered + missing + replaced,
118
+ "coverage_percentage": percentage,
119
+ }
120
+
121
+
122
+ # --- CLI --------------------------------------------------------------------
123
+
124
+
125
+ def _build_parser():
126
+ parser = argparse.ArgumentParser(
127
+ prog="skf-coverage-tally",
128
+ description=(
129
+ "Deterministic technology-coverage tally (coverage.md §6). Counts "
130
+ "Covered/Missing/Replaced verdicts and computes coveragePercentage "
131
+ "with Replaced excluded from the denominator and half-up rounding."
132
+ ),
133
+ formatter_class=argparse.RawDescriptionHelpFormatter,
134
+ epilog=(
135
+ "Example:\n"
136
+ " uv run skf-coverage-tally.py "
137
+ "'{\"rows\":[{\"technology\":\"react\",\"verdict\":\"Covered\"},"
138
+ "{\"technology\":\"postgres\",\"verdict\":\"Missing\"}]}'"
139
+ ),
140
+ )
141
+ src = parser.add_mutually_exclusive_group()
142
+ src.add_argument(
143
+ "json_input",
144
+ nargs="?",
145
+ help="JSON object as a positional argument (single-quote it on the shell).",
146
+ )
147
+ src.add_argument(
148
+ "--json-input",
149
+ dest="json_input_flag",
150
+ help="JSON object passed via flag (overrides positional).",
151
+ )
152
+ src.add_argument(
153
+ "--stdin",
154
+ action="store_true",
155
+ help="Read the JSON object from stdin.",
156
+ )
157
+ return parser
158
+
159
+
160
+ def _resolve_input(args):
161
+ if args.stdin:
162
+ return sys.stdin.read()
163
+ if args.json_input_flag is not None:
164
+ return args.json_input_flag
165
+ if args.json_input is not None:
166
+ return args.json_input
167
+ return ""
168
+
169
+
170
+ def main(argv=None):
171
+ parser = _build_parser()
172
+ args = parser.parse_args(argv)
173
+ raw = _resolve_input(args)
174
+ if not raw.strip():
175
+ parser.print_usage(file=sys.stderr)
176
+ print(
177
+ "error: no input provided (positional arg, --json-input, or --stdin)",
178
+ file=sys.stderr,
179
+ )
180
+ return 1
181
+
182
+ try:
183
+ data = json.loads(raw)
184
+ except json.JSONDecodeError as exc:
185
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
186
+ return 1
187
+
188
+ result = tally(data)
189
+ print(json.dumps(result, indent=2))
190
+ if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
191
+ return 2
192
+ return 0
193
+
194
+
195
+ if __name__ == "__main__":
196
+ raise SystemExit(main())