bmad-module-skill-forge 2.2.0 → 4.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 (437) 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 +143 -18
  18. package/docs/verifying-a-skill.md +87 -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 +3 -3
  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 +31 -59
  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 +233 -64
  64. package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
  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 +772 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
  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 +21 -3
  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 +197 -55
  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 +5247 -96
  86. package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +664 -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 +469 -205
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +700 -0
  94. package/src/shared/scripts/skf-hash-content.py +180 -20
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +161 -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 +143 -20
  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 +176 -28
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
  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 +155 -14
  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 +787 -121
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +120 -67
  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 +23 -2
  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 +3945 -152
  138. package/src/shared/scripts/skf-write-skill-brief.py +406 -25
  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 +28 -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/health-check.md +2 -0
  168. package/src/skf-audit-skill/references/init.md +149 -210
  169. package/src/skf-audit-skill/references/re-index.md +103 -110
  170. package/src/skf-audit-skill/references/report.md +96 -33
  171. package/src/skf-audit-skill/references/semantic-diff.md +34 -25
  172. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  173. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  174. package/src/skf-audit-skill/references/structural-diff.md +80 -90
  175. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  176. package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
  177. package/src/skf-brief-skill/SKILL.md +37 -26
  178. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  179. package/src/skf-brief-skill/assets/scope-templates.md +18 -25
  180. package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
  181. package/src/skf-brief-skill/customize.toml +48 -20
  182. package/src/skf-brief-skill/references/analyze-target.md +144 -127
  183. package/src/skf-brief-skill/references/confirm-brief.md +11 -9
  184. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  185. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  186. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  187. package/src/skf-brief-skill/references/headless-args.md +84 -16
  188. package/src/skf-brief-skill/references/health-check.md +1 -1
  189. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  190. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  191. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  192. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  193. package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
  194. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  195. package/src/skf-brief-skill/references/version-resolution.md +7 -10
  196. package/src/skf-brief-skill/references/write-brief.md +66 -57
  197. package/src/skf-campaign/SKILL.md +31 -34
  198. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  199. package/src/skf-campaign/customize.toml +47 -16
  200. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  201. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  202. package/src/skf-campaign/references/health-check.md +7 -16
  203. package/src/skf-campaign/references/step-01-setup.md +46 -84
  204. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  205. package/src/skf-campaign/references/step-03-pins.md +26 -18
  206. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  207. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  208. package/src/skf-campaign/references/step-06-batch.md +41 -34
  209. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  210. package/src/skf-campaign/references/step-08-verify.md +28 -29
  211. package/src/skf-campaign/references/step-09-refine.md +29 -30
  212. package/src/skf-campaign/references/step-10-export.md +48 -36
  213. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  214. package/src/skf-campaign/references/step-resume.md +37 -68
  215. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  216. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  217. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  218. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  219. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  220. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  221. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  222. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  223. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  224. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  225. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  226. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  227. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  228. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  229. package/src/skf-create-skill/SKILL.md +27 -20
  230. package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
  231. package/src/skf-create-skill/assets/skill-sections.md +21 -11
  232. package/src/skf-create-skill/customize.toml +20 -8
  233. package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
  234. package/src/skf-create-skill/references/batch-mode.md +81 -0
  235. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  236. package/src/skf-create-skill/references/compile.md +119 -70
  237. package/src/skf-create-skill/references/component-extraction.md +113 -91
  238. package/src/skf-create-skill/references/enrich.md +41 -23
  239. package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
  240. package/src/skf-create-skill/references/extract.md +204 -162
  241. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  242. package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
  243. package/src/skf-create-skill/references/extraction-patterns.md +588 -201
  244. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  245. package/src/skf-create-skill/references/health-check.md +1 -1
  246. package/src/skf-create-skill/references/load-brief.md +53 -36
  247. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  248. package/src/skf-create-skill/references/report.md +37 -57
  249. package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
  250. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  251. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  252. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  253. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  254. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  255. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  256. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  257. package/src/skf-create-skill/references/validate.md +173 -99
  258. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  259. package/src/skf-create-stack-skill/SKILL.md +29 -56
  260. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  261. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  262. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  263. package/src/skf-create-stack-skill/customize.toml +23 -12
  264. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  265. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  266. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  267. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  268. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  269. package/src/skf-create-stack-skill/references/init.md +90 -32
  270. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  271. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  272. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  273. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  274. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  275. package/src/skf-create-stack-skill/references/report.md +39 -33
  276. package/src/skf-create-stack-skill/references/validate.md +20 -39
  277. package/src/skf-drop-skill/SKILL.md +28 -51
  278. package/src/skf-drop-skill/customize.toml +44 -30
  279. package/src/skf-drop-skill/references/execute.md +83 -146
  280. package/src/skf-drop-skill/references/health-check.md +2 -0
  281. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  282. package/src/skf-drop-skill/references/report.md +46 -30
  283. package/src/skf-drop-skill/references/select.md +122 -124
  284. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  285. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  286. package/src/skf-export-skill/SKILL.md +40 -62
  287. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  288. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  289. package/src/skf-export-skill/customize.toml +42 -13
  290. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  291. package/src/skf-export-skill/references/health-check.md +2 -0
  292. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  293. package/src/skf-export-skill/references/load-skill.md +88 -62
  294. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  295. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  296. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  297. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  298. package/src/skf-export-skill/references/package.md +15 -73
  299. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  300. package/src/skf-export-skill/references/result-envelope.md +31 -16
  301. package/src/skf-export-skill/references/summary.md +41 -17
  302. package/src/skf-export-skill/references/token-report.md +4 -12
  303. package/src/skf-export-skill/references/update-context.md +118 -237
  304. package/src/skf-forger/SKILL.md +61 -37
  305. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  306. package/src/skf-forger/customize.toml +43 -0
  307. package/src/skf-forger/references/pipeline-mode.md +73 -18
  308. package/src/skf-forger/scripts/forge-status.py +299 -0
  309. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  310. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  311. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  312. package/src/skf-quick-skill/SKILL.md +54 -53
  313. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  314. package/src/skf-quick-skill/customize.toml +51 -20
  315. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  316. package/src/skf-quick-skill/references/compile.md +30 -31
  317. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  318. package/src/skf-quick-skill/references/finalize.md +69 -18
  319. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  320. package/src/skf-quick-skill/references/health-check.md +5 -3
  321. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  322. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  323. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  324. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  325. package/src/skf-refine-architecture/SKILL.md +43 -53
  326. package/src/skf-refine-architecture/customize.toml +49 -20
  327. package/src/skf-refine-architecture/references/compile.md +97 -54
  328. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  329. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  330. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  331. package/src/skf-refine-architecture/references/improvements.md +14 -20
  332. package/src/skf-refine-architecture/references/init.md +98 -57
  333. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  334. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  335. package/src/skf-refine-architecture/references/report.md +65 -54
  336. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  337. package/src/skf-rename-skill/SKILL.md +19 -54
  338. package/src/skf-rename-skill/customize.toml +32 -26
  339. package/src/skf-rename-skill/references/execute.md +230 -184
  340. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  341. package/src/skf-rename-skill/references/health-check.md +1 -7
  342. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  343. package/src/skf-rename-skill/references/report.md +63 -52
  344. package/src/skf-rename-skill/references/select.md +197 -115
  345. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  346. package/src/skf-setup/SKILL.md +26 -26
  347. package/src/skf-setup/customize.toml +14 -10
  348. package/src/skf-setup/references/auto-index.md +50 -63
  349. package/src/skf-setup/references/ccc-index.md +29 -90
  350. package/src/skf-setup/references/detect-and-tier.md +58 -67
  351. package/src/skf-setup/references/health-check.md +2 -0
  352. package/src/skf-setup/references/invocation-contract.md +14 -0
  353. package/src/skf-setup/references/report.md +43 -166
  354. package/src/skf-setup/references/tier-rules.md +5 -5
  355. package/src/skf-setup/references/write-config.md +34 -57
  356. package/src/skf-test-skill/SKILL.md +17 -57
  357. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  358. package/src/skf-test-skill/customize.toml +27 -13
  359. package/src/skf-test-skill/references/coherence-check.md +134 -62
  360. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  361. package/src/skf-test-skill/references/coverage-check.md +209 -264
  362. package/src/skf-test-skill/references/detect-mode.md +5 -5
  363. package/src/skf-test-skill/references/external-validators.md +94 -82
  364. package/src/skf-test-skill/references/health-check.md +6 -7
  365. package/src/skf-test-skill/references/init.md +164 -91
  366. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  367. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  368. package/src/skf-test-skill/references/report.md +164 -142
  369. package/src/skf-test-skill/references/score.md +80 -107
  370. package/src/skf-test-skill/references/scoring-rules.md +42 -155
  371. package/src/skf-test-skill/references/source-access-protocol.md +18 -94
  372. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  373. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  374. package/src/skf-test-skill/scripts/build-result-context.py +380 -0
  375. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  376. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  377. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  378. package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
  379. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  380. package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
  381. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  382. package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
  383. package/src/skf-test-skill/scripts/score-signatures.py +479 -0
  384. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  385. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  386. package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
  387. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  388. package/src/skf-update-skill/SKILL.md +38 -25
  389. package/src/skf-update-skill/customize.toml +27 -12
  390. package/src/skf-update-skill/references/detect-changes.md +253 -223
  391. package/src/skf-update-skill/references/gap-driven.md +336 -0
  392. package/src/skf-update-skill/references/health-check.md +31 -5
  393. package/src/skf-update-skill/references/init.md +254 -84
  394. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  395. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  396. package/src/skf-update-skill/references/merge.md +182 -49
  397. package/src/skf-update-skill/references/re-extract.md +53 -262
  398. package/src/skf-update-skill/references/report.md +102 -35
  399. package/src/skf-update-skill/references/write.md +357 -155
  400. package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
  401. package/src/skf-verify-stack/SKILL.md +23 -42
  402. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  403. package/src/skf-verify-stack/customize.toml +55 -22
  404. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  405. package/src/skf-verify-stack/references/coverage.md +67 -64
  406. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  407. package/src/skf-verify-stack/references/init.md +107 -62
  408. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  409. package/src/skf-verify-stack/references/integrations.md +106 -65
  410. package/src/skf-verify-stack/references/report.md +59 -42
  411. package/src/skf-verify-stack/references/requirements.md +32 -9
  412. package/src/skf-verify-stack/references/synthesize.md +53 -39
  413. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  414. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  415. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  416. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  417. package/tools/cli/commands/install.js +11 -4
  418. package/tools/cli/commands/status.js +15 -3
  419. package/tools/cli/commands/update.js +11 -4
  420. package/tools/cli/lib/tool-check.js +320 -0
  421. package/tools/cli/lib/version-check.js +22 -9
  422. package/tools/cli/skf-cli.js +4 -1
  423. package/.gitattributes +0 -16
  424. package/.nvmrc +0 -1
  425. package/docs/_internal/RELEASING.md +0 -679
  426. package/release-audits/v1.0.0-launch-audit.md +0 -728
  427. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  428. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  429. package/src/skf-campaign/manifest.yaml +0 -15
  430. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  431. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  432. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  433. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  434. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  435. package/src/skf-update-skill/references/validate.md +0 -124
  436. package/tools/validate-docs-drift.js +0 -233
  437. package/tools/validate-docs-links.js +0 -665
@@ -0,0 +1,479 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = []
5
+ # ///
6
+ """Signature Accuracy and Type Coverage for SKF test-skill, from checked per-file results.
7
+
8
+ coverage-check.md section 2 compares each documented signature with the
9
+ source and section 2b scores two categories from the comparison: Signature
10
+ Accuracy and Type Coverage, 22% and 14% of a contextual score. Which
11
+ signature differs is a judgment, made by a subagent that reads the source;
12
+ everything after it has one answer per input and runs here: the list of
13
+ signatures to compare, the schema check of each subagent result, the two
14
+ ratios and the gap records of the wrong signatures.
15
+
16
+ plan the comparisons to make. For each documented export with a
17
+ signature (an inventory entry of any kind but `method` that carries
18
+ `params`, `return_type` or `usage_signature`) in the signature
19
+ universe, grouped by source file, with the line that defines it in
20
+ the source and the documented signature. The signature universe is
21
+ the surface's `all` set plus every name on its export records, so
22
+ the names of a Python sub-package load-coverage-inputs.py keeps out
23
+ of `all` (excluded.nestedEntries) keep their checks. Also the whole
24
+ documented-signature map, for the fallback scan that has no surface
25
+ lines yet.
26
+ score the categories. Reads every subagent result, checks its schema,
27
+ and computes:
28
+ Signature Accuracy = matching / compared * 100, where compared is
29
+ the documented signatures of the signature universe (with a
30
+ fallback scan's exports_found) and matching those no result
31
+ names in signature_mismatches[]: a wrong signature is wrong
32
+ whichever set the denominator counts
33
+ Type Coverage = documented types / total types * 100, where
34
+ total types counts the names of the --surface-set set (the
35
+ denominator set coverage-check section 2b picked) whose kind is
36
+ a type kind (TYPE_KINDS: interface, type alias, enum and class,
37
+ with a Rust struct counted as a class and a trait as an
38
+ interface) and documented types those the inventory documents
39
+ A category with nothing to compare scores 100 (nothing documented
40
+ is wrong) and says so in `warnings`. Each signature mismatch of a
41
+ compared name becomes a Critical `signature-mismatch` record in the
42
+ gap ledger's record format (gap-ledger.py append --input reads
43
+ --gaps-output); a mismatch a result reports for any other name (one
44
+ the skill does not document, or the surface lacks) is listed in
45
+ `warnings` instead, and no gap.
46
+
47
+ Inputs, all files in the run folder:
48
+ --inventory the validated inventory validate-inventory.py wrote (--output)
49
+ --surface load-coverage-inputs.py surface output: `exports[]` (name,
50
+ kind, file, line, signatureLine) and the name `sets`;
51
+ --surface-set picks the set (default "all") score's Type
52
+ Coverage counts, while plan and Signature Accuracy compare
53
+ the signature universe whichever set it picks
54
+ --results a subagent result, as the subagent returned it (a wrapping
55
+ markdown fence is stripped), repeatable. One object, or a
56
+ list of them:
57
+ {"file": "<source file>",
58
+ "exports_found": ["..."], (fallback scan only)
59
+ "types_found": ["..."], (fallback scan only)
60
+ "signature_mismatches": [
61
+ {"name", "line", "source_sig", "documented_sig", "issue"}]}
62
+
63
+ Rounding is compute-score.py's (JS-compatible half-up, two decimals).
64
+
65
+ Usage:
66
+ uv run score-signatures.py plan --inventory <file> [--surface <file>] [--output <file>]
67
+ uv run score-signatures.py score --inventory <file> --surface <file> [--surface-set <set>] \\
68
+ --results <file> [--results <file> ...] [--output <file>] [--gaps-output <file>]
69
+
70
+ Output (stdout; --output also writes it, --gaps-output writes the records):
71
+ plan: {"compared": N, "files": [{"file", "checks": [{"name", "line",
72
+ "signatureLine", "documented": {...}}]}], "documentedSignatures": {...}}
73
+ score: {"valid": true, "signatureAccuracy", "matchingSignatures",
74
+ "totalDocumented", "comparedNames": [the names compared],
75
+ "typeCoverage", "documentedTypes", "totalTypes",
76
+ "missingTypes": [...], "mismatches": [...], "gapRecords": [...],
77
+ "warnings": [...]}
78
+ or {"valid": false, "violations": [...]} when a result breaks the schema
79
+
80
+ Exit codes:
81
+ 0 the result was printed
82
+ 1 an input file cannot be read or is not the JSON it should be, or the
83
+ surface has no --surface-set set
84
+ 2 a subagent result breaks the schema (violations[] names each breach;
85
+ nothing is written to --output or --gaps-output)
86
+ """
87
+
88
+ from __future__ import annotations
89
+
90
+ import argparse
91
+ import importlib.util
92
+ import json
93
+ import math
94
+ import sys
95
+ from pathlib import Path
96
+
97
+ # Surface kinds that count toward total_types. A Rust struct is a class and a
98
+ # trait an interface; `type` is a type alias.
99
+ TYPE_KINDS = frozenset({"interface", "type", "enum", "class", "struct", "trait"})
100
+ SIGNATURE_FIELDS = ("params", "return_type", "usage_signature")
101
+ MISMATCH_FIELDS = ("name", "line", "source_sig", "documented_sig", "issue")
102
+
103
+ _INVENTORY_MODULE = None
104
+
105
+
106
+ def _inventory():
107
+ """validate-inventory.py beside this script: the fence rule lives there."""
108
+ global _INVENTORY_MODULE
109
+ if _INVENTORY_MODULE is None:
110
+ sibling = Path(__file__).resolve().parent / "validate-inventory.py"
111
+ spec = importlib.util.spec_from_file_location("skf_validate_inventory", sibling)
112
+ if spec is None or spec.loader is None or not sibling.is_file():
113
+ raise ImportError(f"validate-inventory.py not found beside {Path(__file__).name}")
114
+ module = importlib.util.module_from_spec(spec)
115
+ spec.loader.exec_module(module)
116
+ _INVENTORY_MODULE = module
117
+ return _INVENTORY_MODULE
118
+
119
+
120
+ class InputError(Exception):
121
+ """An input file that cannot be read, or is not the JSON it should be."""
122
+
123
+
124
+ def round2(value: float) -> float:
125
+ """Round to 2 decimals with JS-compatible half-up rounding (matches compute-score.py)."""
126
+ return math.floor(value * 100 + 0.5) / 100
127
+
128
+
129
+ def _read_text(path: str, flag: str) -> str:
130
+ try:
131
+ return Path(path).read_bytes().decode("utf-8-sig")
132
+ except (OSError, UnicodeDecodeError) as exc:
133
+ raise InputError(f"cannot read {flag} {path}: {exc}") from exc
134
+
135
+
136
+ def _read_json(path: str, flag: str):
137
+ try:
138
+ return json.loads(_read_text(path, flag))
139
+ except json.JSONDecodeError as exc:
140
+ raise InputError(f"{flag} {path} is not JSON: {exc.msg}") from exc
141
+
142
+
143
+ # --------------------------------------------------------------------------
144
+ # Inputs
145
+ # --------------------------------------------------------------------------
146
+
147
+
148
+ def load_inventory(path: str) -> list[dict]:
149
+ data = _read_json(path, "--inventory")
150
+ if isinstance(data, dict) and isinstance(data.get("inventory"), dict):
151
+ data = data["inventory"] # validate-inventory.py's whole result
152
+ if not isinstance(data, dict) or not isinstance(data.get("exports"), list):
153
+ raise InputError(f"--inventory {path} holds no `exports` list")
154
+ return [e for e in data["exports"] if isinstance(e, dict)]
155
+
156
+
157
+ def load_surface(path: str, surface_set: str) -> tuple[list[str], list[dict], list[str]]:
158
+ """(the chosen name set, every surface export record, the `all` set)."""
159
+ data = _read_json(path, "--surface")
160
+ sets = data.get("sets") if isinstance(data, dict) else None
161
+
162
+ def name_set(key: str) -> list[str]:
163
+ names = sets.get(key) if isinstance(sets, dict) else None
164
+ if not isinstance(names, list):
165
+ raise InputError(f"--surface {path} has no name set `{key}`")
166
+ return [n for n in names if isinstance(n, str) and n]
167
+
168
+ exports = data.get("exports") if isinstance(data.get("exports"), list) else []
169
+ return name_set(surface_set), [e for e in exports if isinstance(e, dict)], name_set("all")
170
+
171
+
172
+ def check_result(item: object, where: str) -> list[str]:
173
+ """The schema breaches of one per-file result object."""
174
+ if not isinstance(item, dict):
175
+ return [f"{where} is not a JSON object"]
176
+ errors = []
177
+ if not (isinstance(item.get("file"), str) and item["file"].strip()):
178
+ errors.append(f"{where}: `file` must be a non-empty string")
179
+ for key in ("exports_found", "types_found"):
180
+ value = item.get(key)
181
+ if value is not None and not (isinstance(value, list) and all(isinstance(v, str) for v in value)):
182
+ errors.append(f"{where}: `{key}` must be a list of strings when present")
183
+ mismatches = item.get("signature_mismatches")
184
+ if not isinstance(mismatches, list):
185
+ errors.append(f"{where}: `signature_mismatches` must be present and a list (may be empty)")
186
+ return errors
187
+ for i, entry in enumerate(mismatches):
188
+ at = f"{where}.signature_mismatches[{i}]"
189
+ if not isinstance(entry, dict):
190
+ errors.append(f"{at} is not an object")
191
+ continue
192
+ missing = [f for f in MISMATCH_FIELDS if f not in entry]
193
+ if missing:
194
+ errors.append(f"{at} missing field(s): {', '.join(missing)}")
195
+ continue
196
+ line = entry["line"]
197
+ if isinstance(line, bool) or not isinstance(line, int) or line < 1:
198
+ errors.append(f"{at}: `line` must be a line number (an integer from 1)")
199
+ for field in ("name", "source_sig", "documented_sig", "issue"):
200
+ if not (isinstance(entry[field], str) and entry[field].strip()):
201
+ errors.append(f"{at}: `{field}` must be a non-empty string")
202
+ return errors
203
+
204
+
205
+ def parse_result_text(text: str, where: str) -> tuple[list[dict], list[str]]:
206
+ """(the result objects, the schema breaches) of one subagent response."""
207
+ inner = _inventory().strip_fences(text)
208
+ try:
209
+ data = json.loads(inner)
210
+ except json.JSONDecodeError as exc:
211
+ return [], [f"{where} is not valid JSON: {exc.msg}"]
212
+ items = data if isinstance(data, list) else [data]
213
+ errors = []
214
+ for i, item in enumerate(items):
215
+ errors += check_result(item, where if len(items) == 1 else f"{where}[{i}]")
216
+ return ([] if errors else items), errors
217
+
218
+
219
+ def load_results(paths: list[str]) -> tuple[list[dict], list[str]]:
220
+ """Every result object of the --results files, and every schema breach."""
221
+ results, violations = [], []
222
+ for path in paths:
223
+ items, errors = parse_result_text(_read_text(path, "--results"), Path(path).name)
224
+ results += items
225
+ violations += errors
226
+ return results, violations
227
+
228
+
229
+ # --------------------------------------------------------------------------
230
+ # Plan and score
231
+ # --------------------------------------------------------------------------
232
+
233
+
234
+ def _has_signature(entry: dict) -> bool:
235
+ return any(
236
+ isinstance(entry.get(f), list) or (isinstance(entry.get(f), str) and entry[f].strip())
237
+ for f in SIGNATURE_FIELDS
238
+ )
239
+
240
+
241
+ def documented_signatures(inventory: list[dict]) -> dict[str, dict]:
242
+ """{name: its documented signature fields} for each documented export with one."""
243
+ out: dict[str, dict] = {}
244
+ for entry in inventory:
245
+ name = entry.get("name")
246
+ if entry.get("kind") == "method" or not (isinstance(name, str) and name) or name in out:
247
+ continue
248
+ if _has_signature(entry):
249
+ out[name] = {f: entry[f] for f in SIGNATURE_FIELDS if f in entry}
250
+ return out
251
+
252
+
253
+ def documented_names(inventory: list[dict]) -> set[str]:
254
+ return {e["name"] for e in inventory
255
+ if isinstance(e.get("name"), str) and e["name"] and e.get("kind") != "method"}
256
+
257
+
258
+ def _records_by_name(exports: list[dict]) -> dict[str, list[dict]]:
259
+ by_name: dict[str, list[dict]] = {}
260
+ for record in sorted(exports, key=lambda r: (str(r.get("file") or ""), r.get("line") or 0)):
261
+ name = record.get("name")
262
+ if isinstance(name, str) and name:
263
+ by_name.setdefault(name, []).append(record)
264
+ return by_name
265
+
266
+
267
+ def signature_universe(names: list[str], exports: list[dict]) -> set[str]:
268
+ """The names whose documented signature is compared: the `all` set
269
+ (`names`) plus every name on the surface's export records. A nested
270
+ Python top's names leave `all` but keep their records, so a documented
271
+ one still has its signature checked."""
272
+ return set(names) | set(_records_by_name(exports))
273
+
274
+
275
+ def plan(inventory: list[dict], names: list[str] | None, exports: list[dict]) -> dict:
276
+ """The comparisons over the signature universe of `names` (the `all`
277
+ set) and `exports`; none without a surface (`names` None)."""
278
+ signatures = documented_signatures(inventory)
279
+ files: dict[str, list[dict]] = {}
280
+ compared = 0
281
+ if names is not None:
282
+ by_name = _records_by_name(exports)
283
+ for name in sorted(signature_universe(names, exports) & set(signatures)):
284
+ record = (by_name.get(name) or [{}])[0]
285
+ compared += 1
286
+ files.setdefault(str(record.get("file") or ""), []).append({
287
+ "name": name,
288
+ "line": record.get("line"),
289
+ "signatureLine": record.get("signatureLine"),
290
+ "documented": signatures[name],
291
+ })
292
+ return {
293
+ "compared": compared,
294
+ "files": [{"file": f, "checks": checks} for f, checks in sorted(files.items())],
295
+ "documentedSignatures": signatures,
296
+ }
297
+
298
+
299
+ def _gap_record(m: dict) -> dict:
300
+ name, where = m["name"], f"{m['file']}:{m['line']}"
301
+ return {
302
+ "severity": "Critical",
303
+ "category": "signature-mismatch",
304
+ "title": f"Signature mismatch: {name}",
305
+ "source": where,
306
+ "export": name,
307
+ "issue": f"documented `{m['documented_sig']}`, source `{m['source_sig']}`: {m['issue']}",
308
+ "remediation": (f"Update the documented signature of `{name}` from `{m['documented_sig']}` "
309
+ f"to `{m['source_sig']}`, the signature the source defines at `{where}`."),
310
+ }
311
+
312
+
313
+ def score(inventory: list[dict], names: list[str], exports: list[dict], results: list[dict],
314
+ type_names: list[str] | None = None) -> dict:
315
+ """The two categories: Signature Accuracy over the signature universe of
316
+ `names` (the `all` set) and `exports`, Type Coverage over `type_names`
317
+ when given, else over `names`. A fallback scan's exports_found join the
318
+ universe, and `names` when no `type_names` is given."""
319
+ by_name = _records_by_name(exports)
320
+ # A fallback scan reports its own names and types.
321
+ fallback_types = {n for r in results for n in r.get("types_found") or []}
322
+ surface = set(names) | {n for r in results for n in r.get("exports_found") or []}
323
+ documented = documented_names(inventory)
324
+ signatures = documented_signatures(inventory)
325
+ compared = (surface | signature_universe(names, exports)) & set(signatures)
326
+ warnings = []
327
+
328
+ mismatches = []
329
+ seen = set()
330
+ for result in results:
331
+ for entry in result["signature_mismatches"]:
332
+ key = (entry["name"], result["file"], entry["line"])
333
+ if key in seen:
334
+ continue
335
+ seen.add(key)
336
+ if entry["name"] not in compared:
337
+ warnings.append(f"{result['file']}:{entry['line']}: a signature mismatch for "
338
+ f"`{entry['name']}`, which is no documented signature on the source surface: "
339
+ "not scored, no gap")
340
+ continue
341
+ mismatches.append({"name": entry["name"], "file": result["file"], "line": entry["line"],
342
+ "source_sig": entry["source_sig"], "documented_sig": entry["documented_sig"],
343
+ "issue": entry["issue"]})
344
+ mismatches.sort(key=lambda m: (m["file"], m["line"], m["name"]))
345
+
346
+ wrong = {m["name"] for m in mismatches}
347
+ if compared:
348
+ signature_accuracy = round2((len(compared) - len(wrong)) / len(compared) * 100)
349
+ else:
350
+ signature_accuracy = 100.0
351
+ warnings.append("no documented signature is on the source surface: Signature Accuracy has "
352
+ "nothing to compare and scores 100")
353
+
354
+ def kind_of(name: str) -> str | None:
355
+ if name in fallback_types:
356
+ return "type"
357
+ return next((r.get("kind") for r in by_name.get(name, []) if r.get("kind")), None)
358
+
359
+ types = {n for n in (surface if type_names is None else set(type_names)) if kind_of(n) in TYPE_KINDS}
360
+ documented_types = types & documented
361
+ if types:
362
+ type_coverage = round2(len(documented_types) / len(types) * 100)
363
+ else:
364
+ type_coverage = 100.0
365
+ warnings.append("the source surface has no interface, type alias, enum or class: Type "
366
+ "Coverage has nothing to cover and scores 100")
367
+
368
+ return {
369
+ "valid": True,
370
+ "signatureAccuracy": signature_accuracy,
371
+ "matchingSignatures": len(compared) - len(wrong),
372
+ "totalDocumented": len(compared),
373
+ "comparedNames": sorted(compared),
374
+ "typeCoverage": type_coverage,
375
+ "documentedTypes": len(documented_types),
376
+ "totalTypes": len(types),
377
+ "missingTypes": sorted(types - documented),
378
+ "mismatches": mismatches,
379
+ "gapRecords": [_gap_record(m) for m in mismatches],
380
+ "warnings": warnings,
381
+ }
382
+
383
+
384
+ # --------------------------------------------------------------------------
385
+ # CLI
386
+ # --------------------------------------------------------------------------
387
+
388
+
389
+ def _write_json(path: str, payload) -> None:
390
+ target = Path(path)
391
+ target.parent.mkdir(parents=True, exist_ok=True)
392
+ target.write_bytes((json.dumps(payload, indent=2, ensure_ascii=False) + "\n").encode("utf-8"))
393
+
394
+
395
+ def _cmd_plan(args: argparse.Namespace) -> tuple[dict, int]:
396
+ inventory = load_inventory(args.inventory)
397
+ names, exports = (None, [])
398
+ if args.surface is not None:
399
+ _, exports, names = load_surface(args.surface, args.surface_set)
400
+ out = plan(inventory, names, exports)
401
+ if args.output:
402
+ _write_json(args.output, out)
403
+ return out, 0
404
+
405
+
406
+ def _cmd_score(args: argparse.Namespace) -> tuple[dict, int]:
407
+ inventory = load_inventory(args.inventory)
408
+ chosen, exports, names = load_surface(args.surface, args.surface_set)
409
+ results, violations = load_results(args.results or [])
410
+ if violations:
411
+ for path in (args.output, args.gaps_output):
412
+ if path:
413
+ Path(path).unlink(missing_ok=True)
414
+ return {"valid": False, "violations": violations}, 2
415
+ out = score(inventory, names, exports, results, None if args.surface_set == "all" else chosen)
416
+ if args.output:
417
+ _write_json(args.output, out)
418
+ if args.gaps_output:
419
+ _write_json(args.gaps_output, out["gapRecords"])
420
+ return out, 0
421
+
422
+
423
+ def _build_parser() -> argparse.ArgumentParser:
424
+ parser = argparse.ArgumentParser(
425
+ prog="score-signatures",
426
+ description="Signature Accuracy and Type Coverage from schema-checked per-file results.",
427
+ )
428
+ sub = parser.add_subparsers(dest="cmd", required=True)
429
+
430
+ p = sub.add_parser("plan", help="the signature comparisons for the subagents to make")
431
+ p.add_argument("--inventory", required=True, help="the validated inventory file")
432
+ p.add_argument("--surface", help="load-coverage-inputs.py surface output")
433
+ p.add_argument("--surface-set", default="all",
434
+ help="a surface name set, which must exist (default: all); the plan compares the signature "
435
+ "universe whichever set it names")
436
+ p.add_argument("--output", help="also write the plan to this file")
437
+ p.set_defaults(func=_cmd_plan)
438
+
439
+ p = sub.add_parser("score", help="the two category scores and the mismatch gap records")
440
+ p.add_argument("--inventory", required=True, help="the validated inventory file")
441
+ p.add_argument("--surface", required=True, help="load-coverage-inputs.py surface output")
442
+ p.add_argument("--surface-set", default="all",
443
+ help="the surface name set Type Coverage counts (default: all)")
444
+ p.add_argument("--results", action="append", help="a subagent result file (repeatable)")
445
+ p.add_argument("--output", help="also write the scores to this file")
446
+ p.add_argument("--gaps-output", help="write the gap ledger records to this file")
447
+ p.set_defaults(func=_cmd_score)
448
+ return parser
449
+
450
+
451
+ def main(argv: list[str] | None = None) -> int:
452
+ args = _build_parser().parse_args(argv)
453
+ try:
454
+ out, code = args.func(args)
455
+ except (InputError, ImportError) as exc:
456
+ print(f"error: {exc}", file=sys.stderr)
457
+ return 1
458
+ print(json.dumps(out, indent=2))
459
+ return code
460
+
461
+
462
+ def _force_utf8(*streams) -> None:
463
+ """Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
464
+
465
+ A Windows console pipes them as cp1252, which cannot print every character
466
+ of the --help text, so --help would stop with UnicodeEncodeError.
467
+ """
468
+ for stream in streams:
469
+ if hasattr(stream, "reconfigure"):
470
+ errors = getattr(stream, "errors", None)
471
+ if errors is None:
472
+ stream.reconfigure(encoding="utf-8")
473
+ else:
474
+ stream.reconfigure(encoding="utf-8", errors=errors)
475
+
476
+
477
+ if __name__ == "__main__":
478
+ _force_utf8(sys.stdout, sys.stderr)
479
+ sys.exit(main())
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = []
5
+ # ///
6
+ """Print the stdin payload of a shared SKF helper, read from the files on disk.
7
+
8
+ coverage-check.md §0b runs skf-detect-workspaces.py (is the local source a
9
+ monorepo?), a shared helper that takes one JSON payload on stdin and reads
10
+ no file itself. A payload the model writes by hand can cut a file short or
11
+ escape it wrongly, and the helper then fails or answers for the wrong text.
12
+ This script reads the files and prints the payload, and the step pipes it
13
+ into the helper:
14
+
15
+ uv run stage-helper-payload.py detect-workspaces --source-root <dir> \\
16
+ | uv run skf-detect-workspaces.py
17
+
18
+ (The Quick-tier export scan needs no payload: skf-extract-public-api.py
19
+ --mode quick reads its files itself, with --manifest-file and --entry-file.)
20
+
21
+ detect-workspaces prints {"tree": [...], "manifests": {...}}:
22
+
23
+ tree every file below --source-root, as a POSIX path relative to
24
+ it, sorted. Folders named in SKIPPED_DIRS (installed
25
+ dependencies, virtual environments, build output, caches) and
26
+ hidden folders (a name that starts with ".") are not read, and a
27
+ linked folder is not followed.
28
+ manifests the text of each file directly in --source-root that is not
29
+ hidden, holds at most MAX_ROOT_FILE_BYTES and reads as UTF-8, by
30
+ its name. The detector reads the root manifests it knows
31
+ (package.json, pnpm-workspace.yaml, lerna.json, Cargo.toml ...)
32
+ and ignores the rest, so this script keeps no list of their
33
+ names to fall out of step with the detector's.
34
+
35
+ The payload is one line of ASCII JSON on stdout, so a pipe carries the same
36
+ bytes on every platform.
37
+
38
+ Exit codes:
39
+ 0 the payload is printed
40
+ 1 --source-root is not a folder; one JSON line {"status": "error",
41
+ "error": ...} goes to stderr and nothing to stdout, so the helper the
42
+ payload was piped into reads no input and fails too
43
+ 2 bad arguments (argparse: a missing or unknown argument, usage on stderr)
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import argparse
49
+ import json
50
+ import os
51
+ import sys
52
+ from pathlib import Path
53
+
54
+ # Folders that hold no workspace member of the repository itself.
55
+ SKIPPED_DIRS = frozenset({
56
+ "node_modules", "bower_components", "jspm_packages", "vendor", "Pods",
57
+ "venv", "site-packages", "__pycache__", "target",
58
+ })
59
+ MAX_ROOT_FILE_BYTES = 1024 * 1024
60
+
61
+
62
+ class StageError(Exception):
63
+ """A path the payload cannot be built from."""
64
+
65
+
66
+ def _source_root(value: str) -> Path:
67
+ root = Path(value)
68
+ if not root.is_dir():
69
+ raise StageError(f"--source-root is not a folder: {value}")
70
+ return root
71
+
72
+
73
+ # --------------------------------------------------------------------------
74
+ # detect-workspaces
75
+ # --------------------------------------------------------------------------
76
+
77
+
78
+ def _skipped(name: str) -> bool:
79
+ return name.startswith(".") or name in SKIPPED_DIRS
80
+
81
+
82
+ def list_tree(root: Path) -> list[str]:
83
+ """Every file below `root` as a sorted POSIX path, outside skipped and linked folders."""
84
+ tree = []
85
+ for dirpath, dirnames, filenames in os.walk(root):
86
+ here = Path(dirpath)
87
+ dirnames[:] = [d for d in dirnames if not _skipped(d) and not (here / d).is_symlink()]
88
+ for name in filenames:
89
+ tree.append((here / name).relative_to(root).as_posix())
90
+ return sorted(tree)
91
+
92
+
93
+ def root_texts(root: Path) -> dict[str, str]:
94
+ """The text of each file directly in `root` that is not hidden, small enough, and UTF-8."""
95
+ texts = {}
96
+ for path in sorted(root.iterdir()):
97
+ if path.name.startswith(".") or path.is_symlink() or not path.is_file():
98
+ continue
99
+ if path.stat().st_size > MAX_ROOT_FILE_BYTES:
100
+ continue
101
+ try:
102
+ texts[path.name] = path.read_bytes().decode("utf-8-sig")
103
+ except UnicodeDecodeError:
104
+ continue
105
+ return texts
106
+
107
+
108
+ def workspaces_payload(source_root: str) -> dict:
109
+ root = _source_root(source_root)
110
+ return {"tree": list_tree(root), "manifests": root_texts(root)}
111
+
112
+
113
+ # --------------------------------------------------------------------------
114
+ # CLI
115
+ # --------------------------------------------------------------------------
116
+
117
+
118
+ def _cmd_detect_workspaces(args: argparse.Namespace) -> dict:
119
+ return workspaces_payload(args.source_root)
120
+
121
+
122
+ def _build_parser() -> argparse.ArgumentParser:
123
+ parser = argparse.ArgumentParser(
124
+ prog="stage-helper-payload",
125
+ description="Print a shared helper's stdin payload, read from the files on disk.",
126
+ )
127
+ sub = parser.add_subparsers(dest="cmd", required=True)
128
+
129
+ p = sub.add_parser("detect-workspaces", help="the tree and root files for skf-detect-workspaces.py")
130
+ p.add_argument("--source-root", required=True, help="the local source folder")
131
+ p.set_defaults(func=_cmd_detect_workspaces)
132
+
133
+ return parser
134
+
135
+
136
+ def main(argv: list[str] | None = None) -> int:
137
+ args = _build_parser().parse_args(argv)
138
+ try:
139
+ payload = args.func(args)
140
+ except (StageError, OSError) as exc:
141
+ print(json.dumps({"status": "error", "error": str(exc)}), file=sys.stderr)
142
+ return 1
143
+ print(json.dumps(payload))
144
+ return 0
145
+
146
+
147
+ if __name__ == "__main__":
148
+ sys.exit(main())