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