bmad-module-skill-forge 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +51 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +14 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +117 -9
  75. package/src/shared/scripts/skf-detect-docs.py +892 -4
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +215 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +17 -6
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +55 -18
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -57
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -89
  348. package/src/skf-setup/references/detect-and-tier.md +58 -66
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -645
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -1,92 +1,302 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
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.
5
+ """SKF Severity Classify: rule-based severity classification for drift findings.
6
+
7
+ Applies the severity rules of audit-skill's references/severity-rules.md to a
8
+ list of drift findings and computes an overall drift score. Used by
9
+ audit-skill (severity-classify step).
10
+
11
+ CLI:
12
+ python3 skf-severity-classify.py findings.json [-o result.json]
13
+ python3 skf-severity-classify.py - < findings.json
14
+ python3 skf-severity-classify.py --from-diff structural-diff.json [-o findings.json]
15
+ python3 skf-severity-classify.py [--from-diff DIFF] [--file-drift FILE] [--constituents FILE]
16
+ [--semantic FILE] [--doc-drift FILE] [-o findings.json]
17
+ python3 skf-severity-classify.py --rules [--format json|markdown]
18
+
19
+ Classify (the default mode):
20
+ FINDINGS is a file path, `-` for stdin, or the JSON array itself. Pass a
21
+ file: a `detail` that quotes a signature reaches the helper intact, where
22
+ `echo '<JSON>'` breaks on the first single quote.
23
+
24
+ Input: a JSON array of findings, each an object with:
25
+ - type: removed | added | changed | moved | renamed | deprecated |
26
+ semantic
27
+ - category: what the change is about. The (type, category) pair must be
28
+ one the rule table below accepts (--rules prints it); a pair
29
+ it does not accept is an error, never a default severity.
30
+ A Script/Asset Drift row (skf-compare-file-hashes.py compare)
31
+ is added/file, removed/file or changed/file.
32
+ - count: optional positive integer: the exports a rollup row stands
33
+ for (default 1)
34
+ - category_choices: optional list (written by --from-diff): when it is
35
+ present, category must be one of it
36
+ - every other key (detail, name, file, line, confidence, ...) is carried
37
+ through untouched. type, category and category_choices compare case-
38
+ insensitively.
39
+
40
+ The added-exports rule counts exports, not rows: the counts of every
41
+ added/export finding are summed, and more than 3 grade each of them HIGH,
42
+ 3 or fewer MEDIUM. A rollup row of 15 added exports therefore grades HIGH.
43
+
44
+ Output:
45
+ {
46
+ "status": "ok",
47
+ "drift_score": "CLEAN" | "MINOR" | "SIGNIFICANT" | "CRITICAL",
48
+ "total_findings": N, # rows
49
+ "total_items": N, # rows weighted by count
50
+ "added_export_count": N, # what the added-exports rule compared
51
+ "by_severity": {"CRITICAL": N, "HIGH": N, "MEDIUM": N, "LOW": N},
52
+ "findings": [ {<finding>, "severity": "...", "rule": "<the rule>"} ]
53
+ }
54
+ by_severity counts rows weighted by count, so it sums to total_items.
55
+ findings are sorted CRITICAL first, input order kept within a severity.
56
+ drift_score follows severity-rules.md's Overall Drift Score table: CLEAN
57
+ (no findings), MINOR (LOW only), SIGNIFICANT (any MEDIUM or HIGH, no
58
+ CRITICAL), CRITICAL (any CRITICAL).
59
+
60
+ Findings that fit no rule print {status: "error", error, problems:
61
+ [{index, type, category, problem}]}, naming every one of them, and exit
62
+ 1; an input that is not an array prints {status: "error", error}.
63
+
64
+ --from-diff DIFF:
65
+ Projects a skf-structural-diff.py result into the findings array, so the
66
+ diff's buckets reach the classifier without a hand-built table. Each
67
+ finding carries name, detail, file, line and confidence (and
68
+ source_library for a --group-by source_library diff), and a fixed
69
+ category where the bucket decides it:
70
+ added[] added/export
71
+ removed[] removed/export, category_choices [export, internal_helper]:
72
+ an internal helper referenced in documented patterns grades
73
+ HIGH, a public export CRITICAL
74
+ moved[] moved/export
75
+ changed[] one finding per export: changed/location when its line is
76
+ the only field that changed; otherwise changed/signature with
77
+ category_choices listing the categories a signature or type
78
+ change can have, from signature (CRITICAL, the default) down
79
+ to style and whitespace (LOW)
80
+ A removed or added finding whose name the diff lists in ambiguous_names[]
81
+ carries "ambiguous_name": true. A reviewer who judges a removed and an
82
+ added finding of one ambiguous name to be one export that moved records
83
+ it as the projection records a move: the added finding becomes
84
+ {type: moved, category: export, detail: "moved from <removed file:line>
85
+ to <file:line>"} without ambiguous_name, and the removed finding is
86
+ dropped. Every other ambiguous finding stays as projected: a finding
87
+ that keeps ambiguous_name with another type fits no rule.
88
+ label_changes[] and signature_unverified[] are never findings. Only the
89
+ categories with category_choices need judgment; the rest are fixed.
90
+
91
+ --file-drift FILE, --constituents FILE, --semantic FILE, --doc-drift FILE:
92
+ Project the other drift sources into the same findings array, after the
93
+ diff's findings and in this order. A hash comparison labels no export,
94
+ so its findings carry a null line and confidence:
95
+ --file-drift a skf-compare-file-hashes.py compare result: each
96
+ added[] path is added/file, each removed[] path
97
+ removed/file and each changed[] item changed/file,
98
+ with the path as name and file
99
+ --constituents a skf-hash-content.py compare-constituent-hashes
100
+ result: each drifted[] entry is changed/constituent
101
+ (detail "metadata.json <stored> -> <current>"), each
102
+ missing[] entry removed/constituent (its reason as the
103
+ detail), with skill_name as name and skill_path as
104
+ file; fresh[] and skipped_null_hash[] are no findings
105
+ --semantic the semantic diff's findings, an array of objects whose
106
+ type is semantic, kept as written
107
+ --doc-drift a skf-detect-docs.py compare-hashes result, the whole
108
+ drift of a docs-only skill: each changed[] entry is
109
+ changed/doc_source (detail "content <old> -> <new>"),
110
+ with its url as name and file; unchanged[],
111
+ fetch_failed[] and skipped_null_hash[] are no findings
112
+ Each may be given with or without --from-diff. A FILE of these four
113
+ that does not exist is skipped (the stage that saves it did not run),
114
+ but at least one source file must exist. The summary line's sources
115
+ gives the findings each flag added, or null for a skipped file.
116
+
117
+ --rules:
118
+ Prints the rule table: each rule's severity and text, and the types and
119
+ categories it accepts, as JSON
120
+ ({status, added_export_threshold, rules: [{severity, rule, types,
121
+ categories, added_exports}]}) or, with --format markdown, as a table.
122
+
123
+ -o FILE writes the output to FILE and prints one summary line on stdout.
124
+
125
+ Exit codes:
126
+ 0 output written
127
+ 1 input error: unreadable or invalid JSON, not a findings array, a
128
+ finding that fits no rule, a source file that is not what its flag
129
+ reads, no source file that exists, or an -o FILE that cannot be
130
+ written (an error that starts "Cannot write output")
131
+ ({status: "error", ...} on stdout)
132
+ 2 usage error (argparse, usage on stderr)
19
133
  """
20
134
 
21
135
  from __future__ import annotations
22
136
 
137
+ import argparse
23
138
  import json
24
139
  import sys
140
+ from pathlib import Path
141
+
142
+ SEVERITIES = ("CRITICAL", "HIGH", "MEDIUM", "LOW")
25
143
 
26
- # --- Severity Rules (from severity-rules.md) ---
144
+ # More new public exports than this grade HIGH; this many or fewer MEDIUM.
145
+ ADDED_EXPORT_THRESHOLD = 3
27
146
 
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
- ]
147
+ # Categories that name a public export of any kind.
148
+ PUBLIC_EXPORTS = ("export", "function", "class", "type", "interface")
149
+ # The types a LOW bucket applies to.
150
+ STRUCTURAL_TYPES = ("added", "removed", "changed", "moved", "renamed")
33
151
 
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
- ]
152
+ LINE_ONLY_RULE = "Line-only changes (same export and file, only the line number changed)"
153
+ CONSTITUENT_CHANGED_RULE = "Constituent skills changed since the stack was composed (compose-mode stacks)"
154
+ CONSTITUENT_MISSING_RULE = "Constituent skills that can no longer be found (compose-mode stacks)"
155
+ SCRIPT_ASSET_RULE = "Added or changed script, asset or doc files (Script/Asset Drift)"
156
+ DOC_SOURCE_RULE = "Changed documentation sources of a docs-only skill (doc drift)"
40
157
 
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
158
 
47
- LOW_CATEGORIES = {"style", "convention", "comment", "documentation", "whitespace", "test", "internal", "private"}
159
+ def _rule(severity, rule, types, categories, above_threshold=None):
160
+ """One row of the rule table. above_threshold: True when the rule applies
161
+ only above ADDED_EXPORT_THRESHOLD added exports, False only at or below
162
+ it, None always."""
163
+ return {
164
+ "severity": severity,
165
+ "rule": rule,
166
+ "types": tuple(types),
167
+ "categories": tuple(categories),
168
+ "above_threshold": above_threshold,
169
+ }
170
+
171
+
172
+ # --- Severity Rules (severity-rules.md, in its order) ---
173
+
174
+ RULES = (
175
+ _rule("CRITICAL", "Removed or renamed public exports (functions, classes, types)",
176
+ ("removed", "renamed"), PUBLIC_EXPORTS),
177
+ _rule("CRITICAL", "Changed function signatures (parameter count, parameter types, return type)",
178
+ ("changed",), ("signature", "parameter_count", "parameter_type", "return_type")),
179
+ _rule("CRITICAL", "Removed or renamed modules/files referenced in skill",
180
+ ("removed", "renamed"), ("module", "file")),
181
+ _rule("CRITICAL", "Changed class inheritance or interface contracts",
182
+ ("changed",), ("inheritance", "interface", "interface_contract")),
183
+ _rule("HIGH", "New public API exports not documented in skill (>3 new exports)",
184
+ ("added",), ("export",), above_threshold=True),
185
+ _rule("HIGH", "Removed internal helpers that are referenced in documented patterns",
186
+ ("removed",), ("internal_helper",)),
187
+ _rule("HIGH", "Changed default parameter values that affect documented behavior",
188
+ ("changed",), ("default_value",)),
189
+ _rule("HIGH", "New required parameters added to documented functions",
190
+ ("changed",), ("required_parameter",)),
191
+ _rule("HIGH", "Deprecated APIs still documented as current in skill",
192
+ ("deprecated",), PUBLIC_EXPORTS),
193
+ _rule("HIGH", CONSTITUENT_CHANGED_RULE, ("changed",), ("constituent",)),
194
+ _rule("HIGH", DOC_SOURCE_RULE, ("changed",), ("doc_source",)),
195
+ _rule("MEDIUM", "Implementation changes behind a stable public API",
196
+ ("changed",), ("implementation",)),
197
+ _rule("MEDIUM", "Implementation changes behind a stable public API",
198
+ ("semantic",), ("behavior", "dependency", "architecture")),
199
+ _rule("MEDIUM", "New optional parameters with defaults on documented functions",
200
+ ("changed",), ("optional_parameter",)),
201
+ _rule("MEDIUM", "New public exports not in skill (1-3 new exports)",
202
+ ("added",), ("export",), above_threshold=False),
203
+ _rule("MEDIUM", "Moved functions between files (same API, different location)",
204
+ ("moved",), PUBLIC_EXPORTS),
205
+ _rule("MEDIUM", "Changed internal implementation patterns documented in skill conventions",
206
+ ("changed",), ("internal_pattern",)),
207
+ _rule("MEDIUM", "Changed internal implementation patterns documented in skill conventions",
208
+ ("semantic",), ("pattern", "convention", "deprecated_pattern")),
209
+ _rule("MEDIUM", SCRIPT_ASSET_RULE, ("added", "changed"), ("file",)),
210
+ _rule("MEDIUM", CONSTITUENT_MISSING_RULE, ("removed",), ("constituent",)),
211
+ _rule("LOW", "Style or convention changes (formatting, naming patterns)",
212
+ STRUCTURAL_TYPES, ("style", "convention")),
213
+ _rule("LOW", "Comment or documentation changes in source",
214
+ STRUCTURAL_TYPES, ("comment", "documentation")),
215
+ _rule("LOW", "Whitespace or structural reorganization",
216
+ STRUCTURAL_TYPES, ("whitespace",)),
217
+ _rule("LOW", LINE_ONLY_RULE, ("changed",), ("location",)),
218
+ _rule("LOW", "New private/internal functions not affecting public API",
219
+ STRUCTURAL_TYPES, ("private", "internal")),
220
+ _rule("LOW", "Test file changes", STRUCTURAL_TYPES, ("test",)),
221
+ )
222
+
223
+
224
+ def _index_rules(rules):
225
+ """(type, category) -> the rules that accept the pair."""
226
+ index = {}
227
+ for rule in rules:
228
+ for f_type in rule["types"]:
229
+ for category in rule["categories"]:
230
+ index.setdefault((f_type, category), []).append(rule)
231
+ return index
232
+
233
+
234
+ PAIRS = _index_rules(RULES)
235
+
236
+ # What --from-diff offers for a finding whose category needs judgment.
237
+ REMOVED_CHOICES = ("export", "internal_helper")
238
+ CHANGED_CHOICES = (
239
+ "signature", "parameter_count", "parameter_type", "return_type",
240
+ "inheritance", "interface", "interface_contract",
241
+ "default_value", "required_parameter",
242
+ "optional_parameter", "implementation",
243
+ "style", "whitespace",
244
+ )
245
+
246
+
247
+ def _norm(value):
248
+ return value.strip().lower() if isinstance(value, str) else None
249
+
250
+
251
+ def _pair(finding):
252
+ return _norm(finding.get("type")), _norm(finding.get("category"))
253
+
254
+
255
+ def _count(finding):
256
+ return finding.get("count", 1)
257
+
258
+
259
+ def match_rule(finding, added_export_count=0):
260
+ """The rule that grades a finding, or None when no rule accepts its pair."""
261
+ above = added_export_count > ADDED_EXPORT_THRESHOLD
262
+ for rule in PAIRS.get(_pair(finding), ()):
263
+ if rule["above_threshold"] is None or rule["above_threshold"] == above:
264
+ return rule
265
+ return None
48
266
 
49
267
 
50
268
  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"
269
+ """Classify a single finding's severity. Returns the severity, or None
270
+ for a type/category pair no rule accepts."""
271
+ rule = match_rule(finding, added_export_count)
272
+ return rule["severity"] if rule else None
273
+
274
+
275
+ def _problem(finding):
276
+ """Why a finding does not fit the rule table, or None."""
277
+ if not isinstance(finding, dict):
278
+ return "a finding must be a JSON object"
279
+ f_type, category = _pair(finding)
280
+ if f_type is None or category is None:
281
+ return "type and category must both be strings"
282
+ if (f_type, category) not in PAIRS:
283
+ return "unknown type/category pair"
284
+ choices = finding.get("category_choices")
285
+ if choices is not None and (not isinstance(choices, list) or category not in {_norm(c) for c in choices}):
286
+ return "category is not one of category_choices"
287
+ if finding.get("ambiguous_name") and f_type not in ("removed", "added"):
288
+ return "an ambiguous_name finding stays removed or added: a move judged from it drops the flag"
289
+ count = _count(finding)
290
+ if isinstance(count, bool) or not isinstance(count, int) or count < 1:
291
+ return "count must be a positive integer"
292
+ return None
293
+
294
+
295
+ def _error(message, problems=None):
296
+ out = {"status": "error", "error": message}
297
+ if problems is not None:
298
+ out["problems"] = problems
299
+ return out
90
300
 
91
301
 
92
302
  def compute_drift_score(classified):
@@ -105,59 +315,453 @@ def compute_drift_score(classified):
105
315
  def classify_all(findings):
106
316
  """Classify all findings and compute drift score."""
107
317
  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
- )
318
+ return _error("Input must be a JSON array of findings")
319
+
320
+ problems = []
321
+ for index, finding in enumerate(findings):
322
+ problem = _problem(finding)
323
+ if problem:
324
+ given = finding if isinstance(finding, dict) else {}
325
+ problems.append({
326
+ "index": index,
327
+ "type": given.get("type"),
328
+ "category": given.get("category"),
329
+ "problem": problem,
330
+ })
331
+ if problems:
332
+ named = "; ".join(
333
+ f"finding {p['index']} ({p['type']}/{p['category']}): {p['problem']}" for p in problems[:10]
334
+ )
335
+ more = f"; and {len(problems) - 10} more" if len(problems) > 10 else ""
336
+ return _error(
337
+ f"{len(problems)} finding(s) fit no severity rule: {named}{more}. "
338
+ "Run with --rules for the accepted type/category pairs.",
339
+ problems,
340
+ )
341
+
342
+ # The added-exports threshold counts exports: a rollup row adds its count.
343
+ added_export_count = sum(_count(f) for f in findings if _pair(f) == ("added", "export"))
115
344
 
116
345
  classified = []
117
346
  for finding in findings:
118
- severity = classify_finding(finding, added_export_count)
119
- classified.append({**finding, "severity": severity})
347
+ rule = match_rule(finding, added_export_count)
348
+ classified.append({**finding, "severity": rule["severity"], "rule": rule["rule"]})
120
349
 
121
350
  # 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))
351
+ severity_order = {s: i for i, s in enumerate(SEVERITIES)}
352
+ classified.sort(key=lambda f: severity_order[f["severity"]])
124
353
 
125
- drift_score = compute_drift_score(classified)
126
-
127
- # Summary
128
- by_severity = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}
354
+ by_severity = {s: 0 for s in SEVERITIES}
129
355
  for f in classified:
130
- by_severity[f["severity"]] = by_severity.get(f["severity"], 0) + 1
356
+ by_severity[f["severity"]] += _count(f)
131
357
 
132
358
  return {
133
359
  "status": "ok",
134
- "drift_score": drift_score,
360
+ "drift_score": compute_drift_score(classified),
135
361
  "total_findings": len(classified),
362
+ "total_items": sum(by_severity.values()),
363
+ "added_export_count": added_export_count,
136
364
  "by_severity": by_severity,
137
365
  "findings": classified,
138
366
  }
139
367
 
140
368
 
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)
369
+ # --------------------------------------------------------------------------
370
+ # --from-diff: a structural diff projected into findings
371
+ # --------------------------------------------------------------------------
372
+
373
+
374
+ def _text(value):
375
+ if isinstance(value, list):
376
+ return "(" + ", ".join(str(v) for v in value) + ")"
377
+ return str(value)
378
+
379
+
380
+ def _shape(rec):
381
+ """An export's signature for a detail: its signature, else its params and
382
+ return type, else None."""
383
+ if rec.get("signature"):
384
+ return str(rec["signature"])
385
+ if rec.get("params") is None and not rec.get("return_type"):
386
+ return None
387
+ shape = _text(rec.get("params") or [])
388
+ if rec.get("return_type"):
389
+ shape += f" -> {rec['return_type']}"
390
+ return shape
391
+
392
+
393
+ def _where(file, line):
394
+ return f"{file}:{line}" if line is not None else str(file)
395
+
396
+
397
+ def _finding(f_type, category, rec, detail, choices=None, ambiguous=False):
398
+ finding = {"type": f_type, "category": category}
399
+ if choices:
400
+ finding["category_choices"] = list(choices)
401
+ finding.update(
402
+ name=rec.get("name"),
403
+ detail=detail,
404
+ file=rec.get("file"),
405
+ line=rec.get("line"),
406
+ confidence=rec.get("confidence"),
407
+ )
408
+ if "source_library" in rec:
409
+ finding["source_library"] = rec["source_library"]
410
+ if ambiguous:
411
+ finding["ambiguous_name"] = True
412
+ return finding
413
+
414
+
415
+ def project_diff(diff):
416
+ """The findings array for a skf-structural-diff.py result (see --from-diff).
417
+
418
+ Raises ValueError when diff is not a structural diff.
419
+ """
420
+ buckets = ("added", "removed", "changed", "moved")
421
+ if not isinstance(diff, dict) or not all(isinstance(diff.get(b), list) for b in buckets):
422
+ raise ValueError(
423
+ "not a structural diff: expected an object with added, removed, changed "
424
+ "and moved lists (the output of skf-structural-diff.py)"
425
+ )
426
+ ambiguous = {
427
+ (item.get("source_library"), item.get("name"))
428
+ for item in diff.get("ambiguous_names") or []
429
+ if isinstance(item, dict)
430
+ }
431
+ records = {b: [r for r in diff[b] if isinstance(r, dict)] for b in buckets}
432
+
433
+ findings = []
434
+ for rec in records["added"]:
435
+ kind = rec.get("type") or "export"
436
+ shape = _shape(rec)
437
+ detail = f"new {kind}: {shape}" if shape else f"new {kind}"
438
+ findings.append(_finding("added", "export", rec, detail,
439
+ ambiguous=(rec.get("source_library"), rec.get("name")) in ambiguous))
440
+ for rec in records["removed"]:
441
+ kind = rec.get("type") or "export"
442
+ shape = _shape(rec)
443
+ detail = f"removed {kind}: {shape}" if shape else f"removed {kind}"
444
+ findings.append(_finding("removed", "export", rec, detail, choices=REMOVED_CHOICES,
445
+ ambiguous=(rec.get("source_library"), rec.get("name")) in ambiguous))
446
+
447
+ # One finding per changed export, in the diff's order.
448
+ changes = {}
449
+ for item in records["changed"]:
450
+ key = (item.get("source_library"), item.get("name"), item.get("file"))
451
+ changes.setdefault(key, []).append(item)
452
+ for items in changes.values():
453
+ rec = items[0]
454
+ fields = [item.get("field") for item in items]
455
+ if fields == ["line"]:
456
+ detail = f"line {rec.get('baseline_value')} -> {rec.get('current_value')}"
457
+ findings.append(_finding("changed", "location", rec, detail))
458
+ continue
459
+ detail = "; ".join(
460
+ f"{item.get('field')}: {_text(item.get('baseline_value'))} -> {_text(item.get('current_value'))}"
461
+ for item in items
462
+ )
463
+ findings.append(_finding("changed", "signature", rec, detail, choices=CHANGED_CHOICES))
464
+
465
+ for rec in records["moved"]:
466
+ detail = (
467
+ f"moved from {_where(rec.get('previous_file'), rec.get('previous_line'))} "
468
+ f"to {_where(rec.get('current_file'), rec.get('line'))}"
469
+ )
470
+ findings.append(_finding("moved", "export", {**rec, "file": rec.get("current_file")}, detail))
471
+ return findings
472
+
473
+
474
+ def _lists(data, keys, what):
475
+ """Raise ValueError unless data is an object holding a list at each key."""
476
+ if not isinstance(data, dict) or not all(isinstance(data.get(k), list) for k in keys):
477
+ raise ValueError(f"not {what}: expected an object with {', '.join(keys)} lists")
478
+
479
+
480
+ def project_file_drift(data):
481
+ """The findings for a skf-compare-file-hashes.py compare result (see --file-drift).
482
+
483
+ Raises ValueError when data is not one.
484
+ """
485
+ _lists(data, ("added", "removed", "changed"),
486
+ "a file drift result (the output of skf-compare-file-hashes.py compare)")
487
+ findings = []
488
+ for f_type, detail in (("added", "new file"), ("removed", "file removed")):
489
+ for path in data[f_type]:
490
+ if isinstance(path, str):
491
+ findings.append(_finding(f_type, "file", {"name": path, "file": path}, detail))
492
+ for item in data["changed"]:
493
+ if isinstance(item, dict):
494
+ path = item.get("path")
495
+ detail = f"content {item.get('stored_hash')} -> {item.get('current_hash')}"
496
+ findings.append(_finding("changed", "file", {"name": path, "file": path}, detail))
497
+ return findings
498
+
499
+
500
+ def project_constituents(data):
501
+ """The findings for a skf-hash-content.py compare-constituent-hashes result
502
+ (see --constituents).
503
+
504
+ Raises ValueError when data is not one.
505
+ """
506
+ _lists(data, ("drifted", "missing"),
507
+ "a constituent freshness result (the output of skf-hash-content.py compare-constituent-hashes)")
508
+ findings = []
509
+ for rec in data["drifted"]:
510
+ if isinstance(rec, dict):
511
+ detail = f"metadata.json {rec.get('stored_hash')} -> {rec.get('current_hash')}"
512
+ findings.append(_finding("changed", "constituent",
513
+ {"name": rec.get("skill_name"), "file": rec.get("skill_path")}, detail))
514
+ for rec in data["missing"]:
515
+ if isinstance(rec, dict):
516
+ findings.append(_finding("removed", "constituent",
517
+ {"name": rec.get("skill_name"), "file": rec.get("skill_path")},
518
+ rec.get("reason")))
519
+ return findings
520
+
521
+
522
+ def project_semantic(data):
523
+ """The semantic diff's findings, kept as written (see --semantic).
524
+
525
+ Raises ValueError unless data is an array of semantic findings.
526
+ """
527
+ if not isinstance(data, list) or not all(
528
+ isinstance(f, dict) and _norm(f.get("type")) == "semantic" for f in data):
529
+ raise ValueError("not a semantic findings file: expected an array of objects whose type is semantic")
530
+ return list(data)
531
+
532
+
533
+ def project_doc_drift(data):
534
+ """The findings for a skf-detect-docs.py compare-hashes result (see
535
+ --doc-drift).
536
+
537
+ Raises ValueError when data is not one.
538
+ """
539
+ _lists(data, ("changed", "unchanged", "fetch_failed"),
540
+ "a doc drift result (the output of skf-detect-docs.py compare-hashes)")
541
+ findings = []
542
+ for rec in data["changed"]:
543
+ if isinstance(rec, dict):
544
+ url = rec.get("url")
545
+ detail = f"content {rec.get('old_hash')} -> {rec.get('new_hash')}"
546
+ findings.append(_finding("changed", "doc_source", {"name": url, "file": url}, detail))
547
+ return findings
548
+
549
+
550
+ # Each projection source: its name in the summary line, its argparse dest,
551
+ # its projection, and whether a file that does not exist is skipped.
552
+ SOURCES = (
553
+ ("diff", "from_diff", project_diff, False),
554
+ ("file_drift", "file_drift", project_file_drift, True),
555
+ ("constituents", "constituents", project_constituents, True),
556
+ ("semantic", "semantic", project_semantic, True),
557
+ ("doc_drift", "doc_drift", project_doc_drift, True),
558
+ )
559
+
560
+
561
+ def project_sources(paths):
562
+ """(findings, sources) for the projection flags.
563
+
564
+ paths maps each source name to its file, or None when its flag is not
565
+ given. sources gives the findings each given flag added, or None for a
566
+ skipped file. Raises ValueError, naming the file, when a file cannot be
567
+ read or is not what its flag reads, or when no source file exists.
568
+ """
569
+ findings, sources = [], {}
570
+ for name, _, project, optional in SOURCES:
571
+ path = paths.get(name)
572
+ if path is None:
573
+ continue
574
+ path = Path(path)
575
+ if optional and not path.exists():
576
+ sources[name] = None
577
+ continue
578
+ try:
579
+ part = project(json.loads(path.read_text(encoding="utf-8")))
580
+ except (OSError, UnicodeDecodeError, ValueError) as e:
581
+ # json.JSONDecodeError is a ValueError.
582
+ raise ValueError(f"{path}: {e}") from e
583
+ findings.extend(part)
584
+ sources[name] = len(part)
585
+ if sources and all(count is None for count in sources.values()):
586
+ raise ValueError("none of the source files exists: " + ", ".join(str(paths[n]) for n in sources))
587
+ return findings, sources
588
+
589
+
590
+ # --------------------------------------------------------------------------
591
+ # --rules
592
+ # --------------------------------------------------------------------------
593
+
594
+
595
+ def _added_exports(rule):
596
+ if rule["above_threshold"] is None:
597
+ return None
598
+ if rule["above_threshold"]:
599
+ return f"more than {ADDED_EXPORT_THRESHOLD}"
600
+ return f"{ADDED_EXPORT_THRESHOLD} or fewer"
601
+
602
+
603
+ def rules_table():
604
+ """The rule table as --rules prints it in JSON."""
605
+ return {
606
+ "status": "ok",
607
+ "added_export_threshold": ADDED_EXPORT_THRESHOLD,
608
+ "rules": [
609
+ {
610
+ "severity": rule["severity"],
611
+ "rule": rule["rule"],
612
+ "types": list(rule["types"]),
613
+ "categories": list(rule["categories"]),
614
+ "added_exports": _added_exports(rule),
615
+ }
616
+ for rule in RULES
617
+ ],
618
+ }
619
+
620
+
621
+ def rules_markdown():
622
+ """The rule table as a Markdown table."""
623
+ lines = ["| Severity | Types | Categories | Rule |", "| --- | --- | --- | --- |"]
624
+ for rule in RULES:
625
+ types = ", ".join(f"`{t}`" for t in rule["types"])
626
+ categories = ", ".join(f"`{c}`" for c in rule["categories"])
627
+ lines.append(f"| {rule['severity']} | {types} | {categories} | {rule['rule']} |")
628
+ return "\n".join(lines)
629
+
630
+
631
+ # --------------------------------------------------------------------------
632
+ # CLI
633
+ # --------------------------------------------------------------------------
634
+
635
+
636
+ def _load_findings(arg):
637
+ """Parse FINDINGS: `-` for stdin, a JSON array given inline, or a file."""
638
+ if arg == "-":
639
+ return json.loads(sys.stdin.buffer.read().decode("utf-8"))
640
+ if arg.lstrip().startswith("["):
641
+ return json.loads(arg)
642
+ return json.loads(Path(arg).read_text(encoding="utf-8"))
643
+
644
+
645
+ def _build_parser():
646
+ # --help prints the module docstring: the findings, --from-diff and
647
+ # --rules contracts a calling step cites.
648
+ parser = argparse.ArgumentParser(
649
+ prog="skf-severity-classify.py",
650
+ description=__doc__,
651
+ formatter_class=argparse.RawDescriptionHelpFormatter,
652
+ )
653
+ parser.add_argument(
654
+ "findings",
655
+ nargs="?",
656
+ metavar="FINDINGS",
657
+ help="findings JSON file, - for stdin, or a JSON array",
658
+ )
659
+ parser.add_argument(
660
+ "--from-diff",
661
+ metavar="DIFF",
662
+ help="project a skf-structural-diff.py result file into a findings array",
663
+ )
664
+ parser.add_argument(
665
+ "--file-drift",
666
+ metavar="FILE",
667
+ help="also project a skf-compare-file-hashes.py compare result (skipped when FILE does not exist)",
668
+ )
669
+ parser.add_argument(
670
+ "--constituents",
671
+ metavar="FILE",
672
+ help="also project a compare-constituent-hashes result (skipped when FILE does not exist)",
673
+ )
674
+ parser.add_argument(
675
+ "--semantic",
676
+ metavar="FILE",
677
+ help="also add the semantic diff's findings array (skipped when FILE does not exist)",
678
+ )
679
+ parser.add_argument(
680
+ "--doc-drift",
681
+ metavar="FILE",
682
+ help="also project a skf-detect-docs.py compare-hashes result (skipped when FILE does not exist)",
683
+ )
684
+ parser.add_argument(
685
+ "--rules",
686
+ action="store_true",
687
+ help="print the rule table: severities and the accepted type/category pairs",
688
+ )
689
+ parser.add_argument(
690
+ "--format",
691
+ choices=["json", "markdown"],
692
+ help="with --rules: print JSON (the default) or a Markdown table",
693
+ )
694
+ parser.add_argument(
695
+ "-o",
696
+ "--output",
697
+ metavar="FILE",
698
+ help="write the output to FILE and print a summary line on stdout",
699
+ )
700
+ return parser
146
701
 
147
- arg = sys.argv[1]
148
702
 
703
+ def _emit(text, output, summary):
704
+ """Print text, or write it to output and print the summary line. Returns
705
+ an error dict when output cannot be written."""
706
+ if not output:
707
+ print(text)
708
+ return None
149
709
  try:
150
- if arg == "-":
151
- data = json.load(sys.stdin)
152
- elif arg.startswith("["):
153
- data = json.loads(arg)
710
+ Path(output).write_text(text + "\n", encoding="utf-8")
711
+ except OSError as exc:
712
+ return _error(f"Cannot write output: {exc}")
713
+ print(json.dumps({"status": "ok", "output": str(output), **summary}))
714
+ return None
715
+
716
+
717
+ def main(argv=None):
718
+ parser = _build_parser()
719
+ args = parser.parse_args(argv)
720
+ paths = {name: getattr(args, dest) for name, dest, _, _ in SOURCES}
721
+ projecting = any(path is not None for path in paths.values())
722
+ modes = [args.findings is not None, projecting, args.rules]
723
+ if sum(modes) != 1:
724
+ parser.error("give exactly one of FINDINGS, the projection flags (--from-diff, --file-drift, "
725
+ "--constituents, --semantic, --doc-drift) or --rules")
726
+ if args.format and not args.rules:
727
+ parser.error("--format applies to --rules only")
728
+
729
+ if args.rules:
730
+ if args.format == "markdown":
731
+ text = rules_markdown()
154
732
  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)
733
+ text = json.dumps(rules_table(), indent=2)
734
+ failed = _emit(text, args.output, {})
735
+ elif projecting:
736
+ try:
737
+ findings, sources = project_sources(paths)
738
+ except ValueError as e:
739
+ print(json.dumps(_error(str(e)), indent=2))
740
+ return 1
741
+ failed = _emit(json.dumps(findings, indent=2), args.output, {
742
+ "findings": len(findings),
743
+ "needs_judgment": sum(1 for f in findings if "category_choices" in f),
744
+ "sources": sources,
745
+ })
746
+ else:
747
+ try:
748
+ data = _load_findings(args.findings)
749
+ except (OSError, UnicodeDecodeError, ValueError) as e:
750
+ print(json.dumps(_error(str(e)), indent=2))
751
+ return 1
752
+ result = classify_all(data)
753
+ if result["status"] != "ok":
754
+ print(json.dumps(result, indent=2))
755
+ return 1
756
+ failed = _emit(json.dumps(result, indent=2), args.output, {
757
+ key: result[key] for key in ("drift_score", "total_findings", "total_items", "by_severity")
758
+ })
759
+
760
+ if failed:
761
+ print(json.dumps(failed, indent=2))
762
+ return 1
763
+ return 0
764
+
765
+
766
+ if __name__ == "__main__":
767
+ sys.exit(main())