bmad-module-skill-forge 0.9.0 → 0.10.1-alpha.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 (268) hide show
  1. package/.claude-plugin/marketplace.json +39 -0
  2. package/.gitattributes +16 -0
  3. package/README.md +105 -38
  4. package/docs/404.md +13 -10
  5. package/docs/RELEASING.md +185 -0
  6. package/docs/STABILITY.md +123 -0
  7. package/docs/_data/pinned.yaml +98 -0
  8. package/docs/agents.md +16 -11
  9. package/docs/architecture.md +221 -0
  10. package/docs/bmad-synergy.md +11 -13
  11. package/docs/concepts.md +18 -108
  12. package/docs/examples.md +121 -80
  13. package/docs/getting-started.md +85 -147
  14. package/docs/how-it-works.md +25 -536
  15. package/docs/index.md +44 -27
  16. package/docs/skill-model.md +328 -0
  17. package/docs/troubleshooting.md +39 -0
  18. package/docs/verifying-a-skill.md +232 -0
  19. package/docs/why-skf.md +93 -0
  20. package/docs/workflows.md +119 -13
  21. package/package.json +25 -4
  22. package/src/README.md +23 -25
  23. package/src/forger/forge-tier.yaml +4 -2
  24. package/src/forger/preferences.yaml +7 -1
  25. package/src/knowledge/agentskills-spec.md +1 -1
  26. package/src/knowledge/architecture-verification.md +1 -1
  27. package/src/knowledge/ccc-bridge.md +16 -15
  28. package/src/knowledge/overview.md +11 -11
  29. package/src/knowledge/progressive-capability.md +3 -3
  30. package/src/knowledge/provenance-tracking.md +9 -3
  31. package/src/knowledge/qmd-registry.md +8 -8
  32. package/src/knowledge/skf-knowledge-index.csv +2 -2
  33. package/src/knowledge/skill-lifecycle.md +16 -1
  34. package/src/knowledge/split-body-strategy.md +1 -1
  35. package/src/knowledge/version-paths.md +17 -12
  36. package/src/module-help.csv +18 -17
  37. package/src/module.yaml +4 -0
  38. package/src/shared/health-check.md +372 -0
  39. package/src/shared/references/feasibility-report-schema.md +86 -0
  40. package/src/shared/references/headless-gate-convention.md +62 -0
  41. package/src/shared/references/output-contract-schema.md +35 -0
  42. package/src/shared/references/pipeline-contracts.md +102 -0
  43. package/src/shared/scripts/skf-atomic-write.py +370 -0
  44. package/src/shared/scripts/skf-manifest-ops.py +236 -0
  45. package/src/shared/scripts/skf-preflight.py +164 -0
  46. package/src/shared/scripts/skf-rebuild-managed-sections.py +201 -0
  47. package/src/shared/scripts/skf-severity-classify.py +163 -0
  48. package/src/shared/scripts/skf-skill-inventory.py +210 -0
  49. package/src/shared/scripts/skf-structural-diff.py +257 -0
  50. package/src/shared/scripts/skf-validate-frontmatter.py +318 -0
  51. package/src/shared/scripts/skf-validate-output.py +247 -0
  52. package/src/skf-analyze-source/SKILL.md +57 -0
  53. package/src/skf-analyze-source/assets/skill-brief-schema.md +125 -0
  54. package/src/skf-analyze-source/references/unit-detection-heuristics.md +124 -0
  55. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-01-init.md +6 -66
  56. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-01b-continue.md +4 -59
  57. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-02-scan-project.md +7 -68
  58. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-03-identify-units.md +6 -66
  59. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-04-map-and-detect.md +7 -68
  60. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-05-recommend.md +6 -66
  61. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-06-generate-briefs.md +14 -66
  62. package/src/skf-analyze-source/steps-c/step-07-health-check.md +22 -0
  63. package/src/skf-audit-skill/SKILL.md +59 -0
  64. package/src/{workflows/audit-skill/data → skf-audit-skill/assets}/drift-report-template.md +7 -7
  65. package/src/{workflows/audit-skill/data → skf-audit-skill/references}/severity-rules.md +11 -11
  66. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-01-init.md +24 -68
  67. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-02-re-index.md +36 -71
  68. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-03-structural-diff.md +21 -63
  69. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-04-semantic-diff.md +6 -65
  70. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-05-severity-classify.md +5 -63
  71. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-06-report.md +10 -59
  72. package/src/skf-audit-skill/steps-c/step-07-health-check.md +22 -0
  73. package/src/skf-brief-skill/SKILL.md +53 -0
  74. package/src/{workflows/brief-skill/data → skf-brief-skill/assets}/scope-templates.md +23 -0
  75. package/src/skf-brief-skill/assets/skill-brief-schema.md +206 -0
  76. package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-01-gather-intent.md +6 -67
  77. package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-02-analyze-target.md +6 -66
  78. package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-03-scope-definition.md +12 -68
  79. package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-04-confirm-brief.md +5 -63
  80. package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-05-write-brief.md +10 -69
  81. package/src/skf-brief-skill/steps-c/step-06-health-check.md +22 -0
  82. package/src/skf-create-skill/SKILL.md +63 -0
  83. package/src/{workflows/create-skill/data → skf-create-skill/assets}/compile-assembly-rules.md +60 -0
  84. package/src/{workflows/create-skill/data → skf-create-skill/assets}/skill-sections.md +92 -23
  85. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +102 -0
  86. package/src/{workflows/create-skill/data → skf-create-skill/references}/extraction-patterns-tracing.md +13 -13
  87. package/src/{workflows/create-skill/data → skf-create-skill/references}/extraction-patterns.md +8 -3
  88. package/src/skf-create-skill/references/source-resolution-protocols.md +239 -0
  89. package/src/{workflows/create-skill/data → skf-create-skill/references}/tier-degradation-rules.md +8 -7
  90. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-01-load-brief.md +25 -63
  91. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-02-ecosystem-check.md +8 -64
  92. package/src/skf-create-skill/steps-c/step-03-extract.md +379 -0
  93. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-03d-component-extraction.md +18 -35
  94. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-04-enrich.md +12 -69
  95. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-05-compile.md +71 -75
  96. package/src/skf-create-skill/steps-c/step-06-validate.md +267 -0
  97. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-07-generate-artifacts.md +30 -74
  98. package/src/skf-create-skill/steps-c/step-08-report.md +145 -0
  99. package/src/skf-create-skill/steps-c/step-09-health-check.md +23 -0
  100. package/src/{workflows/create-skill/steps-c → skf-create-skill/steps-c/sub}/step-02b-ccc-discover.md +8 -66
  101. package/src/skf-create-skill/steps-c/sub/step-03b-fetch-temporal.md +229 -0
  102. package/src/{workflows/create-skill/steps-c → skf-create-skill/steps-c/sub}/step-03c-fetch-docs.md +18 -42
  103. package/src/skf-create-stack-skill/SKILL.md +64 -0
  104. package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/assets}/stack-skill-template.md +2 -1
  105. package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/compose-mode-rules.md +19 -5
  106. package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/integration-patterns.md +1 -1
  107. package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/manifest-patterns.md +10 -10
  108. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-01-init.md +14 -55
  109. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-02-detect-manifests.md +29 -63
  110. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-03-rank-and-confirm.md +9 -59
  111. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-04-parallel-extract.md +29 -64
  112. package/src/skf-create-stack-skill/steps-c/step-05-detect-integrations.md +164 -0
  113. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-06-compile-stack.md +7 -60
  114. package/src/skf-create-stack-skill/steps-c/step-07-generate-output.md +328 -0
  115. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-08-validate.md +17 -63
  116. package/src/skf-create-stack-skill/steps-c/step-09-report.md +128 -0
  117. package/src/skf-create-stack-skill/steps-c/step-10-health-check.md +22 -0
  118. package/src/skf-drop-skill/SKILL.md +57 -0
  119. package/src/{workflows/drop-skill → skf-drop-skill}/steps-c/step-01-select.md +9 -69
  120. package/src/{workflows/drop-skill → skf-drop-skill}/steps-c/step-02-execute.md +30 -106
  121. package/src/skf-drop-skill/steps-c/step-03-report.md +83 -0
  122. package/src/skf-drop-skill/steps-c/step-04-health-check.md +22 -0
  123. package/src/skf-export-skill/SKILL.md +57 -0
  124. package/src/skf-export-skill/assets/managed-section-format.md +138 -0
  125. package/src/{workflows/export-skill/data → skf-export-skill/assets}/snippet-format.md +8 -12
  126. package/src/skf-export-skill/steps-c/step-01-load-skill.md +247 -0
  127. package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-02-package.md +5 -58
  128. package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-03-generate-snippet.md +15 -77
  129. package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-04-update-context.md +59 -133
  130. package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-05-token-report.md +4 -58
  131. package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-06-summary.md +20 -64
  132. package/src/skf-export-skill/steps-c/step-07-health-check.md +22 -0
  133. package/src/skf-forger/SKILL.md +110 -0
  134. package/src/skf-forger/bmad-skill-manifest.yaml +11 -0
  135. package/src/skf-quick-skill/SKILL.md +56 -0
  136. package/src/{workflows/quick-skill/data → skf-quick-skill/references}/registry-resolution.md +6 -6
  137. package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-01-resolve-target.md +9 -62
  138. package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-02-ecosystem-check.md +5 -59
  139. package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-03-quick-extract.md +18 -61
  140. package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-04-compile.md +6 -60
  141. package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-05-validate.md +58 -78
  142. package/src/skf-quick-skill/steps-c/step-06-write.md +73 -0
  143. package/src/skf-quick-skill/steps-c/step-07-health-check.md +22 -0
  144. package/src/skf-refine-architecture/SKILL.md +57 -0
  145. package/src/{workflows/refine-architecture/data → skf-refine-architecture/references}/refinement-rules.md +16 -16
  146. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-01-init.md +6 -63
  147. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-02-gap-analysis.md +29 -71
  148. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-03-issue-detection.md +5 -65
  149. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-04-improvements.md +5 -64
  150. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-05-compile.md +5 -56
  151. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-06-report.md +14 -64
  152. package/src/skf-refine-architecture/steps-c/step-07-health-check.md +22 -0
  153. package/src/skf-rename-skill/SKILL.md +58 -0
  154. package/src/{workflows/rename-skill → skf-rename-skill}/steps-c/step-01-select.md +12 -73
  155. package/src/{workflows/rename-skill → skf-rename-skill}/steps-c/step-02-execute.md +32 -113
  156. package/src/skf-rename-skill/steps-c/step-03-report.md +83 -0
  157. package/src/skf-rename-skill/steps-c/step-04-health-check.md +22 -0
  158. package/src/skf-setup/SKILL.md +55 -0
  159. package/src/{workflows/setup-forge → skf-setup}/steps-c/step-01-detect-and-tier.md +6 -62
  160. package/src/{workflows/setup-forge → skf-setup}/steps-c/step-01b-ccc-index.md +9 -65
  161. package/src/{workflows/setup-forge → skf-setup}/steps-c/step-02-write-config.md +15 -60
  162. package/src/{workflows/setup-forge → skf-setup}/steps-c/step-03-auto-index.md +7 -66
  163. package/src/skf-setup/steps-c/step-04-report.md +92 -0
  164. package/src/skf-setup/steps-c/step-05-health-check.md +22 -0
  165. package/src/skf-test-skill/SKILL.md +59 -0
  166. package/src/skf-test-skill/references/migration-section-rules.md +114 -0
  167. package/src/skf-test-skill/references/scoring-rules.md +207 -0
  168. package/src/skf-test-skill/references/source-access-protocol.md +109 -0
  169. package/src/skf-test-skill/scripts/compute-score.py +310 -0
  170. package/src/skf-test-skill/steps-c/step-01-init.md +238 -0
  171. package/src/{workflows/test-skill → skf-test-skill}/steps-c/step-02-detect-mode.md +4 -62
  172. package/src/skf-test-skill/steps-c/step-03-coverage-check.md +347 -0
  173. package/src/skf-test-skill/steps-c/step-04-coherence-check.md +239 -0
  174. package/src/{workflows/test-skill → skf-test-skill}/steps-c/step-04b-external-validators.md +42 -77
  175. package/src/skf-test-skill/steps-c/step-05-score.md +266 -0
  176. package/src/skf-test-skill/steps-c/step-06-report.md +290 -0
  177. package/src/skf-test-skill/steps-c/step-07-health-check.md +25 -0
  178. package/src/skf-test-skill/templates/test-report-template.md +58 -0
  179. package/src/skf-update-skill/SKILL.md +58 -0
  180. package/src/{workflows/update-skill/data → skf-update-skill/references}/manual-section-rules.md +6 -6
  181. package/src/{workflows/update-skill/data → skf-update-skill/references}/merge-conflict-rules.md +5 -5
  182. package/src/skf-update-skill/references/remote-source-resolution.md +94 -0
  183. package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-01-init.md +22 -69
  184. package/src/skf-update-skill/steps-c/step-02-detect-changes.md +255 -0
  185. package/src/skf-update-skill/steps-c/step-03-re-extract.md +336 -0
  186. package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-04-merge.md +60 -72
  187. package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-05-validate.md +5 -62
  188. package/src/skf-update-skill/steps-c/step-06-write.md +278 -0
  189. package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-07-report.md +11 -66
  190. package/src/skf-update-skill/steps-c/step-08-health-check.md +22 -0
  191. package/src/skf-verify-stack/SKILL.md +60 -0
  192. package/src/skf-verify-stack/assets/feasibility-report-template.md +76 -0
  193. package/src/{workflows/verify-stack/data → skf-verify-stack/references}/coverage-patterns.md +12 -4
  194. package/src/skf-verify-stack/references/integration-verification-rules.md +78 -0
  195. package/src/skf-verify-stack/steps-c/step-01-init.md +178 -0
  196. package/src/{workflows/verify-stack → skf-verify-stack}/steps-c/step-02-coverage.md +27 -72
  197. package/src/skf-verify-stack/steps-c/step-03-integrations.md +179 -0
  198. package/src/{workflows/verify-stack → skf-verify-stack}/steps-c/step-04-requirements.md +14 -67
  199. package/src/skf-verify-stack/steps-c/step-05-synthesize.md +147 -0
  200. package/src/skf-verify-stack/steps-c/step-06-report.md +156 -0
  201. package/src/skf-verify-stack/steps-c/step-07-health-check.md +25 -0
  202. package/tools/cli/commands/install.js +1 -1
  203. package/tools/cli/commands/status.js +21 -23
  204. package/tools/cli/commands/uninstall.js +11 -19
  205. package/tools/cli/commands/update.js +2 -2
  206. package/tools/cli/lib/ide-skills.js +216 -0
  207. package/tools/cli/lib/installer.js +29 -52
  208. package/tools/cli/lib/manifest.js +11 -5
  209. package/tools/cli/lib/platform-codes.yaml +223 -0
  210. package/tools/cli/lib/ui.js +76 -47
  211. package/tools/validate-docs-drift.js +193 -0
  212. package/src/agents/forger.agent.yaml +0 -114
  213. package/src/workflows/README.md +0 -174
  214. package/src/workflows/analyze-source/data/skill-brief-schema.md +0 -125
  215. package/src/workflows/analyze-source/data/unit-detection-heuristics.md +0 -124
  216. package/src/workflows/analyze-source/workflow.md +0 -61
  217. package/src/workflows/audit-skill/workflow.md +0 -64
  218. package/src/workflows/brief-skill/data/skill-brief-schema.md +0 -159
  219. package/src/workflows/brief-skill/workflow.md +0 -58
  220. package/src/workflows/create-skill/data/source-resolution-protocols.md +0 -200
  221. package/src/workflows/create-skill/steps-c/step-03-extract.md +0 -301
  222. package/src/workflows/create-skill/steps-c/step-03b-fetch-temporal.md +0 -253
  223. package/src/workflows/create-skill/steps-c/step-06-validate.md +0 -254
  224. package/src/workflows/create-skill/steps-c/step-08-report.md +0 -176
  225. package/src/workflows/create-skill/workflow.md +0 -53
  226. package/src/workflows/create-stack-skill/steps-c/step-05-detect-integrations.md +0 -196
  227. package/src/workflows/create-stack-skill/steps-c/step-07-generate-output.md +0 -270
  228. package/src/workflows/create-stack-skill/steps-c/step-09-report.md +0 -145
  229. package/src/workflows/create-stack-skill/workflow.md +0 -59
  230. package/src/workflows/drop-skill/steps-c/step-03-report.md +0 -135
  231. package/src/workflows/drop-skill/workflow.md +0 -63
  232. package/src/workflows/export-skill/data/managed-section-format.md +0 -103
  233. package/src/workflows/export-skill/steps-c/step-01-load-skill.md +0 -237
  234. package/src/workflows/export-skill/workflow.md +0 -58
  235. package/src/workflows/quick-skill/steps-c/step-06-write.md +0 -179
  236. package/src/workflows/quick-skill/workflow.md +0 -58
  237. package/src/workflows/refine-architecture/workflow.md +0 -61
  238. package/src/workflows/rename-skill/steps-c/step-03-report.md +0 -132
  239. package/src/workflows/rename-skill/workflow.md +0 -64
  240. package/src/workflows/setup-forge/steps-c/step-04-report.md +0 -156
  241. package/src/workflows/setup-forge/workflow.md +0 -51
  242. package/src/workflows/test-skill/data/scoring-rules.md +0 -123
  243. package/src/workflows/test-skill/data/source-access-protocol.md +0 -51
  244. package/src/workflows/test-skill/steps-c/step-01-init.md +0 -217
  245. package/src/workflows/test-skill/steps-c/step-03-coverage-check.md +0 -280
  246. package/src/workflows/test-skill/steps-c/step-04-coherence-check.md +0 -271
  247. package/src/workflows/test-skill/steps-c/step-05-score.md +0 -222
  248. package/src/workflows/test-skill/steps-c/step-06-report.md +0 -207
  249. package/src/workflows/test-skill/templates/test-report-template.md +0 -28
  250. package/src/workflows/test-skill/workflow.md +0 -57
  251. package/src/workflows/update-skill/data/remote-source-resolution.md +0 -57
  252. package/src/workflows/update-skill/steps-c/step-02-detect-changes.md +0 -221
  253. package/src/workflows/update-skill/steps-c/step-03-re-extract.md +0 -256
  254. package/src/workflows/update-skill/steps-c/step-06-write.md +0 -265
  255. package/src/workflows/update-skill/workflow.md +0 -56
  256. package/src/workflows/verify-stack/data/feasibility-report-template.md +0 -63
  257. package/src/workflows/verify-stack/data/integration-verification-rules.md +0 -73
  258. package/src/workflows/verify-stack/steps-c/step-01-init.md +0 -179
  259. package/src/workflows/verify-stack/steps-c/step-03-integrations.md +0 -195
  260. package/src/workflows/verify-stack/steps-c/step-05-synthesize.md +0 -191
  261. package/src/workflows/verify-stack/steps-c/step-06-report.md +0 -197
  262. package/src/workflows/verify-stack/workflow.md +0 -61
  263. package/tools/cli/lib/compiler.js +0 -306
  264. package/tools/cli/lib/ide-commands.js +0 -244
  265. /package/src/{workflows/analyze-source → skf-analyze-source}/templates/analysis-report-template.md +0 -0
  266. /package/src/{workflows/quick-skill/data → skf-quick-skill/assets}/skill-template.md +0 -0
  267. /package/src/{workflows/setup-forge/data → skf-setup/references}/tier-rules.md +0 -0
  268. /package/src/{workflows/test-skill/data → skf-test-skill/assets}/output-section-formats.md +0 -0
@@ -0,0 +1,210 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Skill Inventory — Scan skills directory and produce structured inventory.
6
+
7
+ Scans the skills output folder, reads manifests and metadata, resolves active
8
+ versions via symlinks, and outputs a JSON inventory. Reused by 9+ skills.
9
+
10
+ CLI: python3 skf-skill-inventory.py <skills-output-folder>
11
+ python3 skf-skill-inventory.py <skills-output-folder> --skill <name>
12
+ python3 skf-skill-inventory.py <skills-output-folder> --manifest-only
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+ from pathlib import Path
21
+
22
+
23
+ def read_json_file(path):
24
+ """Read a JSON file, returning (data, None) or (None, error)."""
25
+ try:
26
+ with open(path, encoding="utf-8") as f:
27
+ return json.load(f), None
28
+ except FileNotFoundError:
29
+ return None, f"Not found: {path}"
30
+ except json.JSONDecodeError as e:
31
+ return None, f"JSON parse error in {path}: {e}"
32
+
33
+
34
+ def resolve_active_version(skill_group_dir):
35
+ """Resolve the active version for a skill group directory.
36
+
37
+ Returns (version_string, resolved_path) or (None, None).
38
+ """
39
+ active_link = skill_group_dir / "active"
40
+ if active_link.is_symlink() or active_link.is_dir():
41
+ target = active_link.resolve()
42
+ if target.is_dir():
43
+ return target.name, target
44
+ return None, None
45
+
46
+
47
+ def scan_skill_group(skill_group_dir, skill_name):
48
+ """Scan a single skill group directory and return its inventory entry."""
49
+ entry = {
50
+ "name": skill_name,
51
+ "path": str(skill_group_dir),
52
+ "versions": [],
53
+ "active_version": None,
54
+ "active_path": None,
55
+ "metadata": None,
56
+ "has_skill_md": False,
57
+ "has_provenance_map": False,
58
+ "has_context_snippet": False,
59
+ "errors": [],
60
+ }
61
+
62
+ # Check for version directories (contain a skill-name subdirectory)
63
+ for child in sorted(skill_group_dir.iterdir()):
64
+ if child.is_dir() and child.name != "active" and not child.name.startswith("."):
65
+ # Check if this is a version dir (contains skill-name subdir or SKILL.md)
66
+ skill_subdir = child / skill_name
67
+ if skill_subdir.is_dir():
68
+ entry["versions"].append(child.name)
69
+ elif (child / "SKILL.md").exists():
70
+ # Flat version dir without skill-name nesting
71
+ entry["versions"].append(child.name)
72
+
73
+ # Resolve active version
74
+ active_ver, active_path = resolve_active_version(skill_group_dir)
75
+ if active_ver:
76
+ entry["active_version"] = active_ver
77
+ # The active path points to the version dir; skill files are in version/skill-name/
78
+ skill_pkg = active_path / skill_name
79
+ if skill_pkg.is_dir():
80
+ entry["active_path"] = str(skill_pkg)
81
+ elif (active_path / "SKILL.md").exists():
82
+ entry["active_path"] = str(active_path)
83
+ else:
84
+ entry["active_path"] = str(active_path)
85
+
86
+ # If no versions found, check for flat layout (SKILL.md at group root)
87
+ if not entry["versions"]:
88
+ if (skill_group_dir / "SKILL.md").exists():
89
+ entry["versions"].append("flat")
90
+ entry["active_version"] = "flat"
91
+ entry["active_path"] = str(skill_group_dir)
92
+
93
+ # Load metadata from active path
94
+ active_dir = Path(entry["active_path"]) if entry["active_path"] else None
95
+ if active_dir and active_dir.is_dir():
96
+ entry["has_skill_md"] = (active_dir / "SKILL.md").exists()
97
+ entry["has_provenance_map"] = (active_dir / "provenance-map.json").exists()
98
+ entry["has_context_snippet"] = (active_dir / "context-snippet.md").exists()
99
+
100
+ metadata_path = active_dir / "metadata.json"
101
+ meta, meta_err = read_json_file(metadata_path)
102
+ if meta:
103
+ entry["metadata"] = {
104
+ "version": meta.get("version"),
105
+ "language": meta.get("language"),
106
+ "source_authority": meta.get("source_authority"),
107
+ "source_repo": meta.get("source_repo"),
108
+ "generated_by": meta.get("generated_by"),
109
+ "confidence_tier": meta.get("confidence_tier"),
110
+ "exports_total": meta.get("stats", {}).get("exports_total"),
111
+ }
112
+ elif meta_err and "Not found" not in meta_err:
113
+ entry["errors"].append(meta_err)
114
+
115
+ return entry
116
+
117
+
118
+ def scan_inventory(skills_folder, skill_filter=None, manifest_only=False):
119
+ """Scan the skills output folder and produce an inventory."""
120
+ skills_dir = Path(skills_folder)
121
+
122
+ if not skills_dir.is_dir():
123
+ return {
124
+ "status": "error",
125
+ "error": f"Skills directory not found: {skills_dir}",
126
+ "code": "DIR_NOT_FOUND",
127
+ }
128
+
129
+ result = {
130
+ "status": "ok",
131
+ "skills_folder": str(skills_dir),
132
+ "manifest": None,
133
+ "manifest_error": None,
134
+ "skills": [],
135
+ "summary": {
136
+ "total_skills": 0,
137
+ "total_versions": 0,
138
+ "with_metadata": 0,
139
+ "with_provenance": 0,
140
+ },
141
+ }
142
+
143
+ # Load export manifest
144
+ manifest_path = skills_dir / ".export-manifest.json"
145
+ manifest, manifest_err = read_json_file(manifest_path)
146
+ if manifest:
147
+ result["manifest"] = manifest
148
+ else:
149
+ result["manifest_error"] = manifest_err
150
+
151
+ if manifest_only:
152
+ return result
153
+
154
+ # Determine which skills to scan
155
+ skill_names = set()
156
+
157
+ # From manifest exports
158
+ if manifest and "exports" in manifest:
159
+ skill_names.update(manifest["exports"].keys())
160
+
161
+ # From directory listing
162
+ for child in skills_dir.iterdir():
163
+ if child.is_dir() and not child.name.startswith("."):
164
+ skill_names.add(child.name)
165
+
166
+ # Apply filter
167
+ if skill_filter:
168
+ if skill_filter in skill_names:
169
+ skill_names = {skill_filter}
170
+ else:
171
+ return {
172
+ "status": "error",
173
+ "error": f"Skill '{skill_filter}' not found in {skills_dir}",
174
+ "code": "SKILL_NOT_FOUND",
175
+ "available": sorted(skill_names),
176
+ }
177
+
178
+ # Scan each skill group
179
+ for name in sorted(skill_names):
180
+ skill_group_dir = skills_dir / name
181
+ if skill_group_dir.is_dir():
182
+ entry = scan_skill_group(skill_group_dir, name)
183
+ result["skills"].append(entry)
184
+
185
+ # Compute summary
186
+ result["summary"]["total_skills"] = len(result["skills"])
187
+ result["summary"]["total_versions"] = sum(len(s["versions"]) for s in result["skills"])
188
+ result["summary"]["with_metadata"] = sum(1 for s in result["skills"] if s["metadata"])
189
+ result["summary"]["with_provenance"] = sum(1 for s in result["skills"] if s["has_provenance_map"])
190
+
191
+ return result
192
+
193
+
194
+ if __name__ == "__main__":
195
+ if len(sys.argv) < 2:
196
+ print("Usage: python3 skf-skill-inventory.py <skills-output-folder> [--skill <name>] [--manifest-only]", file=sys.stderr)
197
+ sys.exit(1)
198
+
199
+ folder = sys.argv[1]
200
+ skill = None
201
+ manifest_only = "--manifest-only" in sys.argv
202
+
203
+ if "--skill" in sys.argv:
204
+ idx = sys.argv.index("--skill")
205
+ if idx + 1 < len(sys.argv):
206
+ skill = sys.argv[idx + 1]
207
+
208
+ result = scan_inventory(folder, skill_filter=skill, manifest_only=manifest_only)
209
+ print(json.dumps(result, indent=2))
210
+ sys.exit(0 if result["status"] == "ok" else 1)
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.9"
4
+ # dependencies = []
5
+ # ///
6
+ """Structural Diff — Deterministic comparison of skill export inventories.
7
+
8
+ Compares a baseline export inventory against a current export inventory and
9
+ produces a structured JSON diff showing added, removed, and changed entries.
10
+ Used by audit-skill and update-skill to replace LLM-based inventory comparison.
11
+
12
+ CLI:
13
+ python3 skf-structural-diff.py baseline.json current.json
14
+ python3 skf-structural-diff.py baseline.json current.json -o diff-result.json
15
+
16
+ Input:
17
+ Two JSON files. Each file must be either:
18
+ - An object with an "exports" array (provenance-map / extraction-snapshot format)
19
+ - A plain array of export entries
20
+
21
+ Each export entry must have at minimum:
22
+ - name: string (primary key for matching)
23
+ - type: string (function/class/type/const/interface/etc.)
24
+
25
+ Optional fields used for change detection:
26
+ - signature: string
27
+ - file: string
28
+ - line: number
29
+ - confidence: string
30
+
31
+ Output:
32
+ JSON object:
33
+ summary: { added, removed, changed, moved, unchanged }
34
+ added: list of entries present in current but not baseline
35
+ removed: list of entries present in baseline but not current
36
+ changed: list of { name, field, baseline_value, current_value }
37
+ moved: list of { name, previous_file, current_file }
38
+ unchanged_count: number of entries that matched exactly
39
+
40
+ Exit codes:
41
+ 0 — inventories are identical (no diff)
42
+ 1 — differences found (or error)
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import argparse
48
+ import json
49
+ import sys
50
+ from pathlib import Path
51
+
52
+
53
+ # Fields compared for change detection (in order).
54
+ # "name" is the primary key and is not diffed as a field.
55
+ # "file" is excluded — file moves are tracked separately in the "moved" list.
56
+ DIFF_FIELDS = ["type", "signature", "line", "confidence"]
57
+
58
+
59
+ def load_inventory(path: Path) -> tuple[list[dict], str | None]:
60
+ """Load an export inventory from a JSON file.
61
+
62
+ Accepts either:
63
+ - {"exports": [...]} (provenance-map / extraction-snapshot)
64
+ - [...] (plain array)
65
+
66
+ Returns (exports, error_message).
67
+ """
68
+ try:
69
+ text = path.read_text(encoding="utf-8")
70
+ except OSError as exc:
71
+ return [], f"Cannot read file '{path}': {exc}"
72
+
73
+ try:
74
+ data = json.loads(text)
75
+ except json.JSONDecodeError as exc:
76
+ return [], f"Invalid JSON in '{path}': {exc}"
77
+
78
+ if isinstance(data, list):
79
+ return data, None
80
+ if isinstance(data, dict):
81
+ if "exports" in data and isinstance(data["exports"], list):
82
+ return data["exports"], None
83
+ # If it's a plain object without "exports", treat values as entries
84
+ # (some inventory formats are name-keyed objects)
85
+ if all(isinstance(v, dict) for v in data.values()):
86
+ return list(data.values()), None
87
+ return [], f"Unrecognised inventory format in '{path}': expected array or object with 'exports' key"
88
+
89
+ return [], f"Unrecognised inventory format in '{path}': top-level value must be array or object"
90
+
91
+
92
+ def index_by_name(entries: list[dict]) -> dict[str, dict]:
93
+ """Build a name-keyed dict from an export list, skipping nameless entries."""
94
+ result: dict[str, dict] = {}
95
+ for entry in entries:
96
+ name = entry.get("name", "").strip()
97
+ if name:
98
+ result[name] = entry
99
+ return result
100
+
101
+
102
+ def entry_matches(base: dict, curr: dict) -> bool:
103
+ """Return True if two entries are identical across all DIFF_FIELDS."""
104
+ for field in DIFF_FIELDS:
105
+ if base.get(field) != curr.get(field):
106
+ return False
107
+ return True
108
+
109
+
110
+ def diff_inventories(
111
+ baseline_entries: list[dict],
112
+ current_entries: list[dict],
113
+ ) -> dict:
114
+ """Compute the structural diff between two export inventories.
115
+
116
+ Returns a dict with keys: summary, added, removed, changed, unchanged_count.
117
+ """
118
+ baseline = index_by_name(baseline_entries)
119
+ current = index_by_name(current_entries)
120
+
121
+ baseline_names = set(baseline.keys())
122
+ current_names = set(current.keys())
123
+
124
+ added_names = current_names - baseline_names
125
+ removed_names = baseline_names - current_names
126
+ common_names = baseline_names & current_names
127
+
128
+ added = [current[n] for n in sorted(added_names)]
129
+ removed = [baseline[n] for n in sorted(removed_names)]
130
+
131
+ changed: list[dict] = []
132
+ moved: list[dict] = []
133
+ unchanged_count = 0
134
+
135
+ for name in sorted(common_names):
136
+ base_entry = baseline[name]
137
+ curr_entry = current[name]
138
+
139
+ # Detect file moves separately
140
+ base_file = base_entry.get("file")
141
+ curr_file = curr_entry.get("file")
142
+ if base_file and curr_file and base_file != curr_file:
143
+ moved.append({
144
+ "name": name,
145
+ "previous_file": base_file,
146
+ "current_file": curr_file,
147
+ })
148
+
149
+ entry_changed = False
150
+ for field in DIFF_FIELDS:
151
+ base_val = base_entry.get(field)
152
+ curr_val = curr_entry.get(field)
153
+ if base_val != curr_val:
154
+ changed.append({
155
+ "name": name,
156
+ "field": field,
157
+ "baseline_value": base_val,
158
+ "current_value": curr_val,
159
+ })
160
+ entry_changed = True
161
+
162
+ if not entry_changed:
163
+ unchanged_count += 1
164
+
165
+ # Count distinct names that have at least one field change
166
+ changed_names = len({c["name"] for c in changed})
167
+
168
+ return {
169
+ "summary": {
170
+ "added": len(added),
171
+ "removed": len(removed),
172
+ "changed": changed_names,
173
+ "moved": len(moved),
174
+ "unchanged": unchanged_count,
175
+ },
176
+ "added": added,
177
+ "removed": removed,
178
+ "changed": changed,
179
+ "moved": moved,
180
+ "unchanged_count": unchanged_count,
181
+ }
182
+
183
+
184
+ def main() -> int:
185
+ parser = argparse.ArgumentParser(
186
+ prog="skf-structural-diff.py",
187
+ description=(
188
+ "Compare two JSON export inventories and produce a structured diff.\n"
189
+ "\n"
190
+ "Each inventory file must be either a JSON array of export entries\n"
191
+ "or a JSON object with an 'exports' key containing such an array.\n"
192
+ "Every entry must have at minimum 'name' and 'type' fields.\n"
193
+ "\n"
194
+ "Exit code 0 means no differences were found.\n"
195
+ "Exit code 1 means differences were found (or an error occurred)."
196
+ ),
197
+ formatter_class=argparse.RawDescriptionHelpFormatter,
198
+ epilog=(
199
+ "examples:\n"
200
+ " python3 skf-structural-diff.py provenance-map.json current-snapshot.json\n"
201
+ " python3 skf-structural-diff.py baseline.json current.json -o diff.json\n"
202
+ ),
203
+ )
204
+ parser.add_argument(
205
+ "baseline",
206
+ metavar="baseline.json",
207
+ help="path to the baseline export inventory (e.g. provenance-map.json)",
208
+ )
209
+ parser.add_argument(
210
+ "current",
211
+ metavar="current.json",
212
+ help="path to the current export inventory (e.g. extraction-snapshot.json)",
213
+ )
214
+ parser.add_argument(
215
+ "-o",
216
+ "--output",
217
+ metavar="FILE",
218
+ help="write JSON output to FILE instead of stdout",
219
+ )
220
+
221
+ args = parser.parse_args()
222
+
223
+ baseline_path = Path(args.baseline)
224
+ current_path = Path(args.current)
225
+
226
+ baseline_entries, err = load_inventory(baseline_path)
227
+ if err:
228
+ print(json.dumps({"status": "error", "error": err}, indent=2))
229
+ return 1
230
+
231
+ current_entries, err = load_inventory(current_path)
232
+ if err:
233
+ print(json.dumps({"status": "error", "error": err}, indent=2))
234
+ return 1
235
+
236
+ result = diff_inventories(baseline_entries, current_entries)
237
+ output_text = json.dumps(result, indent=2)
238
+
239
+ if args.output:
240
+ out_path = Path(args.output)
241
+ try:
242
+ out_path.write_text(output_text + "\n", encoding="utf-8")
243
+ except OSError as exc:
244
+ print(
245
+ json.dumps({"status": "error", "error": f"Cannot write output: {exc}"}, indent=2)
246
+ )
247
+ return 1
248
+ else:
249
+ print(output_text)
250
+
251
+ summary = result["summary"]
252
+ has_diff = summary["added"] or summary["removed"] or summary["changed"] or summary["moved"]
253
+ return 1 if has_diff else 0
254
+
255
+
256
+ if __name__ == "__main__":
257
+ sys.exit(main())