bmad-module-skill-forge 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +15 -5
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -90
  348. package/src/skf-setup/references/detect-and-tier.md +58 -67
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -679
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -1,32 +1,76 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.10"
3
+ # requires-python = ">=3.11"
4
4
  # dependencies = []
5
5
  # ///
6
6
  """Deterministic previous-vs-current delta for skf-verify-stack (synthesize.md §3).
7
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.
8
+ Deciding each finding's verdict is judgment, made by the run that wrote the
9
+ report; classifying how two runs differ is not. "improved / regressed / new /
10
+ unchanged" is a set-diff plus a fixed verdict ranking, one correct answer per
11
+ input. Doing it in prose lets the counts drift (mismatched pair keys,
12
+ inconsistent ranking), so it lives here. So does reading the finding sets out
13
+ of the two reports (--previous-report, --current-report): each one sits in a
14
+ table with a pinned header, so reading it has one correct answer too.
13
15
 
14
16
  Verdict ranking (higher = healthier):
15
17
  * coverage — Missing(0) < Covered(1). Replaced is intentional removal, not a
16
18
  gap; Replaced findings are bucketed separately and never scored improved/
17
19
  regressed (mirrors their exclusion from the coverage denominator).
18
20
  * integration — Blocked(0) < Risky(1) < Plausible(2) < Verified(3).
19
- * confidence tier — T2(1) < T1-low(2) < T1(3); a drop is a regression.
21
+ * evidence tier: T3(0) < T2(1) < T1-low(2) < T1(3); a drop is a regression.
22
+
23
+ Tiers use one scale, the confidence tiers T1, T1-low, T2 and T3 (case-sensitive),
24
+ for every skill: the caller maps each skill to it before calling, or passes
25
+ --inventory, whose skf-enumerate-stack-skills.py inventory gives each skill's
26
+ evidence_tier (verify-stack does). metadata.json
27
+ `confidence_tier` is no input here: a single skill records its forge tier there
28
+ (Quick, Forge, Forge+ or Deep) and a stack its confidence tier, so its values sit
29
+ on two scales that do not compare. Any other token, a forge tier included, is
30
+ rejected (exit 2, code UNKNOWN_TIER), never skipped: a skipped token would hide
31
+ a regression.
20
32
 
21
33
  Matching keys are normalized so identity is stable across runs:
22
34
  * coverage findings match on technology.lower()
23
35
  * integration findings match on the unordered pair {libA, libB} (lowercased),
24
36
  so "A↔B" in one run equals "B↔A" in the other.
25
37
 
38
+ Reading a report (--previous-report, --current-report):
39
+ * A report whose frontmatter schemaVersion is not "1.0" is not read.
40
+ * coverage: the Technology and Verdict columns of the first table under
41
+ `## Coverage Analysis` whose header row has both (coverage.md §6 writes
42
+ `| Technology | Source Section | Skill Match | Verdict |`).
43
+ * integration: the lib_a, lib_b and verdict columns of the canonical
44
+ `| lib_a | lib_b | verdict | rationale |` table under
45
+ `## Integration Verdicts`, read by read_verdict_table() of the shared
46
+ skf-validate-feasibility-report.py, the reader the report's consumers use.
47
+ A report with no such table, or with two, is not read.
48
+ * tiers, from the previous report only: the skill and evidence_tier columns
49
+ of the first table under `## Evidence Sources` whose header row has both
50
+ (synthesize.md §5 writes `| skill | evidence_tier | confidence_tier |
51
+ metadata_schema_version | skill_md |`). A report written before tiers were
52
+ recorded has no such table, or one with no row: previousTiersRecorded is
53
+ then false and no tier is compared.
54
+ Tables in fenced code or in an HTML comment are skipped. A verdict or tier
55
+ cell holds the token alone (case-sensitive), and every row names its
56
+ technology, its two libraries or its skill.
57
+
26
58
  CLI usage:
27
59
  uv run skf-report-delta.py '<JSON>' # JSON literal positional
28
60
  uv run skf-report-delta.py --json-input '<JSON>' # explicit flag form
29
61
  cat input.json | uv run skf-report-delta.py --stdin # piped input
62
+ echo '{"currentTiers": {...}}' | uv run skf-report-delta.py \\
63
+ --previous-report <earlier.md> --current-report <this-run.md> --stdin
64
+ uv run skf-report-delta.py --previous-report <earlier.md> \\
65
+ --current-report <this-run.md> --inventory <skill-inventory.json>
66
+
67
+ --previous-report fills `previous` and `previousTiers` from that report,
68
+ --current-report fills `current` from its report, and --inventory fills
69
+ `currentTiers` from an skf-enumerate-stack-skills.py inventory (each
70
+ skills[] entry's name and evidence_tier), so the JSON leaves them out; with
71
+ both report flags the JSON may be left out too. --no-tiers compares no
72
+ tier: both tier maps, the previous report's tier table and --inventory are
73
+ ignored.
30
74
 
31
75
  Input schema (one object):
32
76
  {
@@ -35,8 +79,8 @@ Input schema (one object):
35
79
  "integration": [{"libA": "react", "libB": "express", "verdict": "Verified"}, ...]
36
80
  },
37
81
  "current": { ... same shape ... },
38
- "previousTiers": {"react": "T1", ...}, # optional, skill_name -> tier
39
- "currentTiers": {"react": "T2", ...} # optional
82
+ "previousTiers": {"react": "T1", ...}, # optional, skill_name -> T1|T1-low|T2|T3
83
+ "currentTiers": {"react": "T2", ...} # optional, same scale
40
84
  }
41
85
 
42
86
  Output (stdout, one object):
@@ -48,30 +92,88 @@ Output (stdout, one object):
48
92
  "dropped": [labels], "droppedCount": <int>,
49
93
  "replaced": [labels], "replacedCount": <int>,
50
94
  "tierDowngrades": [{"skill": "...", "from": "T1", "to": "T2"}],
51
- "tierDowngradeCount": <int>
95
+ "tierDowngradeCount": <int>,
96
+ "tiersCompared": <bool>, # both tier maps were there to compare
97
+ "previousTiersRecorded": <bool> | null # --previous-report: the report records
98
+ # tiers; null without it or with --no-tiers
52
99
  }
53
100
 
101
+ Errors (stdout, one object): {"error": "<why>", "code": "<code>"}, plus "report"
102
+ ("previous" or "current") and "path" when the error comes from a report, and
103
+ "path" alone when it comes from --inventory:
104
+ INVALID_INPUT the JSON does not fit the input schema above, or the
105
+ --inventory file cannot be read or holds no skills[] list
106
+ UNKNOWN_TIER a tier token outside T1, T1-low, T2 and T3
107
+ INVALID_REPORT a report that cannot be read, or is not read (see above)
108
+ HELPER_MISSING the shared skf-validate-feasibility-report.py is not in the
109
+ shared scripts folder beside this skill's folder
110
+
54
111
  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)
112
+ 0 delta emitted successfully
113
+ 1 no input, input that is not JSON, or HELPER_MISSING
114
+ 2 input rejected: INVALID_INPUT, UNKNOWN_TIER or INVALID_REPORT
58
115
  """
59
116
 
60
117
  from __future__ import annotations
61
118
 
62
119
  import argparse
120
+ import importlib.util
63
121
  import json
122
+ import re
64
123
  import sys
124
+ from pathlib import Path
65
125
 
66
126
  COVERAGE_RANK = {"Missing": 0, "Covered": 1}
67
127
  COVERAGE_VERDICTS = ("Covered", "Missing", "Replaced")
68
128
  INTEGRATION_RANK = {"Blocked": 0, "Risky": 1, "Plausible": 2, "Verified": 3}
69
129
  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"}
130
+ TIER_RANK = {"T3": 0, "T2": 1, "T1-low": 2, "T1": 3}
131
+ TIERS = tuple(sorted(TIER_RANK, key=TIER_RANK.get, reverse=True)) # strongest first
132
+
133
+ INVALID_INPUT = "INVALID_INPUT"
134
+ UNKNOWN_TIER = "UNKNOWN_TIER"
135
+ INVALID_REPORT = "INVALID_REPORT"
136
+ HELPER_MISSING = "HELPER_MISSING"
137
+
138
+ # The shared feasibility-report reader. Installed (under _bmad/skf/ or an
139
+ # IDE's skills folder) and in a dev checkout (src/), shared/ sits beside
140
+ # this skill's folder.
141
+ SHARED_READER = (
142
+ Path(__file__).resolve().parent.parent.parent
143
+ / "shared"
144
+ / "scripts"
145
+ / "skf-validate-feasibility-report.py"
146
+ )
147
+ SCHEMA_VERSION = "1.0"
148
+
149
+ # The two tables read here besides the canonical verdict table: the section
150
+ # each sits under and the columns read from it (see "Reading a report").
151
+ COVERAGE_SECTION = "Coverage Analysis"
152
+ COVERAGE_COLUMNS = ("Technology", "Verdict")
153
+ TIERS_SECTION = "Evidence Sources"
154
+ TIER_COLUMNS = ("skill", "evidence_tier")
155
+
156
+ # Table scanning, the way the shared reader scans for the verdict table.
157
+ _FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})")
158
+ _COMMENT_OPEN_RE = re.compile(r"^\s{0,3}<!--")
159
+ _COMMENT_CLOSE = "-->"
160
+ _SECTION_END_RE = re.compile(r"^#{1,2}\s")
161
+ _H2_RE = re.compile(r"^##\s+(.+?)\s*$")
162
+ _CELL_SEPARATOR_RE = re.compile(r"(?<!\\)\|")
163
+ _DELIMITER_CELL_RE = re.compile(r"^:?-+:?$")
164
+
165
+
166
+ class ReportError(Exception):
167
+ """A file the delta does not read; `code` is INVALID_REPORT or UNKNOWN_TIER
168
+ for a report, INVALID_INPUT for an --inventory file."""
169
+
170
+ def __init__(self, code, message):
171
+ super().__init__(message)
172
+ self.code = code
173
+
174
+
175
+ def make_error(message, code=INVALID_INPUT):
176
+ return {"error": message, "code": code}
75
177
 
76
178
 
77
179
  def _validate_side(side, name):
@@ -101,18 +203,29 @@ def _validate_side(side, name):
101
203
 
102
204
 
103
205
  def _validate(inp):
206
+ """Return (message, code) for an input compute() rejects, else None."""
104
207
  if inp is None or not isinstance(inp, dict):
105
- return "Input must be a JSON object"
208
+ return "Input must be a JSON object", INVALID_INPUT
106
209
  if "previous" not in inp or "current" not in inp:
107
- return "Input requires `previous` and `current` objects"
210
+ return "Input requires `previous` and `current` objects", INVALID_INPUT
108
211
  for name in ("previous", "current"):
109
212
  err = _validate_side(inp[name], name)
110
213
  if err:
111
- return err
214
+ return err, INVALID_INPUT
112
215
  for key in ("previousTiers", "currentTiers"):
113
216
  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"
217
+ if tiers is None:
218
+ continue
219
+ if not isinstance(tiers, dict):
220
+ return f"`{key}` must be an object mapping skill_name -> tier", INVALID_INPUT
221
+ for skill, tier in tiers.items():
222
+ if tier not in TIERS:
223
+ return (
224
+ f"`{key}.{skill}` tier {tier!r} not one of: {', '.join(TIERS)} "
225
+ "(map each skill to its evidence tier before calling; a forge tier "
226
+ "such as Deep is on another scale)",
227
+ UNKNOWN_TIER,
228
+ )
116
229
  return None
117
230
 
118
231
 
@@ -134,10 +247,10 @@ def _rank(domain, verdict):
134
247
 
135
248
 
136
249
  def compute(inp):
137
- """Pure delta over the two extracted finding sets."""
250
+ """Pure delta over the two finding sets."""
138
251
  err = _validate(inp)
139
252
  if err:
140
- return make_error(err)
253
+ return make_error(*err)
141
254
 
142
255
  prev = _index(inp["previous"])
143
256
  curr = _index(inp["current"])
@@ -171,14 +284,13 @@ def compute(inp):
171
284
  else:
172
285
  buckets["dropped"].append(label)
173
286
 
287
+ prev_tiers = inp.get("previousTiers")
288
+ curr_tiers = inp.get("currentTiers")
289
+ compared = prev_tiers is not None and curr_tiers is not None
174
290
  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
291
+ for skill in sorted(set(prev_tiers or {}) & set(curr_tiers or {})):
292
+ pr = TIER_RANK[prev_tiers[skill]]
293
+ cr = TIER_RANK[curr_tiers[skill]]
182
294
  if cr < pr:
183
295
  tier_downgrades.append({"skill": skill, "from": prev_tiers[skill], "to": curr_tiers[skill]})
184
296
 
@@ -188,6 +300,249 @@ def compute(inp):
188
300
  result[name + "Count"] = len(items)
189
301
  result["tierDowngrades"] = tier_downgrades
190
302
  result["tierDowngradeCount"] = len(tier_downgrades)
303
+ result["tiersCompared"] = compared
304
+ return result
305
+
306
+
307
+ # --- Reading a report ---------------------------------------------------------
308
+
309
+
310
+ def load_reader():
311
+ """Import the shared feasibility-report reader, or None when it is missing."""
312
+ try:
313
+ if not SHARED_READER.is_file():
314
+ return None
315
+ except OSError:
316
+ return None # a folder on the way that cannot be searched
317
+ spec = importlib.util.spec_from_file_location("skf_validate_feasibility_report", SHARED_READER)
318
+ if spec is None or spec.loader is None:
319
+ return None
320
+ module = importlib.util.module_from_spec(spec)
321
+ spec.loader.exec_module(module)
322
+ return module
323
+
324
+
325
+ def _cells(line):
326
+ """Split one table row into its trimmed cells; `\\|` is a pipe inside a cell."""
327
+ row = line.strip()
328
+ if row.startswith("|"):
329
+ row = row[1:]
330
+ if row.endswith("|") and not row.endswith("\\|"):
331
+ row = row[:-1]
332
+ return [cell.strip().replace("\\|", "|") for cell in _CELL_SEPARATOR_RE.split(row)]
333
+
334
+
335
+ def _section_tables(body, section, first_line):
336
+ """Yield (header, rows) for each table under `## {section}`.
337
+
338
+ `header` holds the header row's cells, and `rows` each data row as
339
+ (file_line, cells), counting from `first_line`, the file line of the
340
+ body's first line. A table is a line that starts with a pipe, then a
341
+ delimiter row, then every following line that starts with a pipe.
342
+ Fenced code and HTML comments are skipped.
343
+ """
344
+ lines = body.split("\n")
345
+ fence = None
346
+ in_comment = in_section = False
347
+ idx = 0
348
+ while idx < len(lines):
349
+ line = lines[idx]
350
+ idx += 1
351
+ fence_match = _FENCE_RE.match(line)
352
+ if fence is not None:
353
+ marker = fence_match.group(1) if fence_match else ""
354
+ if marker[:1] == fence[0] and len(marker) >= len(fence):
355
+ fence = None
356
+ continue
357
+ if in_comment:
358
+ in_comment = _COMMENT_CLOSE not in line
359
+ continue
360
+ if fence_match:
361
+ fence = fence_match.group(1)
362
+ continue
363
+ if _COMMENT_OPEN_RE.match(line):
364
+ in_comment = _COMMENT_CLOSE not in line
365
+ continue
366
+ if _SECTION_END_RE.match(line):
367
+ heading = _H2_RE.match(line)
368
+ in_section = heading is not None and heading.group(1).strip() == section
369
+ continue
370
+ if not in_section or not line.lstrip().startswith("|") or idx >= len(lines):
371
+ continue
372
+ delimiter = lines[idx]
373
+ if not delimiter.lstrip().startswith("|") or not all(
374
+ _DELIMITER_CELL_RE.match(cell) for cell in _cells(delimiter)
375
+ ):
376
+ continue
377
+ header, rows = _cells(line), []
378
+ idx += 1
379
+ while idx < len(lines) and lines[idx].lstrip().startswith("|"):
380
+ rows.append((first_line + idx, _cells(lines[idx])))
381
+ idx += 1
382
+ yield header, rows
383
+
384
+
385
+ def _columns(tables, wanted):
386
+ """The first table whose header has every wanted column, as (indexes, rows), else None."""
387
+ for header, rows in tables:
388
+ if all(name in header for name in wanted):
389
+ return [header.index(name) for name in wanted], rows
390
+ return None
391
+
392
+
393
+ def _cell(cells, index):
394
+ return cells[index] if index < len(cells) else ""
395
+
396
+
397
+ def read_report(path, reader, with_tiers):
398
+ """Read one report's finding sets (see "Reading a report").
399
+
400
+ Returns (side, tiers): `side` is {"coverage": [...], "integration": [...]}
401
+ in compute()'s input shape, and `tiers` maps each skill to its evidence
402
+ tier, or is None when `with_tiers` is false or the report records no
403
+ tier. Raises ReportError for a report that is not read.
404
+ """
405
+ shown = f"`{path}`"
406
+ try:
407
+ # utf-8-sig drops the byte order mark some editors add, as the shared
408
+ # reader does.
409
+ content = Path(path).read_text(encoding="utf-8-sig")
410
+ except (OSError, UnicodeDecodeError) as exc:
411
+ raise ReportError(INVALID_REPORT, f"{shown} cannot be read: {exc}") from exc
412
+ frontmatter, body = reader.split_frontmatter(content)
413
+ version = frontmatter.get("schemaVersion")
414
+ if version != SCHEMA_VERSION:
415
+ found = "no schemaVersion" if version is None else f"schemaVersion {version!r}"
416
+ raise ReportError(INVALID_REPORT, f"{shown} has {found}, not {SCHEMA_VERSION!r}")
417
+ # The body starts on the file line after the frontmatter's closing `---`.
418
+ first_line = len(content.split("\n")) - len(body.split("\n")) + 1
419
+
420
+ table = _columns(_section_tables(body, COVERAGE_SECTION, first_line), COVERAGE_COLUMNS)
421
+ if table is None:
422
+ raise ReportError(
423
+ INVALID_REPORT,
424
+ f"{shown} has no table with Technology and Verdict columns under `## {COVERAGE_SECTION}`",
425
+ )
426
+ (tech_at, verdict_at), rows = table
427
+ coverage = []
428
+ for line, cells in rows:
429
+ tech, verdict = _cell(cells, tech_at), _cell(cells, verdict_at)
430
+ if not tech:
431
+ raise ReportError(INVALID_REPORT, f"{shown} line {line}: a coverage row names no technology")
432
+ if verdict not in COVERAGE_VERDICTS:
433
+ raise ReportError(
434
+ INVALID_REPORT,
435
+ f"{shown} line {line}: coverage verdict {verdict!r} for `{tech}` is not one of: "
436
+ f"{', '.join(COVERAGE_VERDICTS)}",
437
+ )
438
+ coverage.append({"technology": tech, "verdict": verdict})
439
+
440
+ verdict_header = "| " + " | ".join(reader.VERDICT_TABLE_HEADER) + " |"
441
+ found, rows, duplicate_line = reader.read_verdict_table(body, first_line)
442
+ if not found:
443
+ raise ReportError(INVALID_REPORT, f"{shown} has no `{verdict_header}` table under `## Integration Verdicts`")
444
+ if duplicate_line is not None:
445
+ raise ReportError(
446
+ INVALID_REPORT, f"{shown} holds the `{verdict_header}` table twice (again at line {duplicate_line})"
447
+ )
448
+ integration = []
449
+ for line, (lib_a, lib_b, verdict, _rationale) in rows:
450
+ if not lib_a or not lib_b:
451
+ raise ReportError(INVALID_REPORT, f"{shown} line {line}: a verdict row names fewer than two libraries")
452
+ if verdict not in INTEGRATION_VERDICTS:
453
+ raise ReportError(
454
+ INVALID_REPORT,
455
+ f"{shown} line {line}: verdict {verdict!r} for `{lib_a}` and `{lib_b}` is not one of: "
456
+ f"{', '.join(INTEGRATION_VERDICTS)}",
457
+ )
458
+ integration.append({"libA": lib_a, "libB": lib_b, "verdict": verdict})
459
+
460
+ tiers = None
461
+ table = _columns(_section_tables(body, TIERS_SECTION, first_line), TIER_COLUMNS) if with_tiers else None
462
+ if table is not None and table[1]:
463
+ (skill_at, tier_at), rows = table
464
+ tiers = {}
465
+ for line, cells in rows:
466
+ skill, tier = _cell(cells, skill_at), _cell(cells, tier_at)
467
+ if not skill:
468
+ raise ReportError(INVALID_REPORT, f"{shown} line {line}: an `## {TIERS_SECTION}` row names no skill")
469
+ if tier not in TIERS:
470
+ raise ReportError(
471
+ UNKNOWN_TIER,
472
+ f"{shown} line {line}: skill `{skill}` has evidence_tier {tier!r}, not one of: "
473
+ f"{', '.join(TIERS)}",
474
+ )
475
+ tiers[skill] = tier
476
+ return {"coverage": coverage, "integration": integration}, tiers
477
+
478
+
479
+ def read_inventory_tiers(path):
480
+ """`currentTiers` from an skf-enumerate-stack-skills.py inventory file.
481
+
482
+ Maps each skills[] entry's name to its evidence_tier, which compute()
483
+ checks against the tier scale. Raises ReportError (INVALID_INPUT) for a
484
+ file that cannot be read, is not JSON or holds no skills[] list of named
485
+ entries.
486
+ """
487
+ shown = f"--inventory `{path}`"
488
+ try:
489
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
490
+ except (OSError, UnicodeDecodeError) as exc:
491
+ raise ReportError(INVALID_INPUT, f"{shown} cannot be read: {exc}") from exc
492
+ except json.JSONDecodeError as exc:
493
+ raise ReportError(INVALID_INPUT, f"{shown} is not JSON: {exc.msg}") from exc
494
+ skills = data.get("skills") if isinstance(data, dict) else None
495
+ if not isinstance(skills, list):
496
+ raise ReportError(INVALID_INPUT, f"{shown} holds no `skills` list")
497
+ tiers = {}
498
+ for i, entry in enumerate(skills):
499
+ name = entry.get("name") if isinstance(entry, dict) else None
500
+ if not isinstance(name, str) or not name.strip():
501
+ raise ReportError(INVALID_INPUT, f"{shown} skills[{i}] has no `name`")
502
+ tiers[name.strip()] = entry.get("evidence_tier")
503
+ return tiers
504
+
505
+
506
+ def run(data, previous_report=None, current_report=None, no_tiers=False, reader=None, inventory=None):
507
+ """The delta for one call: the JSON input, each side a report flag names read from that report.
508
+
509
+ `reader` is the shared report reader (load_reader()), needed only with a
510
+ report path. `inventory`, an skf-enumerate-stack-skills.py inventory file,
511
+ gives `currentTiers`.
512
+ """
513
+ if not isinstance(data, dict):
514
+ return make_error("Input must be a JSON object")
515
+ inp = dict(data)
516
+ recorded = None
517
+ for side, path in (("previous", previous_report), ("current", current_report)):
518
+ if path is None:
519
+ continue
520
+ taken = ("previous", "previousTiers") if side == "previous" else ("current",)
521
+ clash = [key for key in taken if key in inp]
522
+ if clash:
523
+ return make_error(f"`{clash[0]}` comes from --{side}-report: leave it out of the JSON")
524
+ with_tiers = side == "previous" and not no_tiers
525
+ try:
526
+ inp[side], tiers = read_report(path, reader, with_tiers)
527
+ except ReportError as exc:
528
+ return {**make_error(str(exc), exc.code), "report": side, "path": path}
529
+ if with_tiers:
530
+ recorded = tiers is not None
531
+ if tiers is not None:
532
+ inp["previousTiers"] = tiers
533
+ if inventory is not None and not no_tiers:
534
+ if "currentTiers" in inp:
535
+ return make_error("`currentTiers` comes from --inventory: leave it out of the JSON")
536
+ try:
537
+ inp["currentTiers"] = read_inventory_tiers(inventory)
538
+ except ReportError as exc:
539
+ return {**make_error(str(exc), exc.code), "path": inventory}
540
+ if no_tiers:
541
+ inp.pop("previousTiers", None)
542
+ inp.pop("currentTiers", None)
543
+ result = compute(inp)
544
+ if "code" not in result:
545
+ result["previousTiersRecorded"] = recorded
191
546
  return result
192
547
 
193
548
 
@@ -197,23 +552,46 @@ def compute(inp):
197
552
  def _build_parser():
198
553
  parser = argparse.ArgumentParser(
199
554
  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
- ),
555
+ # The module docstring is the contract synthesize.md points --help at:
556
+ # the rankings, the one tier scale, how a report is read, and the input,
557
+ # output and error schemas.
558
+ description=__doc__,
205
559
  formatter_class=argparse.RawDescriptionHelpFormatter,
206
560
  epilog=(
207
- "Example:\n"
561
+ "Examples:\n"
208
562
  " uv run skf-report-delta.py "
209
563
  "'{\"previous\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Missing\"}]},"
210
- "\"current\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Covered\"}]}}'"
564
+ "\"current\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Covered\"}]}}'\n"
565
+ " uv run skf-report-delta.py "
566
+ "--previous-report forge-data/feasibility-report-my-app-20260101-080000.md "
567
+ "--current-report forge-data/feasibility-report-my-app-20260930-120000.md "
568
+ "--inventory skill-inventory.json"
211
569
  ),
212
570
  )
213
571
  src = parser.add_mutually_exclusive_group()
214
572
  src.add_argument("json_input", nargs="?", help="JSON object as a positional argument.")
215
573
  src.add_argument("--json-input", dest="json_input_flag", help="JSON object passed via flag.")
216
574
  src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
575
+ parser.add_argument(
576
+ "--previous-report",
577
+ metavar="PATH",
578
+ help="read `previous` and `previousTiers` from this earlier feasibility report",
579
+ )
580
+ parser.add_argument(
581
+ "--current-report",
582
+ metavar="PATH",
583
+ help="read `current` from this run's feasibility report",
584
+ )
585
+ parser.add_argument(
586
+ "--inventory",
587
+ metavar="PATH",
588
+ help="read `currentTiers` from this skf-enumerate-stack-skills.py inventory (each skill's evidence_tier)",
589
+ )
590
+ parser.add_argument(
591
+ "--no-tiers",
592
+ action="store_true",
593
+ help="compare no tier: ignore both tier maps, the previous report's tier table and --inventory",
594
+ )
217
595
  return parser
218
596
 
219
597
 
@@ -232,12 +610,15 @@ def main(argv=None):
232
610
  args = parser.parse_args(argv)
233
611
  raw = _resolve_input(args)
234
612
  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
613
+ if args.previous_report is None or args.current_report is None:
614
+ parser.print_usage(file=sys.stderr)
615
+ print(
616
+ "error: no input provided (positional arg, --json-input, or --stdin; "
617
+ "it may be left out only with both --previous-report and --current-report)",
618
+ file=sys.stderr,
619
+ )
620
+ return 1
621
+ raw = "{}"
241
622
 
242
623
  try:
243
624
  data = json.loads(raw)
@@ -245,12 +626,35 @@ def main(argv=None):
245
626
  print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
246
627
  return 1
247
628
 
248
- result = compute(data)
629
+ reader = None
630
+ if args.previous_report is not None or args.current_report is not None:
631
+ reader = load_reader()
632
+ if reader is None:
633
+ message = f"the shared report reader is missing: {SHARED_READER}"
634
+ print(json.dumps(make_error(message, HELPER_MISSING), indent=2))
635
+ return 1
636
+
637
+ result = run(data, args.previous_report, args.current_report, args.no_tiers, reader, args.inventory)
249
638
  print(json.dumps(result, indent=2))
250
- if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
251
- return 2
252
- return 0
639
+ return 2 if "code" in result else 0
640
+
641
+
642
+ def _force_utf8(*streams) -> None:
643
+ """Reconfigure the given streams to UTF-8, keeping each stream's error handler.
644
+
645
+ A Windows console pipes them as cp1252, which cannot print every character
646
+ of the --help text, so --help would stop with UnicodeEncodeError, and which
647
+ garbles UTF-8 JSON piped into --stdin.
648
+ """
649
+ for stream in streams:
650
+ if hasattr(stream, "reconfigure"):
651
+ errors = getattr(stream, "errors", None)
652
+ if errors is None:
653
+ stream.reconfigure(encoding="utf-8")
654
+ else:
655
+ stream.reconfigure(encoding="utf-8", errors=errors)
253
656
 
254
657
 
255
658
  if __name__ == "__main__":
659
+ _force_utf8(sys.stdin, sys.stdout, sys.stderr)
256
660
  raise SystemExit(main())