bmad-module-skill-forge 1.9.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 (281) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.nvmrc +1 -1
  3. package/README.md +10 -5
  4. package/docs/_data/pinned.yaml +2 -2
  5. package/docs/agents.md +11 -2
  6. package/docs/architecture.md +5 -4
  7. package/docs/bmad-synergy.md +30 -7
  8. package/docs/campaign.md +172 -0
  9. package/docs/examples.md +7 -3
  10. package/docs/forge-auto.md +90 -0
  11. package/docs/getting-started.md +9 -0
  12. package/docs/how-it-works.md +6 -4
  13. package/docs/index.md +4 -3
  14. package/docs/skill-model.md +1 -1
  15. package/docs/troubleshooting.md +17 -1
  16. package/docs/verifying-a-skill.md +2 -2
  17. package/docs/why-skf.md +1 -1
  18. package/docs/workflows.md +64 -24
  19. package/package.json +2 -2
  20. package/src/knowledge/skill-lifecycle.md +1 -1
  21. package/src/module-help.csv +4 -3
  22. package/src/shared/data/language-corpora.json +32 -0
  23. package/src/shared/references/pipeline-contracts.md +6 -3
  24. package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
  25. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  26. package/src/shared/scripts/skf-count-tokens.py +222 -0
  27. package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
  28. package/src/shared/scripts/skf-detect-docs.py +240 -4
  29. package/src/shared/scripts/skf-detect-language.py +82 -3
  30. package/src/shared/scripts/skf-detect-tools.py +105 -2
  31. package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
  32. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  33. package/src/shared/scripts/skf-extract-public-api.py +53 -0
  34. package/src/shared/scripts/skf-find-cycles.py +246 -0
  35. package/src/shared/scripts/skf-hash-content.py +487 -2
  36. package/src/shared/scripts/skf-language-corpora.py +100 -0
  37. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  38. package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
  39. package/src/shared/scripts/skf-preapply.py +1 -1
  40. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  41. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  42. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  43. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  44. package/src/shared/scripts/skf-shape-detect.py +576 -25
  45. package/src/shared/scripts/skf-shard-body.py +461 -0
  46. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  47. package/src/shared/scripts/skf-structural-diff.py +307 -67
  48. package/src/shared/scripts/skf-validate-brief-schema.py +41 -6
  49. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  50. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  51. package/src/shared/scripts/skf-validate-output.py +443 -10
  52. package/src/shared/scripts/skf-validate-pins.py +3 -3
  53. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  54. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  55. package/src/shared/scripts/skf-write-skill-brief.py +21 -5
  56. package/src/skf-analyze-source/SKILL.md +17 -32
  57. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  58. package/src/skf-analyze-source/customize.toml +2 -0
  59. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  60. package/src/skf-analyze-source/references/continue.md +8 -13
  61. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  62. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  63. package/src/skf-analyze-source/references/health-check.md +1 -2
  64. package/src/skf-analyze-source/references/identify-units.md +25 -36
  65. package/src/skf-analyze-source/references/init.md +12 -25
  66. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  67. package/src/skf-analyze-source/references/recommend.md +4 -14
  68. package/src/skf-analyze-source/references/scan-project.md +3 -11
  69. package/src/skf-analyze-source/references/step-auto-scope.md +211 -197
  70. package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
  71. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  72. package/src/skf-audit-skill/SKILL.md +9 -6
  73. package/src/skf-audit-skill/customize.toml +2 -0
  74. package/src/skf-audit-skill/references/init.md +58 -32
  75. package/src/skf-audit-skill/references/re-index.md +15 -34
  76. package/src/skf-audit-skill/references/report.md +11 -58
  77. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  78. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  79. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  80. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  81. package/src/skf-brief-skill/SKILL.md +7 -36
  82. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  83. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
  84. package/src/skf-brief-skill/customize.toml +14 -0
  85. package/src/skf-brief-skill/references/analyze-target.md +13 -18
  86. package/src/skf-brief-skill/references/confirm-brief.md +2 -15
  87. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  88. package/src/skf-brief-skill/references/gather-intent.md +27 -24
  89. package/src/skf-brief-skill/references/headless-args.md +1 -1
  90. package/src/skf-brief-skill/references/health-check.md +1 -22
  91. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  92. package/src/skf-brief-skill/references/scope-definition.md +6 -15
  93. package/src/skf-brief-skill/references/step-auto-brief.md +22 -9
  94. package/src/skf-brief-skill/references/step-auto-validate.md +3 -7
  95. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  96. package/src/skf-brief-skill/references/write-brief.md +19 -13
  97. package/src/skf-campaign/SKILL.md +121 -0
  98. package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
  99. package/src/skf-campaign/customize.toml +75 -0
  100. package/src/skf-campaign/manifest.yaml +15 -0
  101. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  102. package/src/skf-campaign/references/campaign-directive-spec.md +32 -0
  103. package/src/skf-campaign/references/health-check.md +35 -0
  104. package/src/skf-campaign/references/step-01-setup.md +121 -0
  105. package/src/skf-campaign/references/step-02-strategy.md +102 -0
  106. package/src/skf-campaign/references/step-03-pins.md +56 -0
  107. package/src/skf-campaign/references/step-04-provenance.md +63 -0
  108. package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
  109. package/src/skf-campaign/references/step-06-batch.md +88 -0
  110. package/src/skf-campaign/references/step-07-capstone.md +63 -0
  111. package/src/skf-campaign/references/step-08-verify.md +75 -0
  112. package/src/skf-campaign/references/step-09-refine.md +83 -0
  113. package/src/skf-campaign/references/step-10-export.md +106 -0
  114. package/src/skf-campaign/references/step-11-maintenance.md +84 -0
  115. package/src/skf-campaign/references/step-resume.md +115 -0
  116. package/src/skf-campaign/scripts/.gitkeep +0 -0
  117. package/src/skf-campaign/scripts/campaign-deps.py +244 -0
  118. package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
  119. package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
  120. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  121. package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
  122. package/src/skf-campaign/scripts/campaign-report.py +249 -0
  123. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  124. package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
  125. package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
  126. package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
  127. package/src/skf-campaign/templates/campaign-report-template.md +54 -0
  128. package/src/skf-campaign/templates/kickoff-template.md +48 -0
  129. package/src/skf-create-skill/SKILL.md +15 -4
  130. package/src/skf-create-skill/assets/compile-assembly-rules.md +35 -34
  131. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  132. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  133. package/src/skf-create-skill/customize.toml +13 -0
  134. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  135. package/src/skf-create-skill/references/compile.md +37 -47
  136. package/src/skf-create-skill/references/component-extraction.md +8 -35
  137. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  138. package/src/skf-create-skill/references/enrich.md +4 -17
  139. package/src/skf-create-skill/references/extract.md +20 -27
  140. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  141. package/src/skf-create-skill/references/extraction-patterns.md +10 -8
  142. package/src/skf-create-skill/references/generate-artifacts.md +20 -25
  143. package/src/skf-create-skill/references/health-check.md +2 -2
  144. package/src/skf-create-skill/references/load-brief.md +16 -18
  145. package/src/skf-create-skill/references/report.md +27 -14
  146. package/src/skf-create-skill/references/source-resolution-protocols.md +4 -4
  147. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  148. package/src/skf-create-skill/references/step-doc-rot.md +33 -15
  149. package/src/skf-create-skill/references/step-doc-sources.md +13 -4
  150. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  151. package/src/skf-create-skill/references/sub/fetch-docs.md +33 -46
  152. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  153. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  154. package/src/skf-create-skill/references/validate.md +69 -29
  155. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  156. package/src/skf-create-stack-skill/SKILL.md +15 -9
  157. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  158. package/src/skf-create-stack-skill/customize.toml +10 -0
  159. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  160. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  161. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  162. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  163. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  164. package/src/skf-create-stack-skill/references/init.md +9 -81
  165. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  166. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  167. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  168. package/src/skf-create-stack-skill/references/report.md +27 -62
  169. package/src/skf-create-stack-skill/references/validate.md +35 -31
  170. package/src/skf-drop-skill/SKILL.md +20 -23
  171. package/src/skf-drop-skill/customize.toml +10 -0
  172. package/src/skf-drop-skill/references/execute.md +67 -36
  173. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  174. package/src/skf-drop-skill/references/health-check.md +1 -13
  175. package/src/skf-drop-skill/references/report.md +44 -10
  176. package/src/skf-drop-skill/references/select.md +93 -48
  177. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  178. package/src/skf-export-skill/SKILL.md +17 -14
  179. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  180. package/src/skf-export-skill/customize.toml +10 -6
  181. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  182. package/src/skf-export-skill/references/load-skill.md +44 -42
  183. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  184. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  185. package/src/skf-export-skill/references/package.md +27 -33
  186. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  187. package/src/skf-export-skill/references/result-envelope.md +28 -0
  188. package/src/skf-export-skill/references/summary.md +11 -5
  189. package/src/skf-export-skill/references/token-report.md +42 -24
  190. package/src/skf-export-skill/references/update-context.md +32 -33
  191. package/src/skf-forger/SKILL.md +32 -39
  192. package/src/skf-forger/references/pipeline-mode.md +34 -0
  193. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  194. package/src/skf-quick-skill/SKILL.md +10 -60
  195. package/src/skf-quick-skill/customize.toml +9 -0
  196. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  197. package/src/skf-quick-skill/references/compile.md +15 -52
  198. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  199. package/src/skf-quick-skill/references/finalize.md +12 -6
  200. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  201. package/src/skf-quick-skill/references/health-check.md +3 -4
  202. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  203. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  204. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  205. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  206. package/src/skf-refine-architecture/SKILL.md +16 -8
  207. package/src/skf-refine-architecture/customize.toml +9 -0
  208. package/src/skf-refine-architecture/references/compile.md +10 -8
  209. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  210. package/src/skf-refine-architecture/references/health-check.md +1 -1
  211. package/src/skf-refine-architecture/references/improvements.md +9 -25
  212. package/src/skf-refine-architecture/references/init.md +9 -16
  213. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  214. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  215. package/src/skf-refine-architecture/references/report.md +14 -11
  216. package/src/skf-rename-skill/SKILL.md +11 -21
  217. package/src/skf-rename-skill/customize.toml +10 -0
  218. package/src/skf-rename-skill/references/execute.md +112 -82
  219. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  220. package/src/skf-rename-skill/references/report.md +16 -8
  221. package/src/skf-rename-skill/references/select.md +69 -42
  222. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  223. package/src/skf-setup/SKILL.md +21 -13
  224. package/src/skf-setup/customize.toml +10 -0
  225. package/src/skf-setup/references/auto-index.md +11 -10
  226. package/src/skf-setup/references/ccc-index.md +14 -15
  227. package/src/skf-setup/references/detect-and-tier.md +10 -8
  228. package/src/skf-setup/references/report.md +26 -32
  229. package/src/skf-setup/references/tier-rules.md +2 -32
  230. package/src/skf-setup/references/write-config.md +9 -10
  231. package/src/skf-test-skill/SKILL.md +10 -8
  232. package/src/skf-test-skill/customize.toml +2 -0
  233. package/src/skf-test-skill/references/coherence-check.md +19 -31
  234. package/src/skf-test-skill/references/coverage-check.md +111 -67
  235. package/src/skf-test-skill/references/detect-mode.md +10 -41
  236. package/src/skf-test-skill/references/external-validators.md +11 -19
  237. package/src/skf-test-skill/references/init.md +39 -21
  238. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  239. package/src/skf-test-skill/references/report.md +60 -40
  240. package/src/skf-test-skill/references/score.md +32 -64
  241. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  242. package/src/skf-test-skill/references/source-access-protocol.md +15 -5
  243. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  244. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  245. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  246. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  247. package/src/skf-test-skill/scripts/compute-score.py +135 -4
  248. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  249. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  250. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  251. package/src/skf-update-skill/SKILL.md +17 -5
  252. package/src/skf-update-skill/customize.toml +11 -6
  253. package/src/skf-update-skill/references/detect-changes.md +48 -36
  254. package/src/skf-update-skill/references/health-check.md +5 -11
  255. package/src/skf-update-skill/references/init.md +40 -32
  256. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  257. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  258. package/src/skf-update-skill/references/merge.md +13 -57
  259. package/src/skf-update-skill/references/re-extract.md +27 -25
  260. package/src/skf-update-skill/references/report.md +14 -7
  261. package/src/skf-update-skill/references/validate.md +32 -30
  262. package/src/skf-update-skill/references/write.md +86 -51
  263. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  264. package/src/skf-verify-stack/SKILL.md +11 -21
  265. package/src/skf-verify-stack/customize.toml +12 -3
  266. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  267. package/src/skf-verify-stack/references/coverage.md +21 -10
  268. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  269. package/src/skf-verify-stack/references/health-check.md +1 -1
  270. package/src/skf-verify-stack/references/init.md +20 -25
  271. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  272. package/src/skf-verify-stack/references/integrations.md +44 -49
  273. package/src/skf-verify-stack/references/report.md +28 -34
  274. package/src/skf-verify-stack/references/requirements.md +3 -3
  275. package/src/skf-verify-stack/references/synthesize.md +36 -39
  276. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  277. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  278. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  279. package/tools/cli/lib/ui.js +3 -2
  280. package/docs/deepwiki.md +0 -89
  281. package/src/skf-rename-skill/references/rebuild-context.md +0 -110
@@ -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())
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """Deterministic previous-vs-current delta for skf-verify-stack (synthesize.md §3).
7
+
8
+ Reading two feasibility reports and deciding each finding's verdict is judgment;
9
+ classifying how the two runs differ is not. Given the two already-extracted
10
+ finding sets, "improved / regressed / new / unchanged" is a set-diff plus a
11
+ fixed verdict ranking — one correct answer per input. Doing it in prose lets the
12
+ counts drift (mismatched pair keys, inconsistent ranking), so it lives here.
13
+
14
+ Verdict ranking (higher = healthier):
15
+ * coverage — Missing(0) < Covered(1). Replaced is intentional removal, not a
16
+ gap; Replaced findings are bucketed separately and never scored improved/
17
+ regressed (mirrors their exclusion from the coverage denominator).
18
+ * integration — Blocked(0) < Risky(1) < Plausible(2) < Verified(3).
19
+ * confidence tier — T2(1) < T1-low(2) < T1(3); a drop is a regression.
20
+
21
+ Matching keys are normalized so identity is stable across runs:
22
+ * coverage findings match on technology.lower()
23
+ * integration findings match on the unordered pair {libA, libB} (lowercased),
24
+ so "A↔B" in one run equals "B↔A" in the other.
25
+
26
+ CLI usage:
27
+ uv run skf-report-delta.py '<JSON>' # JSON literal positional
28
+ uv run skf-report-delta.py --json-input '<JSON>' # explicit flag form
29
+ cat input.json | uv run skf-report-delta.py --stdin # piped input
30
+
31
+ Input schema (one object):
32
+ {
33
+ "previous": {
34
+ "coverage": [{"technology": "react", "verdict": "Covered"}, ...],
35
+ "integration": [{"libA": "react", "libB": "express", "verdict": "Verified"}, ...]
36
+ },
37
+ "current": { ... same shape ... },
38
+ "previousTiers": {"react": "T1", ...}, # optional, skill_name -> tier
39
+ "currentTiers": {"react": "T2", ...} # optional
40
+ }
41
+
42
+ Output (stdout, one object):
43
+ {
44
+ "improved": [labels], "improvedCount": <int>,
45
+ "regressed": [labels], "regressedCount": <int>,
46
+ "unchanged": [labels], "unchangedCount": <int>,
47
+ "new": [labels], "newCount": <int>,
48
+ "dropped": [labels], "droppedCount": <int>,
49
+ "replaced": [labels], "replacedCount": <int>,
50
+ "tierDowngrades": [{"skill": "...", "from": "T1", "to": "T2"}],
51
+ "tierDowngradeCount": <int>
52
+ }
53
+
54
+ Exit codes:
55
+ 0 — delta emitted successfully
56
+ 1 — no input / input could not be parsed as JSON
57
+ 2 — input parsed but schema/semantics invalid (error object emitted as JSON)
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ import argparse
63
+ import json
64
+ import sys
65
+
66
+ COVERAGE_RANK = {"Missing": 0, "Covered": 1}
67
+ COVERAGE_VERDICTS = ("Covered", "Missing", "Replaced")
68
+ INTEGRATION_RANK = {"Blocked": 0, "Risky": 1, "Plausible": 2, "Verified": 3}
69
+ INTEGRATION_VERDICTS = tuple(INTEGRATION_RANK)
70
+ TIER_RANK = {"T2": 1, "T1-LOW": 2, "T1": 3, "T3": 0}
71
+
72
+
73
+ def make_error(message):
74
+ return {"error": message, "code": "INVALID_INPUT"}
75
+
76
+
77
+ def _validate_side(side, name):
78
+ if not isinstance(side, dict):
79
+ return f"`{name}` must be an object with `coverage`/`integration` lists"
80
+ cov = side.get("coverage", [])
81
+ integ = side.get("integration", [])
82
+ if not isinstance(cov, list) or not isinstance(integ, list):
83
+ return f"`{name}.coverage` and `{name}.integration` must be lists"
84
+ for i, row in enumerate(cov):
85
+ if not isinstance(row, dict) or not isinstance(row.get("technology"), str):
86
+ return f"`{name}.coverage[{i}]` requires a string `technology`"
87
+ if row.get("verdict") not in COVERAGE_VERDICTS:
88
+ return (
89
+ f"`{name}.coverage[{i}]` verdict {row.get('verdict')!r} not one of: "
90
+ f"{', '.join(COVERAGE_VERDICTS)}"
91
+ )
92
+ for i, row in enumerate(integ):
93
+ if not isinstance(row, dict) or not isinstance(row.get("libA"), str) or not isinstance(row.get("libB"), str):
94
+ return f"`{name}.integration[{i}]` requires string `libA` and `libB`"
95
+ if row.get("verdict") not in INTEGRATION_VERDICTS:
96
+ return (
97
+ f"`{name}.integration[{i}]` verdict {row.get('verdict')!r} not one of: "
98
+ f"{', '.join(INTEGRATION_VERDICTS)}"
99
+ )
100
+ return None
101
+
102
+
103
+ def _validate(inp):
104
+ if inp is None or not isinstance(inp, dict):
105
+ return "Input must be a JSON object"
106
+ if "previous" not in inp or "current" not in inp:
107
+ return "Input requires `previous` and `current` objects"
108
+ for name in ("previous", "current"):
109
+ err = _validate_side(inp[name], name)
110
+ if err:
111
+ return err
112
+ for key in ("previousTiers", "currentTiers"):
113
+ tiers = inp.get(key)
114
+ if tiers is not None and not isinstance(tiers, dict):
115
+ return f"`{key}` must be an object mapping skill_name -> tier"
116
+ return None
117
+
118
+
119
+ def _index(side):
120
+ """Return {normalized_key: (label, domain, verdict)} for one report side."""
121
+ out = {}
122
+ for row in side.get("coverage", []):
123
+ tech = row["technology"].strip()
124
+ out[("cov", tech.lower())] = (tech, "coverage", row["verdict"])
125
+ for row in side.get("integration", []):
126
+ a, b = row["libA"].strip(), row["libB"].strip()
127
+ pair = tuple(sorted([a.lower(), b.lower()]))
128
+ out[("int", pair)] = (f"{a} ↔ {b}", "integration", row["verdict"])
129
+ return out
130
+
131
+
132
+ def _rank(domain, verdict):
133
+ return COVERAGE_RANK.get(verdict) if domain == "coverage" else INTEGRATION_RANK.get(verdict)
134
+
135
+
136
+ def compute(inp):
137
+ """Pure delta over the two extracted finding sets."""
138
+ err = _validate(inp)
139
+ if err:
140
+ return make_error(err)
141
+
142
+ prev = _index(inp["previous"])
143
+ curr = _index(inp["current"])
144
+
145
+ buckets = {k: [] for k in ("improved", "regressed", "unchanged", "new", "dropped", "replaced")}
146
+
147
+ for key, (label, domain, verdict) in curr.items():
148
+ if key not in prev:
149
+ # Replaced-on-arrival is informational, not a regression/new gap.
150
+ if verdict == "Replaced":
151
+ buckets["replaced"].append(label)
152
+ else:
153
+ buckets["new"].append(label)
154
+ continue
155
+ _, _, prev_verdict = prev[key]
156
+ if verdict == "Replaced" or prev_verdict == "Replaced":
157
+ buckets["replaced"].append(label)
158
+ continue
159
+ pr, cr = _rank(domain, prev_verdict), _rank(domain, verdict)
160
+ if cr > pr:
161
+ buckets["improved"].append(label)
162
+ elif cr < pr:
163
+ buckets["regressed"].append(label)
164
+ else:
165
+ buckets["unchanged"].append(label)
166
+
167
+ for key, (label, _domain, verdict) in prev.items():
168
+ if key not in curr:
169
+ if verdict == "Replaced":
170
+ buckets["replaced"].append(label)
171
+ else:
172
+ buckets["dropped"].append(label)
173
+
174
+ tier_downgrades = []
175
+ prev_tiers = inp.get("previousTiers") or {}
176
+ curr_tiers = inp.get("currentTiers") or {}
177
+ for skill in sorted(set(prev_tiers) & set(curr_tiers)):
178
+ pr = TIER_RANK.get(str(prev_tiers[skill]).upper())
179
+ cr = TIER_RANK.get(str(curr_tiers[skill]).upper())
180
+ if pr is None or cr is None:
181
+ continue
182
+ if cr < pr:
183
+ tier_downgrades.append({"skill": skill, "from": prev_tiers[skill], "to": curr_tiers[skill]})
184
+
185
+ result = {}
186
+ for name, items in buckets.items():
187
+ result[name] = sorted(items)
188
+ result[name + "Count"] = len(items)
189
+ result["tierDowngrades"] = tier_downgrades
190
+ result["tierDowngradeCount"] = len(tier_downgrades)
191
+ return result
192
+
193
+
194
+ # --- CLI --------------------------------------------------------------------
195
+
196
+
197
+ def _build_parser():
198
+ parser = argparse.ArgumentParser(
199
+ prog="skf-report-delta",
200
+ description=(
201
+ "Deterministic previous-vs-current feasibility delta (synthesize.md "
202
+ "§3). Consumes the two extracted finding sets and emits improved / "
203
+ "regressed / unchanged / new / dropped counts plus tier downgrades."
204
+ ),
205
+ formatter_class=argparse.RawDescriptionHelpFormatter,
206
+ epilog=(
207
+ "Example:\n"
208
+ " uv run skf-report-delta.py "
209
+ "'{\"previous\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Missing\"}]},"
210
+ "\"current\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Covered\"}]}}'"
211
+ ),
212
+ )
213
+ src = parser.add_mutually_exclusive_group()
214
+ src.add_argument("json_input", nargs="?", help="JSON object as a positional argument.")
215
+ src.add_argument("--json-input", dest="json_input_flag", help="JSON object passed via flag.")
216
+ src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
217
+ return parser
218
+
219
+
220
+ def _resolve_input(args):
221
+ if args.stdin:
222
+ return sys.stdin.read()
223
+ if args.json_input_flag is not None:
224
+ return args.json_input_flag
225
+ if args.json_input is not None:
226
+ return args.json_input
227
+ return ""
228
+
229
+
230
+ def main(argv=None):
231
+ parser = _build_parser()
232
+ args = parser.parse_args(argv)
233
+ raw = _resolve_input(args)
234
+ if not raw.strip():
235
+ parser.print_usage(file=sys.stderr)
236
+ print(
237
+ "error: no input provided (positional arg, --json-input, or --stdin)",
238
+ file=sys.stderr,
239
+ )
240
+ return 1
241
+
242
+ try:
243
+ data = json.loads(raw)
244
+ except json.JSONDecodeError as exc:
245
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
246
+ return 1
247
+
248
+ result = compute(data)
249
+ print(json.dumps(result, indent=2))
250
+ if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
251
+ return 2
252
+ return 0
253
+
254
+
255
+ if __name__ == "__main__":
256
+ raise SystemExit(main())