bmad-module-skill-forge 2.1.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 +51 -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 +14 -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 +117 -9
  75. package/src/shared/scripts/skf-detect-docs.py +892 -4
  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 +215 -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 +17 -6
  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 +55 -18
  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 -57
  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 -89
  348. package/src/skf-setup/references/detect-and-tier.md +58 -66
  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 -645
  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
@@ -4,21 +4,27 @@
4
4
  # ///
5
5
  """SKF Detect Workspaces — pure detector for monorepo / multi-package layouts.
6
6
 
7
- Takes a JSON payload on stdin describing a target repo's file tree plus the
8
- contents of a small set of root manifests, and returns whether a workspace
9
- layout is present, which manifest kind drives it, and the list of resolved
10
- workspaces. The helper does NO file I/O — the caller (typically
11
- skf-brief-skill step 2 §1b) fetches files via `gh api` / local filesystem
12
- and pipes the relevant content in.
7
+ Takes a target repo's file tree plus the contents of a small set of root
8
+ manifests, and returns whether a workspace layout is present, which manifest
9
+ kind drives it, and the list of resolved workspaces. detect() does no file
10
+ I/O: the CLI reads the tree and the manifests from a JSON payload (stdin or
11
+ --json), or from the files --tree-file and --manifest-dir name, so a caller
12
+ (skf-brief-skill step 2 §1b, skf-test-skill's coverage check) can stage them
13
+ instead of typing a tree or a manifest into a shell string. With --snapshot
14
+ it prints the facts the tree gives an analysis instead (see "Tree snapshot"
15
+ below).
13
16
 
14
17
  Detection runs in priority order; the first matching detector wins:
15
18
 
16
19
  1. npm-workspaces package.json has `workspaces: [...]` or `{packages: [...]}`
17
20
  2. pnpm-workspaces pnpm-workspace.yaml exists with a `packages:` list
18
21
  3. lerna lerna.json exists (`packages` field optional, defaults to `packages/*`)
19
- 4. cargo-workspace Cargo.toml has `[workspace]` with `members = [...]`
20
- 5. python-multi-package multiple pyproject.toml under packages/* or apps/*
21
- 6. generic-folders apps/, packages/, libs/, or code/ each with subdirs
22
+ 4. rush rush.json lists `projects[]` (JSON with comments; its
23
+ content must be supplied under `manifests`)
24
+ 5. nx nx.json at the root, and folders holding a project.json
25
+ 6. cargo-workspace Cargo.toml has `[workspace]` with `members = [...]`
26
+ 7. python-multi-package multiple pyproject.toml under packages/* or apps/*
27
+ 8. generic-folders apps/, packages/, libs/, or code/ each with subdirs
22
28
  containing a recognisable manifest
23
29
 
24
30
  Glob members (`packages/*`, `apps/*`, etc.) are resolved against the supplied
@@ -35,7 +41,9 @@ Input JSON shape (stdin):
35
41
  "package.json": "<raw text>",
36
42
  "Cargo.toml": "<raw text>",
37
43
  "pnpm-workspace.yaml": "<raw text>",
38
- "lerna.json": "<raw text>"
44
+ "lerna.json": "<raw text>",
45
+ "rush.json": "<raw text>",
46
+ "nx.json": "<raw text>"
39
47
  }
40
48
  }
41
49
 
@@ -43,17 +51,19 @@ Input JSON shape (stdin):
43
51
  Per-workspace child manifests (e.g. `packages/foo/package.json`) MUST appear
44
52
  here for glob resolution; their contents are optional.
45
53
  - `manifests` is a dict keyed by repository-relative path; only root-level
46
- manifests must be supplied. Per-workspace manifest contents may be
47
- included to populate the workspace `name` field, but absence is fine
48
- (the path basename is used as a fallback).
54
+ manifests must be supplied (nx.json may be left out when the tree lists
55
+ it). Per-workspace manifest contents may be included to populate the
56
+ workspace `name` field, but absence is fine (the path basename is used
57
+ as a fallback); for Nx that is each project.json, for Rush the
58
+ `packageName` in rush.json names each project.
49
59
 
50
60
  Output JSON shape (stdout):
51
61
 
52
62
  {
53
63
  "is_monorepo": true | false,
54
64
  "manifest_kind": "npm-workspaces" | "pnpm-workspaces" | "lerna"
55
- | "cargo-workspace" | "python-multi-package"
56
- | "generic-folders" | null,
65
+ | "rush" | "nx" | "cargo-workspace"
66
+ | "python-multi-package" | "generic-folders" | null,
57
67
  "workspaces": [
58
68
  {"name": "foo", "path": "packages/foo", "manifest": "packages/foo/package.json"},
59
69
  ...
@@ -61,21 +71,117 @@ Output JSON shape (stdout):
61
71
  "warnings": ["..."]
62
72
  }
63
73
 
74
+ `warnings` also names each root manifest whose content a detector reads
75
+ (CONTENT_MANIFESTS) that the tree lists but `manifests` does not hold: the
76
+ detector that needs it cannot run, so a workspace layout it would find is
77
+ missed.
78
+
79
+ CLI:
80
+ uv run skf-detect-workspaces.py < payload.json
81
+ uv run skf-detect-workspaces.py --json '{"tree": [...], "manifests": {...}}'
82
+ uv run skf-detect-workspaces.py --tree-file <file> --manifest-files
83
+ uv run skf-detect-workspaces.py --tree-file <file> [--manifest-dir <dir>]
84
+ uv run skf-detect-workspaces.py --tree-file <file> [--manifest-dir <dir>] \\
85
+ --snapshot [--root <folder>]
86
+
87
+ --tree-file reads the tree from a file (`-` reads it from stdin) through
88
+ skf-detect-language.py's read_tree_file() (the sibling in this folder, which
89
+ must sit beside this script), whose --tree-file text lists the listings it
90
+ reads; one that holds no path is refused. With --tree-file, stdin is never
91
+ read for the payload: `manifests` comes with --json or --manifest-dir, or is
92
+ empty, and the payload may not carry `tree` as well. When the listing says
93
+ it was cut short (a GitHub tree's `truncated`), `warnings` says so: a
94
+ workspace whose manifest the listing lacks is not found.
95
+
96
+ --manifest-files prints the root manifests the --tree-file listing holds
97
+ whose content a detector reads (CONTENT_MANIFESTS), one repo path per line
98
+ with LF endings on every platform (nothing when there are none), and exits:
99
+ a shell loop fetches exactly those files into the folder --manifest-dir then
100
+ reads.
101
+
102
+ --manifest-dir reads `manifests` from a folder laid out like the repository,
103
+ a local checkout or the folder the --manifest-files paths were fetched into:
104
+ each root manifest of CONTENT_MANIFESTS the tree lists, then the manifest of
105
+ each workspace found, which names it. A file missing there, larger than
106
+ MAX_MANIFEST_BYTES or not UTF-8 was not supplied. Passing `manifests` in the
107
+ payload as well is an error.
108
+
109
+ Tree snapshot (--snapshot)
110
+ --------------------------
111
+
112
+ The facts an analysis of the repository, or of one folder of it (--root,
113
+ such as a workspace path), reads from the tree, counted by the script so no
114
+ caller counts or samples a long listing by hand. It takes the same inputs:
115
+
116
+ {
117
+ "root": "" | "<folder>", the folder described ("" for
118
+ the repository)
119
+ "truncated": bool, the --tree-file listing says it was cut
120
+ short (a GitHub tree's `truncated`); the
121
+ counts are then lower bounds, and a
122
+ workspace may be missed
123
+ "file_count": N, files under the root
124
+ "source_file_count": N, of those, the source files: the
125
+ extensions skf-detect-language.py's
126
+ extension-frequency rule counts
127
+ "dir_count": N, folders under the root
128
+ "top_level_files": [...], names of the files directly in the root
129
+ "top_level_dirs": [...], names of the folders directly in the root
130
+ "manifest_kind": the workspace layout the detectors find at the
131
+ repository root (null without one, and with
132
+ --root)
133
+ "workspaces": [...], its workspaces, as detection lists them
134
+ "module_root": "" | "<folder>", where the module candidates are
135
+ "module_candidates": [{"path": "<folder>", "file_count": N,
136
+ "source_file_count": N}, ...], each folder
137
+ directly in module_root but hidden ones,
138
+ with the files and source files under it
139
+ "registry_candidates": [...], repo paths of the component registry
140
+ files under the root
141
+ "warnings": [...] the detection warnings at the repository
142
+ root
143
+ }
144
+
145
+ Modules. Which folders are the modules is the caller's judgment, so the
146
+ snapshot names none: it lists the candidates and what they hold. The module
147
+ root is the root's `src/` folder, else its `lib/` folder, else the root
148
+ itself, followed down while it holds no file and exactly one folder (hidden
149
+ ones and NON_MODULE_DIRS aside) that holds folders of its own (a Python
150
+ `src/<package>/` layout, or Java's `src/main/java/com/acme/`). The modules
151
+ are the workspaces when the detectors find some, else the candidates that
152
+ hold the library's own code (a folder with no source file, or of tests,
153
+ docs, examples, scripts, build tooling or CI, is none); a candidate that
154
+ holds most of the source files is the package itself (pandas/ at the root of
155
+ pandas), and a snapshot with --root <it> lists its own candidates. The count
156
+ of the modules picked is skf-recommend-scope-type.py's `module_count` input.
157
+
158
+ Registry candidates are the files the component-registry rule of
159
+ skf-recommend-scope-type.py reads (registry.ts, components.ts and their .tsx
160
+ forms, at any depth), found by that script's own rule (the sibling in this
161
+ folder, which must sit beside this script).
162
+
64
163
  Exit codes:
65
164
 
66
165
  0 success (regardless of is_monorepo value)
67
166
  1 payload error (malformed top-level JSON shape)
68
- 2 stdin / argparse / JSON-decode error
167
+ 2 input error: empty stdin, invalid JSON, a bad or conflicting flag
168
+ (argparse), the tree or the manifests passed twice, a tree listing that
169
+ cannot be read, holds no path or reports a failure, a sibling script that
170
+ cannot be loaded, or a --root under which the tree lists no file
69
171
  """
70
172
 
71
173
  from __future__ import annotations
72
174
 
73
175
  import argparse
74
176
  import fnmatch
177
+ import importlib.util
75
178
  import json
179
+ import os
180
+ import posixpath
76
181
  import re
77
182
  import sys
78
183
  import tomllib
184
+ from pathlib import Path
79
185
  from typing import Optional
80
186
 
81
187
 
@@ -90,6 +196,25 @@ RECOGNISED_MANIFESTS = (
90
196
  "build.gradle.kts",
91
197
  "pom.xml",
92
198
  )
199
+ # Root manifests whose content a detector reads (nx.json counts by its place
200
+ # in the tree alone): --manifest-files lists them, --manifest-dir reads them,
201
+ # and a warning names each one the tree lists that was not supplied.
202
+ CONTENT_MANIFESTS = ("package.json", "pnpm-workspace.yaml", "lerna.json", "rush.json", "Cargo.toml")
203
+ MAX_MANIFEST_BYTES = 1024 * 1024
204
+ # Folders a snapshot's module root is never followed down through (names
205
+ # matched in lower case): tests, test helpers and fixtures, docs and sites,
206
+ # examples, playgrounds and benchmarks, build output, vendored dependencies
207
+ # and caches, and non-code resources. They still appear among the module
208
+ # candidates, with what they hold: which folders are modules is the
209
+ # caller's judgment.
210
+ NON_MODULE_DIRS = frozenset({
211
+ "test", "tests", "__tests__", "testing", "spec", "specs", "testdata", "fixture", "fixtures", "e2e",
212
+ "test-utils", "test_utils", "testutils", "__testutils__",
213
+ "doc", "docs", "website", "www", "example", "examples", "demo", "demos", "sample", "samples",
214
+ "playground", "playgrounds", "bench", "benches", "benchmark", "benchmarks", "scripts",
215
+ "build", "dist", "out", "target", "coverage", "node_modules", "vendor", "__pycache__",
216
+ "resources",
217
+ })
93
218
 
94
219
 
95
220
  # --------------------------------------------------------------------------
@@ -286,6 +411,117 @@ def detect_lerna(
286
411
  return _build_workspaces(dirs, tree, manifests)
287
412
 
288
413
 
414
+ def _strip_json_comments(content: str) -> str:
415
+ """JSON with `//` and `/* */` comments and trailing commas (rush.json is
416
+ JSONC) made plain JSON; string contents are left alone."""
417
+ n = len(content)
418
+
419
+ def comment_end(i: int) -> Optional[int]:
420
+ """The index just past the comment that starts at `i`, or None."""
421
+ if content.startswith("//", i):
422
+ end = content.find("\n", i)
423
+ return n if end < 0 else end
424
+ if content.startswith("/*", i):
425
+ end = content.find("*/", i + 2)
426
+ return n if end < 0 else end + 2
427
+ return None
428
+
429
+ def next_token(i: int) -> str:
430
+ """The first character at or after `i` outside blanks and comments."""
431
+ while i < n:
432
+ end = comment_end(i)
433
+ if end is not None:
434
+ i = end
435
+ elif content[i].isspace():
436
+ i += 1
437
+ else:
438
+ return content[i]
439
+ return ""
440
+
441
+ out: list[str] = []
442
+ i = 0
443
+ while i < n:
444
+ ch = content[i]
445
+ end = comment_end(i)
446
+ if ch == '"':
447
+ j = i + 1
448
+ while j < n and content[j] != '"':
449
+ j += 2 if content[j] == "\\" else 1
450
+ out.append(content[i : j + 1])
451
+ i = j + 1
452
+ elif end is not None:
453
+ i = end
454
+ else:
455
+ # a comma before the closing bracket of its list or object goes
456
+ if not (ch == "," and next_token(i + 1) in ("}", "]")):
457
+ out.append(ch)
458
+ i += 1
459
+ return "".join(out)
460
+
461
+
462
+ def detect_rush(
463
+ tree: set[str], manifests: dict[str, str], warnings: list[str]
464
+ ) -> Optional[list[dict]]:
465
+ """Rush: rush.json's `projects[]`, each a `projectFolder` with a
466
+ package.json in the tree, named by its `packageName`. A rush.json the
467
+ tree lists without its content is named in detect()'s warnings."""
468
+ content = manifests.get("rush.json")
469
+ if content is None:
470
+ return None
471
+ try:
472
+ data = json.loads(_strip_json_comments(content))
473
+ except json.JSONDecodeError as e:
474
+ warnings.append(f"rush.json JSON parse error: {e}")
475
+ return None
476
+ projects = data.get("projects") if isinstance(data, dict) else None
477
+ if not isinstance(projects, list):
478
+ return None
479
+ out: list[dict] = []
480
+ seen: set[str] = set()
481
+ for project in projects:
482
+ if not isinstance(project, dict) or not isinstance(project.get("projectFolder"), str):
483
+ continue
484
+ folder = _normalise_path(project["projectFolder"].strip())
485
+ manifest = f"{folder}/package.json"
486
+ if not folder or folder in seen or manifest not in tree:
487
+ continue
488
+ seen.add(folder)
489
+ name = project.get("packageName")
490
+ if not isinstance(name, str) or not name:
491
+ name = _read_workspace_name(manifest, manifests, folder.rsplit("/", 1)[-1])
492
+ out.append({"name": name, "path": folder, "manifest": manifest})
493
+ return sorted(out, key=lambda ws: ws["path"])
494
+
495
+
496
+ def detect_nx(
497
+ tree: set[str], manifests: dict[str, str], warnings: list[str]
498
+ ) -> Optional[list[dict]]:
499
+ """Nx: with nx.json at the root, each folder below it holding a
500
+ project.json is a project, named by that file's `name` when its
501
+ content is supplied. An Nx repository that declares its projects
502
+ through package.json workspaces is found by the npm or pnpm detector."""
503
+ if "nx.json" not in tree and "nx.json" not in manifests:
504
+ return None
505
+ out: list[dict] = []
506
+ for path in sorted(tree):
507
+ if not path.endswith("/project.json"):
508
+ continue
509
+ folder = path[: -len("/project.json")]
510
+ if "node_modules" in folder.split("/"):
511
+ continue
512
+ name = folder.rsplit("/", 1)[-1]
513
+ content = manifests.get(path)
514
+ if content is not None:
515
+ try:
516
+ declared = json.loads(content).get("name")
517
+ except (json.JSONDecodeError, AttributeError):
518
+ declared = None
519
+ if isinstance(declared, str) and declared:
520
+ name = declared
521
+ out.append({"name": name, "path": folder, "manifest": path})
522
+ return out
523
+
524
+
289
525
  def detect_cargo_workspace(
290
526
  tree: set[str], manifests: dict[str, str], warnings: list[str]
291
527
  ) -> Optional[list[dict]]:
@@ -356,6 +592,8 @@ DETECTORS: list[tuple[str, callable]] = [
356
592
  ("npm-workspaces", detect_npm_workspaces),
357
593
  ("pnpm-workspaces", detect_pnpm_workspaces),
358
594
  ("lerna", detect_lerna),
595
+ ("rush", detect_rush),
596
+ ("nx", detect_nx),
359
597
  ("cargo-workspace", detect_cargo_workspace),
360
598
  ("python-multi-package", detect_python_multi_package),
361
599
  ("generic-folders", detect_generic_folders),
@@ -369,6 +607,8 @@ ECOSYSTEM_OF_KIND = {
369
607
  "npm-workspaces": "js",
370
608
  "pnpm-workspaces": "js",
371
609
  "lerna": "js",
610
+ "rush": "js",
611
+ "nx": "js",
372
612
  "cargo-workspace": "rust",
373
613
  "python-multi-package": "python",
374
614
  }
@@ -377,6 +617,8 @@ ROOT_MANIFEST_OF_KIND = {
377
617
  "npm-workspaces": "package.json",
378
618
  "pnpm-workspaces": "pnpm-workspace.yaml",
379
619
  "lerna": "lerna.json",
620
+ "rush": "rush.json",
621
+ "nx": "nx.json",
380
622
  "cargo-workspace": "Cargo.toml",
381
623
  "python-multi-package": None, # discovered from the tree; no single root manifest
382
624
  }
@@ -430,6 +672,12 @@ def detect(payload: dict) -> dict:
430
672
  if not isinstance(manifests, dict):
431
673
  return {"_payload_error": "missing or non-dict 'manifests' field"}
432
674
  tree: set[str] = {_normalise_path(p) for p in tree_raw if isinstance(p, str) and p.strip()}
675
+ for name in CONTENT_MANIFESTS:
676
+ if name in tree and name not in manifests:
677
+ warnings.append(
678
+ f"{name} is in the tree but its content was not supplied: pass it under manifests "
679
+ "(or with --manifest-dir) so the detectors can read it"
680
+ )
433
681
 
434
682
  for kind, detector in DETECTORS:
435
683
  result = detector(tree, manifests, warnings)
@@ -450,28 +698,256 @@ def detect(payload: dict) -> dict:
450
698
  }
451
699
 
452
700
 
453
- def main(argv: Optional[list[str]] = None) -> int:
454
- parser = argparse.ArgumentParser(description="Detect monorepo / workspace layouts from a tree + manifests payload.")
455
- parser.add_argument("--json", help="Inline JSON payload (otherwise read from stdin).")
456
- args = parser.parse_args(argv)
701
+ # --------------------------------------------------------------------------
702
+ # Tree snapshot
703
+ # --------------------------------------------------------------------------
704
+
705
+
706
+ def _module_root(files: list[str], dirs: set[str]) -> str:
707
+ """The folder whose own folders are a snapshot's module candidates,
708
+ relative to the folder the snapshot describes: its `src/`, else its
709
+ `lib/`, else the folder itself, followed down while it holds no file
710
+ and exactly one folder (hidden ones and NON_MODULE_DIRS aside), which
711
+ holds folders of its own (a wrapper such as a Python `src/<package>/`,
712
+ never a lone `src/components/`)."""
713
+ children: dict[str, list[str]] = {}
714
+ for folder in dirs:
715
+ name = posixpath.basename(folder)
716
+ if not name.startswith(".") and name.lower() not in NON_MODULE_DIRS:
717
+ children.setdefault(posixpath.dirname(folder), []).append(folder)
718
+ holds_files = {posixpath.dirname(f) for f in files}
719
+ base = next((d for d in ("src", "lib") if d in dirs), "")
720
+ while True:
721
+ found = children.get(base, [])
722
+ if len(found) != 1 or base in holds_files or not children.get(found[0]):
723
+ return base
724
+ base = found[0]
725
+
726
+
727
+ def snapshot(tree: list, manifests: dict, root: str = "", truncated: bool = False) -> dict:
728
+ """The facts the tree gives an analysis of `root` ("" for the
729
+ repository): see "Tree snapshot" in the module docstring. Raises
730
+ ValueError when the tree lists no file under a `root` given, and
731
+ ImportError when skf-detect-language.py or skf-recommend-scope-type.py
732
+ does not sit beside this script."""
733
+ root = _normalise_path(root.strip())
734
+ root = "" if root == "." else root
735
+ prefix = f"{root}/" if root else ""
736
+ files = sorted({p for p in (_normalise_path(p) for p in tree if isinstance(p, str) and p.strip())
737
+ if p.startswith(prefix)})
738
+ if root and not files:
739
+ raise ValueError(f"the tree lists no file under --root {root}")
740
+ is_source = _sibling("skf-detect-language.py").is_source_file
741
+ rel = [p[len(prefix):] for p in files]
742
+ dirs = {"/".join(parts[:depth]) for parts in (r.split("/") for r in rel) for depth in range(1, len(parts))}
743
+ detected = {"manifest_kind": None, "workspaces": [], "warnings": []}
744
+ if not root:
745
+ detected = detect({"tree": files, "manifests": manifests})
746
+ base = _module_root(rel, dirs)
747
+ # (files, source files) under each folder directly in the module root
748
+ counts: dict[str, list[int]] = {}
749
+ for path in rel:
750
+ rest = path[len(base) + 1:] if base else path
751
+ if (base and not path.startswith(base + "/")) or "/" not in rest or rest.startswith("."):
752
+ continue
753
+ tally = counts.setdefault(rest.split("/", 1)[0], [0, 0])
754
+ tally[0] += 1
755
+ tally[1] += int(is_source(path))
756
+ module_root = prefix + base if base else root
757
+ return {
758
+ "root": root,
759
+ "truncated": truncated,
760
+ "file_count": len(files),
761
+ "source_file_count": sum(1 for path in rel if is_source(path)),
762
+ "dir_count": len(dirs),
763
+ "top_level_files": sorted(r for r in rel if "/" not in r),
764
+ "top_level_dirs": sorted(d for d in dirs if "/" not in d),
765
+ "manifest_kind": detected["manifest_kind"],
766
+ "workspaces": detected["workspaces"],
767
+ "module_root": module_root,
768
+ "module_candidates": [
769
+ {"path": f"{module_root}/{name}" if module_root else name,
770
+ "file_count": tally[0], "source_file_count": tally[1]}
771
+ for name, tally in sorted(counts.items())
772
+ ],
773
+ "registry_candidates": _sibling("skf-recommend-scope-type.py")._find_registry_files(files),
774
+ "warnings": detected["warnings"],
775
+ }
776
+
777
+
778
+ # --------------------------------------------------------------------------
779
+ # Files: the tree listing, the manifests, the sibling scripts
780
+ # --------------------------------------------------------------------------
781
+
782
+
783
+ _SIBLINGS: dict[str, object] = {}
784
+
785
+
786
+ def _sibling(filename: str):
787
+ """A helper of this folder, loaded once as a module: skf-detect-language.py
788
+ reads a --tree-file listing, and skf-recommend-scope-type.py names the
789
+ component registry files a snapshot lists. Raises ImportError when it
790
+ does not sit beside this script."""
791
+ module = _SIBLINGS.get(filename)
792
+ if module is None:
793
+ path = Path(__file__).resolve().parent / filename
794
+ name = "skf_" + filename.removeprefix("skf-").removesuffix(".py").replace("-", "_")
795
+ spec = importlib.util.spec_from_file_location(name, path)
796
+ if spec is None or spec.loader is None:
797
+ raise ImportError(f"cannot load {filename} beside {Path(__file__).name}")
798
+ module = importlib.util.module_from_spec(spec)
799
+ try:
800
+ spec.loader.exec_module(module)
801
+ except (OSError, SyntaxError) as exc:
802
+ raise ImportError(f"cannot load {filename} beside {Path(__file__).name}: {exc}") from exc
803
+ _SIBLINGS[filename] = module
804
+ return module
805
+
806
+
807
+ def read_manifest_dir(folder: str, paths: list[str]) -> dict[str, str]:
808
+ """{repo path: text} of each of `paths` read from `folder`, a folder laid
809
+ out like the repository. A path outside the repository, or a file
810
+ missing there, larger than MAX_MANIFEST_BYTES or not UTF-8, is left
811
+ out: its content was not supplied."""
812
+ out: dict[str, str] = {}
813
+ for rel in dict.fromkeys(paths):
814
+ if rel.startswith("/") or ".." in rel.split("/"):
815
+ continue
816
+ path = os.path.join(folder, *rel.split("/"))
817
+ try:
818
+ if os.path.getsize(path) > MAX_MANIFEST_BYTES:
819
+ continue
820
+ with open(path, "rb") as fh:
821
+ out[rel] = fh.read().decode("utf-8-sig")
822
+ except (OSError, UnicodeDecodeError):
823
+ continue
824
+ return out
825
+
826
+
827
+ def _listed(tree: object) -> set[str]:
828
+ return {_normalise_path(p) for p in tree if isinstance(p, str)} if isinstance(tree, list) else set()
829
+
830
+
831
+ def _fail(message: str, code: int) -> int:
832
+ print(json.dumps({"error": message}), file=sys.stderr)
833
+ return code
834
+
835
+
836
+ def _force_utf8(*streams) -> None:
837
+ """Reconfigure the streams to UTF-8, keeping each one's error handler: a
838
+ default Windows console reads stdin as cp1252, which cannot carry a
839
+ UTF-8 payload or tree listing. For stdin this must run before the first
840
+ read. Skips in-process test doubles without reconfigure()."""
841
+ for stream in streams:
842
+ if hasattr(stream, "reconfigure"):
843
+ errors = getattr(stream, "errors", None)
844
+ if errors is None:
845
+ stream.reconfigure(encoding="utf-8")
846
+ else:
847
+ stream.reconfigure(encoding="utf-8", errors=errors)
457
848
 
458
- raw = args.json
459
- if raw is None:
460
- raw = sys.stdin.read()
461
- if not raw.strip():
462
- print(json.dumps({"error": "empty input"}), file=sys.stderr)
463
- return 2
464
849
 
850
+ def _decode(raw: str) -> tuple[Optional[dict], Optional[str], int]:
851
+ """(payload, error message, exit code) of a JSON payload's text."""
852
+ if not raw.strip():
853
+ return None, "empty input", 2
465
854
  try:
466
855
  payload = json.loads(raw)
467
856
  except json.JSONDecodeError as e:
468
- print(json.dumps({"error": f"json decode error: {e}"}), file=sys.stderr)
469
- return 2
857
+ return None, f"json decode error: {e}", 2
858
+ if not isinstance(payload, dict):
859
+ return None, "the payload must be a JSON object", 1
860
+ return payload, None, 0
861
+
862
+
863
+ def main(argv: Optional[list[str]] = None) -> int:
864
+ _force_utf8(sys.stdin, sys.stdout)
865
+ parser = argparse.ArgumentParser(description="Detect monorepo / workspace layouts from a tree + manifests payload.")
866
+ parser.add_argument("--json", help="Inline JSON payload (otherwise read from stdin).")
867
+ parser.add_argument(
868
+ "--tree-file",
869
+ help="the repository's file list: JSON (a `tree` list, or a list) or one path per line; - reads stdin. "
870
+ "With --tree-file <file>, stdin is not read: pass the manifests with --manifest-dir or --json",
871
+ )
872
+ parser.add_argument(
873
+ "--manifest-dir",
874
+ help="a folder laid out like the repository that holds the manifests (read as `manifests`)",
875
+ )
876
+ parser.add_argument(
877
+ "--manifest-files",
878
+ action="store_true",
879
+ help="print the root manifests of the --tree-file listing whose content the detectors read, "
880
+ "one path per line, and exit",
881
+ )
882
+ parser.add_argument(
883
+ "--snapshot",
884
+ action="store_true",
885
+ help="print the tree snapshot (counts, top-level entries, workspaces, module candidates, registry files) "
886
+ "instead",
887
+ )
888
+ parser.add_argument("--root", help="with --snapshot: the folder to describe (default: the repository)")
889
+ args = parser.parse_args(argv)
890
+ if args.manifest_files and (args.tree_file is None or args.json is not None or args.manifest_dir is not None
891
+ or args.snapshot or args.root is not None):
892
+ return _fail("--manifest-files lists the root manifests of --tree-file and reads nothing else", 2)
893
+ if args.root is not None and not args.snapshot:
894
+ return _fail("--root names the folder --snapshot describes: pass --snapshot", 2)
895
+
896
+ truncated = False
897
+ if args.tree_file is not None:
898
+ try:
899
+ tree, truncated = _sibling("skf-detect-language.py").read_tree_file(args.tree_file)
900
+ except (ImportError, ValueError) as e: # ValueError: the sibling's TreeListingError
901
+ return _fail(str(e), 2)
902
+ if args.manifest_files:
903
+ listed = _listed(tree)
904
+ if hasattr(sys.stdout, "reconfigure"):
905
+ sys.stdout.reconfigure(newline="\n") # a shell loop reads the lines: no CR on Windows
906
+ sys.stdout.write("".join(f"{name}\n" for name in CONTENT_MANIFESTS if name in listed))
907
+ return 0
908
+ payload: dict = {}
909
+ if args.json is not None:
910
+ payload, message, code = _decode(args.json)
911
+ if message:
912
+ return _fail(message, code)
913
+ if "tree" in payload:
914
+ return _fail("pass the tree once: in the payload or with --tree-file, not both", 2)
915
+ payload["tree"] = tree
916
+ if args.manifest_dir is None:
917
+ payload.setdefault("manifests", {})
918
+ else:
919
+ payload, message, code = _decode(args.json if args.json is not None else sys.stdin.read())
920
+ if message:
921
+ return _fail(message, code)
922
+ if args.manifest_dir is not None:
923
+ if "manifests" in payload:
924
+ return _fail("pass the manifests once: in the payload or with --manifest-dir, not both", 2)
925
+ listed = _listed(payload.get("tree"))
926
+ payload["manifests"] = read_manifest_dir(args.manifest_dir, [n for n in CONTENT_MANIFESTS if n in listed])
927
+ found = detect(payload)
928
+ if found.get("workspaces"):
929
+ # Each workspace's own manifest names it.
930
+ payload["manifests"].update(
931
+ read_manifest_dir(args.manifest_dir, [ws["manifest"] for ws in found["workspaces"]]))
932
+
933
+ if args.snapshot:
934
+ if not isinstance(payload.get("tree"), list):
935
+ return _fail("missing or non-list 'tree' field", 1)
936
+ if not isinstance(payload.get("manifests"), dict):
937
+ return _fail("missing or non-dict 'manifests' field", 1)
938
+ try:
939
+ out = snapshot(payload["tree"], payload["manifests"], args.root or "", truncated)
940
+ except (ImportError, ValueError) as e: # a missing sibling, or a --root without files
941
+ return _fail(str(e), 2)
942
+ print(json.dumps(out, separators=(",", ":")))
943
+ return 0
470
944
 
471
945
  result = detect(payload)
472
946
  if "_payload_error" in result:
473
- print(json.dumps({"error": result["_payload_error"]}), file=sys.stderr)
474
- return 1
947
+ return _fail(result["_payload_error"], 1)
948
+ if truncated:
949
+ result["warnings"].append(
950
+ "the --tree-file listing was cut short (truncated): a workspace whose manifest it does not list is missed")
475
951
 
476
952
  print(json.dumps(result, separators=(",", ":")))
477
953
  return 0