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
@@ -1,5 +1,5 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
4
  # ///
5
5
  """SKF Hash Content — SHA-256 hashing helpers for skill workflows.
@@ -17,13 +17,14 @@ Two patterns recur across the update-skill workflow's stage prose:
17
17
  content hash, compare against stored hash" and classify entries as
18
18
  MODIFIED_FILE / DELETED_FILE / (UNCHANGED). Same hashing primitive,
19
19
  iterated, plus a stat-and-compare. NEW_FILE detection is intentionally
20
- out of scope here — that lives in `skf-detect-scripts-assets.py`.
20
+ out of scope here: that lives in `skf-detect-scripts-assets.py`,
21
+ piped into update-skill's `skf-new-file-diff.py`.
21
22
 
22
23
  3. **[MANUAL]-section integrity** — the update-skill workflow's headline
23
24
  Workflow Rule is "[MANUAL] sections survive regeneration with zero
24
25
  content loss." init.md §5 captures a pre-write inventory of every
25
- `<!-- [MANUAL:name] --> … <!-- [/MANUAL:name] -->` block; write.md §1
26
- (HALT gate) and validate.md Check B verify the post-merge SKILL.md
26
+ `<!-- [MANUAL:name] --> … <!-- [/MANUAL:name] -->` block; merge.md §6b,
27
+ write.md §1 (HALT gate) and write.md §7 verify the merged SKILL.md
27
28
  against it. Doing that by an LLM marker-count + eyeball can silently
28
29
  pass a block whose interior was truncated (marker count unchanged).
29
30
  The `manual-inventory` / `manual-verify` subcommands extract each block
@@ -49,7 +50,11 @@ Subcommands:
49
50
  "stats": {"total": N, "unchanged": U, "modified": M, "deleted": D}
50
51
  }
51
52
  The provenance file must contain `file_entries` as a top-level array
52
- OR be the array itself (handles both schemas).
53
+ OR be the array itself (handles both schemas). A map with no
54
+ `file_entries` field, or a null one, tracks no file (a skill with no
55
+ script, asset or promoted document, or a map written before the
56
+ field existed): it compares no row. A `file_entries` that is neither
57
+ null nor an array is an error.
53
58
 
54
59
  manual-inventory <skill-md>
55
60
  Extract every `<!-- [MANUAL:name] --> … <!-- [/MANUAL:name] -->` block
@@ -82,17 +87,41 @@ Subcommands:
82
87
  The inventory may be the object emitted by `manual-inventory` OR a bare
83
88
  `blocks[]` array (handles both shapes).
84
89
 
90
+ manual-inventory-amend --inventory <inventory.json> --plan <plan.json>
91
+ --output <amended-inventory.json>
92
+ Apply the user's [MANUAL] conflict decisions to a captured inventory,
93
+ so the post-merge check verifies what the user approved instead of
94
+ the pre-merge blocks. The plan is
95
+ {"decisions": [
96
+ {"name": "...", "action": "keep" | "remove" | "edit",
97
+ "content_file": "<path>" | "content": "<text>"}, ...]}
98
+ `remove` (an [R]emove or an orphan the user removed) drops every
99
+ block of that name; `edit` (an [E]dit) rehashes the one block of that
100
+ name from the approved interior: the bytes of `content_file` (a path
101
+ relative to the plan's folder, or absolute), or `content` encoded as
102
+ UTF-8, exactly the bytes the merged file holds between the markers,
103
+ newlines included. `keep` and a block the plan does not name stay as
104
+ captured. Writes the amended inventory ({blocks[], count}, the shape
105
+ `manual-inventory` emits) to --output through a temporary file and a
106
+ rename, then prints
107
+ {"output": "...", "count": N, "removed": [names],
108
+ "edited": [names], "kept": [names]}
109
+ A plan with no decisions copies the inventory. A plan that names a
110
+ block the inventory does not hold, decides one name twice, edits a
111
+ name that several blocks share, or gives an edit no content (or both
112
+ forms) is refused with exit 1 and nothing written.
113
+
85
114
  compare-constituent-hashes <provenance-map.json> [--skills-root <root>]
86
115
  Constituent-drift detection for compose-mode stack skills. Replaces the
87
116
  per-constituent read + SHA-256 + compare loop at
88
- `src/skf-audit-skill/references/init.md` (Stack Skill Detection). Reads
89
- the `constituents[]` array from a compose-mode provenance map (each
90
- entry carrying `skill_name`, `skill_path`, and the compile-time
91
- `metadata_hash`), reads each constituent's live
92
- `{skill_path}/active/{skill_name}/metadata.json`, recomputes its
93
- SHA-256 with the same `sha256:` prefix convention the writer used
94
- (`skf-enumerate-stack-skills._sha256_of_bytes`), and buckets each
95
- constituent:
117
+ `src/skf-audit-skill/references/constituent-freshness.md` (step 1c,
118
+ Constituent Freshness). Reads the `constituents[]` array from a
119
+ compose-mode provenance map (each entry carrying `skill_name`,
120
+ `skill_path`, and the compile-time `metadata_hash`), reads each
121
+ constituent's live `{skill_path}/active/{skill_name}/metadata.json`,
122
+ recomputes its SHA-256 with the same `sha256:` prefix convention the
123
+ writer used (`skf-enumerate-stack-skills._sha256_of_bytes`), and
124
+ buckets each constituent:
96
125
  {
97
126
  "drifted": [{"skill_name","skill_path","stored_hash","current_hash"}],
98
127
  "fresh": [{"skill_name"}],
@@ -126,6 +155,8 @@ CLI examples:
126
155
  --provenance-map /path/to/provenance-map.json
127
156
  uv run skf-hash-content.py manual-inventory SKILL.md
128
157
  uv run skf-hash-content.py manual-verify --inventory inventory.json SKILL.md
158
+ uv run skf-hash-content.py manual-inventory-amend --inventory inventory.json \\
159
+ --plan manual-plan.json --output amended-inventory.json
129
160
 
130
161
  Exit codes:
131
162
  0 — operation succeeded (including: file in compare missing on disk, its
@@ -212,11 +243,14 @@ def load_file_entries(provenance_path: Path) -> list[dict]:
212
243
  """Extract the file_entries list from a provenance file.
213
244
 
214
245
  Accepts two shapes:
215
- - top-level object with a `file_entries` key (canonical)
246
+ - top-level object with a `file_entries` key (canonical); with no
247
+ such key, or a null one, the map tracks no file and the list is
248
+ empty
216
249
  - top-level array of entries (already-extracted)
217
250
 
218
- Returns a copy of the list. Raises ValueError on malformed JSON or
219
- missing key.
251
+ Returns a copy of the list. Raises ValueError on malformed JSON, a
252
+ `file_entries` that is not an array, or a top level that is neither
253
+ an object nor an array.
220
254
  """
221
255
  try:
222
256
  data = json.loads(provenance_path.read_text(encoding="utf-8"))
@@ -228,9 +262,7 @@ def load_file_entries(provenance_path: Path) -> list[dict]:
228
262
  if isinstance(data, dict):
229
263
  entries = data.get("file_entries")
230
264
  if entries is None:
231
- raise ValueError(
232
- f"provenance file {provenance_path} has no `file_entries` field"
233
- )
265
+ return [] # no tracked file: a map without (or with a null) file_entries
234
266
  if not isinstance(entries, list):
235
267
  raise ValueError(
236
268
  f"`file_entries` in {provenance_path} is not an array"
@@ -466,6 +498,102 @@ def manual_verify(inventory_path: Path, skill_md_path: Path) -> dict:
466
498
  return classify_manual_blocks(inv_blocks, cur_blocks)
467
499
 
468
500
 
501
+ _PLAN_ACTIONS = ("keep", "remove", "edit")
502
+
503
+
504
+ def _load_plan(plan_path: Path) -> list[dict]:
505
+ """The decisions[] of a manual plan. Raises ValueError on malformed input."""
506
+ try:
507
+ data = json.loads(plan_path.read_text(encoding="utf-8"))
508
+ except (json.JSONDecodeError, OSError) as exc:
509
+ raise ValueError(f"failed to read plan file {plan_path}: {exc}") from exc
510
+ if not isinstance(data, dict) or not isinstance(data.get("decisions"), list):
511
+ raise ValueError(f"plan file {plan_path} must be an object with a `decisions` array")
512
+ decisions = data["decisions"]
513
+ for d in decisions:
514
+ if not isinstance(d, dict) or not isinstance(d.get("name"), str) or not d["name"].strip():
515
+ raise ValueError(f"every decision in {plan_path} needs a non-empty `name`: {d!r}")
516
+ if d.get("action") not in _PLAN_ACTIONS:
517
+ raise ValueError(f"decision for {d['name']!r}: `action` must be one of "
518
+ f"{', '.join(_PLAN_ACTIONS)}, got {d.get('action')!r}")
519
+ return decisions
520
+
521
+
522
+ def _edited_interior(decision: dict, plan_dir: Path) -> bytes:
523
+ """The approved interior bytes of an `edit` decision."""
524
+ has_file, has_text = "content_file" in decision, "content" in decision
525
+ if has_file == has_text:
526
+ raise ValueError(f"edit of {decision['name']!r} needs exactly one of `content_file` and `content`")
527
+ if has_text:
528
+ if not isinstance(decision["content"], str):
529
+ raise ValueError(f"edit of {decision['name']!r}: `content` must be a string")
530
+ return decision["content"].encode("utf-8")
531
+ raw = decision["content_file"]
532
+ if not isinstance(raw, str) or not raw:
533
+ raise ValueError(f"edit of {decision['name']!r}: `content_file` must be a path")
534
+ path = Path(raw)
535
+ if not path.is_absolute():
536
+ path = plan_dir / path
537
+ try:
538
+ return path.read_bytes()
539
+ except OSError as exc:
540
+ raise ValueError(f"edit of {decision['name']!r}: cannot read {path}: {exc}") from exc
541
+
542
+
543
+ def amend_inventory(inv_blocks: list[dict], decisions: list[dict], plan_dir: Path) -> tuple[list[dict], dict]:
544
+ """The inventory blocks with the plan's decisions applied, and what changed.
545
+
546
+ Pure apart from reading each edit's content file. Raises ValueError for
547
+ a plan the inventory cannot take (see the module docstring).
548
+ """
549
+ names = [b.get("name") for b in inv_blocks]
550
+ seen: set[str] = set()
551
+ by_name: dict[str, dict] = {}
552
+ for d in decisions:
553
+ name = d["name"]
554
+ if name in seen:
555
+ raise ValueError(f"the plan decides {name!r} twice")
556
+ seen.add(name)
557
+ if name not in names:
558
+ raise ValueError(f"the plan names {name!r}, which the inventory does not hold")
559
+ if d["action"] == "edit" and names.count(name) > 1:
560
+ raise ValueError(f"the plan edits {name!r}, which {names.count(name)} blocks share")
561
+ by_name[name] = d
562
+ out: list[dict] = []
563
+ for block in inv_blocks:
564
+ decision = by_name.get(block.get("name"))
565
+ if decision is None or decision["action"] == "keep":
566
+ out.append(dict(block))
567
+ elif decision["action"] == "edit":
568
+ out.append({**block, "content_hash": sha256_of_bytes(_edited_interior(decision, plan_dir))})
569
+ changes = {action: [d["name"] for d in decisions if d["action"] == key]
570
+ for action, key in (("removed", "remove"), ("edited", "edit"), ("kept", "keep"))}
571
+ return out, changes
572
+
573
+
574
+ def _write_atomic(path: Path, text: str) -> None:
575
+ """Write `text` to `path` through a temporary file and one rename."""
576
+ tmp = path.with_name(f".{path.name}.skf-tmp")
577
+ try:
578
+ tmp.write_bytes(text.encode("utf-8"))
579
+ tmp.replace(path)
580
+ except OSError:
581
+ try:
582
+ tmp.unlink()
583
+ except OSError:
584
+ pass
585
+ raise
586
+
587
+
588
+ def manual_inventory_amend(inventory_path: Path, plan_path: Path, output_path: Path) -> dict:
589
+ """Write the amended inventory to `output_path` and return the summary."""
590
+ inv_blocks = load_inventory_blocks(inventory_path)
591
+ decisions = _load_plan(plan_path)
592
+ blocks, changes = amend_inventory(inv_blocks, decisions, plan_path.parent)
593
+ _write_atomic(output_path, json.dumps({"blocks": blocks, "count": len(blocks)}, indent=2) + "\n")
594
+ return {"output": output_path.as_posix(), "count": len(blocks), **changes}
595
+
596
+
469
597
  # --------------------------------------------------------------------------
470
598
  # Constituent-hash comparison (compose-mode stack drift)
471
599
  # --------------------------------------------------------------------------
@@ -516,7 +644,8 @@ def load_constituents(provenance_path: Path) -> list[dict]:
516
644
  def _constituent_metadata_path(skills_root: Path, skill_path: str, skill_name: str) -> Path:
517
645
  """Resolve a constituent's live metadata.json path.
518
646
 
519
- Mirrors init.md's Stack Skill Detection prose:
647
+ Mirrors the by-hand fallback of constituent-freshness.md (audit-skill
648
+ step 1c):
520
649
  `{skill_path}/active/{skill_name}/metadata.json`. A relative `skill_path`
521
650
  resolves against `skills_root`; an absolute one is used as-is.
522
651
  """
@@ -675,6 +804,28 @@ def _cmd_manual_verify(args: argparse.Namespace) -> int:
675
804
  return 0
676
805
 
677
806
 
807
+ def _cmd_manual_inventory_amend(args: argparse.Namespace) -> int:
808
+ inventory, plan, output = Path(args.inventory), Path(args.plan), Path(args.output)
809
+ for label, path in (("inventory", inventory), ("plan", plan)):
810
+ if not path.is_file():
811
+ print(f"error: {label} not found: {path}", file=sys.stderr)
812
+ return 1
813
+ if not output.parent.is_dir():
814
+ print(f"error: output folder not found: {output.parent}", file=sys.stderr)
815
+ return 1
816
+ try:
817
+ result = manual_inventory_amend(inventory, plan, output)
818
+ except ValueError as exc:
819
+ print(f"error: {exc}", file=sys.stderr)
820
+ return 1
821
+ except OSError as exc:
822
+ print(f"error: cannot write {output}: {exc}", file=sys.stderr)
823
+ return 1
824
+ json.dump(result, sys.stdout, indent=2)
825
+ sys.stdout.write("\n")
826
+ return 0
827
+
828
+
678
829
  def _cmd_compare_constituents(args: argparse.Namespace) -> int:
679
830
  provenance = Path(args.provenance_map)
680
831
  skills_root = Path(args.skills_root)
@@ -741,6 +892,15 @@ def _build_parser() -> argparse.ArgumentParser:
741
892
  )
742
893
  p_ver.set_defaults(func=_cmd_manual_verify)
743
894
 
895
+ p_amend = sub.add_parser(
896
+ "manual-inventory-amend",
897
+ help="apply the user's [MANUAL] remove and edit decisions to a captured inventory",
898
+ )
899
+ p_amend.add_argument("--inventory", required=True, help="the manual-inventory JSON captured before the merge")
900
+ p_amend.add_argument("--plan", required=True, help="the plan JSON holding the user's decisions")
901
+ p_amend.add_argument("--output", required=True, help="where to write the amended inventory JSON")
902
+ p_amend.set_defaults(func=_cmd_manual_inventory_amend)
903
+
744
904
  p_con = sub.add_parser(
745
905
  "compare-constituent-hashes",
746
906
  help="classify compose-mode stack constituents as drifted/fresh/missing",
@@ -1,5 +1,5 @@
1
1
  # /// script
2
- # requires-python = ">=3.9"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
4
  # ///
5
5
  """SKF Language Corpora — canonical companion prose corpora for a language.
@@ -1,46 +1,52 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
4
  # ///
5
5
  """SKF Load Provenance — normalize provenance-map.json into deterministic projections.
6
6
 
7
- Two places in `skf-audit-skill` walk the same provenance map to extract
8
- identical deterministic projections:
7
+ `skf-audit-skill` reads one provenance map in several steps, and each needs
8
+ a projection of it with one correct answer:
9
9
 
10
10
  1. **init.md §4 Load Provenance Map + Stack Skill Detection** — extracts
11
11
  `source_root`, `baseline_commit` (`source_commit`), `baseline_ref`
12
- (`source_ref`), and detects stack-skill flags (`provenance_version` and
13
- top-level `libraries` for legacy v1 stacks).
14
-
15
- 2. **structural-diff.md §1 Prepare Comparison Sets** — re-walks the map to
16
- build a `reexport_map` from any `__init__.py` re-export mapping recorded
17
- in the provenance map, used to canonicalize public-API renames before
18
- diffing.
19
-
20
- Both files also reference the **bounded scan list** — the union of
21
- `entries[].source_file` and `file_entries[].source_file` — which `re-index.md`
22
- §2 consumes to constrain its scan. Collecting that list once at provenance
23
- load time and surfacing it as a normalized field removes ~80 lines of
24
- LLM walk-and-extract prose from the audit-skill references.
12
+ (`source_ref`), detects stack-skill flags (`provenance_version` and
13
+ top-level `libraries` for legacy v1 stacks), and takes the baseline
14
+ facts init.md §7 shows (export count, generation time and age).
15
+
16
+ 2. **re-index.md §2 and §3**: the **bounded scan list** (the union of
17
+ `entries[].source_file` and `file_entries[].source_file`), which the
18
+ recipe runner reads, and for a stack the library of each file
19
+ (`source_library_by_file`), which skf-extraction-snapshot.py gives each
20
+ export of the snapshot it builds (it loads this script beside it).
21
+
22
+ skf-structural-diff.py derives the re-export map from the provenance map
23
+ itself and applies the quote-style, stdlib-prefix and re-export transforms
24
+ on both sides (structural-diff.md §1), so no step carries `reexport_map`
25
+ from here; the field stays for a caller that passes it to the diff as
26
+ `--reexport-map`. The diff also takes a semantic kind a by-eye read
27
+ records as the runner's base kind for the same declaration (its
28
+ export-type comparison).
25
29
 
26
30
  The transforms here are pure projections — no I/O against the source tree,
27
- no comparisons. The script reads one JSON, emits one JSON. The downstream
28
- canonicalization transforms in structural-diff.md §1 (quote-style on string
29
- defaults, stdlib module qualification on signature components) STAY in the
30
- LLM prose because they require per-signature judgment; this script handles
31
- only the deterministic projections.
31
+ no comparisons. The script reads one JSON, emits one JSON.
32
32
 
33
33
  Subcommand:
34
- normalize <map.json>
34
+ normalize <map.json> [--now <ISO-8601>]
35
35
  Emit JSON:
36
36
  {
37
37
  "bounded_scan_files": ["<rel-path, forward-slash>", ...],
38
38
  "is_stack_skill": <bool>,
39
39
  "legacy_stack_provenance": <bool>,
40
+ "compose_mode_stack": <bool>,
40
41
  "source_root": "<path or null>",
41
42
  "baseline_commit": "<sha or null>",
42
43
  "baseline_ref": "<ref or null>",
43
- "reexport_map": {"<from-internal>": "<to-public>"}
44
+ "reexport_map": {"<from-internal>": "<to-public>"},
45
+ "source_library_by_file": {"<rel-path>": "<library>"},
46
+ "export_count": <int>,
47
+ "generated_at": "<YYYY-MM-DDTHH:MM:SSZ or null>",
48
+ "age_days": <int or null>,
49
+ "age_source": "generated_at" | "mtime" | null
44
50
  }
45
51
 
46
52
  Bounded scan = union of `entries[].source_file` and
@@ -55,6 +61,17 @@ Subcommand:
55
61
  Note: detecting `is_stack_skill` for a v1 map by the presence of
56
62
  `libraries` alone (the documented behavior in init.md) is also
57
63
  honored — see condition handling in `detect_stack_flags`.
64
+ - `compose_mode_stack` is true for a stack whose map holds a
65
+ `constituents` list: the compose-mode variant create-stack-skill
66
+ writes, whose entries come from constituent skills rather than a
67
+ source tree. Audit-skill checks its constituents' freshness
68
+ instead of re-indexing a source.
69
+
70
+ File-to-library map: each `entries[].source_file` (forward slashes)
71
+ with the `source_library` of its entries, for a stack's per-library
72
+ diff. A file whose entries name no library is left out, and when its
73
+ entries name two, the first one's wins. Empty for a single skill
74
+ whose entries carry no `source_library`.
58
75
 
59
76
  Re-export map: read `reexport_map` field directly from the provenance
60
77
  map if it exists (writer-side captures __init__.py walk results there).
@@ -62,8 +79,18 @@ Subcommand:
62
79
  entries — older provenance writers may have used that shape. Empty
63
80
  object when neither is present.
64
81
 
82
+ Baseline facts for init.md §7 (the audit baseline summary):
83
+ - `export_count` is the number of `entries[]` objects.
84
+ - `generated_at` is the map's `generated_at` in UTC, whatever zone
85
+ it was written in; null when absent or not an ISO-8601 date-time.
86
+ - `age_days` is the whole days from `generated_at` to now (never
87
+ below 0), or from the file's modification time when the map has
88
+ no usable `generated_at` (`age_source` says which; null when
89
+ neither is known). `--now` fixes "now" for a reproducible run.
90
+
65
91
  CLI examples:
66
92
  uv run skf-load-provenance.py normalize /path/to/provenance-map.json
93
+ uv run skf-load-provenance.py normalize /path/to/provenance-map.json --now 2026-05-01T00:00:00Z
67
94
 
68
95
  Exit codes:
69
96
  0 — normalization succeeded (including empty / well-formed map with no
@@ -75,7 +102,9 @@ from __future__ import annotations
75
102
 
76
103
  import argparse
77
104
  import json
105
+ import re
78
106
  import sys
107
+ from datetime import datetime, timedelta, timezone
79
108
  from pathlib import Path
80
109
 
81
110
 
@@ -141,6 +170,25 @@ def bounded_scan_files(data: dict) -> list[str]:
141
170
  return sorted(paths)
142
171
 
143
172
 
173
+ def source_library_by_file(data: dict) -> dict[str, str]:
174
+ """{source_file: source_library} from `entries[]`, paths forward-slash.
175
+
176
+ The first library named for a file wins; entries without a string
177
+ `source_library` add nothing.
178
+ """
179
+ out: dict[str, str] = {}
180
+ entries = data.get("entries")
181
+ if not isinstance(entries, list):
182
+ return out
183
+ for entry in entries:
184
+ if not isinstance(entry, dict):
185
+ continue
186
+ sf, library = entry.get("source_file"), entry.get("source_library")
187
+ if isinstance(sf, str) and sf and isinstance(library, str) and library:
188
+ out.setdefault(_posix(sf), library)
189
+ return dict(sorted(out.items()))
190
+
191
+
144
192
  def _normalize_version(v: object) -> tuple[int, ...] | None:
145
193
  """Parse a provenance_version string like "2.0" / "1.5" into a tuple.
146
194
 
@@ -226,7 +274,77 @@ def extract_reexport_map(data: dict) -> dict[str, str]:
226
274
  return out
227
275
 
228
276
 
229
- def normalize(data: dict) -> dict:
277
+ ISO_TIME_RE = re.compile(
278
+ r"(\d{4})-(\d{2})-(\d{2})"
279
+ r"(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d+))?)?)?"
280
+ r"\s*([Zz]|[+-]\d{2}(?::?\d{2})?)?")
281
+
282
+
283
+ # Keep identical to _parse_iso_utc in skf-skill-inventory.py
284
+ # (test/test-skf-skill-inventory.py pins the copies).
285
+ def _parse_iso_utc(value):
286
+ """An ISO-8601 date or date-time as an aware UTC datetime, else None.
287
+
288
+ A time without a zone is read as UTC, a date alone as its midnight UTC.
289
+ """
290
+ if not isinstance(value, str):
291
+ return None
292
+ m = ISO_TIME_RE.fullmatch(value.strip())
293
+ if not m:
294
+ return None
295
+ year, month, day, hour, minute, second, fraction, zone = m.groups()
296
+ offset = timedelta(0)
297
+ if zone and zone not in ("Z", "z"):
298
+ digits = zone[1:].replace(":", "")
299
+ offset = timedelta(hours=int(digits[:2]), minutes=int(digits[2:] or 0))
300
+ if zone[0] == "-":
301
+ offset = -offset
302
+ try:
303
+ moment = datetime(int(year), int(month), int(day), int(hour or 0), int(minute or 0),
304
+ int(second or 0), int((fraction or "0")[:6].ljust(6, "0")))
305
+ return (moment - offset).replace(tzinfo=timezone.utc)
306
+ except (ValueError, OverflowError): # no such date, or out of datetime's range
307
+ return None
308
+
309
+
310
+ def export_count(data: dict) -> int:
311
+ """The number of `entries[]` objects (the exports the map records)."""
312
+ entries = data.get("entries")
313
+ if not isinstance(entries, list):
314
+ return 0
315
+ return sum(1 for entry in entries if isinstance(entry, dict))
316
+
317
+
318
+ def provenance_age(
319
+ data: dict, mtime: datetime | None = None, now: datetime | None = None
320
+ ) -> dict:
321
+ """{generated_at, age_days, age_source} for the audit baseline.
322
+
323
+ `generated_at` is the map's field in UTC (null when absent or not
324
+ ISO-8601). The age counts whole days from it to `now` (the current time
325
+ when None), else from `mtime` (the file's modification time), never
326
+ below 0.
327
+ """
328
+ generated = _parse_iso_utc(data.get("generated_at"))
329
+ if generated is not None:
330
+ since, source = generated, "generated_at"
331
+ elif mtime is not None:
332
+ since, source = mtime, "mtime"
333
+ else:
334
+ since, source = None, None
335
+ age_days = None
336
+ if since is not None:
337
+ age_days = max(0, ((now or datetime.now(timezone.utc)) - since).days)
338
+ return {
339
+ "generated_at": generated.strftime("%Y-%m-%dT%H:%M:%SZ") if generated else None,
340
+ "age_days": age_days,
341
+ "age_source": source,
342
+ }
343
+
344
+
345
+ def normalize(
346
+ data: dict, mtime: datetime | None = None, now: datetime | None = None
347
+ ) -> dict:
230
348
  """Build the full normalized projection record."""
231
349
  is_stack, legacy = detect_stack_flags(data)
232
350
  source_root = data.get("source_root") if isinstance(data.get("source_root"), str) else None
@@ -240,10 +358,14 @@ def normalize(data: dict) -> dict:
240
358
  "bounded_scan_files": bounded_scan_files(data),
241
359
  "is_stack_skill": is_stack,
242
360
  "legacy_stack_provenance": legacy,
361
+ "compose_mode_stack": is_stack and isinstance(data.get("constituents"), list),
243
362
  "source_root": source_root,
244
363
  "baseline_commit": baseline_commit,
245
364
  "baseline_ref": baseline_ref,
246
365
  "reexport_map": extract_reexport_map(data),
366
+ "source_library_by_file": source_library_by_file(data),
367
+ "export_count": export_count(data),
368
+ **provenance_age(data, mtime, now),
247
369
  }
248
370
 
249
371
 
@@ -257,12 +379,19 @@ def _cmd_normalize(args: argparse.Namespace) -> int:
257
379
  if not path.is_file():
258
380
  print(f"error: provenance map not found: {path}", file=sys.stderr)
259
381
  return 1
382
+ now = None
383
+ if args.now is not None:
384
+ now = _parse_iso_utc(args.now)
385
+ if now is None:
386
+ print(f"error: --now is not an ISO-8601 date-time: {args.now}", file=sys.stderr)
387
+ return 1
260
388
  try:
261
389
  data = load_provenance(path)
262
- except ValueError as exc:
390
+ mtime = datetime.fromtimestamp(path.stat().st_mtime, timezone.utc)
391
+ except (ValueError, OSError) as exc:
263
392
  print(f"error: {exc}", file=sys.stderr)
264
393
  return 1
265
- result = normalize(data)
394
+ result = normalize(data, mtime=mtime, now=now)
266
395
  json.dump(result, sys.stdout, indent=2)
267
396
  sys.stdout.write("\n")
268
397
  return 0
@@ -273,7 +402,8 @@ def _build_parser() -> argparse.ArgumentParser:
273
402
  prog="skf-load-provenance",
274
403
  description=(
275
404
  "Normalize provenance-map.json into deterministic projections "
276
- "(bounded scan, stack flags, source_root/commit/ref, reexport map)."
405
+ "(bounded scan, stack flags, source_root/commit/ref, reexport map, "
406
+ "file-to-library map, export count, generated_at and age)."
277
407
  ),
278
408
  )
279
409
  sub = parser.add_subparsers(dest="cmd", required=True)
@@ -281,6 +411,10 @@ def _build_parser() -> argparse.ArgumentParser:
281
411
  "normalize", help="emit normalized projection JSON from a provenance map"
282
412
  )
283
413
  p_norm.add_argument("provenance_map", help="path to provenance-map.json")
414
+ p_norm.add_argument(
415
+ "--now",
416
+ help="ISO-8601 date-time that age_days counts to (default: the current time)",
417
+ )
284
418
  p_norm.set_defaults(func=_cmd_normalize)
285
419
  return parser
286
420
 
@@ -1,5 +1,5 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
4
  # ///
5
5
  """SKF Manifest Ops — CRUD operations on the export manifest.