bmad-module-skill-forge 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +15 -5
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -90
  348. package/src/skf-setup/references/detect-and-tier.md +58 -67
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -679
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -1,5 +1,5 @@
1
1
  # /// script
2
- # requires-python = ">=3.9"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
4
  # ///
5
5
  """SKF Detect Language — deterministic primary-language detection from a flat file tree.
@@ -19,8 +19,9 @@ Detection rules (apply in order, first match wins):
19
19
  "python-multi-package" → python (high)
20
20
  A non-JS workspace root's language wins over a nested package.json +
21
21
  tsconfig.json (e.g. a docs/ or website/ site that is not a workspace
22
- member). JS-family workspaces (npm/pnpm/lerna) carry no entry here and
23
- fall through to rule 1, whose root package.json correctly resolves js/ts.
22
+ member). JS-family workspaces (npm/pnpm/lerna/rush/nx) carry no entry
23
+ here and fall through to rule 1, whose package.json files correctly
24
+ resolve js/ts.
24
25
  1. package.json (with optional tsconfig.json companion):
25
26
  tsconfig.json present → typescript (high)
26
27
  tsconfig.json absent → javascript (high)
@@ -29,26 +30,70 @@ Detection rules (apply in order, first match wins):
29
30
  4. go.mod → go (high)
30
31
  5. pom.xml → java (high)
31
32
  6. build.gradle.kts → kotlin (high)
32
- 7. build.gradle (Groovy) — check tree:
33
+ 7. Package.swift → swift (high)
34
+ 8. Gemfile → ruby (high)
35
+ 9. build.gradle (Groovy), the tree decides:
33
36
  src/main/kotlin/ → kotlin (medium)
34
37
  else → java (medium)
35
- 8. Package.swift → swift (high)
36
- 9. *.csproj | *.sln → csharp (high)
37
- 10. Gemfile → ruby (high)
38
+ 10. *.csproj | *.sln → csharp (high)
38
39
  11. Extension-frequency fallback over the full tree.
39
40
  dominant extension >= 50% of code files → that language (medium)
40
41
  no clear winner → unknown (low)
41
42
 
43
+ Which manifests decide (rules 1-10). The tree's root is the deepest folder
44
+ that holds every path: the repository root for a whole listing, a unit's
45
+ own folder for the files of one unit. A manifest in a hidden folder or in
46
+ a folder _NON_CORE_PATH_SEGMENTS names (docs, examples, tests, fixtures,
47
+ benchmarks, scripts, tools, ...) below that root is not the project's own
48
+ package, so it decides only when no other manifest exists. The rules then
49
+ run over the shallowest of the remaining manifests first: the manifests of
50
+ the least deep folder that holds one decide, in rule order, so a root
51
+ pyproject.toml beats docs/package.json and pydantic-core/Cargo.toml. A
52
+ manifest at the root decides at its rule's confidence. When none sits at
53
+ the root (CPython's listing holds only Platforms/ and PCbuild/ ones), the
54
+ shallowest manifest below it decides at medium confidence, and
55
+ detected_languages ends with source_language, the language most of the
56
+ tree's source files are written in, so the caller sees both answers. The
57
+ tsconfig.json check reads every core folder (every folder when no
58
+ manifest is core), so a docs site's tsconfig.json does not make a root
59
+ package.json typescript.
60
+
42
61
  CLI:
43
62
  echo '{"tree": ["path1", "path2", ...]}' | uv run skf-detect-language.py
44
63
  uv run skf-detect-language.py --json '{"tree": [...]}'
64
+ uv run skf-detect-language.py --tree-file <file> [--workspace-signal <manifest_kind>]
65
+ <listing> | uv run skf-detect-language.py --tree-file - [--workspace-signal <manifest_kind>]
66
+ uv run skf-detect-language.py scope-patterns --tree-file <file> --language <language>
67
+
68
+ --tree-file reads the repository's file list from a file (`-` reads it from
69
+ stdin), so a tree of any size reaches the script whole and never passes
70
+ through a shell string or the model:
71
+ - JSON: an object whose `tree` lists the paths, such as the output of
72
+ skf-github-probe.py tree (a listing whose `status` is not "ok" is
73
+ refused, with its `message`) or a GitHub git/trees response (only its
74
+ `blob` entries count); or a JSON list of paths.
75
+ - Anything else: one path per line, as `git -c core.quotePath=false
76
+ ls-files` prints them (a leading `./`, as `find .` prints it, is
77
+ dropped).
78
+ A byte order mark before the listing is dropped, and a listing that holds
79
+ no path is refused. These are the listings skf-recommend-scope-type.py's
80
+ --tree-file reads, which takes `[]` as its deliberate fallback.
81
+ read_tree_file() is the reader skf-detect-workspaces.py and
82
+ skf-shape-detect.py load for their own --tree-file. With --tree-file, stdin
83
+ is never read for the payload, so a payload piped in beside a --tree-file
84
+ <file> is not seen: pass workspace_signal with --workspace-signal (or the
85
+ payload with --json, which may not carry `tree` as well).
86
+ --workspace-signal sets the payload's workspace_signal in any form; passing
87
+ it in the payload as well is an error.
45
88
 
46
89
  Input (JSON object on stdin or via --json):
47
- tree — list of repo-relative file paths (required, non-empty)
48
- workspace_signal — optional manifest_kind from skf-detect-workspaces. When it
49
- names a non-JS workspace ("cargo-workspace",
50
- "python-multi-package"), rule 0 returns the root language
51
- and ignores nested package.json/tsconfig matches.
90
+ tree list of repo-relative file paths (required, non-empty;
91
+ or --tree-file)
92
+ workspace_signal optional manifest_kind from skf-detect-workspaces (or
93
+ --workspace-signal). When it names a non-JS workspace
94
+ ("cargo-workspace", "python-multi-package"), rule 0
95
+ returns the root language and ignores nested
96
+ package.json/tsconfig matches.
52
97
 
53
98
  Output (JSON on stdout):
54
99
  language — javascript | typescript | rust | python | go
@@ -56,38 +101,85 @@ Output (JSON on stdout):
56
101
  | unknown
57
102
  confidence — "high" | "medium" | "low"
58
103
  detection_source — human-readable string naming what fired (manifest
59
- basename, extension share, etc.)
60
- fallback_to_extension_frequency — bool (true when rule 10 fired)
104
+ basename, extension share, etc.); a manifest below
105
+ the root is named by its path
106
+ fallback_to_extension_frequency: bool (true when rule 11 fired)
107
+ source_language: the extension-frequency answer over the full tree
108
+ (rule 11's language, or unknown), whatever decided
109
+ `language`: the language most source files are in
61
110
  detected_languages — ordered, deduplicated list of EVERY manifest-level
62
- match in the same priority order the winner walk uses.
111
+ match, ranked as the winner walk ranks them (folder
112
+ depth, then rule order), then the languages of the
113
+ manifests that never decide (non-core folders), then
114
+ source_language when no manifest sits at the root.
63
115
  language == detected_languages[0] whenever the manifest
64
116
  table fired, so a caller can auto-pick detected_languages[0]
65
117
  and gate multi-language disambiguation on
66
118
  len(detected_languages) > 1. Special cases: a
67
119
  workspace_signal override (rule 0) is decisive and returns
68
120
  a single-element [language]; the extension-frequency
69
- fallback (rule 10) contributes [language] for a best guess
121
+ fallback (rule 11) contributes [language] for a best guess
70
122
  or [] when language is "unknown".
71
123
 
72
124
  Exit codes:
73
- 0 — recommendation produced (even when language is "unknown")
74
- 2 — internal error (bad JSON input, IO failure)
125
+ 0 recommendation produced (even when language is "unknown")
126
+ 2 bad input: invalid JSON, a missing or empty tree, the tree or the
127
+ workspace_signal passed twice, or a tree listing that cannot be read,
128
+ holds no path or reports a failure
129
+
130
+ scope-patterns prints a brief's `scope.include` and `scope.exclude` for one
131
+ language (a value `language` above takes) from the --tree-file listing alone,
132
+ so a manifests-only sparse checkout, which holds no source folder on disk,
133
+ roots them the same as a full one. The paths are read below the tree's root
134
+ (above), and the patterns carry that root, so the files of one unit give
135
+ patterns rooted at the unit's folder. The source folders are the first of
136
+ these to hold files of the language's extensions (_SCOPE_RULES) and leave
137
+ no package out:
138
+ 1. the language's conventional folders, in order (src/ then lib/ for
139
+ JavaScript and TypeScript; `**/src/main/java` is every module's). A
140
+ file counts when no folder down to the conventional one is hidden or
141
+ non-core, whatever the folders below it are named (a Java package
142
+ path such as com/example/);
143
+ 2. for Python, every top-level core folder that holds an __init__.py
144
+ itself: the import packages (sklearn/ of scikit-learn, django/);
145
+ 3. otherwise the whole tree, less each top-level hidden or non-core
146
+ folder that holds files of the language. Go always takes the whole
147
+ tree, and `unknown` matches every file.
148
+ A folder leaves a package out when a core folder below the root that holds
149
+ a manifest (not a vendored copy) has files of the language outside it, in
150
+ its own files or a core folder of its own: a monorepo merged into one
151
+ skill, whose root lib/ is one package among its members (aws-sdk-js-v3,
152
+ with clients/ and packages/ beside it). Then only a later conventional
153
+ folder that matches at any depth (`**/src/main/java`) may still answer,
154
+ and otherwise the whole tree does.
155
+ `include` lists `<folder>/**/*<ext>` for each extension the source folders
156
+ hold (every extension of the language when they hold none); `exclude` lists
157
+ the language's test, benchmark and vendored-code globs, then the left-out
158
+ folders. Output: {language, source_folders, include, exclude}, with
159
+ `source_folders` repo-relative (`.` for the repository root). Exit 2 on an
160
+ unusable listing or a language outside the list.
75
161
  """
76
162
 
77
163
  from __future__ import annotations
78
164
 
79
165
  import argparse
80
166
  import json
167
+ import re
81
168
  import sys
82
169
  from collections import Counter
83
170
  from typing import Any
84
171
 
172
+ # A --tree-file listing is JSON when it opens like a JSON object or list of
173
+ # paths. A bare leading bracket is not enough: a line listing can start with
174
+ # a path such as `[slug]/page.tsx`.
175
+ JSON_LISTING_RE = re.compile(r'\{\s*["}]|\[\s*["{\]]')
176
+
85
177
  # Workspace manifest_kind → root language (rule 0). Only non-JS workspace kinds
86
178
  # appear here: a Cargo/Python workspace root is unambiguously rust/python, and a
87
179
  # nested package.json+tsconfig (a docs or website subproject) must not win. JS
88
- # workspace kinds (npm-workspaces/pnpm-workspaces/lerna) are intentionally
89
- # absent — their root manifest IS package.json, so rule 1 resolves js/ts
90
- # correctly. generic-folders / null carry no language signal.
180
+ # workspace kinds (npm-workspaces/pnpm-workspaces/lerna/rush/nx) are
181
+ # intentionally absent: their repositories hold package.json files, so rule 1
182
+ # resolves js/ts correctly. generic-folders / null carry no language signal.
91
183
  _WORKSPACE_SIGNAL_LANGUAGE: dict[str, str] = {
92
184
  "cargo-workspace": "rust",
93
185
  "python-multi-package": "python",
@@ -140,6 +232,19 @@ _EXTENSION_TO_LANGUAGE: dict[str, str] = {
140
232
 
141
233
  _DOMINANCE_THRESHOLD = 0.50
142
234
 
235
+ # Folders whose manifests are not the project's own package: a docs or
236
+ # website site, an example, a test fixture, a benchmark, a script or a tool.
237
+ # A copy of skf-shape-detect.py's _NON_CORE_PATH_SEGMENTS, which cannot be
238
+ # imported from there: skf-shape-detect.py loads this script. A hidden folder
239
+ # (.github/, .devcontainer/) is non-core too.
240
+ _NON_CORE_PATH_SEGMENTS = frozenset({
241
+ "example", "examples", "demo", "demos", "sample", "samples",
242
+ "playground", "playgrounds", "e2e", "benchmark", "benchmarks", "bench",
243
+ "fixture", "fixtures", "website", "websites", "www",
244
+ "docs", "doc", "scripts", "tools", "tooling", "devtools", "dev-tools",
245
+ "test", "tests", "__tests__", "integration", "smoke",
246
+ })
247
+
143
248
 
144
249
  def _die(message: str, code: int = 2) -> None:
145
250
  sys.stderr.write(f"skf-detect-language: {message}\n")
@@ -171,6 +276,12 @@ def _extension(path: str) -> str:
171
276
  return "." + base.rsplit(".", 1)[-1].lower()
172
277
 
173
278
 
279
+ def is_source_file(path: str) -> bool:
280
+ """Whether a path is a source file the extension-frequency fallback
281
+ counts (skf-detect-workspaces.py's tree snapshot counts the same)."""
282
+ return _extension(path) in _EXTENSION_TO_LANGUAGE
283
+
284
+
174
285
  def _frequency_fallback(tree: list[str]) -> dict[str, Any]:
175
286
  """Score the tree by source-extension frequency. Returns the result envelope."""
176
287
  counter: Counter[str] = Counter()
@@ -211,18 +322,119 @@ def _frequency_fallback(tree: list[str]) -> dict[str, Any]:
211
322
  }
212
323
 
213
324
 
325
+ def _parts(path: str) -> list[str]:
326
+ """The segments of a repo-relative path, without empty and `.` ones."""
327
+ return [p for p in path.split("/") if p and p != "."]
328
+
329
+
330
+ def _tree_root(tree: list[str]) -> list[str]:
331
+ """The folder segments every path of the tree sits under (the tree's root)."""
332
+ root: list[str] | None = None
333
+ for path in tree:
334
+ folders = _parts(path)[:-1]
335
+ if root is None:
336
+ root = folders
337
+ continue
338
+ depth = 0
339
+ while depth < min(len(root), len(folders)) and root[depth] == folders[depth]:
340
+ depth += 1
341
+ root = root[:depth]
342
+ if not root:
343
+ break
344
+ return root or []
345
+
346
+
347
+ def _is_core(folders: list[str]) -> bool:
348
+ """Whether no folder below the tree's root is hidden or a non-core folder."""
349
+ return not any(seg.startswith(".") or seg.lower() in _NON_CORE_PATH_SEGMENTS for seg in folders)
350
+
351
+
352
+ def _rule_of(basename: str) -> int | None:
353
+ """The rank of the table rule a manifest basename fires, in rule order
354
+ (package.json first, the suffix rules last), or None."""
355
+ if basename == "package.json":
356
+ return 0
357
+ for i, (name, _language, _source) in enumerate(_MANIFEST_RULES):
358
+ if basename == name:
359
+ return 1 + i
360
+ if basename == "build.gradle":
361
+ return 1 + len(_MANIFEST_RULES)
362
+ for i, (suffix, _language, _source) in enumerate(_SUFFIX_RULES):
363
+ if basename.endswith(suffix):
364
+ return 2 + len(_MANIFEST_RULES) + i
365
+ return None
366
+
367
+
368
+ def _rule_result(rank: int, context: dict[str, bool]) -> dict[str, Any]:
369
+ """The result a table rule gives; `context` says whether the paths its
370
+ checks read hold a tsconfig.json and a src/main/kotlin/ folder."""
371
+ if rank == 0:
372
+ if context["tsconfig"]:
373
+ return {"language": "typescript", "confidence": "high",
374
+ "detection_source": "package.json + tsconfig.json present"}
375
+ return {"language": "javascript", "confidence": "high",
376
+ "detection_source": "package.json present (no tsconfig.json)"}
377
+ if rank <= len(_MANIFEST_RULES):
378
+ _name, language, source = _MANIFEST_RULES[rank - 1]
379
+ return {"language": language, "confidence": "high", "detection_source": source}
380
+ if rank == 1 + len(_MANIFEST_RULES):
381
+ # build.gradle (Groovy DSL): build.gradle.kts, earlier in the table,
382
+ # already decided when both sit in the same folder depth.
383
+ if context["kotlin"]:
384
+ return {"language": "kotlin", "confidence": "medium",
385
+ "detection_source": "build.gradle (Groovy) + src/main/kotlin/ present"}
386
+ return {"language": "java", "confidence": "medium",
387
+ "detection_source": "build.gradle (Groovy) present without src/main/kotlin/ \u2014 defaulting to java"}
388
+ _suffix, language, source = _SUFFIX_RULES[rank - 2 - len(_MANIFEST_RULES)]
389
+ return {"language": language, "confidence": "high", "detection_source": source}
390
+
391
+
392
+ def _ranked_manifests(tree: list[str]) -> dict[str, Any]:
393
+ """The tree's manifests, ranked for the winner walk (see the module
394
+ docstring): `deciding` and `other` list (depth, rule rank, path, result)
395
+ tuples in rank order, `deciding` the manifests that may decide and
396
+ `other` the non-core ones that may not."""
397
+ root = _tree_root(tree)
398
+ core: list[tuple[int, int, str]] = []
399
+ rest: list[tuple[int, int, str]] = []
400
+ tsconfig = {True: False, False: False} # a tsconfig.json in a core / non-core folder
401
+ for path in tree:
402
+ parts = _parts(path)
403
+ if not parts:
404
+ continue
405
+ folders = parts[len(root):-1]
406
+ is_core = _is_core(folders)
407
+ if parts[-1] == "tsconfig.json":
408
+ tsconfig[is_core] = True
409
+ rank = _rule_of(parts[-1])
410
+ if rank is not None:
411
+ (core if is_core else rest).append((len(folders), rank, path))
412
+ # A source path such as src/main/kotlin/com/example/App.kt is the
413
+ # project's own whatever its folder names: the Kotlin check reads them all.
414
+ kotlin = _has_path_segment(tree, "src/main/kotlin/")
415
+ whole = {"tsconfig": tsconfig[True] or tsconfig[False], "kotlin": kotlin}
416
+ context = {"tsconfig": tsconfig[True], "kotlin": kotlin} if core else whole
417
+ deciding, other = (core, rest) if core else (rest, [])
418
+ return {
419
+ "deciding": [(d, r, p, _rule_result(r, context)) for d, r, p in sorted(deciding)],
420
+ "other": [(d, r, p, _rule_result(r, whole)) for d, r, p in sorted(other)],
421
+ }
422
+
423
+
214
424
  def _detected_languages(payload: dict[str, Any], winner: dict[str, Any]) -> list[str]:
215
425
  """Accumulate every manifest-level match into an ordered, deduplicated list.
216
426
 
217
- The walk mirrors the winner-selection priority order exactly, so
218
- winner["language"] == detected_languages[0] whenever the manifest table
219
- fired. Two decisive short-circuits diverge from a full walk on purpose:
427
+ The walk follows the winner walk's ranking exactly (folder depth, then
428
+ rule order), so winner["language"] == detected_languages[0] whenever the
429
+ manifest table fired; the non-core manifests follow, then, when no
430
+ manifest sits at the root, the tree's source language. Two decisive
431
+ short-circuits diverge from a full walk on purpose:
220
432
 
221
433
  * A workspace_signal override (rule 0) is authoritative — the workspace
222
434
  root language wins over any nested package.json, so this returns a
223
435
  single-element [winner_language] and never surfaces a spurious
224
436
  multi-language gate for what workspace detection already resolved.
225
- * When no manifest matched (rule 10 fired), the list carries the
437
+ * When no manifest matched (rule 11 fired), the list carries the
226
438
  extension-frequency best guess as a single element, or [] when the
227
439
  guess was "unknown".
228
440
  """
@@ -232,35 +444,21 @@ def _detected_languages(payload: dict[str, Any], winner: dict[str, Any]) -> list
232
444
  if isinstance(workspace_signal, str) and workspace_signal in _WORKSPACE_SIGNAL_LANGUAGE:
233
445
  return [winner["language"]]
234
446
 
447
+ ranked = _ranked_manifests(tree)
235
448
  langs: list[str] = []
236
-
237
- def add(lang: str) -> None:
238
- if lang not in langs:
239
- langs.append(lang)
240
-
241
- # Rule 1 — package.json (tsconfig.json disambiguation)
242
- if _has_basename(tree, "package.json"):
243
- add("typescript" if _has_basename(tree, "tsconfig.json") else "javascript")
244
-
245
- # Rules 2-6 / 8 / 10 — single-basename manifests (same order as _MANIFEST_RULES)
246
- for basename, language, _source in _MANIFEST_RULES:
247
- if _has_basename(tree, basename):
248
- add(language)
249
-
250
- # Rule 7b — build.gradle (Groovy) Java/Kotlin disambiguation
251
- if _has_basename(tree, "build.gradle"):
252
- add("kotlin" if _has_path_segment(tree, "src/main/kotlin/") else "java")
253
-
254
- # Rules 8-9 — suffix-based (csproj, sln)
255
- for suffix, language, _source in _SUFFIX_RULES:
256
- if _has_suffix(tree, suffix):
257
- add(language)
449
+ for _depth, _rank, _path, result in ranked["deciding"] + ranked["other"]:
450
+ if result["language"] not in langs:
451
+ langs.append(result["language"])
258
452
 
259
453
  if langs:
454
+ no_root_manifest = ranked["deciding"][0][0] > 0
455
+ source = winner["source_language"]
456
+ if no_root_manifest and source != "unknown" and source not in langs:
457
+ langs.append(source)
260
458
  return langs
261
459
 
262
460
  # No manifest matched — the winner came from the extension-frequency
263
- # fallback (rule 10). Carry its best guess, or [] when it was "unknown".
461
+ # fallback (rule 11). Carry its best guess, or [] when it was "unknown".
264
462
  fallback_language = winner["language"]
265
463
  return [] if fallback_language == "unknown" else [fallback_language]
266
464
 
@@ -269,11 +467,13 @@ def detect(payload: dict[str, Any]) -> dict[str, Any]:
269
467
  """Apply the documented rule walk and annotate the full manifest match set.
270
468
 
271
469
  Returns the single-winner envelope (language/confidence/detection_source/
272
- fallback_to_extension_frequency) unchanged, plus detected_languages[] — the
273
- ordered, deduplicated set of every manifest-level match — so a caller can
274
- both auto-pick and gate multi-language disambiguation off one JSON shape.
470
+ fallback_to_extension_frequency) plus source_language, the
471
+ extension-frequency answer, and detected_languages[] (the ordered,
472
+ deduplicated set of every manifest-level match), so a caller can both
473
+ auto-pick and gate multi-language disambiguation off one JSON shape.
275
474
  """
276
475
  winner = _winner(payload)
476
+ winner["source_language"] = _frequency_fallback(payload["tree"])["language"]
277
477
  winner["detected_languages"] = _detected_languages(payload, winner)
278
478
  return winner
279
479
 
@@ -298,66 +498,216 @@ def _winner(payload: dict[str, Any]) -> dict[str, Any]:
298
498
  "fallback_to_extension_frequency": False,
299
499
  }
300
500
 
301
- # Rule 1 — package.json (with tsconfig.json disambiguation)
302
- if _has_basename(tree, "package.json"):
303
- if _has_basename(tree, "tsconfig.json"):
304
- return {
305
- "language": "typescript",
306
- "confidence": "high",
307
- "detection_source": "package.json + tsconfig.json present",
308
- "fallback_to_extension_frequency": False,
501
+ # Rules 1-10: the manifest table, over the manifests nearest the
502
+ # tree's root (the non-core folders left out unless nothing else holds
503
+ # a manifest): the first ranked manifest decides.
504
+ deciding = _ranked_manifests(tree)["deciding"]
505
+ if deciding:
506
+ depth, _rank, path, result = deciding[0]
507
+ if depth > 0:
508
+ # No manifest at the root: one below it is a guess about the
509
+ # whole tree, never a high-confidence answer.
510
+ result = {
511
+ "language": result["language"],
512
+ "confidence": "medium",
513
+ "detection_source": f"{result['detection_source']} at {path} (no manifest at the tree's root)",
309
514
  }
310
- return {
311
- "language": "javascript",
312
- "confidence": "high",
313
- "detection_source": "package.json present (no tsconfig.json)",
314
- "fallback_to_extension_frequency": False,
315
- }
515
+ return {**result, "fallback_to_extension_frequency": False}
316
516
 
317
- # Rules 2-6 — single-basename manifests walked in priority order. Note
318
- # that build.gradle (Groovy DSL) is NOT in this table; it requires
319
- # tree-aware Java/Kotlin disambiguation handled in Rule 7b below.
320
- # build.gradle.kts IS in the table (returns kotlin high) and fires
321
- # here before the Groovy variant is considered.
322
- for basename, language, source in _MANIFEST_RULES:
323
- if _has_basename(tree, basename):
324
- return {
325
- "language": language,
326
- "confidence": "high",
327
- "detection_source": source,
328
- "fallback_to_extension_frequency": False,
329
- }
517
+ # Rule 11: extension-frequency fallback
518
+ return _frequency_fallback(tree)
330
519
 
331
- # Rule 7b — build.gradle (Groovy DSL): check src/main/kotlin/ to disambiguate.
332
- # This rule sits *after* the basename loop because build.gradle.kts is
333
- # already covered by the loop and we don't want it to pre-empt that match.
334
- if _has_basename(tree, "build.gradle"):
335
- if _has_path_segment(tree, "src/main/kotlin/"):
336
- return {
337
- "language": "kotlin",
338
- "confidence": "medium",
339
- "detection_source": "build.gradle (Groovy) + src/main/kotlin/ present",
340
- "fallback_to_extension_frequency": False,
341
- }
342
- return {
343
- "language": "java",
344
- "confidence": "medium",
345
- "detection_source": "build.gradle (Groovy) present without src/main/kotlin/ — defaulting to java",
346
- "fallback_to_extension_frequency": False,
347
- }
348
520
 
349
- # Rules 8-9 — suffix-based (csproj, sln)
350
- for suffix, language, source in _SUFFIX_RULES:
351
- if _has_suffix(tree, suffix):
352
- return {
353
- "language": language,
354
- "confidence": "high",
355
- "detection_source": source,
356
- "fallback_to_extension_frequency": False,
357
- }
521
+ # --------------------------------------------------------------------------
522
+ # scope-patterns: a brief's include and exclude patterns
523
+ # --------------------------------------------------------------------------
524
+
525
+ # Per language: the extensions an include lists, the conventional source
526
+ # folders in the order they are tried (None: the whole tree, as Go keeps its
527
+ # packages in the module's own folders; a leading `**/` matches the folder at
528
+ # any depth) and the exclude globs, where `{ext}` stands for each included
529
+ # extension. `unknown` has no extension: it matches every file.
530
+ _SCOPE_RULES: dict[str, tuple[tuple[str, ...], tuple[str, ...] | None, tuple[str, ...]]] = {
531
+ "typescript": ((".ts", ".tsx"), ("src", "lib"), ("**/*.test{ext}", "**/*.spec{ext}", "**/node_modules/**")),
532
+ "javascript": ((".js", ".jsx", ".mjs", ".cjs"), ("src", "lib"),
533
+ ("**/*.test{ext}", "**/*.spec{ext}", "**/node_modules/**")),
534
+ "python": ((".py",), ("src",), ("**/*_test.py", "**/test_*.py", "**/tests/**")),
535
+ "rust": ((".rs",), ("src",), ("**/tests/**", "**/benches/**")),
536
+ "go": ((".go",), None, ("**/*_test.go", "**/vendor/**")),
537
+ "java": ((".java",), ("src/main/java", "**/src/main/java"), ("**/src/test/**",)),
538
+ "kotlin": ((".kt",), ("src/main/kotlin", "**/src/main/kotlin"), ("**/src/test/**",)),
539
+ "swift": ((".swift",), ("Sources",), ("**/Tests/**",)),
540
+ "csharp": ((".cs",), ("src",), ("**/bin/**", "**/obj/**", "**/*Tests/**")),
541
+ "ruby": ((".rb",), ("lib",), ("**/spec/**", "**/test/**")),
542
+ "php": ((".php",), ("src", "lib"), ("**/tests/**", "**/vendor/**")),
543
+ "unknown": ((), (), ("**/test/**", "**/tests/**")),
544
+ }
358
545
 
359
- # Rule 10 — extension-frequency fallback
360
- return _frequency_fallback(tree)
546
+
547
+ def _depth_below(parts: list[str], folder: str) -> int:
548
+ """How many of a file's folder segments reach down to `folder` (whose
549
+ leading `**/` matches it at any depth), or 0 when the file is not below
550
+ it. Only those segments are judged core: a Java package path below
551
+ src/main/java/ (com/example/, org/.../samples/) is the project's own."""
552
+ segs = folder.split("/")
553
+ folders = parts[:-1]
554
+ if segs[0] != "**":
555
+ return len(segs) if folders[:len(segs)] == segs and _is_core(segs) else 0
556
+ tail = segs[1:]
557
+ for i in range(len(folders) - len(tail) + 1):
558
+ if folders[i:i + len(tail)] == tail and _is_core(folders[:i + len(tail)]):
559
+ return i + len(tail)
560
+ return 0
561
+
562
+
563
+ # Folders that hold a copy of someone else's code: a manifest there is no
564
+ # package of the repository's own (scope_patterns' merged-monorepo check).
565
+ _VENDORED_SEGMENTS = frozenset({"vendor", "vendored", "third_party", "third-party", "thirdparty"})
566
+
567
+
568
+ def _leaves_out_a_package(paths: list[list[str]], files: list[list[str]], folders: list[str]) -> bool:
569
+ """Whether a package below the root (a core folder, not a vendored copy,
570
+ that holds a manifest) holds files of the language that none of
571
+ `folders` covers, in a folder of its own that is core: a monorepo merged
572
+ into one skill, whose root src/ or lib/ is one package among its members
573
+ (aws-sdk-js-v3's lib/ beside clients/ and packages/)."""
574
+ packages = {tuple(parts[:-1]) for parts in paths
575
+ if len(parts) > 1 and _rule_of(parts[-1]) is not None and _is_core(parts[:-1])
576
+ and not any(seg.lower() in _VENDORED_SEGMENTS for seg in parts[:-1])}
577
+ for parts in files:
578
+ if any(_depth_below(parts, folder) for folder in folders):
579
+ continue
580
+ below = parts[:-1]
581
+ if any(tuple(below[:len(pkg)]) == pkg and _is_core(below[:len(pkg) + 1]) for pkg in packages):
582
+ return True
583
+ return False
584
+
585
+
586
+ def scope_patterns(tree: list[str], language: str) -> dict[str, Any]:
587
+ """A brief's include and exclude patterns for `language` from the file
588
+ list alone (see the module docstring)."""
589
+ exts, conventional, excludes = _SCOPE_RULES[language]
590
+ root = _tree_root(tree)
591
+ prefix = "".join(f"{seg}/" for seg in root)
592
+ paths = [parts for parts in (_parts(p)[len(root):] for p in tree) if parts]
593
+ files = [parts for parts in paths if not exts or _extension(parts[-1]) in exts]
594
+ sources = [parts for parts in files if _is_core(parts[:-1])]
595
+ folders: list[str] = []
596
+ merged = False # a source folder held files but left a package's out
597
+ for folder in conventional or ():
598
+ if merged and not folder.startswith("**/"):
599
+ continue
600
+ if any(_depth_below(parts, folder) for parts in files):
601
+ if not _leaves_out_a_package(paths, files, [folder]):
602
+ folders = [folder]
603
+ break
604
+ merged = True
605
+ if not folders and not merged and language == "python":
606
+ packages = sorted({parts[0] for parts in sources if len(parts) == 2 and parts[1] == "__init__.py"})
607
+ if packages and not _leaves_out_a_package(paths, files, packages):
608
+ folders = packages
609
+ left_out: list[str] = []
610
+ if not folders:
611
+ left_out = sorted({parts[0] for parts in files if len(parts) > 1 and not _is_core(parts[:1])})
612
+ chosen = ([parts for parts in files if any(_depth_below(parts, f) for f in folders)] if folders
613
+ else sources)
614
+ held = [ext for ext in exts if any(_extension(parts[-1]) == ext for parts in chosen)] or list(exts)
615
+ names = [f"*{ext}" for ext in held] or ["*"]
616
+ bases = [f"{prefix}{folder}/" for folder in folders] or [prefix]
617
+ exclude: list[str] = []
618
+ for glob in excludes:
619
+ for ext in held if "{ext}" in glob else [""]:
620
+ if glob.replace("{ext}", ext) not in exclude:
621
+ exclude.append(glob.replace("{ext}", ext))
622
+ return {
623
+ "language": language,
624
+ "source_folders": [base.rstrip("/") or "." for base in bases],
625
+ "include": [f"{base}**/{name}" for base in bases for name in names],
626
+ "exclude": exclude + [f"{prefix}{folder}/**" for folder in left_out],
627
+ }
628
+
629
+
630
+ def _scope_patterns_main(argv: list[str]) -> int:
631
+ parser = argparse.ArgumentParser(
632
+ prog="skf-detect-language.py scope-patterns",
633
+ description="Print a brief's scope.include and scope.exclude for one language from a file list.",
634
+ )
635
+ parser.add_argument("--tree-file", required=True,
636
+ help="the repository's file list, as the detector's --tree-file reads it; - reads stdin")
637
+ parser.add_argument("--language", required=True, choices=sorted(_SCOPE_RULES),
638
+ help="the language the skill documents: a value the detector's `language` takes")
639
+ args = parser.parse_args(argv)
640
+ try:
641
+ tree = read_tree_file(args.tree_file)[0]
642
+ except TreeListingError as e:
643
+ _die(str(e))
644
+ json.dump(scope_patterns(tree, args.language), sys.stdout, ensure_ascii=False)
645
+ sys.stdout.write("\n")
646
+ return 0
647
+
648
+
649
+ # --------------------------------------------------------------------------
650
+ # --tree-file listings
651
+ # --------------------------------------------------------------------------
652
+
653
+
654
+ class TreeListingError(ValueError):
655
+ """A --tree-file listing that cannot be read, holds no path or reports a failure."""
656
+
657
+
658
+ def parse_tree_listing(text: str, source: str) -> tuple[list[str], bool]:
659
+ """(the file paths, whether the listing was cut short) of a --tree-file
660
+ listing: JSON, or one path per line, after a byte order mark (Windows
661
+ PowerShell writes one into a pipe). Only a JSON listing can say it was
662
+ cut short (a GitHub tree's `truncated`). `source` says where the
663
+ listing came from in an error. Raises TreeListingError, also for a
664
+ listing that holds no path (`[]`, or a tree with no file)."""
665
+ stripped = text.lstrip("").strip()
666
+ if not stripped:
667
+ raise TreeListingError(f"the tree listing {source} is empty; the command that wrote it may have failed")
668
+ truncated = False
669
+ if not JSON_LISTING_RE.match(stripped):
670
+ paths = []
671
+ for line in stripped.splitlines():
672
+ path = line.strip().removeprefix("./")
673
+ if path:
674
+ paths.append(path)
675
+ else:
676
+ try:
677
+ data = json.loads(stripped)
678
+ except json.JSONDecodeError as e:
679
+ raise TreeListingError(f"the tree listing {source} is not valid JSON: {e}") from e
680
+ entries = data
681
+ if isinstance(data, dict):
682
+ status = data.get("status")
683
+ if status is not None and status != "ok":
684
+ raise TreeListingError(
685
+ f"the tree listing {source} reports a failure ({status}): {data.get('message') or 'no message'}")
686
+ entries, truncated = data.get("tree"), data.get("truncated") is True
687
+ if not isinstance(entries, list):
688
+ raise TreeListingError(f"the tree listing {source} holds no list of paths (a `tree` list, or a JSON list)")
689
+ paths = []
690
+ for entry in entries:
691
+ if isinstance(entry, str):
692
+ paths.append(entry)
693
+ elif isinstance(entry, dict) and isinstance(entry.get("path"), str) and entry.get("type", "blob") == "blob":
694
+ paths.append(entry["path"])
695
+ if not paths:
696
+ raise TreeListingError(f"the tree listing {source} holds no file path; the command that wrote it may have failed")
697
+ return paths, truncated
698
+
699
+
700
+ def read_tree_file(value: str) -> tuple[list[str], bool]:
701
+ """parse_tree_listing() of the file `value` names, or of stdin for `-`
702
+ (a caller reconfigures stdin to UTF-8 first). Raises TreeListingError."""
703
+ if value == "-":
704
+ return parse_tree_listing(sys.stdin.read(), "on stdin")
705
+ try:
706
+ with open(value, encoding="utf-8-sig") as fh:
707
+ text = fh.read()
708
+ except (OSError, UnicodeDecodeError) as e:
709
+ raise TreeListingError(f"cannot read --tree-file {value}: {getattr(e, 'strerror', None) or e}") from e
710
+ return parse_tree_listing(text, f"in {value}")
361
711
 
362
712
 
363
713
  def _parse_argv(argv: list[str]) -> dict:
@@ -365,8 +715,35 @@ def _parse_argv(argv: list[str]) -> dict:
365
715
  description="Detect primary language from a flat repo file tree by walking the documented rule table.",
366
716
  )
367
717
  parser.add_argument("--json", help="JSON payload (alternative to stdin)")
718
+ parser.add_argument(
719
+ "--tree-file",
720
+ help="the repository's file list: JSON (a `tree` list, or a list) or one path per line; - reads stdin. "
721
+ "With --tree-file <file>, stdin is not read: pass the other keys with --workspace-signal or --json",
722
+ )
723
+ parser.add_argument(
724
+ "--workspace-signal",
725
+ help="the payload's workspace_signal: the manifest_kind skf-detect-workspaces.py returns",
726
+ )
368
727
  args = parser.parse_args(argv)
369
- raw = args.json if args.json is not None else sys.stdin.read()
728
+ if args.tree_file is not None:
729
+ # The listing is the tree; stdin is never read for the payload here.
730
+ payload = _load_payload(args.json) if args.json is not None else {}
731
+ if "tree" in payload:
732
+ _die("pass the tree once: in the payload or with --tree-file, not both")
733
+ try:
734
+ payload["tree"] = read_tree_file(args.tree_file)[0]
735
+ except TreeListingError as e:
736
+ _die(str(e))
737
+ else:
738
+ payload = _load_payload(args.json if args.json is not None else sys.stdin.read())
739
+ if args.workspace_signal is not None:
740
+ if "workspace_signal" in payload:
741
+ _die("pass workspace_signal once: in the payload or with --workspace-signal, not both")
742
+ payload["workspace_signal"] = args.workspace_signal
743
+ return payload
744
+
745
+
746
+ def _load_payload(raw: str) -> dict:
370
747
  if not raw or not raw.strip():
371
748
  _die("empty input (expected JSON payload on stdin or via --json)")
372
749
  try:
@@ -399,6 +776,8 @@ def _force_utf8(*streams) -> None:
399
776
 
400
777
  def main(argv: list[str]) -> int:
401
778
  _force_utf8(sys.stdin, sys.stdout)
779
+ if argv[:1] == ["scope-patterns"]:
780
+ return _scope_patterns_main(argv[1:])
402
781
  payload = _parse_argv(argv)
403
782
  result = detect(payload)
404
783
  json.dump(result, sys.stdout, ensure_ascii=False)