bmad-module-skill-forge 1.4.1 → 1.5.1

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 (231) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +0 -8
  3. package/docs/_data/pinned.yaml +1 -1
  4. package/docs/_internal/STABILITY.md +1 -1
  5. package/docs/architecture.md +2 -2
  6. package/docs/examples.md +1 -1
  7. package/docs/workflows.md +5 -5
  8. package/package.json +2 -2
  9. package/src/README.md +1 -1
  10. package/src/knowledge/ccc-bridge.md +12 -12
  11. package/src/knowledge/qmd-registry.md +5 -5
  12. package/src/shared/health-check.md +5 -5
  13. package/src/shared/references/description-guard-protocol.md +100 -0
  14. package/src/shared/references/output-contract-schema.md +1 -1
  15. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -6
  16. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +9 -3
  17. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +149 -0
  18. package/src/shared/scripts/schemas/skill-brief.v1.json +22 -2
  19. package/src/shared/scripts/skf-build-change-manifest.py +420 -0
  20. package/src/shared/scripts/skf-check-workspace-drift.py +321 -0
  21. package/src/shared/scripts/skf-compare-file-hashes.py +357 -0
  22. package/src/shared/scripts/skf-description-guard.py +359 -0
  23. package/src/shared/scripts/skf-detect-language.py +2 -2
  24. package/src/shared/scripts/skf-detect-scripts-assets.py +613 -0
  25. package/src/shared/scripts/skf-detect-tools.py +147 -16
  26. package/src/shared/scripts/skf-detect-workspaces.py +1 -1
  27. package/src/shared/scripts/skf-disqualify-candidates.py +576 -0
  28. package/src/shared/scripts/skf-emit-brief-result-envelope.py +3 -3
  29. package/src/shared/scripts/skf-emit-result-envelope.py +93 -9
  30. package/src/shared/scripts/skf-enumerate-stack-skills.py +514 -0
  31. package/src/shared/scripts/skf-forge-tier-rw.py +11 -11
  32. package/src/shared/scripts/skf-hash-content.py +284 -0
  33. package/src/shared/scripts/skf-load-provenance.py +295 -0
  34. package/src/shared/scripts/skf-manifest-ops.py +22 -6
  35. package/src/shared/scripts/skf-merge-ccc-exclusions.py +34 -4
  36. package/src/shared/scripts/skf-pair-intersect.py +250 -0
  37. package/src/shared/scripts/skf-provenance-gap-dispatch.py +433 -0
  38. package/src/shared/scripts/skf-qmd-classify-collections.py +42 -8
  39. package/src/shared/scripts/skf-recommend-scope-type.py +2 -2
  40. package/src/shared/scripts/skf-render-quick-metadata.py +2 -2
  41. package/src/shared/scripts/skf-resolve-authoritative-files.py +504 -0
  42. package/src/shared/scripts/skf-scan-manifests.py +738 -0
  43. package/src/shared/scripts/skf-scan-skill-md-structure.py +375 -0
  44. package/src/shared/scripts/skf-update-active-symlink.py +315 -0
  45. package/src/shared/scripts/skf-validate-brief-inputs.py +5 -5
  46. package/src/shared/scripts/skf-validate-brief-schema.py +348 -0
  47. package/src/shared/scripts/skf-write-skill-brief.py +61 -5
  48. package/src/skf-analyze-source/SKILL.md +69 -16
  49. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  50. package/src/skf-analyze-source/customize.toml +55 -0
  51. package/src/skf-analyze-source/{steps-c/step-01b-continue.md → references/continue.md} +12 -12
  52. package/src/skf-analyze-source/{steps-c/step-06-generate-briefs.md → references/generate-briefs.md} +29 -10
  53. package/src/skf-analyze-source/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  54. package/src/skf-analyze-source/{steps-c/step-03-identify-units.md → references/identify-units.md} +48 -14
  55. package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +32 -9
  56. package/src/skf-analyze-source/{steps-c/step-04-map-and-detect.md → references/map-and-detect.md} +52 -25
  57. package/src/skf-analyze-source/{steps-c/step-05-recommend.md → references/recommend.md} +15 -8
  58. package/src/skf-analyze-source/{steps-c/step-02-scan-project.md → references/scan-project.md} +16 -10
  59. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -0
  60. package/src/skf-analyze-source/templates/analysis-report-template.md +6 -6
  61. package/src/skf-audit-skill/SKILL.md +66 -15
  62. package/src/skf-audit-skill/assets/drift-report-template.md +6 -5
  63. package/src/skf-audit-skill/customize.toml +49 -0
  64. package/src/skf-audit-skill/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  65. package/src/skf-audit-skill/{steps-c/step-01-init.md → references/init.md} +63 -19
  66. package/src/skf-audit-skill/{steps-c/step-02-re-index.md → references/re-index.md} +11 -11
  67. package/src/skf-audit-skill/{steps-c/step-06-report.md → references/report.md} +25 -7
  68. package/src/skf-audit-skill/{steps-c/step-04-semantic-diff.md → references/semantic-diff.md} +6 -6
  69. package/src/skf-audit-skill/{steps-c/step-05-severity-classify.md → references/severity-classify.md} +5 -5
  70. package/src/skf-audit-skill/references/severity-rules.md +2 -0
  71. package/src/skf-audit-skill/{steps-c/step-03-structural-diff.md → references/structural-diff.md} +35 -17
  72. package/src/skf-brief-skill/SKILL.md +49 -19
  73. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  74. package/src/skf-brief-skill/assets/skill-brief-schema.md +32 -2
  75. package/src/skf-brief-skill/customize.toml +44 -0
  76. package/src/skf-brief-skill/{steps-c/step-02-analyze-target.md → references/analyze-target.md} +27 -15
  77. package/src/skf-brief-skill/{steps-c/step-04-confirm-brief.md → references/confirm-brief.md} +13 -10
  78. package/src/skf-brief-skill/references/draft-checkpoint.md +4 -4
  79. package/src/skf-brief-skill/{steps-c/step-01-gather-intent.md → references/gather-intent.md} +35 -29
  80. package/src/skf-brief-skill/references/headless-args.md +9 -9
  81. package/src/skf-brief-skill/references/headless-source-authority-detection.md +1 -1
  82. package/src/skf-brief-skill/{steps-c/step-06-health-check.md → references/health-check.md} +4 -2
  83. package/src/skf-brief-skill/references/portfolio-similarity-check.md +2 -2
  84. package/src/skf-brief-skill/references/qmd-collection-registration.md +2 -2
  85. package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +27 -20
  86. package/src/skf-brief-skill/references/version-resolution.md +2 -2
  87. package/src/skf-brief-skill/{steps-c/step-05-write-brief.md → references/write-brief.md} +29 -16
  88. package/src/skf-create-skill/SKILL.md +25 -19
  89. package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -3
  90. package/src/skf-create-skill/assets/skill-sections.md +5 -5
  91. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +11 -11
  92. package/src/skf-create-skill/customize.toml +35 -0
  93. package/src/skf-create-skill/references/authoritative-files-protocol.md +142 -0
  94. package/src/skf-create-skill/{steps-c/step-05-compile.md → references/compile.md} +16 -16
  95. package/src/skf-create-skill/{steps-c/step-03d-component-extraction.md → references/component-extraction.md} +16 -16
  96. package/src/skf-create-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +4 -4
  97. package/src/skf-create-skill/{steps-c/step-04-enrich.md → references/enrich.md} +3 -3
  98. package/src/skf-create-skill/{steps-c/step-03-extract.md → references/extract.md} +52 -120
  99. package/src/skf-create-skill/references/extraction-patterns.md +10 -8
  100. package/src/skf-create-skill/{steps-c/step-07-generate-artifacts.md → references/generate-artifacts.md} +11 -11
  101. package/src/skf-create-skill/{steps-c/step-09-health-check.md → references/health-check.md} +4 -2
  102. package/src/skf-create-skill/{steps-c/step-01-load-brief.md → references/load-brief.md} +40 -32
  103. package/src/skf-create-skill/{steps-c/step-08-report.md → references/report.md} +8 -8
  104. package/src/skf-create-skill/references/source-resolution-protocols.md +6 -6
  105. package/src/skf-create-skill/{steps-c/sub/step-02b-ccc-discover.md → references/sub/ccc-discover.md} +5 -5
  106. package/src/skf-create-skill/{steps-c/sub/step-03c-fetch-docs.md → references/sub/fetch-docs.md} +28 -12
  107. package/src/skf-create-skill/{steps-c/sub/step-03b-fetch-temporal.md → references/sub/fetch-temporal.md} +81 -50
  108. package/src/skf-create-skill/{steps-c/step-06-validate.md → references/validate.md} +48 -28
  109. package/src/skf-create-stack-skill/SKILL.md +76 -17
  110. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +102 -0
  111. package/src/skf-create-stack-skill/assets/stack-skill-template.md +1 -1
  112. package/src/skf-create-stack-skill/customize.toml +46 -0
  113. package/src/skf-create-stack-skill/{steps-c/step-06-compile-stack.md → references/compile-stack.md} +6 -5
  114. package/src/skf-create-stack-skill/references/compose-mode-rules.md +3 -1
  115. package/src/skf-create-stack-skill/{steps-c/step-05-detect-integrations.md → references/detect-integrations.md} +37 -11
  116. package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +32 -28
  117. package/src/skf-create-stack-skill/{steps-c/step-07-generate-output.md → references/generate-output.md} +11 -85
  118. package/src/skf-create-stack-skill/{steps-c/step-10-health-check.md → references/health-check.md} +4 -2
  119. package/src/skf-create-stack-skill/{steps-c/step-01-init.md → references/init.md} +70 -4
  120. package/src/skf-create-stack-skill/references/integration-patterns.md +2 -0
  121. package/src/skf-create-stack-skill/references/manifest-patterns.md +2 -0
  122. package/src/skf-create-stack-skill/{steps-c/step-04-parallel-extract.md → references/parallel-extract.md} +44 -17
  123. package/src/skf-create-stack-skill/{steps-c/step-03-rank-and-confirm.md → references/rank-and-confirm.md} +5 -4
  124. package/src/skf-create-stack-skill/{steps-c/step-09-report.md → references/report.md} +3 -3
  125. package/src/skf-create-stack-skill/{steps-c/step-08-validate.md → references/validate.md} +6 -6
  126. package/src/skf-drop-skill/SKILL.md +84 -12
  127. package/src/skf-drop-skill/customize.toml +57 -0
  128. package/src/skf-drop-skill/{steps-c/step-02-execute.md → references/execute.md} +49 -34
  129. package/src/skf-drop-skill/{steps-c/step-04-health-check.md → references/health-check.md} +6 -4
  130. package/src/skf-drop-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  131. package/src/skf-drop-skill/{steps-c/step-01-select.md → references/select.md} +63 -15
  132. package/src/skf-export-skill/SKILL.md +85 -16
  133. package/src/skf-export-skill/assets/managed-section-format.md +3 -3
  134. package/src/skf-export-skill/assets/snippet-format.md +2 -2
  135. package/src/skf-export-skill/customize.toml +49 -0
  136. package/src/skf-export-skill/{steps-c/step-03-generate-snippet.md → references/generate-snippet.md} +11 -11
  137. package/src/skf-export-skill/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  138. package/src/skf-export-skill/{steps-c/step-01-load-skill.md → references/load-skill.md} +11 -49
  139. package/src/skf-export-skill/references/manifest-rebuild.md +68 -0
  140. package/src/skf-export-skill/references/multi-skill-mode.md +38 -0
  141. package/src/skf-export-skill/references/orphan-context-detection.md +75 -0
  142. package/src/skf-export-skill/references/orphan-row-detection.md +102 -0
  143. package/src/skf-export-skill/{steps-c/step-02-package.md → references/package.md} +7 -7
  144. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +79 -0
  145. package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +23 -11
  146. package/src/skf-export-skill/{steps-c/step-05-token-report.md → references/token-report.md} +5 -5
  147. package/src/skf-export-skill/{steps-c/step-04-update-context.md → references/update-context.md} +109 -113
  148. package/src/skf-quick-skill/SKILL.md +63 -123
  149. package/src/skf-quick-skill/customize.toml +44 -0
  150. package/src/skf-quick-skill/references/batch-mode.md +102 -0
  151. package/src/skf-quick-skill/{steps-c/step-04-compile.md → references/compile.md} +10 -10
  152. package/src/skf-quick-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +5 -5
  153. package/src/skf-quick-skill/{steps-c/step-06-finalize.md → references/finalize.md} +7 -7
  154. package/src/skf-quick-skill/{steps-c/step-07-health-check.md → references/health-check.md} +3 -3
  155. package/src/skf-quick-skill/{steps-c/step-03-quick-extract.md → references/quick-extract.md} +5 -5
  156. package/src/skf-quick-skill/references/registry-resolution.md +2 -0
  157. package/src/skf-quick-skill/{steps-c/step-01-resolve-target.md → references/resolve-target.md} +14 -10
  158. package/src/skf-quick-skill/{steps-c/step-05-write-and-validate.md → references/write-and-validate.md} +4 -4
  159. package/src/skf-refine-architecture/SKILL.md +86 -16
  160. package/src/skf-refine-architecture/customize.toml +49 -0
  161. package/src/skf-refine-architecture/{steps-c/step-05-compile.md → references/compile.md} +4 -4
  162. package/src/skf-refine-architecture/{steps-c/step-02-gap-analysis.md → references/gap-analysis.md} +5 -5
  163. package/src/skf-refine-architecture/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  164. package/src/skf-refine-architecture/{steps-c/step-04-improvements.md → references/improvements.md} +5 -5
  165. package/src/skf-refine-architecture/references/init.md +144 -0
  166. package/src/skf-refine-architecture/{steps-c/step-03-issue-detection.md → references/issue-detection.md} +5 -5
  167. package/src/skf-refine-architecture/references/refinement-rules.md +2 -0
  168. package/src/skf-refine-architecture/{steps-c/step-06-report.md → references/report.md} +14 -5
  169. package/src/skf-rename-skill/SKILL.md +82 -12
  170. package/src/skf-rename-skill/customize.toml +52 -0
  171. package/src/skf-rename-skill/{steps-c/step-02-execute.md → references/execute.md} +94 -99
  172. package/src/skf-rename-skill/references/health-check.md +30 -0
  173. package/src/skf-rename-skill/references/rebuild-context.md +110 -0
  174. package/src/skf-rename-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  175. package/src/skf-rename-skill/{steps-c/step-01-select.md → references/select.md} +59 -20
  176. package/src/skf-setup/SKILL.md +35 -35
  177. package/src/skf-setup/customize.toml +33 -0
  178. package/src/skf-setup/{steps-c/step-03-auto-index.md → references/auto-index.md} +10 -18
  179. package/src/skf-setup/{steps-c/step-01b-ccc-index.md → references/ccc-index.md} +16 -31
  180. package/src/skf-setup/{steps-c/step-01-detect-and-tier.md → references/detect-and-tier.md} +23 -10
  181. package/src/skf-setup/{steps-c/step-05-health-check.md → references/health-check.md} +1 -1
  182. package/src/skf-setup/{steps-c/step-04-report.md → references/report.md} +21 -19
  183. package/src/skf-setup/references/tier-rules.md +1 -1
  184. package/src/skf-setup/{steps-c/step-02-write-config.md → references/write-config.md} +14 -13
  185. package/src/skf-test-skill/SKILL.md +77 -15
  186. package/src/skf-test-skill/customize.toml +54 -0
  187. package/src/skf-test-skill/{steps-c/step-04-coherence-check.md → references/coherence-check.md} +37 -73
  188. package/src/skf-test-skill/{steps-c/step-03-coverage-check.md → references/coverage-check.md} +16 -39
  189. package/src/skf-test-skill/{steps-c/step-02-detect-mode.md → references/detect-mode.md} +4 -26
  190. package/src/skf-test-skill/{steps-c/step-04b-external-validators.md → references/external-validators.md} +6 -28
  191. package/src/skf-test-skill/references/health-check.md +14 -0
  192. package/src/skf-test-skill/{steps-c/step-01-init.md → references/init.md} +21 -37
  193. package/src/skf-test-skill/references/migration-section-rules.md +4 -2
  194. package/src/skf-test-skill/{steps-c/step-06-report.md → references/report.md} +41 -51
  195. package/src/skf-test-skill/{steps-c/step-05-score.md → references/score.md} +26 -43
  196. package/src/skf-test-skill/references/scoring-rules.md +8 -6
  197. package/src/skf-test-skill/references/source-access-protocol.md +7 -5
  198. package/src/skf-test-skill/scripts/compute-score.py +88 -17
  199. package/src/skf-test-skill/templates/test-report-template.md +13 -13
  200. package/src/skf-update-skill/SKILL.md +23 -14
  201. package/src/skf-update-skill/customize.toml +44 -0
  202. package/src/skf-update-skill/{steps-c/step-02-detect-changes.md → references/detect-changes.md} +176 -68
  203. package/src/skf-update-skill/references/health-check.md +32 -0
  204. package/src/skf-update-skill/{steps-c/step-01-init.md → references/init.md} +56 -8
  205. package/src/skf-update-skill/references/manual-section-rules.md +4 -0
  206. package/src/skf-update-skill/references/merge-conflict-rules.md +4 -0
  207. package/src/skf-update-skill/{steps-c/step-04-merge.md → references/merge.md} +8 -8
  208. package/src/skf-update-skill/{steps-c/step-03-re-extract.md → references/re-extract.md} +53 -44
  209. package/src/skf-update-skill/references/remote-source-resolution.md +5 -1
  210. package/src/skf-update-skill/references/report.md +214 -0
  211. package/src/skf-update-skill/{steps-c/step-05-validate.md → references/validate.md} +5 -5
  212. package/src/skf-update-skill/{steps-c/step-06-write.md → references/write.md} +73 -51
  213. package/src/skf-verify-stack/SKILL.md +89 -17
  214. package/src/skf-verify-stack/assets/feasibility-report-template.md +4 -4
  215. package/src/skf-verify-stack/customize.toml +50 -0
  216. package/src/skf-verify-stack/references/coverage-patterns.md +2 -2
  217. package/src/skf-verify-stack/{steps-c/step-02-coverage.md → references/coverage.md} +13 -9
  218. package/src/skf-verify-stack/{steps-c/step-07-health-check.md → references/health-check.md} +7 -5
  219. package/src/skf-verify-stack/references/init.md +170 -0
  220. package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
  221. package/src/skf-verify-stack/{steps-c/step-03-integrations.md → references/integrations.md} +18 -14
  222. package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +27 -42
  223. package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +13 -9
  224. package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +14 -10
  225. package/tools/validate-docs-drift.js +1 -1
  226. package/src/skf-refine-architecture/steps-c/step-01-init.md +0 -136
  227. package/src/skf-rename-skill/steps-c/step-04-health-check.md +0 -22
  228. package/src/skf-test-skill/steps-c/step-07-health-check.md +0 -25
  229. package/src/skf-update-skill/steps-c/step-07-report.md +0 -148
  230. package/src/skf-update-skill/steps-c/step-08-health-check.md +0 -22
  231. package/src/skf-verify-stack/steps-c/step-01-init.md +0 -178
@@ -4,11 +4,11 @@
4
4
  # ///
5
5
  """SKF Forge Tier RW — Read/write primitives for forger-sidecar YAML files.
6
6
 
7
- Replaces the prose-driven YAML emission in `src/skf-setup/steps-c/
8
- step-02-write-config.md` §1-§3 (and the prose-driven cleanup logic in
9
- step-03 §5/§5b) with one Python invocation. The script is the source
7
+ Replaces the prose-driven YAML emission in `src/skf-setup/references/
8
+ write-config.md` §1-§3 (and the prose-driven cleanup logic in
9
+ step 3 §5/§5b) with one Python invocation. The script is the source
10
10
  of truth for the on-disk forge-tier.yaml schema (matching the
11
- canonical template at step-02-write-config.md:24-58) and guarantees
11
+ canonical template at write-config.md:24-58) and guarantees
12
12
  that registry arrays are PRESERVED across rewrites — losing
13
13
  `qmd_collections` or `ccc_index_registry` would break every
14
14
  downstream skill (skf-create-skill, skf-audit-skill, skf-update-skill,
@@ -37,7 +37,7 @@ Subcommands:
37
37
  the same `name` is replaced, otherwise appended). All
38
38
  other forge-tier state (tools / tier / ccc_index /
39
39
  ccc_index_registry / other qmd_collections entries) is
40
- preserved verbatim. Used by skf-brief-skill step-05 §5
40
+ preserved verbatim. Used by skf-brief-skill step 5 §5
41
41
  and skf-create-skill to register Deep-tier QMD
42
42
  collections without re-rendering the whole file in
43
43
  prose.
@@ -61,7 +61,7 @@ exit non-zero (1 for user error, 2 for I/O failure).
61
61
 
62
62
  Cross-platform: pure stdlib + PyYAML. Atomic writes via temp + rename
63
63
  mirror skf-atomic-write.py's pattern. Concurrent access requires
64
- external `flock` coordination — see step-07 of skf-create-skill for
64
+ external `flock` coordination — see step 7 of skf-create-skill for
65
65
  the precedent.
66
66
 
67
67
  CLI — invoke via `uv run` so the PEP 723 PyYAML dependency declared
@@ -177,7 +177,7 @@ def render_forge_tier_yaml(payload: dict) -> str:
177
177
  """Render the canonical forge-tier.yaml from a context payload.
178
178
 
179
179
  Preserves the human-readable section comments from the template at
180
- step-02-write-config.md:24-58. Sections are emitted in a fixed order
180
+ write-config.md:24-58. Sections are emitted in a fixed order
181
181
  so re-runs against unchanged inputs produce byte-identical output.
182
182
  """
183
183
  tools = payload["tools"]
@@ -187,7 +187,7 @@ def render_forge_tier_yaml(payload: dict) -> str:
187
187
  ccc_index_registry = payload.get("ccc_index_registry", [])
188
188
  qmd_collections = payload.get("qmd_collections", [])
189
189
 
190
- # Render `tools` block in the canonical key order (matches step-02 template).
190
+ # Render `tools` block in the canonical key order (matches step 2 template).
191
191
  tools_ordered = {
192
192
  "ast_grep": tools["ast_grep"],
193
193
  "gh_cli": tools["gh_cli"],
@@ -219,7 +219,7 @@ def render_forge_tier_yaml(payload: dict) -> str:
219
219
  f"tier: {tier}",
220
220
  f"tier_detected_at: {_yaml_scalar(tier_detected_at)}",
221
221
  "",
222
- "# CCC semantic index state (managed by setup step-01b and extraction workflows)",
222
+ "# CCC semantic index state (managed by setup step 1b and extraction workflows)",
223
223
  _yaml_block({"ccc_index": ccc_ordered}),
224
224
  "",
225
225
  "# CCC index registry (tracks which source paths have been indexed for skill workflows)",
@@ -242,13 +242,13 @@ def _yaml_scalar(value) -> str:
242
242
  def _merge_preserved_fields(payload: dict, existing: dict | None) -> dict:
243
243
  """Inject preserved fields from the existing file into the new payload.
244
244
 
245
- Three preservation rules (per step-02 §1 "Note on re-runs"):
245
+ Three preservation rules (per step 2 §1 "Note on re-runs"):
246
246
  - `qmd_collections` array — preserved entirely from existing.
247
247
  - `ccc_index_registry` array — preserved entirely from existing.
248
248
  - `ccc_index.staleness_threshold_hours` scalar — preserved if user set
249
249
  a non-default value; else uses payload value or DEFAULT.
250
250
  Note: `ccc_index.exclude_patterns` is NOT preserved (rewritten fresh
251
- by step-01b on every run, per the explicit step-02 contract).
251
+ by step 1b on every run, per the explicit step 2 contract).
252
252
  """
253
253
  if existing is None:
254
254
  return payload
@@ -0,0 +1,284 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Hash Content — SHA-256 hashing helpers for skill workflows.
6
+
7
+ Two patterns recur across the update-skill workflow's stage prose:
8
+
9
+ 1. **Single-file hash** — detect-changes.md §1b, when a candidate file is
10
+ promoted into `brief.scope.include`, the LLM is asked to "compute SHA-256
11
+ content hash of candidate.path" and emit a record
12
+ `{path, heuristic, size_bytes, line_count, content_hash}` for
13
+ `promoted_docs_new[]`. Deterministic file-read + hashlib.sha256.
14
+
15
+ 2. **Bulk-compare against provenance** — detect-changes.md §Category D,
16
+ where the LLM is asked to "for each file_entry: compute current SHA-256
17
+ content hash, compare against stored hash" and classify entries as
18
+ MODIFIED_FILE / DELETED_FILE / (UNCHANGED). Same hashing primitive,
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`.
21
+
22
+ Subcommands:
23
+ hash <path> [--include-path]
24
+ Emit JSON {"content_hash": "sha256:...", "size_bytes": N, "line_count": L}
25
+ for a single file. With --include-path, the record also includes
26
+ "path": <as given>, so batch callers can collect records by streaming
27
+ multiple invocations.
28
+
29
+ compare <source-root> --provenance-map <path>
30
+ Read `file_entries[]` from a provenance-map JSON and classify each row
31
+ against the current source tree. Emits
32
+ {
33
+ "comparisons": [
34
+ {"source_file": "...", "classification": "UNCHANGED|MODIFIED_FILE|DELETED_FILE",
35
+ "stored_hash": "sha256:...", "current_hash": "sha256:..."|null,
36
+ "current_size_bytes": N|null}, ...
37
+ ],
38
+ "stats": {"total": N, "unchanged": U, "modified": M, "deleted": D}
39
+ }
40
+ The provenance file must contain `file_entries` as a top-level array
41
+ OR be the array itself (handles both schemas).
42
+
43
+ Hash format: `sha256:` prefix on a hex digest. Matches the convention used
44
+ by skf-detect-scripts-assets.py and the existing prose ("SHA-256 content
45
+ hash" — agnostic about prefix, but the prefix is the SKF convention to make
46
+ hashes self-describing for future algorithm migrations).
47
+
48
+ CLI examples:
49
+ uv run skf-hash-content.py hash docs/AGENTS.md
50
+ uv run skf-hash-content.py hash docs/AGENTS.md --include-path
51
+ uv run skf-hash-content.py compare /path/to/source \\
52
+ --provenance-map /path/to/provenance-map.json
53
+
54
+ Exit codes:
55
+ 0 — operation succeeded (including: file in compare missing on disk;
56
+ its classification is DELETED_FILE, not an error)
57
+ 1 — user error (bad path, malformed JSON, missing required field)
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ import argparse
63
+ import hashlib
64
+ import json
65
+ import sys
66
+ from pathlib import Path
67
+
68
+
69
+ # --------------------------------------------------------------------------
70
+ # Hashing primitives
71
+ # --------------------------------------------------------------------------
72
+
73
+
74
+ def sha256_of_file(path: Path) -> str:
75
+ """SHA-256 of file content, with sha256: prefix."""
76
+ h = hashlib.sha256()
77
+ with path.open("rb") as fh:
78
+ for chunk in iter(lambda: fh.read(65536), b""):
79
+ h.update(chunk)
80
+ return "sha256:" + h.hexdigest()
81
+
82
+
83
+ def count_lines(path: Path) -> int:
84
+ """Count newlines in file content; 0 if unreadable."""
85
+ n = 0
86
+ try:
87
+ with path.open("rb") as fh:
88
+ for chunk in iter(lambda: fh.read(65536), b""):
89
+ n += chunk.count(b"\n")
90
+ except OSError:
91
+ return 0
92
+ return n
93
+
94
+
95
+ def hash_record(path: Path, *, include_path: bool = False) -> dict:
96
+ """Build a single-file record: content_hash + size_bytes + line_count."""
97
+ rec: dict = {
98
+ "content_hash": sha256_of_file(path),
99
+ "size_bytes": path.stat().st_size,
100
+ "line_count": count_lines(path),
101
+ }
102
+ if include_path:
103
+ rec = {"path": path.as_posix(), **rec}
104
+ return rec
105
+
106
+
107
+ # --------------------------------------------------------------------------
108
+ # Provenance comparison
109
+ # --------------------------------------------------------------------------
110
+
111
+
112
+ UNCHANGED = "UNCHANGED"
113
+ MODIFIED_FILE = "MODIFIED_FILE"
114
+ DELETED_FILE = "DELETED_FILE"
115
+
116
+
117
+ def load_file_entries(provenance_path: Path) -> list[dict]:
118
+ """Extract the file_entries list from a provenance file.
119
+
120
+ Accepts two shapes:
121
+ - top-level object with a `file_entries` key (canonical)
122
+ - top-level array of entries (already-extracted)
123
+
124
+ Returns a copy of the list. Raises ValueError on malformed JSON or
125
+ missing key.
126
+ """
127
+ try:
128
+ data = json.loads(provenance_path.read_text(encoding="utf-8"))
129
+ except (json.JSONDecodeError, OSError) as exc:
130
+ raise ValueError(f"failed to read provenance file {provenance_path}: {exc}") from exc
131
+
132
+ if isinstance(data, list):
133
+ return list(data)
134
+ if isinstance(data, dict):
135
+ entries = data.get("file_entries")
136
+ if entries is None:
137
+ raise ValueError(
138
+ f"provenance file {provenance_path} has no `file_entries` field"
139
+ )
140
+ if not isinstance(entries, list):
141
+ raise ValueError(
142
+ f"`file_entries` in {provenance_path} is not an array"
143
+ )
144
+ return list(entries)
145
+ raise ValueError(
146
+ f"provenance file {provenance_path} must be an object or array; "
147
+ f"got {type(data).__name__}"
148
+ )
149
+
150
+
151
+ def classify_entry(source_root: Path, entry: dict) -> dict:
152
+ """Classify a single file_entry against the current source tree."""
153
+ source_file = entry.get("source_file")
154
+ stored_hash = entry.get("content_hash")
155
+ if not isinstance(source_file, str):
156
+ raise ValueError(f"file_entry missing required `source_file`: {entry!r}")
157
+
158
+ current = (source_root / source_file).resolve()
159
+ # guard against ../.. escapes — resolved path must stay inside source_root
160
+ try:
161
+ current.relative_to(source_root.resolve())
162
+ except ValueError:
163
+ return {
164
+ "source_file": source_file,
165
+ "classification": DELETED_FILE,
166
+ "stored_hash": stored_hash,
167
+ "current_hash": None,
168
+ "current_size_bytes": None,
169
+ }
170
+
171
+ if not current.is_file():
172
+ return {
173
+ "source_file": source_file,
174
+ "classification": DELETED_FILE,
175
+ "stored_hash": stored_hash,
176
+ "current_hash": None,
177
+ "current_size_bytes": None,
178
+ }
179
+
180
+ current_hash = sha256_of_file(current)
181
+ if current_hash == stored_hash:
182
+ classification = UNCHANGED
183
+ else:
184
+ classification = MODIFIED_FILE
185
+ return {
186
+ "source_file": source_file,
187
+ "classification": classification,
188
+ "stored_hash": stored_hash,
189
+ "current_hash": current_hash,
190
+ "current_size_bytes": current.stat().st_size,
191
+ }
192
+
193
+
194
+ def compare(source_root: Path, provenance_path: Path) -> dict:
195
+ entries = load_file_entries(provenance_path)
196
+ comparisons = [classify_entry(source_root, entry) for entry in entries]
197
+ counts = {UNCHANGED: 0, MODIFIED_FILE: 0, DELETED_FILE: 0}
198
+ for c in comparisons:
199
+ counts[c["classification"]] += 1
200
+ return {
201
+ "comparisons": comparisons,
202
+ "stats": {
203
+ "total": len(comparisons),
204
+ "unchanged": counts[UNCHANGED],
205
+ "modified": counts[MODIFIED_FILE],
206
+ "deleted": counts[DELETED_FILE],
207
+ },
208
+ }
209
+
210
+
211
+ # --------------------------------------------------------------------------
212
+ # CLI
213
+ # --------------------------------------------------------------------------
214
+
215
+
216
+ def _cmd_hash(args: argparse.Namespace) -> int:
217
+ path = Path(args.path)
218
+ if not path.is_file():
219
+ print(f"error: file not found: {path}", file=sys.stderr)
220
+ return 1
221
+ rec = hash_record(path, include_path=args.include_path)
222
+ json.dump(rec, sys.stdout)
223
+ sys.stdout.write("\n")
224
+ return 0
225
+
226
+
227
+ def _cmd_compare(args: argparse.Namespace) -> int:
228
+ source_root = Path(args.source_root)
229
+ provenance = Path(args.provenance_map)
230
+ if not source_root.is_dir():
231
+ print(f"error: source root not a directory: {source_root}", file=sys.stderr)
232
+ return 1
233
+ if not provenance.is_file():
234
+ print(f"error: provenance map not found: {provenance}", file=sys.stderr)
235
+ return 1
236
+ try:
237
+ result = compare(source_root, provenance)
238
+ except ValueError as exc:
239
+ print(f"error: {exc}", file=sys.stderr)
240
+ return 1
241
+ json.dump(result, sys.stdout, indent=2)
242
+ sys.stdout.write("\n")
243
+ return 0
244
+
245
+
246
+ def _build_parser() -> argparse.ArgumentParser:
247
+ parser = argparse.ArgumentParser(
248
+ prog="skf-hash-content",
249
+ description="SHA-256 hashing helpers: single-file record or bulk-compare against provenance.",
250
+ )
251
+ sub = parser.add_subparsers(dest="cmd", required=True)
252
+
253
+ p_hash = sub.add_parser("hash", help="emit hash record for a single file")
254
+ p_hash.add_argument("path", help="path to the file")
255
+ p_hash.add_argument(
256
+ "--include-path",
257
+ action="store_true",
258
+ help="include the path string in the emitted record",
259
+ )
260
+ p_hash.set_defaults(func=_cmd_hash)
261
+
262
+ p_cmp = sub.add_parser(
263
+ "compare",
264
+ help="classify provenance file_entries[] against current source tree",
265
+ )
266
+ p_cmp.add_argument("source_root", help="path to the source tree root")
267
+ p_cmp.add_argument(
268
+ "--provenance-map",
269
+ required=True,
270
+ help="path to provenance-map.json (object with file_entries[] or bare array)",
271
+ )
272
+ p_cmp.set_defaults(func=_cmd_compare)
273
+
274
+ return parser
275
+
276
+
277
+ def main(argv: list[str] | None = None) -> int:
278
+ parser = _build_parser()
279
+ args = parser.parse_args(argv)
280
+ return args.func(args)
281
+
282
+
283
+ if __name__ == "__main__":
284
+ raise SystemExit(main())
@@ -0,0 +1,295 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Load Provenance — normalize provenance-map.json into deterministic projections.
6
+
7
+ Two places in `skf-audit-skill` walk the same provenance map to extract
8
+ identical deterministic projections:
9
+
10
+ 1. **init.md §4 Load Provenance Map + Stack Skill Detection** — extracts
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.
25
+
26
+ 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.
32
+
33
+ Subcommand:
34
+ normalize <map.json>
35
+ Emit JSON:
36
+ {
37
+ "bounded_scan_files": ["<rel-path, forward-slash>", ...],
38
+ "is_stack_skill": <bool>,
39
+ "legacy_stack_provenance": <bool>,
40
+ "source_root": "<path or null>",
41
+ "baseline_commit": "<sha or null>",
42
+ "baseline_ref": "<ref or null>",
43
+ "reexport_map": {"<from-internal>": "<to-public>"}
44
+ }
45
+
46
+ Bounded scan = union of `entries[].source_file` and
47
+ `file_entries[].source_file`, deduplicated, sorted, normalized to
48
+ POSIX forward-slash separators.
49
+
50
+ Stack-skill detection mirrors `init.md` §Stack Skill Detection:
51
+ - `is_stack_skill` is true if `provenance_version >= "2.0"` AND a
52
+ top-level `libraries` key is present.
53
+ - `legacy_stack_provenance` is true if `provenance_version` is "1"
54
+ (or "1.0", or absent treated as v1) AND `skill_type == "stack"`.
55
+ Note: detecting `is_stack_skill` for a v1 map by the presence of
56
+ `libraries` alone (the documented behavior in init.md) is also
57
+ honored — see condition handling in `detect_stack_flags`.
58
+
59
+ Re-export map: read `reexport_map` field directly from the provenance
60
+ map if it exists (writer-side captures __init__.py walk results there).
61
+ Otherwise, walk `entries[]` for any `reexported_as` field on individual
62
+ entries — older provenance writers may have used that shape. Empty
63
+ object when neither is present.
64
+
65
+ CLI examples:
66
+ uv run skf-load-provenance.py normalize /path/to/provenance-map.json
67
+
68
+ Exit codes:
69
+ 0 — normalization succeeded (including empty / well-formed map with no
70
+ entries — emits all-defaults JSON)
71
+ 1 — user error (file not found, malformed JSON, structurally invalid map)
72
+ """
73
+
74
+ from __future__ import annotations
75
+
76
+ import argparse
77
+ import json
78
+ import sys
79
+ from pathlib import Path
80
+
81
+
82
+ # --------------------------------------------------------------------------
83
+ # Provenance map I/O
84
+ # --------------------------------------------------------------------------
85
+
86
+
87
+ def load_provenance(path: Path) -> dict:
88
+ """Read a provenance map JSON. Returns the top-level object.
89
+
90
+ Raises ValueError on read failure or non-object top-level.
91
+ """
92
+ try:
93
+ text = path.read_text(encoding="utf-8")
94
+ except OSError as exc:
95
+ raise ValueError(f"failed to read provenance file {path}: {exc}") from exc
96
+ try:
97
+ data = json.loads(text)
98
+ except json.JSONDecodeError as exc:
99
+ raise ValueError(f"malformed JSON in provenance file {path}: {exc}") from exc
100
+ if not isinstance(data, dict):
101
+ raise ValueError(
102
+ f"provenance file {path} must be a JSON object at top level; "
103
+ f"got {type(data).__name__}"
104
+ )
105
+ return data
106
+
107
+
108
+ # --------------------------------------------------------------------------
109
+ # Projections
110
+ # --------------------------------------------------------------------------
111
+
112
+
113
+ def _posix(p: str) -> str:
114
+ """Normalize a path string to forward-slash form."""
115
+ return p.replace("\\", "/")
116
+
117
+
118
+ def bounded_scan_files(data: dict) -> list[str]:
119
+ """Union of entries[].source_file + file_entries[].source_file.
120
+
121
+ Deduplicated; sorted; forward-slash. Non-string source_file values are
122
+ silently skipped (consistent with downstream code that expects strings).
123
+ """
124
+ paths: set[str] = set()
125
+ entries = data.get("entries")
126
+ if isinstance(entries, list):
127
+ for entry in entries:
128
+ if not isinstance(entry, dict):
129
+ continue
130
+ sf = entry.get("source_file")
131
+ if isinstance(sf, str) and sf:
132
+ paths.add(_posix(sf))
133
+ file_entries = data.get("file_entries")
134
+ if isinstance(file_entries, list):
135
+ for entry in file_entries:
136
+ if not isinstance(entry, dict):
137
+ continue
138
+ sf = entry.get("source_file")
139
+ if isinstance(sf, str) and sf:
140
+ paths.add(_posix(sf))
141
+ return sorted(paths)
142
+
143
+
144
+ def _normalize_version(v: object) -> tuple[int, ...] | None:
145
+ """Parse a provenance_version string like "2.0" / "1.5" into a tuple.
146
+
147
+ Returns None on absence or unparseable input (treat as legacy v1).
148
+ """
149
+ if v is None:
150
+ return None
151
+ if isinstance(v, (int, float)):
152
+ return (int(v),)
153
+ if not isinstance(v, str):
154
+ return None
155
+ parts: list[int] = []
156
+ for chunk in v.split("."):
157
+ try:
158
+ parts.append(int(chunk))
159
+ except ValueError:
160
+ return None
161
+ return tuple(parts) if parts else None
162
+
163
+
164
+ def detect_stack_flags(data: dict) -> tuple[bool, bool]:
165
+ """Return (is_stack_skill, legacy_stack_provenance).
166
+
167
+ Mirrors init.md §Stack Skill Detection:
168
+ - v2+ with libraries → is_stack_skill=true
169
+ - v2+ with skill_type=="stack" → is_stack_skill=true
170
+ - v1 / no version with top-level `libraries` → is_stack_skill=true,
171
+ legacy_stack_provenance=true
172
+ - v1 / no version with skill_type=="stack" → is_stack_skill=true,
173
+ legacy_stack_provenance=true
174
+ - otherwise → both false
175
+ """
176
+ version = _normalize_version(data.get("provenance_version"))
177
+ has_libraries = isinstance(data.get("libraries"), (dict, list))
178
+ skill_type = data.get("skill_type") if isinstance(data.get("skill_type"), str) else None
179
+ is_v2 = version is not None and version[0] >= 2
180
+
181
+ if is_v2:
182
+ # v2 stack signaled by either skill_type or libraries presence
183
+ is_stack = (skill_type == "stack") or has_libraries
184
+ legacy = False
185
+ return is_stack, legacy
186
+
187
+ # v1 / unversioned — legacy detection by `libraries` key OR skill_type
188
+ if has_libraries or skill_type == "stack":
189
+ return True, True
190
+ return False, False
191
+
192
+
193
+ def extract_reexport_map(data: dict) -> dict[str, str]:
194
+ """Build the public-API re-export map.
195
+
196
+ Two writer schemas are supported:
197
+
198
+ 1. **Top-level `reexport_map`** — modern writers persist the
199
+ __init__.py walk results here directly, as `{"<internal>": "<public>"}`.
200
+
201
+ 2. **Per-entry `reexported_as`** — legacy writers (or hand-edited maps)
202
+ may carry the re-export on each `entries[]` item as
203
+ `{"export_name": "_Impl", "reexported_as": "Public"}`. We collect
204
+ all such pairs into the same map shape.
205
+
206
+ Non-string keys/values are silently skipped.
207
+ """
208
+ out: dict[str, str] = {}
209
+ top = data.get("reexport_map")
210
+ if isinstance(top, dict):
211
+ for k, v in top.items():
212
+ if isinstance(k, str) and isinstance(v, str):
213
+ out[k] = v
214
+ entries = data.get("entries")
215
+ if isinstance(entries, list):
216
+ for entry in entries:
217
+ if not isinstance(entry, dict):
218
+ continue
219
+ internal = entry.get("export_name")
220
+ public = entry.get("reexported_as")
221
+ if isinstance(internal, str) and isinstance(public, str):
222
+ # per-entry value wins only if the top-level didn't already
223
+ # carry a binding for the internal name (top-level is the
224
+ # canonical aggregated form)
225
+ out.setdefault(internal, public)
226
+ return out
227
+
228
+
229
+ def normalize(data: dict) -> dict:
230
+ """Build the full normalized projection record."""
231
+ is_stack, legacy = detect_stack_flags(data)
232
+ source_root = data.get("source_root") if isinstance(data.get("source_root"), str) else None
233
+ baseline_commit = (
234
+ data.get("source_commit") if isinstance(data.get("source_commit"), str) else None
235
+ )
236
+ baseline_ref = (
237
+ data.get("source_ref") if isinstance(data.get("source_ref"), str) else None
238
+ )
239
+ return {
240
+ "bounded_scan_files": bounded_scan_files(data),
241
+ "is_stack_skill": is_stack,
242
+ "legacy_stack_provenance": legacy,
243
+ "source_root": source_root,
244
+ "baseline_commit": baseline_commit,
245
+ "baseline_ref": baseline_ref,
246
+ "reexport_map": extract_reexport_map(data),
247
+ }
248
+
249
+
250
+ # --------------------------------------------------------------------------
251
+ # CLI
252
+ # --------------------------------------------------------------------------
253
+
254
+
255
+ def _cmd_normalize(args: argparse.Namespace) -> int:
256
+ path = Path(args.provenance_map)
257
+ if not path.is_file():
258
+ print(f"error: provenance map not found: {path}", file=sys.stderr)
259
+ return 1
260
+ try:
261
+ data = load_provenance(path)
262
+ except ValueError as exc:
263
+ print(f"error: {exc}", file=sys.stderr)
264
+ return 1
265
+ result = normalize(data)
266
+ json.dump(result, sys.stdout, indent=2)
267
+ sys.stdout.write("\n")
268
+ return 0
269
+
270
+
271
+ def _build_parser() -> argparse.ArgumentParser:
272
+ parser = argparse.ArgumentParser(
273
+ prog="skf-load-provenance",
274
+ description=(
275
+ "Normalize provenance-map.json into deterministic projections "
276
+ "(bounded scan, stack flags, source_root/commit/ref, reexport map)."
277
+ ),
278
+ )
279
+ sub = parser.add_subparsers(dest="cmd", required=True)
280
+ p_norm = sub.add_parser(
281
+ "normalize", help="emit normalized projection JSON from a provenance map"
282
+ )
283
+ p_norm.add_argument("provenance_map", help="path to provenance-map.json")
284
+ p_norm.set_defaults(func=_cmd_normalize)
285
+ return parser
286
+
287
+
288
+ def main(argv: list[str] | None = None) -> int:
289
+ parser = _build_parser()
290
+ args = parser.parse_args(argv)
291
+ return args.func(args)
292
+
293
+
294
+ if __name__ == "__main__":
295
+ raise SystemExit(main())