bmad-module-skill-forge 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +51 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +14 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +117 -9
  75. package/src/shared/scripts/skf-detect-docs.py +892 -4
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +215 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +17 -6
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +55 -18
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -57
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -89
  348. package/src/skf-setup/references/detect-and-tier.md +58 -66
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -645
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -0,0 +1,746 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Extraction Inventory: write, extend and read create-skill's extraction inventory on disk.
6
+
7
+ skf-create-skill step 3 keeps its extraction inventory as JSON beside the
8
+ staging folder (`_bmad-output/.skf-stage/{skill-name}.inventory.json`), so
9
+ the exports, their citations and the extraction rules survive a context
10
+ compaction between extraction and compile. `init` seeds it from the recipe
11
+ runner's JSON and the script/asset detector's JSON, so the model never
12
+ types a record those tools already wrote; the model then sends only what it
13
+ produced itself (`patch`, `add`, `set`). Step 3c adds the T3 items it
14
+ extracts from fetched documentation, step 4 the T2 annotations it finds in
15
+ QMD, and step 7 writes `extraction-rules.yaml` from it (`rules`). Every
16
+ write goes through a temporary file and one rename.
17
+
18
+ Subcommands:
19
+
20
+ init --inventory <file> --skill <name>
21
+ --mode source|docs-only|component-library
22
+ --tier Quick|Forge|Forge+|Deep
23
+ [--extraction <runner JSON>] [--detected <detector JSON>]
24
+ Write a new inventory, replacing any file at --inventory. From
25
+ --extraction (skf-extract-public-api.py --mode full): every
26
+ export as the runner recorded it (confidence T1, extraction_method
27
+ ast-grep), with `internal: true` on each one its
28
+ entry_point_diff.internal lists; files_scanned (its
29
+ files_in_scope); aggregates, counts and arms; extraction_rules
30
+ (recipe_set, the recipes ids, scope, ast_grep_version); and the
31
+ runner warnings as lines (the head cap, each errors[] item, each
32
+ file_issues[] file, each files_without_recipes extension, each
33
+ warnings[] entry). From --detected (skf-detect-scripts-assets.py
34
+ detect): scripts_inventory and assets_inventory.
35
+
36
+ add --inventory <file> --field exports|t2_annotations|t3_items|warnings
37
+ Read a JSON list on stdin (objects; strings for warnings) and
38
+ append it to that list. An entry already in the list (equal
39
+ JSON; for exports, the same export_name and source_file) is not
40
+ added twice and counts in `duplicates`. An exports entry needs an
41
+ export_name; a missing confidence, extraction_method,
42
+ ast_node_type or ast_recipe defaults to T1-low, source-read,
43
+ null and null (an export read by eye). A t3_items entry whose
44
+ export_name names an export the inventory already holds is
45
+ dropped, not added: T3 content never overrides a T1, T1-low or
46
+ T2 entry for the same export.
47
+
48
+ patch --inventory <file>
49
+ Read a JSON list of objects on stdin, each an export_name, an
50
+ optional source_file, and any of signature, params and
51
+ return_type, and set those fields on the export it names (by
52
+ export_name and source_file; by export_name alone when no
53
+ source_file is given). A name that matches no export is listed
54
+ in `unmatched`, one that matches several in `ambiguous`; neither
55
+ changes anything. params lists each parameter as the runner
56
+ records one, {name, type, default, optional}, or as a typed
57
+ string ('userId: string').
58
+
59
+ set --inventory <file>
60
+ Read a JSON object on stdin and set each of its keys: top_exports,
61
+ co_imports, promoted_docs, component_catalog (lists),
62
+ authoritative_files_scan (an object or null), files_scanned (an
63
+ integer), counts and arms (objects, merged into what is there)
64
+ and intent_mapping. intent_mapping maps `scripts` and `assets`
65
+ to {"intent": "<free text>", "kept": [source_file, ...]}: the
66
+ inventory keeps only the kept entries of that list and records
67
+ the others in the mapping's `left_out`.
68
+
69
+ provenance --inventory <file> --target <provenance-map.json> [--add-entries]
70
+ Write the provenance map to --target, so compile never types an
71
+ entry the inventory holds. stdin holds the map's header, a JSON
72
+ object: source_repo and source_commit (required, null allowed),
73
+ and source_ref (default null), provenance_version (default
74
+ "2.0"), skill_name (default the inventory's), skill_type
75
+ (default "single") and generated_at (default now, ISO-8601
76
+ UTC); any other key is refused. entries[] holds the entry of
77
+ every export of the inventory, in its order: export_name,
78
+ export_type, params, return_type, source_file, source_line,
79
+ confidence, extraction_method, ast_node_type and
80
+ signature_source, labeled by the tool that found it. An export
81
+ an ast-grep rule matched is T1 (extraction_method ast-grep, the
82
+ ast_node_type its recipe declares, signature_source T1); any
83
+ other was read by eye and is T1-low (extraction_method
84
+ source-read, ast_node_type null, signature_source T1-low).
85
+ params holds the map's typed strings, one per parameter the
86
+ runner (or a patch) recorded: `name: type`, `name?: type` for a
87
+ JS/TS parameter that is optional and has no default,
88
+ ` = default` after a default, and the type alone for a
89
+ parameter with no name; a string a patch or an add sent is kept
90
+ as it is. params and return_type are null on an export whose
91
+ params is null: any export but a function, or a function nobody
92
+ read the signature of, which `unsigned` lists (patch them and
93
+ run provenance again). The inventory is not changed.
94
+ With --add-entries, stdin holds instead a JSON object with an
95
+ `entries` list, a `file_entries` list or both, appended to the
96
+ map at --target: the entries the inventory does not hold (T2 and
97
+ T3) and the file entries. An entry whose export_name and
98
+ source_file the map already holds, or a file entry whose
99
+ file_name it holds, is not added and counts in `duplicates`, so
100
+ an inventory export's entry is never replaced.
101
+
102
+ rules --inventory <file> --language <language> --target <file>
103
+ Write extraction-rules.yaml (the skill, its language, tier and
104
+ extraction mode, and the inventory's extraction_rules) to
105
+ --target. The inventory is not changed.
106
+
107
+ summary --inventory <file>
108
+ Print the inventory's counts without changing it.
109
+
110
+ Inventory shape (the keys this helper reads or writes):
111
+
112
+ {
113
+ "skill_name": "...", "extraction_mode": "...", "tier": "...",
114
+ "files_scanned": N | null,
115
+ "exports": [{"export_name": "...", "source_file": "...", ...}, ...],
116
+ "top_exports": ["name", ...],
117
+ "aggregates": {...}, "counts": {...}, "arms": {...},
118
+ "scripts_inventory": [...], "assets_inventory": [...],
119
+ "intent_mapping": {"scripts": {"intent", "kept", "left_out"}, ...},
120
+ "co_imports": [...], "promoted_docs": [...],
121
+ "authoritative_files_scan": {...} | null,
122
+ "extraction_rules": {"recipe_set", "recipes", "scope", "ast_grep_version"},
123
+ "t2_annotations": [...], "t3_items": [...], "warnings": ["..."]
124
+ }
125
+
126
+ Output (stdout, exit 0), for every subcommand but rules and provenance:
127
+
128
+ {
129
+ "status": "ok",
130
+ "inventory": "<path, / separators>",
131
+ "extraction_mode": "...",
132
+ "counts": {"files_scanned": N | null, "exports": N, "t1": N,
133
+ "t1_low": N, "by_type": {"<export_type>": N, ...},
134
+ "top_exports": N, "co_imports": N, "scripts": N,
135
+ "assets": N, "t2_annotations": N, "t2_past": N,
136
+ "t2_future": N, "functions_enriched": N, "t3_items": N,
137
+ "items": N},
138
+ "added": N, "duplicates": N, "dropped": ["name"], # add
139
+ "patched": N, "unmatched": [...], "ambiguous": [...], # patch
140
+ "set": ["key", ...] # set
141
+ }
142
+
143
+ `items` is exports plus t3_items: what compile can document. t1 and t1_low
144
+ count the exports by their `confidence`, by_type by their `export_type`.
145
+ t2_past and t2_future count the T2 annotations by their `temporal`, and
146
+ functions_enriched the distinct export_name values among them. rules
147
+ prints {"status": "ok", "target": "<path>", "bytes": N}, provenance
148
+ {"status": "ok", "target": "<path>", "entries": N, "unsigned": ["name
149
+ (file)", ...]}, and provenance --add-entries {"status": "ok", "target":
150
+ "<path>", "entries": N, "file_entries": N, "added": N, "duplicates": N},
151
+ the counts of the map after the merge.
152
+
153
+ Exit codes:
154
+ 0 success
155
+ 1 input error, nothing changed: an inventory that is missing, not JSON
156
+ or has no exports list, an --extraction or --detected file that
157
+ cannot be read, a provenance map at --target that --add-entries
158
+ cannot read, a stdin payload of the wrong shape, an unknown key or a
159
+ kept file the inventory does not list, or a write that failed
160
+ 2 usage error (argparse)
161
+ """
162
+
163
+ from __future__ import annotations
164
+
165
+ import argparse
166
+ import json
167
+ import os
168
+ import re
169
+ import sys
170
+ import time
171
+ from pathlib import Path
172
+
173
+ FIELDS = ("exports", "t2_annotations", "t3_items", "warnings")
174
+ MODES = ("source", "docs-only", "component-library")
175
+ TIERS = ("Quick", "Forge", "Forge+", "Deep")
176
+ LISTS = ("top_exports", "co_imports", "promoted_docs", "component_catalog", "scripts_inventory",
177
+ "assets_inventory", "t2_annotations", "t3_items", "warnings")
178
+ PATCHABLE = ("signature", "params", "return_type")
179
+ # set: the key, the JSON types it takes, and whether an object merges into the one there.
180
+ SETTABLE = {
181
+ "top_exports": ((list,), False),
182
+ "co_imports": ((list,), False),
183
+ "promoted_docs": ((list,), False),
184
+ "component_catalog": ((list,), False),
185
+ "authoritative_files_scan": ((dict, type(None)), False),
186
+ "files_scanned": ((int,), False),
187
+ "counts": ((dict,), True),
188
+ "arms": ((dict,), True),
189
+ "intent_mapping": ((dict,), False),
190
+ }
191
+ # intent_mapping kinds and the inventory list each one filters.
192
+ INTENT_LISTS = {"scripts": "scripts_inventory", "assets": "assets_inventory"}
193
+ BY_EYE_DEFAULTS = {"confidence": "T1-low", "extraction_method": "source-read", "ast_node_type": None,
194
+ "ast_recipe": None}
195
+ YAML_KEY_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*")
196
+
197
+
198
+ class InventoryError(Exception):
199
+ """Exit 1."""
200
+
201
+
202
+ def load(path: Path) -> dict:
203
+ try:
204
+ data = json.loads(path.read_text(encoding="utf-8"))
205
+ except (OSError, UnicodeDecodeError, ValueError) as exc:
206
+ raise InventoryError(f"cannot read the inventory {path.as_posix()}: {exc}") from exc
207
+ if not isinstance(data, dict) or not isinstance(data.get("exports"), list):
208
+ raise InventoryError(f"the inventory {path.as_posix()} is not an object with an exports list")
209
+ for key in LISTS:
210
+ if key in data and not isinstance(data[key], list):
211
+ raise InventoryError(f"the inventory's {key} is not a list")
212
+ return data
213
+
214
+
215
+ def _read_json(path: Path, label: str):
216
+ try:
217
+ return json.loads(path.read_text(encoding="utf-8"))
218
+ except (OSError, UnicodeDecodeError, ValueError) as exc:
219
+ raise InventoryError(f"cannot read the {label} {path.as_posix()}: {exc}") from exc
220
+
221
+
222
+ def counts(data: dict) -> dict:
223
+ exports = [e for e in data["exports"] if isinstance(e, dict)]
224
+ notes = [n for n in data.get("t2_annotations") or [] if isinstance(n, dict)]
225
+
226
+ def _length(key: str) -> int:
227
+ value = data.get(key)
228
+ return len(value) if isinstance(value, list) else 0
229
+
230
+ by_type: dict[str, int] = {}
231
+ for export in exports:
232
+ kind = export.get("export_type")
233
+ if isinstance(kind, str) and kind:
234
+ by_type[kind] = by_type.get(kind, 0) + 1
235
+ files = data.get("files_scanned")
236
+ return {
237
+ "files_scanned": files if isinstance(files, int) else None,
238
+ "exports": len(data["exports"]),
239
+ "t1": sum(1 for e in exports if e.get("confidence") == "T1"),
240
+ "t1_low": sum(1 for e in exports if e.get("confidence") == "T1-low"),
241
+ "by_type": dict(sorted(by_type.items())),
242
+ "top_exports": _length("top_exports"),
243
+ "co_imports": _length("co_imports"),
244
+ "scripts": _length("scripts_inventory"),
245
+ "assets": _length("assets_inventory"),
246
+ "t2_annotations": _length("t2_annotations"),
247
+ "t2_past": sum(1 for n in notes if n.get("temporal") == "T2-past"),
248
+ "t2_future": sum(1 for n in notes if n.get("temporal") == "T2-future"),
249
+ "functions_enriched": len({n["export_name"] for n in notes if isinstance(n.get("export_name"), str)}),
250
+ "t3_items": _length("t3_items"),
251
+ "items": len(data["exports"]) + _length("t3_items"),
252
+ }
253
+
254
+
255
+ def runner_warnings(extraction: dict) -> list[str]:
256
+ """The recipe runner's anomalies, one line each, for Gate 2 and the evidence report."""
257
+ lines: list[str] = []
258
+ recipes = [r for r in extraction.get("recipes") or [] if isinstance(r, dict)]
259
+ if extraction.get("truncated"):
260
+ capped = ", ".join(f"`{r.get('id')}`" for r in recipes if r.get("truncated")) or "a recipe"
261
+ lines.append(f"Extraction hit the head cap: {capped} matched more than {extraction.get('head_cap')} "
262
+ "exports, so some exports were read by eye or left out. Narrow `scope.include` in the "
263
+ "brief until no recipe reaches the cap.")
264
+ for error in extraction.get("errors") or []:
265
+ if isinstance(error, dict):
266
+ lines.append(f"ast-grep did not finish on {error.get('files')} files from `{error.get('first_file')}` "
267
+ f"({error.get('detail')}): exports in them may be missing")
268
+ for issue in extraction.get("file_issues") or []:
269
+ if isinstance(issue, dict):
270
+ lines.append(f"`{issue.get('file')}` ({issue.get('issue')}): its exports are read by eye")
271
+ without = extraction.get("files_without_recipes")
272
+ if isinstance(without, dict):
273
+ for ext, number in sorted(without.items()):
274
+ lines.append(f"{number} `{ext}` files in scope are in a language no recipe reads")
275
+ lines.extend(w for w in extraction.get("warnings") or [] if isinstance(w, str) and w)
276
+ return lines
277
+
278
+
279
+ def init(skill: str, mode: str, tier: str, extraction: dict | None = None, detected: dict | None = None) -> dict:
280
+ """A new inventory, seeded from the runner's JSON and the detector's JSON."""
281
+ data: dict = {
282
+ "skill_name": skill, "extraction_mode": mode, "tier": tier, "files_scanned": None,
283
+ "exports": [], "top_exports": [], "aggregates": {}, "counts": {}, "arms": {},
284
+ "scripts_inventory": [], "assets_inventory": [], "intent_mapping": {}, "co_imports": [],
285
+ "promoted_docs": [], "authoritative_files_scan": None, "extraction_rules": {},
286
+ "t2_annotations": [], "t3_items": [], "warnings": [],
287
+ }
288
+ if extraction is not None:
289
+ if not isinstance(extraction, dict):
290
+ raise InventoryError("the --extraction file is not a JSON object")
291
+ diff = extraction.get("entry_point_diff")
292
+ internal = {(i.get("name"), i.get("source_file"))
293
+ for i in (diff.get("internal") or [] if isinstance(diff, dict) else []) if isinstance(i, dict)}
294
+ for record in extraction.get("exports") or []:
295
+ if not isinstance(record, dict):
296
+ continue
297
+ export = dict(record)
298
+ export.setdefault("confidence", "T1")
299
+ export.setdefault("extraction_method", "ast-grep")
300
+ if (export.get("export_name"), export.get("source_file")) in internal:
301
+ export["internal"] = True
302
+ data["exports"].append(export)
303
+ files = extraction.get("files_in_scope")
304
+ data["files_scanned"] = files if isinstance(files, int) else None
305
+ for key in ("aggregates", "counts", "arms"):
306
+ value = extraction.get(key)
307
+ data[key] = dict(value) if isinstance(value, dict) else {}
308
+ ast_grep = extraction.get("ast_grep")
309
+ data["extraction_rules"] = {
310
+ "recipe_set": extraction.get("recipe_set"),
311
+ "recipes": [r.get("id") for r in extraction.get("recipes") or [] if isinstance(r, dict)],
312
+ "scope": extraction.get("scope") if isinstance(extraction.get("scope"), dict) else {},
313
+ "ast_grep_version": ast_grep.get("version") if isinstance(ast_grep, dict) else None,
314
+ }
315
+ data["warnings"] = runner_warnings(extraction)
316
+ if detected is not None:
317
+ if not isinstance(detected, dict):
318
+ raise InventoryError("the --detected file is not a JSON object")
319
+ for key in ("scripts_inventory", "assets_inventory"):
320
+ value = detected.get(key) or []
321
+ if not isinstance(value, list) or not all(isinstance(e, dict) for e in value):
322
+ raise InventoryError(f"the --detected file's {key} is not a list of objects")
323
+ data[key] = [dict(e) for e in value]
324
+ return data
325
+
326
+
327
+ def _same(a, b) -> bool:
328
+ return json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)
329
+
330
+
331
+ def add(data: dict, field: str, entries: list) -> tuple[int, int, list[str]]:
332
+ """Append `entries` to data[field]; return (added, duplicates, dropped export names)."""
333
+ if field not in FIELDS:
334
+ raise InventoryError(f"--field must be one of {', '.join(FIELDS)}")
335
+ kind = str if field == "warnings" else dict
336
+ if not isinstance(entries, list) or not all(isinstance(e, kind) for e in entries):
337
+ raise InventoryError(f"stdin must hold a JSON list of {'strings' if kind is str else 'objects'}")
338
+ if field == "exports":
339
+ if not all(isinstance(e.get("export_name"), str) and e["export_name"] for e in entries):
340
+ raise InventoryError("every exports entry needs an export_name")
341
+ entries = [{**BY_EYE_DEFAULTS, **e} for e in entries]
342
+ target = data.setdefault(field, [])
343
+ known = {e.get("export_name") for e in data["exports"] if isinstance(e, dict)} - {None}
344
+ added = duplicates = 0
345
+ dropped: list[str] = []
346
+ for entry in entries:
347
+ if field == "t3_items" and entry.get("export_name") in known:
348
+ dropped.append(entry["export_name"])
349
+ continue
350
+ if field == "exports":
351
+ key = (entry["export_name"], entry.get("source_file"))
352
+ repeat = any(isinstance(e, dict) and (e.get("export_name"), e.get("source_file")) == key
353
+ for e in target)
354
+ else:
355
+ repeat = any(_same(entry, e) for e in target)
356
+ if repeat:
357
+ duplicates += 1
358
+ continue
359
+ target.append(entry)
360
+ added += 1
361
+ return added, duplicates, dropped
362
+
363
+
364
+ def patch(data: dict, entries: list) -> tuple[int, list[str], list[str]]:
365
+ """Set signature, params and return_type on the exports named; return (patched, unmatched, ambiguous)."""
366
+ if not isinstance(entries, list) or not all(isinstance(e, dict) for e in entries):
367
+ raise InventoryError("stdin must hold a JSON list of objects")
368
+ for entry in entries:
369
+ if not isinstance(entry.get("export_name"), str) or not entry["export_name"]:
370
+ raise InventoryError("every patch entry needs an export_name")
371
+ extra = sorted(set(entry) - {"export_name", "source_file", *PATCHABLE})
372
+ if extra:
373
+ raise InventoryError(f"patch sets only {', '.join(PATCHABLE)}; {entry['export_name']} also has "
374
+ f"{', '.join(extra)}")
375
+ patched = 0
376
+ unmatched: list[str] = []
377
+ ambiguous: list[str] = []
378
+ for entry in entries:
379
+ name, source = entry["export_name"], entry.get("source_file")
380
+ found = [e for e in data["exports"] if isinstance(e, dict) and e.get("export_name") == name
381
+ and (source is None or e.get("source_file") == source)]
382
+ label = name if source is None else f"{name} ({source})"
383
+ if not found:
384
+ unmatched.append(label)
385
+ elif len(found) > 1:
386
+ ambiguous.append(label)
387
+ else:
388
+ found[0].update({k: entry[k] for k in PATCHABLE if k in entry})
389
+ patched += 1
390
+ return patched, unmatched, ambiguous
391
+
392
+
393
+ def set_fields(data: dict, values: dict) -> list[str]:
394
+ """Set the keys of `values` on the inventory (see the module docstring); return the keys set."""
395
+ if not isinstance(values, dict):
396
+ raise InventoryError("stdin must hold a JSON object")
397
+ unknown = sorted(set(values) - set(SETTABLE))
398
+ if unknown:
399
+ raise InventoryError(f"set takes only {', '.join(SETTABLE)}; not {', '.join(unknown)}")
400
+ for key, value in values.items():
401
+ types, _ = SETTABLE[key]
402
+ if not isinstance(value, types) or (key == "files_scanned" and isinstance(value, bool)):
403
+ names = " or ".join("null" if t is type(None) else t.__name__ for t in types)
404
+ raise InventoryError(f"{key} must be {names}")
405
+ mapping = _intent_mapping(data, values["intent_mapping"]) if "intent_mapping" in values else {}
406
+ for key, value in values.items():
407
+ if key == "intent_mapping":
408
+ data["intent_mapping"] = {**(data.get("intent_mapping") or {}), **mapping}
409
+ elif SETTABLE[key][1]:
410
+ data[key] = {**(data.get(key) if isinstance(data.get(key), dict) else {}), **value}
411
+ else:
412
+ data[key] = value
413
+ return list(values)
414
+
415
+
416
+ def _intent_mapping(data: dict, mapping: dict) -> dict:
417
+ """Keep only the kept entries of each list a free-text intent names; return the recorded mapping."""
418
+ unknown = sorted(set(mapping) - set(INTENT_LISTS))
419
+ if unknown:
420
+ raise InventoryError(f"intent_mapping takes only {', '.join(INTENT_LISTS)}; not {', '.join(unknown)}")
421
+ recorded: dict = {}
422
+ filtered: dict[str, list] = {}
423
+ for kind, spec in mapping.items():
424
+ kept_files = spec.get("kept") if isinstance(spec, dict) else None
425
+ if not isinstance(spec, dict) or not isinstance(spec.get("intent"), str) or not isinstance(kept_files, list) \
426
+ or not all(isinstance(f, str) for f in kept_files):
427
+ raise InventoryError(f'intent_mapping.{kind} must be {{"intent": "<text>", "kept": [source_file, ...]}}')
428
+ entries = [e for e in data.get(INTENT_LISTS[kind]) or [] if isinstance(e, dict)]
429
+ listed = {e.get("source_file") for e in entries}
430
+ missing = [f for f in spec["kept"] if f not in listed]
431
+ if missing:
432
+ raise InventoryError(f"{INTENT_LISTS[kind]} lists no {', '.join(missing)}")
433
+ kept = set(spec["kept"])
434
+ filtered[kind] = [e for e in entries if e.get("source_file") in kept]
435
+ earlier = (data.get("intent_mapping") or {}).get(kind)
436
+ left_out = {e.get("source_file") for e in entries if e.get("source_file") not in kept}
437
+ if isinstance(earlier, dict) and earlier.get("intent") == spec["intent"]:
438
+ left_out |= set(earlier.get("left_out") or [])
439
+ recorded[kind] = {"intent": spec["intent"], "kept": sorted(kept), "left_out": sorted(left_out - kept)}
440
+ for kind, entries in filtered.items():
441
+ data[INTENT_LISTS[kind]] = entries
442
+ return recorded
443
+
444
+
445
+ JS_LANGUAGES = frozenset({"typescript", "tsx", "javascript"})
446
+
447
+
448
+ def typed_param(param, language: str | None) -> str | None:
449
+ """One parameter as the provenance map writes it ('userId: string',
450
+ 'options?: TokenOptions', 'retries: int = 3'); a string is kept."""
451
+ if isinstance(param, str):
452
+ return param
453
+ if not isinstance(param, dict):
454
+ return None
455
+ name, type_, default = param.get("name"), param.get("type"), param.get("default")
456
+ if not name:
457
+ return type_ or None
458
+ text = str(name)
459
+ if param.get("optional") and default is None and language in JS_LANGUAGES and not text.startswith("..."):
460
+ text += "?"
461
+ if type_:
462
+ text += f": {type_}"
463
+ if default is not None:
464
+ text += f" = {default}"
465
+ return text
466
+
467
+
468
+ # provenance: the header keys stdin may give, and the default of each (REQUIRED: no default).
469
+ REQUIRED = object()
470
+ MAP_HEADER = {
471
+ "provenance_version": "2.0",
472
+ "skill_name": None, # the inventory's
473
+ "skill_type": "single",
474
+ "source_repo": REQUIRED,
475
+ "source_commit": REQUIRED,
476
+ "source_ref": None,
477
+ "generated_at": None, # now
478
+ }
479
+
480
+
481
+ def _labels(export: dict) -> dict:
482
+ """The labels the tool that found `export` gives its entry (compile.md §6)."""
483
+ if export.get("extraction_method") == "ast-grep":
484
+ return {"confidence": "T1", "extraction_method": "ast-grep", "ast_node_type": export.get("ast_node_type"),
485
+ "signature_source": "T1"}
486
+ return {"confidence": "T1-low", "extraction_method": "source-read", "ast_node_type": None,
487
+ "signature_source": "T1-low"}
488
+
489
+
490
+ def provenance_entries(data: dict) -> tuple[list[dict], list[str]]:
491
+ """(the provenance map entry of every export, the functions among them with no params)."""
492
+ entries: list[dict] = []
493
+ unsigned: list[str] = []
494
+ for export in data.get("exports") or []:
495
+ if not isinstance(export, dict):
496
+ continue
497
+ raw = export.get("params")
498
+ params = None
499
+ if isinstance(raw, list):
500
+ params = [s for s in (typed_param(p, export.get("language")) for p in raw) if s is not None]
501
+ elif export.get("export_type") == "function":
502
+ unsigned.append(f"{export.get('export_name')} ({export.get('source_file')})")
503
+ entries.append({
504
+ "export_name": export.get("export_name"),
505
+ "export_type": export.get("export_type"),
506
+ "params": params,
507
+ "return_type": export.get("return_type") if params is not None else None,
508
+ "source_file": export.get("source_file"),
509
+ "source_line": export.get("source_line"),
510
+ **_labels(export),
511
+ })
512
+ return entries, unsigned
513
+
514
+
515
+ def map_header(data: dict, given) -> dict:
516
+ """The provenance map's header: stdin's keys over the defaults."""
517
+ if not isinstance(given, dict):
518
+ raise InventoryError("stdin must hold the map's header, a JSON object with source_repo and source_commit")
519
+ unknown = sorted(set(given) - set(MAP_HEADER))
520
+ if unknown:
521
+ raise InventoryError(f"the map's header takes only {', '.join(MAP_HEADER)}; not {', '.join(unknown)}")
522
+ missing = [key for key, default in MAP_HEADER.items() if default is REQUIRED and key not in given]
523
+ if missing:
524
+ raise InventoryError(f"the map's header needs {', '.join(missing)} (null when unknown)")
525
+ defaults = {"skill_name": data.get("skill_name"),
526
+ "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
527
+ return {key: given[key] if key in given else defaults.get(key, default) for key, default in MAP_HEADER.items()}
528
+
529
+
530
+ def provenance_map(data: dict, header) -> tuple[dict, list[str]]:
531
+ """(the provenance map of the inventory, the functions with no params)."""
532
+ entries, unsigned = provenance_entries(data)
533
+ return {**map_header(data, header), "entries": entries}, unsigned
534
+
535
+
536
+ def add_map_entries(provenance: dict, payload) -> tuple[int, int]:
537
+ """Append stdin's `entries` and `file_entries` to the map; return (added, duplicates)."""
538
+ if not isinstance(payload, dict) or not payload or set(payload) - {"entries", "file_entries"}:
539
+ raise InventoryError('stdin must hold {"entries": [...], "file_entries": [...]} (either list, or both)')
540
+ keys = {"entries": ("export_name", "source_file"), "file_entries": ("file_name",)}
541
+ added = duplicates = 0
542
+ for field, items in payload.items():
543
+ if not isinstance(items, list) or not all(isinstance(e, dict) for e in items):
544
+ raise InventoryError(f"{field} must be a list of objects")
545
+ need = keys[field][0]
546
+ if not all(isinstance(e.get(need), str) and e[need] for e in items):
547
+ raise InventoryError(f"every {field} item needs a {need}")
548
+ target = provenance.setdefault(field, [])
549
+ if not isinstance(target, list):
550
+ raise InventoryError(f"the map's {field} is not a list")
551
+ held = {tuple(e.get(k) for k in keys[field]) for e in target if isinstance(e, dict)}
552
+ for item in items:
553
+ key = tuple(item.get(k) for k in keys[field])
554
+ if key in held:
555
+ duplicates += 1
556
+ continue
557
+ held.add(key)
558
+ target.append(item)
559
+ added += 1
560
+ return added, duplicates
561
+
562
+
563
+ def rules_yaml(data: dict, language: str) -> str:
564
+ """extraction-rules.yaml: how the skill was extracted, for a later run to reproduce it."""
565
+ head = {"skill_name": data.get("skill_name"), "language": language, "tier": data.get("tier"),
566
+ "extraction_mode": data.get("extraction_mode")}
567
+ rules = data.get("extraction_rules") if isinstance(data.get("extraction_rules"), dict) else {}
568
+ lines = ["# Generated by skf-extraction-inventory.py rules from the extraction inventory."]
569
+ lines += _yaml_lines({**head, **rules})
570
+ return "\n".join(lines) + "\n"
571
+
572
+
573
+ def _yaml_scalar(value) -> str:
574
+ if isinstance(value, dict):
575
+ return "{}"
576
+ if isinstance(value, list):
577
+ return "[]"
578
+ return json.dumps(value, ensure_ascii=False) # JSON scalars are YAML scalars
579
+
580
+
581
+ def _yaml_key(key) -> str:
582
+ text = str(key)
583
+ return text if YAML_KEY_RE.fullmatch(text) else json.dumps(text, ensure_ascii=False)
584
+
585
+
586
+ def _yaml_lines(value, indent: int = 0) -> list[str]:
587
+ pad = " " * indent
588
+ lines: list[str] = []
589
+ items = value.items() if isinstance(value, dict) else ((None, v) for v in value)
590
+ for key, item in items:
591
+ lead = f"{pad}{_yaml_key(key)}:" if isinstance(value, dict) else f"{pad}-"
592
+ if isinstance(item, (dict, list)) and item:
593
+ nested = _yaml_lines(item, indent + 1)
594
+ if isinstance(value, list):
595
+ lines.append(f"{lead} {nested[0].lstrip()}")
596
+ lines.extend(nested[1:])
597
+ else:
598
+ lines.append(lead)
599
+ lines.extend(nested)
600
+ else:
601
+ lines.append(f"{lead} {_yaml_scalar(item)}")
602
+ return lines
603
+
604
+
605
+ def write(path: Path, data: dict | str) -> None:
606
+ """Write `data` (JSON, or text as it is) to `path` through a temporary file and one rename."""
607
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
608
+ try:
609
+ with open(tmp, "w", encoding="utf-8", newline="\n") as fh:
610
+ if isinstance(data, str):
611
+ fh.write(data)
612
+ else:
613
+ json.dump(data, fh, indent=2, ensure_ascii=False)
614
+ fh.write("\n")
615
+ fh.flush()
616
+ os.fsync(fh.fileno())
617
+ os.replace(tmp, path)
618
+ except OSError as exc:
619
+ try:
620
+ tmp.unlink()
621
+ except OSError:
622
+ pass
623
+ raise InventoryError(f"cannot write {path.as_posix()}: {exc}") from exc
624
+
625
+
626
+ def _result(path: Path, data: dict, **extra) -> dict:
627
+ return {"status": "ok", "inventory": path.as_posix(), "extraction_mode": data.get("extraction_mode"),
628
+ "counts": counts(data), **extra}
629
+
630
+
631
+ def _stdin_json():
632
+ raw = sys.stdin.read()
633
+ try:
634
+ return json.loads(raw) if raw.strip() else None
635
+ except ValueError as exc:
636
+ raise InventoryError(f"stdin is not JSON: {exc}") from exc
637
+
638
+
639
+ def _build_parser() -> argparse.ArgumentParser:
640
+ parser = argparse.ArgumentParser(
641
+ prog="skf-extraction-inventory.py",
642
+ description=__doc__,
643
+ formatter_class=argparse.RawDescriptionHelpFormatter,
644
+ )
645
+ sub = parser.add_subparsers(dest="command", required=True)
646
+ inventory_help = "the extraction inventory JSON"
647
+ p_init = sub.add_parser("init", help="write a new inventory from the runner's and the detector's JSON")
648
+ p_init.add_argument("--inventory", required=True, type=Path, help=inventory_help)
649
+ p_init.add_argument("--skill", required=True, help="the skill name")
650
+ p_init.add_argument("--mode", required=True, choices=MODES, help="the extraction mode")
651
+ p_init.add_argument("--tier", required=True, choices=TIERS, help="the forge tier")
652
+ p_init.add_argument("--extraction", type=Path, default=None,
653
+ help="the recipe runner's JSON (skf-extract-public-api.py --mode full -o)")
654
+ p_init.add_argument("--detected", type=Path, default=None,
655
+ help="the script and asset detector's JSON (skf-detect-scripts-assets.py detect)")
656
+ p_add = sub.add_parser("add", help="append a JSON list from stdin to one of the inventory's lists")
657
+ p_add.add_argument("--inventory", required=True, type=Path, help=inventory_help)
658
+ p_add.add_argument("--field", required=True, choices=FIELDS, help="the list to append to")
659
+ p_patch = sub.add_parser("patch", help="set signature, params and return_type on exports from stdin")
660
+ p_patch.add_argument("--inventory", required=True, type=Path, help=inventory_help)
661
+ p_set = sub.add_parser("set", help="set the keys of a JSON object from stdin")
662
+ p_set.add_argument("--inventory", required=True, type=Path, help=inventory_help)
663
+ p_rules = sub.add_parser("rules", help="write extraction-rules.yaml from the inventory")
664
+ p_rules.add_argument("--inventory", required=True, type=Path, help=inventory_help)
665
+ p_rules.add_argument("--language", required=True, help="the brief's language")
666
+ p_rules.add_argument("--target", required=True, type=Path, help="the extraction-rules.yaml to write")
667
+ p_prov = sub.add_parser("provenance", help="write the provenance map from the inventory, the header on stdin")
668
+ p_prov.add_argument("--inventory", required=True, type=Path, help=inventory_help)
669
+ p_prov.add_argument("--target", required=True, type=Path, help="the provenance-map.json to write")
670
+ p_prov.add_argument("--add-entries", action="store_true",
671
+ help="append stdin's entries and file_entries to the map at --target instead")
672
+ p_summary = sub.add_parser("summary", help="print the inventory's counts")
673
+ p_summary.add_argument("--inventory", required=True, type=Path, help=inventory_help)
674
+ return parser
675
+
676
+
677
+ def main(argv: list[str] | None = None) -> int:
678
+ args = _build_parser().parse_args(argv)
679
+ path = args.inventory
680
+ try:
681
+ if args.command == "init":
682
+ extraction = _read_json(args.extraction, "runner JSON") if args.extraction else None
683
+ detected = _read_json(args.detected, "detector JSON") if args.detected else None
684
+ data = init(args.skill, args.mode, args.tier, extraction, detected)
685
+ write(path, data)
686
+ result = _result(path, data)
687
+ elif args.command == "provenance" and args.add_entries:
688
+ load(path)
689
+ provenance = _read_json(args.target, "provenance map")
690
+ if not isinstance(provenance, dict) or not isinstance(provenance.get("entries"), list):
691
+ raise InventoryError(f"the provenance map {args.target.as_posix()} has no entries list: "
692
+ "run provenance without --add-entries first")
693
+ added, duplicates = add_map_entries(provenance, _stdin_json())
694
+ write(args.target, provenance)
695
+ result = {"status": "ok", "target": args.target.as_posix(), "entries": len(provenance["entries"]),
696
+ "file_entries": len(provenance.get("file_entries") or []), "added": added,
697
+ "duplicates": duplicates}
698
+ elif args.command == "provenance":
699
+ provenance, unsigned = provenance_map(load(path), _stdin_json())
700
+ write(args.target, provenance)
701
+ result = {"status": "ok", "target": args.target.as_posix(), "entries": len(provenance["entries"]),
702
+ "unsigned": unsigned}
703
+ elif args.command == "rules":
704
+ text = rules_yaml(load(path), args.language)
705
+ write(args.target, text)
706
+ result = {"status": "ok", "target": args.target.as_posix(), "bytes": len(text.encode("utf-8"))}
707
+ else:
708
+ data = load(path)
709
+ if args.command == "summary":
710
+ result = _result(path, data)
711
+ elif args.command == "add":
712
+ payload = _stdin_json()
713
+ added, duplicates, dropped = add(data, args.field, [] if payload is None else payload)
714
+ write(path, data)
715
+ result = _result(path, data, added=added, duplicates=duplicates, dropped=dropped)
716
+ elif args.command == "patch":
717
+ payload = _stdin_json()
718
+ patched, unmatched, ambiguous = patch(data, [] if payload is None else payload)
719
+ write(path, data)
720
+ result = _result(path, data, patched=patched, unmatched=unmatched, ambiguous=ambiguous)
721
+ else:
722
+ payload = _stdin_json()
723
+ keys = set_fields(data, {} if payload is None else payload)
724
+ write(path, data)
725
+ result = _result(path, data, set=keys)
726
+ except InventoryError as exc:
727
+ print(json.dumps({"status": "error", "message": str(exc)}), file=sys.stderr)
728
+ return 1
729
+ print(json.dumps(result, ensure_ascii=False))
730
+ return 0
731
+
732
+
733
+ def _force_utf8(*streams) -> None:
734
+ """Reconfigure stdin and the JSON streams to UTF-8 (a Windows console uses cp1252)."""
735
+ for stream in streams:
736
+ if hasattr(stream, "reconfigure"):
737
+ errors = getattr(stream, "errors", None)
738
+ if errors is None:
739
+ stream.reconfigure(encoding="utf-8")
740
+ else:
741
+ stream.reconfigure(encoding="utf-8", errors=errors)
742
+
743
+
744
+ if __name__ == "__main__":
745
+ _force_utf8(sys.stdin, sys.stdout, sys.stderr)
746
+ sys.exit(main())