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
@@ -1,104 +1,125 @@
1
1
  # /// script
2
- # requires-python = ">=3.9"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
4
  # ///
5
- """SKF Recommend Scope Type — deterministic 5-rule heuristic ladder.
6
-
7
- Single source of truth for the scope-type recommendation logic that
8
- skf-brief-skill step 3 §2c applies. Both the interactive recommendation
9
- and the headless auto-selection paths invoke this script with the same
10
- inputs to eliminate the drift seam between them.
11
-
12
- The five-rule ladder (apply in order, first match wins):
13
-
14
- 1. component-library — registry.ts / components.ts present (with 10+
15
- entries or `Component[]` annotation when contents available;
16
- presence-only when contents not available)
17
- 2. reference-app — intent text mentions wiring / integration / starter
18
- / build-config keywords, OR analysis flagged as demo/example
19
- 3. specific-modules — intent names a specific subset ("just the X",
20
- "only the Y") OR module_count >= 6
21
- 4. public-api — export_count <= 8 AND intent mentions "the API",
22
- "the SDK", "client library", "public API"
23
- 5. full-library — fallback when no rule matches
24
-
25
- Plus two short-circuits applied before the ladder:
26
-
27
- - source_type == "docs-only" → docs-only (no source surface to scope)
28
- - When mode == "interactive" the component-registry rule additionally
29
- inspects file contents (10+ entries or Component[] annotation); when
30
- mode == "headless" with no entry_files supplied, the script falls back
31
- to file-presence-only matching for that rule.
5
+ """SKF Recommend Scope Type: the scope-type ladder, applied to facts and to
6
+ the intent signals the caller classified.
7
+
8
+ Single source of truth for the scope-type recommendation that
9
+ skf-brief-skill step 3 §2c presents and a headless run applies. Both
10
+ paths call this script with the same inputs, so they cannot drift.
11
+
12
+ The script reads no free text. What a user's intent asks for is a
13
+ judgment: "the whole library, not just the parser" names no module, and
14
+ "Kickstarter-style" asks for no starter app although it contains the word.
15
+ So the caller reads the intent and passes three typed signals, and the
16
+ script keeps only the rules that rest on facts (the file tree, a registry
17
+ file's contents, the module and export counts, the source type) and the
18
+ order of the ladder.
19
+
20
+ The ladder (first match wins):
21
+
22
+ 0. docs-only source_type is docs-only: no source surface to scope
23
+ 1. component-library a registry.ts / components.ts (or .tsx) at any depth
24
+ of the tree whose contents (entry_files, or
25
+ --entry-dir) show 10+ entries or a Component[]
26
+ annotation. In headless mode, a registry file whose
27
+ contents were not given counts by its presence.
28
+ 2. reference-app signal wants_wiring_pattern
29
+ 3. specific-modules signal named_module_subset is not empty, else
30
+ module_count >= 6
31
+ 4. public-api signal wants_narrow_api and 0 < export_count <= 8
32
+ 5. full-library no rule matched
32
33
 
33
34
  CLI:
34
- echo '{...}' | uv run skf-recommend-scope-type.py
35
- uv run skf-recommend-scope-type.py --json '{...}'
35
+ uv run skf-recommend-scope-type.py --tree-file <file> [--entry-dir <dir>] [--extract-file <file>] < payload.json
36
+ uv run skf-recommend-scope-type.py --tree-file <file> --json '{...}'
37
+ <listing> | uv run skf-recommend-scope-type.py --tree-file - --json '{...}'
38
+ uv run skf-recommend-scope-type.py --tree-file <file> --registry-files
39
+
40
+ --tree-file reads the repository's file list from a file (`-` reads it from
41
+ stdin, and the payload then comes with --json):
42
+ - JSON: an object whose `tree` lists the paths, such as the output of
43
+ skf-github-probe.py tree (a listing whose `status` is not "ok" is
44
+ refused, with its `message`) or a GitHub git/trees response (only its
45
+ `blob` entries count); or a JSON list of paths.
46
+ - Anything else: one path per line, as `git -c core.quotePath=false
47
+ ls-files` prints them (a leading `./`, as `find .` prints it, is
48
+ dropped).
49
+ The payload may carry the list as `tree` instead; passing both is an error.
50
+
51
+ --registry-files prints the registry files the --tree-file listing holds,
52
+ one repo path per line with LF endings on every platform (nothing when there
53
+ are none), and exits without reading a payload: a shell loop fetches exactly
54
+ those files.
55
+
56
+ --entry-dir reads each registry file the tree lists from <dir>/<repo path>,
57
+ as the payload's `entry_files`: a local checkout, or the folder the caller
58
+ fetched the --registry-files paths into. A registry file missing there is
59
+ one whose contents were not given. Passing `entry_files` too is an error.
60
+
61
+ --extract-file reads the extractor's output (skf-extract-public-api.py
62
+ --mode quick, or any JSON object with an `exports` list) and sets
63
+ `export_count` to the number of distinct export names it lists, so no
64
+ caller counts the exports by hand. Passing `export_count` too is an error.
36
65
 
37
66
  Input (JSON object on stdin or via --json):
38
- intent — string (combined intent + scope_hint), default ""
39
- module_count — integer (top-level modules from step 2), default 0
40
- export_count — integer (named exports from manifest), default 0
41
- tree — list of repo-relative file paths, default []
42
- entry_files — optional [{path, content}], default null
43
- source_type — "source" | "docs-only" | null, default null
44
- mode — "interactive" | "headless", default "headless"
67
+ signals object, required unless source_type is docs-only. Exactly
68
+ these keys, classified by the caller from the user's intent
69
+ and scope hints:
70
+ wants_wiring_pattern true | false: the user wants how an
71
+ app, starter or example wires its
72
+ parts together, not a library's API
73
+ named_module_subset the module names the user limits the
74
+ skill to ([] when none)
75
+ wants_narrow_api true | false: the user wants only the
76
+ public API, SDK or client surface
77
+ module_count integer >= 0 (top-level modules from step 2), default 0
78
+ export_count integer >= 0 (named exports from step 2), default 0 (or
79
+ --extract-file)
80
+ tree list of repo-relative file paths, default [] (or --tree-file)
81
+ entry_files optional [{path, content}]: the registry files' contents
82
+ (or --entry-dir)
83
+ source_type "source" | "docs-only" | null, default null
84
+ mode "interactive" | "headless", default "headless"
85
+ A payload that carries free text (`intent`, `scope_hint`) or any other key
86
+ is refused.
45
87
 
46
88
  Output (JSON on stdout):
47
- scope_type — one of full-library | specific-modules | public-api
48
- | component-library | reference-app | docs-only
49
- matched_heuristic — "component-registry" | "reference-app-keywords"
50
- | "specific-modules-naming" | "specific-modules-count"
51
- | "narrow-public-api" | "default-full-library"
52
- | "docs-only-shortcircuit"
53
- signals — object naming the signals that fired (path, count, etc.)
54
- rationale — one-sentence explanation referencing the signals
89
+ scope_type full-library | specific-modules | public-api
90
+ | component-library | reference-app | docs-only
91
+ matched_heuristic "docs-only-shortcircuit" | "component-registry"
92
+ | "reference-app-intent" | "specific-modules-naming"
93
+ | "specific-modules-count" | "narrow-public-api"
94
+ | "default-full-library"
95
+ signals what fired (a path, a count, the signal values)
96
+ rationale one sentence naming what fired
55
97
 
56
98
  Exit codes:
57
- 0 — recommendation produced
58
- 2 — internal error (bad JSON input, IO failure)
99
+ 0 recommendation produced (or, with --registry-files, the list printed)
100
+ 2 bad input: invalid JSON, an unknown or mistyped key, no signals object,
101
+ the tree, the registry contents or the export count passed twice, a
102
+ tree listing that cannot be read, is empty or reports a failure, or an
103
+ --extract-file that cannot be read or holds no `exports` list
59
104
  """
60
105
 
61
106
  from __future__ import annotations
62
107
 
63
108
  import argparse
64
109
  import json
110
+ import os
65
111
  import re
66
112
  import sys
67
- from typing import Any
68
-
69
- REFERENCE_APP_KEYWORDS = (
70
- "wiring",
71
- "integration example",
72
- "integration sample",
73
- "starter",
74
- "build config",
75
- "build-config",
76
- "demo app",
77
- "example app",
78
- "reference app",
79
- )
80
-
81
- NARROW_PUBLIC_API_KEYWORDS = (
82
- "the api",
83
- "the sdk",
84
- "public api",
85
- "client library",
86
- "client sdk",
87
- )
88
-
89
- # Phrases that indicate the user wants only a specific subset of modules
90
- # rather than the whole library. The match is substring + lowercase, so
91
- # "just the auth module" / "only the streaming part" / "specifically the
92
- # parser" all match.
93
- SPECIFIC_MODULE_NAMING_PATTERNS = (
94
- re.compile(r"\bjust the\s+\w+", re.IGNORECASE),
95
- re.compile(r"\bonly the\s+\w+", re.IGNORECASE),
96
- re.compile(r"\bspecifically the\s+\w+", re.IGNORECASE),
97
- re.compile(r"\bjust\s+(?:want|need|use)\s+the\s+\w+", re.IGNORECASE),
98
- re.compile(r"\bonly\s+(?:want|need|use)\s+the\s+\w+", re.IGNORECASE),
99
- re.compile(r"\bjust\s+\w+\s+module\b", re.IGNORECASE),
100
- re.compile(r"\bonly\s+\w+\s+module\b", re.IGNORECASE),
101
- )
113
+
114
+ SIGNAL_KEYS = ("wants_wiring_pattern", "named_module_subset", "wants_narrow_api")
115
+ PAYLOAD_KEYS = frozenset({"signals", "module_count", "export_count", "tree", "entry_files", "source_type", "mode"})
116
+ # Free-text keys (the old payload carried `intent`), refused by name: the
117
+ # script reads no free text.
118
+ FREE_TEXT_KEYS = ("intent", "scope_hint")
119
+ # A --tree-file listing is JSON when it opens like a JSON object or list of
120
+ # paths. A bare leading bracket is not enough: a line listing can start with
121
+ # a path such as `[slug]/page.tsx`.
122
+ JSON_LISTING_RE = re.compile(r'\{\s*["}]|\[\s*["{\]]')
102
123
 
103
124
  VALID_MODES = {"interactive", "headless"}
104
125
  VALID_SOURCE_TYPES = {None, "source", "docs-only"}
@@ -115,8 +136,12 @@ def _find_registry_files(tree: list[str]) -> list[str]:
115
136
  for path in tree:
116
137
  if not isinstance(path, str):
117
138
  continue
118
- basename = path.rsplit("/", 1)[-1]
119
- if basename in {"registry.ts", "components.ts", "registry.tsx", "components.tsx"}:
139
+ parts = path.split("/")
140
+ # Never a path outside the repository: --registry-files feeds a shell
141
+ # loop that writes each file under a folder.
142
+ if path.startswith("/") or ".." in parts:
143
+ continue
144
+ if parts[-1] in {"registry.ts", "components.ts", "registry.tsx", "components.tsx"}:
120
145
  hits.append(path)
121
146
  return hits
122
147
 
@@ -186,67 +211,90 @@ def _component_registry_match(
186
211
  if isinstance(path, str) and isinstance(content, str):
187
212
  contents[path] = content
188
213
 
189
- # When we have contents for any registry file, do the deep check
214
+ # A registry file with contents is judged by them; one without contents
215
+ # counts by its presence in headless mode only.
216
+ not_given: list[str] = []
190
217
  for path in registry_paths:
191
- if path in contents:
192
- entry_count = _registry_entry_count(contents[path])
193
- has_annotation = _has_component_array_annotation(contents[path])
194
- if entry_count >= 10 or has_annotation:
195
- return {
196
- "registry_path": path,
197
- "entry_count": entry_count if entry_count >= 10 else None,
198
- "component_array_annotation": has_annotation,
199
- "contents_inspected": True,
200
- }
201
- # File present but neither threshold met — content disqualifies
218
+ if path not in contents:
219
+ not_given.append(path)
202
220
  continue
203
-
204
- # No contents available for any registry file
205
- if mode == "headless":
221
+ entry_count = _registry_entry_count(contents[path])
222
+ has_annotation = _has_component_array_annotation(contents[path])
223
+ if entry_count >= 10 or has_annotation:
224
+ return {
225
+ "registry_path": path,
226
+ "entry_count": entry_count if entry_count >= 10 else None,
227
+ "component_array_annotation": has_annotation,
228
+ "contents_inspected": True,
229
+ }
230
+ # Neither threshold met: the contents disqualify this file
231
+
232
+ if mode == "headless" and not_given:
206
233
  return {
207
- "registry_path": registry_paths[0],
234
+ "registry_path": not_given[0],
208
235
  "entry_count": None,
209
236
  "component_array_annotation": False,
210
237
  "contents_inspected": False,
211
238
  }
212
- # Interactive without contents — fall through; the rule does not match
239
+ # Interactive without contents: the rule does not match
213
240
  return None
214
241
 
215
242
 
216
- def _reference_app_match(intent_lower: str) -> dict | None:
217
- matches = [kw for kw in REFERENCE_APP_KEYWORDS if kw in intent_lower]
218
- if matches:
219
- return {"keywords": matches}
220
- return None
243
+ def _read_signals(payload: dict) -> dict:
244
+ """The caller's classified intent signals, checked for shape."""
245
+ signals = payload.get("signals")
246
+ wanted = ", ".join(SIGNAL_KEYS)
247
+ if not isinstance(signals, dict):
248
+ _die(f"signals must be an object with {wanted}, classified from the user's intent; got {signals!r}")
249
+ unknown = sorted(set(signals) - set(SIGNAL_KEYS))
250
+ missing = [key for key in SIGNAL_KEYS if key not in signals]
251
+ if unknown or missing:
252
+ _die(f"signals must have exactly {wanted}; unknown {unknown}, missing {missing}")
253
+ for key in ("wants_wiring_pattern", "wants_narrow_api"):
254
+ if not isinstance(signals[key], bool):
255
+ _die(f"signals.{key} must be true or false; got {signals[key]!r}")
256
+ names = signals["named_module_subset"]
257
+ if not isinstance(names, list) or not all(isinstance(n, str) and n.strip() for n in names):
258
+ _die(f"signals.named_module_subset must be a list of module names ([] when the intent names none); got {names!r}")
259
+ return {
260
+ "wants_wiring_pattern": signals["wants_wiring_pattern"],
261
+ "named_module_subset": [n.strip() for n in names],
262
+ "wants_narrow_api": signals["wants_narrow_api"],
263
+ }
221
264
 
222
265
 
223
- def _specific_modules_match(intent: str, module_count: int) -> tuple[str, dict] | None:
224
- naming_hits: list[str] = []
225
- for pattern in SPECIFIC_MODULE_NAMING_PATTERNS:
226
- m = pattern.search(intent)
227
- if m:
228
- naming_hits.append(m.group(0).strip())
229
- if naming_hits:
230
- return ("specific-modules-naming", {"phrases": naming_hits})
231
- if module_count >= 6:
232
- return ("specific-modules-count", {"module_count": module_count})
233
- return None
266
+ def _count(payload: dict, key: str) -> int:
267
+ value = payload.get(key)
268
+ if value is None:
269
+ return 0
270
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
271
+ _die(f"{key} must be a whole number >= 0; got {value!r}")
272
+ return value
234
273
 
235
274
 
236
- def _narrow_public_api_match(intent_lower: str, export_count: int) -> dict | None:
237
- keyword_hits = [kw for kw in NARROW_PUBLIC_API_KEYWORDS if kw in intent_lower]
238
- if keyword_hits and 0 < export_count <= 8:
239
- return {"keywords": keyword_hits, "export_count": export_count}
275
+ def _specific_modules_match(named: list[str], module_count: int) -> tuple[str, dict] | None:
276
+ if named:
277
+ return ("specific-modules-naming", {"named_module_subset": named})
278
+ if module_count >= 6:
279
+ return ("specific-modules-count", {"module_count": module_count})
240
280
  return None
241
281
 
242
282
 
243
283
  def recommend(payload: dict) -> dict:
244
- """Apply the five-rule ladder. Always returns a recommendation."""
245
- intent = (payload.get("intent") or "").strip()
246
- intent_lower = intent.lower()
247
- module_count = int(payload.get("module_count") or 0)
248
- export_count = int(payload.get("export_count") or 0)
249
- tree = payload.get("tree") or []
284
+ """Apply the ladder. Always returns a recommendation."""
285
+ for key in FREE_TEXT_KEYS:
286
+ if key in payload:
287
+ _die(f"{key} is free text, which this script no longer reads: classify it into signals "
288
+ f"({', '.join(SIGNAL_KEYS)}) and pass those")
289
+ unknown = sorted(set(payload) - PAYLOAD_KEYS)
290
+ if unknown:
291
+ _die(f"unknown payload keys {unknown}; expected {sorted(PAYLOAD_KEYS)}")
292
+
293
+ tree = payload.get("tree")
294
+ if tree is None:
295
+ tree = []
296
+ if not isinstance(tree, list):
297
+ _die(f"tree must be a list of file paths; got {type(tree).__name__}")
250
298
  entry_files = payload.get("entry_files")
251
299
  source_type = payload.get("source_type")
252
300
  mode = payload.get("mode") or "headless"
@@ -255,17 +303,23 @@ def recommend(payload: dict) -> dict:
255
303
  _die(f"source_type must be one of {sorted(t for t in VALID_SOURCE_TYPES if t)} or null; got {source_type!r}")
256
304
  if mode not in VALID_MODES:
257
305
  _die(f"mode must be one of {sorted(VALID_MODES)}; got {mode!r}")
306
+ module_count = _count(payload, "module_count")
307
+ export_count = _count(payload, "export_count")
258
308
 
259
- # Short-circuit: docs-only has no source surface to scope
309
+ # Short-circuit: docs-only has no source surface to scope, and needs no signals
260
310
  if source_type == "docs-only":
311
+ if "signals" in payload:
312
+ _read_signals(payload)
261
313
  return {
262
314
  "scope_type": "docs-only",
263
315
  "matched_heuristic": "docs-only-shortcircuit",
264
316
  "signals": {"source_type": "docs-only"},
265
- "rationale": "source_type is docs-only — there is no source surface to scope, so the brief uses the docs-only template.",
317
+ "rationale": "Docs Only because source_type is docs-only: there is no source surface to scope, so the brief uses the docs-only template.",
266
318
  }
267
319
 
268
- # Rule 1 — component-library
320
+ signals = _read_signals(payload)
321
+
322
+ # Rule 1: component-library
269
323
  cr = _component_registry_match(tree, entry_files, mode)
270
324
  if cr:
271
325
  rationale_bits = [f"a component registry was detected at {cr['registry_path']}"]
@@ -274,7 +328,7 @@ def recommend(payload: dict) -> dict:
274
328
  if cr.get("component_array_annotation"):
275
329
  rationale_bits.append("and a Component[] type annotation")
276
330
  if not cr.get("contents_inspected"):
277
- rationale_bits.append("(presence-only match — file contents not inspected in headless mode)")
331
+ rationale_bits.append("(presence-only match: file contents not inspected in headless mode)")
278
332
  return {
279
333
  "scope_type": "component-library",
280
334
  "matched_heuristic": "component-registry",
@@ -282,45 +336,40 @@ def recommend(payload: dict) -> dict:
282
336
  "rationale": "Component Library because " + " ".join(rationale_bits) + ".",
283
337
  }
284
338
 
285
- # Rule 2 — reference-app
286
- ra = _reference_app_match(intent_lower)
287
- if ra:
288
- kw_str = ", ".join(f"'{k}'" for k in ra["keywords"])
339
+ # Rule 2: reference-app
340
+ if signals["wants_wiring_pattern"]:
289
341
  return {
290
342
  "scope_type": "reference-app",
291
- "matched_heuristic": "reference-app-keywords",
292
- "signals": ra,
293
- "rationale": f"Reference App because the intent mentions {kw_str} — language consistent with a wiring-pattern skill rather than a library API.",
343
+ "matched_heuristic": "reference-app-intent",
344
+ "signals": {"wants_wiring_pattern": True},
345
+ "rationale": "Reference App because the intent asks how an app, starter or example wires its parts together, a wiring-pattern skill rather than a library API.",
294
346
  }
295
347
 
296
- # Rule 3 — specific-modules
297
- sm = _specific_modules_match(intent, module_count)
348
+ # Rule 3: specific-modules
349
+ sm = _specific_modules_match(signals["named_module_subset"], module_count)
298
350
  if sm:
299
- heuristic, signals = sm
351
+ heuristic, fired = sm
300
352
  if heuristic == "specific-modules-naming":
301
- phrases = ", ".join(f"'{p}'" for p in signals["phrases"])
302
- rationale = f"Specific Modules because the intent names a subset ({phrases})."
353
+ rationale = f"Specific Modules because the intent limits the skill to {', '.join(fired['named_module_subset'])}."
303
354
  else:
304
- rationale = f"Specific Modules because the analysis surfaced {signals['module_count']} top-level modules — likely too many for a single cohesive scope."
355
+ rationale = f"Specific Modules because the analysis surfaced {fired['module_count']} top-level modules, likely too many for a single cohesive scope."
305
356
  return {
306
357
  "scope_type": "specific-modules",
307
358
  "matched_heuristic": heuristic,
308
- "signals": signals,
359
+ "signals": fired,
309
360
  "rationale": rationale,
310
361
  }
311
362
 
312
- # Rule 4 — narrow public API
313
- pa = _narrow_public_api_match(intent_lower, export_count)
314
- if pa:
315
- kw_str = ", ".join(f"'{k}'" for k in pa["keywords"])
363
+ # Rule 4: narrow public API
364
+ if signals["wants_narrow_api"] and 0 < export_count <= 8:
316
365
  return {
317
366
  "scope_type": "public-api",
318
367
  "matched_heuristic": "narrow-public-api",
319
- "signals": pa,
320
- "rationale": f"Public API Only because the intent mentions {kw_str} and the manifest exposes {pa['export_count']} named exports — a clear narrow public surface.",
368
+ "signals": {"wants_narrow_api": True, "export_count": export_count},
369
+ "rationale": f"Public API Only because the intent asks for the public API only and the manifest exposes {export_count} named exports, a clear narrow public surface.",
321
370
  }
322
371
 
323
- # Rule 5 — fallback
372
+ # Rule 5: fallback
324
373
  return {
325
374
  "scope_type": "full-library",
326
375
  "matched_heuristic": "default-full-library",
@@ -328,19 +377,118 @@ def recommend(payload: dict) -> dict:
328
377
  "module_count": module_count,
329
378
  "export_count": export_count,
330
379
  },
331
- "rationale": "Full Library — no signal matched a narrower scope, so the default is to cover everything.",
380
+ "rationale": "Full Library because no signal matched a narrower scope, so the default is to cover everything.",
332
381
  }
333
382
 
334
383
 
335
- def _parse_argv(argv: list[str]) -> dict:
384
+ def _parse_tree_listing(text: str, source: str) -> list[str]:
385
+ """The file paths a --tree-file listing names: JSON, or one path per line."""
386
+ stripped = text.strip()
387
+ if not stripped:
388
+ _die(f"the tree listing {source} is empty; the command that wrote it may have failed")
389
+ if not JSON_LISTING_RE.match(stripped):
390
+ paths = []
391
+ for line in stripped.splitlines():
392
+ path = line.strip().removeprefix("./")
393
+ if path:
394
+ paths.append(path)
395
+ return paths
396
+ try:
397
+ data = json.loads(stripped)
398
+ except json.JSONDecodeError as e:
399
+ _die(f"the tree listing {source} is not valid JSON: {e}")
400
+ entries = data
401
+ if isinstance(data, dict):
402
+ status = data.get("status")
403
+ if status is not None and status != "ok":
404
+ _die(f"the tree listing {source} reports a failure ({status}): {data.get('message') or 'no message'}")
405
+ entries = data.get("tree")
406
+ if not isinstance(entries, list):
407
+ _die(f"the tree listing {source} holds no list of paths (a `tree` list, or a JSON list)")
408
+ paths = []
409
+ for entry in entries:
410
+ if isinstance(entry, str):
411
+ paths.append(entry)
412
+ elif isinstance(entry, dict) and isinstance(entry.get("path"), str) and entry.get("type", "blob") == "blob":
413
+ paths.append(entry["path"])
414
+ return paths
415
+
416
+
417
+ def _read_tree_file(value: str) -> list[str]:
418
+ if value == "-":
419
+ return _parse_tree_listing(sys.stdin.read(), "on stdin")
420
+ try:
421
+ with open(value, encoding="utf-8-sig") as fh:
422
+ text = fh.read()
423
+ except (OSError, UnicodeDecodeError) as e:
424
+ _die(f"cannot read --tree-file {value}: {getattr(e, 'strerror', None) or e}")
425
+ return _parse_tree_listing(text, f"in {value}")
426
+
427
+
428
+ def _read_entry_dir(folder: str, tree: object) -> list[dict]:
429
+ """The registry files the tree lists, read from `folder` laid out like the repository."""
430
+ files = []
431
+ for path in _find_registry_files(tree if isinstance(tree, list) else []):
432
+ try:
433
+ with open(os.path.join(folder, *path.split("/")), encoding="utf-8", errors="replace") as fh:
434
+ files.append({"path": path, "content": fh.read()})
435
+ except OSError:
436
+ continue # not fetched: its contents were not given
437
+ return files
438
+
439
+
440
+ def _read_export_count(value: str) -> int:
441
+ """The number of distinct export names an extractor result lists."""
442
+ try:
443
+ with open(value, encoding="utf-8-sig") as fh:
444
+ data = json.load(fh)
445
+ except (OSError, UnicodeDecodeError) as e:
446
+ _die(f"cannot read --extract-file {value}: {getattr(e, 'strerror', None) or e}")
447
+ except json.JSONDecodeError as e:
448
+ _die(f"--extract-file {value} is not valid JSON: {e}")
449
+ exports = data.get("exports") if isinstance(data, dict) else None
450
+ if not isinstance(exports, list):
451
+ _die(f"--extract-file {value} holds no `exports` list (skf-extract-public-api.py output)")
452
+ return len({e["name"] for e in exports if isinstance(e, dict) and isinstance(e.get("name"), str) and e["name"]})
453
+
454
+
455
+ def _parse_args(argv: list[str]) -> argparse.Namespace:
336
456
  parser = argparse.ArgumentParser(
337
- description="Recommend a scope type by applying the documented 5-rule heuristic ladder.",
457
+ description="Recommend a scope type: the documented ladder over facts and classified intent signals.",
338
458
  )
339
459
  parser.add_argument(
340
460
  "--json",
341
461
  help="JSON payload (alternative to stdin)",
342
462
  )
463
+ parser.add_argument(
464
+ "--tree-file",
465
+ help="the repository's file list: JSON (a `tree` list, or a list) or one path per line; - reads stdin",
466
+ )
467
+ parser.add_argument(
468
+ "--entry-dir",
469
+ help="a folder laid out like the repository that holds the registry files the tree lists (read as entry_files)",
470
+ )
471
+ parser.add_argument(
472
+ "--extract-file",
473
+ help="the extractor's output (skf-extract-public-api.py): its distinct export names are counted as export_count",
474
+ )
475
+ parser.add_argument(
476
+ "--registry-files",
477
+ action="store_true",
478
+ help="print the registry files the --tree-file listing holds, one path per line, and exit",
479
+ )
343
480
  args = parser.parse_args(argv)
481
+ if args.registry_files:
482
+ if args.tree_file is None:
483
+ _die("--registry-files lists the registry files of --tree-file, so pass it")
484
+ if args.json is not None or args.entry_dir is not None or args.extract_file is not None:
485
+ _die("--registry-files reads only --tree-file")
486
+ elif args.tree_file == "-" and args.json is None:
487
+ _die("--tree-file - reads the tree from stdin, so pass the payload with --json")
488
+ return args
489
+
490
+
491
+ def _read_payload(args: argparse.Namespace) -> dict:
344
492
  if args.json is not None:
345
493
  raw = args.json
346
494
  else:
@@ -353,6 +501,18 @@ def _parse_argv(argv: list[str]) -> dict:
353
501
  _die(f"invalid JSON input: {e}")
354
502
  if not isinstance(payload, dict):
355
503
  _die("payload must be a JSON object")
504
+ if args.tree_file is not None:
505
+ if "tree" in payload:
506
+ _die("pass the tree once: in the payload or with --tree-file, not both")
507
+ payload["tree"] = _read_tree_file(args.tree_file)
508
+ if args.entry_dir is not None:
509
+ if "entry_files" in payload:
510
+ _die("pass the registry contents once: as entry_files in the payload or with --entry-dir, not both")
511
+ payload["entry_files"] = _read_entry_dir(args.entry_dir, payload.get("tree"))
512
+ if args.extract_file is not None:
513
+ if "export_count" in payload:
514
+ _die("pass the export count once: as export_count in the payload or with --extract-file, not both")
515
+ payload["export_count"] = _read_export_count(args.extract_file)
356
516
  return payload
357
517
 
358
518
 
@@ -377,7 +537,14 @@ def _force_utf8(*streams) -> None:
377
537
 
378
538
  def main(argv: list[str]) -> int:
379
539
  _force_utf8(sys.stdin, sys.stdout)
380
- payload = _parse_argv(argv)
540
+ args = _parse_args(argv)
541
+ if args.registry_files:
542
+ paths = _find_registry_files(_read_tree_file(args.tree_file))
543
+ if hasattr(sys.stdout, "reconfigure"):
544
+ sys.stdout.reconfigure(newline="\n") # a shell loop reads the lines: no CR on Windows
545
+ sys.stdout.write("".join(f"{path}\n" for path in paths))
546
+ return 0
547
+ payload = _read_payload(args)
381
548
  result = recommend(payload)
382
549
  json.dump(result, sys.stdout, ensure_ascii=False)
383
550
  sys.stdout.write("\n")