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,539 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Extraction Snapshot: audit-skill's current export inventory, built by script.
6
+
7
+ audit-skill's step 2 (re-index.md) re-extracts the files the provenance map
8
+ covers and writes `extraction-snapshot.json`, which step 3 diffs against the
9
+ map. The recipe runner (skf-extract-public-api.py --mode full) does the
10
+ extraction; this helper gives it the files to read and turns its JSON, with
11
+ the exports the step read by eye, into the snapshot, so no step opens the
12
+ runner's JSON, copies hundreds of exports by hand or decides by eye whether
13
+ every file was read.
14
+
15
+ CLI:
16
+
17
+ uv run skf-extraction-snapshot.py scan-list <provenance-map> -o <files.json>
18
+ uv run skf-extraction-snapshot.py build --source-root <dir> --tier <tier> \\
19
+ --date <timestamp> [--source-path <recorded root>] \\
20
+ [--provenance-map <map>] [--extraction <runner.json>] \\
21
+ [--details <by-eye.json>]... -o <extraction-snapshot.json>
22
+ uv run skf-extraction-snapshot.py relocate <extraction-snapshot.json> \\
23
+ --diff <structural-diff.json> --extraction <runner.json>
24
+
25
+ scan-list
26
+
27
+ Writes the bounded scan list, the files the provenance map covers (the
28
+ union of `entries[].source_file` and `file_entries[].source_file`, sorted,
29
+ with forward slashes: skf-load-provenance.py's `bounded_scan_files`), as a
30
+ JSON list of paths, the form the runner's --files-from reads. Prints
31
+ {"status": "ok", "output": <file>, "files": N}.
32
+
33
+ build
34
+
35
+ The files in scope are the bounded scan list with --provenance-map (every
36
+ audit passes it), else every file the runner or the details name. Each
37
+ gets one status, in this order of precedence:
38
+
39
+ missing not a file under --source-root
40
+ hash-tracked only `file_entries[]` names it (a script, an asset or a
41
+ promoted doc): step 3 compares its content hash, so
42
+ nothing reads it for exports
43
+ read-by-eye the details name it in `files` (the step read it)
44
+ parse-failed the runner listed it in `file_issues` (a syntax error,
45
+ not UTF-8, unreadable, or a language no recipe reads):
46
+ read it by eye
47
+ unread no runner result, a runner that could not run
48
+ (`no-ast-grep`), or an incomplete run that names it as
49
+ unread (`errors[].unread`) or found no export in it
50
+ extracted the runner read it
51
+
52
+ `complete` is true when no file is `parse-failed` or `unread`: step 2
53
+ reads those by eye, adds them to the details and builds again. A file
54
+ carries `fallback: true` when the runner ran and left it `parse-failed`
55
+ or `unread`: read by eye after an ast-grep failure.
56
+
57
+ Exports: the runner's exports of the files in scope (each T1, with the
58
+ declaration on one line as `signature`), then each export of the details
59
+ (`name` or `export_name`, `file` or `source_file`, `line` or
60
+ `source_line`, `type` or `export_type`, `signature`; T1-low and
61
+ `source-read` unless it says otherwise). A details export with the name
62
+ and file of a runner export fills only the fields that one lacks; any
63
+ other is added. An export of a file outside the files in scope, or of a
64
+ `hash-tracked` one (the map records no export there, so it would read
65
+ as added), is left out, with a warning. With --provenance-map, each export of a file the
66
+ map gives a library (`source_library_by_file`) carries it as
67
+ `source_library`, which step 3's `--group-by source_library` diff reads.
68
+
69
+ Details file: {"files": [paths read by eye], "exports": [export objects]}
70
+ (either key may be absent). Give --details once per file: one per worker.
71
+
72
+ Output (-o), the snapshot step 3 reads:
73
+
74
+ {
75
+ "extraction_date": "<--date>",
76
+ "confidence_tier": "<--tier>",
77
+ "source_root": "<--source-path, else --source-root>",
78
+ "bounded_scan": true | false,
79
+ "bounded_scan_source": "provenance-map" | "source-tree-fallback",
80
+ "runner_status": "ok" | "incomplete" | "no-ast-grep" | null,
81
+ "runner_errors": [the runner's errors[]],
82
+ "files_scanned": N,
83
+ "complete": bool,
84
+ "files": [{"file", "status", "issue": <the runner's issue> | null,
85
+ "fallback": bool, "exports": N}, ...],
86
+ "files_by_status": {"extracted": N, "read-by-eye": N,
87
+ "hash-tracked": N, "missing": N,
88
+ "parse-failed": N, "unread": N},
89
+ "ast_fallback_files": ["<file>", ...],
90
+ "extraction_gaps": [{"name", "file", "line", "entry"}, ...],
91
+ "truncated": bool, # a recipe hit the runner's head cap
92
+ "cap_hits": ["<recipe id>", ...],
93
+ "outside_scope": [{"path": "<file>", "names": [...],
94
+ "entries": [...]}, ...],
95
+ "counts": {"exports": N, "by_type": {"<type>": N, ...},
96
+ "t1": N, "t1_low": N},
97
+ "exports": [{"name", "type", "signature", "file", "line",
98
+ "confidence", "extraction_method", "ast_node_type",
99
+ "source_library"?}, ...],
100
+ "relocations": [{"name", "file", "line"}, ...], # relocate's
101
+ "warnings": ["..."]
102
+ }
103
+
104
+ `ast_fallback_files` names the files read after an ast-grep failure (the
105
+ `fallback` ones) when the runner ran; when it did not, it is
106
+ ["all files (ast-grep unavailable)"], and [] at the Quick tier.
107
+ `extraction_gaps` lists the names a package entry point exports that no
108
+ recipe found (the runner's `entry_point_diff.extraction_gaps`), less the
109
+ ones an export of the snapshot now holds: read them by eye.
110
+ `outside_scope` groups the runner's `entry_point_diff.outside_scope` by
111
+ the file that defines each name: public API a package entry point exports
112
+ from a file the skill does not cover, which the drift report lists under
113
+ Out-of-Scope New Public API for update-skill's scope reconciliation.
114
+
115
+ Prints {"status": "ok", "output", "runner_status", "complete",
116
+ "to_read": [{"file", "status", "issue"}, ...] (the `parse-failed` and
117
+ `unread` files), "extraction_gaps", "ast_fallback_files",
118
+ "files_by_status", "counts"} on stdout: what step 2 acts on, so it never
119
+ opens the runner's JSON.
120
+
121
+ relocate
122
+
123
+ Step 3's relocation check: --extraction is the runner's JSON over the
124
+ files that hold a name the diff reports removed, outside the bounded scan
125
+ list. Each export the runner found there under a name of the diff's
126
+ `removed[]`, in a file outside the snapshot's files in scope, joins the
127
+ snapshot's exports (T1, with the removed entry's `source_library` when it
128
+ has one) and `relocations`; the counts are recounted and the snapshot is
129
+ written back in place. Prints {"status": "ok", "output", "added": N,
130
+ "names": [...]}; added 0 changes nothing.
131
+
132
+ Exit codes: 0 the file was written (or relocate found nothing to add); 2
133
+ an input that cannot be read or parsed, or an output that cannot be
134
+ written ({"status": "error", "error"} on stdout), or a usage error
135
+ (argparse, on stderr).
136
+ """
137
+
138
+ from __future__ import annotations
139
+
140
+ import argparse
141
+ import importlib.util
142
+ import json
143
+ import sys
144
+ from collections import Counter
145
+ from pathlib import Path
146
+
147
+ STATUSES = ("extracted", "read-by-eye", "hash-tracked", "missing", "parse-failed", "unread")
148
+ TO_READ = ("parse-failed", "unread")
149
+ RUNNER_READ = frozenset({"ok", "incomplete"})
150
+ AST_GREP_UNAVAILABLE = "all files (ast-grep unavailable)"
151
+ EXIT_ERROR = 2
152
+
153
+
154
+ class SnapshotError(Exception):
155
+ """An input or output the helper cannot use (exit 2)."""
156
+
157
+
158
+ _PROVENANCE = None
159
+
160
+
161
+ def _provenance():
162
+ """skf-load-provenance.py, installed beside this script, loaded once."""
163
+ global _PROVENANCE
164
+ if _PROVENANCE is None:
165
+ path = Path(__file__).resolve().parent / "skf-load-provenance.py"
166
+ spec = importlib.util.spec_from_file_location("skf_load_provenance", path)
167
+ if spec is None or spec.loader is None:
168
+ raise SnapshotError(f"cannot load skf-load-provenance.py beside {Path(__file__).name}")
169
+ module = importlib.util.module_from_spec(spec)
170
+ try:
171
+ spec.loader.exec_module(module)
172
+ except (OSError, ImportError, SyntaxError) as exc:
173
+ raise SnapshotError(f"cannot load skf-load-provenance.py: {exc}") from exc
174
+ _PROVENANCE = module
175
+ return _PROVENANCE
176
+
177
+
178
+ def _posix(path: str) -> str:
179
+ """A path as the scan list writes it: forward slashes, no leading `./`."""
180
+ rel = path.strip().replace("\\", "/")
181
+ while rel.startswith("./"):
182
+ rel = rel[2:]
183
+ return rel
184
+
185
+
186
+ def _read_json(path: Path, what: str) -> object:
187
+ try:
188
+ return json.loads(path.read_text(encoding="utf-8"))
189
+ except OSError as exc:
190
+ raise SnapshotError(f"cannot read {what} {path}: {exc.strerror or exc}") from exc
191
+ except (UnicodeDecodeError, ValueError) as exc:
192
+ raise SnapshotError(f"{what} {path} is not JSON: {exc}") from exc
193
+
194
+
195
+ def _read_object(path: Path, what: str) -> dict:
196
+ data = _read_json(path, what)
197
+ if not isinstance(data, dict):
198
+ raise SnapshotError(f"{what} {path} must be a JSON object")
199
+ return data
200
+
201
+
202
+ def _write(path: Path, data: object) -> None:
203
+ try:
204
+ path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
205
+ except OSError as exc:
206
+ raise SnapshotError(f"cannot write {path}: {exc.strerror or exc}") from exc
207
+
208
+
209
+ def _first(entry: dict, *keys: str) -> object:
210
+ for key in keys:
211
+ if entry.get(key) is not None:
212
+ return entry[key]
213
+ return None
214
+
215
+
216
+ # --------------------------------------------------------------------------
217
+ # Building the snapshot
218
+ # --------------------------------------------------------------------------
219
+
220
+
221
+ def _runner_export(export: dict) -> dict:
222
+ return {
223
+ "name": export.get("export_name"),
224
+ "type": export.get("export_type"),
225
+ "signature": _first(export, "signature", "signature_line"),
226
+ "file": _posix(str(export.get("source_file") or "")),
227
+ "line": export.get("source_line"),
228
+ "confidence": export.get("confidence") or "T1",
229
+ "extraction_method": export.get("extraction_method") or "ast-grep",
230
+ "ast_node_type": export.get("ast_node_type"),
231
+ }
232
+
233
+
234
+ def _details_export(export: dict) -> dict | None:
235
+ name, file = _first(export, "name", "export_name"), _first(export, "file", "source_file")
236
+ if not isinstance(name, str) or not name.strip() or not isinstance(file, str) or not file.strip():
237
+ return None
238
+ return {
239
+ "name": name.strip(),
240
+ "type": _first(export, "type", "export_type"),
241
+ "signature": export.get("signature"),
242
+ "file": _posix(file),
243
+ "line": _first(export, "line", "source_line"),
244
+ "confidence": export.get("confidence") or "T1-low",
245
+ "extraction_method": export.get("extraction_method") or "source-read",
246
+ "ast_node_type": export.get("ast_node_type"),
247
+ }
248
+
249
+
250
+ def _details(path: Path) -> tuple[list[str], list[dict]]:
251
+ data = _read_object(path, "details file")
252
+ files, exports = data.get("files", []), data.get("exports", [])
253
+ if not isinstance(files, list) or not all(isinstance(f, str) for f in files):
254
+ raise SnapshotError(f"details file {path}: `files` must be a list of paths")
255
+ if not isinstance(exports, list) or not all(isinstance(e, dict) for e in exports):
256
+ raise SnapshotError(f"details file {path}: `exports` must be a list of objects")
257
+ return [_posix(f) for f in files], exports
258
+
259
+
260
+ def _entry_point_diff(runner: dict) -> dict:
261
+ diff = runner.get("entry_point_diff")
262
+ return diff if isinstance(diff, dict) else {}
263
+
264
+
265
+ def _outside_scope(runner: dict) -> list[dict]:
266
+ grouped: dict[str, dict] = {}
267
+ for item in _entry_point_diff(runner).get("outside_scope") or []:
268
+ if not isinstance(item, dict) or not isinstance(item.get("file"), str) or not item.get("name"):
269
+ continue
270
+ group = grouped.setdefault(_posix(item["file"]), {"names": [], "entries": []})
271
+ if item["name"] not in group["names"]:
272
+ group["names"].append(item["name"])
273
+ entry = item.get("entry")
274
+ if isinstance(entry, str) and entry not in group["entries"]:
275
+ group["entries"].append(entry)
276
+ return [{"path": path, "names": sorted(g["names"]), "entries": sorted(g["entries"])}
277
+ for path, g in sorted(grouped.items())]
278
+
279
+
280
+ def _extraction_gaps(runner: dict, exports: list[dict]) -> list[dict]:
281
+ """The runner's extraction gaps no export of the snapshot holds yet."""
282
+ held = {(e["name"], e["file"]) for e in exports}
283
+ names = {e["name"] for e in exports}
284
+ gaps = []
285
+ for item in _entry_point_diff(runner).get("extraction_gaps") or []:
286
+ if not isinstance(item, dict) or not isinstance(item.get("name"), str) or not item["name"]:
287
+ continue
288
+ file = _posix(item["file"]) if isinstance(item.get("file"), str) else None
289
+ if (item["name"], file) in held or (file is None and item["name"] in names):
290
+ continue
291
+ gap = {"name": item["name"], "file": file, "line": item.get("line"), "entry": item.get("entry")}
292
+ if gap not in gaps:
293
+ gaps.append(gap)
294
+ return gaps
295
+
296
+
297
+ def _ordered(exports) -> list[dict]:
298
+ return sorted(exports, key=lambda e: (e["file"], e["line"] if isinstance(e["line"], int) else 0, e["name"]))
299
+
300
+
301
+ def _counts(exports: list[dict]) -> dict:
302
+ by_type = Counter(str(e["type"]) for e in exports if e.get("type") is not None)
303
+ t1 = sum(1 for e in exports if e.get("confidence") == "T1")
304
+ return {"exports": len(exports), "by_type": dict(sorted(by_type.items())), "t1": t1,
305
+ "t1_low": len(exports) - t1}
306
+
307
+
308
+ def build(source_root: Path, tier: str, date: str, provenance: dict | None, runner: dict | None,
309
+ details_files: list[str], details_exports: list[dict], root_text: str | None = None) -> dict:
310
+ """The snapshot (see the module docstring)."""
311
+ warnings: list[str] = []
312
+ runner = runner or {}
313
+ runner_status = runner.get("status") if runner else None
314
+ ran = runner_status in RUNNER_READ
315
+ issues = {_posix(i["file"]): i.get("issue") for i in runner.get("file_issues") or []
316
+ if isinstance(i, dict) and isinstance(i.get("file"), str)}
317
+ unread = {_posix(f) for e in runner.get("errors") or [] if isinstance(e, dict)
318
+ for f in e.get("unread") or [] if isinstance(f, str)}
319
+ runner_exports = [_runner_export(e) for e in runner.get("exports") or []
320
+ if isinstance(e, dict) and e.get("export_name")] if ran else []
321
+ extra = [x for x in (_details_export(e) for e in details_exports) if x is not None]
322
+ if len(extra) < len(details_exports):
323
+ warnings.append(f"{len(details_exports) - len(extra)} details export(s) name no export or no file: "
324
+ "left out")
325
+
326
+ if provenance is not None:
327
+ scope = list(_provenance().bounded_scan_files(provenance))
328
+ # the files a code entry names; any other is a file_entries[] row
329
+ code_files = set(_provenance().bounded_scan_files({"entries": provenance.get("entries")}))
330
+ libraries = _provenance().source_library_by_file(provenance)
331
+ else:
332
+ named = [e["file"] for e in runner_exports + extra] + list(issues) + list(unread) + details_files
333
+ scope = sorted(set(f for f in named if f))
334
+ code_files = set(scope)
335
+ libraries = {}
336
+ in_scope = set(scope)
337
+
338
+ exports: dict[tuple[str, str], dict] = {}
339
+ hashed = Counter()
340
+ for record in runner_exports + extra:
341
+ if record["file"] not in in_scope:
342
+ warnings.append(f"{record['name']} in {record['file']}: the file is outside the files in scope, "
343
+ "left out")
344
+ continue
345
+ if record["file"] not in code_files:
346
+ hashed[record["file"]] += 1
347
+ continue
348
+ key = (record["name"], record["file"])
349
+ held = exports.get(key)
350
+ if held is None:
351
+ exports[key] = record
352
+ else:
353
+ for field, value in record.items():
354
+ if held.get(field) is None and value is not None:
355
+ held[field] = value
356
+ warnings += [f"{count} export(s) in {file}: only file_entries[] names the file, which step 3 compares "
357
+ "by hash, left out" for file, count in sorted(hashed.items())]
358
+ per_file = Counter(file for _, file in exports)
359
+
360
+ files = []
361
+ read_by_eye = set(details_files)
362
+ for rel in scope:
363
+ issue = issues.get(rel)
364
+ if not (source_root / rel).is_file():
365
+ runner_said = "missing"
366
+ elif rel not in code_files:
367
+ runner_said = "hash-tracked"
368
+ elif ran and issue is not None and issue != "missing":
369
+ runner_said = "parse-failed"
370
+ elif not ran or (runner_status == "incomplete" and (rel in unread or not per_file[rel])):
371
+ runner_said = "unread"
372
+ else:
373
+ runner_said = "extracted"
374
+ by_eye = rel in read_by_eye and runner_said not in ("missing", "hash-tracked")
375
+ status = "read-by-eye" if by_eye else runner_said
376
+ files.append({"file": rel, "status": status,
377
+ "issue": issue if runner_said in ("parse-failed", "hash-tracked") else None,
378
+ "fallback": ran and runner_said in TO_READ, "exports": per_file[rel]})
379
+
380
+ ordered = _ordered(exports.values())
381
+ for record in ordered:
382
+ library = libraries.get(record["file"])
383
+ if library is not None:
384
+ record["source_library"] = library
385
+ by_status = Counter(f["status"] for f in files)
386
+ recipes = [r for r in runner.get("recipes") or [] if isinstance(r, dict)]
387
+ cap_hits = [r.get("id") for r in recipes if r.get("truncated")]
388
+ if ran:
389
+ fallback_files = [f["file"] for f in files if f["fallback"]]
390
+ else:
391
+ fallback_files = [] if tier.strip().lower() == "quick" else [AST_GREP_UNAVAILABLE]
392
+ return {
393
+ "extraction_date": date,
394
+ "confidence_tier": tier,
395
+ "source_root": root_text if root_text is not None else str(source_root),
396
+ "bounded_scan": provenance is not None,
397
+ "bounded_scan_source": "provenance-map" if provenance is not None else "source-tree-fallback",
398
+ "runner_status": runner_status,
399
+ "runner_errors": [e for e in runner.get("errors") or [] if isinstance(e, dict)],
400
+ "files_scanned": len(scope),
401
+ "complete": not any(by_status[status] for status in TO_READ),
402
+ "files": files,
403
+ "files_by_status": {status: by_status[status] for status in STATUSES},
404
+ "ast_fallback_files": fallback_files,
405
+ "extraction_gaps": _extraction_gaps(runner, ordered) if ran else [],
406
+ "truncated": bool(runner.get("truncated")) or bool(cap_hits),
407
+ "cap_hits": cap_hits,
408
+ "outside_scope": _outside_scope(runner) if ran else [],
409
+ "counts": _counts(ordered),
410
+ "exports": ordered,
411
+ "relocations": [],
412
+ "warnings": warnings,
413
+ }
414
+
415
+
416
+ def relocate(snapshot: dict, diff: dict, runner: dict) -> list[dict]:
417
+ """Add to `snapshot` each export `runner` found under a removed name of
418
+ `diff` in a file outside the snapshot's scope; the exports added."""
419
+ removed: dict[str, dict] = {}
420
+ for item in diff.get("removed") or []:
421
+ if isinstance(item, dict) and isinstance(item.get("name"), str):
422
+ removed.setdefault(item["name"], item)
423
+ exports = snapshot.get("exports")
424
+ if not isinstance(exports, list) or not all(isinstance(e, dict) for e in exports):
425
+ raise SnapshotError("the snapshot's `exports` must be a list of objects")
426
+ scope = {f.get("file") for f in snapshot.get("files") or [] if isinstance(f, dict)}
427
+ held = {(e.get("name"), e.get("file")) for e in exports}
428
+ added = []
429
+ if runner.get("status") in RUNNER_READ:
430
+ for export in runner.get("exports") or []:
431
+ if not isinstance(export, dict) or export.get("export_name") not in removed:
432
+ continue
433
+ record = _runner_export(export)
434
+ if record["file"] in scope or (record["name"], record["file"]) in held:
435
+ continue
436
+ library = removed[record["name"]].get("source_library")
437
+ if isinstance(library, str):
438
+ record["source_library"] = library
439
+ held.add((record["name"], record["file"]))
440
+ added.append(record)
441
+ if added:
442
+ snapshot["exports"] = _ordered(exports + added)
443
+ snapshot["counts"] = _counts(snapshot["exports"])
444
+ snapshot["relocations"] = list(snapshot.get("relocations") or []) + [
445
+ {"name": e["name"], "file": e["file"], "line": e["line"]} for e in added]
446
+ return added
447
+
448
+
449
+ # --------------------------------------------------------------------------
450
+ # CLI
451
+ # --------------------------------------------------------------------------
452
+
453
+
454
+ def _cmd_scan_list(args: argparse.Namespace) -> dict:
455
+ provenance = _read_object(Path(args.provenance_map), "provenance map")
456
+ files = list(_provenance().bounded_scan_files(provenance))
457
+ _write(Path(args.output), files)
458
+ return {"status": "ok", "output": args.output, "files": len(files)}
459
+
460
+
461
+ def _cmd_build(args: argparse.Namespace) -> dict:
462
+ root = Path(args.source_root)
463
+ if not root.is_dir():
464
+ raise SnapshotError(f"source root not found: {args.source_root}")
465
+ provenance = _read_object(Path(args.provenance_map), "provenance map") if args.provenance_map else None
466
+ runner = _read_object(Path(args.extraction), "runner result") if args.extraction else None
467
+ details_files: list[str] = []
468
+ details_exports: list[dict] = []
469
+ for path in args.details or []:
470
+ files, exports = _details(Path(path))
471
+ details_files += files
472
+ details_exports += exports
473
+ snapshot = build(root, args.tier, args.date, provenance, runner, details_files, details_exports,
474
+ root_text=args.source_path or args.source_root)
475
+ _write(Path(args.output), snapshot)
476
+ return {"status": "ok", "output": args.output, "runner_status": snapshot["runner_status"],
477
+ "complete": snapshot["complete"],
478
+ "to_read": [{"file": f["file"], "status": f["status"], "issue": f["issue"]}
479
+ for f in snapshot["files"] if f["status"] in TO_READ],
480
+ "extraction_gaps": snapshot["extraction_gaps"], "ast_fallback_files": snapshot["ast_fallback_files"],
481
+ "files_by_status": snapshot["files_by_status"], "counts": snapshot["counts"]}
482
+
483
+
484
+ def _cmd_relocate(args: argparse.Namespace) -> dict:
485
+ path = Path(args.snapshot)
486
+ snapshot = _read_object(path, "snapshot")
487
+ added = relocate(snapshot, _read_object(Path(args.diff), "structural diff"),
488
+ _read_object(Path(args.extraction), "runner result"))
489
+ if added:
490
+ _write(path, snapshot)
491
+ return {"status": "ok", "output": args.snapshot, "added": len(added),
492
+ "names": sorted({e["name"] for e in added})}
493
+
494
+
495
+ def _build_parser() -> argparse.ArgumentParser:
496
+ parser = argparse.ArgumentParser(
497
+ prog="skf-extraction-snapshot",
498
+ description=__doc__,
499
+ formatter_class=argparse.RawDescriptionHelpFormatter,
500
+ )
501
+ sub = parser.add_subparsers(dest="cmd", required=True)
502
+ scan = sub.add_parser("scan-list", help="write the files the provenance map covers as a JSON list")
503
+ scan.add_argument("provenance_map", help="path to provenance-map.json")
504
+ scan.add_argument("-o", "--output", required=True, help="the JSON list to write")
505
+ scan.set_defaults(func=_cmd_scan_list)
506
+ snap = sub.add_parser("build", help="build extraction-snapshot.json")
507
+ snap.add_argument("--source-root", required=True, help="the source tree the runner read")
508
+ snap.add_argument("--source-path",
509
+ help="the source root the provenance map records, written as the snapshot's source_root "
510
+ "(default: --source-root)")
511
+ snap.add_argument("--tier", required=True, help="the forge tier the re-index ran at")
512
+ snap.add_argument("--date", required=True, help="the run's timestamp, recorded as extraction_date")
513
+ snap.add_argument("--provenance-map", help="the provenance map: its files are the files in scope")
514
+ snap.add_argument("--extraction", help="the JSON skf-extract-public-api.py --mode full wrote")
515
+ snap.add_argument("--details", action="append",
516
+ help="the exports read by eye: {\"files\": [...], \"exports\": [...]}; repeat per file")
517
+ snap.add_argument("-o", "--output", required=True, help="the snapshot to write")
518
+ snap.set_defaults(func=_cmd_build)
519
+ move = sub.add_parser("relocate", help="add the exports the runner found under removed names elsewhere")
520
+ move.add_argument("snapshot", help="extraction-snapshot.json, rewritten in place")
521
+ move.add_argument("--diff", required=True, help="the structural diff whose removed[] names to look for")
522
+ move.add_argument("--extraction", required=True, help="the runner's JSON over the candidate files")
523
+ move.set_defaults(func=_cmd_relocate)
524
+ return parser
525
+
526
+
527
+ def main(argv: list[str] | None = None) -> int:
528
+ args = _build_parser().parse_args(argv)
529
+ try:
530
+ result = args.func(args)
531
+ except SnapshotError as exc:
532
+ print(json.dumps({"status": "error", "error": str(exc)}))
533
+ return EXIT_ERROR
534
+ print(json.dumps(result))
535
+ return 0
536
+
537
+
538
+ if __name__ == "__main__":
539
+ sys.exit(main())