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,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.10"
3
+ # requires-python = ">=3.11"
4
4
  # ///
5
5
  """Deterministic Documented-vs-Source coverage reconciliation.
6
6
 
@@ -12,28 +12,35 @@ already-extracted name lists (the §1 documented inventory and the §2 source
12
12
  barrel) into a reproducible {documented, missing, stale, exportCoverage}
13
13
  result, so the numerator no longer swings between runs on split-body skills.
14
14
 
15
- It covers the three branches §2c enumerates:
15
+ It covers the four branches §2c enumerates:
16
16
 
17
17
  * "barrel" (enumerated path) — intersect documented_set against the source
18
18
  barrel_set: Documented = |documented_set ∩ barrel_set|,
19
19
  Missing = barrel_set − documented_set, Stale = documented_set − barrel_set,
20
20
  Export Coverage = |Documented| / |barrel_set| * 100.
21
21
 
22
- * "scalar" (§4 priority-1 effective_denominator, no enumerated name set) —
23
- grep each documented name across SKILL.md ∪ references/*.md;
22
+ * "scalar" (§2b priority-1 effective_denominator, no enumerated name set):
23
+ look each documented name up in SKILL.md ∪ references/*.md;
24
24
  Documented = count present, denominator = effective_denominator,
25
25
  Missing = max(0, denominator − Documented), Stale not enumerable (empty),
26
26
  Export Coverage = min(100, Documented / denominator * 100).
27
27
 
28
- * "stack" (skill_type == "stack", empty source barrel) — grep each
28
+ * "stack" (skill_type == "stack", empty source barrel): look each
29
29
  composition-surface name (provenance-map cited contracts, ::-excluded, or
30
- libraries + integration_pairs — resolved upstream) across
30
+ libraries + integration_pairs, from load-coverage-inputs.py) up in
31
31
  SKILL.md ∪ references/*.md; denominator = stack_denominator,
32
32
  Missing = max(0, denominator − Documented), Stale not enumerable,
33
33
  Export Coverage = min(100, Documented / stack_denominator * 100).
34
34
 
35
- The two grep branches bound their outputs because the numerator and the
36
- denominator are independent measures of different sets — the grep count can
35
+ * "docsOnly" (a skill whose citations are all [EXT:...], or no source
36
+ access): no source to compare against, so Export Coverage measures
37
+ documentation completeness: complete items / documented items * 100,
38
+ one item per name and kind of the §1 inventory, complete when it has a
39
+ description and, for a function or method, params and a return type
40
+ (validate-inventory.py's rule).
41
+
42
+ The two lookup branches bound their outputs because the numerator and the
43
+ denominator are independent measures of different sets: the lookup count can
37
44
  exceed a consumer-surface denominator without either being wrong. Unbounded,
38
45
  that produced a negative Missing and a >100% coverage that compute-score.py
39
46
  rejects as out of range. `documented` is left as the true count (it is reported
@@ -41,54 +48,101 @@ verbatim as "Documented in SKILL.md"); the derived ratio is what gets bounded,
41
48
  and `numeratorSurplus` / `coverageUncapped` / `coverageCapped` report the
42
49
  overshoot so a deflated denominator stays visible instead of being swallowed.
43
50
 
51
+ A name counts as written when SKILL.md or a .md file directly in references/
52
+ holds it as a fixed string, case-sensitive, never inside a longer identifier
53
+ (`get` is not found in `target` or `getAll`): the rule validate-inventory.py
54
+ defines, which this script loads from beside it.
55
+
44
56
  CLI usage:
57
+ uv run reconcile-coverage.py --denominator-source <source> [file flags] --output <file>
45
58
  uv run reconcile-coverage.py '<JSON>' # JSON literal positional
46
59
  uv run reconcile-coverage.py --json-input '<JSON>' # explicit flag form
47
60
  cat input.json | uv run reconcile-coverage.py --stdin # piped input
48
61
 
62
+ File flags read the inputs from the run folder instead of a JSON payload;
63
+ each sets the input field it names and wins over the same field in the JSON:
64
+ --denominator-source denominatorSource
65
+ --denominator-value denominatorValue (scalar), when no --coverage-inputs
66
+ file gives it
67
+ --skill-dir skillPackagePath (scalar, stack)
68
+ --inventory PATH exports: the validated inventory validate-inventory.py
69
+ wrote with --output
70
+ --surface PATH barrelSet: one name set of load-coverage-inputs.py
71
+ surface, chosen by --surface-set (default "all")
72
+ --coverage-inputs PATH load-coverage-inputs.py metadata output: for the
73
+ scalar branch, denominatorValue from its
74
+ `effectiveDenominator` (stats.effective_denominator);
75
+ for the stack branch, compositionNames and
76
+ denominatorValue from its `stack` object
77
+ --verified PATH scalar: verify-declared-numerator.py's result; when it
78
+ found the declared count inflated, its `verified`
79
+ count is the numerator
80
+ --output PATH also write the result to this file (UTF-8 JSON); a
81
+ refused input removes a file left there earlier
82
+
49
83
  Input schema (one object):
50
84
  {
51
- "denominatorSource": "barrel" | "scalar" | "stack", # required
85
+ "denominatorSource": "barrel" | "scalar" | "stack" | "docsOnly", # required
52
86
  "exports": [ {"name": "...", "kind": "..."}, ... ], # §1 inventory
53
87
  "barrelSet": ["a", "b", ...], # barrel: resolved name set
54
88
  "perFileResults": [ {"exports_found": ["a", ...]} ], # barrel: union'd if no barrelSet
55
89
  "denominatorValue": <int>, # scalar/stack: resolved denominator
56
- "compositionNames": ["lib::x", ...], # stack: names to grep
57
- "skillPackagePath": "/path/to/skill" # scalar/stack: SKILL.md ∪ references
90
+ "compositionNames": ["lib::x", ...], # stack: names to look up
91
+ "skillPackagePath": "/path/to/skill", # scalar/stack: SKILL.md ∪ references
92
+ "verifiedNumerator": <int> # scalar: overrides the lookup count
58
93
  }
59
94
 
60
95
  Output (stdout, one object):
61
96
  {
62
- "branch": "enumerated" | "scalar" | "stack",
63
- "denominatorSource": "barrel" | "scalar" | "stack",
97
+ "branch": "enumerated" | "scalar" | "stack" | "docsOnly",
98
+ "denominatorSource": "barrel" | "scalar" | "stack" | "docsOnly",
64
99
  "denominator": <int>,
65
100
  "documented": <int>,
66
- "missing": [names], # enumerated: source names not documented; scalar/stack: []
101
+ "missing": [names], # enumerated: source names not documented; others: []
67
102
  "missingCount": <int>, # always present; never negative
68
- "stale": [names], # enumerated: documented names not in source; scalar/stack: []
103
+ "stale": [names], # enumerated: documented names not in source; others: []
69
104
  "staleCount": <int>, # always present
70
- "staleApplicable": <bool>, # false for scalar/stack (no barrel to enumerate)
105
+ "staleApplicable": <bool>, # false for scalar/stack/docsOnly (no barrel to enumerate)
71
106
  "exportCoverage": <float>, # scalar/stack: capped at 100
72
107
  "numeratorSurplus": <int>, # scalar/stack only: max(0, documented − denominator)
73
108
  "coverageUncapped": <float>, # scalar/stack only: the ratio before the cap
74
- "coverageCapped": <bool> # scalar/stack only: true when the cap bound the ratio
109
+ "coverageCapped": <bool>, # scalar/stack only: true when the cap bound the ratio
110
+ "numeratorSource": "lookup" | "verified", # scalar only
111
+ "incomplete": [{"name", "kind", "missing"}] # docsOnly only: the incomplete items
75
112
  }
76
113
 
77
114
  Exit codes:
78
- 0 — reconciliation emitted successfully
79
- 1 — no input / input could not be parsed as JSON
80
- 2 — input parsed but schema/semantics invalid (error object emitted as JSON)
115
+ 0 reconciliation emitted successfully
116
+ 1 no input, input that does not parse as JSON, or an input file that cannot be read
117
+ 2 input parsed but schema/semantics invalid (error object emitted as JSON)
81
118
  """
82
119
 
83
120
  from __future__ import annotations
84
121
 
85
122
  import argparse
123
+ import importlib.util
86
124
  import json
87
125
  import math
88
126
  import sys
89
127
  from pathlib import Path
90
128
 
91
- VALID_SOURCES = ("barrel", "scalar", "stack")
129
+ VALID_SOURCES = ("barrel", "scalar", "stack", "docsOnly")
130
+
131
+ _INVENTORY_MODULE = None
132
+
133
+
134
+ def _inventory():
135
+ """validate-inventory.py beside this script: the name match and completeness rules live there."""
136
+ global _INVENTORY_MODULE
137
+ if _INVENTORY_MODULE is None:
138
+ sibling = Path(__file__).resolve().parent / "validate-inventory.py"
139
+ spec = importlib.util.spec_from_file_location("skf_validate_inventory", sibling)
140
+ if spec is None or spec.loader is None or not sibling.is_file():
141
+ raise ImportError(f"validate-inventory.py not found beside {Path(__file__).name}")
142
+ module = importlib.util.module_from_spec(spec)
143
+ spec.loader.exec_module(module)
144
+ _INVENTORY_MODULE = module
145
+ return _INVENTORY_MODULE
92
146
 
93
147
 
94
148
  def round2(value):
@@ -135,34 +189,17 @@ def build_barrel_set(inp):
135
189
  return barrel
136
190
 
137
191
 
138
- # --- Grep (documented-name presence) ---------------------------------------
192
+ # --- Documented-name lookup ---------------------------------------------------
139
193
 
140
194
 
141
195
  def load_doc_text(skill_package_path):
142
- """Concatenate SKILL.md ∪ references/*.md text for name-presence grepping.
143
-
144
- Deterministic: references are read in sorted order. Reads as UTF-8 so
145
- non-ASCII exports do not mojibake on Windows (cp1252 default).
146
- """
147
- root = Path(skill_package_path)
148
- parts = []
149
- skill_md = root / "SKILL.md"
150
- if skill_md.is_file():
151
- parts.append(skill_md.read_text(encoding="utf-8"))
152
- refs_dir = root / "references"
153
- if refs_dir.is_dir():
154
- for ref in sorted(refs_dir.glob("*.md")):
155
- parts.append(ref.read_text(encoding="utf-8"))
156
- return "\n".join(parts)
196
+ """SKILL.md ∪ references/*.md text, as validate-inventory.py reads it."""
197
+ return _inventory().load_doc_text(skill_package_path)
157
198
 
158
199
 
159
200
  def names_present(names, doc_text):
160
- """Return the sorted, de-duplicated set of `names` that appear in doc_text.
161
-
162
- Substring containment, case-sensitive — mirrors `grep "{name}"`.
163
- """
164
- present = {n for n in names if isinstance(n, str) and n and n in doc_text}
165
- return sorted(present)
201
+ """The sorted, de-duplicated `names` the doc text writes (validate-inventory.py's match)."""
202
+ return _inventory().names_present(names, doc_text)
166
203
 
167
204
 
168
205
  # --- Core reconciliation ----------------------------------------------------
@@ -180,17 +217,46 @@ def _validate(inp):
180
217
  if source == "barrel":
181
218
  if inp.get("barrelSet") is None and inp.get("perFileResults") is None:
182
219
  return "barrel branch requires either `barrelSet` or `perFileResults`"
220
+ elif source == "docsOnly":
221
+ if not isinstance(inp.get("exports"), list):
222
+ return "docsOnly branch requires the §1 inventory `exports` (or --inventory)"
183
223
  else: # scalar / stack
184
224
  dv = inp.get("denominatorValue")
185
225
  if not isinstance(dv, int) or isinstance(dv, bool) or dv < 0:
186
226
  return f"{source} branch requires integer `denominatorValue` >= 0"
187
227
  if not inp.get("skillPackagePath"):
188
- return f"{source} branch requires `skillPackagePath` to grep SKILL.md ∪ references"
228
+ return f"{source} branch requires `skillPackagePath` to read SKILL.md ∪ references"
189
229
  if source == "stack" and inp.get("compositionNames") is None:
190
- return "stack branch requires `compositionNames` (composition-surface names to grep)"
230
+ return "stack branch requires `compositionNames` (composition-surface names to look up)"
231
+ verified = inp.get("verifiedNumerator")
232
+ if verified is not None and (not isinstance(verified, int) or isinstance(verified, bool) or verified < 0):
233
+ return "verifiedNumerator must be an integer >= 0"
191
234
  return None
192
235
 
193
236
 
237
+ def _docs_only(inp):
238
+ tally = _inventory().completeness(inp["exports"])
239
+ total = tally["total"]
240
+ if total == 0:
241
+ return make_error(
242
+ "docsOnly branch: the inventory documents no item, so Export Coverage is "
243
+ "undefined; the §2b docs-only guard should HALT"
244
+ )
245
+ return {
246
+ "branch": "docsOnly",
247
+ "denominatorSource": "docsOnly",
248
+ "denominator": total,
249
+ "documented": tally["complete"],
250
+ "missing": [],
251
+ "missingCount": total - tally["complete"],
252
+ "stale": [],
253
+ "staleCount": 0,
254
+ "staleApplicable": False,
255
+ "exportCoverage": round2(tally["complete"] / total * 100),
256
+ "incomplete": tally["incomplete"],
257
+ }
258
+
259
+
194
260
  def reconcile(inp, doc_text=None):
195
261
  """Pure reconciliation. `doc_text` may be injected (tests); else loaded from disk."""
196
262
  err = _validate(inp)
@@ -199,6 +265,9 @@ def reconcile(inp, doc_text=None):
199
265
 
200
266
  source = inp["denominatorSource"]
201
267
 
268
+ if source == "docsOnly":
269
+ return _docs_only(inp)
270
+
202
271
  if source == "barrel":
203
272
  documented_set = build_documented_set(inp.get("exports"))
204
273
  barrel_set = build_barrel_set(inp)
@@ -225,39 +294,43 @@ def reconcile(inp, doc_text=None):
225
294
  "exportCoverage": round2(documented / denominator * 100),
226
295
  }
227
296
 
228
- # scalar / stack — grep-based numerator against SKILL.md ∪ references
297
+ # scalar / stack: looked-up numerator against SKILL.md ∪ references
229
298
  denominator = inp["denominatorValue"]
230
- if doc_text is None:
231
- doc_text = load_doc_text(inp["skillPackagePath"])
232
-
233
- if source == "scalar":
234
- candidates = sorted(build_documented_set(inp.get("exports")))
235
- else: # stack
236
- candidates = [n for n in inp["compositionNames"] if isinstance(n, str) and n]
237
-
238
- present = names_present(candidates, doc_text)
239
- documented = len(present)
240
-
241
299
  if denominator == 0:
242
300
  return make_error(
243
301
  f"{source} branch: denominatorValue is 0 — Export Coverage is undefined; "
244
302
  "upstream §2b guard should HALT before reconciliation"
245
303
  )
246
304
 
247
- # The grep numerator and the resolved denominator are independent measures,
248
- # so `documented` can legitimately exceed `denominator` — a consumer-surface
249
- # denominator counts one surface while the documented body may also name
250
- # migration aliases, re-exported sibling symbols, and other extras. Left
251
- # unbounded that yields a negative Missing and >100% coverage (which
252
- # compute-score.py then rejects outright as out of range).
305
+ numerator_source = "lookup"
306
+ if source == "scalar" and inp.get("verifiedNumerator") is not None:
307
+ # §2b's numerator ground truth found the declared count padded: its
308
+ # verified count is authoritative and replaces the lookup.
309
+ documented = inp["verifiedNumerator"]
310
+ numerator_source = "verified"
311
+ else:
312
+ if doc_text is None:
313
+ doc_text = load_doc_text(inp["skillPackagePath"])
314
+ if source == "scalar":
315
+ candidates = sorted(build_documented_set(inp.get("exports")))
316
+ else: # stack
317
+ candidates = [n for n in inp["compositionNames"] if isinstance(n, str) and n]
318
+ documented = len(names_present(candidates, doc_text))
319
+
320
+ # The looked-up numerator and the resolved denominator are independent
321
+ # measures, so `documented` can legitimately exceed `denominator`: a
322
+ # consumer-surface denominator counts one surface while the documented body
323
+ # may also name migration aliases, re-exported sibling symbols, and other
324
+ # extras. Left unbounded that yields a negative Missing and >100% coverage
325
+ # (which compute-score.py then rejects outright as out of range).
253
326
  #
254
- # `documented` stays the true grep count: it is reported as "Documented in
327
+ # `documented` stays the true count: it is reported as "Documented in
255
328
  # SKILL.md", so capping it would print a number that was never measured.
256
329
  # The derived ratio is what gets bounded, and the surplus is surfaced rather
257
330
  # than swallowed so a deflated denominator stays visible.
258
331
  surplus = max(0, documented - denominator)
259
332
  coverage_uncapped = round2(documented / denominator * 100)
260
- return {
333
+ result = {
261
334
  "branch": source,
262
335
  "denominatorSource": source,
263
336
  "denominator": denominator,
@@ -272,6 +345,73 @@ def reconcile(inp, doc_text=None):
272
345
  "coverageUncapped": coverage_uncapped,
273
346
  "coverageCapped": surplus > 0,
274
347
  }
348
+ if source == "scalar":
349
+ result["numeratorSource"] = numerator_source
350
+ return result
351
+
352
+
353
+ # --- Run-folder inputs --------------------------------------------------------
354
+
355
+
356
+ class InputFileError(Exception):
357
+ """A file flag names a file that cannot be read or does not hold what it should."""
358
+
359
+
360
+ def _read_json_file(path, flag):
361
+ try:
362
+ return json.loads(Path(path).read_bytes().decode("utf-8-sig"))
363
+ except (OSError, UnicodeDecodeError) as exc:
364
+ raise InputFileError(f"cannot read {flag} {path}: {exc}") from exc
365
+ except json.JSONDecodeError as exc:
366
+ raise InputFileError(f"{flag} {path} is not JSON: {exc.msg}") from exc
367
+
368
+
369
+ def apply_file_inputs(inp, args):
370
+ """Fill the input object from the file flags; each flag wins over the JSON field."""
371
+ if args.denominator_source is not None:
372
+ inp["denominatorSource"] = args.denominator_source
373
+ if args.denominator_value is not None:
374
+ inp["denominatorValue"] = args.denominator_value
375
+ if args.skill_dir is not None:
376
+ inp["skillPackagePath"] = args.skill_dir
377
+ if args.inventory is not None:
378
+ data = _read_json_file(args.inventory, "--inventory")
379
+ if isinstance(data, dict) and isinstance(data.get("inventory"), dict):
380
+ data = data["inventory"] # validate-inventory.py's whole result
381
+ if not isinstance(data, dict) or not isinstance(data.get("exports"), list):
382
+ raise InputFileError(f"--inventory {args.inventory} holds no `exports` list")
383
+ inp["exports"] = data["exports"]
384
+ if args.surface is not None:
385
+ data = _read_json_file(args.surface, "--surface")
386
+ sets = data.get("sets") if isinstance(data, dict) else None
387
+ names = sets.get(args.surface_set) if isinstance(sets, dict) else None
388
+ if not isinstance(names, list):
389
+ raise InputFileError(f"--surface {args.surface} has no name set `{args.surface_set}`")
390
+ inp["barrelSet"] = names
391
+ if args.coverage_inputs is not None:
392
+ data = _read_json_file(args.coverage_inputs, "--coverage-inputs")
393
+ if not isinstance(data, dict):
394
+ raise InputFileError(f"--coverage-inputs {args.coverage_inputs} is not a JSON object")
395
+ if inp.get("denominatorSource") == "scalar":
396
+ if args.denominator_value is None:
397
+ if data.get("effectiveDenominator") is None:
398
+ raise InputFileError(f"--coverage-inputs {args.coverage_inputs} has no "
399
+ "`effectiveDenominator`: metadata.json has no stats.effective_denominator")
400
+ inp["denominatorValue"] = data["effectiveDenominator"]
401
+ else:
402
+ stack = data.get("stack")
403
+ if not isinstance(stack, dict):
404
+ raise InputFileError(f"--coverage-inputs {args.coverage_inputs} has no `stack` object")
405
+ inp["compositionNames"] = stack.get("compositionNames")
406
+ if args.denominator_value is None:
407
+ inp["denominatorValue"] = stack.get("denominator")
408
+ if args.verified is not None:
409
+ data = _read_json_file(args.verified, "--verified")
410
+ if not isinstance(data, dict):
411
+ raise InputFileError(f"--verified {args.verified} is not a verify-declared-numerator.py result")
412
+ if data.get("inflated") is True:
413
+ inp["verifiedNumerator"] = data.get("verified")
414
+ return inp
275
415
 
276
416
 
277
417
  # --- CLI --------------------------------------------------------------------
@@ -283,17 +423,16 @@ def _build_parser():
283
423
  description=(
284
424
  "Deterministic Documented-vs-Source coverage reconciliation "
285
425
  "(coverage-check.md §2c). Consumes the §1 documented inventory and "
286
- "the §2 source barrel (or a resolved scalar/stack denominator + the "
287
- "skill package path) and emits {documented, missing, stale, "
288
- "exportCoverage} so the coverage numerator is reproducible."
426
+ "the §2 source surface (or a resolved scalar/stack denominator + the "
427
+ "skill package path, or the inventory alone for a docs-only skill) and "
428
+ "emits {documented, missing, stale, exportCoverage} so the coverage "
429
+ "numerator is reproducible."
289
430
  ),
290
431
  formatter_class=argparse.RawDescriptionHelpFormatter,
291
432
  epilog=(
292
433
  "Example (barrel branch):\n"
293
- " uv run reconcile-coverage.py "
294
- "'{\"denominatorSource\":\"barrel\","
295
- "\"exports\":[{\"name\":\"a\",\"kind\":\"function\"}],"
296
- "\"barrelSet\":[\"a\",\"b\"]}'"
434
+ " uv run reconcile-coverage.py --denominator-source barrel "
435
+ "--inventory run/inventory.json --surface run/surface.json --output run/coverage.json"
297
436
  ),
298
437
  )
299
438
  src = parser.add_mutually_exclusive_group()
@@ -312,9 +451,27 @@ def _build_parser():
312
451
  action="store_true",
313
452
  help="Read the JSON object from stdin.",
314
453
  )
454
+ files = parser.add_argument_group("inputs read from files (each wins over the same JSON field)")
455
+ files.add_argument("--denominator-source", choices=VALID_SOURCES, help="the §2c branch")
456
+ files.add_argument("--denominator-value", type=int, metavar="N",
457
+ help="the resolved scalar denominator")
458
+ files.add_argument("--skill-dir", metavar="DIR", help="the skill package: SKILL.md and references/")
459
+ files.add_argument("--inventory", metavar="PATH", help="the validated inventory file")
460
+ files.add_argument("--surface", metavar="PATH", help="load-coverage-inputs.py surface output")
461
+ files.add_argument("--surface-set", default="all", metavar="NAME",
462
+ help="the name set of --surface to intersect against (default: all)")
463
+ files.add_argument("--coverage-inputs", metavar="PATH",
464
+ help="load-coverage-inputs.py metadata output (scalar and stack branches)")
465
+ files.add_argument("--verified", metavar="PATH",
466
+ help="verify-declared-numerator.py output (scalar branch)")
467
+ parser.add_argument("--output", metavar="PATH", help="also write the result to this file")
315
468
  return parser
316
469
 
317
470
 
471
+ FILE_FLAGS = ("denominator_source", "denominator_value", "skill_dir", "inventory", "surface",
472
+ "coverage_inputs", "verified")
473
+
474
+
318
475
  def _resolve_input(args):
319
476
  if args.stdin:
320
477
  return sys.stdin.read()
@@ -325,30 +482,67 @@ def _resolve_input(args):
325
482
  return ""
326
483
 
327
484
 
485
+ def _write_output(path, result):
486
+ target = Path(path)
487
+ if result.get("code") == "INVALID_INPUT":
488
+ target.unlink(missing_ok=True)
489
+ return
490
+ target.parent.mkdir(parents=True, exist_ok=True)
491
+ target.write_bytes((json.dumps(result, indent=2, ensure_ascii=False) + "\n").encode("utf-8"))
492
+
493
+
328
494
  def main(argv=None):
329
495
  parser = _build_parser()
330
496
  args = parser.parse_args(argv)
331
497
  raw = _resolve_input(args)
332
- if not raw.strip():
498
+ from_files = any(getattr(args, flag) is not None for flag in FILE_FLAGS)
499
+ if not raw.strip() and not from_files:
333
500
  parser.print_usage(file=sys.stderr)
334
501
  print(
335
- "error: no input provided (positional arg, --json-input, or --stdin)",
502
+ "error: no input provided (positional arg, --json-input, --stdin or the file flags)",
336
503
  file=sys.stderr,
337
504
  )
338
505
  return 1
339
506
 
340
- try:
341
- data = json.loads(raw)
342
- except json.JSONDecodeError as exc:
343
- print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
344
- return 1
507
+ data = {}
508
+ if raw.strip():
509
+ try:
510
+ data = json.loads(raw)
511
+ except json.JSONDecodeError as exc:
512
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
513
+ return 1
514
+
515
+ if isinstance(data, dict):
516
+ try:
517
+ data = apply_file_inputs(data, args)
518
+ except InputFileError as exc:
519
+ print(f"error: {exc}", file=sys.stderr)
520
+ return 1
345
521
 
346
522
  result = reconcile(data)
523
+ if args.output:
524
+ _write_output(args.output, result)
347
525
  print(json.dumps(result, indent=2))
348
526
  if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
349
527
  return 2
350
528
  return 0
351
529
 
352
530
 
531
+ def _force_utf8(*streams) -> None:
532
+ """Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
533
+
534
+ A Windows console pipes them as cp1252, which cannot print every character
535
+ of the --help text, so --help would stop with UnicodeEncodeError.
536
+ """
537
+ for stream in streams:
538
+ if hasattr(stream, "reconfigure"):
539
+ errors = getattr(stream, "errors", None)
540
+ if errors is None:
541
+ stream.reconfigure(encoding="utf-8")
542
+ else:
543
+ stream.reconfigure(encoding="utf-8", errors=errors)
544
+
545
+
353
546
  if __name__ == "__main__":
547
+ _force_utf8(sys.stdout, sys.stderr)
354
548
  raise SystemExit(main())