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,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.10"
3
+ # requires-python = ">=3.11"
4
4
  # dependencies = []
5
5
  # ///
6
6
  """SKF Render Metadata Stats — derive metadata.json's computed stats block and
@@ -40,9 +40,16 @@ distribution ALWAYS bins `entries[]`, so it always sums to `len(entries)`. What
40
40
  to the *citation count* (= `len(entries)`), NOT to `exports_documented`
41
41
  (which is the pattern-surface proxy `pattern_surfaces_documented`, a
42
42
  judgment value). `effective_denominator` is never emitted.
43
- - **stack** — `entries[]` are the cited constituent contracts, so the
44
- distribution sums to the *constituent count* (= `len(entries)`), NOT to
45
- `exports_documented` (a stack's own barrel is empty by design).
43
+ - **stack**: `exports_documented` comes from the payload (a stack's own
44
+ barrel is empty by design), and the distribution still sums to the
45
+ *entry count* (= `len(entries)`), one per provenance entry. A stack's
46
+ metadata.json counts libraries instead: its `confidence_distribution`
47
+ bins each library once, by the library's tier, and sums to
48
+ `library_count`. So create-stack-skill runs this shape in compute mode
49
+ for the label check (`coherence`) and for its evidence report's
50
+ per-entry counts, and writes neither `stats` nor
51
+ `confidence_distribution` into metadata.json; `--check` does not fit a
52
+ stack.
46
53
 
47
54
  Modes
48
55
  -----
@@ -57,7 +64,10 @@ metadata.json; re-derive the computed values (taking the judgment values —
57
64
  on-disk metadata itself) and compare against what is on disk. `coherence.ok`
58
65
  is false with a `violations[]` list when the on-disk file drifted; the emitted
59
66
  `stats`/`confidence_distribution` are the corrected values `validate.md §7`
60
- writes back.
67
+ writes back. While label violations are present (see Label agreement below)
68
+ they are not: the distribution is binned from the mislabeled
69
+ `signature_source`, so the caller relabels the provenance-map, re-runs the
70
+ helper, and writes the re-run's values.
61
71
 
62
72
  Judgment payload (stdin JSON, compute mode)::
63
73
 
@@ -88,11 +98,45 @@ Output JSON (both modes)::
88
98
  "coherence": {"ok": true, "violations": []}
89
99
  }
90
100
 
101
+ Label agreement (both modes)
102
+ ---------------------------
103
+
104
+ Every `entries[]` row whose `confidence` is T1 or T1-low (case-insensitive)
105
+ must carry the labels its `extraction_method` implies, so a label always names
106
+ the tool that produced the entry, never the forge tier:
107
+
108
+ - `extraction_method` is one of ast-grep, source-read, ast_bridge,
109
+ source_reading, qmd_bridge, compose-from-skill.
110
+ - ast-grep and ast_bridge need confidence T1; ast-grep also needs a
111
+ non-null `ast_node_type` (the `kind` the matching ast-grep recipe declares
112
+ in create-skill's extraction-patterns.md).
113
+ - source-read and source_reading need confidence T1-low and a
114
+ `signature_source` other than T1; source-read also needs `ast_node_type`
115
+ null (an absent key counts as null).
116
+ - qmd_bridge and compose-from-skill carry no pairing.
117
+
118
+ A row read by eye (source-read, source_reading) is checked at any other
119
+ `confidence` (T2, T3, none) too, for its `signature_source` alone: the
120
+ distribution bins every row by `signature_source`, so a T1 there would count
121
+ a signature read by eye as T1. The other rows with another confidence
122
+ (ast-grep, ast_bridge, qmd_bridge, compose-from-skill, an unknown method)
123
+ and `file_entries[]` are not checked. Each disagreement is a violation keyed
124
+ by the row's index, because reference-app and stack maps repeat export
125
+ names::
126
+
127
+ {"field": "provenance.entries[12].confidence", "export_name": "search",
128
+ "expected": "T1-low", "actual": "T1",
129
+ "note": "extraction_method source-read pairs with confidence T1-low"}
130
+
131
+ The step that runs the helper relabels the entry to match its method (never
132
+ the reverse) and runs the helper again.
133
+
91
134
  Exit codes:
92
- 0 — coherence ok (no violations)
93
- 1 — coherence violations present (drift to auto-fix / bad signature_source);
94
- the JSON on stdout is still authoritative — rely on it, not the code
95
- 2 — usage / IO / JSON-decode error
135
+ 0: coherence ok (no violations)
136
+ 1: coherence violations present (drift to auto-fix, a bad signature_source,
137
+ or a label that disagrees with extraction_method); the JSON on stdout is
138
+ still authoritative, so rely on it, not the code
139
+ 2: usage, IO or JSON-decode error
96
140
  """
97
141
 
98
142
  from __future__ import annotations
@@ -109,6 +153,16 @@ _SIG_MAP = {"T1": "t1", "T1-LOW": "t1_low", "T2": "t2", "T3": "t3"}
109
153
  _VALID_SHAPES = ("library", "stack", "reference-app")
110
154
  _DIST_KEYS = ("t1", "t1_low", "t2", "t3")
111
155
 
156
+ # extraction_method values a T1 or T1-low entry may carry, and the labels
157
+ # each one implies. ast-grep / source-read are what create-skill and
158
+ # update-skill write; the other four come from stack maps.
159
+ _KNOWN_METHODS = (
160
+ "ast-grep", "source-read", "ast_bridge", "source_reading", "qmd_bridge", "compose-from-skill",
161
+ )
162
+ _T1_METHODS = ("ast-grep", "ast_bridge") # confidence T1
163
+ _T1_LOW_METHODS = ("source-read", "source_reading") # confidence T1-low, signature_source never T1
164
+ _LABELED_CONFIDENCE = ("T1", "T1-LOW")
165
+
112
166
 
113
167
  # --------------------------------------------------------------------------
114
168
  # IO
@@ -275,13 +329,95 @@ def _cmp_ratio(violations: list, field: str, expected, actual) -> None:
275
329
  violations.append({"field": field, "expected": expected, "actual": actual})
276
330
 
277
331
 
332
+ def _upper(value: object) -> str | None:
333
+ return value.strip().upper() if isinstance(value, str) else None
334
+
335
+
336
+ def check_label_agreement(prov: dict) -> list[dict]:
337
+ """Check that each T1 / T1-low entry's labels match its extraction_method,
338
+ and that no entry read by eye claims a T1 signature_source.
339
+
340
+ Returns one violation per disagreeing field, keyed by the entry's index
341
+ (`provenance.entries[<i>].<field>`) since export names repeat in
342
+ reference-app and stack maps. `expected` is the value the method implies;
343
+ the caller relabels the entry to it and never changes a known method.
344
+ An entry read by eye (source-read, source_reading) with any other
345
+ confidence is checked for its signature_source alone; the other entries
346
+ with another confidence, and `file_entries[]`, are not checked.
347
+ """
348
+ entries = prov.get("entries")
349
+ if not isinstance(entries, list):
350
+ return []
351
+ violations: list[dict] = []
352
+
353
+ for i, entry in enumerate(entries):
354
+ if not isinstance(entry, dict):
355
+ continue
356
+ confidence = entry.get("confidence")
357
+ method = entry.get("extraction_method")
358
+ labeled = _upper(confidence) in _LABELED_CONFIDENCE
359
+ # Off T1 / T1-low only an entry read by eye is checked, for its
360
+ # signature_source: binning reads that label whatever the confidence,
361
+ # so a T1 there would count a signature read by eye as T1.
362
+ if not labeled and method not in _T1_LOW_METHODS:
363
+ continue
364
+ name = entry.get("export_name")
365
+
366
+ def flag(field: str, expected: object, actual: object, note: str) -> None:
367
+ violations.append({
368
+ "field": f"provenance.entries[{i}].{field}",
369
+ "export_name": name,
370
+ "expected": expected,
371
+ "actual": actual,
372
+ "note": note,
373
+ })
374
+
375
+ if method not in _KNOWN_METHODS:
376
+ flag("extraction_method", sorted(_KNOWN_METHODS), method,
377
+ "a T1 or T1-low entry names the tool that produced it: set ast-grep for an "
378
+ "ast-grep match or source-read for an entry read by eye")
379
+ continue
380
+
381
+ if method in _T1_METHODS:
382
+ if _upper(confidence) != "T1":
383
+ flag("confidence", "T1", confidence,
384
+ f"extraction_method {method} pairs with confidence T1")
385
+ node = entry.get("ast_node_type")
386
+ # "non-null" is the violation's own expected value, not a node kind:
387
+ # a fixer that copied it has still not recorded the kind.
388
+ if method == "ast-grep" and (not (isinstance(node, str) and node.strip())
389
+ or _upper(node) == "NON-NULL"):
390
+ flag("ast_node_type", "non-null", node,
391
+ "extraction_method ast-grep pairs with a non-null ast_node_type: copy "
392
+ "the kind the matching recipe declares")
393
+ elif method in _T1_LOW_METHODS:
394
+ if labeled and _upper(confidence) != "T1-LOW":
395
+ flag("confidence", "T1-low", confidence,
396
+ f"extraction_method {method} pairs with confidence T1-low")
397
+ sig = entry.get("signature_source")
398
+ if _upper(sig) == "T1":
399
+ flag("signature_source", "T1-low", sig,
400
+ f"extraction_method {method} pairs with a signature_source other than T1 "
401
+ "(any tier but T1 passes)")
402
+ node = entry.get("ast_node_type")
403
+ if labeled and method == "source-read" and node is not None:
404
+ flag("ast_node_type", None, node,
405
+ "extraction_method source-read pairs with ast_node_type null: no ast-grep "
406
+ "rule matched this entry")
407
+ # qmd_bridge and compose-from-skill carry no pairing.
408
+
409
+ return violations
410
+
411
+
278
412
  def coherence_compute(derived: dict, prov: dict) -> dict:
279
413
  """Internal-consistency checks for compute mode.
280
414
 
281
415
  The distribution must account for every provenance entry (a bin-sum below
282
416
  the entry count means some entries carry a missing/unrecognized
283
417
  signature_source). When the provenance-map already tracks scripts/assets in
284
- file_entries[], those counts must agree with the stats counts.
418
+ file_entries[], those counts must agree with the stats counts. Every T1 or
419
+ T1-low entry's labels must match its extraction_method, and no entry read
420
+ by eye may claim a T1 signature_source (check_label_agreement).
285
421
  """
286
422
  violations: list[dict] = []
287
423
  entry_count = derived["_derivation"]["entry_count"]
@@ -311,6 +447,7 @@ def coherence_compute(derived: dict, prov: dict) -> dict:
311
447
  _cmp_int(violations, "stats.assets_count", fe["asset"], ac,
312
448
  "stats.assets_count disagrees with provenance-map file_entries[file_type=asset]")
313
449
 
450
+ violations.extend(check_label_agreement(prov))
314
451
  return {"ok": not violations, "violations": violations}
315
452
 
316
453
 
@@ -320,6 +457,8 @@ def check_stats(derived: dict, metadata: dict, prov: dict) -> dict:
320
457
  Every mismatch is a `{field, expected, actual}` violation, where `expected`
321
458
  is the re-derived (correct) value and `actual` is what is on disk.
322
459
  validate.md §7 either writes `derived` verbatim or fixes each violation.
460
+ Label violations from check_label_agreement are appended too; they concern
461
+ the provenance-map, which is relabeled before the stats are fixed.
323
462
  """
324
463
  violations: list[dict] = []
325
464
  m_stats = metadata.get("stats") if isinstance(metadata.get("stats"), dict) else {}
@@ -364,6 +503,8 @@ def check_stats(derived: dict, metadata: dict, prov: dict) -> dict:
364
503
  _cmp_int(violations, "provenance.file_entries.asset", claimed_ac, fe["asset"],
365
504
  "provenance-map file_entries[file_type=asset] count disagrees with metadata assets_count")
366
505
 
506
+ # provenance entry labels vs extraction_method (the map, not metadata.json)
507
+ violations.extend(check_label_agreement(prov))
367
508
  return {"ok": not violations, "violations": violations}
368
509
 
369
510
 
@@ -406,8 +547,11 @@ def build_parser() -> argparse.ArgumentParser:
406
547
  description=(
407
548
  "Derive metadata.json stats + confidence_distribution from the staged "
408
549
  "provenance-map (compute mode), or re-verify an on-disk metadata.json "
409
- "against the map (--check). Replaces the hand-binning/summing in "
410
- "skf-create-skill compile.md §4 and validate.md §7."
550
+ "against the map (--check). Both modes also check that each T1 or "
551
+ "T1-low entry's confidence, signature_source and ast_node_type match "
552
+ "its extraction_method, and that no entry read by eye claims a T1 "
553
+ "signature_source at any confidence. Replaces the hand-binning/summing "
554
+ "in skf-create-skill compile.md §4 and validate.md §7."
411
555
  ),
412
556
  )
413
557
  parser.add_argument("provenance_map", help="path to the staged provenance-map.json")
@@ -1,14 +1,16 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
4
  # ///
5
- """SKF Render Quick Metadata — render metadata.json per the canonical
5
+ """SKF Render Quick Metadata: render metadata.json per the canonical
6
6
  skill-template.md schema with quick-skill-specific population rules.
7
7
 
8
- Pure renderer — no I/O beyond stdin/stdout. Reads a JSON payload of
9
- extracted state on stdin and emits the corresponding metadata.json
10
- on stdout. Replaces the hand-assembly the LLM previously did in
11
- skf-quick-skill step 4 §4.
8
+ Replaces the hand-assembly the LLM previously did in skf-quick-skill step 4
9
+ section 4. The export list, the dependencies and the package's version come
10
+ from the files step 3 wrote (skf-extract-public-api.py's output, or
11
+ skf-skills-module.py extract's), and the SKF version from the installed
12
+ module, so the model stages only the fields it decides: no export list and
13
+ no file probe passes through a shell string.
12
14
 
13
15
  Constants vs. input-derived split mirrors step 4's documentation:
14
16
 
@@ -23,7 +25,7 @@ Constants vs. input-derived split mirrors step 4's documentation:
23
25
  stats.exports_internal, stats.scripts_count, stats.assets_count 0
24
26
  stats.public_api_coverage, stats.total_coverage 1.0
25
27
 
26
- Input-derived (from stdin payload):
28
+ Input-derived:
27
29
  name, version, description, language, source_repo,
28
30
  source_root, source_commit, source_package,
29
31
  exports[], dependencies[], compatibility,
@@ -35,7 +37,33 @@ Constants vs. input-derived split mirrors step 4's documentation:
35
37
  confidence_distribution.t1_low count(exports)
36
38
  stats.exports_documented / public_api / total count(exports)
37
39
 
38
- Input JSON shape (stdin):
40
+ CLI:
41
+
42
+ uv run skf-render-quick-metadata.py --input <metadata-input.json> \\
43
+ [--extraction <file>]... [--skf-root <dir>] [--output <metadata.json>]
44
+ python3 skf-render-quick-metadata.py < payload.json
45
+
46
+ With --input, the payload is that file (a quoted heredoc stages it, so a
47
+ description holding an apostrophe, a quote or a `$` arrives as written),
48
+ and each --extraction file is the JSON step 3 printed for one module, in
49
+ module order (the parent first). A payload field that is absent or null
50
+ takes its value from them:
51
+
52
+ exports every extraction's exports[] names, in order, each once
53
+ (a payload `exports` list is the --exports override and
54
+ wins)
55
+ dependencies every extraction's dependencies[], each once
56
+ version the first extraction's version that is set, else "1.0.0"
57
+ (a payload version, the target's pinned version, wins)
58
+ source_package the first extraction's package_name, else name
59
+ description the first extraction's description
60
+
61
+ and tool_versions.skf, unless the payload names skf_version, is probed
62
+ under --skf-root (the installed module, {project-root}/_bmad/skf): the
63
+ `version` of its package.json, else the first line of its VERSION file,
64
+ else "unknown". --output also writes the rendered metadata.json there.
65
+
66
+ Without --input, the whole payload comes on stdin, every field typed:
39
67
 
40
68
  {
41
69
  "name": "foo",
@@ -54,15 +82,15 @@ Input JSON shape (stdin):
54
82
  "skf_version": "1.2.0" (default "unknown")
55
83
  }
56
84
 
57
- CLI:
58
-
59
- python3 skf-render-quick-metadata.py < input.json
85
+ The rendered metadata.json is printed on stdout either way.
60
86
 
61
87
  Exit codes:
62
88
 
63
89
  0 success
64
- 1 payload-level error (missing required field)
65
- 2 stdin / argparse / JSON-decode error
90
+ 1 payload-level error (missing required field: name, language or
91
+ source_repo)
92
+ 2 stdin / argparse / JSON-decode error, or an --input, --extraction or
93
+ --output file that cannot be read or written
66
94
  """
67
95
 
68
96
  from __future__ import annotations
@@ -71,6 +99,7 @@ import argparse
71
99
  import datetime as dt
72
100
  import json
73
101
  import sys
102
+ from pathlib import Path
74
103
 
75
104
 
76
105
  REQUIRED_FIELDS = ("name", "language", "source_repo")
@@ -103,6 +132,46 @@ def _iso_utc_now() -> str:
103
132
  return dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
104
133
 
105
134
 
135
+ def probe_skf_version(skf_root: str | Path | None) -> str:
136
+ """The installed SKF's version: package.json `version`, else the VERSION file's first line, else "unknown"."""
137
+ if not skf_root:
138
+ return "unknown"
139
+ root = Path(skf_root)
140
+ try:
141
+ version = json.loads((root / "package.json").read_text(encoding="utf-8-sig")).get("version")
142
+ if isinstance(version, str) and version.strip():
143
+ return version.strip()
144
+ except (OSError, ValueError, AttributeError):
145
+ pass
146
+ try:
147
+ lines = (root / "VERSION").read_text(encoding="utf-8-sig").strip().splitlines()
148
+ if lines and lines[0].strip():
149
+ return lines[0].strip()
150
+ except (OSError, UnicodeDecodeError):
151
+ pass
152
+ return "unknown"
153
+
154
+
155
+ def merge_extractions(payload: dict, extractions: list[dict], skf_version: str | None = None) -> dict:
156
+ """The full renderer payload: the staged fields, then what the extraction files give."""
157
+ merged = dict(payload)
158
+
159
+ def first(key: str):
160
+ return next((e[key] for e in extractions if isinstance(e.get(key), str) and e[key].strip()), None)
161
+
162
+ if not isinstance(merged.get("exports"), list):
163
+ merged["exports"] = [x for e in extractions for x in (e.get("exports") or []) if isinstance(x, (dict, str))]
164
+ if not isinstance(merged.get("dependencies"), list):
165
+ deps = [d for e in extractions for d in (e.get("dependencies") or []) if isinstance(d, str)]
166
+ merged["dependencies"] = list(dict.fromkeys(deps))
167
+ for key, source in (("version", "version"), ("source_package", "package_name"), ("description", "description")):
168
+ if not merged.get(key):
169
+ merged[key] = first(source)
170
+ if not merged.get("skf_version") and skf_version is not None:
171
+ merged["skf_version"] = skf_version
172
+ return merged
173
+
174
+
106
175
  def render_metadata(payload: dict, *, now_fn=_iso_utc_now) -> dict:
107
176
  """Render the metadata.json envelope. Pass `now_fn` to inject a
108
177
  deterministic timestamp in tests.
@@ -161,30 +230,65 @@ def render_metadata(payload: dict, *, now_fn=_iso_utc_now) -> dict:
161
230
  return metadata
162
231
 
163
232
 
233
+ def _read_object(path: str, flag: str) -> dict:
234
+ """A JSON object from a file; raises ValueError with the message to print."""
235
+ try:
236
+ data = json.loads(Path(path).read_text(encoding="utf-8-sig"))
237
+ except (OSError, UnicodeDecodeError) as e:
238
+ raise ValueError(f"cannot read {flag} {path}: {getattr(e, 'strerror', None) or e}") from e
239
+ except json.JSONDecodeError as e:
240
+ raise ValueError(f"{flag} {path} is not valid JSON: {e}") from e
241
+ if not isinstance(data, dict):
242
+ raise ValueError(f"{flag} {path} must hold a JSON object")
243
+ return data
244
+
245
+
164
246
  def main(argv: list[str]) -> int:
165
247
  parser = argparse.ArgumentParser(
166
- description="Render skf-quick-skill metadata.json from extracted state on stdin.",
248
+ description="Render skf-quick-skill metadata.json from staged fields and the extraction files "
249
+ "(or from a whole payload on stdin).",
167
250
  )
168
- parser.parse_args(argv) # currently no flags; kept for forward compat
251
+ parser.add_argument("--input", help="the staged fields (metadata-input.json); without it, stdin")
252
+ parser.add_argument("--extraction", action="append", default=[],
253
+ help="step 3's extraction output for one module (repeatable, parent first)")
254
+ parser.add_argument("--skf-root", help="the installed SKF module folder to read its version from")
255
+ parser.add_argument("--output", help="also write the rendered metadata.json here")
256
+ args = parser.parse_args(argv)
169
257
 
170
- raw = sys.stdin.read()
171
- if not raw.strip():
172
- sys.stderr.write("error: no input on stdin (expected JSON payload)\n")
173
- return 2
174
- try:
175
- payload = json.loads(raw)
176
- except json.JSONDecodeError as e:
177
- sys.stderr.write(f"error: stdin JSON parse error: {e}\n")
178
- return 2
179
- if not isinstance(payload, dict):
180
- sys.stderr.write("error: stdin payload must be a JSON object\n")
181
- return 2
258
+ if args.input is None:
259
+ raw = sys.stdin.read()
260
+ if not raw.strip():
261
+ sys.stderr.write("error: no input on stdin (expected JSON payload)\n")
262
+ return 2
263
+ try:
264
+ payload = json.loads(raw)
265
+ except json.JSONDecodeError as e:
266
+ sys.stderr.write(f"error: stdin JSON parse error: {e}\n")
267
+ return 2
268
+ if not isinstance(payload, dict):
269
+ sys.stderr.write("error: stdin payload must be a JSON object\n")
270
+ return 2
271
+ else:
272
+ try:
273
+ payload = _read_object(args.input, "--input")
274
+ extractions = [_read_object(path, "--extraction") for path in args.extraction]
275
+ except ValueError as e:
276
+ sys.stderr.write(f"error: {e}\n")
277
+ return 2
278
+ payload = merge_extractions(payload, extractions, probe_skf_version(args.skf_root))
182
279
 
183
280
  result = render_metadata(payload)
184
281
  if "_error" in result:
185
282
  sys.stderr.write(f"error: {result['_error']}\n")
186
283
  return 1
187
- print(json.dumps(result, indent=2))
284
+ text = json.dumps(result, indent=2)
285
+ if args.output:
286
+ try:
287
+ Path(args.output).write_text(text + "\n", encoding="utf-8")
288
+ except OSError as e:
289
+ sys.stderr.write(f"error: cannot write --output {args.output}: {e.strerror or e}\n")
290
+ return 2
291
+ print(text)
188
292
  return 0
189
293
 
190
294