bmad-module-skill-forge 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +15 -5
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -90
  348. package/src/skf-setup/references/detect-and-tier.md +58 -67
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -679
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -0,0 +1,685 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = []
5
+ # ///
6
+ """SKF render drift tables: the drift report's per-item tables, from the JSON
7
+ audit-skill saved.
8
+
9
+ Step 3 (references/structural-diff.md section 5) and step 5
10
+ (references/severity-classify.md section 3) each append a section of
11
+ tables to the drift report, and step 6 (references/report.md section 2)
12
+ its Out-of-Scope New Public API table. Every cell of those tables is a
13
+ field of the JSON an earlier step saved, so this script prints them,
14
+ rather than the model copying hundreds of rows by hand, where a row can be
15
+ dropped or mis-matched. The model keeps only what the JSON does not hold:
16
+ the section heading and step 3's Comparison and Method lines.
17
+
18
+ structural DIFF [--file-drift FILE]
19
+ DIFF is the skf-structural-diff.py result step 3 section 1 saved
20
+ (structural-diff.json). Prints, in this order:
21
+ ### Added Exports (N) Export | Type | Signature | Location | Confidence
22
+ ### Removed Exports (N) Export | Type | Original Signature |
23
+ Original Location | Confidence
24
+ ### Moved Exports (N) Export | From | To | Confidence
25
+ ### Changed Exports (N) Export | Change Type | Before | After |
26
+ Location | Confidence (one row per changed
27
+ field; a line change reads `location`)
28
+ ### Ambiguous Names (N) Export | Removed At | Added At (only when
29
+ ambiguous_names[] is non-empty)
30
+ ### Summary Added, Removed, Moved, Changed and
31
+ **Total Drift Items**, their sum
32
+ **Signatures not compared:** (only when summary.signature_unverified
33
+ is above 0)
34
+ ### By Library a stack diff (--group-by source_library):
35
+ each groups[] entry's counts
36
+ ### Script/Asset Drift (added N, removed N, changed N)
37
+ File | Change | Detail, from FILE, the
38
+ skf-compare-file-hashes.py compare result
39
+ step 3 section 4b saved (only when FILE is
40
+ given and exists; when FILE cannot be read
41
+ or is not such a result, the heading reads
42
+ `### Script/Asset Drift: skipped (<reason>)`
43
+ and the other tables still print)
44
+ ### Provenance label differences (not drift) (N)
45
+ Export | Baseline label | Current label
46
+ (only when label_changes[] is non-empty)
47
+ Every heading count comes from the diff's summary (FILE's stats for
48
+ Script/Asset Drift), never from the rows. An export of a stack reads
49
+ `<library>: <name>`.
50
+
51
+ severity CLASSIFICATION
52
+ CLASSIFICATION is the skf-severity-classify.py result step 5 section 2
53
+ saved (severity.json). Prints the **Overall Drift Score** line, one table
54
+ per severity, CRITICAL first:
55
+ ### <SEVERITY> (by_severity[<SEVERITY>])
56
+ # | Finding | Type | Detail | Location |
57
+ Confidence
58
+ and the Classification Summary, whose **Total** is total_items. Type is
59
+ `semantic` for a semantic finding, `doc` for a changed document of a
60
+ docs-only skill and `structural` for every other one; a finding with no
61
+ confidence or file (a hash comparison) shows `n/a`.
62
+
63
+ outside-scope SNAPSHOT
64
+ SNAPSHOT is the extraction-snapshot.json step 2 wrote
65
+ (skf-extraction-snapshot.py build). Prints
66
+ ### Out-of-Scope New Public API
67
+ Path | Evidence, one row per outside_scope
68
+ entry: `path` | exports `name`, ... through
69
+ `entry`, ... (no `through` part when the
70
+ entry names no entry point)
71
+ and nothing when outside_scope is empty or absent (Quick tier). The table
72
+ is the shape skf-provenance-gap-dispatch.py parses back for
73
+ update-skill's scope reconciliation, and never rolls up: the snapshot
74
+ already groups the names by file.
75
+
76
+ Rollup (structural and severity, mechanical):
77
+ 10 or more rows of one table that share a file roll up into one row, so a
78
+ deleted or a new file reads as one row. Removed rows go one level on: 10
79
+ or more of the removed rows left that share a directory (never the top
80
+ level) roll up too, so a removed package tree reads as one row. Added
81
+ rows stop at the file, since a directory is no shared cause: exports
82
+ added across the files of one directory keep a row each.
83
+ Rows roll up only with rows of their own kind:
84
+ - Added and Removed Exports: per library. The Export cell reads
85
+ `N exports (rep: a, b, c, ...)`, the Location the file or directory.
86
+ - Script/Asset Drift: per change (added, removed, changed); a removed
87
+ file by directory.
88
+ - Provenance label differences: 10 or more rows with one baseline and
89
+ one current label (compared case-insensitively) roll up whatever
90
+ their files.
91
+ - Severity tables: added and removed findings and Script/Asset files,
92
+ per type, category and library, a removed one by directory too.
93
+ The Finding cell reads
94
+ `<type> in <file or directory> (xN; rep: a, b, c, ...)`, N counting
95
+ each finding's `count`, and the Detail cell the findings' shared
96
+ detail, else their rule.
97
+ Moved, Changed and Ambiguous rows, and changed-signature, moved and
98
+ semantic findings, never roll up. A rollup changes how a table reads,
99
+ never a count: every heading and summary comes from the JSON.
100
+
101
+ Output: the markdown on stdout, UTF-8.
102
+
103
+ Exit codes:
104
+ 0 the tables were printed (for outside-scope, also when there was
105
+ nothing to print)
106
+ 1 DIFF, CLASSIFICATION or SNAPSHOT is missing, is not JSON or is not
107
+ what the command reads ({"status": "error", "error"} on stdout, the
108
+ error starting with the input's path)
109
+ 2 usage error (argparse, usage on stderr)
110
+ """
111
+
112
+ from __future__ import annotations
113
+
114
+ import argparse
115
+ import json
116
+ import posixpath
117
+ import sys
118
+ from pathlib import Path
119
+
120
+ # Rows of one table that share a file or a directory roll up from this many on.
121
+ ROLLUP_MIN = 10
122
+ # Representative names a rollup row shows.
123
+ REP_COUNT = 3
124
+ SEVERITIES = ("CRITICAL", "HIGH", "MEDIUM", "LOW")
125
+ NONE = "(none)"
126
+ NA = "n/a"
127
+ # Characters a plain table cell escapes, so text renders as written.
128
+ _MARKDOWN = "\\`*_[]<>|"
129
+
130
+
131
+ class RenderError(Exception):
132
+ """An input the tables cannot be rendered from: exit 1."""
133
+
134
+
135
+ # --------------------------------------------------------------------------
136
+ # Cells
137
+ # --------------------------------------------------------------------------
138
+
139
+
140
+ def _flat(value) -> str:
141
+ """One line of text for a value; a list reads (a, b)."""
142
+ if isinstance(value, list):
143
+ value = "(" + ", ".join(str(v) for v in value) + ")"
144
+ return " ".join(str(value).split())
145
+
146
+
147
+ def text(value) -> str:
148
+ """A plain cell: markdown characters escaped, `n/a` for no value.
149
+
150
+ >>> text("def __init__(self) | None")
151
+ 'def \\\\_\\\\_init\\\\_\\\\_(self) \\\\| None'
152
+ >>> text(None)
153
+ 'n/a'
154
+ """
155
+ if value is None or value == "":
156
+ return NA
157
+ return "".join("\\" + ch if ch in _MARKDOWN else ch for ch in _flat(value))
158
+
159
+
160
+ def code(value) -> str:
161
+ """A code span holding the value as written, `(none)` for no value.
162
+
163
+ A pipe is escaped, which a GFM table needs even inside a code span, and
164
+ the fence is longer than any run of backticks in the value.
165
+
166
+ >>> code("a | b")
167
+ '`a \\\\| b`'
168
+ >>> code("x`y")
169
+ '`` x`y ``'
170
+ >>> code(None)
171
+ '(none)'
172
+ """
173
+ if value is None or value == "":
174
+ return NONE
175
+ flat = _flat(value).replace("|", "\\|")
176
+ longest = run = 0
177
+ for ch in flat:
178
+ run = run + 1 if ch == "`" else 0
179
+ longest = max(longest, run)
180
+ fence = "`" * (longest + 1)
181
+ pad = " " if longest else ""
182
+ return f"{fence}{pad}{flat}{pad}{fence}"
183
+
184
+
185
+ def where(file, line=None) -> str:
186
+ """`file:line`, `file` without a line, `n/a` without a file."""
187
+ if file is None or file == "":
188
+ return NA
189
+ return code(f"{file}:{line}" if line is not None else file)
190
+
191
+
192
+ def export_cell(item: dict) -> str:
193
+ """An export's name, after its library for a stack."""
194
+ name = code(item.get("name"))
195
+ if item.get("source_library") is not None:
196
+ return f"{text(item['source_library'])}: {name}"
197
+ return name
198
+
199
+
200
+ def _table(header: tuple[str, ...], rows: list[list[str]]) -> str:
201
+ """A markdown table, or `None.` when it has no row."""
202
+ if not rows:
203
+ return "None."
204
+ lines = ["| " + " | ".join(header) + " |", "|" + "|".join("-" * (len(h) + 2) for h in header) + "|"]
205
+ lines += ["| " + " | ".join(row) + " |" for row in rows]
206
+ return "\n".join(lines)
207
+
208
+
209
+ def _distinct(values) -> list:
210
+ """The values in first-seen order, each once."""
211
+ out = []
212
+ for value in values:
213
+ if value not in out:
214
+ out.append(value)
215
+ return out
216
+
217
+
218
+ def _labels(values) -> str:
219
+ """Confidence labels of rolled-up rows, each once."""
220
+ return ", ".join(text(v) for v in _distinct(values))
221
+
222
+
223
+ def _rep(names: list) -> str:
224
+ """The first REP_COUNT names as code spans, then `…` when there are more."""
225
+ shown = ", ".join(code(n) for n in names[:REP_COUNT])
226
+ return shown + (", …" if len(names) > REP_COUNT else "")
227
+
228
+
229
+ # --------------------------------------------------------------------------
230
+ # Rollup
231
+ # --------------------------------------------------------------------------
232
+
233
+
234
+ def _directory(path) -> str | None:
235
+ """A path's directory, or None at the top level."""
236
+ if not isinstance(path, str) or not path:
237
+ return None
238
+ parent = posixpath.dirname(path.replace("\\", "/").rstrip("/"))
239
+ return parent or None
240
+
241
+
242
+ def plan_rows(items: list, key, path, eligible=lambda item: True, by_directory=lambda item: True) -> list[tuple]:
243
+ """The rows of one table in order: ("item", item) for a row of its own,
244
+ ("rollup", place, members) for a rollup row at its first member's place.
245
+
246
+ Eligible items of one key that share a path roll up when ROLLUP_MIN or
247
+ more do; then the eligible items left that by_directory admits and that
248
+ share a directory, the directory read as `<dir>/`.
249
+
250
+ >>> rows = plan_rows([{"f": "a.py"}] * 10 + [{"f": "b.py"}], key=lambda i: 0, path=lambda i: i["f"])
251
+ >>> [(r[0], r[1] if r[0] == "rollup" else r[1]["f"]) for r in rows]
252
+ [('rollup', 'a.py'), ('item', 'b.py')]
253
+ """
254
+ member_of: dict[int, int] = {}
255
+ groups: list[tuple[str, list[int]]] = []
256
+ for level in ("file", "directory"):
257
+ buckets: dict[tuple, list[int]] = {}
258
+ for index, item in enumerate(items):
259
+ if index in member_of or not eligible(item) or (level == "directory" and not by_directory(item)):
260
+ continue
261
+ place = path(item) if level == "file" else _directory(path(item))
262
+ if not isinstance(place, str) or not place:
263
+ continue
264
+ buckets.setdefault((key(item), place), []).append(index)
265
+ for (_, place), members in buckets.items():
266
+ if len(members) >= ROLLUP_MIN:
267
+ for index in members:
268
+ member_of[index] = len(groups)
269
+ groups.append((place if level == "file" else place + "/", members))
270
+ rows: list[tuple] = []
271
+ shown: set[int] = set()
272
+ for index, item in enumerate(items):
273
+ group = member_of.get(index)
274
+ if group is None:
275
+ rows.append(("item", item))
276
+ elif group not in shown:
277
+ shown.add(group)
278
+ place, members = groups[group]
279
+ rows.append(("rollup", place, [items[m] for m in members]))
280
+ return rows
281
+
282
+
283
+ # --------------------------------------------------------------------------
284
+ # Inputs
285
+ # --------------------------------------------------------------------------
286
+
287
+
288
+ def load_json(path: Path, what: str):
289
+ try:
290
+ return json.loads(path.read_text(encoding="utf-8"))
291
+ except FileNotFoundError:
292
+ raise RenderError(f"{path}: no such file ({what})") from None
293
+ except (OSError, UnicodeDecodeError) as exc:
294
+ raise RenderError(f"{path}: cannot read {what}: {exc}") from None
295
+ except json.JSONDecodeError as exc:
296
+ raise RenderError(f"{path}: {what} is not JSON: {exc}") from None
297
+
298
+
299
+ def _records(data: dict, key: str) -> list[dict]:
300
+ return [r for r in data.get(key) or [] if isinstance(r, dict)]
301
+
302
+
303
+ def _count(summary: dict, key: str) -> int:
304
+ value = summary.get(key, 0)
305
+ return value if isinstance(value, int) and not isinstance(value, bool) else 0
306
+
307
+
308
+ # --------------------------------------------------------------------------
309
+ # structural
310
+ # --------------------------------------------------------------------------
311
+
312
+
313
+ def _shape(rec: dict):
314
+ """An export's signature: its signature, else its params and return type."""
315
+ if rec.get("signature"):
316
+ return rec["signature"]
317
+ if rec.get("params") is None and not rec.get("return_type"):
318
+ return None
319
+ shape = _flat(rec.get("params") or [])
320
+ if rec.get("return_type"):
321
+ shape += f" -> {rec['return_type']}"
322
+ return shape
323
+
324
+
325
+ def _export_rows(records: list[dict], removed: bool) -> list[list[str]]:
326
+ """Added or Removed Exports rows; only removed ones roll up by directory."""
327
+ rows = []
328
+ plan = plan_rows(records, key=lambda r: r.get("source_library"), path=lambda r: r.get("file"),
329
+ by_directory=lambda r: removed)
330
+ for row in plan:
331
+ if row[0] == "item":
332
+ rec = row[1]
333
+ rows.append([export_cell(rec), text(rec.get("type")), code(_shape(rec)),
334
+ where(rec.get("file"), rec.get("line")), text(rec.get("confidence"))])
335
+ continue
336
+ _, place, members = row
337
+ library = members[0].get("source_library")
338
+ cell = f"{len(members)} exports (rep: {_rep([m.get('name') for m in members])})"
339
+ if library is not None:
340
+ cell = f"{text(library)}: {cell}"
341
+ rows.append([cell, ", ".join(text(t) for t in _distinct(m.get("type") for m in members)), NA,
342
+ code(place), _labels(m.get("confidence") for m in members)])
343
+ return rows
344
+
345
+
346
+ def _label(side) -> str:
347
+ side = side if isinstance(side, dict) else {}
348
+ return " / ".join(text(side.get(k)) if side.get(k) is not None else NONE
349
+ for k in ("confidence", "extraction_method"))
350
+
351
+
352
+ def _label_rows(records: list[dict]) -> list[list[str]]:
353
+ def pair(rec):
354
+ return tuple(_label(rec.get(side)).casefold() for side in ("baseline", "current"))
355
+
356
+ buckets: dict[tuple, list[int]] = {}
357
+ for index, rec in enumerate(records):
358
+ buckets.setdefault(pair(rec), []).append(index)
359
+ rolled = {members[0]: members for members in buckets.values() if len(members) >= ROLLUP_MIN}
360
+ skip = {i for members in rolled.values() for i in members[1:]}
361
+ rows = []
362
+ for index, rec in enumerate(records):
363
+ if index in skip:
364
+ continue
365
+ cell = export_cell(rec)
366
+ if index in rolled:
367
+ members = [records[i] for i in rolled[index]]
368
+ cell = f"{len(members)} exports (rep: {_rep([m.get('name') for m in members])})"
369
+ rows.append([cell, _label(rec.get("baseline")), _label(rec.get("current"))])
370
+ return rows
371
+
372
+
373
+ def check_file_drift(data) -> None:
374
+ """Raise RenderError unless data is a skf-compare-file-hashes.py compare result."""
375
+ if not isinstance(data, dict) or not all(isinstance(data.get(k), list) for k in ("added", "removed", "changed")):
376
+ raise RenderError("not a file drift result: expected an object with added, removed and changed lists "
377
+ "(the output of skf-compare-file-hashes.py compare)")
378
+
379
+
380
+ def _file_drift_section(data) -> str:
381
+ check_file_drift(data)
382
+ stats = data.get("stats") if isinstance(data.get("stats"), dict) else {}
383
+ items = [{"path": p, "change": "added", "detail": "new file"} for p in data["added"] if isinstance(p, str)]
384
+ items += [{"path": p, "change": "removed", "detail": "file removed"} for p in data["removed"]
385
+ if isinstance(p, str)]
386
+ items += [{"path": c.get("path"), "change": "changed",
387
+ "detail": f"content {c.get('stored_hash')} -> {c.get('current_hash')}"}
388
+ for c in data["changed"] if isinstance(c, dict)]
389
+ rows = []
390
+ for row in plan_rows(items, key=lambda i: i["change"], path=lambda i: i["path"],
391
+ by_directory=lambda i: i["change"] == "removed"):
392
+ if row[0] == "item":
393
+ item = row[1]
394
+ rows.append([code(item["path"]), item["change"], text(item["detail"])])
395
+ continue
396
+ _, place, members = row
397
+ names = [posixpath.basename(str(m["path"]).replace("\\", "/")) for m in members]
398
+ details = _distinct(m["detail"] for m in members)
399
+ rows.append([f"{code(place)} ({len(members)} files, rep: {_rep(names)})", members[0]["change"],
400
+ text(details[0]) if len(details) == 1 else NA])
401
+ heading = (f"### Script/Asset Drift (added {_count(stats, 'added')}, removed {_count(stats, 'removed')}, "
402
+ f"changed {_count(stats, 'changed')})")
403
+ return heading + "\n\n" + _table(("File", "Change", "Detail"), rows)
404
+
405
+
406
+ def render_structural(diff, file_drift=None, file_drift_skipped=None) -> str:
407
+ """The Structural Drift tables of a skf-structural-diff.py result (and,
408
+ given one, a skf-compare-file-hashes.py compare result, or the reason
409
+ one could not be read, which its skipped heading shows)."""
410
+ buckets = ("added", "removed", "changed", "moved")
411
+ if not isinstance(diff, dict) or not all(isinstance(diff.get(b), list) for b in buckets) \
412
+ or not isinstance(diff.get("summary"), dict):
413
+ raise RenderError("not a structural diff: expected an object with a summary and added, removed, changed "
414
+ "and moved lists (the output of skf-structural-diff.py)")
415
+ summary = diff["summary"]
416
+ parts = []
417
+
418
+ parts.append(f"### Added Exports ({_count(summary, 'added')})\n\n" + _table(
419
+ ("Export", "Type", "Signature", "Location", "Confidence"),
420
+ _export_rows(_records(diff, "added"), removed=False)))
421
+ parts.append(f"### Removed Exports ({_count(summary, 'removed')})\n\n" + _table(
422
+ ("Export", "Type", "Original Signature", "Original Location", "Confidence"),
423
+ _export_rows(_records(diff, "removed"), removed=True)))
424
+
425
+ moved = [[export_cell(m), where(m.get("previous_file"), m.get("previous_line")),
426
+ where(m.get("current_file"), m.get("line")), text(m.get("confidence"))]
427
+ for m in _records(diff, "moved")]
428
+ parts.append(f"### Moved Exports ({_count(summary, 'moved')})\n\n"
429
+ + _table(("Export", "From", "To", "Confidence"), moved))
430
+
431
+ changed = []
432
+ for c in _records(diff, "changed"):
433
+ field = c.get("field")
434
+ changed.append([export_cell(c), text("location" if field == "line" else field),
435
+ code(c.get("baseline_value")), code(c.get("current_value")),
436
+ where(c.get("file"), c.get("line")), text(c.get("confidence"))])
437
+ parts.append(f"### Changed Exports ({_count(summary, 'changed')})\n\n"
438
+ + _table(("Export", "Change Type", "Before", "After", "Location", "Confidence"), changed))
439
+
440
+ ambiguous = _records(diff, "ambiguous_names")
441
+ if ambiguous:
442
+ rows = []
443
+ for a in ambiguous:
444
+ sides = [", ".join(where(s.get("file"), s.get("line")) for s in a.get(side) or [] if isinstance(s, dict))
445
+ or NA for side in ("removed", "added")]
446
+ rows.append([export_cell(a), *sides])
447
+ parts.append(f"### Ambiguous Names ({_count(summary, 'ambiguous_names')})\n\n"
448
+ + _table(("Export", "Removed At", "Added At"), rows))
449
+
450
+ counts = [(label, _count(summary, key)) for label, key in
451
+ (("Added", "added"), ("Removed", "removed"), ("Moved", "moved"), ("Changed", "changed"))]
452
+ rows = [[label, str(n)] for label, n in counts] + [["**Total Drift Items**", str(sum(n for _, n in counts))]]
453
+ block = "### Summary\n\n" + _table(("Category", "Count"), rows)
454
+ unverified = _count(summary, "signature_unverified")
455
+ if unverified:
456
+ block += (f"\n\n**Signatures not compared:** {unverified} matched exports hold their signature in "
457
+ "different fields on the two sides, so a change there cannot be seen.")
458
+ parts.append(block)
459
+
460
+ groups = _records(diff, "groups")
461
+ if groups:
462
+ rows = [[text(g.get("source_library")) if g.get("source_library") is not None else NONE,
463
+ *(str(_count(g.get("summary") or {}, k)) for k in ("added", "removed", "moved", "changed"))]
464
+ for g in groups]
465
+ parts.append("### By Library\n\n" + _table(("Library", "Added", "Removed", "Moved", "Changed"), rows))
466
+
467
+ if file_drift_skipped is not None:
468
+ parts.append(f"### Script/Asset Drift: skipped ({file_drift_skipped})")
469
+ elif file_drift is not None:
470
+ parts.append(_file_drift_section(file_drift))
471
+
472
+ labels = _records(diff, "label_changes")
473
+ if labels:
474
+ parts.append(f"### Provenance label differences (not drift) ({_count(summary, 'label_changes')})\n\n"
475
+ "These rows are informational: a label names the tool that extracted the export, so they "
476
+ "are excluded from Total Drift Items and are not findings.\n\n"
477
+ + _table(("Export", "Baseline label", "Current label"), _label_rows(labels)))
478
+ return "\n\n".join(parts) + "\n"
479
+
480
+
481
+ # --------------------------------------------------------------------------
482
+ # severity
483
+ # --------------------------------------------------------------------------
484
+
485
+
486
+ def _norm(value):
487
+ return value.strip().lower() if isinstance(value, str) else None
488
+
489
+
490
+ def _weight(finding: dict) -> int:
491
+ count = finding.get("count", 1)
492
+ return count if isinstance(count, int) and not isinstance(count, bool) and count > 0 else 1
493
+
494
+
495
+ def _kind(finding: dict) -> str:
496
+ if _norm(finding.get("type")) == "semantic":
497
+ return "semantic"
498
+ if _norm(finding.get("category")) == "doc_source":
499
+ return "doc"
500
+ return "structural"
501
+
502
+
503
+ def _rolls_up(finding: dict) -> bool:
504
+ """Added and removed findings and Script/Asset files: the rows step 3 rolls up."""
505
+ return _norm(finding.get("type")) in ("added", "removed") or _norm(finding.get("category")) == "file"
506
+
507
+
508
+ def _rep_name(finding: dict):
509
+ name = finding.get("name")
510
+ if isinstance(name, str) and name == finding.get("file"):
511
+ return posixpath.basename(name.replace("\\", "/")) or name
512
+ return name
513
+
514
+
515
+ def _severity_rows(findings: list[dict]) -> list[list[str]]:
516
+ plan = plan_rows(findings,
517
+ key=lambda f: (_norm(f.get("type")), _norm(f.get("category")), f.get("source_library")),
518
+ path=lambda f: f.get("file"), eligible=_rolls_up,
519
+ by_directory=lambda f: _norm(f.get("type")) == "removed")
520
+ rows = []
521
+ for number, row in enumerate(plan, start=1):
522
+ if row[0] == "item":
523
+ f = row[1]
524
+ finding = export_cell(f)
525
+ if _weight(f) > 1:
526
+ finding += f" (×{_weight(f)})"
527
+ rows.append([str(number), finding, _kind(f), text(f.get("detail")), where(f.get("file"), f.get("line")),
528
+ text(f.get("confidence"))])
529
+ continue
530
+ _, place, members = row
531
+ first = members[0]
532
+ cell = (f"{text(first.get('type'))} in {code(place)} "
533
+ f"(×{sum(_weight(m) for m in members)}; rep: {_rep([_rep_name(m) for m in members])})")
534
+ if first.get("source_library") is not None:
535
+ cell = f"{text(first['source_library'])}: {cell}"
536
+ details = _distinct(m.get("detail") for m in members)
537
+ detail = details[0] if len(details) == 1 else first.get("rule")
538
+ rows.append([str(number), cell, _kind(first), text(detail), code(place),
539
+ _labels(m.get("confidence") for m in members)])
540
+ return rows
541
+
542
+
543
+ def render_severity(result) -> str:
544
+ """The Severity Classification section of a skf-severity-classify.py result."""
545
+ if not isinstance(result, dict) or not isinstance(result.get("findings"), list) \
546
+ or not isinstance(result.get("by_severity"), dict) or "drift_score" not in result:
547
+ raise RenderError("not a severity classification: expected an object with drift_score, by_severity and "
548
+ "findings (the output of skf-severity-classify.py)")
549
+ by_severity = result["by_severity"]
550
+ findings = [f for f in result["findings"] if isinstance(f, dict)]
551
+ parts = [f"**Overall Drift Score: {text(result['drift_score'])}**"]
552
+ header = ("#", "Finding", "Type", "Detail", "Location", "Confidence")
553
+ for level in SEVERITIES:
554
+ rows = _severity_rows([f for f in findings if f.get("severity") == level])
555
+ parts.append(f"### {level} ({_count(by_severity, level)})\n\n" + _table(header, rows))
556
+ rows = [[level, str(_count(by_severity, level))] for level in SEVERITIES]
557
+ rows.append(["**Total**", str(_count(result, "total_items"))])
558
+ parts.append("### Classification Summary\n\n" + _table(("Severity", "Count"), rows))
559
+ return "\n\n".join(parts) + "\n"
560
+
561
+
562
+ # --------------------------------------------------------------------------
563
+ # outside-scope
564
+ # --------------------------------------------------------------------------
565
+
566
+
567
+ def _strings(values) -> list[str]:
568
+ return [v for v in values if isinstance(v, str) and v] if isinstance(values, list) else []
569
+
570
+
571
+ def render_outside_scope(snapshot) -> str:
572
+ """The Out-of-Scope New Public API table of an extraction snapshot, or
573
+ nothing when its outside_scope is empty or absent.
574
+
575
+ >>> render_outside_scope({"exports": [], "outside_scope": [
576
+ ... {"path": "src/b.ts", "names": ["Beta"], "entries": ["index.ts"]}]}).splitlines()[-1]
577
+ '| `src/b.ts` | exports `Beta` through `index.ts` |'
578
+ """
579
+ if not isinstance(snapshot, dict) or not isinstance(snapshot.get("exports"), list) \
580
+ or not isinstance(snapshot.get("outside_scope", []), list):
581
+ raise RenderError("not an extraction snapshot: expected an object with an exports list and, when present, "
582
+ "an outside_scope list (the output of skf-extraction-snapshot.py build)")
583
+ rows = []
584
+ for item in _records(snapshot, "outside_scope"):
585
+ names = _strings(item.get("names"))
586
+ if not isinstance(item.get("path"), str) or not item["path"] or not names:
587
+ continue
588
+ evidence = "exports " + ", ".join(code(n) for n in names)
589
+ entries = _strings(item.get("entries"))
590
+ if entries:
591
+ evidence += " through " + ", ".join(code(e) for e in entries)
592
+ rows.append([code(item["path"]), evidence])
593
+ if not rows:
594
+ return ""
595
+ return "### Out-of-Scope New Public API\n\n" + _table(("Path", "Evidence"), rows) + "\n"
596
+
597
+
598
+ # --------------------------------------------------------------------------
599
+ # CLI
600
+ # --------------------------------------------------------------------------
601
+
602
+
603
+ def _named(path: Path, func, *args):
604
+ """func(*args), a RenderError it raises starting with the input's path."""
605
+ try:
606
+ return func(*args)
607
+ except RenderError as exc:
608
+ raise RenderError(f"{path}: {exc}") from None
609
+
610
+
611
+ def _structural(args) -> str:
612
+ path = Path(args.diff)
613
+ diff = load_json(path, "the structural diff")
614
+ # The Script/Asset Drift check is supplementary: a saved file it cannot
615
+ # read skips its table, never the export tables.
616
+ file_drift = skipped = None
617
+ if args.file_drift is not None and Path(args.file_drift).is_file():
618
+ drift_path = Path(args.file_drift)
619
+ try:
620
+ file_drift = load_json(drift_path, "the file drift result")
621
+ _named(drift_path, check_file_drift, file_drift)
622
+ except RenderError as exc:
623
+ file_drift, skipped = None, str(exc)
624
+ return _named(path, render_structural, diff, file_drift, skipped)
625
+
626
+
627
+ def _severity(args) -> str:
628
+ path = Path(args.classification)
629
+ return _named(path, render_severity, load_json(path, "the severity classification"))
630
+
631
+
632
+ def _outside_scope(args) -> str:
633
+ path = Path(args.snapshot)
634
+ return _named(path, render_outside_scope, load_json(path, "the extraction snapshot"))
635
+
636
+
637
+ def _build_parser() -> argparse.ArgumentParser:
638
+ parser = argparse.ArgumentParser(
639
+ prog="render-drift-tables.py",
640
+ description=__doc__,
641
+ formatter_class=argparse.RawDescriptionHelpFormatter,
642
+ )
643
+ sub = parser.add_subparsers(dest="command", required=True)
644
+ structural = sub.add_parser("structural", help="Print step 3's Structural Drift tables from structural-diff.json")
645
+ structural.add_argument("diff", help="structural-diff.json, the skf-structural-diff.py result step 3 saved")
646
+ structural.add_argument("--file-drift", default=None,
647
+ help="file-drift.json, step 3 section 4b's comparison: its Script/Asset Drift table "
648
+ "(none when the file does not exist, a skipped heading when it cannot be read)")
649
+ structural.set_defaults(func=_structural)
650
+ severity = sub.add_parser("severity", help="Print step 5's Severity Classification section from severity.json")
651
+ severity.add_argument("classification", help="severity.json, the skf-severity-classify.py result step 5 saved")
652
+ severity.set_defaults(func=_severity)
653
+ outside = sub.add_parser("outside-scope",
654
+ help="Print step 6's Out-of-Scope New Public API table from extraction-snapshot.json")
655
+ outside.add_argument("snapshot", help="extraction-snapshot.json, the snapshot step 2 wrote (nothing is printed "
656
+ "when its outside_scope is empty)")
657
+ outside.set_defaults(func=_outside_scope)
658
+ return parser
659
+
660
+
661
+ def _force_utf8(*streams) -> None:
662
+ """Reconfigure the output streams to UTF-8 (a Windows console uses cp1252)."""
663
+ for stream in streams:
664
+ if hasattr(stream, "reconfigure"):
665
+ errors = getattr(stream, "errors", None)
666
+ if errors is None:
667
+ stream.reconfigure(encoding="utf-8")
668
+ else:
669
+ stream.reconfigure(encoding="utf-8", errors=errors)
670
+
671
+
672
+ def main(argv: list[str] | None = None) -> int:
673
+ args = _build_parser().parse_args(argv)
674
+ try:
675
+ output = args.func(args)
676
+ except RenderError as exc:
677
+ print(json.dumps({"status": "error", "error": str(exc)}))
678
+ return 1
679
+ sys.stdout.write(output)
680
+ return 0
681
+
682
+
683
+ if __name__ == "__main__":
684
+ _force_utf8(sys.stdout, sys.stderr)
685
+ sys.exit(main())