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
@@ -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 Shape Detect — classify repos into known skill shapes from manifest files.
@@ -13,7 +13,8 @@ three pipelines.
13
13
  The five-shape heuristic ladder (apply in order, first match wins):
14
14
 
15
15
  1. language-reference — parser/grammar/language-toolchain project
16
- Signals: parser-related deps (pest, antlr4, tree-sitter, lark ...)
16
+ Signals: a compiler tree, a grammar file, a package named after a
17
+ parser tool (pest, antlr4, tree-sitter, lark ...)
17
18
  2. stack-compose — multi-ecosystem composite project
18
19
  Signals: manifests from 2+ distinct ecosystems
19
20
  3. reference-app — application, CLI, or demo project
@@ -22,18 +23,79 @@ The five-shape heuristic ladder (apply in order, first match wins):
22
23
  Signals: main/module/exports fields, [lib] target, export count
23
24
  5. unknown — no heuristic matched
24
25
 
26
+ A reference-app that rests on a framework dependency alone (no bin) in a
27
+ package that names itself a library also carries the signal
28
+ `app_or_library:framework_dep`: an app built on the framework and a
29
+ library that extends it (FastAPI, a Flask or Django extension, axum-extra)
30
+ declare the same dependencies, so the caller judges which one it is from
31
+ the README or the manifest description. The shape stays reference-app.
32
+
33
+ A parser tool among the runtime dependencies (a consumer: nom, pyparsing,
34
+ tree-sitter ...) decides no shape: a language or DSL built on it and an app
35
+ or library that only reads its input with it declare the same dependency.
36
+ The ladder goes on and its shape carries the signal
37
+ `language_or_user:parser_dep`, so the caller judges which one it is from the
38
+ README or the manifest description. A dev or build dependency is no
39
+ consumer, nor is one in a non-core member (examples/, docs/) or the monorepo
40
+ coordinator root, as with frameworks. Only when the parser dependency is the
41
+ repository's only signal (a bare DSL manifest: no rung answers and no
42
+ manifest declares a bin) does it still give language-reference, in place of
43
+ unknown.
44
+
25
45
  CLI:
26
46
  uv run src/shared/scripts/skf-shape-detect.py \\
27
47
  --repo-url <url> --manifests <path1,path2,...>
48
+ uv run src/shared/scripts/skf-shape-detect.py \\
49
+ --repo-url <url> --manifests <path1,path2,...> --tree-file <file>
50
+ uv run src/shared/scripts/skf-shape-detect.py \\
51
+ --repo-url <url> --manifests-file <scan.json> --manifest-dir <root> \\
52
+ --tree-file <file>
28
53
 
29
54
  Input:
30
55
  --repo-url repository URL (required; context only, no cloning)
31
56
  --manifests comma-separated local file paths to manifest files (may be
32
57
  empty when a tree-level signal is supplied instead)
58
+ --manifests-file the JSON envelope `skf-scan-manifests.py scan <root>`
59
+ printed, in place of --manifests: its `manifests[]`
60
+ entries of the types this script classifies (the _PARSERS
61
+ file names) are read, each `path` resolved against
62
+ --manifest-dir, so no caller filters, resolves or joins
63
+ the manifest list. A manifest's own path below that root
64
+ (not where the root sits) decides whether it is core and
65
+ how deep it is.
66
+ --manifest-dir the folder the scan ran on (required with
67
+ --manifests-file)
68
+ --tree-file the repository's whole file list (`-` reads it from stdin),
69
+ read by skf-detect-language.py's read_tree_file() (the
70
+ sibling in this folder, which must sit beside this
71
+ script), whose --tree-file text lists the listings it
72
+ reads (`git -c core.quotePath=false ls-tree -r --name-only
73
+ HEAD` prints one). The script finds the tree-level signals
74
+ in it itself (tree_signals()), in place of --grammar-files
75
+ and --tree-paths
33
76
  --grammar-files comma-separated repo-relative grammar files (*.y, *.g4,
34
77
  *.pest, Grammar/python.gram, ...); a whole-language signal
35
78
  --tree-paths comma-separated repo-relative directory/structural signals
36
79
  harvested from the clone (compiler/ dir, lexer/parser/ast)
80
+ Passing --tree-file with --grammar-files or --tree-paths is an error, and
81
+ so is passing --manifests with --manifests-file.
82
+
83
+ tree_signals() reads every file of the listing and every folder that holds
84
+ one, so no caller's filter drops a path the gates accept (a root
85
+ `grammar.js`, `Python/ceval.c`, `check.go`), but leaves out a path with a
86
+ hidden segment or a segment of _NON_CORE_PATH_SEGMENTS (tests and fixtures,
87
+ docs, examples, benchmarks, scripts and tools): a CI workflow's check.yml,
88
+ scripts/check.sh or a test fixture's vm.js is not the repository's own
89
+ code. The grammar files are the paths _is_grammar_file() accepts at most
90
+ _GRAMMAR_MAX_DEPTH segments deep (a grammar deeper in the tree is a vendored
91
+ or test fixture) and outside a parsing folder below the top of the tree (a
92
+ library's reader of an input format, such as sympy/parsing/latex/LaTeX.g4,
93
+ _FORMAT_PARSER_DIRS), and the tree paths are every folder (with a trailing /)
94
+ and every file, which the compiler-folder, triad and corroborating-member
95
+ gates of _whole_language_tree() then judge: the triad among the paths near a
96
+ compiler folder, the corroborating member anywhere. A listing that says it
97
+ was cut short (a GitHub tree's `truncated`) adds the signal
98
+ `tree_truncated`.
37
99
 
38
100
  Output (JSON on stdout):
39
101
  shape library-API | reference-app | language-reference
@@ -46,25 +108,26 @@ Output (JSON on stdout):
46
108
  Exit codes:
47
109
  0 shape classified (not unknown)
48
110
  1 unknown shape (no heuristic matched)
49
- 2 error (invalid args, missing/unreadable files, parse failure)
111
+ 2 error (invalid args, missing/unreadable files, a tree listing that
112
+ cannot be read, holds no path or reports a failure, a manifests file
113
+ that is not a scan envelope, parse failure)
50
114
  """
51
115
 
52
116
  from __future__ import annotations
53
117
 
54
118
  import argparse
119
+ import importlib.util
55
120
  import json
56
121
  import re
57
122
  import sys
123
+ import tomllib
58
124
  from pathlib import Path
59
125
  from typing import Any
60
126
 
61
- try:
62
- import tomllib
63
- except ImportError:
64
- tomllib = None # type: ignore[assignment]
65
-
66
127
  # ---------------------------------------------------------------------------
67
- # Parser/grammar deps that signal language-reference shape
128
+ # Parser/grammar tools: a package named after one is a language-reference
129
+ # producer; one among the runtime deps raises the language_or_user:parser_dep
130
+ # question (detect() below)
68
131
  # ---------------------------------------------------------------------------
69
132
 
70
133
  _PARSER_DEPS_NPM = frozenset({
@@ -100,6 +163,17 @@ _GRAMMAR_EXTS = frozenset({
100
163
  ".ungram",
101
164
  })
102
165
  _GRAMMAR_BASENAMES = frozenset({"grammar.js", "grammar.json", "python.gram"})
166
+ # A grammar file deeper than this many path segments is a vendored or test
167
+ # fixture, not the repo's own grammar (Grammar/python.gram is 2 deep).
168
+ _GRAMMAR_MAX_DEPTH = 4
169
+ # A library keeps its readers of other formats in a parsing folder of its own
170
+ # package: sympy's sympy/parsing/latex/LaTeX.g4 and
171
+ # sympy/parsing/autolev/Autolev.g4 sit beside sympy/core/ and the library's
172
+ # other modules. A grammar below such a folder parses an input the library
173
+ # reads, not the language the repository implements. A parsing folder at the
174
+ # top of the tree is not one of these: there the parser is the repository's
175
+ # own.
176
+ _FORMAT_PARSER_DIRS = frozenset({"parsing", "parsers"})
103
177
 
104
178
  # Concrete parsers a repo CONSUMES. If a repo's own runtime deps contain one of
105
179
  # these it delegates parsing — a formatter/linter/bundler, never a
@@ -247,9 +321,14 @@ def _whole_language_tree(tree_paths: list[str]) -> tuple[str, str] | None:
247
321
  ``_COMPILER_DIRS_CASE`` (case-sensitive ``Parser``). A file named
248
322
  ``Parser.js`` or ``compiler.dart`` never satisfies this; a bare
249
323
  ``src/`` / ``lib/`` / ``src/language/`` directory never does either.
250
- (D) a lexer+parser+AST triad, parser MANDATORY, at least 2 of 3 present.
251
- (W) a corroborating codegen / VM / type-checker member, so a markdown or
252
- CSS library (lexer+parser+AST only) does not qualify.
324
+ (D) a lexer+parser+AST triad, parser MANDATORY, at least 2 of 3 present,
325
+ among the members NEAR that compiler directory (_near_compiler_dir):
326
+ a large library's lexer and parser in an unrelated corner of the
327
+ tree (PyTorch's TorchScript frontend beside its torch/compiler/ API
328
+ package) are no compiler's triad.
329
+ (W) a corroborating codegen / VM / type-checker member anywhere in the
330
+ tree, so a markdown or CSS library (lexer+parser+AST only) does not
331
+ qualify.
253
332
 
254
333
  Gates G (delegating consumer) and L (markup identity) depend on manifest
255
334
  data and are applied by the caller.
@@ -258,15 +337,13 @@ def _whole_language_tree(tree_paths: list[str]) -> tuple[str, str] | None:
258
337
  return None
259
338
 
260
339
  compiler_dirs: list[str] = []
261
- stems: set[str] = set()
262
- basenames: set[str] = set()
340
+ members_of: list[tuple[str, str, str]] = [] # (path, basename, stem)
263
341
  for tp in tree_paths:
264
342
  norm = tp.rstrip("/")
265
343
  if not norm:
266
344
  continue
267
345
  base = norm.rsplit("/", 1)[-1]
268
- basenames.add(base)
269
- stems.add(base.rsplit(".", 1)[0].lower() if "." in base else base.lower())
346
+ members_of.append((norm, base, base.rsplit(".", 1)[0].lower() if "." in base else base.lower()))
270
347
  if tp.endswith("/"):
271
348
  low = norm.lower()
272
349
  if any(low == m or low.endswith("/" + m) for m in _COMPILER_DIRS) or \
@@ -277,183 +354,101 @@ def _whole_language_tree(tree_paths: list[str]) -> tuple[str, str] | None:
277
354
  if not compiler_dirs:
278
355
  return None
279
356
 
280
- # (D) — triad, parser mandatory, >= 2 of 3
281
- lexer = bool(stems & _LEXER_STEMS) or "rustc_lexer" in basenames
282
- parser = bool(stems & _PARSER_STEMS) or "rustc_parse" in basenames
283
- binder, checker = "binder" in stems, "checker" in stems
284
- ast = (bool(stems & _AST_STEMS) or "rustc_ast" in basenames
285
- or (binder and checker))
286
- if not parser or (lexer + parser + ast) < 2:
287
- return None
288
-
289
- # (W) — corroborating compiler-grade member
357
+ # (W): a corroborating compiler-grade member, anywhere in the tree
358
+ stems = {stem for _, _, stem in members_of}
290
359
  w = bool(stems & (_CODEGEN_STEMS | _VM_STEMS | _CHECK_STEMS)) or \
291
- any(b.startswith("rustc_codegen") for b in basenames)
360
+ any(base.startswith("rustc_codegen") for _, base, _ in members_of)
292
361
  if not w:
293
362
  return None
294
363
 
295
- members = ",".join(
296
- m for m, present in (("lexer", lexer), ("parser", parser), ("ast", ast))
297
- if present
298
- )
299
- return compiler_dirs[0], members
300
-
301
-
302
- # ---------------------------------------------------------------------------
303
- # Minimal TOML parser (Python < 3.11 fallback)
304
- # ---------------------------------------------------------------------------
305
-
306
- def _split_preserving_nesting(s: str, sep: str) -> list[str]:
307
- parts: list[str] = []
308
- buf: list[str] = []
309
- depth = 0
310
- in_str = ""
311
- for ch in s:
312
- if in_str:
313
- buf.append(ch)
314
- if ch == in_str:
315
- in_str = ""
316
- continue
317
- if ch in ('"', "'"):
318
- in_str = ch
319
- buf.append(ch)
320
- continue
321
- if ch in ("[", "{"):
322
- depth += 1
323
- buf.append(ch)
324
- continue
325
- if ch in ("]", "}"):
326
- depth -= 1
327
- buf.append(ch)
328
- continue
329
- if ch == sep and depth == 0:
330
- parts.append("".join(buf))
331
- buf = []
332
- continue
333
- buf.append(ch)
334
- if buf:
335
- parts.append("".join(buf))
336
- return parts
337
-
338
-
339
- def _decode_toml_value(raw: str) -> Any:
340
- s = raw.strip()
341
- if not s:
342
- return ""
343
- if len(s) >= 2 and s[0] == '"' and s[-1] == '"':
344
- return s[1:-1]
345
- if len(s) >= 2 and s[0] == "'" and s[-1] == "'":
346
- return s[1:-1]
347
- if s == "true":
364
+ # (D): a triad near a compiler directory, parser mandatory, >= 2 of 3
365
+ for compiler_dir in compiler_dirs:
366
+ near = [(base, stem) for path, base, stem in members_of if _near_compiler_dir(path, compiler_dir)]
367
+ stems = {stem for _, stem in near}
368
+ basenames = {base for base, _ in near}
369
+ lexer = bool(stems & _LEXER_STEMS) or "rustc_lexer" in basenames
370
+ parser = bool(stems & _PARSER_STEMS) or "rustc_parse" in basenames
371
+ ast = (bool(stems & _AST_STEMS) or "rustc_ast" in basenames
372
+ or ("binder" in stems and "checker" in stems))
373
+ if parser and (lexer + parser + ast) >= 2:
374
+ members = ",".join(
375
+ m for m, present in (("lexer", lexer), ("parser", parser), ("ast", ast))
376
+ if present
377
+ )
378
+ return compiler_dir, members
379
+ return None
380
+
381
+
382
+ def _near_compiler_dir(path: str, compiler_dir: str) -> bool:
383
+ """Whether a tree path is near a compiler directory, for gate D: the
384
+ directory itself or a path under it, or a path at most two levels below
385
+ the directory that holds it (CPython's Parser/ has Python/ast.c beside
386
+ it, the Go port's internal/compiler/ has internal/parser/parser.go)."""
387
+ if path == compiler_dir or path.startswith(compiler_dir + "/"):
348
388
  return True
349
- if s == "false":
389
+ parent = compiler_dir.rsplit("/", 1)[0] if "/" in compiler_dir else ""
390
+ if parent and not path.startswith(parent + "/"):
350
391
  return False
351
- if s.startswith("["):
352
- idx = s.rfind("]")
353
- if idx < 0:
354
- return []
355
- inner = s[1:idx].strip()
356
- if not inner:
357
- return []
358
- items = []
359
- for item in _split_preserving_nesting(inner, ","):
360
- item = item.strip()
361
- if item and item[0] != "#":
362
- items.append(_decode_toml_value(item))
363
- return items
364
- if s.startswith("{"):
365
- idx = s.rfind("}")
366
- if idx < 0:
367
- return {}
368
- inner = s[1:idx].strip()
369
- result: dict[str, Any] = {}
370
- for pair in _split_preserving_nesting(inner, ","):
371
- pair = pair.strip()
372
- if "=" in pair:
373
- k, _, v = pair.partition("=")
374
- result[k.strip()] = _decode_toml_value(v.strip())
375
- return result
376
- try:
377
- return int(s) if "." not in s else float(s)
378
- except ValueError:
379
- return s
380
-
381
-
382
- def _loads_toml_fallback(content: str) -> dict[str, Any]:
383
- """Parse the TOML subset found in pyproject.toml / Cargo.toml."""
384
- root: dict[str, Any] = {}
385
- current = root
386
- lines = content.split("\n")
387
- i = 0
388
- while i < len(lines):
389
- raw = lines[i]
390
- i += 1
391
- stripped = raw.strip()
392
- if not stripped or stripped[0] == "#":
392
+ return path[len(parent) + 1 if parent else 0:].count("/") <= 1
393
+
394
+
395
+ def tree_signals(files: list[str]) -> tuple[list[str], list[str]]:
396
+ """(grammar files, tree paths) of a repository's whole file list, the
397
+ tree-level inputs of detect(): the grammar files _is_grammar_file()
398
+ accepts at most _GRAMMAR_MAX_DEPTH segments deep and not below a
399
+ _FORMAT_PARSER_DIRS folder under the top one, and every folder (with
400
+ a trailing /) then every file as tree paths, so the gates of
401
+ _whole_language_tree(), not a name filter, decide what counts. A path
402
+ with a hidden segment or a segment of _NON_CORE_PATH_SEGMENTS (tests and
403
+ fixtures, docs, examples, benchmarks, scripts and tools) is left out of
404
+ both: a CI workflow, a script or a test fixture is not the repository's
405
+ own code. An entry that ends in / is a folder."""
406
+ grammar: set[str] = set()
407
+ dirs: set[str] = set()
408
+ names: set[str] = set()
409
+ for raw in files:
410
+ entry = raw.strip()
411
+ parts = [p for p in entry.split("/") if p and p != "."]
412
+ if not parts or any(p.startswith(".") or p.lower() in _NON_CORE_PATH_SEGMENTS for p in parts):
393
413
  continue
394
-
395
- # [[array.of.tables]]
396
- if stripped.startswith("[["):
397
- end = stripped.find("]]")
398
- if end < 0:
399
- continue
400
- path = [p.strip() for p in stripped[2:end].split(".")]
401
- target = root
402
- for key in path[:-1]:
403
- target = target.setdefault(key, {})
404
- arr = target.setdefault(path[-1], [])
405
- if not isinstance(arr, list):
406
- arr = [arr]
407
- target[path[-1]] = arr
408
- entry: dict[str, Any] = {}
409
- arr.append(entry)
410
- current = entry
411
- continue
412
-
413
- # [table]
414
- if stripped.startswith("[") and not stripped.startswith("[["):
415
- end = stripped.find("]")
416
- if end < 0:
417
- continue
418
- path = [p.strip() for p in stripped[1:end].split(".")]
419
- current = root
420
- for key in path:
421
- nxt = current.setdefault(key, {})
422
- if not isinstance(nxt, dict):
423
- nxt = {}
424
- current[key] = nxt
425
- current = nxt
426
- continue
427
-
428
- # key = value
429
- eq = stripped.find("=")
430
- if eq < 0:
431
- continue
432
- key = stripped[:eq].strip().strip('"')
433
- val_str = stripped[eq + 1:].strip()
434
-
435
- # Remove trailing comment outside strings
436
- if val_str and val_str[0] not in ('"', "'", "[", "{"):
437
- ci = val_str.find(" #")
438
- if ci >= 0:
439
- val_str = val_str[:ci].strip()
440
-
441
- # Multi-line array
442
- if val_str.startswith("[") and "]" not in val_str:
443
- while i < len(lines):
444
- val_str += " " + lines[i].strip()
445
- i += 1
446
- if "]" in val_str:
447
- break
448
-
449
- current[key] = _decode_toml_value(val_str)
450
- return root
414
+ path = "/".join(parts)
415
+ if entry.endswith("/"):
416
+ dirs.add(path + "/")
417
+ else:
418
+ names.add(path)
419
+ if (len(parts) <= _GRAMMAR_MAX_DEPTH and _is_grammar_file(path)
420
+ and not any(p.lower() in _FORMAT_PARSER_DIRS for p in parts[1:-1])):
421
+ grammar.add(path)
422
+ for depth in range(1, len(parts)):
423
+ dirs.add("/".join(parts[:depth]) + "/")
424
+ return sorted(grammar), sorted(dirs) + sorted(names)
425
+
426
+
427
+ _SIBLINGS: dict[str, Any] = {}
428
+
429
+
430
+ def _sibling(filename: str) -> Any:
431
+ """A helper of this folder, loaded once as a module: skf-detect-language.py
432
+ reads a --tree-file listing. Raises ImportError when it does not sit
433
+ beside this script."""
434
+ module = _SIBLINGS.get(filename)
435
+ if module is None:
436
+ path = Path(__file__).resolve().parent / filename
437
+ name = "skf_" + filename.removeprefix("skf-").removesuffix(".py").replace("-", "_")
438
+ spec = importlib.util.spec_from_file_location(name, path)
439
+ if spec is None or spec.loader is None:
440
+ raise ImportError(f"cannot load {filename} beside {Path(__file__).name}")
441
+ module = importlib.util.module_from_spec(spec)
442
+ try:
443
+ spec.loader.exec_module(module)
444
+ except (OSError, SyntaxError) as exc:
445
+ raise ImportError(f"cannot load {filename} beside {Path(__file__).name}: {exc}") from exc
446
+ _SIBLINGS[filename] = module
447
+ return module
451
448
 
452
449
 
453
450
  def _parse_toml(content: str) -> dict[str, Any]:
454
- if tomllib is not None:
455
- return tomllib.loads(content)
456
- return _loads_toml_fallback(content)
451
+ return tomllib.loads(content)
457
452
 
458
453
 
459
454
  # ---------------------------------------------------------------------------
@@ -817,6 +812,29 @@ def _parse_manifest(path: Path) -> dict[str, Any]:
817
812
  return parser(path)
818
813
 
819
814
 
815
+ def read_manifests_file(path: str) -> list[str]:
816
+ """The manifest paths of a skf-scan-manifests.py envelope that this
817
+ script classifies (a _PARSERS file name), in the envelope's order and
818
+ as the envelope spells them: relative to the folder the scan ran on."""
819
+ try:
820
+ envelope = json.loads(Path(path).read_bytes().decode("utf-8-sig"))
821
+ except (OSError, UnicodeDecodeError, ValueError) as exc:
822
+ _die(f"cannot read the manifests file {path}: {exc}", "MANIFESTS_FILE_ERROR")
823
+ entries = envelope.get("manifests") if isinstance(envelope, dict) else None
824
+ if not isinstance(entries, list):
825
+ _die(f"{path} is not a skf-scan-manifests.py envelope: it has no manifests list",
826
+ "MANIFESTS_FILE_ERROR")
827
+ paths: list[str] = []
828
+ for entry in entries:
829
+ rel = entry.get("path") if isinstance(entry, dict) else None
830
+ if not isinstance(rel, str) or not rel:
831
+ _die(f"{path}: a manifests entry has no path", "MANIFESTS_FILE_ERROR")
832
+ rel = rel.replace("\\", "/")
833
+ if rel.rsplit("/", 1)[-1] in _PARSERS and rel not in paths:
834
+ paths.append(rel)
835
+ return paths
836
+
837
+
820
838
  # ---------------------------------------------------------------------------
821
839
  # Core classification
822
840
  # ---------------------------------------------------------------------------
@@ -826,6 +844,7 @@ def detect(
826
844
  manifest_paths: list[str],
827
845
  grammar_files: list[str] | None = None,
828
846
  tree_paths: list[str] | None = None,
847
+ manifest_dir: str | None = None,
829
848
  ) -> dict[str, Any]:
830
849
  """Classify a repo into a skill shape from its manifest files.
831
850
 
@@ -834,7 +853,9 @@ def detect(
834
853
  tree-level signals harvested from the clone; they let whole-language repos
835
854
  that carry no parser-generator dependency — and even manifest-less ones —
836
855
  be classified. When all three inputs are empty there is nothing to
837
- classify and we error, exactly as before.
856
+ classify and we error, exactly as before. With `manifest_dir`, each
857
+ manifest path is relative to it: the file read is the joined path, and
858
+ the core and depth checks see the relative one.
838
859
  """
839
860
  grammar_files = grammar_files or []
840
861
  tree_paths = tree_paths or []
@@ -843,7 +864,7 @@ def detect(
843
864
 
844
865
  parsed: list[dict[str, Any]] = []
845
866
  for mp in manifest_paths:
846
- p = Path(mp)
867
+ p = Path(manifest_dir) / mp if manifest_dir is not None else Path(mp)
847
868
  if not p.is_file():
848
869
  _die(f"Manifest not found: {p.as_posix()}", "MANIFEST_NOT_FOUND")
849
870
  m = _parse_manifest(p)
@@ -934,15 +955,20 @@ def detect(
934
955
  # like "parser"/"compiler"/"lang", which are false-positive farms (a CSS
935
956
  # parser, compiler-builtins, an arg parser are ordinary libraries).
936
957
  #
937
- # CONSUMER: a project that depends on a parser generator (a DSL built on
938
- # lalrpop) is also a language project. Exclude the repo's own producer name
939
- # from the consumer list so a self-reference isn't double-counted as "uses".
958
+ # CONSUMER: a project with a parser tool among its core members' RUNTIME
959
+ # deps, read like the framework signals above: a dev or build dependency
960
+ # tests or builds the project, and an examples/ member or the coordinator
961
+ # root is not the product. It may be a language built on the tool (a DSL
962
+ # on lalrpop) or only read its input with it (a CLI on nom), so it
963
+ # decides no shape: the ladder asks (below). Exclude the repo's own
964
+ # producer name from the consumer list so a self-reference isn't
965
+ # double-counted as "uses".
940
966
  own_names = {
941
967
  (m.get("name") or "").strip().lower() for m in parsed if m.get("name")
942
968
  }
943
969
  parser_producers = sorted(n for n in own_names if n in _ALL_PARSER_DEPS)
944
970
  parser_deps = sorted(
945
- d for d in all_deps
971
+ d for d in core_runtime_deps
946
972
  if d.lower() in _ALL_PARSER_DEPS and d.lower() not in parser_producers
947
973
  )
948
974
 
@@ -1019,17 +1045,22 @@ def detect(
1019
1045
  return {"shape": "language-reference", "signals": signals,
1020
1046
  "confidence": round(confidence, 2), **result_base}
1021
1047
 
1022
- # 1b. language-reference — a parser/grammar producer (own name) or a project
1023
- # built on a parser generator (consumer dep).
1024
- if parser_producers or parser_deps:
1025
- # A producer (named itself a grammar tool) is a stronger signal than a
1026
- # consumer (merely depends on one).
1027
- base = 0.80 if parser_producers else 0.75
1048
+ # 1b. language-reference: a parser/grammar producer (own name).
1049
+ if parser_producers:
1028
1050
  n_sig = len(parser_producers) + len(parser_deps)
1029
- confidence = _clamp(base + n_sig * 0.05, base, 0.90)
1051
+ confidence = _clamp(0.80 + n_sig * 0.05, 0.80, 0.90)
1030
1052
  return {"shape": "language-reference", "signals": signals,
1031
1053
  "confidence": round(confidence, 2), **result_base}
1032
1054
 
1055
+ # A consumer dep fits a language built on the parser tool and a project
1056
+ # that only reads its input with it alike: the dependency names alone
1057
+ # cannot tell them apart, so the rungs below answer with their own shape
1058
+ # and this signal hands that question to the caller. Gates G and L hold
1059
+ # here too: a repo they exclude is no language reference either way.
1060
+ parser_question = bool(parser_deps) and not delegating_consumer and not markup_identity
1061
+ if parser_question:
1062
+ signals.append("language_or_user:parser_dep")
1063
+
1033
1064
  # 2. stack-compose
1034
1065
  if len(ecosystems) > 1:
1035
1066
  confidence = _clamp(0.80 + (len(ecosystems) - 2) * 0.05, 0.80, 0.90)
@@ -1041,6 +1072,13 @@ def detect(
1041
1072
  # core runtime framework. A single-package repo with a bin is an app.
1042
1073
  app_trigger = has_framework if package_count > 1 else (app_has_bin or has_framework)
1043
1074
  if app_trigger:
1075
+ # A framework dependency with no bin, in a package that names itself a
1076
+ # library, fits an app built on the framework and a library that
1077
+ # extends it (FastAPI on starlette, axum-extra on axum) alike: the
1078
+ # dependency names alone cannot tell them apart, so the signal hands
1079
+ # that question to the caller and the shape stays reference-app.
1080
+ if not app_has_bin and has_library_structure:
1081
+ signals.append("app_or_library:framework_dep")
1044
1082
  strength = (1 if app_has_bin else 0) + (1 if has_framework else 0)
1045
1083
  confidence = _clamp(0.80 + (strength - 1) * 0.05, 0.80, 0.90)
1046
1084
  return {"shape": "reference-app", "signals": signals,
@@ -1059,7 +1097,21 @@ def detect(
1059
1097
  return {"shape": "library-API", "signals": signals,
1060
1098
  "confidence": round(confidence, 2), **result_base}
1061
1099
 
1062
- # 5. unknown
1100
+ # 5. language-reference: a consumer dep is the repository's only signal (a
1101
+ # bare DSL manifest: no rung above answered and no manifest declares a
1102
+ # bin), so no other shape is left to weigh the question against. A bin
1103
+ # that rung 3 does not count (the bin-only members of a monorepo) is an
1104
+ # app signal all the same: such a repository stays unknown, like its twin
1105
+ # with no parser dependency, and the caller analyzes it interactively.
1106
+ if parser_question and not has_bin:
1107
+ signals.remove("language_or_user:parser_dep")
1108
+ return {"shape": "language-reference", "signals": signals,
1109
+ "confidence": 0.75, **result_base}
1110
+
1111
+ # 6. unknown: the interactive fallback weighs the parser itself, with no
1112
+ # shape for the question to qualify.
1113
+ if parser_question:
1114
+ signals.remove("language_or_user:parser_dep")
1063
1115
  return {"shape": "unknown", "signals": signals,
1064
1116
  "confidence": 0.0, **result_base}
1065
1117
 
@@ -1095,15 +1147,31 @@ def _force_utf8(*streams) -> None:
1095
1147
 
1096
1148
 
1097
1149
  def main(argv: list[str]) -> int:
1098
- _force_utf8(sys.stdout, sys.stderr)
1150
+ _force_utf8(sys.stdin, sys.stdout, sys.stderr)
1099
1151
  parser = argparse.ArgumentParser(
1100
1152
  description="Classify a repo into a known skill shape from its manifest files.",
1101
1153
  )
1102
1154
  parser.add_argument("--repo-url", required=True, help="Repository URL")
1103
1155
  parser.add_argument(
1104
- "--manifests", required=True,
1156
+ "--manifests", default=None,
1105
1157
  help="Comma-separated local file paths to manifest files (may be empty "
1106
- "when --grammar-files or --tree-paths carry the signal)",
1158
+ "when --tree-file, --grammar-files or --tree-paths carry the signal); "
1159
+ "required unless --manifests-file is given",
1160
+ )
1161
+ parser.add_argument(
1162
+ "--manifests-file", default=None,
1163
+ help="The JSON envelope skf-scan-manifests.py scan printed, in place of "
1164
+ "--manifests: its manifests of the types this script classifies, "
1165
+ "each path resolved against --manifest-dir",
1166
+ )
1167
+ parser.add_argument(
1168
+ "--manifest-dir", default=None,
1169
+ help="The folder the scan ran on (required with --manifests-file)",
1170
+ )
1171
+ parser.add_argument(
1172
+ "--tree-file", default=None,
1173
+ help="The repository's whole file list: JSON (a `tree` list, or a list) or "
1174
+ "one path per line; - reads stdin. Replaces --grammar-files and --tree-paths",
1107
1175
  )
1108
1176
  parser.add_argument(
1109
1177
  "--grammar-files", default="",
@@ -1117,13 +1185,39 @@ def main(argv: list[str]) -> int:
1117
1185
  )
1118
1186
  args = parser.parse_args(argv)
1119
1187
 
1120
- manifest_paths = [p.strip() for p in args.manifests.split(",") if p.strip()]
1188
+ if args.manifests_file is not None:
1189
+ if args.manifests is not None:
1190
+ _die("pass the manifests once: --manifests or --manifests-file", "INVALID_ARGS")
1191
+ if args.manifest_dir is None:
1192
+ _die("--manifests-file needs --manifest-dir, the folder the scan ran on", "INVALID_ARGS")
1193
+ manifest_paths = read_manifests_file(args.manifests_file)
1194
+ elif args.manifests is None:
1195
+ _die("--manifests or --manifests-file is required", "MISSING_MANIFESTS")
1196
+ else:
1197
+ if args.manifest_dir is not None:
1198
+ _die("--manifest-dir goes with --manifests-file", "INVALID_ARGS")
1199
+ manifest_paths = [p.strip() for p in args.manifests.split(",") if p.strip()]
1121
1200
  grammar_files = [p.strip() for p in args.grammar_files.split(",") if p.strip()]
1122
1201
  tree_paths = [p.strip() for p in args.tree_paths.split(",") if p.strip()]
1202
+ truncated = False
1203
+ if args.tree_file is not None:
1204
+ if grammar_files or tree_paths:
1205
+ _die("pass the tree once: --tree-file, or --grammar-files and --tree-paths", "INVALID_ARGS")
1206
+ try:
1207
+ files, truncated = _sibling("skf-detect-language.py").read_tree_file(args.tree_file)
1208
+ except (ImportError, ValueError) as exc: # ValueError: the sibling's TreeListingError
1209
+ _die(str(exc), "TREE_FILE_ERROR")
1210
+ grammar_files, tree_paths = tree_signals(files)
1123
1211
  if not manifest_paths and not grammar_files and not tree_paths:
1124
- _die("--manifests requires at least one path", "MISSING_MANIFESTS")
1125
-
1126
- result = detect(args.repo_url, manifest_paths, grammar_files, tree_paths)
1212
+ if args.tree_file is None:
1213
+ _die("--manifests requires at least one path", "MISSING_MANIFESTS")
1214
+ # The listing holds only paths tree_signals() leaves out (hidden,
1215
+ # tests, docs, scripts): no signal to classify.
1216
+ result = {"shape": "unknown", "signals": [], "confidence": 0.0, "export_count": 0, "package_count": 0}
1217
+ else:
1218
+ result = detect(args.repo_url, manifest_paths, grammar_files, tree_paths, args.manifest_dir)
1219
+ if truncated:
1220
+ result["signals"].append("tree_truncated")
1127
1221
  json.dump(result, sys.stdout, ensure_ascii=False)
1128
1222
  sys.stdout.write("\n")
1129
1223