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
@@ -0,0 +1,760 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = ["pyyaml"]
5
+ # ///
6
+ """SKF test-skill coverage inputs: the counts, name sets and candidates coverage-check reads.
7
+
8
+ coverage-check.md decides which denominator clause applies to a skill and
9
+ whether a bookkeeping variant is a real export; everything it counts, folds
10
+ or unions on the way runs here, so no count is made by hand. Three
11
+ subcommands, each reading files and writing one JSON object:
12
+
13
+ census docs-only detection. Counts the citations SKILL.md and each .md
14
+ file directly in references/ write, by form: `[EXT:` against
15
+ the local forms `[AST:`, `[SRC:` and a stack's `[from skill:`
16
+ (`[QMD:` and `[DOC:` are counted and decide nothing). A skill is
17
+ docs-only when it cites `[EXT:` and no local form.
18
+
19
+ metadata the metadata.json and provenance-map counts. Cluster A
20
+ (`stats.exports_public_api`, the `exports[]` length) and Cluster
21
+ B (`stats.exports_documented`, the provenance entry names, the
22
+ `confidence_distribution`) under the keys
23
+ check-metadata-coherence.py reads; the provenance named exports
24
+ (`::` impl-block methods left out); the inflation signature
25
+ (`stats.exports_documented` equals `stats.effective_denominator`)
26
+ with the declared names verify-declared-numerator.py looks up
27
+ (the `exports[]` names, else the provenance named exports); and,
28
+ for a stack, its composition surface: the distinct provenance
29
+ named exports when the map has entries, else each library and
30
+ each integration pair (`{a} + {b}`, as the stack's Library Pair
31
+ Integrations heading writes it), as reconcile-coverage.py's
32
+ stack branch takes them.
33
+
34
+ surface the enumerated source surface, from every input given (their
35
+ union):
36
+ --extraction skf-extract-public-api.py --mode full output:
37
+ the names in entry_point_diff.public, less the
38
+ outside_scope names (defined in a file the brief
39
+ scopes out); exports[] only gives each name's
40
+ kind, line and signature line, looked up by the
41
+ name its defining file gives it (an aliased
42
+ re-export's `local`) and that file.
43
+ extraction_gaps are listed to read by eye.
44
+ --quick skf-extract-public-api.py --mode quick output
45
+ --per-file a subagent's per-file result (exports_found and
46
+ types_found), schema-checked as
47
+ score-signatures.py checks it
48
+ --name a name read by eye whose file is not known
49
+ --provenance the provenance entries' named exports (State 2)
50
+ --metadata the metadata.json exports[] names (States 2, 3)
51
+ A surface read from the source (--extraction, --quick or
52
+ --per-file) takes no name from --provenance and --metadata:
53
+ they are the baselines of the candidates and guards. Without
54
+ one, with --provenance and --metadata both, `state2` gives the
55
+ provenance count, the metadata count and their union count,
56
+ which compute-score.py turns into the State 2 divergence. --fold
57
+ folds the bookkeeping variants among the provenance and metadata
58
+ names to their base name (`X_def`, `X_exact`, `a11y_X`, and
59
+ `<prefix>X` for each --fold-prefix when `X` is itself a name)
60
+ unless --keep names the variant as a real export, and
61
+ `canonical` gives the provenance map's fold summary. --brief
62
+ gives the skill brief's scope and language; an extraction's
63
+ recorded scope wins over it.
64
+
65
+ Name sets (surface `sets`, each sorted): `all` (the union above);
66
+ `scope.include` and `tier_a_include` when the scope has those globs and a
67
+ record names a file: the names whose file (an extraction's definition file,
68
+ the quick output's source_file, a per-file result's file, a provenance
69
+ entry's source_file) matches a glob of the list and no scope.exclude glob,
70
+ so a name with no file is in `all` only; and, from an extraction only,
71
+ `subpaths` (the names a non-root `exports` map subpath without `*` reaches)
72
+ and `root` (the names the root entry points reach). reconcile-coverage.py
73
+ and score-signatures.py --surface-set pick one.
74
+
75
+ Candidates and guards (with an extraction, --brief or --metadata): each
76
+ set's count, the metadata `stats.effective_denominator` (--metadata), and
77
+ the extractor's own effective_denominator and its basis. Both guards run
78
+ only on a surface read from the source. The deflation guard compares the
79
+ re-derived scope.include set (the `all` set without one) with
80
+ `stats.effective_denominator` and fires above DEFLATION_PCT when the scope
81
+ has no tier_a_include; the inflation guard compares the scope.include set
82
+ with the provenance entry count (--provenance) and fires above
83
+ INFLATION_PCT when the scope has no tier_a_include; the umbrella ratio (an
84
+ extraction only) is the share of the root entry points' names that are
85
+ re-exports, and above UMBRELLA_RATIO the barrel is an umbrella. Globs follow
86
+ skf-extract-public-api.py: `**` spans any number of path segments, none
87
+ included, and `*` and `?` stay inside one.
88
+
89
+ Fallback verdict: `extraction.fallback.needed` is true when the extractor
90
+ found no ast-grep (`no-ast-grep`), read no file in scope, read files only
91
+ in languages it has no recipe for (`files_without_recipes` and no export),
92
+ or found files no recipe reads while the skill's language (the --brief
93
+ `language`, else the metadata.json `language`) is not one RECIPE_LANGUAGES
94
+ holds, so the per-file scan has to run instead. An `incomplete` run keeps
95
+ its exports; `extraction.readByEye` lists the files its errors and file
96
+ issues name.
97
+
98
+ Usage:
99
+ uv run load-coverage-inputs.py census --skill-dir <dir> [--output <file>]
100
+ uv run load-coverage-inputs.py metadata --metadata <metadata.json> \\
101
+ [--provenance <provenance-map.json>] [--output <file>]
102
+ uv run load-coverage-inputs.py surface [--extraction <file>] [--quick <file>]... \\
103
+ [--per-file <file>]... [--name <name>]... [--provenance <file>] [--metadata <file>] \\
104
+ [--brief <skill-brief.yaml>] [--fold] [--fold-prefix <prefix>]... [--keep <name>]... \\
105
+ [--output <file>]
106
+
107
+ Exit codes:
108
+ 0 the result was printed (and written to --output)
109
+ 1 an input file is missing, unreadable or not the JSON (or the YAML
110
+ brief) it should be (one line on stderr)
111
+ 2 a per-file result breaks the schema: {"valid": false, "violations":
112
+ [...]} names each breach and nothing is written to --output; or a
113
+ usage error (argparse)
114
+ """
115
+
116
+ from __future__ import annotations
117
+
118
+ import argparse
119
+ import importlib.util
120
+ import json
121
+ import math
122
+ import re
123
+ import sys
124
+ from pathlib import Path
125
+
126
+ DEFLATION_PCT = 25
127
+ INFLATION_PCT = 25
128
+ UMBRELLA_RATIO = 0.5
129
+ METHOD_SEPARATOR = "::"
130
+ CITATION_FORMS = {
131
+ "EXT": "[EXT:",
132
+ "AST": "[AST:",
133
+ "SRC": "[SRC:",
134
+ "from skill": "[from skill:",
135
+ "QMD": "[QMD:",
136
+ "DOC": "[DOC:",
137
+ }
138
+ LOCAL_FORMS = ("AST", "SRC", "from skill")
139
+ # Bookkeeping variants folded to their base name, in rule order.
140
+ FOLD_SUFFIXES = ("_def", "_exact")
141
+ FOLD_PREFIX = "a11y_"
142
+ ROOT_SUBPATHS = (None, ".")
143
+ # The languages skf-extract-public-api.py's recipes read (its LANGUAGE_FAMILIES).
144
+ RECIPE_LANGUAGES = frozenset({"python", "rust", "go", "golang", "typescript", "ts", "tsx", "javascript", "js",
145
+ "jsx", "vue"})
146
+
147
+
148
+ class InputError(Exception):
149
+ """An input file that cannot be used."""
150
+
151
+
152
+ class SchemaError(Exception):
153
+ """Per-file results that break the schema: `violations` names each breach."""
154
+
155
+ def __init__(self, violations: list[str]):
156
+ super().__init__("; ".join(violations))
157
+ self.violations = violations
158
+
159
+
160
+ _SIBLINGS: dict[str, object] = {}
161
+
162
+
163
+ def _sibling(filename: str):
164
+ """A script beside this one, loaded once: validate-inventory.py (the doc
165
+ text) and score-signatures.py (the per-file result schema)."""
166
+ module = _SIBLINGS.get(filename)
167
+ if module is None:
168
+ path = Path(__file__).resolve().parent / filename
169
+ spec = importlib.util.spec_from_file_location("skf_" + filename[:-3].replace("-", "_"), path)
170
+ if spec is None or spec.loader is None or not path.is_file():
171
+ raise InputError(f"{filename} not found beside {Path(__file__).name}")
172
+ module = importlib.util.module_from_spec(spec)
173
+ spec.loader.exec_module(module)
174
+ _SIBLINGS[filename] = module
175
+ return module
176
+
177
+
178
+ def round2(value: float) -> float:
179
+ """Round to 2 decimals with JS-compatible half-up rounding (matches compute-score.py)."""
180
+ return math.floor(value * 100 + 0.5) / 100
181
+
182
+
183
+ def _read_json(path: str, flag: str):
184
+ try:
185
+ return json.loads(Path(path).read_bytes().decode("utf-8-sig"))
186
+ except (OSError, UnicodeDecodeError) as exc:
187
+ raise InputError(f"cannot read {flag} {path}: {exc}") from exc
188
+ except json.JSONDecodeError as exc:
189
+ raise InputError(f"{flag} {path} is not JSON: {exc.msg}") from exc
190
+
191
+
192
+ def _object(path: str, flag: str) -> dict:
193
+ data = _read_json(path, flag)
194
+ if not isinstance(data, dict):
195
+ raise InputError(f"{flag} {path} is not a JSON object")
196
+ return data
197
+
198
+
199
+ def _count(value) -> int | None:
200
+ return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
201
+
202
+
203
+ def _export_name(item) -> str | None:
204
+ """The name of a metadata exports[] item: a string, or an object's name."""
205
+ if isinstance(item, str):
206
+ return item or None
207
+ if isinstance(item, dict):
208
+ for key in ("name", "export_name"):
209
+ if isinstance(item.get(key), str) and item[key]:
210
+ return item[key]
211
+ return None
212
+
213
+
214
+ def _entries(provenance: dict | None) -> list[dict]:
215
+ entries = provenance.get("entries") if isinstance(provenance, dict) else None
216
+ return [e for e in entries if isinstance(e, dict)] if isinstance(entries, list) else []
217
+
218
+
219
+ def _entry_names(provenance: dict | None) -> list[str]:
220
+ return [e["export_name"] for e in _entries(provenance)
221
+ if isinstance(e.get("export_name"), str) and e["export_name"]]
222
+
223
+
224
+ # --------------------------------------------------------------------------
225
+ # census
226
+ # --------------------------------------------------------------------------
227
+
228
+
229
+ def census(skill_dir: str) -> dict:
230
+ root = Path(skill_dir)
231
+ if not (root / "SKILL.md").is_file():
232
+ raise InputError(f"--skill-dir {skill_dir} holds no SKILL.md")
233
+ try:
234
+ text = _sibling("validate-inventory.py").load_doc_text(skill_dir)
235
+ except (OSError, UnicodeDecodeError) as exc:
236
+ raise InputError(f"cannot read the skill text under {skill_dir}: {exc}") from exc
237
+ counts = {form: text.count(marker) for form, marker in CITATION_FORMS.items()}
238
+ local = sum(counts[form] for form in LOCAL_FORMS)
239
+ refs = root / "references"
240
+ files = ["SKILL.md"] + ([f"references/{p.name}" for p in sorted(refs.glob("*.md"))] if refs.is_dir() else [])
241
+ return {
242
+ "citations": counts,
243
+ "localCitations": local,
244
+ "docsOnly": counts["EXT"] > 0 and local == 0,
245
+ "files": files,
246
+ }
247
+
248
+
249
+ # --------------------------------------------------------------------------
250
+ # metadata
251
+ # --------------------------------------------------------------------------
252
+
253
+
254
+ def _pair_name(pair) -> str | None:
255
+ if isinstance(pair, str):
256
+ return pair or None
257
+ if isinstance(pair, (list, tuple)) and len(pair) == 2 and all(isinstance(p, str) and p for p in pair):
258
+ return f"{pair[0]} + {pair[1]}"
259
+ if isinstance(pair, dict) and all(isinstance(pair.get(k), str) and pair[k] for k in ("a", "b")):
260
+ return f"{pair['a']} + {pair['b']}"
261
+ return None
262
+
263
+
264
+ def _library_name(lib) -> str | None:
265
+ if isinstance(lib, str):
266
+ return lib or None
267
+ if isinstance(lib, dict) and isinstance(lib.get("name"), str) and lib["name"]:
268
+ return lib["name"]
269
+ return None
270
+
271
+
272
+ def metadata_inputs(meta: dict, provenance: dict | None) -> dict:
273
+ stats = meta.get("stats") if isinstance(meta.get("stats"), dict) else {}
274
+ exports = meta.get("exports")
275
+ names = [n for n in (_export_name(e) for e in exports) if n] if isinstance(exports, list) else []
276
+ entry_names = _entry_names(provenance) if provenance is not None else None
277
+ named = sorted({n for n in entry_names or [] if METHOD_SEPARATOR not in n})
278
+ dist = stats.get("confidence_distribution")
279
+ if not isinstance(dist, dict):
280
+ dist = meta.get("confidence_distribution") if isinstance(meta.get("confidence_distribution"), dict) else None
281
+ if dist is not None:
282
+ dist = {tier: dist[tier] for tier in ("t1", "t1_low", "t2", "t3") if _count(dist.get(tier)) is not None}
283
+ documented = _count(stats.get("exports_documented"))
284
+ effective = _count(stats.get("effective_denominator"))
285
+ skill_type = meta.get("skill_type") if isinstance(meta.get("skill_type"), str) else None
286
+
287
+ stack = None
288
+ if skill_type == "stack":
289
+ if named:
290
+ stack = {"basis": "provenance", "denominator": len(named), "compositionNames": named}
291
+ else:
292
+ libraries = [n for n in (_library_name(lib) for lib in meta.get("libraries") or []) if n]
293
+ pairs = [n for n in (_pair_name(p) for p in meta.get("integration_pairs") or []) if n]
294
+ stack = {"basis": "composition", "denominator": len(libraries) + len(pairs),
295
+ "compositionNames": libraries + pairs}
296
+
297
+ return {
298
+ "skillType": skill_type,
299
+ "scopeType": meta.get("scope_type") if isinstance(meta.get("scope_type"), str) else None,
300
+ "clusterA": {
301
+ "exports_public_api": _count(stats.get("exports_public_api")),
302
+ "exports_length": len(exports) if isinstance(exports, list) else None,
303
+ },
304
+ "clusterB": {"exports_documented": documented},
305
+ "provenanceExportNames": entry_names,
306
+ "confidenceDistribution": dist or None,
307
+ "namedExports": {"count": len(named), "names": named} if entry_names is not None else None,
308
+ "effectiveDenominator": effective,
309
+ "inflationSignature": documented is not None and effective is not None and documented == effective,
310
+ "declaredNames": sorted(set(names)) if names else named,
311
+ "stack": stack,
312
+ }
313
+
314
+
315
+ # --------------------------------------------------------------------------
316
+ # surface
317
+ # --------------------------------------------------------------------------
318
+
319
+
320
+ _GLOB_CACHE: dict[str, re.Pattern[str]] = {}
321
+
322
+
323
+ def glob_match(rel_path: str, pattern: str) -> bool:
324
+ """skf-resolve-authoritative-files.py's glob rule, which the extractor applies."""
325
+ compiled = _GLOB_CACHE.get(pattern)
326
+ if compiled is None:
327
+ parts = pattern.split("/")
328
+ out: list[str] = []
329
+ for i, part in enumerate(parts):
330
+ if part == "**":
331
+ out.append("(?:.*/)?" if i + 1 < len(parts) else ".*")
332
+ continue
333
+ out.append("".join("[^/]*" if ch == "*" else "[^/]" if ch == "?" else re.escape(ch) for ch in part))
334
+ if i + 1 < len(parts):
335
+ out.append("/")
336
+ compiled = re.compile("^" + "".join(out) + "$")
337
+ _GLOB_CACHE[pattern] = compiled
338
+ return bool(compiled.match(rel_path))
339
+
340
+
341
+ def _norm(path) -> str:
342
+ text = str(path or "").strip().replace("\\", "/")
343
+ while text.startswith("./"):
344
+ text = text[2:]
345
+ return text.lstrip("/")
346
+
347
+
348
+ def _record(name, kind, file, line, signature_line, origin) -> dict:
349
+ return {"name": name, "kind": kind, "file": file, "line": line, "signatureLine": signature_line,
350
+ "origin": origin}
351
+
352
+
353
+ def _globs(value) -> list[str]:
354
+ return [g for g in (_norm(v) for v in value if isinstance(v, str)) if g] if isinstance(value, list) else []
355
+
356
+
357
+ def _scope(include, exclude, tier_a_include) -> dict:
358
+ return {"include": _globs(include), "exclude": _globs(exclude), "tier_a_include": _globs(tier_a_include)}
359
+
360
+
361
+ def read_brief(path: str) -> dict:
362
+ """A skill brief's scope globs and language, the fields skf-extract-public-api.py reads."""
363
+ try:
364
+ import yaml # --brief only; `uv run` installs it from the header
365
+ except ImportError as exc:
366
+ raise InputError("--brief is read with PyYAML: run the script with `uv run`") from exc
367
+ try:
368
+ data = yaml.safe_load(Path(path).read_bytes().decode("utf-8-sig"))
369
+ except (OSError, UnicodeDecodeError) as exc:
370
+ raise InputError(f"cannot read --brief {path}: {exc}") from exc
371
+ except yaml.YAMLError as exc:
372
+ raise InputError(f"--brief {path} is not valid YAML: {' '.join(str(exc).split())}") from exc
373
+ if not isinstance(data, dict):
374
+ raise InputError(f"--brief {path} is not a YAML mapping")
375
+ scope = data.get("scope") if isinstance(data.get("scope"), dict) else {}
376
+ return {"scope": _scope(scope.get("include"), scope.get("exclude"), scope.get("tier_a_include")),
377
+ "language": data.get("language") if isinstance(data.get("language"), str) else None}
378
+
379
+
380
+ def matching(records: list[dict], globs: list[str], excludes: list[str]) -> list[str]:
381
+ """The names whose record names a file one of `globs` matches and no exclude glob does."""
382
+ return sorted({r["name"] for r in records if r.get("file")
383
+ and any(glob_match(_norm(r["file"]), g) for g in globs)
384
+ and not any(glob_match(_norm(r["file"]), g) for g in excludes)})
385
+
386
+
387
+ def from_extraction(data: dict, language: str | None = None) -> dict:
388
+ """The public surface of an extraction, less what lies outside scope."""
389
+ if data.get("mode") != "full":
390
+ raise InputError("--extraction is not skf-extract-public-api.py --mode full output")
391
+ diff = data.get("entry_point_diff") if isinstance(data.get("entry_point_diff"), dict) else {}
392
+ public = [p for p in diff.get("public") or [] if isinstance(p, dict) and isinstance(p.get("name"), str)]
393
+ outside = [o for o in diff.get("outside_scope") or [] if isinstance(o, dict)]
394
+ gaps = [g for g in diff.get("extraction_gaps") or [] if isinstance(g, dict)]
395
+ outside_keys = {(o.get("name"), o.get("file")) for o in outside}
396
+ index = {(e.get("export_name"), e.get("source_file")): e
397
+ for e in data.get("exports") or [] if isinstance(e, dict)}
398
+ records = []
399
+ for info in public:
400
+ name, file = info["name"], info.get("file")
401
+ if (name, file) in outside_keys:
402
+ continue
403
+ # An aliased re-export (`export { Options as Config }`) is recorded
404
+ # under the name its defining file gives it, `local`.
405
+ found = index.get((info.get("local") or name, file)) or index.get((name, file))
406
+ kind = found.get("export_type") if found else None
407
+ records.append(_record(name, None if kind == "re-export" else kind, file,
408
+ found.get("source_line") if found else info.get("line"),
409
+ found.get("signature_line") if found else None, "extraction"))
410
+
411
+ scope = data.get("scope") if isinstance(data.get("scope"), dict) else {}
412
+
413
+ entry_points = data.get("entry_points") if isinstance(data.get("entry_points"), dict) else {}
414
+ entry_files = [e for e in entry_points.get("files") or [] if isinstance(e, dict)]
415
+ subpath_files = {e.get("file") for e in entry_files
416
+ if e.get("subpath") not in ROOT_SUBPATHS and "*" not in str(e.get("subpath"))}
417
+ root_files = {e.get("file") for e in entry_files if e.get("subpath") in ROOT_SUBPATHS}
418
+ root_names = [p for p in public if p.get("entry") in root_files]
419
+ reexported = [p for p in root_names if p.get("via") not in (None, "declaration")]
420
+
421
+ sets = {}
422
+ if subpath_files:
423
+ sets["subpaths"] = sorted({p["name"] for p in public if p.get("entry") in subpath_files})
424
+ if root_files:
425
+ sets["root"] = sorted({p["name"] for p in root_names})
426
+
427
+ counts = data.get("counts") if isinstance(data.get("counts"), dict) else {}
428
+ arms = data.get("arms") if isinstance(data.get("arms"), dict) else {}
429
+ without = data.get("files_without_recipes") if isinstance(data.get("files_without_recipes"), dict) else {}
430
+ status = data.get("status")
431
+ reason = None
432
+ if status == "no-ast-grep":
433
+ reason = "no ast-grep the runner can run"
434
+ elif not data.get("files_in_scope"):
435
+ reason = "no file in scope"
436
+ elif without and not data.get("exports"):
437
+ reason = f"no recipe reads the files in scope ({', '.join(sorted(without))})"
438
+ elif without and language and language.strip().lower() not in RECIPE_LANGUAGES:
439
+ reason = (f"no recipe reads the skill's language, {language} "
440
+ f"({', '.join(sorted(without))} in scope)")
441
+ read_by_eye = sorted({str(i["file"]) for i in data.get("file_issues") or []
442
+ if isinstance(i, dict) and i.get("file") and i.get("issue") != "no-recipes"}
443
+ | {str(e["first_file"]) for e in data.get("errors") or []
444
+ if isinstance(e, dict) and e.get("first_file")})
445
+ return {
446
+ "records": records,
447
+ "sets": sets,
448
+ "outside": [{"name": o.get("name"), "file": o.get("file")} for o in outside],
449
+ "gaps": [{"name": g.get("name"), "file": g.get("file"), "line": g.get("line")} for g in gaps],
450
+ "scope": _scope(scope.get("include"), scope.get("exclude"), scope.get("tier_a_include")),
451
+ "umbrella": {
452
+ "entries": sorted(f for f in root_files if f),
453
+ "names": len(root_names),
454
+ "reexported": len(reexported),
455
+ "ratio": round2(len(reexported) / len(root_names)) if root_names else None,
456
+ "umbrella": bool(root_names) and len(reexported) / len(root_names) > UMBRELLA_RATIO,
457
+ },
458
+ "extraction": {
459
+ "status": status,
460
+ "filesInScope": data.get("files_in_scope"),
461
+ "truncated": data.get("truncated"),
462
+ "effectiveDenominator": _count(counts.get("effective_denominator")),
463
+ "effectiveDenominatorBasis": counts.get("effective_denominator_basis"),
464
+ "exportsPublicApi": _count(counts.get("exports_public_api")),
465
+ "arms": arms,
466
+ "readByEye": read_by_eye,
467
+ "fallback": {"needed": reason is not None, "reason": reason},
468
+ },
469
+ }
470
+
471
+
472
+ def from_quick(data: dict) -> list[dict]:
473
+ exports = data.get("exports")
474
+ if not isinstance(exports, list):
475
+ raise InputError("--quick is not skf-extract-public-api.py --mode quick output (no exports list)")
476
+ return [_record(e["name"], e.get("type"), e.get("source_file"), None, None, "quick")
477
+ for e in exports if isinstance(e, dict) and isinstance(e.get("name"), str) and e["name"]]
478
+
479
+
480
+ def from_per_file(path: str) -> tuple[list[dict], list[str]]:
481
+ """(the records of one per-file result, its schema breaches)."""
482
+ scorer = _sibling("score-signatures.py")
483
+ try:
484
+ text = Path(path).read_bytes().decode("utf-8-sig")
485
+ except (OSError, UnicodeDecodeError) as exc:
486
+ raise InputError(f"cannot read --per-file {path}: {exc}") from exc
487
+ results, errors = scorer.parse_result_text(text, Path(path).name)
488
+ if errors:
489
+ return [], errors
490
+ records = []
491
+ for result in results:
492
+ types = set(result.get("types_found") or [])
493
+ for name in sorted(set(result.get("exports_found") or []) | types):
494
+ if name:
495
+ records.append(_record(name, "type" if name in types else None, result["file"], None, None,
496
+ "per-file"))
497
+ return records, []
498
+
499
+
500
+ def fold_names(names: list[str], prefixes: list[str], keep: set[str],
501
+ tallied: set[str] | None = None) -> tuple[dict[str, str], dict]:
502
+ """({variant: base}, the fold tally of `tallied`, else of every name) under the canonicalization rules."""
503
+ present = set(names)
504
+ variants: dict[str, str] = {}
505
+ tally = {"_def": 0, "_exact": 0, FOLD_PREFIX: 0, "other": 0}
506
+ for name in sorted(present):
507
+ if name in keep:
508
+ continue
509
+ rule, base = None, None
510
+ for suffix in FOLD_SUFFIXES:
511
+ if name.endswith(suffix) and len(name) > len(suffix):
512
+ rule, base = suffix, name[: -len(suffix)]
513
+ break
514
+ if rule is None and name.startswith(FOLD_PREFIX) and len(name) > len(FOLD_PREFIX):
515
+ rule, base = FOLD_PREFIX, name[len(FOLD_PREFIX):]
516
+ if rule is None:
517
+ for prefix in prefixes:
518
+ if prefix and name.startswith(prefix) and name[len(prefix):] in present:
519
+ rule, base = "other", name[len(prefix):]
520
+ break
521
+ if rule is not None:
522
+ variants[name] = base
523
+ if tallied is None or name in tallied:
524
+ tally[rule] += 1
525
+ return variants, tally
526
+
527
+
528
+ NO_UMBRELLA = {"entries": [], "names": 0, "reexported": 0, "ratio": None, "umbrella": False}
529
+
530
+
531
+ def surface(args: argparse.Namespace) -> dict:
532
+ records: list[dict] = []
533
+ sets: dict[str, list[str]] = {}
534
+ warnings: list[str] = []
535
+ out: dict = {"candidates": None, "guards": None, "state2": None, "canonical": None,
536
+ "extraction": None, "extractionGaps": [], "excluded": {"outsideScope": []}}
537
+
538
+ meta = _object(args.metadata, "--metadata") if args.metadata else None
539
+ provenance = _object(args.provenance, "--provenance") if args.provenance else None
540
+ brief = read_brief(args.brief) if args.brief else None
541
+ language = (brief or {}).get("language")
542
+ if not language and meta is not None and isinstance(meta.get("language"), str):
543
+ language = meta["language"]
544
+
545
+ extracted = None
546
+ if args.extraction:
547
+ extracted = from_extraction(_object(args.extraction, "--extraction"), language)
548
+ records += extracted["records"]
549
+ sets.update(extracted["sets"])
550
+ out["extraction"] = extracted["extraction"]
551
+ out["extractionGaps"] = extracted["gaps"]
552
+ out["excluded"]["outsideScope"] = extracted["outside"]
553
+ for path in args.quick or []:
554
+ data = _object(path, "--quick")
555
+ records += from_quick(data)
556
+ warnings += [f"{Path(path).name}: {w}" for w in data.get("warnings") or [] if isinstance(w, str)]
557
+ violations: list[str] = []
558
+ for path in args.per_file or []:
559
+ found, errors = from_per_file(path)
560
+ records += found
561
+ violations += errors
562
+ if violations:
563
+ raise SchemaError(violations)
564
+ records += [_record(name, None, None, None, None, "by-eye") for name in args.name or [] if name]
565
+
566
+ # A surface read from the source takes no name from the provenance map or
567
+ # metadata.json: they are only the baselines of the candidates and guards.
568
+ source_read = extracted is not None or bool(args.quick) or bool(args.per_file)
569
+ if not source_read and (provenance is not None or meta is not None):
570
+ named = [e for e in _entries(provenance)
571
+ if isinstance(e.get("export_name"), str) and e["export_name"]
572
+ and METHOD_SEPARATOR not in e["export_name"]]
573
+ prov_names = sorted({e["export_name"] for e in named})
574
+ exports = meta.get("exports") if meta is not None else None
575
+ meta_names = sorted({n for n in (_export_name(e) for e in exports) if n}) if isinstance(exports, list) else []
576
+ base_of: dict[str, str] = {}
577
+ if args.fold:
578
+ # One fold over both lists, so a variant folded out of the map
579
+ # never comes back through metadata.json.
580
+ base_of, tally = fold_names(sorted(set(prov_names) | set(meta_names)), args.fold_prefix or [],
581
+ set(args.keep or []), tallied=set(prov_names))
582
+ if provenance is not None:
583
+ canonical = sorted({base_of.get(n, n) for n in prov_names})
584
+ out["canonical"] = {
585
+ "raw": len(prov_names),
586
+ "canonical": len(canonical),
587
+ "folded": tally,
588
+ "variants": {n: base_of[n] for n in prov_names if n in base_of},
589
+ "summary": (f"Provenance-map canonicalization: {len(prov_names)} raw entries → "
590
+ f"{len(canonical)} canonical bases ({len(prov_names) - len(canonical)} "
591
+ f"bookkeeping variants folded: _def×{tally['_def']}, _exact×{tally['_exact']}, "
592
+ f"a11y_×{tally[FOLD_PREFIX]}, other×{tally['other']})"),
593
+ }
594
+ seen = set()
595
+ for e in named:
596
+ name = base_of.get(e["export_name"], e["export_name"])
597
+ if name in seen:
598
+ continue
599
+ seen.add(name)
600
+ records.append(_record(name, e.get("export_type"), e.get("source_file"), e.get("source_line"),
601
+ None, "provenance"))
602
+ meta_bases = sorted({base_of.get(n, n) for n in meta_names})
603
+ records += [_record(n, None, None, None, None, "metadata") for n in meta_bases]
604
+ if seen and meta_bases:
605
+ out["state2"] = {
606
+ "provenanceCount": len(seen),
607
+ "metadataCount": len(meta_bases),
608
+ "unionCount": len(seen | set(meta_bases)),
609
+ "metadataOnly": sorted(set(meta_bases) - seen),
610
+ }
611
+
612
+ records.sort(key=lambda r: (r["name"], str(r["file"] or ""), r["origin"]))
613
+ sets["all"] = sorted({r["name"] for r in records})
614
+
615
+ # The scope: the one the extractor recorded, else the brief's.
616
+ scope = extracted["scope"] if extracted is not None and any(extracted["scope"].values()) else None
617
+ if scope is None:
618
+ scope = brief["scope"] if brief is not None else _scope(None, None, None)
619
+ filed = [r for r in records if r["file"]]
620
+ if filed and scope["include"]:
621
+ sets["scope.include"] = matching(filed, scope["include"], scope["exclude"])
622
+ if filed and scope["tier_a_include"]:
623
+ sets["tier_a_include"] = matching(filed, scope["tier_a_include"], scope["exclude"])
624
+
625
+ if extracted is not None or brief is not None or meta is not None:
626
+ stats = meta.get("stats") if isinstance(meta, dict) and isinstance(meta.get("stats"), dict) else {}
627
+ stats_eff = _count(stats.get("effective_denominator"))
628
+ include_union = len(sets["scope.include"]) if "scope.include" in sets else len(sets["all"])
629
+ extraction = extracted["extraction"] if extracted is not None else {}
630
+ out["candidates"] = {
631
+ "statsEffectiveDenominator": stats_eff,
632
+ "tierAIncludeUnion": len(sets["tier_a_include"]) if "tier_a_include" in sets else None,
633
+ "scopeIncludeUnion": len(sets["scope.include"]) if "scope.include" in sets else None,
634
+ "subpathUnion": len(sets["subpaths"]) if "subpaths" in sets else None,
635
+ "rootBarrel": len(sets["root"]) if "root" in sets else None,
636
+ "extractorEffectiveDenominator": extraction.get("effectiveDenominator"),
637
+ "extractorBasis": extraction.get("effectiveDenominatorBasis"),
638
+ }
639
+ no_tier_a = not scope["tier_a_include"]
640
+ deflation = {"applicable": source_read and bool(stats_eff), "effectiveDenominator": stats_eff,
641
+ "rederived": include_union if source_read else None, "pct": None, "fires": False}
642
+ if deflation["applicable"]:
643
+ deflation["pct"] = round2((include_union - stats_eff) / stats_eff * 100)
644
+ deflation["fires"] = no_tier_a and deflation["pct"] > DEFLATION_PCT
645
+ entry_count = len(_entries(provenance)) if provenance is not None else 0
646
+ inflation = {"applicable": source_read and no_tier_a and bool(scope["include"]) and entry_count > 0,
647
+ "scopeIncludeUnion": include_union if scope["include"] else None,
648
+ "provenanceEntries": entry_count if provenance is not None else None,
649
+ "pct": None, "fires": False}
650
+ if inflation["applicable"]:
651
+ inflation["pct"] = round2((include_union - entry_count) / entry_count * 100)
652
+ inflation["fires"] = inflation["pct"] > INFLATION_PCT
653
+ out["guards"] = {
654
+ "deflation": deflation,
655
+ "inflation": inflation,
656
+ "umbrella": extracted["umbrella"] if extracted is not None else dict(NO_UMBRELLA),
657
+ "thresholds": {"deflationPct": DEFLATION_PCT, "inflationPct": INFLATION_PCT,
658
+ "umbrellaRatio": UMBRELLA_RATIO},
659
+ }
660
+
661
+ return {
662
+ "inputs": {
663
+ "extraction": args.extraction, "quick": args.quick or [], "perFile": args.per_file or [],
664
+ "names": args.name or [], "provenance": args.provenance, "metadata": args.metadata,
665
+ "brief": args.brief, "fold": bool(args.fold),
666
+ },
667
+ "exports": records,
668
+ "sets": sets,
669
+ **out,
670
+ "warnings": warnings,
671
+ }
672
+
673
+
674
+ # --------------------------------------------------------------------------
675
+ # CLI
676
+ # --------------------------------------------------------------------------
677
+
678
+
679
+ def _cmd_census(args: argparse.Namespace) -> dict:
680
+ return census(args.skill_dir)
681
+
682
+
683
+ def _cmd_metadata(args: argparse.Namespace) -> dict:
684
+ meta = _object(args.metadata, "--metadata")
685
+ provenance = _object(args.provenance, "--provenance") if args.provenance else None
686
+ return metadata_inputs(meta, provenance)
687
+
688
+
689
+ def _build_parser() -> argparse.ArgumentParser:
690
+ parser = argparse.ArgumentParser(
691
+ prog="load-coverage-inputs",
692
+ description="The counts, name sets and denominator candidates coverage-check reads, from files.",
693
+ )
694
+ sub = parser.add_subparsers(dest="cmd", required=True)
695
+
696
+ p = sub.add_parser("census", help="count the citation forms: is the skill docs-only?")
697
+ p.add_argument("--skill-dir", required=True, help="the skill package: SKILL.md and references/")
698
+ p.add_argument("--output", help="also write the result to this file")
699
+ p.set_defaults(func=_cmd_census)
700
+
701
+ p = sub.add_parser("metadata", help="the metadata.json and provenance-map counts")
702
+ p.add_argument("--metadata", required=True, help="the skill's metadata.json")
703
+ p.add_argument("--provenance", help="the provenance map, when there is one")
704
+ p.add_argument("--output", help="also write the result to this file")
705
+ p.set_defaults(func=_cmd_metadata)
706
+
707
+ p = sub.add_parser("surface", help="the enumerated source surface, its name sets and candidates")
708
+ p.add_argument("--extraction", help="skf-extract-public-api.py --mode full output")
709
+ p.add_argument("--quick", action="append", help="skf-extract-public-api.py --mode quick output (repeatable)")
710
+ p.add_argument("--per-file", action="append", help="a subagent per-file result (repeatable)")
711
+ p.add_argument("--name", action="append", help="a name read by eye whose file is not known (repeatable)")
712
+ p.add_argument("--provenance", help="the provenance map: its named exports")
713
+ p.add_argument("--metadata", help="metadata.json: its exports[] names, stats and language")
714
+ p.add_argument("--brief", help="the skill brief: its scope globs and language")
715
+ p.add_argument("--fold", action="store_true", help="fold the provenance and metadata bookkeeping variants")
716
+ p.add_argument("--fold-prefix", action="append", help="a renderer prefix to fold (repeatable)")
717
+ p.add_argument("--keep", action="append", help="a variant that is a real export (repeatable)")
718
+ p.add_argument("--output", help="also write the result to this file")
719
+ p.set_defaults(func=surface)
720
+ return parser
721
+
722
+
723
+ def main(argv: list[str] | None = None) -> int:
724
+ args = _build_parser().parse_args(argv)
725
+ try:
726
+ result = args.func(args)
727
+ except InputError as exc:
728
+ print(f"error: {exc}", file=sys.stderr)
729
+ return 1
730
+ except SchemaError as exc:
731
+ if args.output:
732
+ Path(args.output).unlink(missing_ok=True)
733
+ print(json.dumps({"valid": False, "violations": exc.violations}, indent=2))
734
+ return 2
735
+ if args.output:
736
+ target = Path(args.output)
737
+ target.parent.mkdir(parents=True, exist_ok=True)
738
+ target.write_bytes((json.dumps(result, indent=2, ensure_ascii=False) + "\n").encode("utf-8"))
739
+ print(json.dumps(result, indent=2))
740
+ return 0
741
+
742
+
743
+ def _force_utf8(*streams) -> None:
744
+ """Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
745
+
746
+ A Windows console pipes them as cp1252, which cannot print every character
747
+ of the --help text, so --help would stop with UnicodeEncodeError.
748
+ """
749
+ for stream in streams:
750
+ if hasattr(stream, "reconfigure"):
751
+ errors = getattr(stream, "errors", None)
752
+ if errors is None:
753
+ stream.reconfigure(encoding="utf-8")
754
+ else:
755
+ stream.reconfigure(encoding="utf-8", errors=errors)
756
+
757
+
758
+ if __name__ == "__main__":
759
+ _force_utf8(sys.stdout, sys.stderr)
760
+ sys.exit(main())