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,887 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """SKF Detect Registry: a component library's files, its demo files and its
6
+ component registry, listed, counted and scored for create-skill step 3d.
7
+
8
+ create-skill's component-library extraction (references/component-extraction.md)
9
+ leaves demo and example files out before it extracts, and reads the
10
+ component registry, an array of entries such as `{id, name, category}`, as
11
+ the library's primary API surface. Listing the files in the brief's scope,
12
+ counting the demo files per pattern, scoring each registry candidate by its
13
+ entries and fields and listing the registry's entries is fixed work, and the
14
+ score alone decides whether a headless run uses a candidate, so this helper
15
+ does all of it. The step keeps the user's confirmation and the judgment on
16
+ whether a borderline candidate really is the registry.
17
+
18
+ CLI:
19
+ uv run skf-detect-registry.py demo (--source-root <dir> | --files-from <list>) \\
20
+ [--brief <skill-brief.yaml>] [--pattern GLOB]... [--files-to <file>] \\
21
+ [--kept-to <file>]
22
+ uv run skf-detect-registry.py registry --files-from <list> --source-root <dir>
23
+ uv run skf-detect-registry.py registry --source-root <dir> --path <file> [--entries]
24
+ uv run skf-detect-registry.py registry --files-from <list> --candidates-only
25
+
26
+ --files-from reads a file list: one path per line, relative to the source
27
+ root, or a JSON list of paths; `-` reads it from stdin. A list that opens
28
+ with `[` but is not a JSON list of strings is read line by line (a Next.js
29
+ route such as `[slug]/page.tsx` can come first). It also reads the listing
30
+ skf-github-probe.py tree --out writes (the sibling in this folder), the
31
+ tree.json skf-brief-skill stages in its run folder: a JSON object whose
32
+ `tree` lists the paths; one whose `status` is not `ok` is a usage error.
33
+
34
+ demo
35
+ ----
36
+ Which files. --source-root lists the files under it: those git lists there
37
+ (tracked, and untracked but not ignored), or every file when it is not in a
38
+ git work tree, leaving out hidden entries and symbolic links, as
39
+ skf-extract-public-api.py --mode full lists a tree. --files-from takes the
40
+ paths a list names instead (a remote repository's tree listing). --brief
41
+ keeps those its scope.include globs match (every file when it has none) and
42
+ no scope.exclude glob does: the step's filtered file list. --files-to writes
43
+ that list, and --kept-to the files of it no demo pattern matches, each as a
44
+ JSON list, which --files-from here and skf-extract-public-api.py's
45
+ --files-from both read whatever the paths hold.
46
+
47
+ Which patterns. Without --pattern, the auto-detected patterns (DEMO_FOLDERS
48
+ as `**/<folder>/**`, DEMO_FILE_PATTERNS as `**/<pattern>`) are tried and
49
+ only those that match a file are reported. With --pattern (the brief's
50
+ `scope.demo_patterns`, or the patterns the user gave), each is reported,
51
+ matched or not, and `also_matched` reports the auto-detected patterns that
52
+ match a file the given ones leave in. A given pattern is read as a glob,
53
+ with two shorthands: a name ending in `/` with no other `/` is a folder at
54
+ any depth (`examples/` is `**/examples/**`), and a pattern with no `/` is a
55
+ file name at any depth (`*.stories.*` is `**/*.stories.*`). Globs follow
56
+ skf-resolve-authoritative-files.py's rules (the sibling in this folder):
57
+ `**` spans any number of path segments, none included, and `*` and `?` stay
58
+ inside one.
59
+
60
+ {
61
+ "mode": "auto" | "given",
62
+ "files_listed": N, # the files --source-root or --files-from gave
63
+ "files_scanned": N, # those in --brief's scope
64
+ "patterns": [{"pattern": "<glob>", "files": N,
65
+ "sample": ["<path>", ...]}, ...], # up to 3 paths each
66
+ "excluded": N, # files at least one pattern matches
67
+ "directories": N, # distinct folders holding them
68
+ "kept": N,
69
+ "also_matched": [{"pattern", "files", "sample"}, ...], # with --pattern
70
+ "files_file": "<path>" | null,
71
+ "kept_file": "<path>" | null
72
+ }
73
+
74
+ registry
75
+ --------
76
+ The candidates are the listed files named, in priority order (`rule`):
77
+ 1 registry.ts, registry.tsx or registry.js, in any folder
78
+ 2 components.ts or components.tsx in a registry/, catalog/ or components/
79
+ folder
80
+ 3 index.ts or index.tsx in a registry/ or catalog/ folder
81
+ With --path, the one file it names is the only candidate (a brief's
82
+ `scope.registry_path`, or a path the user gave), listed or not.
83
+
84
+ Each candidate is read from --source-root, and its largest array of object
85
+ literals is taken as the registry: its entries are the object literals it
86
+ holds, and `fields` counts, for each key, the entries that have it (a
87
+ spread such as `...base` and a computed key such as `[key]: value` add no
88
+ key). The score, out of 9:
89
+ +3 every entry has `id`
90
+ +2 every entry has `name` or every entry has `component`
91
+ +2 every entry has `category` or every entry has `tags`
92
+ +1 the file's folder is registry/ or catalog/
93
+ +1 20 or more entries
94
+ A candidate qualifies with 10 or more entries and a score of 5 or more.
95
+ `selected` is the qualifying candidate with the highest score (then the
96
+ lower rule, then the path), and `headless_accept` is true when its score is
97
+ 7 or more: the score a headless run accepts a candidate at without asking.
98
+ With --path, the file is selected whatever its score, and headless_accept
99
+ is true, unless it cannot be read or lies outside --source-root: then its
100
+ `error` says why, selected is null and headless_accept false.
101
+
102
+ The parse is lexical, for JavaScript and TypeScript (JSX included): strings,
103
+ template literals, comments and regular expression literals are skipped,
104
+ and brackets are matched. A file it cannot read is listed with its `error`
105
+ and no entries.
106
+
107
+ {
108
+ "explicit": bool, # --path was given
109
+ "candidates": [{"path", "rule": 0-3, # 0: --path
110
+ "error": null | "<why it was not read>",
111
+ "array": {"name": "<binding or key>" | null,
112
+ "line": N, "annotation": "<type>" | null} | null,
113
+ "entry_count": N, "other_elements": N,
114
+ "fields": {"<key>": N, ...},
115
+ "score": N, "score_parts": {"id", "name_or_component",
116
+ "category_or_tags", "registry_folder",
117
+ "entries_20_plus"},
118
+ "qualifies": bool,
119
+ "sample": [{"line": N, "id"?, "name"?, "component"?,
120
+ "category"?, "tags"?}, ...],
121
+ "entries": [{"line": N, "keys": ["<key>", ...],
122
+ "id"?, ..., "description"?}, ...]},
123
+ ...], # entries: --entries, selected one only
124
+ "selected": "<path>" | null,
125
+ "headless_accept": bool
126
+ }
127
+
128
+ `sample` holds the first 5 entries, each with the line of its `{` and the
129
+ SAMPLE_FIELDS it has: a string's text, a list of strings, or the value's
130
+ source (whitespace collapsed), cut at 80 characters. --entries adds
131
+ `entries` to the selected candidate: every entry, with the line of its
132
+ `{`, `keys` (every key it has, in order) and the ENTRY_FIELDS it has, in
133
+ full. The candidates are listed qualifying first, in the order `selected`
134
+ is picked from. --candidates-only prints the candidate paths the list
135
+ holds, one per line (LF, on Windows too), and reads no file: a caller
136
+ without a local tree fetches exactly those into a folder and passes it as
137
+ --source-root.
138
+
139
+ Exit codes:
140
+ 0 JSON (or, with --candidates-only, the paths) printed, a
141
+ candidate-free list included
142
+ 2 usage error, or a file list, a brief or a folder that cannot be read
143
+ """
144
+
145
+ from __future__ import annotations
146
+
147
+ import argparse
148
+ import importlib.util
149
+ import json
150
+ import os
151
+ import posixpath
152
+ import re
153
+ import sys
154
+ from pathlib import Path
155
+ from typing import NamedTuple
156
+
157
+ DEMO_FOLDERS = ("demo", "demos", "stories", "__stories__", "storybook", "examples", "example")
158
+ DEMO_FILE_PATTERNS = ("*.stories.*", "*.story.*", "*.example.*", "*.demo.*")
159
+ DEMO_SAMPLE = 3
160
+
161
+ # (rule, file names, folders the file must sit in; None for any folder)
162
+ REGISTRY_RULES = (
163
+ (1, ("registry.ts", "registry.tsx", "registry.js"), None),
164
+ (2, ("components.ts", "components.tsx"), ("registry", "catalog", "components")),
165
+ (3, ("index.ts", "index.tsx"), ("registry", "catalog")),
166
+ )
167
+ REGISTRY_FOLDERS = ("registry", "catalog")
168
+ MIN_ENTRIES = 10
169
+ MIN_SCORE = 5
170
+ AUTO_ACCEPT_SCORE = 7
171
+ MANY_ENTRIES = 20
172
+ SAMPLE_SIZE = 5
173
+ SAMPLE_FIELDS = ("id", "name", "component", "category", "tags")
174
+ ENTRY_FIELDS = (*SAMPLE_FIELDS, "description")
175
+ VALUE_TEXT_LIMIT = 80
176
+
177
+ _SIBLINGS: dict[str, object] = {}
178
+
179
+
180
+ class UsageError(Exception):
181
+ """An input the helper cannot use (exit 2)."""
182
+
183
+
184
+ # --------------------------------------------------------------------------
185
+ # File list
186
+ # --------------------------------------------------------------------------
187
+
188
+
189
+ def _norm(path: object) -> str:
190
+ """A path as the helper compares it: forward slashes, no leading `./`."""
191
+ text = str(path or "").strip().replace("\\", "/")
192
+ while text.startswith("./"):
193
+ text = text[2:]
194
+ return text
195
+
196
+
197
+ def _sibling(filename: str):
198
+ """A helper beside this script, loaded once: skf-resolve-authoritative-files.py
199
+ (the brief, its scope and the glob rules) and skf-source-tree.py (git
200
+ without the caller's hooks, never one planted in the current folder)."""
201
+ module = _SIBLINGS.get(filename)
202
+ if module is None:
203
+ path = Path(__file__).resolve().parent / filename
204
+ name = "skf_" + filename.removeprefix("skf-").removesuffix(".py").replace("-", "_")
205
+ spec = importlib.util.spec_from_file_location(name, path)
206
+ if spec is None or spec.loader is None:
207
+ raise UsageError(f"cannot load {filename} beside {Path(__file__).name}")
208
+ module = importlib.util.module_from_spec(spec)
209
+ spec.loader.exec_module(module)
210
+ _SIBLINGS[filename] = module
211
+ return module
212
+
213
+
214
+ def _hidden(rel: str) -> bool:
215
+ return any(part.startswith(".") for part in rel.split("/"))
216
+
217
+
218
+ def list_tree(root: Path) -> list[str]:
219
+ """The files under `root` (see the demo section of the module docstring),
220
+ as sorted POSIX paths relative to it: those git lists in a git work
221
+ tree, else every file, as when git lists none."""
222
+ listed: list[str] = []
223
+ res = _sibling("skf-source-tree.py")._git(root, "ls-files", "-z", "--cached", "--others", "--exclude-standard")
224
+ if res is not None and res[0] == 0:
225
+ for raw in res[1].split(b"\0"):
226
+ rel = os.fsdecode(raw).replace("\\", "/") if raw else ""
227
+ if rel and (root / rel).is_file() and not (root / rel).is_symlink():
228
+ listed.append(rel)
229
+ if not listed:
230
+ for dirpath, dirnames, filenames in os.walk(root):
231
+ rel_dir = os.path.relpath(dirpath, root).replace(os.sep, "/")
232
+ rel_dir = "" if rel_dir == "." else rel_dir
233
+ dirnames[:] = sorted(d for d in dirnames
234
+ if not d.startswith(".") and not os.path.islink(os.path.join(dirpath, d)))
235
+ for name in filenames:
236
+ if not name.startswith(".") and not os.path.islink(os.path.join(dirpath, name)):
237
+ listed.append(f"{rel_dir}/{name}" if rel_dir else name)
238
+ return sorted(rel for rel in listed if not _hidden(rel))
239
+
240
+
241
+ def brief_scope(brief: str) -> tuple[list[str], list[str]]:
242
+ """(scope.include, scope.exclude) of a skill brief, as globs."""
243
+ resolver = _sibling("skf-resolve-authoritative-files.py")
244
+ try:
245
+ includes, excludes, _amendments = resolver.extract_scope(resolver.load_brief(Path(brief)))
246
+ except ValueError as exc:
247
+ raise UsageError(str(exc)) from exc
248
+ return [g for g in map(_norm, includes) if g], [g for g in map(_norm, excludes) if g]
249
+
250
+
251
+ def in_scope(files: list[str], includes: list[str], excludes: list[str]) -> list[str]:
252
+ """The files an include glob matches (every file when there is none) and no exclude glob does."""
253
+ glob_match = _sibling("skf-resolve-authoritative-files.py").glob_match
254
+ return [rel for rel in files
255
+ if (not includes or any(glob_match(rel, g) for g in includes))
256
+ and not any(glob_match(rel, g) for g in excludes)]
257
+
258
+
259
+ def read_file_list(source: str) -> list[str]:
260
+ """The paths of a --files-from list (a file, or `-` for stdin), in order, each once."""
261
+ if source == "-":
262
+ text = sys.stdin.read()
263
+ else:
264
+ try:
265
+ text = Path(source).read_text(encoding="utf-8")
266
+ except (OSError, UnicodeDecodeError) as exc:
267
+ raise UsageError(f"cannot read file list {source}: {exc}") from exc
268
+ items: object = None
269
+ if text.lstrip().startswith(("[", "{")):
270
+ try:
271
+ items = json.loads(text)
272
+ except ValueError:
273
+ items = None
274
+ if isinstance(items, dict):
275
+ # skf-github-probe.py tree --out: the paths are its `tree`
276
+ if items.get("status", "ok") != "ok":
277
+ raise UsageError(f"file list {source} reports a failure ({items.get('status')}): "
278
+ f"{items.get('message') or 'no message'}")
279
+ items = items.get("tree")
280
+ if not isinstance(items, list) or not all(isinstance(item, str) for item in items):
281
+ raise UsageError(f"file list {source} holds no `tree` list of paths")
282
+ if not isinstance(items, list) or not all(isinstance(item, str) for item in items):
283
+ items = text.splitlines()
284
+ return list(dict.fromkeys(rel for rel in (_norm(item) for item in items) if rel))
285
+
286
+
287
+ # --------------------------------------------------------------------------
288
+ # demo
289
+ # --------------------------------------------------------------------------
290
+
291
+
292
+ def auto_patterns() -> list[str]:
293
+ """The auto-detected demo patterns, folders first, as globs."""
294
+ return [f"**/{name}/**" for name in DEMO_FOLDERS] + [f"**/{pattern}" for pattern in DEMO_FILE_PATTERNS]
295
+
296
+
297
+ def normalize_pattern(pattern: str) -> str | None:
298
+ """A given demo pattern as a glob (see the demo section of the module docstring)."""
299
+ text = _norm(pattern)
300
+ if not text:
301
+ return None
302
+ if text.endswith("/"):
303
+ folder = text.rstrip("/")
304
+ if not folder:
305
+ return None
306
+ return f"**/{folder}/**" if "/" not in folder else f"{folder}/**"
307
+ return text if "/" in text else f"**/{text}"
308
+
309
+
310
+ def _matches(files: list[str], patterns: list[str]) -> tuple[dict[str, list[str]], list[str], list[str]]:
311
+ """(the files each pattern matches, the files any matches, the others)."""
312
+ glob_match = _sibling("skf-resolve-authoritative-files.py").glob_match
313
+ matches: dict[str, list[str]] = {p: [] for p in patterns}
314
+ hit: list[str] = []
315
+ missed: list[str] = []
316
+ for rel in files:
317
+ found = [p for p in patterns if glob_match(rel, p)]
318
+ for p in found:
319
+ matches[p].append(rel)
320
+ (hit if found else missed).append(rel)
321
+ return matches, hit, missed
322
+
323
+
324
+ def _report(matches: dict[str, list[str]], every: bool) -> list[dict]:
325
+ return [{"pattern": p, "files": len(found), "sample": sorted(found)[:DEMO_SAMPLE]}
326
+ for p, found in matches.items() if found or every]
327
+
328
+
329
+ def detect_demo(files: list[str], given: list[str] | None = None) -> tuple[dict, list[str]]:
330
+ """(the demo result, the kept files) for a file list."""
331
+ if given is None:
332
+ patterns, mode = auto_patterns(), "auto"
333
+ else:
334
+ patterns = list(dict.fromkeys(p for p in (normalize_pattern(g) for g in given) if p))
335
+ mode = "given"
336
+ matches, excluded, kept = _matches(files, patterns)
337
+ also = _report(_matches(kept, auto_patterns())[0], False) if mode == "given" else []
338
+ result = {
339
+ "mode": mode,
340
+ "files_listed": len(files),
341
+ "files_scanned": len(files),
342
+ "patterns": _report(matches, mode == "given"),
343
+ "excluded": len(excluded),
344
+ "directories": len({posixpath.dirname(rel) for rel in excluded}),
345
+ "kept": len(kept),
346
+ "also_matched": also,
347
+ "files_file": None,
348
+ "kept_file": None,
349
+ }
350
+ return result, kept
351
+
352
+
353
+ # --------------------------------------------------------------------------
354
+ # registry: lexical scan
355
+ # --------------------------------------------------------------------------
356
+
357
+
358
+ class Token(NamedTuple):
359
+ kind: str # "punct", "str", "ident", "num", "other"
360
+ text: str # a string's text, else the token as written
361
+ start: int
362
+ end: int
363
+ line: int
364
+
365
+
366
+ _IDENT_RE = re.compile(r"[A-Za-z_$À-￿][\w$À-￿]*")
367
+ _NUMBER_RE = re.compile(r"\d[\w.]*")
368
+ _OPENERS = {"(": ")", "[": "]", "{": "}"}
369
+ _CLOSERS = {")", "]", "}"}
370
+ # After one of these a `/` opens a regular expression literal, not a division.
371
+ _REGEX_KEYWORDS = frozenset({"return", "typeof", "case", "in", "of", "new", "delete", "void", "throw",
372
+ "instanceof", "yield", "await", "else", "do"})
373
+ _ESCAPES = {"n": "\n", "t": "\t", "r": "\r", "b": "\b", "f": "\f", "v": "\v", "0": "\0"}
374
+
375
+
376
+ def _quoted(src: str, i: int) -> tuple[str, int] | None:
377
+ """(text, index after the closing quote) of the string opening at src[i], or None
378
+ when the line ends first (an apostrophe in JSX text is no string)."""
379
+ quote = src[i]
380
+ out: list[str] = []
381
+ j = i + 1
382
+ while j < len(src):
383
+ ch = src[j]
384
+ if ch == "\\" and j + 1 < len(src):
385
+ nxt = src[j + 1]
386
+ if nxt == "\n":
387
+ j += 2
388
+ continue
389
+ out.append(_ESCAPES.get(nxt, nxt))
390
+ j += 2
391
+ continue
392
+ if ch == quote:
393
+ return "".join(out), j + 1
394
+ if ch == "\n":
395
+ return None
396
+ out.append(ch)
397
+ j += 1
398
+ return None
399
+
400
+
401
+ def _skip_template(src: str, i: int) -> int:
402
+ """The index after the template literal opening at src[i] (a backtick),
403
+ its `${ ... }` expressions included, however nested."""
404
+ j = i + 1
405
+ while j < len(src):
406
+ ch = src[j]
407
+ if ch == "\\":
408
+ j += 2
409
+ continue
410
+ if ch == "`":
411
+ return j + 1
412
+ if ch == "$" and src.startswith("${", j):
413
+ j = _skip_expression(src, j + 2)
414
+ continue
415
+ j += 1
416
+ return len(src)
417
+
418
+
419
+ def _skip_expression(src: str, j: int) -> int:
420
+ """The index after the `}` that closes a template expression starting at src[j]."""
421
+ depth = 0
422
+ while j < len(src):
423
+ ch = src[j]
424
+ if ch in "'\"":
425
+ quoted = _quoted(src, j)
426
+ j = quoted[1] if quoted else j + 1
427
+ continue
428
+ if ch == "`":
429
+ j = _skip_template(src, j)
430
+ continue
431
+ if ch == "{":
432
+ depth += 1
433
+ elif ch == "}":
434
+ if depth == 0:
435
+ return j + 1
436
+ depth -= 1
437
+ j += 1
438
+ return len(src)
439
+
440
+
441
+ def _regex_end(src: str, i: int) -> int | None:
442
+ """The index after the regular expression literal opening at src[i], or None
443
+ when the line ends first."""
444
+ j = i + 1
445
+ in_class = False
446
+ while j < len(src):
447
+ ch = src[j]
448
+ if ch == "\n":
449
+ return None
450
+ if ch == "\\":
451
+ j += 2
452
+ continue
453
+ if ch == "[":
454
+ in_class = True
455
+ elif ch == "]":
456
+ in_class = False
457
+ elif ch == "/" and not in_class:
458
+ j += 1
459
+ while j < len(src) and (src[j].isalnum() or src[j] == "_"):
460
+ j += 1
461
+ return j
462
+ j += 1
463
+ return None
464
+
465
+
466
+ def _regex_allowed(prev: Token | None) -> bool:
467
+ """True when a `/` after `prev` opens a regular expression literal."""
468
+ if prev is None:
469
+ return True
470
+ if prev.kind == "ident":
471
+ return prev.text in _REGEX_KEYWORDS
472
+ if prev.kind in ("str", "num"):
473
+ return False
474
+ return prev.text not in (")", "]", "}", "<")
475
+
476
+
477
+ def tokenize(src: str) -> list[Token]:
478
+ """The tokens of a JavaScript or TypeScript source, comments left out."""
479
+ tokens: list[Token] = []
480
+ line_starts = [0] + [m.end() for m in re.finditer("\n", src)]
481
+
482
+ def line_of(pos: int) -> int:
483
+ lo, hi = 0, len(line_starts) - 1
484
+ while lo < hi:
485
+ mid = (lo + hi + 1) // 2
486
+ if line_starts[mid] <= pos:
487
+ lo = mid
488
+ else:
489
+ hi = mid - 1
490
+ return lo + 1
491
+
492
+ i = 0
493
+ n = len(src)
494
+ while i < n:
495
+ ch = src[i]
496
+ if ch.isspace():
497
+ i += 1
498
+ continue
499
+ if src.startswith("//", i):
500
+ end = src.find("\n", i)
501
+ i = n if end == -1 else end
502
+ continue
503
+ if src.startswith("/*", i):
504
+ end = src.find("*/", i + 2)
505
+ i = n if end == -1 else end + 2
506
+ continue
507
+ start = i
508
+ if ch in "'\"":
509
+ quoted = _quoted(src, i)
510
+ if quoted is None:
511
+ tokens.append(Token("other", ch, start, i + 1, line_of(start)))
512
+ i += 1
513
+ else:
514
+ tokens.append(Token("str", quoted[0], start, quoted[1], line_of(start)))
515
+ i = quoted[1]
516
+ continue
517
+ if ch == "`":
518
+ end = _skip_template(src, i)
519
+ tokens.append(Token("str", src[i + 1:end - 1], start, end, line_of(start)))
520
+ i = end
521
+ continue
522
+ if ch == "/" and _regex_allowed(tokens[-1] if tokens else None):
523
+ end = _regex_end(src, i)
524
+ if end is not None:
525
+ tokens.append(Token("other", src[i:end], start, end, line_of(start)))
526
+ i = end
527
+ continue
528
+ m = _IDENT_RE.match(src, i)
529
+ if m:
530
+ tokens.append(Token("ident", m.group(0), start, m.end(), line_of(start)))
531
+ i = m.end()
532
+ continue
533
+ m = _NUMBER_RE.match(src, i)
534
+ if m:
535
+ tokens.append(Token("num", m.group(0), start, m.end(), line_of(start)))
536
+ i = m.end()
537
+ continue
538
+ if src.startswith("...", i):
539
+ tokens.append(Token("punct", "...", start, i + 3, line_of(start)))
540
+ i += 3
541
+ continue
542
+ if src.startswith("=>", i):
543
+ tokens.append(Token("punct", "=>", start, i + 2, line_of(start)))
544
+ i += 2
545
+ continue
546
+ tokens.append(Token("punct", ch, start, i + 1, line_of(start)))
547
+ i += 1
548
+ return tokens
549
+
550
+
551
+ def match_brackets(tokens: list[Token]) -> dict[int, int]:
552
+ """Index of each opening bracket -> index of its closing one (unmatched ones left out)."""
553
+ pairs: dict[int, int] = {}
554
+ stack: list[int] = []
555
+ for index, token in enumerate(tokens):
556
+ if token.kind != "punct":
557
+ continue
558
+ if token.text in _OPENERS:
559
+ stack.append(index)
560
+ elif token.text in _CLOSERS:
561
+ while stack:
562
+ opener = stack.pop()
563
+ if _OPENERS[tokens[opener].text] == token.text:
564
+ pairs[opener] = index
565
+ break
566
+ return pairs
567
+
568
+
569
+ def _split(tokens: list[Token], pairs: dict[int, int], start: int, end: int) -> list[tuple[int, int]]:
570
+ """The comma-separated parts of tokens[start:end] as (start, end) ranges, empty ones left out."""
571
+ parts: list[tuple[int, int]] = []
572
+ i = head = start
573
+ while i < end:
574
+ token = tokens[i]
575
+ if token.kind == "punct" and token.text in _OPENERS and i in pairs:
576
+ i = pairs[i] + 1
577
+ continue
578
+ if token.kind == "punct" and token.text == ",":
579
+ if i > head:
580
+ parts.append((head, i))
581
+ head = i + 1
582
+ i += 1
583
+ if end > head:
584
+ parts.append((head, end))
585
+ return parts
586
+
587
+
588
+ def _source(src: str, tokens: list[Token], start: int, end: int) -> str:
589
+ return " ".join(src[tokens[start].start:tokens[end - 1].end].split())
590
+
591
+
592
+ def _value(src: str, tokens: list[Token], start: int, end: int, pairs: dict[int, int]):
593
+ """An entry value: a string's text, a list of strings, or the value's source."""
594
+ if end - start == 1 and tokens[start].kind in ("str", "ident", "num"):
595
+ return tokens[start].text
596
+ if tokens[start].text == "[" and pairs.get(start) == end - 1:
597
+ items = _split(tokens, pairs, start + 1, end - 1)
598
+ if all(b - a == 1 and tokens[a].kind == "str" for a, b in items):
599
+ return [tokens[a].text for a, _ in items]
600
+ return _source(src, tokens, start, end)
601
+
602
+
603
+ def _cut(value):
604
+ """A sample value: a text longer than VALUE_TEXT_LIMIT cut short."""
605
+ if isinstance(value, str) and len(value) > VALUE_TEXT_LIMIT:
606
+ return value[:VALUE_TEXT_LIMIT - 3] + "..."
607
+ return value
608
+
609
+
610
+ def _object_entry(src: str, tokens: list[Token], pairs: dict[int, int], open_index: int) -> dict:
611
+ """{keys, line, values} of the object literal whose `{` is tokens[open_index]."""
612
+ keys: list[str] = []
613
+ values: dict = {}
614
+ for a, b in _split(tokens, pairs, open_index + 1, pairs[open_index]):
615
+ first = tokens[a]
616
+ if first.text == "...":
617
+ continue
618
+ key = None
619
+ value_start = None
620
+ if first.kind in ("ident", "str", "num"):
621
+ nxt = tokens[a + 1] if a + 1 < b else None
622
+ if nxt is None:
623
+ key, value_start = first.text, a # shorthand: `{ id, name }`
624
+ elif nxt.text == ":":
625
+ key, value_start = first.text, a + 2
626
+ elif nxt.text in ("(", "<"):
627
+ key = first.text # a method
628
+ elif first.kind == "ident" and first.text in ("get", "set", "async") and nxt.kind == "ident":
629
+ key = nxt.text
630
+ if key is None or key in keys:
631
+ continue
632
+ keys.append(key)
633
+ if key in ENTRY_FIELDS and value_start is not None and value_start < b:
634
+ values[key] = _value(src, tokens, value_start, b, pairs)
635
+ return {"keys": keys, "line": tokens[open_index].line, "values": values}
636
+
637
+
638
+ def _binding(src: str, tokens: list[Token], open_index: int) -> tuple[str | None, str | None]:
639
+ """(name, type annotation) of what the array at tokens[open_index] is bound to."""
640
+ if open_index == 0:
641
+ return None, None
642
+ prev = tokens[open_index - 1]
643
+ if prev.text == ":" and open_index >= 2 and tokens[open_index - 2].kind in ("ident", "str"):
644
+ return tokens[open_index - 2].text, None # `items: [` in an object
645
+ if prev.text == "default" and open_index >= 2 and tokens[open_index - 2].text == "export":
646
+ return "default", None
647
+ if prev.text != "=":
648
+ return None, None
649
+ # Walk back over `name: Type =` to a declared name.
650
+ i = open_index - 2
651
+ colon = None
652
+ depth = 0
653
+ while i >= 0:
654
+ token = tokens[i]
655
+ if token.kind == "punct" and token.text in _CLOSERS:
656
+ depth += 1
657
+ elif token.kind == "punct" and token.text in _OPENERS:
658
+ if depth == 0:
659
+ break
660
+ depth -= 1
661
+ elif depth == 0 and token.text in (";", "=", ","):
662
+ break
663
+ elif depth == 0 and token.text == ":" and colon is None:
664
+ colon = i
665
+ elif depth == 0 and token.kind == "ident" and i > 0 and tokens[i - 1].text in ("const", "let", "var"):
666
+ annotation = _source(src, tokens, colon + 1, open_index - 1) if colon is not None else None
667
+ return token.text, annotation
668
+ i -= 1
669
+ if open_index >= 2 and tokens[open_index - 2].kind == "ident": # `module.exports = [`
670
+ j = open_index - 2
671
+ while j >= 2 and tokens[j - 1].text == "." and tokens[j - 2].kind == "ident":
672
+ j -= 2
673
+ return _source(src, tokens, j, open_index - 1), None
674
+ return None, None
675
+
676
+
677
+ def registry_arrays(src: str) -> list[dict]:
678
+ """Every array literal of the source that holds object literals."""
679
+ tokens = tokenize(src)
680
+ pairs = match_brackets(tokens)
681
+ arrays = []
682
+ for open_index, close_index in sorted(pairs.items()):
683
+ if tokens[open_index].text != "[":
684
+ continue
685
+ entries = []
686
+ others = 0
687
+ for a, b in _split(tokens, pairs, open_index + 1, close_index):
688
+ if tokens[a].text == "{" and pairs.get(a) == b - 1:
689
+ entries.append(_object_entry(src, tokens, pairs, a))
690
+ else:
691
+ others += 1
692
+ if entries:
693
+ name, annotation = _binding(src, tokens, open_index)
694
+ arrays.append({"name": name, "line": tokens[open_index].line, "annotation": annotation,
695
+ "entries": entries, "other_elements": others})
696
+ return arrays
697
+
698
+
699
+ # --------------------------------------------------------------------------
700
+ # registry: candidates and score
701
+ # --------------------------------------------------------------------------
702
+
703
+
704
+ def candidate_rule(rel: str) -> int | None:
705
+ """The rule (1-3) a listed path is a registry candidate by, else None."""
706
+ folder, name = posixpath.split(rel)
707
+ parent = posixpath.basename(folder)
708
+ for rule, names, folders in REGISTRY_RULES:
709
+ if name in names and (folders is None or parent in folders):
710
+ return rule
711
+ return None
712
+
713
+
714
+ def score_candidate(rel: str, entry_count: int, fields: dict[str, int]) -> dict[str, int]:
715
+ """The score parts of a candidate (see the registry section of the module docstring)."""
716
+ everywhere = {key for key, count in fields.items() if entry_count and count == entry_count}
717
+ return {
718
+ "id": 3 if "id" in everywhere else 0,
719
+ "name_or_component": 2 if everywhere & {"name", "component"} else 0,
720
+ "category_or_tags": 2 if everywhere & {"category", "tags"} else 0,
721
+ "registry_folder": 1 if posixpath.basename(posixpath.dirname(rel)) in REGISTRY_FOLDERS else 0,
722
+ "entries_20_plus": 1 if entry_count >= MANY_ENTRIES else 0,
723
+ }
724
+
725
+
726
+ def evaluate(root: Path, rel: str, rule: int, entries: bool = False) -> dict:
727
+ """One candidate, read from root and scored; with `entries`, every entry listed."""
728
+ result = {"path": rel, "rule": rule, "error": None, "array": None, "entry_count": 0,
729
+ "other_elements": 0, "fields": {}, "score": 0, "score_parts": {}, "qualifies": False,
730
+ "sample": []}
731
+ target = root / rel
732
+ try:
733
+ if not target.resolve().is_relative_to(root.resolve()):
734
+ raise OSError("the path leads outside --source-root")
735
+ src = target.read_bytes().decode("utf-8-sig")
736
+ except (OSError, UnicodeDecodeError) as exc:
737
+ result["error"] = f"cannot read {rel}: {exc}"
738
+ result["score_parts"] = score_candidate(rel, 0, {})
739
+ result["score"] = sum(result["score_parts"].values())
740
+ return result
741
+ arrays = registry_arrays(src.replace("\r\n", "\n"))
742
+ best = max(arrays, key=lambda a: len(a["entries"]), default=None) # max keeps the first of a tie
743
+ fields: dict[str, int] = {}
744
+ if best is not None:
745
+ for entry in best["entries"]:
746
+ for key in entry["keys"]:
747
+ fields[key] = fields.get(key, 0) + 1
748
+ result["array"] = {"name": best["name"], "line": best["line"], "annotation": best["annotation"]}
749
+ result["entry_count"] = len(best["entries"])
750
+ result["other_elements"] = best["other_elements"]
751
+ result["sample"] = [{"line": e["line"], **{k: _cut(v) for k, v in e["values"].items() if k in SAMPLE_FIELDS}}
752
+ for e in best["entries"][:SAMPLE_SIZE]]
753
+ if entries:
754
+ result["entries"] = [{"line": e["line"], "keys": e["keys"], **e["values"]} for e in best["entries"]]
755
+ result["fields"] = dict(sorted(fields.items()))
756
+ result["score_parts"] = score_candidate(rel, result["entry_count"], fields)
757
+ result["score"] = sum(result["score_parts"].values())
758
+ result["qualifies"] = result["entry_count"] >= MIN_ENTRIES and result["score"] >= MIN_SCORE
759
+ return result
760
+
761
+
762
+ def detect_registry(root: Path, files: list[str], path: str | None = None, entries: bool = False) -> dict:
763
+ """The registry result for a file list, or for the one file --path names;
764
+ with `entries`, the selected candidate lists every entry."""
765
+ if path is not None:
766
+ candidate = evaluate(root, _norm(path), 0, entries)
767
+ readable = candidate["error"] is None
768
+ return {"explicit": True, "candidates": [candidate],
769
+ "selected": candidate["path"] if readable else None, "headless_accept": readable}
770
+ candidates = [evaluate(root, rel, rule) for rel in files if (rule := candidate_rule(rel)) is not None]
771
+ candidates.sort(key=lambda c: (not c["qualifies"], -c["score"], c["rule"], c["path"]))
772
+ selected = candidates[0] if candidates and candidates[0]["qualifies"] else None
773
+ if selected is not None and entries:
774
+ candidates[0] = selected = evaluate(root, selected["path"], selected["rule"], True)
775
+ return {
776
+ "explicit": False,
777
+ "candidates": candidates,
778
+ "selected": selected["path"] if selected else None,
779
+ "headless_accept": bool(selected and selected["score"] >= AUTO_ACCEPT_SCORE),
780
+ }
781
+
782
+
783
+ # --------------------------------------------------------------------------
784
+ # CLI
785
+ # --------------------------------------------------------------------------
786
+
787
+
788
+ def _build_parser() -> argparse.ArgumentParser:
789
+ parser = argparse.ArgumentParser(
790
+ prog="skf-detect-registry.py",
791
+ description=__doc__,
792
+ formatter_class=argparse.RawDescriptionHelpFormatter,
793
+ )
794
+ sub = parser.add_subparsers(dest="command", required=True)
795
+ demo = sub.add_parser("demo", help="list the files in scope and count the files each demo pattern matches")
796
+ files = demo.add_mutually_exclusive_group(required=True)
797
+ files.add_argument("--source-root", metavar="DIR", help="list the files under this folder")
798
+ files.add_argument("--files-from", metavar="FILE",
799
+ help="a file list: one path per line, a JSON list or a tree listing; - for stdin")
800
+ demo.add_argument("--brief", metavar="FILE", help="keep the files in this skill brief's scope")
801
+ demo.add_argument("--pattern", action="append", metavar="GLOB",
802
+ help="a demo pattern (the brief's scope.demo_patterns or the user's); repeatable")
803
+ demo.add_argument("--files-to", metavar="FILE", help="write the files in scope here, as a JSON list")
804
+ demo.add_argument("--kept-to", metavar="FILE", help="write the files no pattern matches here, as a JSON list")
805
+ registry = sub.add_parser("registry", help="find and score the component registry candidates")
806
+ registry.add_argument("--files-from", metavar="FILE",
807
+ help="the filtered file list: one path per line, a JSON list or a tree listing; "
808
+ "- for stdin")
809
+ registry.add_argument("--source-root", metavar="DIR", help="the folder the listed paths are relative to")
810
+ registry.add_argument("--path", metavar="FILE", help="score this file only (relative to --source-root)")
811
+ registry.add_argument("--candidates-only", action="store_true",
812
+ help="print the candidate paths, one per line, and read no file")
813
+ registry.add_argument("--entries", action="store_true", help="list every entry of the selected candidate")
814
+ return parser
815
+
816
+
817
+ def _write_list(target: str, paths: list[str]) -> None:
818
+ try:
819
+ Path(target).parent.mkdir(parents=True, exist_ok=True)
820
+ Path(target).write_bytes((json.dumps(paths, ensure_ascii=False) + "\n").encode("utf-8"))
821
+ except OSError as exc:
822
+ raise UsageError(f"cannot write {target}: {exc}") from exc
823
+
824
+
825
+ def _run(args: argparse.Namespace) -> int:
826
+ if args.command == "demo":
827
+ if args.source_root is not None:
828
+ if not Path(args.source_root).is_dir():
829
+ raise UsageError(f"--source-root must name a folder, got {args.source_root!r}")
830
+ listed = list_tree(Path(args.source_root))
831
+ else:
832
+ listed = read_file_list(args.files_from)
833
+ files = in_scope(listed, *brief_scope(args.brief)) if args.brief else listed
834
+ result, kept = detect_demo(files, args.pattern)
835
+ result["files_listed"] = len(listed)
836
+ for option, key, paths in ((args.files_to, "files_file", files), (args.kept_to, "kept_file", kept)):
837
+ if option:
838
+ _write_list(option, paths)
839
+ result[key] = option
840
+ print(json.dumps(result, ensure_ascii=False))
841
+ return 0
842
+ if args.candidates_only:
843
+ if not args.files_from:
844
+ raise UsageError("--candidates-only needs --files-from")
845
+ files = read_file_list(args.files_from)
846
+ if hasattr(sys.stdout, "reconfigure"):
847
+ sys.stdout.reconfigure(newline="\n") # a shell loop reads the lines: no CR on Windows
848
+ sys.stdout.write("".join(rel + "\n" for rel in files if candidate_rule(rel) is not None))
849
+ return 0
850
+ if not args.source_root or not Path(args.source_root).is_dir():
851
+ raise UsageError(f"--source-root must name a folder, got {args.source_root!r}")
852
+ if args.path is None and not args.files_from:
853
+ raise UsageError("registry needs --files-from, or --path for one file")
854
+ files = read_file_list(args.files_from) if args.files_from and args.path is None else []
855
+ print(json.dumps(detect_registry(Path(args.source_root), files, args.path, args.entries), ensure_ascii=False))
856
+ return 0
857
+
858
+
859
+ def main(argv: list[str] | None = None) -> int:
860
+ args = _build_parser().parse_args(argv)
861
+ try:
862
+ return _run(args)
863
+ except UsageError as exc:
864
+ sys.stderr.write(f"skf-detect-registry: {exc}\n")
865
+ return 2
866
+
867
+
868
+ def _force_utf8(*streams) -> None:
869
+ """Reconfigure stdin, stdout and stderr to UTF-8, keeping each stream's error handler.
870
+
871
+ A Windows console pipes them as cp1252, which cannot print every
872
+ character of a path or a registry value the JSON carries, nor read a
873
+ UTF-8 file list piped to stdin (reconfigured here, before the first
874
+ read).
875
+ """
876
+ for stream in streams:
877
+ if hasattr(stream, "reconfigure"):
878
+ errors = getattr(stream, "errors", None)
879
+ if errors is None:
880
+ stream.reconfigure(encoding="utf-8")
881
+ else:
882
+ stream.reconfigure(encoding="utf-8", errors=errors)
883
+
884
+
885
+ if __name__ == "__main__":
886
+ _force_utf8(sys.stdin, sys.stdout, sys.stderr)
887
+ sys.exit(main())