bmad-module-skill-forge 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +15 -5
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -90
  348. package/src/skf-setup/references/detect-and-tier.md +58 -67
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -679
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -1,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.
@@ -22,8 +22,8 @@ Two patterns recur across the update-skill workflow's stage prose:
22
22
  3. **[MANUAL]-section integrity** — the update-skill workflow's headline
23
23
  Workflow Rule is "[MANUAL] sections survive regeneration with zero
24
24
  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
25
+ `<!-- [MANUAL:name] --> … <!-- [/MANUAL:name] -->` block; merge.md §6b,
26
+ write.md §1 (HALT gate) and write.md §7 verify the merged SKILL.md
27
27
  against it. Doing that by an LLM marker-count + eyeball can silently
28
28
  pass a block whose interior was truncated (marker count unchanged).
29
29
  The `manual-inventory` / `manual-verify` subcommands extract each block
@@ -82,17 +82,41 @@ Subcommands:
82
82
  The inventory may be the object emitted by `manual-inventory` OR a bare
83
83
  `blocks[]` array (handles both shapes).
84
84
 
85
+ manual-inventory-amend --inventory <inventory.json> --plan <plan.json>
86
+ --output <amended-inventory.json>
87
+ Apply the user's [MANUAL] conflict decisions to a captured inventory,
88
+ so the post-merge check verifies what the user approved instead of
89
+ the pre-merge blocks. The plan is
90
+ {"decisions": [
91
+ {"name": "...", "action": "keep" | "remove" | "edit",
92
+ "content_file": "<path>" | "content": "<text>"}, ...]}
93
+ `remove` (an [R]emove or an orphan the user removed) drops every
94
+ block of that name; `edit` (an [E]dit) rehashes the one block of that
95
+ name from the approved interior: the bytes of `content_file` (a path
96
+ relative to the plan's folder, or absolute), or `content` encoded as
97
+ UTF-8, exactly the bytes the merged file holds between the markers,
98
+ newlines included. `keep` and a block the plan does not name stay as
99
+ captured. Writes the amended inventory ({blocks[], count}, the shape
100
+ `manual-inventory` emits) to --output through a temporary file and a
101
+ rename, then prints
102
+ {"output": "...", "count": N, "removed": [names],
103
+ "edited": [names], "kept": [names]}
104
+ A plan with no decisions copies the inventory. A plan that names a
105
+ block the inventory does not hold, decides one name twice, edits a
106
+ name that several blocks share, or gives an edit no content (or both
107
+ forms) is refused with exit 1 and nothing written.
108
+
85
109
  compare-constituent-hashes <provenance-map.json> [--skills-root <root>]
86
110
  Constituent-drift detection for compose-mode stack skills. Replaces the
87
111
  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:
112
+ `src/skf-audit-skill/references/constituent-freshness.md` (step 1c,
113
+ Constituent Freshness). Reads the `constituents[]` array from a
114
+ compose-mode provenance map (each entry carrying `skill_name`,
115
+ `skill_path`, and the compile-time `metadata_hash`), reads each
116
+ constituent's live `{skill_path}/active/{skill_name}/metadata.json`,
117
+ recomputes its SHA-256 with the same `sha256:` prefix convention the
118
+ writer used (`skf-enumerate-stack-skills._sha256_of_bytes`), and
119
+ buckets each constituent:
96
120
  {
97
121
  "drifted": [{"skill_name","skill_path","stored_hash","current_hash"}],
98
122
  "fresh": [{"skill_name"}],
@@ -126,6 +150,8 @@ CLI examples:
126
150
  --provenance-map /path/to/provenance-map.json
127
151
  uv run skf-hash-content.py manual-inventory SKILL.md
128
152
  uv run skf-hash-content.py manual-verify --inventory inventory.json SKILL.md
153
+ uv run skf-hash-content.py manual-inventory-amend --inventory inventory.json \\
154
+ --plan manual-plan.json --output amended-inventory.json
129
155
 
130
156
  Exit codes:
131
157
  0 — operation succeeded (including: file in compare missing on disk, its
@@ -466,6 +492,102 @@ def manual_verify(inventory_path: Path, skill_md_path: Path) -> dict:
466
492
  return classify_manual_blocks(inv_blocks, cur_blocks)
467
493
 
468
494
 
495
+ _PLAN_ACTIONS = ("keep", "remove", "edit")
496
+
497
+
498
+ def _load_plan(plan_path: Path) -> list[dict]:
499
+ """The decisions[] of a manual plan. Raises ValueError on malformed input."""
500
+ try:
501
+ data = json.loads(plan_path.read_text(encoding="utf-8"))
502
+ except (json.JSONDecodeError, OSError) as exc:
503
+ raise ValueError(f"failed to read plan file {plan_path}: {exc}") from exc
504
+ if not isinstance(data, dict) or not isinstance(data.get("decisions"), list):
505
+ raise ValueError(f"plan file {plan_path} must be an object with a `decisions` array")
506
+ decisions = data["decisions"]
507
+ for d in decisions:
508
+ if not isinstance(d, dict) or not isinstance(d.get("name"), str) or not d["name"].strip():
509
+ raise ValueError(f"every decision in {plan_path} needs a non-empty `name`: {d!r}")
510
+ if d.get("action") not in _PLAN_ACTIONS:
511
+ raise ValueError(f"decision for {d['name']!r}: `action` must be one of "
512
+ f"{', '.join(_PLAN_ACTIONS)}, got {d.get('action')!r}")
513
+ return decisions
514
+
515
+
516
+ def _edited_interior(decision: dict, plan_dir: Path) -> bytes:
517
+ """The approved interior bytes of an `edit` decision."""
518
+ has_file, has_text = "content_file" in decision, "content" in decision
519
+ if has_file == has_text:
520
+ raise ValueError(f"edit of {decision['name']!r} needs exactly one of `content_file` and `content`")
521
+ if has_text:
522
+ if not isinstance(decision["content"], str):
523
+ raise ValueError(f"edit of {decision['name']!r}: `content` must be a string")
524
+ return decision["content"].encode("utf-8")
525
+ raw = decision["content_file"]
526
+ if not isinstance(raw, str) or not raw:
527
+ raise ValueError(f"edit of {decision['name']!r}: `content_file` must be a path")
528
+ path = Path(raw)
529
+ if not path.is_absolute():
530
+ path = plan_dir / path
531
+ try:
532
+ return path.read_bytes()
533
+ except OSError as exc:
534
+ raise ValueError(f"edit of {decision['name']!r}: cannot read {path}: {exc}") from exc
535
+
536
+
537
+ def amend_inventory(inv_blocks: list[dict], decisions: list[dict], plan_dir: Path) -> tuple[list[dict], dict]:
538
+ """The inventory blocks with the plan's decisions applied, and what changed.
539
+
540
+ Pure apart from reading each edit's content file. Raises ValueError for
541
+ a plan the inventory cannot take (see the module docstring).
542
+ """
543
+ names = [b.get("name") for b in inv_blocks]
544
+ seen: set[str] = set()
545
+ by_name: dict[str, dict] = {}
546
+ for d in decisions:
547
+ name = d["name"]
548
+ if name in seen:
549
+ raise ValueError(f"the plan decides {name!r} twice")
550
+ seen.add(name)
551
+ if name not in names:
552
+ raise ValueError(f"the plan names {name!r}, which the inventory does not hold")
553
+ if d["action"] == "edit" and names.count(name) > 1:
554
+ raise ValueError(f"the plan edits {name!r}, which {names.count(name)} blocks share")
555
+ by_name[name] = d
556
+ out: list[dict] = []
557
+ for block in inv_blocks:
558
+ decision = by_name.get(block.get("name"))
559
+ if decision is None or decision["action"] == "keep":
560
+ out.append(dict(block))
561
+ elif decision["action"] == "edit":
562
+ out.append({**block, "content_hash": sha256_of_bytes(_edited_interior(decision, plan_dir))})
563
+ changes = {action: [d["name"] for d in decisions if d["action"] == key]
564
+ for action, key in (("removed", "remove"), ("edited", "edit"), ("kept", "keep"))}
565
+ return out, changes
566
+
567
+
568
+ def _write_atomic(path: Path, text: str) -> None:
569
+ """Write `text` to `path` through a temporary file and one rename."""
570
+ tmp = path.with_name(f".{path.name}.skf-tmp")
571
+ try:
572
+ tmp.write_bytes(text.encode("utf-8"))
573
+ tmp.replace(path)
574
+ except OSError:
575
+ try:
576
+ tmp.unlink()
577
+ except OSError:
578
+ pass
579
+ raise
580
+
581
+
582
+ def manual_inventory_amend(inventory_path: Path, plan_path: Path, output_path: Path) -> dict:
583
+ """Write the amended inventory to `output_path` and return the summary."""
584
+ inv_blocks = load_inventory_blocks(inventory_path)
585
+ decisions = _load_plan(plan_path)
586
+ blocks, changes = amend_inventory(inv_blocks, decisions, plan_path.parent)
587
+ _write_atomic(output_path, json.dumps({"blocks": blocks, "count": len(blocks)}, indent=2) + "\n")
588
+ return {"output": output_path.as_posix(), "count": len(blocks), **changes}
589
+
590
+
469
591
  # --------------------------------------------------------------------------
470
592
  # Constituent-hash comparison (compose-mode stack drift)
471
593
  # --------------------------------------------------------------------------
@@ -516,7 +638,8 @@ def load_constituents(provenance_path: Path) -> list[dict]:
516
638
  def _constituent_metadata_path(skills_root: Path, skill_path: str, skill_name: str) -> Path:
517
639
  """Resolve a constituent's live metadata.json path.
518
640
 
519
- Mirrors init.md's Stack Skill Detection prose:
641
+ Mirrors the by-hand fallback of constituent-freshness.md (audit-skill
642
+ step 1c):
520
643
  `{skill_path}/active/{skill_name}/metadata.json`. A relative `skill_path`
521
644
  resolves against `skills_root`; an absolute one is used as-is.
522
645
  """
@@ -675,6 +798,28 @@ def _cmd_manual_verify(args: argparse.Namespace) -> int:
675
798
  return 0
676
799
 
677
800
 
801
+ def _cmd_manual_inventory_amend(args: argparse.Namespace) -> int:
802
+ inventory, plan, output = Path(args.inventory), Path(args.plan), Path(args.output)
803
+ for label, path in (("inventory", inventory), ("plan", plan)):
804
+ if not path.is_file():
805
+ print(f"error: {label} not found: {path}", file=sys.stderr)
806
+ return 1
807
+ if not output.parent.is_dir():
808
+ print(f"error: output folder not found: {output.parent}", file=sys.stderr)
809
+ return 1
810
+ try:
811
+ result = manual_inventory_amend(inventory, plan, output)
812
+ except ValueError as exc:
813
+ print(f"error: {exc}", file=sys.stderr)
814
+ return 1
815
+ except OSError as exc:
816
+ print(f"error: cannot write {output}: {exc}", file=sys.stderr)
817
+ return 1
818
+ json.dump(result, sys.stdout, indent=2)
819
+ sys.stdout.write("\n")
820
+ return 0
821
+
822
+
678
823
  def _cmd_compare_constituents(args: argparse.Namespace) -> int:
679
824
  provenance = Path(args.provenance_map)
680
825
  skills_root = Path(args.skills_root)
@@ -741,6 +886,15 @@ def _build_parser() -> argparse.ArgumentParser:
741
886
  )
742
887
  p_ver.set_defaults(func=_cmd_manual_verify)
743
888
 
889
+ p_amend = sub.add_parser(
890
+ "manual-inventory-amend",
891
+ help="apply the user's [MANUAL] remove and edit decisions to a captured inventory",
892
+ )
893
+ p_amend.add_argument("--inventory", required=True, help="the manual-inventory JSON captured before the merge")
894
+ p_amend.add_argument("--plan", required=True, help="the plan JSON holding the user's decisions")
895
+ p_amend.add_argument("--output", required=True, help="where to write the amended inventory JSON")
896
+ p_amend.set_defaults(func=_cmd_manual_inventory_amend)
897
+
744
898
  p_con = sub.add_parser(
745
899
  "compare-constituent-hashes",
746
900
  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,50 @@
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`.
25
27
 
26
28
  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.
29
+ no comparisons. The script reads one JSON, emits one JSON.
32
30
 
33
31
  Subcommand:
34
- normalize <map.json>
32
+ normalize <map.json> [--now <ISO-8601>]
35
33
  Emit JSON:
36
34
  {
37
35
  "bounded_scan_files": ["<rel-path, forward-slash>", ...],
38
36
  "is_stack_skill": <bool>,
39
37
  "legacy_stack_provenance": <bool>,
38
+ "compose_mode_stack": <bool>,
40
39
  "source_root": "<path or null>",
41
40
  "baseline_commit": "<sha or null>",
42
41
  "baseline_ref": "<ref or null>",
43
- "reexport_map": {"<from-internal>": "<to-public>"}
42
+ "reexport_map": {"<from-internal>": "<to-public>"},
43
+ "source_library_by_file": {"<rel-path>": "<library>"},
44
+ "export_count": <int>,
45
+ "generated_at": "<YYYY-MM-DDTHH:MM:SSZ or null>",
46
+ "age_days": <int or null>,
47
+ "age_source": "generated_at" | "mtime" | null
44
48
  }
45
49
 
46
50
  Bounded scan = union of `entries[].source_file` and
@@ -55,6 +59,17 @@ Subcommand:
55
59
  Note: detecting `is_stack_skill` for a v1 map by the presence of
56
60
  `libraries` alone (the documented behavior in init.md) is also
57
61
  honored — see condition handling in `detect_stack_flags`.
62
+ - `compose_mode_stack` is true for a stack whose map holds a
63
+ `constituents` list: the compose-mode variant create-stack-skill
64
+ writes, whose entries come from constituent skills rather than a
65
+ source tree. Audit-skill checks its constituents' freshness
66
+ instead of re-indexing a source.
67
+
68
+ File-to-library map: each `entries[].source_file` (forward slashes)
69
+ with the `source_library` of its entries, for a stack's per-library
70
+ diff. A file whose entries name no library is left out, and when its
71
+ entries name two, the first one's wins. Empty for a single skill
72
+ whose entries carry no `source_library`.
58
73
 
59
74
  Re-export map: read `reexport_map` field directly from the provenance
60
75
  map if it exists (writer-side captures __init__.py walk results there).
@@ -62,8 +77,18 @@ Subcommand:
62
77
  entries — older provenance writers may have used that shape. Empty
63
78
  object when neither is present.
64
79
 
80
+ Baseline facts for init.md §7 (the audit baseline summary):
81
+ - `export_count` is the number of `entries[]` objects.
82
+ - `generated_at` is the map's `generated_at` in UTC, whatever zone
83
+ it was written in; null when absent or not an ISO-8601 date-time.
84
+ - `age_days` is the whole days from `generated_at` to now (never
85
+ below 0), or from the file's modification time when the map has
86
+ no usable `generated_at` (`age_source` says which; null when
87
+ neither is known). `--now` fixes "now" for a reproducible run.
88
+
65
89
  CLI examples:
66
90
  uv run skf-load-provenance.py normalize /path/to/provenance-map.json
91
+ uv run skf-load-provenance.py normalize /path/to/provenance-map.json --now 2026-05-01T00:00:00Z
67
92
 
68
93
  Exit codes:
69
94
  0 — normalization succeeded (including empty / well-formed map with no
@@ -75,7 +100,9 @@ from __future__ import annotations
75
100
 
76
101
  import argparse
77
102
  import json
103
+ import re
78
104
  import sys
105
+ from datetime import datetime, timedelta, timezone
79
106
  from pathlib import Path
80
107
 
81
108
 
@@ -141,6 +168,25 @@ def bounded_scan_files(data: dict) -> list[str]:
141
168
  return sorted(paths)
142
169
 
143
170
 
171
+ def source_library_by_file(data: dict) -> dict[str, str]:
172
+ """{source_file: source_library} from `entries[]`, paths forward-slash.
173
+
174
+ The first library named for a file wins; entries without a string
175
+ `source_library` add nothing.
176
+ """
177
+ out: dict[str, str] = {}
178
+ entries = data.get("entries")
179
+ if not isinstance(entries, list):
180
+ return out
181
+ for entry in entries:
182
+ if not isinstance(entry, dict):
183
+ continue
184
+ sf, library = entry.get("source_file"), entry.get("source_library")
185
+ if isinstance(sf, str) and sf and isinstance(library, str) and library:
186
+ out.setdefault(_posix(sf), library)
187
+ return dict(sorted(out.items()))
188
+
189
+
144
190
  def _normalize_version(v: object) -> tuple[int, ...] | None:
145
191
  """Parse a provenance_version string like "2.0" / "1.5" into a tuple.
146
192
 
@@ -226,7 +272,77 @@ def extract_reexport_map(data: dict) -> dict[str, str]:
226
272
  return out
227
273
 
228
274
 
229
- def normalize(data: dict) -> dict:
275
+ ISO_TIME_RE = re.compile(
276
+ r"(\d{4})-(\d{2})-(\d{2})"
277
+ r"(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d+))?)?)?"
278
+ r"\s*([Zz]|[+-]\d{2}(?::?\d{2})?)?")
279
+
280
+
281
+ # Keep identical to _parse_iso_utc in skf-skill-inventory.py
282
+ # (test/test-skf-skill-inventory.py pins the copies).
283
+ def _parse_iso_utc(value):
284
+ """An ISO-8601 date or date-time as an aware UTC datetime, else None.
285
+
286
+ A time without a zone is read as UTC, a date alone as its midnight UTC.
287
+ """
288
+ if not isinstance(value, str):
289
+ return None
290
+ m = ISO_TIME_RE.fullmatch(value.strip())
291
+ if not m:
292
+ return None
293
+ year, month, day, hour, minute, second, fraction, zone = m.groups()
294
+ offset = timedelta(0)
295
+ if zone and zone not in ("Z", "z"):
296
+ digits = zone[1:].replace(":", "")
297
+ offset = timedelta(hours=int(digits[:2]), minutes=int(digits[2:] or 0))
298
+ if zone[0] == "-":
299
+ offset = -offset
300
+ try:
301
+ moment = datetime(int(year), int(month), int(day), int(hour or 0), int(minute or 0),
302
+ int(second or 0), int((fraction or "0")[:6].ljust(6, "0")))
303
+ return (moment - offset).replace(tzinfo=timezone.utc)
304
+ except (ValueError, OverflowError): # no such date, or out of datetime's range
305
+ return None
306
+
307
+
308
+ def export_count(data: dict) -> int:
309
+ """The number of `entries[]` objects (the exports the map records)."""
310
+ entries = data.get("entries")
311
+ if not isinstance(entries, list):
312
+ return 0
313
+ return sum(1 for entry in entries if isinstance(entry, dict))
314
+
315
+
316
+ def provenance_age(
317
+ data: dict, mtime: datetime | None = None, now: datetime | None = None
318
+ ) -> dict:
319
+ """{generated_at, age_days, age_source} for the audit baseline.
320
+
321
+ `generated_at` is the map's field in UTC (null when absent or not
322
+ ISO-8601). The age counts whole days from it to `now` (the current time
323
+ when None), else from `mtime` (the file's modification time), never
324
+ below 0.
325
+ """
326
+ generated = _parse_iso_utc(data.get("generated_at"))
327
+ if generated is not None:
328
+ since, source = generated, "generated_at"
329
+ elif mtime is not None:
330
+ since, source = mtime, "mtime"
331
+ else:
332
+ since, source = None, None
333
+ age_days = None
334
+ if since is not None:
335
+ age_days = max(0, ((now or datetime.now(timezone.utc)) - since).days)
336
+ return {
337
+ "generated_at": generated.strftime("%Y-%m-%dT%H:%M:%SZ") if generated else None,
338
+ "age_days": age_days,
339
+ "age_source": source,
340
+ }
341
+
342
+
343
+ def normalize(
344
+ data: dict, mtime: datetime | None = None, now: datetime | None = None
345
+ ) -> dict:
230
346
  """Build the full normalized projection record."""
231
347
  is_stack, legacy = detect_stack_flags(data)
232
348
  source_root = data.get("source_root") if isinstance(data.get("source_root"), str) else None
@@ -240,10 +356,14 @@ def normalize(data: dict) -> dict:
240
356
  "bounded_scan_files": bounded_scan_files(data),
241
357
  "is_stack_skill": is_stack,
242
358
  "legacy_stack_provenance": legacy,
359
+ "compose_mode_stack": is_stack and isinstance(data.get("constituents"), list),
243
360
  "source_root": source_root,
244
361
  "baseline_commit": baseline_commit,
245
362
  "baseline_ref": baseline_ref,
246
363
  "reexport_map": extract_reexport_map(data),
364
+ "source_library_by_file": source_library_by_file(data),
365
+ "export_count": export_count(data),
366
+ **provenance_age(data, mtime, now),
247
367
  }
248
368
 
249
369
 
@@ -257,12 +377,19 @@ def _cmd_normalize(args: argparse.Namespace) -> int:
257
377
  if not path.is_file():
258
378
  print(f"error: provenance map not found: {path}", file=sys.stderr)
259
379
  return 1
380
+ now = None
381
+ if args.now is not None:
382
+ now = _parse_iso_utc(args.now)
383
+ if now is None:
384
+ print(f"error: --now is not an ISO-8601 date-time: {args.now}", file=sys.stderr)
385
+ return 1
260
386
  try:
261
387
  data = load_provenance(path)
262
- except ValueError as exc:
388
+ mtime = datetime.fromtimestamp(path.stat().st_mtime, timezone.utc)
389
+ except (ValueError, OSError) as exc:
263
390
  print(f"error: {exc}", file=sys.stderr)
264
391
  return 1
265
- result = normalize(data)
392
+ result = normalize(data, mtime=mtime, now=now)
266
393
  json.dump(result, sys.stdout, indent=2)
267
394
  sys.stdout.write("\n")
268
395
  return 0
@@ -273,7 +400,8 @@ def _build_parser() -> argparse.ArgumentParser:
273
400
  prog="skf-load-provenance",
274
401
  description=(
275
402
  "Normalize provenance-map.json into deterministic projections "
276
- "(bounded scan, stack flags, source_root/commit/ref, reexport map)."
403
+ "(bounded scan, stack flags, source_root/commit/ref, reexport map, "
404
+ "file-to-library map, export count, generated_at and age)."
277
405
  ),
278
406
  )
279
407
  sub = parser.add_subparsers(dest="cmd", required=True)
@@ -281,6 +409,10 @@ def _build_parser() -> argparse.ArgumentParser:
281
409
  "normalize", help="emit normalized projection JSON from a provenance map"
282
410
  )
283
411
  p_norm.add_argument("provenance_map", help="path to provenance-map.json")
412
+ p_norm.add_argument(
413
+ "--now",
414
+ help="ISO-8601 date-time that age_days counts to (default: the current time)",
415
+ )
284
416
  p_norm.set_defaults(func=_cmd_normalize)
285
417
  return parser
286
418
 
@@ -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.