bmad-module-skill-forge 2.2.0 → 4.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 (437) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +143 -18
  18. package/docs/verifying-a-skill.md +87 -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 +3 -3
  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 +31 -59
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +233 -64
  64. package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
  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 +772 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
  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 +21 -3
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +5247 -96
  86. package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +664 -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 +469 -205
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +700 -0
  94. package/src/shared/scripts/skf-hash-content.py +180 -20
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +161 -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 +143 -20
  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 +176 -28
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
  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 +155 -14
  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 +787 -121
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +120 -67
  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 +23 -2
  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 +3945 -152
  138. package/src/shared/scripts/skf-write-skill-brief.py +406 -25
  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 +28 -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/health-check.md +2 -0
  168. package/src/skf-audit-skill/references/init.md +149 -210
  169. package/src/skf-audit-skill/references/re-index.md +103 -110
  170. package/src/skf-audit-skill/references/report.md +96 -33
  171. package/src/skf-audit-skill/references/semantic-diff.md +34 -25
  172. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  173. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  174. package/src/skf-audit-skill/references/structural-diff.md +80 -90
  175. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  176. package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
  177. package/src/skf-brief-skill/SKILL.md +37 -26
  178. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  179. package/src/skf-brief-skill/assets/scope-templates.md +18 -25
  180. package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
  181. package/src/skf-brief-skill/customize.toml +48 -20
  182. package/src/skf-brief-skill/references/analyze-target.md +144 -127
  183. package/src/skf-brief-skill/references/confirm-brief.md +11 -9
  184. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  185. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  186. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  187. package/src/skf-brief-skill/references/headless-args.md +84 -16
  188. package/src/skf-brief-skill/references/health-check.md +1 -1
  189. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  190. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  191. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  192. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  193. package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
  194. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  195. package/src/skf-brief-skill/references/version-resolution.md +7 -10
  196. package/src/skf-brief-skill/references/write-brief.md +66 -57
  197. package/src/skf-campaign/SKILL.md +31 -34
  198. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  199. package/src/skf-campaign/customize.toml +47 -16
  200. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  201. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  202. package/src/skf-campaign/references/health-check.md +7 -16
  203. package/src/skf-campaign/references/step-01-setup.md +46 -84
  204. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  205. package/src/skf-campaign/references/step-03-pins.md +26 -18
  206. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  207. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  208. package/src/skf-campaign/references/step-06-batch.md +41 -34
  209. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  210. package/src/skf-campaign/references/step-08-verify.md +28 -29
  211. package/src/skf-campaign/references/step-09-refine.md +29 -30
  212. package/src/skf-campaign/references/step-10-export.md +48 -36
  213. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  214. package/src/skf-campaign/references/step-resume.md +37 -68
  215. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  216. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  217. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  218. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  219. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  220. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  221. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  222. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  223. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  224. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  225. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  226. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  227. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  228. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  229. package/src/skf-create-skill/SKILL.md +27 -20
  230. package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
  231. package/src/skf-create-skill/assets/skill-sections.md +21 -11
  232. package/src/skf-create-skill/customize.toml +20 -8
  233. package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
  234. package/src/skf-create-skill/references/batch-mode.md +81 -0
  235. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  236. package/src/skf-create-skill/references/compile.md +119 -70
  237. package/src/skf-create-skill/references/component-extraction.md +113 -91
  238. package/src/skf-create-skill/references/enrich.md +41 -23
  239. package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
  240. package/src/skf-create-skill/references/extract.md +204 -162
  241. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  242. package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
  243. package/src/skf-create-skill/references/extraction-patterns.md +588 -201
  244. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  245. package/src/skf-create-skill/references/health-check.md +1 -1
  246. package/src/skf-create-skill/references/load-brief.md +53 -36
  247. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  248. package/src/skf-create-skill/references/report.md +37 -57
  249. package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
  250. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  251. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  252. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  253. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  254. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  255. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  256. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  257. package/src/skf-create-skill/references/validate.md +173 -99
  258. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  259. package/src/skf-create-stack-skill/SKILL.md +29 -56
  260. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  261. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  262. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  263. package/src/skf-create-stack-skill/customize.toml +23 -12
  264. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  265. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  266. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  267. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  268. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  269. package/src/skf-create-stack-skill/references/init.md +90 -32
  270. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  271. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  272. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  273. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  274. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  275. package/src/skf-create-stack-skill/references/report.md +39 -33
  276. package/src/skf-create-stack-skill/references/validate.md +20 -39
  277. package/src/skf-drop-skill/SKILL.md +28 -51
  278. package/src/skf-drop-skill/customize.toml +44 -30
  279. package/src/skf-drop-skill/references/execute.md +83 -146
  280. package/src/skf-drop-skill/references/health-check.md +2 -0
  281. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  282. package/src/skf-drop-skill/references/report.md +46 -30
  283. package/src/skf-drop-skill/references/select.md +122 -124
  284. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  285. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  286. package/src/skf-export-skill/SKILL.md +40 -62
  287. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  288. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  289. package/src/skf-export-skill/customize.toml +42 -13
  290. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  291. package/src/skf-export-skill/references/health-check.md +2 -0
  292. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  293. package/src/skf-export-skill/references/load-skill.md +88 -62
  294. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  295. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  296. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  297. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  298. package/src/skf-export-skill/references/package.md +15 -73
  299. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  300. package/src/skf-export-skill/references/result-envelope.md +31 -16
  301. package/src/skf-export-skill/references/summary.md +41 -17
  302. package/src/skf-export-skill/references/token-report.md +4 -12
  303. package/src/skf-export-skill/references/update-context.md +118 -237
  304. package/src/skf-forger/SKILL.md +61 -37
  305. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  306. package/src/skf-forger/customize.toml +43 -0
  307. package/src/skf-forger/references/pipeline-mode.md +73 -18
  308. package/src/skf-forger/scripts/forge-status.py +299 -0
  309. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  310. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  311. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  312. package/src/skf-quick-skill/SKILL.md +54 -53
  313. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  314. package/src/skf-quick-skill/customize.toml +51 -20
  315. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  316. package/src/skf-quick-skill/references/compile.md +30 -31
  317. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  318. package/src/skf-quick-skill/references/finalize.md +69 -18
  319. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  320. package/src/skf-quick-skill/references/health-check.md +5 -3
  321. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  322. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  323. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  324. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  325. package/src/skf-refine-architecture/SKILL.md +43 -53
  326. package/src/skf-refine-architecture/customize.toml +49 -20
  327. package/src/skf-refine-architecture/references/compile.md +97 -54
  328. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  329. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  330. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  331. package/src/skf-refine-architecture/references/improvements.md +14 -20
  332. package/src/skf-refine-architecture/references/init.md +98 -57
  333. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  334. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  335. package/src/skf-refine-architecture/references/report.md +65 -54
  336. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  337. package/src/skf-rename-skill/SKILL.md +19 -54
  338. package/src/skf-rename-skill/customize.toml +32 -26
  339. package/src/skf-rename-skill/references/execute.md +230 -184
  340. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  341. package/src/skf-rename-skill/references/health-check.md +1 -7
  342. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  343. package/src/skf-rename-skill/references/report.md +63 -52
  344. package/src/skf-rename-skill/references/select.md +197 -115
  345. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  346. package/src/skf-setup/SKILL.md +26 -26
  347. package/src/skf-setup/customize.toml +14 -10
  348. package/src/skf-setup/references/auto-index.md +50 -63
  349. package/src/skf-setup/references/ccc-index.md +29 -90
  350. package/src/skf-setup/references/detect-and-tier.md +58 -67
  351. package/src/skf-setup/references/health-check.md +2 -0
  352. package/src/skf-setup/references/invocation-contract.md +14 -0
  353. package/src/skf-setup/references/report.md +43 -166
  354. package/src/skf-setup/references/tier-rules.md +5 -5
  355. package/src/skf-setup/references/write-config.md +34 -57
  356. package/src/skf-test-skill/SKILL.md +17 -57
  357. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  358. package/src/skf-test-skill/customize.toml +27 -13
  359. package/src/skf-test-skill/references/coherence-check.md +134 -62
  360. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  361. package/src/skf-test-skill/references/coverage-check.md +209 -264
  362. package/src/skf-test-skill/references/detect-mode.md +5 -5
  363. package/src/skf-test-skill/references/external-validators.md +94 -82
  364. package/src/skf-test-skill/references/health-check.md +6 -7
  365. package/src/skf-test-skill/references/init.md +164 -91
  366. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  367. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  368. package/src/skf-test-skill/references/report.md +164 -142
  369. package/src/skf-test-skill/references/score.md +80 -107
  370. package/src/skf-test-skill/references/scoring-rules.md +42 -155
  371. package/src/skf-test-skill/references/source-access-protocol.md +18 -94
  372. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  373. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  374. package/src/skf-test-skill/scripts/build-result-context.py +380 -0
  375. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  376. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  377. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  378. package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
  379. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  380. package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
  381. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  382. package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
  383. package/src/skf-test-skill/scripts/score-signatures.py +479 -0
  384. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  385. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  386. package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
  387. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  388. package/src/skf-update-skill/SKILL.md +38 -25
  389. package/src/skf-update-skill/customize.toml +27 -12
  390. package/src/skf-update-skill/references/detect-changes.md +253 -223
  391. package/src/skf-update-skill/references/gap-driven.md +336 -0
  392. package/src/skf-update-skill/references/health-check.md +31 -5
  393. package/src/skf-update-skill/references/init.md +254 -84
  394. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  395. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  396. package/src/skf-update-skill/references/merge.md +182 -49
  397. package/src/skf-update-skill/references/re-extract.md +53 -262
  398. package/src/skf-update-skill/references/report.md +102 -35
  399. package/src/skf-update-skill/references/write.md +357 -155
  400. package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
  401. package/src/skf-verify-stack/SKILL.md +23 -42
  402. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  403. package/src/skf-verify-stack/customize.toml +55 -22
  404. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  405. package/src/skf-verify-stack/references/coverage.md +67 -64
  406. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  407. package/src/skf-verify-stack/references/init.md +107 -62
  408. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  409. package/src/skf-verify-stack/references/integrations.md +106 -65
  410. package/src/skf-verify-stack/references/report.md +59 -42
  411. package/src/skf-verify-stack/references/requirements.md +32 -9
  412. package/src/skf-verify-stack/references/synthesize.md +53 -39
  413. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  414. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  415. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  416. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  417. package/tools/cli/commands/install.js +11 -4
  418. package/tools/cli/commands/status.js +15 -3
  419. package/tools/cli/commands/update.js +11 -4
  420. package/tools/cli/lib/tool-check.js +320 -0
  421. package/tools/cli/lib/version-check.js +22 -9
  422. package/tools/cli/skf-cli.js +4 -1
  423. package/.gitattributes +0 -16
  424. package/.nvmrc +0 -1
  425. package/docs/_internal/RELEASING.md +0 -679
  426. package/release-audits/v1.0.0-launch-audit.md +0 -728
  427. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  428. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  429. package/src/skf-campaign/manifest.yaml +0 -15
  430. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  431. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  432. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  433. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  434. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  435. package/src/skf-update-skill/references/validate.md +0 -124
  436. package/tools/validate-docs-drift.js +0 -233
  437. package/tools/validate-docs-links.js +0 -665
@@ -0,0 +1,664 @@
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>]... [--scope-type <type>] \\
22
+ -o <extraction-snapshot.json>
23
+ uv run skf-extraction-snapshot.py relocate <extraction-snapshot.json> \\
24
+ --diff <structural-diff.json> --extraction <runner.json>
25
+
26
+ scan-list
27
+
28
+ Writes the bounded scan list, the files the provenance map covers (the
29
+ union of `entries[].source_file` and `file_entries[].source_file`, sorted,
30
+ with forward slashes: skf-load-provenance.py's `bounded_scan_files`), as a
31
+ JSON list of paths, the form the runner's --files-from reads. Prints
32
+ {"status": "ok", "output": <file>, "files": N}.
33
+
34
+ build
35
+
36
+ The files in scope are the bounded scan list with --provenance-map (every
37
+ audit passes it), else every file the runner or the details name. Each
38
+ gets one status, in this order of precedence:
39
+
40
+ missing not a file under --source-root
41
+ hash-tracked only `file_entries[]` names it (a script, an asset or a
42
+ promoted doc): step 3 compares its content hash, so
43
+ nothing reads it for exports
44
+ read-by-eye the details name it in `files` (the step read it)
45
+ parse-failed the runner listed it in `file_issues` (a syntax error,
46
+ not UTF-8, unreadable, or a language no recipe reads):
47
+ read it by eye
48
+ unread no runner result, a runner that could not run
49
+ (`no-ast-grep`), or an incomplete run that names it as
50
+ unread (`errors[].unread`) or found no export in it
51
+ extracted the runner read it
52
+
53
+ `complete` is true when no file is `parse-failed` or `unread`: step 2
54
+ reads those by eye, adds them to the details and builds again. A file
55
+ carries `fallback: true` when the runner ran and left it `parse-failed`
56
+ or `unread`: read by eye after an ast-grep failure.
57
+
58
+ Exports: the runner's exports of the files in scope (each T1, with the
59
+ declaration on one line as `signature`), then each export of the details
60
+ (`name` or `export_name`, `file` or `source_file`, `line` or
61
+ `source_line`, `type` or `export_type`, `signature`; T1-low and
62
+ `source-read` unless it says otherwise). A details export with the name
63
+ and file of a runner export fills only the fields that one lacks; any
64
+ other is added. An export of a file outside the files in scope, or of a
65
+ `hash-tracked` one (the map records no export there, so it would read
66
+ as added), is left out, with a warning. With --provenance-map, each export of a file the
67
+ map gives a library (`source_library_by_file`) carries it as
68
+ `source_library`, which step 3's `--group-by source_library` diff reads.
69
+
70
+ Public surface (--scope-type public-api, in any case). Each export of a
71
+ language family whose entry points are a barrel is marked `public`, true
72
+ or false, by skf-extraction-inventory.py's public-surface rule
73
+ (`public_surface` over the runner's JSON, with the scope type this flag
74
+ gives, and `public(name, file, language)`, the language of the runner's
75
+ export of that name and file, else the file's extension): the rule
76
+ create-skill keeps a public-api skill's provenance map to, and
77
+ update-skill's change manifest too. Every export stays in the snapshot,
78
+ so a map entry off the surface is never reported removed; step 3's diff
79
+ lists an export marked false that pairs with no map entry under
80
+ `not_public[]`, not `added[]`. With no surface the rule can read (no
81
+ runner JSON, as at Quick tier, a runner that could not run, an
82
+ incomplete run, one with errors, or an entry point the runner could not
83
+ trace) nothing is marked, and `public_surface.problem` says why. Any
84
+ other scope type, and no flag, mark nothing and give `public_surface`
85
+ null; only --scope-type public-api loads skf-extraction-inventory.py,
86
+ from beside this script.
87
+
88
+ Details file: {"files": [paths read by eye], "exports": [export objects]}
89
+ (either key may be absent). Give --details once per file: one per worker.
90
+
91
+ Output (-o), the snapshot step 3 reads:
92
+
93
+ {
94
+ "extraction_date": "<--date>",
95
+ "confidence_tier": "<--tier>",
96
+ "source_root": "<--source-path, else --source-root>",
97
+ "bounded_scan": true | false,
98
+ "bounded_scan_source": "provenance-map" | "source-tree-fallback",
99
+ "runner_status": "ok" | "incomplete" | "no-ast-grep" | null,
100
+ "runner_errors": [the runner's errors[]],
101
+ "files_scanned": N,
102
+ "complete": bool,
103
+ "files": [{"file", "status", "issue": <the runner's issue> | null,
104
+ "fallback": bool, "exports": N}, ...],
105
+ "files_by_status": {"extracted": N, "read-by-eye": N,
106
+ "hash-tracked": N, "missing": N,
107
+ "parse-failed": N, "unread": N},
108
+ "ast_fallback_files": ["<file>", ...],
109
+ "extraction_gaps": [{"name", "file", "line", "entry",
110
+ "export_type"?}, ...],
111
+ "truncated": bool, # a recipe hit the runner's head cap
112
+ "cap_hits": ["<recipe id>", ...],
113
+ "outside_scope": [{"path": "<file>", "names": [...],
114
+ "entries": [...]}, ...],
115
+ "counts": {"exports": N, "by_type": {"<type>": N, ...},
116
+ "t1": N, "t1_low": N},
117
+ "exports": [{"name", "type", "signature", "file", "line",
118
+ "confidence", "extraction_method", "ast_node_type",
119
+ "source_library"?, "public"?}, ...],
120
+ "public_surface": {"problem": "<why no surface was read>" | null,
121
+ "marked_not_public": N} | null,
122
+ "relocations": [{"name", "file", "line"}, ...], # relocate's
123
+ "warnings": ["..."]
124
+ }
125
+
126
+ `ast_fallback_files` names the files read after an ast-grep failure (the
127
+ `fallback` ones) when the runner ran; when it did not, it is
128
+ ["all files (ast-grep unavailable)"], and [] at the Quick tier.
129
+ `extraction_gaps` lists the names a package entry point exports that no
130
+ recipe found (the runner's `entry_point_diff.extraction_gaps`), less the
131
+ ones an export of the snapshot now holds, then, with --provenance-map,
132
+ the baseline gaps: each entry of the map the snapshot lacks whose file
133
+ still declares it (the runner leaves out an underscore name such as
134
+ `__version__`, and a module), which would otherwise read as removed. A
135
+ baseline gap gives the line that declares the name, `entry` null and,
136
+ when the entry has one, its `export_type`. It is checked only for an
137
+ entry whose file is `extracted` or `read-by-eye`, whose name is not
138
+ dotted, and that the snapshot holds neither under its name nor under
139
+ its `reexported_as` target at that file. The selection and the
140
+ declaration are skf-verify-provenance-completeness.py's (`baseline_gaps`,
141
+ by `declared_line`), loaded from beside this script, the rule
142
+ update-skill's Category B applies through skf-build-change-manifest.py
143
+ baseline-gaps: a line at column 0 of a Python or TS/JS file that the
144
+ definition-line rules match with imports left out (an alias that
145
+ renames counts; a plain or star import and a re-export never do),
146
+ and for a `module` or `package` entry the module or package so named
147
+ (at its line 1). An entry no rule can check (its file neither Python
148
+ nor TS/JS, a file the lookup does not read, such as a test file or one
149
+ with a symlink on its path, or a dotted `module` or `package` name) is
150
+ named in one warning, as `name (file)`. Each name and file is listed
151
+ once, and each file is read once. Read them by eye.
152
+ `outside_scope` groups the runner's `entry_point_diff.outside_scope` by
153
+ the file that defines each name: public API a package entry point exports
154
+ from a file the skill does not cover, which the drift report lists under
155
+ Out-of-Scope New Public API for update-skill's scope reconciliation.
156
+
157
+ Prints {"status": "ok", "output", "runner_status", "complete",
158
+ "to_read": [{"file", "status", "issue"}, ...] (the `parse-failed` and
159
+ `unread` files), "extraction_gaps", "ast_fallback_files",
160
+ "files_by_status", "counts", "public_surface"} on stdout: what step 2
161
+ acts on, so it never opens the runner's JSON.
162
+
163
+ relocate
164
+
165
+ Step 3's relocation check: --extraction is the runner's JSON over the
166
+ files that hold a name the diff reports removed, outside the bounded scan
167
+ list. Each export the runner found there under a name of the diff's
168
+ `removed[]`, in a file outside the snapshot's files in scope, joins the
169
+ snapshot's exports (T1, with the removed entry's `source_library` when it
170
+ has one) and `relocations`; the counts are recounted and the snapshot is
171
+ written back in place. Prints {"status": "ok", "output", "added": N,
172
+ "names": [...]}; added 0 changes nothing.
173
+
174
+ Exit codes: 0 the file was written (or relocate found nothing to add); 2
175
+ an input that cannot be read or parsed, a helper beside this script that
176
+ cannot be loaded, or an output that cannot be written ({"status":
177
+ "error", "error"} on stdout), or a usage error (argparse, on stderr).
178
+ """
179
+
180
+ from __future__ import annotations
181
+
182
+ import argparse
183
+ import importlib.util
184
+ import json
185
+ import sys
186
+ from collections import Counter
187
+ from pathlib import Path
188
+
189
+ STATUSES = ("extracted", "read-by-eye", "hash-tracked", "missing", "parse-failed", "unread")
190
+ TO_READ = ("parse-failed", "unread")
191
+ RUNNER_READ = frozenset({"ok", "incomplete"})
192
+ # The files whose provenance entries a baseline gap checks: the ones read.
193
+ BASELINE_GAP_STATUSES = ("extracted", "read-by-eye")
194
+ AST_GREP_UNAVAILABLE = "all files (ast-grep unavailable)"
195
+ # The scope type whose exports are marked by the public-surface rule.
196
+ PUBLIC_API = "public-api"
197
+ EXIT_ERROR = 2
198
+
199
+
200
+ class SnapshotError(Exception):
201
+ """An input or output the helper cannot use (exit 2)."""
202
+
203
+
204
+ _SIBLINGS: dict[str, object] = {}
205
+
206
+
207
+ def _sibling(filename: str, *needs: str):
208
+ """A script installed beside this one, loaded once. Any failure to load
209
+ it, or a script without each name `needs` lists, is a SnapshotError
210
+ (exit 2)."""
211
+ if filename not in _SIBLINGS:
212
+ path = Path(__file__).resolve().parent / filename
213
+ try:
214
+ spec = importlib.util.spec_from_file_location(filename[:-3].replace("-", "_"), path)
215
+ if spec is None or spec.loader is None:
216
+ raise ImportError(f"no loader for {path}")
217
+ module = importlib.util.module_from_spec(spec)
218
+ spec.loader.exec_module(module)
219
+ except Exception as exc: # whatever the script raises, the build reports it
220
+ raise SnapshotError(f"cannot load {filename} beside {Path(__file__).name}: {exc}") from exc
221
+ missing = [need for need in needs if getattr(module, need, None) is None]
222
+ if missing:
223
+ raise SnapshotError(f"cannot load {filename} beside {Path(__file__).name}: "
224
+ f"it has no {', '.join(missing)}")
225
+ _SIBLINGS[filename] = module
226
+ return _SIBLINGS[filename]
227
+
228
+
229
+ def _provenance():
230
+ """skf-load-provenance.py, for the scan list and the libraries (the
231
+ verifier's `baseline_gaps` reads the re-export map itself)."""
232
+ return _sibling("skf-load-provenance.py", "bounded_scan_files", "source_library_by_file")
233
+
234
+
235
+ def _verifier():
236
+ """skf-verify-provenance-completeness.py, for its baseline gaps
237
+ (`baseline_gaps`, by its declaration rule `declared_line`)."""
238
+ return _sibling("skf-verify-provenance-completeness.py", "baseline_gaps", "declared_line")
239
+
240
+
241
+ def _public_api(scope_type: str | None) -> bool:
242
+ return isinstance(scope_type, str) and scope_type.strip().lower() == PUBLIC_API
243
+
244
+
245
+ def _public_surface(runner: dict, scope_type: str):
246
+ """(surface, problem) by skf-extraction-inventory.py's `public_surface` over the runner's JSON with the scope
247
+ type --scope-type gives (an empty one when no runner ran, so the rule names its own problem)."""
248
+ scope = runner.get("scope") if isinstance(runner.get("scope"), dict) else {}
249
+ extraction = {**runner, "scope": {**scope, "type": scope_type}}
250
+ return _sibling("skf-extraction-inventory.py", "public_surface").public_surface(extraction)
251
+
252
+
253
+ def _mark_public(exports: list[dict], runner: dict, scope_type: str | None) -> dict | None:
254
+ """Mark each export `public` by the public-surface rule for a public-api scope type; the snapshot's
255
+ `public_surface` (None for any other scope type)."""
256
+ if not _public_api(scope_type):
257
+ return None
258
+ surface, problem = _public_surface(runner, scope_type)
259
+ marked_not_public = 0
260
+ if surface is not None:
261
+ languages = {(e.get("export_name"), _posix(str(e.get("source_file") or ""))): e.get("language")
262
+ for e in runner.get("exports") or [] if isinstance(e, dict)}
263
+ for record in exports:
264
+ mark = surface.public(record["name"], record["file"], languages.get((record["name"], record["file"])))
265
+ if mark is not None:
266
+ record["public"] = mark
267
+ if mark is False:
268
+ marked_not_public += 1
269
+ return {"problem": problem, "marked_not_public": marked_not_public}
270
+
271
+
272
+ def _posix(path: str) -> str:
273
+ """A path as the scan list writes it: forward slashes, no leading `./`."""
274
+ rel = path.strip().replace("\\", "/")
275
+ while rel.startswith("./"):
276
+ rel = rel[2:]
277
+ return rel
278
+
279
+
280
+ def _read_json(path: Path, what: str) -> object:
281
+ try:
282
+ return json.loads(path.read_text(encoding="utf-8"))
283
+ except OSError as exc:
284
+ raise SnapshotError(f"cannot read {what} {path}: {exc.strerror or exc}") from exc
285
+ except (UnicodeDecodeError, ValueError) as exc:
286
+ raise SnapshotError(f"{what} {path} is not JSON: {exc}") from exc
287
+
288
+
289
+ def _read_object(path: Path, what: str) -> dict:
290
+ data = _read_json(path, what)
291
+ if not isinstance(data, dict):
292
+ raise SnapshotError(f"{what} {path} must be a JSON object")
293
+ return data
294
+
295
+
296
+ def _write(path: Path, data: object) -> None:
297
+ try:
298
+ path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
299
+ except OSError as exc:
300
+ raise SnapshotError(f"cannot write {path}: {exc.strerror or exc}") from exc
301
+
302
+
303
+ def _first(entry: dict, *keys: str) -> object:
304
+ for key in keys:
305
+ if entry.get(key) is not None:
306
+ return entry[key]
307
+ return None
308
+
309
+
310
+ # --------------------------------------------------------------------------
311
+ # Building the snapshot
312
+ # --------------------------------------------------------------------------
313
+
314
+
315
+ def _runner_export(export: dict) -> dict:
316
+ return {
317
+ "name": export.get("export_name"),
318
+ "type": export.get("export_type"),
319
+ "signature": _first(export, "signature", "signature_line"),
320
+ "file": _posix(str(export.get("source_file") or "")),
321
+ "line": export.get("source_line"),
322
+ "confidence": export.get("confidence") or "T1",
323
+ "extraction_method": export.get("extraction_method") or "ast-grep",
324
+ "ast_node_type": export.get("ast_node_type"),
325
+ }
326
+
327
+
328
+ def _details_export(export: dict) -> dict | None:
329
+ name, file = _first(export, "name", "export_name"), _first(export, "file", "source_file")
330
+ if not isinstance(name, str) or not name.strip() or not isinstance(file, str) or not file.strip():
331
+ return None
332
+ return {
333
+ "name": name.strip(),
334
+ "type": _first(export, "type", "export_type"),
335
+ "signature": export.get("signature"),
336
+ "file": _posix(file),
337
+ "line": _first(export, "line", "source_line"),
338
+ "confidence": export.get("confidence") or "T1-low",
339
+ "extraction_method": export.get("extraction_method") or "source-read",
340
+ "ast_node_type": export.get("ast_node_type"),
341
+ }
342
+
343
+
344
+ def _details(path: Path) -> tuple[list[str], list[dict]]:
345
+ data = _read_object(path, "details file")
346
+ files, exports = data.get("files", []), data.get("exports", [])
347
+ if not isinstance(files, list) or not all(isinstance(f, str) for f in files):
348
+ raise SnapshotError(f"details file {path}: `files` must be a list of paths")
349
+ if not isinstance(exports, list) or not all(isinstance(e, dict) for e in exports):
350
+ raise SnapshotError(f"details file {path}: `exports` must be a list of objects")
351
+ return [_posix(f) for f in files], exports
352
+
353
+
354
+ def _entry_point_diff(runner: dict) -> dict:
355
+ diff = runner.get("entry_point_diff")
356
+ return diff if isinstance(diff, dict) else {}
357
+
358
+
359
+ def _outside_scope(runner: dict) -> list[dict]:
360
+ grouped: dict[str, dict] = {}
361
+ for item in _entry_point_diff(runner).get("outside_scope") or []:
362
+ if not isinstance(item, dict) or not isinstance(item.get("file"), str) or not item.get("name"):
363
+ continue
364
+ group = grouped.setdefault(_posix(item["file"]), {"names": [], "entries": []})
365
+ if item["name"] not in group["names"]:
366
+ group["names"].append(item["name"])
367
+ entry = item.get("entry")
368
+ if isinstance(entry, str) and entry not in group["entries"]:
369
+ group["entries"].append(entry)
370
+ return [{"path": path, "names": sorted(g["names"]), "entries": sorted(g["entries"])}
371
+ for path, g in sorted(grouped.items())]
372
+
373
+
374
+ def _extraction_gaps(runner: dict, exports: list[dict]) -> list[dict]:
375
+ """The runner's extraction gaps no export of the snapshot holds yet, one
376
+ per name and file."""
377
+ held = {(e["name"], e["file"]) for e in exports}
378
+ names = {e["name"] for e in exports}
379
+ gaps: dict[tuple, dict] = {}
380
+ for item in _entry_point_diff(runner).get("extraction_gaps") or []:
381
+ if not isinstance(item, dict) or not isinstance(item.get("name"), str) or not item["name"]:
382
+ continue
383
+ file = _posix(item["file"]) if isinstance(item.get("file"), str) else None
384
+ if (item["name"], file) in held or (file is None and item["name"] in names):
385
+ continue
386
+ gaps.setdefault((item["name"], file),
387
+ {"name": item["name"], "file": file, "line": item.get("line"), "entry": item.get("entry")})
388
+ return list(gaps.values())
389
+
390
+
391
+ def _baseline_gaps(provenance: dict, source_root: Path, statuses: dict[str, str], exports: list[dict],
392
+ listed: set[tuple]) -> tuple[list[dict], list[str]]:
393
+ """The provenance entries the snapshot lacks whose cited file still
394
+ declares them, and the entries no rule can check, by the verifier's
395
+ `baseline_gaps` (the selection and the declaration rule both live
396
+ there, beside `declared_line`, for update-skill's `baseline-gaps` too):
397
+ the entries of the files `extracted` or `read-by-eye`, against the
398
+ snapshot's exports, less the gaps `listed` already holds."""
399
+ files = {file for file, status in statuses.items() if status in BASELINE_GAP_STATUSES}
400
+ held = {(e["name"], e["file"]) for e in exports}
401
+ return _verifier().baseline_gaps(provenance, source_root, held, files, listed)
402
+
403
+
404
+ def _ordered(exports) -> list[dict]:
405
+ return sorted(exports, key=lambda e: (e["file"], e["line"] if isinstance(e["line"], int) else 0, e["name"]))
406
+
407
+
408
+ def _counts(exports: list[dict]) -> dict:
409
+ by_type = Counter(str(e["type"]) for e in exports if e.get("type") is not None)
410
+ t1 = sum(1 for e in exports if e.get("confidence") == "T1")
411
+ return {"exports": len(exports), "by_type": dict(sorted(by_type.items())), "t1": t1,
412
+ "t1_low": len(exports) - t1}
413
+
414
+
415
+ def build(source_root: Path, tier: str, date: str, provenance: dict | None, runner: dict | None,
416
+ details_files: list[str], details_exports: list[dict], root_text: str | None = None,
417
+ scope_type: str | None = None) -> dict:
418
+ """The snapshot (see the module docstring)."""
419
+ warnings: list[str] = []
420
+ runner = runner or {}
421
+ runner_status = runner.get("status") if runner else None
422
+ ran = runner_status in RUNNER_READ
423
+ issues = {_posix(i["file"]): i.get("issue") for i in runner.get("file_issues") or []
424
+ if isinstance(i, dict) and isinstance(i.get("file"), str)}
425
+ unread = {_posix(f) for e in runner.get("errors") or [] if isinstance(e, dict)
426
+ for f in e.get("unread") or [] if isinstance(f, str)}
427
+ runner_exports = [_runner_export(e) for e in runner.get("exports") or []
428
+ if isinstance(e, dict) and e.get("export_name")] if ran else []
429
+ extra = [x for x in (_details_export(e) for e in details_exports) if x is not None]
430
+ if len(extra) < len(details_exports):
431
+ warnings.append(f"{len(details_exports) - len(extra)} details export(s) name no export or no file: "
432
+ "left out")
433
+
434
+ if provenance is not None:
435
+ scope = list(_provenance().bounded_scan_files(provenance))
436
+ # the files a code entry names; any other is a file_entries[] row
437
+ code_files = set(_provenance().bounded_scan_files({"entries": provenance.get("entries")}))
438
+ libraries = _provenance().source_library_by_file(provenance)
439
+ else:
440
+ named = [e["file"] for e in runner_exports + extra] + list(issues) + list(unread) + details_files
441
+ scope = sorted(set(f for f in named if f))
442
+ code_files = set(scope)
443
+ libraries = {}
444
+ in_scope = set(scope)
445
+
446
+ exports: dict[tuple[str, str], dict] = {}
447
+ hashed = Counter()
448
+ for record in runner_exports + extra:
449
+ if record["file"] not in in_scope:
450
+ warnings.append(f"{record['name']} in {record['file']}: the file is outside the files in scope, "
451
+ "left out")
452
+ continue
453
+ if record["file"] not in code_files:
454
+ hashed[record["file"]] += 1
455
+ continue
456
+ key = (record["name"], record["file"])
457
+ held = exports.get(key)
458
+ if held is None:
459
+ exports[key] = record
460
+ else:
461
+ for field, value in record.items():
462
+ if held.get(field) is None and value is not None:
463
+ held[field] = value
464
+ warnings += [f"{count} export(s) in {file}: only file_entries[] names the file, which step 3 compares "
465
+ "by hash, left out" for file, count in sorted(hashed.items())]
466
+ per_file = Counter(file for _, file in exports)
467
+
468
+ files = []
469
+ read_by_eye = set(details_files)
470
+ for rel in scope:
471
+ issue = issues.get(rel)
472
+ if not (source_root / rel).is_file():
473
+ runner_said = "missing"
474
+ elif rel not in code_files:
475
+ runner_said = "hash-tracked"
476
+ elif ran and issue is not None and issue != "missing":
477
+ runner_said = "parse-failed"
478
+ elif not ran or (runner_status == "incomplete" and (rel in unread or not per_file[rel])):
479
+ runner_said = "unread"
480
+ else:
481
+ runner_said = "extracted"
482
+ by_eye = rel in read_by_eye and runner_said not in ("missing", "hash-tracked")
483
+ status = "read-by-eye" if by_eye else runner_said
484
+ files.append({"file": rel, "status": status,
485
+ "issue": issue if runner_said in ("parse-failed", "hash-tracked") else None,
486
+ "fallback": ran and runner_said in TO_READ, "exports": per_file[rel]})
487
+
488
+ ordered = _ordered(exports.values())
489
+ surface_state = _mark_public(ordered, runner, scope_type)
490
+ for record in ordered:
491
+ library = libraries.get(record["file"])
492
+ if library is not None:
493
+ record["source_library"] = library
494
+ by_status = Counter(f["status"] for f in files)
495
+ recipes = [r for r in runner.get("recipes") or [] if isinstance(r, dict)]
496
+ cap_hits = [r.get("id") for r in recipes if r.get("truncated")]
497
+ if ran:
498
+ fallback_files = [f["file"] for f in files if f["fallback"]]
499
+ else:
500
+ fallback_files = [] if tier.strip().lower() == "quick" else [AST_GREP_UNAVAILABLE]
501
+ gaps = _extraction_gaps(runner, ordered) if ran else []
502
+ if provenance is not None:
503
+ baseline, unchecked = _baseline_gaps(provenance, source_root, {f["file"]: f["status"] for f in files},
504
+ ordered, {(g["name"], g["file"]) for g in gaps})
505
+ gaps += baseline
506
+ if unchecked:
507
+ warnings.append(f"{len(unchecked)} map entr{'y' if len(unchecked) == 1 else 'ies'} the snapshot "
508
+ f"lacks that no rule can check (in a file neither Python nor TS/JS, a file the "
509
+ f"declaration lookup does not read, or a dotted module name), so step 3 may report "
510
+ f"{'it' if len(unchecked) == 1 else 'them'} removed: "
511
+ + ", ".join(f"{u['name']} ({u['file']})" for u in unchecked))
512
+ return {
513
+ "extraction_date": date,
514
+ "confidence_tier": tier,
515
+ "source_root": root_text if root_text is not None else str(source_root),
516
+ "bounded_scan": provenance is not None,
517
+ "bounded_scan_source": "provenance-map" if provenance is not None else "source-tree-fallback",
518
+ "runner_status": runner_status,
519
+ "runner_errors": [e for e in runner.get("errors") or [] if isinstance(e, dict)],
520
+ "files_scanned": len(scope),
521
+ "complete": not any(by_status[status] for status in TO_READ),
522
+ "files": files,
523
+ "files_by_status": {status: by_status[status] for status in STATUSES},
524
+ "ast_fallback_files": fallback_files,
525
+ "extraction_gaps": gaps,
526
+ "truncated": bool(runner.get("truncated")) or bool(cap_hits),
527
+ "cap_hits": cap_hits,
528
+ "outside_scope": _outside_scope(runner) if ran else [],
529
+ "counts": _counts(ordered),
530
+ "exports": ordered,
531
+ "public_surface": surface_state,
532
+ "relocations": [],
533
+ "warnings": warnings,
534
+ }
535
+
536
+
537
+ def relocate(snapshot: dict, diff: dict, runner: dict) -> list[dict]:
538
+ """Add to `snapshot` each export `runner` found under a removed name of
539
+ `diff` in a file outside the snapshot's scope; the exports added."""
540
+ removed: dict[str, dict] = {}
541
+ for item in diff.get("removed") or []:
542
+ if isinstance(item, dict) and isinstance(item.get("name"), str):
543
+ removed.setdefault(item["name"], item)
544
+ exports = snapshot.get("exports")
545
+ if not isinstance(exports, list) or not all(isinstance(e, dict) for e in exports):
546
+ raise SnapshotError("the snapshot's `exports` must be a list of objects")
547
+ scope = {f.get("file") for f in snapshot.get("files") or [] if isinstance(f, dict)}
548
+ held = {(e.get("name"), e.get("file")) for e in exports}
549
+ added = []
550
+ if runner.get("status") in RUNNER_READ:
551
+ for export in runner.get("exports") or []:
552
+ if not isinstance(export, dict) or export.get("export_name") not in removed:
553
+ continue
554
+ record = _runner_export(export)
555
+ if record["file"] in scope or (record["name"], record["file"]) in held:
556
+ continue
557
+ library = removed[record["name"]].get("source_library")
558
+ if isinstance(library, str):
559
+ record["source_library"] = library
560
+ held.add((record["name"], record["file"]))
561
+ added.append(record)
562
+ if added:
563
+ snapshot["exports"] = _ordered(exports + added)
564
+ snapshot["counts"] = _counts(snapshot["exports"])
565
+ snapshot["relocations"] = list(snapshot.get("relocations") or []) + [
566
+ {"name": e["name"], "file": e["file"], "line": e["line"]} for e in added]
567
+ return added
568
+
569
+
570
+ # --------------------------------------------------------------------------
571
+ # CLI
572
+ # --------------------------------------------------------------------------
573
+
574
+
575
+ def _cmd_scan_list(args: argparse.Namespace) -> dict:
576
+ provenance = _read_object(Path(args.provenance_map), "provenance map")
577
+ files = list(_provenance().bounded_scan_files(provenance))
578
+ _write(Path(args.output), files)
579
+ return {"status": "ok", "output": args.output, "files": len(files)}
580
+
581
+
582
+ def _cmd_build(args: argparse.Namespace) -> dict:
583
+ root = Path(args.source_root)
584
+ if not root.is_dir():
585
+ raise SnapshotError(f"source root not found: {args.source_root}")
586
+ provenance = _read_object(Path(args.provenance_map), "provenance map") if args.provenance_map else None
587
+ runner = _read_object(Path(args.extraction), "runner result") if args.extraction else None
588
+ details_files: list[str] = []
589
+ details_exports: list[dict] = []
590
+ for path in args.details or []:
591
+ files, exports = _details(Path(path))
592
+ details_files += files
593
+ details_exports += exports
594
+ snapshot = build(root, args.tier, args.date, provenance, runner, details_files, details_exports,
595
+ root_text=args.source_path or args.source_root, scope_type=args.scope_type)
596
+ _write(Path(args.output), snapshot)
597
+ return {"status": "ok", "output": args.output, "runner_status": snapshot["runner_status"],
598
+ "complete": snapshot["complete"],
599
+ "to_read": [{"file": f["file"], "status": f["status"], "issue": f["issue"]}
600
+ for f in snapshot["files"] if f["status"] in TO_READ],
601
+ "extraction_gaps": snapshot["extraction_gaps"], "ast_fallback_files": snapshot["ast_fallback_files"],
602
+ "files_by_status": snapshot["files_by_status"], "counts": snapshot["counts"],
603
+ "public_surface": snapshot["public_surface"]}
604
+
605
+
606
+ def _cmd_relocate(args: argparse.Namespace) -> dict:
607
+ path = Path(args.snapshot)
608
+ snapshot = _read_object(path, "snapshot")
609
+ added = relocate(snapshot, _read_object(Path(args.diff), "structural diff"),
610
+ _read_object(Path(args.extraction), "runner result"))
611
+ if added:
612
+ _write(path, snapshot)
613
+ return {"status": "ok", "output": args.snapshot, "added": len(added),
614
+ "names": sorted({e["name"] for e in added})}
615
+
616
+
617
+ def _build_parser() -> argparse.ArgumentParser:
618
+ parser = argparse.ArgumentParser(
619
+ prog="skf-extraction-snapshot",
620
+ description=__doc__,
621
+ formatter_class=argparse.RawDescriptionHelpFormatter,
622
+ )
623
+ sub = parser.add_subparsers(dest="cmd", required=True)
624
+ scan = sub.add_parser("scan-list", help="write the files the provenance map covers as a JSON list")
625
+ scan.add_argument("provenance_map", help="path to provenance-map.json")
626
+ scan.add_argument("-o", "--output", required=True, help="the JSON list to write")
627
+ scan.set_defaults(func=_cmd_scan_list)
628
+ snap = sub.add_parser("build", help="build extraction-snapshot.json")
629
+ snap.add_argument("--source-root", required=True, help="the source tree the runner read")
630
+ snap.add_argument("--source-path",
631
+ help="the source root the provenance map records, written as the snapshot's source_root "
632
+ "(default: --source-root)")
633
+ snap.add_argument("--tier", required=True, help="the forge tier the re-index ran at")
634
+ snap.add_argument("--date", required=True, help="the run's timestamp, recorded as extraction_date")
635
+ snap.add_argument("--provenance-map", help="the provenance map: its files are the files in scope")
636
+ snap.add_argument("--extraction", help="the JSON skf-extract-public-api.py --mode full wrote")
637
+ snap.add_argument("--details", action="append",
638
+ help="the exports read by eye: {\"files\": [...], \"exports\": [...]}; repeat per file")
639
+ snap.add_argument("--scope-type",
640
+ help="the skill's scope type: public-api marks each export `public` by the public-surface "
641
+ "rule; any other marks nothing")
642
+ snap.add_argument("-o", "--output", required=True, help="the snapshot to write")
643
+ snap.set_defaults(func=_cmd_build)
644
+ move = sub.add_parser("relocate", help="add the exports the runner found under removed names elsewhere")
645
+ move.add_argument("snapshot", help="extraction-snapshot.json, rewritten in place")
646
+ move.add_argument("--diff", required=True, help="the structural diff whose removed[] names to look for")
647
+ move.add_argument("--extraction", required=True, help="the runner's JSON over the candidate files")
648
+ move.set_defaults(func=_cmd_relocate)
649
+ return parser
650
+
651
+
652
+ def main(argv: list[str] | None = None) -> int:
653
+ args = _build_parser().parse_args(argv)
654
+ try:
655
+ result = args.func(args)
656
+ except SnapshotError as exc:
657
+ print(json.dumps({"status": "error", "error": str(exc)}))
658
+ return EXIT_ERROR
659
+ print(json.dumps(result))
660
+ return 0
661
+
662
+
663
+ if __name__ == "__main__":
664
+ sys.exit(main())