bmad-module-skill-forge 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +15 -5
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -90
  348. package/src/skf-setup/references/detect-and-tier.md +58 -67
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -679
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -1,50 +1,261 @@
1
1
  # /// script
2
- # requires-python = ">=3.9"
3
- # dependencies = []
2
+ # requires-python = ">=3.11"
3
+ # dependencies = ["pyyaml"]
4
4
  # ///
5
- """Campaign Parse Manifest — deterministic parse of a --manifest target list.
5
+ """Campaign Parse Manifest: validate every campaign target before the first compile.
6
6
 
7
- step-01 (Setup) seeds a headless campaign's targets from a plain-text manifest.
8
- Parsing that format by hand risks silently dropping a malformed line — exactly
9
- the kind of mechanical, all-or-nothing work that belongs in a script. This
10
- parser is the single source of truth for the format documented in SKILL.md
11
- "On Activation".
7
+ step-01 (Setup) takes a campaign's targets from a `--manifest` file, from a
8
+ `--brief` campaign-brief.yaml, or from the operator's answer (a pasted list or
9
+ repository URLs, rewritten as manifest lines and piped in). Every one of those
10
+ sources goes through this script, so a malformed target, a repository URL no
11
+ later stage can read, a name no sub-skill accepts, or two targets with one
12
+ name stop the campaign at Setup, never part-way through a run. step-01 section
13
+ 1 documents the manifest format (one `name,repo_url,tier,pin[;dep1,dep2]`
14
+ target per line) and this parser is its single source of truth.
12
15
 
13
- Format (one target per line):
16
+ Manifest format (one target per line):
14
17
 
15
18
  name,repo_url,tier,pin[;dep1,dep2,...]
16
19
 
20
+ - `name` may be empty: it is then taken from the repository's name
17
21
  - `pin` may be empty (latest): `name,repo_url,tier,` or `name,repo_url,tier`
18
22
  - a trailing `;`-segment lists depends_on names (comma-separated)
19
23
  - blank lines and lines starting with `#` are skipped
20
24
  - `tier` must be `A` or `B`
21
25
 
26
+ Brief format: a campaign-brief.yaml (templates/campaign-brief-template.yaml).
27
+ Each `targets[]` entry is checked the same way; `tier` defaults to `A`, `pin`
28
+ to null and `depends_on` to [], and every other field a target carries (a
29
+ language or scope hint) is kept. `pin` must be a string or null (quote a pin
30
+ such as 1.10, which YAML reads as a number), and `quality_gate.soft_target`
31
+ and `quality_gate.soft_fallback` must be numbers when the brief gives them.
32
+
33
+ Every source, each rule taken from the script of the stage that reads it:
34
+ - `repo_url` must name a GitHub repository: a URL, an SSH URL or `owner/repo`.
35
+ targets[] carries it as `https://github.com/<owner>/<repo>`, the one shape
36
+ the pin stage (skf-validate-pins.py) and every later stage read; another
37
+ host, or a path inside a repository, is an error
38
+ - `name` must be a skill name: lower-case letters, digits and hyphens,
39
+ starting and ending with a letter or digit, at most 64 characters
40
+ (brief-skill's KEBAB_RE, skf-validate-frontmatter.py's length)
41
+ - a Tier A `pin` must be an X.Y.Z version (brief-skill's `target_version`
42
+ rule): the pin reaches the Tier A build only as that input
43
+ - names are unique (a duplicate is an error, never a silent overwrite)
44
+ - `dangling_depends_on` lists each depends_on name that is no target, and
45
+ `tier_inversions` each Tier A target that depends on a Tier B target (the
46
+ strategy stage rejects both, through dependency_problems, which
47
+ campaign-deps.py imports); neither is an error here, so Setup can ask
48
+ about them before it writes state
49
+
50
+ `--stack-name <state-file>` instead prints the capstone's `stack_name`: the
51
+ state's campaign name by the rule that names a target from its repository,
52
+ cut so create-stack-skill's `-stack` suffix still fits in 64 characters, or
53
+ null when nothing is left of it.
54
+
22
55
  CLI:
23
56
  uv run campaign-parse-manifest.py <path/to/manifest.txt>
24
57
  cat manifest.txt | uv run campaign-parse-manifest.py -
58
+ uv run campaign-parse-manifest.py --brief <path/to/campaign-brief.yaml>
59
+ uv run campaign-parse-manifest.py --stack-name <path/to/_campaign-state.yaml>
25
60
 
26
61
  Output (JSON on stdout):
27
- {"targets": [{"name","repo_url","tier","pin","depends_on"}], "errors": [{"line","message"}]}
62
+ {"targets": [{"name","repo_url","tier","pin","depends_on", ...}],
63
+ "errors": [{"line" | "target" | "field", "message"}],
64
+ "filled_names": [{"name","repo_url"}],
65
+ "dangling_depends_on": [{"skill","depends_on"}],
66
+ "tier_inversions": [{"skill","depends_on"}]}
67
+ --brief adds "brief": {"campaign_name","quality_gate","architecture_doc_path",
68
+ "notes"} with only the keys the brief gives.
69
+ --stack-name prints {"stack_name": "<name>" | null}.
28
70
 
29
71
  Exit codes:
30
- 0 parsed cleanly (no errors)
31
- 1 one or more malformed lines (errors[] populated; targets[] omits bad lines)
32
- 2 file error (not found / unreadable)
72
+ 0 parsed cleanly (no errors), or the stack name printed
73
+ 1 one or more malformed lines or targets (errors[] populated; targets[]
74
+ omits them)
75
+ 2 file error (not found / unreadable / a brief or state that is not a
76
+ YAML mapping)
33
77
  """
34
78
 
35
79
  from __future__ import annotations
36
80
 
37
81
  import argparse
82
+ import importlib.util
38
83
  import json
84
+ import re
39
85
  import sys
40
86
  from pathlib import Path
41
- from typing import Any, Dict, List
87
+ from typing import Any, Dict, List, Optional, Tuple
88
+
89
+ import yaml
90
+
91
+ BRIEF_KEYS = ("campaign_name", "quality_gate", "architecture_doc_path", "notes")
92
+ TARGET_KEYS = ("name", "repo_url", "tier", "pin", "depends_on")
93
+ SHARED_SCRIPTS = Path(__file__).resolve().parent.parent.parent / "shared" / "scripts"
94
+ STACK_SUFFIX = "-stack"
95
+ # An owner or repository segment of a GitHub URL.
96
+ SEGMENT_RE = re.compile(r"^[A-Za-z0-9._-]+$")
97
+
98
+
99
+ def _shared(file_name: str) -> Any:
100
+ spec = importlib.util.spec_from_file_location(file_name[:-3].replace("-", "_"), SHARED_SCRIPTS / file_name)
101
+ module = importlib.util.module_from_spec(spec)
102
+ spec.loader.exec_module(module)
103
+ return module
104
+
105
+
106
+ # The rules the later stages apply, read from their own scripts so Setup
107
+ # cannot accept a target they refuse.
108
+ _PINS = _shared("skf-validate-pins.py")
109
+ _BRIEF_INPUTS = _shared("skf-validate-brief-inputs.py")
110
+ GITHUB_URL_RE = _PINS._GITHUB_URL_RE
111
+ KEBAB_RE = _BRIEF_INPUTS.KEBAB_RE
112
+ VERSION_RE = _BRIEF_INPUTS.SEMVER_RE
113
+ MAX_NAME = _shared("skf-validate-frontmatter.py").MAX_SKILL_NAME_LENGTH
114
+
115
+
116
+ def canonical_repo_url(repo_url: str) -> Tuple[Optional[str], Optional[str]]:
117
+ """(`https://github.com/<owner>/<repo>`, None) for a GitHub repository, else (None, why not).
118
+
119
+ Reads a GitHub URL (http or https, `www.` and `.git` allowed), an SSH URL
120
+ (`git@github.com:o/r` or `ssh://git@github.com/o/r`) and `owner/repo`.
121
+ """
122
+ url = repo_url.strip()
123
+ ssh = re.match(r"^(?:ssh://)?git@([^:/\s]+)[:/](.*)$", url)
124
+ if ssh:
125
+ host, path = ssh.group(1), ssh.group(2)
126
+ else:
127
+ rest = re.sub(r"^https?://", "", url, flags=re.I)
128
+ if "://" in rest:
129
+ return None, f"`repo_url` `{repo_url}` is no GitHub URL: use https://github.com/<owner>/<repo>"
130
+ first, _, path = rest.partition("/")
131
+ if rest != url or "." in first:
132
+ host = first
133
+ else:
134
+ host, path = "github.com", rest
135
+ host = host.lower()
136
+ if host.startswith("www."):
137
+ host = host[4:]
138
+ if host != "github.com":
139
+ return None, (
140
+ f"`repo_url` `{repo_url}` is not on GitHub: the pin and provenance stages "
141
+ "read GitHub repositories only"
142
+ )
143
+ parts = [p for p in path.strip().strip("/").split("/") if p]
144
+ if parts and parts[-1].lower().endswith(".git"):
145
+ parts[-1] = parts[-1][:-4]
146
+ if len(parts) < 2 or not all(SEGMENT_RE.match(p) and p.strip(".") for p in parts[:2]):
147
+ return None, f"`repo_url` `{repo_url}` names no repository (use a GitHub URL or `owner/repo`)"
148
+ canonical = f"https://github.com/{parts[0]}/{parts[1]}"
149
+ if len(parts) > 2:
150
+ return None, (
151
+ f"`repo_url` `{repo_url}` names a path inside a repository: give the repository "
152
+ f"itself, `{canonical}`"
153
+ )
154
+ if not GITHUB_URL_RE.match(canonical):
155
+ return None, f"`repo_url` `{repo_url}` names no repository (use a GitHub URL or `owner/repo`)"
156
+ return canonical, None
157
+
158
+
159
+ def name_from_repo(repo: str) -> str:
160
+ """A skill name from a repository name: lower case, runs of other characters as `-`."""
161
+ return re.sub(r"[^a-z0-9]+", "-", repo.lower()).strip("-")
162
+
163
+
164
+ def is_skill_name(name: str) -> bool:
165
+ return bool(KEBAB_RE.match(name)) and len(name) <= MAX_NAME
166
+
167
+
168
+ def stack_name(campaign_name: Any) -> Optional[str]:
169
+ """The capstone's stack_name: the campaign name as a skill name, short enough
170
+ for create-stack-skill to append `-stack` within MAX_NAME; None when empty."""
171
+ name = name_from_repo(campaign_name if isinstance(campaign_name, str) else "")
172
+ room = MAX_NAME if name.endswith(STACK_SUFFIX) else MAX_NAME - len(STACK_SUFFIX)
173
+ return name[:room].rstrip("-") or None
174
+
175
+
176
+ def dependency_problems(skills: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, str]]]:
177
+ """Each depends_on name that is no skill, and each Tier A skill that depends on a Tier B skill.
178
+
179
+ The one statement of both rules: campaign-deps.py imports it for the
180
+ strategy stage, so Setup and Strategy report the same entries. Tier B
181
+ skills are built in the batch stage, after the skill loop that builds
182
+ Tier A skills, so a Tier A skill could never pass that dependency gate.
183
+ """
184
+ tiers = {s.get("name"): s.get("tier") for s in skills}
185
+ dangling: List[Dict[str, str]] = []
186
+ inversions: List[Dict[str, str]] = []
187
+ for skill in skills:
188
+ for dep in skill.get("depends_on") or []:
189
+ if dep not in tiers:
190
+ dangling.append({"skill": skill.get("name"), "depends_on": dep})
191
+ elif skill.get("tier") == "A" and tiers[dep] == "B":
192
+ inversions.append({"skill": skill.get("name"), "depends_on": dep})
193
+ return {"dangling_depends_on": dangling, "tier_inversions": inversions}
194
+
195
+
196
+ class _Collector:
197
+ """Checks targets one at a time and keeps the results of the whole set."""
198
+
199
+ def __init__(self) -> None:
200
+ self.targets: List[Dict[str, Any]] = []
201
+ self.errors: List[Dict[str, Any]] = []
202
+ self.filled: List[Dict[str, str]] = []
203
+ self.seen: set = set()
204
+
205
+ def error(self, where: Dict[str, Any], message: str) -> None:
206
+ self.errors.append({**where, "message": message})
207
+
208
+ def add(self, where: Dict[str, int], target: Dict[str, Any]) -> None:
209
+ name, repo_url, tier, pin = target["name"], target["repo_url"], target["tier"], target["pin"]
210
+ if not repo_url:
211
+ label = f"`{name}`" if name else "target"
212
+ self.error(where, f"{label} has empty `repo_url`")
213
+ return
214
+ canonical, problem = canonical_repo_url(repo_url)
215
+ if canonical is None:
216
+ self.error(where, problem)
217
+ return
218
+ target["repo_url"] = canonical
219
+ if not name:
220
+ name = name_from_repo(canonical.rsplit("/", 1)[1])
221
+ if not name:
222
+ self.error(where, f"empty `name`, and none can be taken from `{repo_url}`")
223
+ return
224
+ target["name"] = name
225
+ self.filled.append({"name": name, "repo_url": canonical})
226
+ if not is_skill_name(name):
227
+ self.error(where, (
228
+ f"`{name}` is no skill name: use lower-case letters, digits and hyphens, starting "
229
+ f"and ending with a letter or digit, at most {MAX_NAME} characters"
230
+ ))
231
+ return
232
+ if tier not in ("A", "B"):
233
+ self.error(where, f"`{name}` has invalid tier `{tier}` (must be A or B)")
234
+ return
235
+ if tier == "A" and pin is not None and not VERSION_RE.match(pin):
236
+ self.error(where, (
237
+ f"`{name}` is Tier A with pin `{pin}`, which is no X.Y.Z version: brief-skill builds "
238
+ "a Tier A skill from a release version only (write it in full, such as `1.2.0` or "
239
+ "`v1.2.0`, or leave the pin empty for the latest release)"
240
+ ))
241
+ return
242
+ if name in self.seen:
243
+ self.error(where, f"duplicate target name `{name}`")
244
+ return
245
+ self.seen.add(name)
246
+ self.targets.append(target)
247
+
248
+ def result(self) -> Dict[str, Any]:
249
+ return {
250
+ "targets": self.targets,
251
+ "errors": self.errors,
252
+ "filled_names": self.filled,
253
+ **dependency_problems(self.targets),
254
+ }
42
255
 
43
256
 
44
257
  def parse_manifest_text(text: str) -> Dict[str, Any]:
45
- targets: List[Dict[str, Any]] = []
46
- errors: List[Dict[str, Any]] = []
47
- seen: set = set()
258
+ found = _Collector()
48
259
 
49
260
  for lineno, raw in enumerate(text.splitlines(), start=1):
50
261
  line = raw.strip()
@@ -54,64 +265,160 @@ def parse_manifest_text(text: str) -> Dict[str, Any]:
54
265
  body, sep, deps_part = line.partition(";")
55
266
  fields = [f.strip() for f in body.split(",")]
56
267
  if len(fields) < 3:
57
- errors.append({"line": lineno, "message": f"expected `name,repo_url,tier[,pin]`, got {len(fields)} field(s)"})
268
+ found.error({"line": lineno}, f"expected `name,repo_url,tier[,pin]`, got {len(fields)} field(s)")
58
269
  continue
59
270
 
60
271
  name, repo_url, tier = fields[0], fields[1], fields[2]
61
272
  pin = fields[3] if len(fields) >= 4 and fields[3] != "" else None
273
+ depends_on = [d.strip() for d in deps_part.split(",") if d.strip()] if sep else []
274
+ found.add(
275
+ {"line": lineno},
276
+ {"name": name, "repo_url": repo_url, "tier": tier, "pin": pin, "depends_on": depends_on},
277
+ )
62
278
 
63
- if not name:
64
- errors.append({"line": lineno, "message": "empty `name`"})
65
- continue
66
- if not repo_url:
67
- errors.append({"line": lineno, "message": f"`{name}` has empty `repo_url`"})
68
- continue
69
- if tier not in ("A", "B"):
70
- errors.append({"line": lineno, "message": f"`{name}` has invalid tier `{tier}` (must be A or B)"})
71
- continue
72
- if name in seen:
73
- errors.append({"line": lineno, "message": f"duplicate target name `{name}`"})
74
- continue
75
- seen.add(name)
279
+ return found.result()
76
280
 
77
- depends_on = [d.strip() for d in deps_part.split(",") if d.strip()] if sep else []
78
281
 
79
- targets.append(
80
- {"name": name, "repo_url": repo_url, "tier": tier, "pin": pin, "depends_on": depends_on}
81
- )
282
+ def _brief_target(found: _Collector, index: int, entry: Any) -> None:
283
+ where = {"target": index}
284
+ if not isinstance(entry, dict):
285
+ found.error(where, "a target must be a mapping with at least `repo_url`")
286
+ return
287
+ name = entry.get("name")
288
+ repo_url = entry.get("repo_url")
289
+ tier = entry.get("tier", "A")
290
+ pin = entry.get("pin")
291
+ depends_on = entry.get("depends_on")
292
+ if name is not None and not isinstance(name, str):
293
+ found.error(where, f"`name` must be a string, got {name!r}")
294
+ return
295
+ if repo_url is not None and not isinstance(repo_url, str):
296
+ found.error(where, f"`repo_url` must be a string, got {repo_url!r}")
297
+ return
298
+ if pin is not None and not isinstance(pin, str):
299
+ found.error(where, f"`pin` must be a string or null, got {pin!r} (quote a version pin)")
300
+ return
301
+ if depends_on is None:
302
+ depends_on = []
303
+ if not isinstance(depends_on, list) or not all(isinstance(d, str) and d.strip() for d in depends_on):
304
+ found.error(where, "`depends_on` must be a list of target names")
305
+ return
306
+ target = {
307
+ "name": (name or "").strip(),
308
+ "repo_url": (repo_url or "").strip(),
309
+ "tier": tier,
310
+ "pin": (pin.strip() or None) if isinstance(pin, str) else None,
311
+ "depends_on": [d.strip() for d in depends_on],
312
+ }
313
+ target.update({k: v for k, v in entry.items() if k not in TARGET_KEYS})
314
+ found.add(where, target)
82
315
 
83
- return {"targets": targets, "errors": errors}
84
316
 
317
+ def parse_brief(brief: Dict[str, Any]) -> Dict[str, Any]:
318
+ found = _Collector()
319
+ targets = brief.get("targets")
320
+ if targets is None:
321
+ targets = []
322
+ if not isinstance(targets, list):
323
+ found.error({"field": "targets"}, "`targets` must be a list")
324
+ targets = []
325
+ for index, entry in enumerate(targets, start=1):
326
+ _brief_target(found, index, entry)
85
327
 
86
- def run(path: str) -> int:
328
+ fields = {k: brief[k] for k in BRIEF_KEYS if k in brief}
329
+ gate = fields.get("quality_gate")
330
+ if gate is not None and not isinstance(gate, dict):
331
+ found.error({"field": "quality_gate"}, "`quality_gate` must be a mapping")
332
+ fields.pop("quality_gate")
333
+ elif isinstance(gate, dict):
334
+ for key in ("soft_target", "soft_fallback"):
335
+ value = gate.get(key)
336
+ if key in gate and (isinstance(value, bool) or not isinstance(value, (int, float))):
337
+ found.error({"field": f"quality_gate.{key}"}, f"`quality_gate.{key}` must be a number, got {value!r}")
338
+ result = found.result()
339
+ result["brief"] = fields
340
+ return result
341
+
342
+
343
+ def _read(path: str) -> tuple:
344
+ p = Path(path)
345
+ if not p.is_file():
346
+ return None, ("MANIFEST_NOT_FOUND", f"Manifest not found: {path}")
347
+ try:
348
+ # utf-8-sig: a byte-order mark an editor wrote is no part of the first name.
349
+ return p.read_text(encoding="utf-8-sig"), None
350
+ except (OSError, UnicodeDecodeError) as exc:
351
+ return None, ("MANIFEST_READ_ERROR", f"Manifest unreadable: {exc}")
352
+
353
+
354
+ def _fail(code: str, message: str) -> int:
355
+ json.dump({"error": message, "code": code}, sys.stderr)
356
+ sys.stderr.write("\n")
357
+ return 2
358
+
359
+
360
+ def _emit(payload: Dict[str, Any]) -> None:
361
+ json.dump(payload, sys.stdout, separators=(",", ":"))
362
+ sys.stdout.write("\n")
363
+
364
+
365
+ def run(path: str, brief: bool = False) -> int:
87
366
  if path == "-":
88
- text = sys.stdin.read()
367
+ # Bytes as UTF-8, whatever the console's code page (cp1252 on Windows).
368
+ text = sys.stdin.buffer.read().decode("utf-8", errors="replace").lstrip("")
89
369
  else:
90
- p = Path(path)
91
- if not p.is_file():
92
- json.dump({"error": f"Manifest not found: {path}", "code": "MANIFEST_NOT_FOUND"}, sys.stderr)
93
- sys.stderr.write("\n")
94
- return 2
370
+ text, problem = _read(path)
371
+ if problem:
372
+ code, message = problem
373
+ if brief:
374
+ code, message = code.replace("MANIFEST", "BRIEF"), message.replace("Manifest", "Brief")
375
+ return _fail(code, message)
376
+
377
+ if brief:
95
378
  try:
96
- text = p.read_text(encoding="utf-8")
97
- except OSError as exc:
98
- json.dump({"error": f"Manifest unreadable: {exc}", "code": "MANIFEST_READ_ERROR"}, sys.stderr)
99
- sys.stderr.write("\n")
100
- return 2
101
-
102
- result = parse_manifest_text(text)
103
- json.dump(result, sys.stdout, separators=(",", ":"))
104
- sys.stdout.write("\n")
379
+ data = yaml.safe_load(text)
380
+ except yaml.YAMLError as exc:
381
+ return _fail("BRIEF_PARSE_ERROR", f"Brief is not valid YAML: {exc}")
382
+ if not isinstance(data, dict):
383
+ return _fail("BRIEF_PARSE_ERROR", "Brief is not a YAML mapping")
384
+ result = parse_brief(data)
385
+ else:
386
+ result = parse_manifest_text(text)
387
+ _emit(result)
105
388
  return 1 if result["errors"] else 0
106
389
 
107
390
 
391
+ def run_stack_name(state_file: str) -> int:
392
+ path = Path(state_file)
393
+ if not path.is_file():
394
+ return _fail("STATE_NOT_FOUND", f"State file not found: {state_file}")
395
+ try:
396
+ state = yaml.safe_load(path.read_text(encoding="utf-8"))
397
+ except (OSError, UnicodeDecodeError, yaml.YAMLError) as exc:
398
+ return _fail("STATE_PARSE_ERROR", f"Failed to parse state file: {exc}")
399
+ if not isinstance(state, dict) or not isinstance(state.get("campaign"), dict):
400
+ return _fail("STATE_PARSE_ERROR", "State file has no campaign mapping")
401
+ _emit({"stack_name": stack_name(state["campaign"].get("name"))})
402
+ return 0
403
+
404
+
108
405
  def main(argv: list[str] | None = None) -> int:
109
406
  parser = argparse.ArgumentParser(
110
407
  prog="campaign-parse-manifest",
111
- description="Parse a --manifest target list into structured targets.",
408
+ description="Validate campaign targets from a --manifest list or a campaign-brief.yaml, "
409
+ "or print the capstone's stack name from a campaign state.",
112
410
  )
113
- parser.add_argument("path", help="path to manifest text file, or `-` for stdin")
411
+ parser.add_argument("path", nargs="?", help="path to manifest text file, or `-` for stdin")
412
+ parser.add_argument("--brief", help="path to a campaign-brief.yaml to validate instead")
413
+ parser.add_argument("--stack-name", dest="stack_state", help="path to _campaign-state.yaml whose campaign name gives the stack name")
114
414
  args = parser.parse_args(argv)
415
+ given = [value for value in (args.path, args.brief, args.stack_state) if value is not None]
416
+ if len(given) != 1:
417
+ parser.error("give one of: a manifest path (or `-` for stdin), --brief <file>, or --stack-name <state-file>")
418
+ if args.stack_state is not None:
419
+ return run_stack_name(args.stack_state)
420
+ if args.brief is not None:
421
+ return run(args.brief, brief=True)
115
422
  return run(args.path)
116
423
 
117
424