bmad-module-skill-forge 0.10.0 → 1.0.0-rc.3

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 (269) hide show
  1. package/.claude-plugin/marketplace.json +56 -0
  2. package/.gitattributes +16 -0
  3. package/README.md +105 -38
  4. package/docs/404.md +13 -10
  5. package/docs/RELEASING.md +616 -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 +30 -9
  22. package/release-audits/v1.0.0-launch-audit.md +207 -0
  23. package/src/README.md +23 -25
  24. package/src/forger/forge-tier.yaml +4 -2
  25. package/src/forger/preferences.yaml +7 -1
  26. package/src/knowledge/agentskills-spec.md +1 -1
  27. package/src/knowledge/architecture-verification.md +1 -1
  28. package/src/knowledge/ccc-bridge.md +16 -15
  29. package/src/knowledge/overview.md +11 -11
  30. package/src/knowledge/progressive-capability.md +3 -3
  31. package/src/knowledge/provenance-tracking.md +9 -3
  32. package/src/knowledge/qmd-registry.md +8 -8
  33. package/src/knowledge/skf-knowledge-index.csv +2 -2
  34. package/src/knowledge/skill-lifecycle.md +16 -1
  35. package/src/knowledge/split-body-strategy.md +1 -1
  36. package/src/knowledge/version-paths.md +17 -12
  37. package/src/module-help.csv +18 -17
  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 -71
  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 -64
  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 -70
  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 +16 -68
  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 +11 -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 -69
  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 +4 -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 +6 -4
  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 -179
  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 -150
  229. package/src/workflows/create-stack-skill/workflow.md +0 -59
  230. package/src/workflows/drop-skill/steps-c/step-03-report.md +0 -136
  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 -178
  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 -133
  239. package/src/workflows/rename-skill/workflow.md +0 -64
  240. package/src/workflows/setup-forge/steps-c/step-04-report.md +0 -159
  241. package/src/workflows/setup-forge/workflow.md +0 -51
  242. package/src/workflows/shared/health-check.md +0 -231
  243. package/src/workflows/test-skill/data/scoring-rules.md +0 -123
  244. package/src/workflows/test-skill/data/source-access-protocol.md +0 -51
  245. package/src/workflows/test-skill/steps-c/step-01-init.md +0 -217
  246. package/src/workflows/test-skill/steps-c/step-03-coverage-check.md +0 -280
  247. package/src/workflows/test-skill/steps-c/step-04-coherence-check.md +0 -271
  248. package/src/workflows/test-skill/steps-c/step-05-score.md +0 -222
  249. package/src/workflows/test-skill/steps-c/step-06-report.md +0 -208
  250. package/src/workflows/test-skill/templates/test-report-template.md +0 -28
  251. package/src/workflows/test-skill/workflow.md +0 -57
  252. package/src/workflows/update-skill/data/remote-source-resolution.md +0 -57
  253. package/src/workflows/update-skill/steps-c/step-02-detect-changes.md +0 -221
  254. package/src/workflows/update-skill/steps-c/step-03-re-extract.md +0 -256
  255. package/src/workflows/update-skill/steps-c/step-06-write.md +0 -265
  256. package/src/workflows/update-skill/workflow.md +0 -56
  257. package/src/workflows/verify-stack/data/feasibility-report-template.md +0 -63
  258. package/src/workflows/verify-stack/data/integration-verification-rules.md +0 -73
  259. package/src/workflows/verify-stack/steps-c/step-01-init.md +0 -179
  260. package/src/workflows/verify-stack/steps-c/step-03-integrations.md +0 -195
  261. package/src/workflows/verify-stack/steps-c/step-05-synthesize.md +0 -191
  262. package/src/workflows/verify-stack/steps-c/step-06-report.md +0 -200
  263. package/src/workflows/verify-stack/workflow.md +0 -61
  264. package/tools/cli/lib/compiler.js +0 -306
  265. package/tools/cli/lib/ide-commands.js +0 -244
  266. /package/src/{workflows/analyze-source → skf-analyze-source}/templates/analysis-report-template.md +0 -0
  267. /package/src/{workflows/quick-skill/data → skf-quick-skill/assets}/skill-template.md +0 -0
  268. /package/src/{workflows/setup-forge/data → skf-setup/references}/tier-rules.md +0 -0
  269. /package/src/{workflows/test-skill/data → skf-test-skill/assets}/output-section-formats.md +0 -0
@@ -0,0 +1,163 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Severity Classify — Rule-based severity classification for drift findings.
6
+
7
+ Applies severity rules to a list of drift findings and computes an overall
8
+ drift score. Used by audit-skill and test-skill.
9
+
10
+ CLI: python3 skf-severity-classify.py <findings-json>
11
+ echo '<JSON>' | python3 skf-severity-classify.py -
12
+
13
+ Input: JSON array of findings, each with:
14
+ - type: "removed"|"added"|"changed"|"moved"|"semantic"
15
+ - category: "export"|"signature"|"parameter"|"module"|"pattern"|"convention"|...
16
+ - detail: description string
17
+
18
+ Output: JSON with classified findings and overall drift score.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import sys
25
+
26
+ # --- Severity Rules (from severity-rules.md) ---
27
+
28
+ CRITICAL_RULES = [
29
+ {"type": "removed", "categories": {"export", "module", "class", "interface"}},
30
+ {"type": "changed", "categories": {"signature", "parameter_count", "return_type", "inheritance", "interface_contract"}},
31
+ {"type": "renamed", "categories": {"export", "module"}},
32
+ ]
33
+
34
+ HIGH_RULES = [
35
+ {"type": "added", "categories": {"export"}, "threshold": 3}, # >3 new exports
36
+ {"type": "removed", "categories": {"internal_helper"}},
37
+ {"type": "changed", "categories": {"default_value", "required_parameter"}},
38
+ {"type": "deprecated", "categories": {"export"}},
39
+ ]
40
+
41
+ MEDIUM_RULES = [
42
+ {"type": "changed", "categories": {"implementation", "optional_parameter", "internal_pattern"}},
43
+ {"type": "added", "categories": {"export"}, "threshold_max": 3}, # 1-3 new exports
44
+ {"type": "moved", "categories": {"export", "function"}},
45
+ ]
46
+
47
+ LOW_CATEGORIES = {"style", "convention", "comment", "documentation", "whitespace", "test", "internal", "private"}
48
+
49
+
50
+ def classify_finding(finding, added_export_count=0):
51
+ """Classify a single finding's severity. Returns severity string."""
52
+ f_type = finding.get("type", "").lower()
53
+ f_category = finding.get("category", "").lower()
54
+
55
+ # Check CRITICAL
56
+ for rule in CRITICAL_RULES:
57
+ if f_type == rule["type"] and f_category in rule["categories"]:
58
+ return "CRITICAL"
59
+
60
+ # Check HIGH
61
+ for rule in HIGH_RULES:
62
+ if f_type == rule["type"]:
63
+ if f_category in rule["categories"]:
64
+ if "threshold" in rule:
65
+ if f_type == "added" and f_category == "export" and added_export_count > rule["threshold"]:
66
+ return "HIGH"
67
+ else:
68
+ return "HIGH"
69
+
70
+ # Check MEDIUM
71
+ for rule in MEDIUM_RULES:
72
+ if f_type == rule["type"]:
73
+ if f_category in rule["categories"]:
74
+ if "threshold_max" in rule:
75
+ if f_type == "added" and f_category == "export" and added_export_count <= rule["threshold_max"]:
76
+ return "MEDIUM"
77
+ else:
78
+ return "MEDIUM"
79
+
80
+ # Check LOW
81
+ if f_category in LOW_CATEGORIES:
82
+ return "LOW"
83
+
84
+ # Semantic findings default to MEDIUM
85
+ if f_type == "semantic":
86
+ return "MEDIUM"
87
+
88
+ # Default: MEDIUM for unrecognized patterns
89
+ return "MEDIUM"
90
+
91
+
92
+ def compute_drift_score(classified):
93
+ """Compute overall drift score from classified findings."""
94
+ severities = {f["severity"] for f in classified}
95
+
96
+ if not classified:
97
+ return "CLEAN"
98
+ if "CRITICAL" in severities:
99
+ return "CRITICAL"
100
+ if "HIGH" in severities or "MEDIUM" in severities:
101
+ return "SIGNIFICANT"
102
+ return "MINOR"
103
+
104
+
105
+ def classify_all(findings):
106
+ """Classify all findings and compute drift score."""
107
+ if not isinstance(findings, list):
108
+ return {"status": "error", "error": "Input must be a JSON array of findings"}
109
+
110
+ # Count added exports for threshold rules
111
+ added_export_count = sum(
112
+ 1 for f in findings
113
+ if f.get("type", "").lower() == "added" and f.get("category", "").lower() == "export"
114
+ )
115
+
116
+ classified = []
117
+ for finding in findings:
118
+ severity = classify_finding(finding, added_export_count)
119
+ classified.append({**finding, "severity": severity})
120
+
121
+ # Sort by severity: CRITICAL > HIGH > MEDIUM > LOW
122
+ severity_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
123
+ classified.sort(key=lambda f: severity_order.get(f["severity"], 4))
124
+
125
+ drift_score = compute_drift_score(classified)
126
+
127
+ # Summary
128
+ by_severity = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}
129
+ for f in classified:
130
+ by_severity[f["severity"]] = by_severity.get(f["severity"], 0) + 1
131
+
132
+ return {
133
+ "status": "ok",
134
+ "drift_score": drift_score,
135
+ "total_findings": len(classified),
136
+ "by_severity": by_severity,
137
+ "findings": classified,
138
+ }
139
+
140
+
141
+ if __name__ == "__main__":
142
+ if len(sys.argv) < 2:
143
+ print("Usage: python3 skf-severity-classify.py <findings-json-or-file>", file=sys.stderr)
144
+ print(" echo '<JSON>' | python3 skf-severity-classify.py -", file=sys.stderr)
145
+ sys.exit(1)
146
+
147
+ arg = sys.argv[1]
148
+
149
+ try:
150
+ if arg == "-":
151
+ data = json.load(sys.stdin)
152
+ elif arg.startswith("["):
153
+ data = json.loads(arg)
154
+ else:
155
+ with open(arg, encoding="utf-8") as f:
156
+ data = json.load(f)
157
+ except (json.JSONDecodeError, FileNotFoundError) as e:
158
+ print(json.dumps({"status": "error", "error": str(e)}))
159
+ sys.exit(1)
160
+
161
+ result = classify_all(data)
162
+ print(json.dumps(result, indent=2))
163
+ sys.exit(0 if result["status"] == "ok" else 1)
@@ -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())