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,18 +1,20 @@
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
- """Deterministic technology-coverage tally for skf-verify-stack (coverage.md §6).
6
+ """Deterministic verdict tallies for skf-verify-stack.
7
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`.
8
+ Each verdict itself is LLM judgment: matching an architecture technology to a
9
+ generated skill involves aliases and prose (coverage.md §3), and so does rating
10
+ an integration pair (integrations.md §4) or a requirement (requirements.md §3).
11
+ This script takes the rows the prompt already decided and does only the part
12
+ with one correct answer: counting each verdict class. --kind picks the rows.
13
13
 
14
- Two gotchas make the in-prose version drift between runs, so they live here
15
- instead:
14
+ coverage (the default; coverage.md §3, persisted in §6)
15
+ Counts Covered, Missing and Replaced and turns the counts into
16
+ `coveragePercentage`. Two gotchas make the in-prose version drift between
17
+ runs, so they live here instead:
16
18
 
17
19
  * The denominator excludes Replaced. `live_count = Covered + Missing`;
18
20
  technologies flagged Replaced (intentionally being removed) are not a gap and
@@ -21,34 +23,104 @@ instead:
21
23
  always yields the same `coveragePercentage` (the shared schema declares
22
24
  `coveragePercentage: <0..100 integer>` but not the rounding rule).
23
25
 
26
+ integrations (integrations.md §4, persisted in §6)
27
+ Counts the rows of the canonical `| lib_a | lib_b | verdict | rationale |`
28
+ table by verdict: one row per integration pair, plus one Risky row per
29
+ circular dependency. --cycles names the JSON skf-find-cycles.py wrote
30
+ ({"cycles": [[...], ...], ...}); each cycle is one more Risky row, so it
31
+ counts toward `pairs_risky` and the frontmatter's `pairsRisky` matches the
32
+ table. Pass the pair rows only: a cycle row in `rows` is counted twice.
33
+
34
+ requirements (requirements.md §3, persisted in §5)
35
+ Counts Fulfilled, Partially Fulfilled and Not Addressed.
36
+
37
+ --render (coverage and integrations)
38
+ Prints the report table's rows from the same rows instead of the counts,
39
+ one Markdown table row per line and nothing else, so no stage types a row
40
+ by hand: coverage.md §5 shows the coverage rows and §6 writes them under
41
+ `| Technology | Source Section | Skill Match | Verdict |`, and
42
+ integrations.md §6 writes the integration rows under the template's
43
+ canonical `| lib_a | lib_b | verdict | rationale |` header. The input is
44
+ checked as for the counts first. A coverage row gives its Source Section
45
+ and Skill Match from `section` and `skill` (`none` when null).
46
+ An integration row needs its `rationale`; after the pair rows comes one row
47
+ per cycle --cycles lists: `cycle`, the cycle's arrow chain (`A → B → C →
48
+ A`), Risky and "circular integration dependency detected: `<chain>`", so
49
+ the delta, which keys rows on their two libraries, never merges a cycle
50
+ into a pair. A cell holds its text on one line, each `|` behind a backslash.
51
+ With no row, nothing is printed.
52
+
24
53
  CLI usage:
25
54
  uv run skf-coverage-tally.py '<JSON>' # JSON literal positional
26
55
  uv run skf-coverage-tally.py --json-input '<JSON>' # explicit flag form
27
56
  cat input.json | uv run skf-coverage-tally.py --stdin # piped input
57
+ uv run skf-coverage-tally.py --kind integrations --cycles cycles.json --stdin < rows.json
58
+ uv run skf-coverage-tally.py --kind requirements --stdin < rows.json
59
+ uv run skf-coverage-tally.py --render --stdin < coverage-rows.json
60
+ uv run skf-coverage-tally.py --kind integrations --render --cycles cycles.json --stdin < rows.json
61
+
62
+ Input schema (one object), by kind:
63
+ coverage:
64
+ {"rows": [{"technology": "react", "verdict": "Covered"},
65
+ {"technology": "postgres","verdict": "Missing"},
66
+ {"technology": "old-orm", "verdict": "Replaced"}]}
67
+ Each technology once (compared trimmed and case-insensitively). A row may
68
+ carry "section" and "skill", each a string or null (--render reads an
69
+ absent one as null). A Covered row names its skill, a Missing or Replaced
70
+ one none: --render checks that on every row and the counts on each row
71
+ that carries "skill", so a rows file the tally accepts also renders.
72
+ integrations:
73
+ {"rows": [{"lib_a": "react", "lib_b": "zod", "verdict": "Verified",
74
+ "rationale": "..."}, ...]}
75
+ verdict: Verified, Plausible, Risky or Blocked (case-sensitive). Each
76
+ pair once, in either order, and never a library paired with itself.
77
+ rationale: the table's rationale cell, which --render requires.
78
+ requirements:
79
+ {"rows": [{"requirement_id": "R1", "verdict": "Fulfilled"}, ...]}
80
+ verdict: Fulfilled, Partially Fulfilled or Not Addressed (case-sensitive).
81
+ Each requirement_id once.
82
+
83
+ Output (stdout, one object), by kind:
84
+ coverage:
85
+ {
86
+ "covered_count": <int>,
87
+ "missing_count": <int>,
88
+ "replaced_count": <int>,
89
+ "live_count": <int>, # Covered + Missing (denominator)
90
+ "total_referenced": <int>, # all rows after dedup
91
+ "coverage_percentage": <int> # round-half-up(covered/live*100); 0 when live==0
92
+ }
93
+ integrations:
94
+ {
95
+ "pairs_verified": <int>,
96
+ "pairs_plausible": <int>,
97
+ "pairs_risky": <int>, # Risky pair rows + cycle_count
98
+ "pairs_blocked": <int>,
99
+ "pair_count": <int>, # the pair rows
100
+ "cycle_count": <int>, # the cycles --cycles lists (0 without it)
101
+ "row_count": <int> # pair_count + cycle_count: the table's rows
102
+ }
103
+ requirements:
104
+ {
105
+ "requirements_fulfilled": <int>,
106
+ "requirements_partial": <int>,
107
+ "requirements_not_addressed": <int>,
108
+ "requirement_count": <int>
109
+ }
28
110
 
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
- }
111
+ --render: the rows as Markdown, one per line, in the order given (the
112
+ cycle rows last), with no JSON.
113
+
114
+ Errors (stdout, one object): {"error": "<why>", "code": "INVALID_INPUT"}.
47
115
 
48
116
  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)
117
+ 0 tally (or, with --render, the rows) emitted successfully
118
+ 1 no input, input that is not JSON, or a --cycles file that cannot be read
119
+ or is not JSON
120
+ 2 input parsed but its schema or semantics are invalid (error object
121
+ emitted as JSON); argparse usage errors, such as --cycles with another
122
+ kind or --render with requirements, exit 2 too, with usage on stderr and
123
+ no JSON
52
124
  """
53
125
 
54
126
  from __future__ import annotations
@@ -57,37 +129,104 @@ import argparse
57
129
  import json
58
130
  import math
59
131
  import sys
132
+ from pathlib import Path
60
133
 
61
134
  VALID_VERDICTS = ("Covered", "Missing", "Replaced")
135
+ INTEGRATION_VERDICTS = ("Verified", "Plausible", "Risky", "Blocked")
136
+ REQUIREMENT_VERDICTS = ("Fulfilled", "Partially Fulfilled", "Not Addressed")
137
+ KINDS = ("coverage", "integrations", "requirements")
138
+ # The kinds whose report table --render prints.
139
+ RENDER_KINDS = ("coverage", "integrations")
140
+ # The Source Section or Skill Match of a coverage row that names none.
141
+ NO_CELL = "none"
142
+ # A cycle row's lib_a, and how its lib_b chains the cycle's nodes.
143
+ CYCLE_LABEL = "cycle"
144
+ CYCLE_ARROW = " → "
145
+ CYCLE_RATIONALE = "circular integration dependency detected: `{chain}`"
62
146
 
63
147
 
64
148
  def make_error(message):
65
149
  return {"error": message, "code": "INVALID_INPUT"}
66
150
 
67
151
 
68
- def _validate(inp):
152
+ def _rows(inp):
153
+ """The `rows` list of an input object, or an error message."""
69
154
  if inp is None or not isinstance(inp, dict):
70
- return "Input must be a JSON object"
155
+ return None, "Input must be a JSON object"
71
156
  rows = inp.get("rows")
72
157
  if not isinstance(rows, list):
73
- return "Missing or invalid required field: rows (must be a list)"
74
- seen = set()
158
+ return None, "Missing or invalid required field: rows (must be a list)"
75
159
  for i, row in enumerate(rows):
76
160
  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()
161
+ return None, f"rows[{i}] must be an object"
162
+ return rows, None
163
+
164
+
165
+ def _name(row, i, key):
166
+ """A row's trimmed, non-empty string `key`, or an error message."""
167
+ value = row.get(key)
168
+ if not isinstance(value, str) or not value.strip():
169
+ return None, f"rows[{i}] requires a non-empty string `{key}`"
170
+ return value.strip(), None
171
+
172
+
173
+ def _verdict(row, i, allowed):
174
+ verdict = row.get("verdict")
175
+ if verdict not in allowed:
176
+ return f"rows[{i}] verdict {verdict!r} is not one of: {', '.join(allowed)}"
177
+ return None
178
+
179
+
180
+ def _optional_text(row, i, key):
181
+ """A row's optional string `key`, trimmed (None when absent, null or blank), or an error message."""
182
+ value = row.get(key)
183
+ if value is None:
184
+ return None, None
185
+ if not isinstance(value, str):
186
+ return None, f"rows[{i}] `{key}` must be a string or null"
187
+ return value.strip() or None, None
188
+
189
+
190
+ def _cells(row, i, render):
191
+ """A coverage row's Source Section and Skill Match (None for none), or an error message.
192
+
193
+ A Covered row names its skill, a Missing or Replaced one none. The pairing
194
+ is checked whenever the row carries `skill` (the rows file coverage.md §3
195
+ writes always does), so the tally refuses a row --render would refuse.
196
+ """
197
+ section, err = _optional_text(row, i, "section")
198
+ if err:
199
+ return None, None, err
200
+ skill, err = _optional_text(row, i, "skill")
201
+ if err:
202
+ return None, None, err
203
+ if (render or "skill" in row) and (row["verdict"] == "Covered") != (skill is not None):
204
+ return None, None, (
205
+ f"rows[{i}] is {row['verdict']} but names {'no' if skill is None else 'a'} skill: "
206
+ "a Covered row names its skill, a Missing or Replaced one none"
207
+ )
208
+ return section, skill, None
209
+
210
+
211
+ def _validate(inp, render=False):
212
+ rows, err = _rows(inp)
213
+ if err:
214
+ return err
215
+ seen = set()
216
+ for i, row in enumerate(rows):
217
+ tech, err = _name(row, i, "technology")
218
+ if err:
219
+ return err
220
+ err = _verdict(row, i, VALID_VERDICTS)
221
+ if err:
222
+ return err
223
+ norm = tech.lower()
88
224
  if norm in seen:
89
- return f"duplicate technology {tech!r} — the coverage list must be deduplicated first"
225
+ return f"duplicate technology {row['technology']!r}: the coverage list must be deduplicated first"
90
226
  seen.add(norm)
227
+ _, _, err = _cells(row, i, render)
228
+ if err:
229
+ return err
91
230
  return None
92
231
 
93
232
 
@@ -119,6 +258,137 @@ def tally(inp):
119
258
  }
120
259
 
121
260
 
261
+ def _cycle_count(cycles):
262
+ """The number of cycles in a skf-find-cycles.py result, or an error message.
263
+
264
+ Each cycle is a closed node path: at least two non-empty names, the first
265
+ repeated at the end (a self-loop is ["A", "A"]).
266
+ """
267
+ if not isinstance(cycles, dict) or not isinstance(cycles.get("cycles"), list):
268
+ return None, "--cycles must hold the skf-find-cycles.py result, an object with a `cycles` list"
269
+ for i, cycle in enumerate(cycles["cycles"]):
270
+ if (
271
+ not isinstance(cycle, list)
272
+ or len(cycle) < 2
273
+ or not all(isinstance(node, str) and node.strip() for node in cycle)
274
+ or cycle[0] != cycle[-1]
275
+ ):
276
+ return None, f"--cycles cycles[{i}] is not a closed node path such as [\"A\", \"B\", \"A\"]: {cycle!r}"
277
+ return len(cycles["cycles"]), None
278
+
279
+
280
+ def tally_integrations(inp, cycles=None):
281
+ """Count the canonical verdict table's rows: the pair rows, and one Risky row per cycle."""
282
+ rows, err = _rows(inp)
283
+ if err:
284
+ return make_error(err)
285
+ counts = {verdict: 0 for verdict in INTEGRATION_VERDICTS}
286
+ seen = set()
287
+ for i, row in enumerate(rows):
288
+ lib_a, err = _name(row, i, "lib_a")
289
+ if err:
290
+ return make_error(err)
291
+ lib_b, err = _name(row, i, "lib_b")
292
+ if err:
293
+ return make_error(err)
294
+ err = _verdict(row, i, INTEGRATION_VERDICTS)
295
+ if err:
296
+ return make_error(err)
297
+ pair = frozenset((lib_a.lower(), lib_b.lower()))
298
+ if len(pair) == 1:
299
+ return make_error(f"rows[{i}] pairs {lib_a!r} with itself")
300
+ if pair in seen:
301
+ return make_error(f"duplicate pair {lib_a!r} / {lib_b!r}: list each integration pair once")
302
+ seen.add(pair)
303
+ counts[row["verdict"]] += 1
304
+
305
+ cycle_count = 0
306
+ if cycles is not None:
307
+ cycle_count, err = _cycle_count(cycles)
308
+ if err:
309
+ return make_error(err)
310
+ return {
311
+ "pairs_verified": counts["Verified"],
312
+ "pairs_plausible": counts["Plausible"],
313
+ "pairs_risky": counts["Risky"] + cycle_count,
314
+ "pairs_blocked": counts["Blocked"],
315
+ "pair_count": len(rows),
316
+ "cycle_count": cycle_count,
317
+ "row_count": len(rows) + cycle_count,
318
+ }
319
+
320
+
321
+ def tally_requirements(inp):
322
+ """Count Fulfilled, Partially Fulfilled and Not Addressed requirements."""
323
+ rows, err = _rows(inp)
324
+ if err:
325
+ return make_error(err)
326
+ counts = {verdict: 0 for verdict in REQUIREMENT_VERDICTS}
327
+ seen = set()
328
+ for i, row in enumerate(rows):
329
+ requirement, err = _name(row, i, "requirement_id")
330
+ if err:
331
+ return make_error(err)
332
+ err = _verdict(row, i, REQUIREMENT_VERDICTS)
333
+ if err:
334
+ return make_error(err)
335
+ if requirement.lower() in seen:
336
+ return make_error(f"duplicate requirement_id {requirement!r}: list each requirement once")
337
+ seen.add(requirement.lower())
338
+ counts[row["verdict"]] += 1
339
+ return {
340
+ "requirements_fulfilled": counts["Fulfilled"],
341
+ "requirements_partial": counts["Partially Fulfilled"],
342
+ "requirements_not_addressed": counts["Not Addressed"],
343
+ "requirement_count": len(rows),
344
+ }
345
+
346
+
347
+ # --- Rendering the report rows ------------------------------------------------
348
+
349
+
350
+ def _cell(value):
351
+ """One table cell: its text on one line, a pipe escaped so it stays in the cell."""
352
+ return " ".join(str(value).split()).replace("|", "\\|")
353
+
354
+
355
+ def _table_row(cells):
356
+ return "| " + " | ".join(_cell(cell) for cell in cells) + " |"
357
+
358
+
359
+ def render_coverage(inp):
360
+ """The coverage table's rows (Technology, Source Section, Skill Match, Verdict), or an error object."""
361
+ err = _validate(inp, render=True)
362
+ if err:
363
+ return make_error(err)
364
+ lines = []
365
+ for i, row in enumerate(inp["rows"]):
366
+ section, skill, _ = _cells(row, i, render=True)
367
+ lines.append(_table_row([row["technology"].strip(), section or NO_CELL, skill or NO_CELL, row["verdict"]]))
368
+ return lines
369
+
370
+
371
+ def render_integrations(inp, cycles=None):
372
+ """The canonical verdict table's rows (lib_a, lib_b, verdict, rationale), then one per cycle.
373
+
374
+ The rows and --cycles are checked as tally_integrations() checks them;
375
+ an error object comes back on the first problem.
376
+ """
377
+ counted = tally_integrations(inp, cycles)
378
+ if "error" in counted:
379
+ return counted
380
+ lines = []
381
+ for i, row in enumerate(inp["rows"]):
382
+ rationale, err = _name(row, i, "rationale")
383
+ if err:
384
+ return make_error(err)
385
+ lines.append(_table_row([row["lib_a"].strip(), row["lib_b"].strip(), row["verdict"], rationale]))
386
+ for cycle in (cycles or {}).get("cycles", []):
387
+ chain = CYCLE_ARROW.join(node.strip() for node in cycle)
388
+ lines.append(_table_row([CYCLE_LABEL, chain, "Risky", CYCLE_RATIONALE.format(chain=chain)]))
389
+ return lines
390
+
391
+
122
392
  # --- CLI --------------------------------------------------------------------
123
393
 
124
394
 
@@ -126,16 +396,25 @@ def _build_parser():
126
396
  parser = argparse.ArgumentParser(
127
397
  prog="skf-coverage-tally",
128
398
  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."
399
+ "Deterministic verdict tallies for skf-verify-stack. --kind coverage "
400
+ "(the default, coverage.md §3) counts Covered/Missing/Replaced verdicts "
401
+ "and computes coveragePercentage with Replaced excluded from the "
402
+ "denominator and half-up rounding. --kind integrations (integrations.md "
403
+ "§4, persisted in §6) counts the verdict table's pair rows, plus one "
404
+ "Risky row per cycle the --cycles file lists. --kind requirements "
405
+ "(requirements.md §3, persisted in §5) counts Fulfilled/Partially "
406
+ "Fulfilled/Not Addressed. --render prints the coverage or integration "
407
+ "report rows instead of the counts."
132
408
  ),
133
409
  formatter_class=argparse.RawDescriptionHelpFormatter,
134
410
  epilog=(
135
- "Example:\n"
411
+ "Examples:\n"
136
412
  " uv run skf-coverage-tally.py "
137
413
  "'{\"rows\":[{\"technology\":\"react\",\"verdict\":\"Covered\"},"
138
- "{\"technology\":\"postgres\",\"verdict\":\"Missing\"}]}'"
414
+ "{\"technology\":\"postgres\",\"verdict\":\"Missing\"}]}'\n"
415
+ " uv run skf-coverage-tally.py --kind integrations --cycles cycles.json "
416
+ "--stdin < rows.json\n"
417
+ " uv run skf-coverage-tally.py --render --stdin < coverage-rows.json"
139
418
  ),
140
419
  )
141
420
  src = parser.add_mutually_exclusive_group()
@@ -154,6 +433,28 @@ def _build_parser():
154
433
  action="store_true",
155
434
  help="Read the JSON object from stdin.",
156
435
  )
436
+ parser.add_argument(
437
+ "--kind",
438
+ choices=KINDS,
439
+ default="coverage",
440
+ help="Which rows to count (default: coverage).",
441
+ )
442
+ parser.add_argument(
443
+ "--cycles",
444
+ metavar="FILE",
445
+ help=(
446
+ "With --kind integrations: the JSON skf-find-cycles.py wrote; each "
447
+ "cycle counts as one more Risky row (with --render, one more row)."
448
+ ),
449
+ )
450
+ parser.add_argument(
451
+ "--render",
452
+ action="store_true",
453
+ help=(
454
+ "With --kind coverage or integrations: print the report table's rows, "
455
+ "one Markdown row per line, instead of the counts."
456
+ ),
457
+ )
157
458
  return parser
158
459
 
159
460
 
@@ -167,9 +468,23 @@ def _resolve_input(args):
167
468
  return ""
168
469
 
169
470
 
471
+ def _read_cycles(path):
472
+ """The parsed --cycles file, or (None, error) when it cannot be read or is not JSON."""
473
+ try:
474
+ return json.loads(Path(path).read_text(encoding="utf-8")), None
475
+ except (OSError, UnicodeDecodeError) as exc:
476
+ return None, f"cannot read the --cycles file {path}: {exc}"
477
+ except json.JSONDecodeError as exc:
478
+ return None, f"the --cycles file {path} is not JSON: {exc.msg}"
479
+
480
+
170
481
  def main(argv=None):
171
482
  parser = _build_parser()
172
483
  args = parser.parse_args(argv)
484
+ if args.cycles is not None and args.kind != "integrations":
485
+ parser.error("--cycles applies only with --kind integrations")
486
+ if args.render and args.kind not in RENDER_KINDS:
487
+ parser.error("--render applies only with --kind coverage or integrations")
173
488
  raw = _resolve_input(args)
174
489
  if not raw.strip():
175
490
  parser.print_usage(file=sys.stderr)
@@ -185,12 +500,43 @@ def main(argv=None):
185
500
  print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
186
501
  return 1
187
502
 
188
- result = tally(data)
503
+ if args.kind == "integrations":
504
+ cycles = None
505
+ if args.cycles is not None:
506
+ cycles, err = _read_cycles(args.cycles)
507
+ if err:
508
+ print(json.dumps(make_error(err), indent=2))
509
+ return 1
510
+ result = (render_integrations if args.render else tally_integrations)(data, cycles)
511
+ elif args.kind == "requirements":
512
+ result = tally_requirements(data)
513
+ else:
514
+ result = (render_coverage if args.render else tally)(data)
515
+ if isinstance(result, list):
516
+ sys.stdout.write("".join(line + "\n" for line in result))
517
+ return 0
189
518
  print(json.dumps(result, indent=2))
190
519
  if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
191
520
  return 2
192
521
  return 0
193
522
 
194
523
 
524
+ def _force_utf8(*streams) -> None:
525
+ """Reconfigure the given streams to UTF-8, keeping each stream's error handler.
526
+
527
+ A Windows console pipes them as cp1252, which cannot print every character
528
+ a --help text or an error message may hold, and which garbles the UTF-8 row
529
+ files the stages redirect into --stdin.
530
+ """
531
+ for stream in streams:
532
+ if hasattr(stream, "reconfigure"):
533
+ errors = getattr(stream, "errors", None)
534
+ if errors is None:
535
+ stream.reconfigure(encoding="utf-8")
536
+ else:
537
+ stream.reconfigure(encoding="utf-8", errors=errors)
538
+
539
+
195
540
  if __name__ == "__main__":
541
+ _force_utf8(sys.stdin, sys.stdout, sys.stderr)
196
542
  raise SystemExit(main())