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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.10"
3
+ # requires-python = ">=3.11"
4
4
  # ///
5
5
  """Deterministic Documented-vs-Source coverage reconciliation.
6
6
 
@@ -12,28 +12,35 @@ already-extracted name lists (the §1 documented inventory and the §2 source
12
12
  barrel) into a reproducible {documented, missing, stale, exportCoverage}
13
13
  result, so the numerator no longer swings between runs on split-body skills.
14
14
 
15
- It covers the three branches §2c enumerates:
15
+ It covers the four branches §2c enumerates:
16
16
 
17
17
  * "barrel" (enumerated path) — intersect documented_set against the source
18
18
  barrel_set: Documented = |documented_set ∩ barrel_set|,
19
19
  Missing = barrel_set − documented_set, Stale = documented_set − barrel_set,
20
20
  Export Coverage = |Documented| / |barrel_set| * 100.
21
21
 
22
- * "scalar" (§4 priority-1 effective_denominator, no enumerated name set) —
23
- grep each documented name across SKILL.md ∪ references/*.md;
22
+ * "scalar" (§2b's scalar branch, stats.effective_denominator, no enumerated name set):
23
+ look each documented name up in SKILL.md ∪ references/*.md;
24
24
  Documented = count present, denominator = effective_denominator,
25
25
  Missing = max(0, denominator − Documented), Stale not enumerable (empty),
26
26
  Export Coverage = min(100, Documented / denominator * 100).
27
27
 
28
- * "stack" (skill_type == "stack", empty source barrel) — grep each
28
+ * "stack" (skill_type == "stack", empty source barrel): look each
29
29
  composition-surface name (provenance-map cited contracts, ::-excluded, or
30
- libraries + integration_pairs — resolved upstream) across
30
+ libraries + integration_pairs, from load-coverage-inputs.py) up in
31
31
  SKILL.md ∪ references/*.md; denominator = stack_denominator,
32
32
  Missing = max(0, denominator − Documented), Stale not enumerable,
33
33
  Export Coverage = min(100, Documented / stack_denominator * 100).
34
34
 
35
- The two grep branches bound their outputs because the numerator and the
36
- denominator are independent measures of different sets — the grep count can
35
+ * "docsOnly" (a skill whose citations are all [EXT:...], or no source
36
+ access): no source to compare against, so Export Coverage measures
37
+ documentation completeness: complete items / documented items * 100,
38
+ one item per name and kind of the §1 inventory, complete when it has a
39
+ description and, for a function or method, params and a return type
40
+ (validate-inventory.py's rule).
41
+
42
+ The two lookup branches bound their outputs because the numerator and the
43
+ denominator are independent measures of different sets: the lookup count can
37
44
  exceed a consumer-surface denominator without either being wrong. Unbounded,
38
45
  that produced a negative Missing and a >100% coverage that compute-score.py
39
46
  rejects as out of range. `documented` is left as the true count (it is reported
@@ -41,54 +48,139 @@ verbatim as "Documented in SKILL.md"); the derived ratio is what gets bounded,
41
48
  and `numeratorSurplus` / `coverageUncapped` / `coverageCapped` report the
42
49
  overshoot so a deflated denominator stays visible instead of being swallowed.
43
50
 
51
+ A name counts as written when SKILL.md or a .md file directly in references/
52
+ holds it as a fixed string, case-sensitive, never inside a longer identifier
53
+ (`get` is not found in `target` or `getAll`): the rule validate-inventory.py
54
+ defines, which this script loads from beside it.
55
+
44
56
  CLI usage:
57
+ uv run reconcile-coverage.py --denominator-source <source> [file flags] --output <file>
45
58
  uv run reconcile-coverage.py '<JSON>' # JSON literal positional
46
59
  uv run reconcile-coverage.py --json-input '<JSON>' # explicit flag form
47
60
  cat input.json | uv run reconcile-coverage.py --stdin # piped input
48
61
 
62
+ File flags read the inputs from the run folder instead of a JSON payload;
63
+ each sets the input field it names and wins over the same field in the JSON:
64
+ --denominator-source denominatorSource
65
+ --denominator-value denominatorValue (scalar), when no --coverage-inputs
66
+ file gives it
67
+ --skill-dir skillPackagePath (scalar, stack)
68
+ --inventory PATH exports: the validated inventory validate-inventory.py
69
+ wrote with --output
70
+ --surface PATH barrelSet: one name set of load-coverage-inputs.py
71
+ surface, chosen by --surface-set (default "all")
72
+ --coverage-inputs PATH load-coverage-inputs.py metadata output: for the
73
+ scalar branch, denominatorValue from its
74
+ `effectiveDenominator` (stats.effective_denominator);
75
+ for the stack branch, compositionNames and
76
+ denominatorValue from its `stack` object
77
+ --verified PATH scalar: verify-declared-numerator.py's result; when it
78
+ found the declared count inflated, its `verified`
79
+ count is the numerator
80
+ --output PATH also write the result to this file (UTF-8 JSON); a
81
+ refused input removes a file left there earlier
82
+
49
83
  Input schema (one object):
50
84
  {
51
- "denominatorSource": "barrel" | "scalar" | "stack", # required
85
+ "denominatorSource": "barrel" | "scalar" | "stack" | "docsOnly", # required
52
86
  "exports": [ {"name": "...", "kind": "..."}, ... ], # §1 inventory
53
87
  "barrelSet": ["a", "b", ...], # barrel: resolved name set
54
88
  "perFileResults": [ {"exports_found": ["a", ...]} ], # barrel: union'd if no barrelSet
55
89
  "denominatorValue": <int>, # scalar/stack: resolved denominator
56
- "compositionNames": ["lib::x", ...], # stack: names to grep
57
- "skillPackagePath": "/path/to/skill" # scalar/stack: SKILL.md ∪ references
90
+ "compositionNames": ["lib::x", ...], # stack: names to look up
91
+ "skillPackagePath": "/path/to/skill", # scalar/stack: SKILL.md ∪ references
92
+ "verifiedNumerator": <int> # scalar: overrides the lookup count
58
93
  }
59
94
 
60
95
  Output (stdout, one object):
61
96
  {
62
- "branch": "enumerated" | "scalar" | "stack",
63
- "denominatorSource": "barrel" | "scalar" | "stack",
97
+ "branch": "enumerated" | "scalar" | "stack" | "docsOnly",
98
+ "denominatorSource": "barrel" | "scalar" | "stack" | "docsOnly",
64
99
  "denominator": <int>,
65
100
  "documented": <int>,
66
- "missing": [names], # enumerated: source names not documented; scalar/stack: []
101
+ "documentedNames": [names], # enumerated only: the documented names the denominator counts
102
+ "missing": [names], # enumerated: source names not documented; others: []
67
103
  "missingCount": <int>, # always present; never negative
68
- "stale": [names], # enumerated: documented names not in source; scalar/stack: []
104
+ "stale": [names], # enumerated: documented names not in source; others: []
69
105
  "staleCount": <int>, # always present
70
- "staleApplicable": <bool>, # false for scalar/stack (no barrel to enumerate)
106
+ "staleApplicable": <bool>, # false for scalar/stack/docsOnly (no barrel to enumerate)
71
107
  "exportCoverage": <float>, # scalar/stack: capped at 100
72
108
  "numeratorSurplus": <int>, # scalar/stack only: max(0, documented − denominator)
73
109
  "coverageUncapped": <float>, # scalar/stack only: the ratio before the cap
74
- "coverageCapped": <bool> # scalar/stack only: true when the cap bound the ratio
110
+ "coverageCapped": <bool>, # scalar/stack only: true when the cap bound the ratio
111
+ "numeratorSource": "lookup" | "verified", # scalar only
112
+ "incomplete": [{"name", "kind", "missing"}] # docsOnly only: the incomplete items
75
113
  }
76
114
 
77
115
  Exit codes:
78
- 0 — reconciliation emitted successfully
79
- 1 — no input / input could not be parsed as JSON
80
- 2 — input parsed but schema/semantics invalid (error object emitted as JSON)
116
+ 0 reconciliation emitted successfully
117
+ 1 no input, input that does not parse as JSON, or an input file that cannot be read
118
+ 2 input parsed but schema/semantics invalid (error object emitted as JSON)
119
+
120
+ Subcommand render (coverage-check.md §5 writes its output unchanged):
121
+ uv run reconcile-coverage.py render --coverage <coverage.json>
122
+ [--surface <surface.json>] [--signatures <signatures.json>]
123
+
124
+ prints the Markdown of the Coverage Analysis section that follows its
125
+ header lines, from the run files:
126
+ **Files Analyzed:** the distinct files the --surface export records name
127
+ (N/A without --surface: a docs-only run has none)
128
+ ### Export Coverage on the enumerated branch, one row per name the
129
+ denominator counts, per stale name and per name whose
130
+ signature mismatches, by name:
131
+ Type the first kind the surface records for the name, else -
132
+ Documented no for a missing name, else yes
133
+ Signature no for a mismatch, yes for a compared name that
134
+ matches, unverified for a documented name when no
135
+ --signatures was given, - otherwise
136
+ Source Location the file:line the surface records for the name, a
137
+ record with a line preferred (gap-ledger.py's rule
138
+ for a missing export's Source), else -
139
+ Status FAIL for a signature mismatch, WARN for a missing or
140
+ stale name, PASS otherwise
141
+ The scalar and stack branches count without naming
142
+ the exports, and a docs-only run has no source
143
+ surface: one line says so instead (NO_TABLE).
144
+ ### Coverage Summary the --coverage counts, the --signatures mismatch count
145
+ and warnings, the numerator surplus when the ratio
146
+ was capped, and the names the --surface leaves out
147
+ or puts back: `excluded.outsideScope` (scoped out
148
+ by the brief), `excluded.nestedEntries` (a nested
149
+ Python entry point's names) and
150
+ `guards.staleScope.restored`, each line only when
151
+ it names one
152
+ ### Category Scores Export Coverage, and Signature Accuracy and Type
153
+ Coverage from --signatures (N/A without it)
154
+ Exit 0 when it printed; 1 when an input cannot be read or is not the
155
+ result of the script that writes it (one line on stderr); 2 a usage error.
81
156
  """
82
157
 
83
158
  from __future__ import annotations
84
159
 
85
160
  import argparse
161
+ import importlib.util
86
162
  import json
87
163
  import math
88
164
  import sys
89
165
  from pathlib import Path
90
166
 
91
- VALID_SOURCES = ("barrel", "scalar", "stack")
167
+ VALID_SOURCES = ("barrel", "scalar", "stack", "docsOnly")
168
+
169
+ _INVENTORY_MODULE = None
170
+
171
+
172
+ def _inventory():
173
+ """validate-inventory.py beside this script: the name match and completeness rules live there."""
174
+ global _INVENTORY_MODULE
175
+ if _INVENTORY_MODULE is None:
176
+ sibling = Path(__file__).resolve().parent / "validate-inventory.py"
177
+ spec = importlib.util.spec_from_file_location("skf_validate_inventory", sibling)
178
+ if spec is None or spec.loader is None or not sibling.is_file():
179
+ raise ImportError(f"validate-inventory.py not found beside {Path(__file__).name}")
180
+ module = importlib.util.module_from_spec(spec)
181
+ spec.loader.exec_module(module)
182
+ _INVENTORY_MODULE = module
183
+ return _INVENTORY_MODULE
92
184
 
93
185
 
94
186
  def round2(value):
@@ -135,34 +227,17 @@ def build_barrel_set(inp):
135
227
  return barrel
136
228
 
137
229
 
138
- # --- Grep (documented-name presence) ---------------------------------------
230
+ # --- Documented-name lookup ---------------------------------------------------
139
231
 
140
232
 
141
233
  def load_doc_text(skill_package_path):
142
- """Concatenate SKILL.md ∪ references/*.md text for name-presence grepping.
143
-
144
- Deterministic: references are read in sorted order. Reads as UTF-8 so
145
- non-ASCII exports do not mojibake on Windows (cp1252 default).
146
- """
147
- root = Path(skill_package_path)
148
- parts = []
149
- skill_md = root / "SKILL.md"
150
- if skill_md.is_file():
151
- parts.append(skill_md.read_text(encoding="utf-8"))
152
- refs_dir = root / "references"
153
- if refs_dir.is_dir():
154
- for ref in sorted(refs_dir.glob("*.md")):
155
- parts.append(ref.read_text(encoding="utf-8"))
156
- return "\n".join(parts)
234
+ """SKILL.md ∪ references/*.md text, as validate-inventory.py reads it."""
235
+ return _inventory().load_doc_text(skill_package_path)
157
236
 
158
237
 
159
238
  def names_present(names, doc_text):
160
- """Return the sorted, de-duplicated set of `names` that appear in doc_text.
161
-
162
- Substring containment, case-sensitive — mirrors `grep "{name}"`.
163
- """
164
- present = {n for n in names if isinstance(n, str) and n and n in doc_text}
165
- return sorted(present)
239
+ """The sorted, de-duplicated `names` the doc text writes (validate-inventory.py's match)."""
240
+ return _inventory().names_present(names, doc_text)
166
241
 
167
242
 
168
243
  # --- Core reconciliation ----------------------------------------------------
@@ -180,17 +255,46 @@ def _validate(inp):
180
255
  if source == "barrel":
181
256
  if inp.get("barrelSet") is None and inp.get("perFileResults") is None:
182
257
  return "barrel branch requires either `barrelSet` or `perFileResults`"
258
+ elif source == "docsOnly":
259
+ if not isinstance(inp.get("exports"), list):
260
+ return "docsOnly branch requires the §1 inventory `exports` (or --inventory)"
183
261
  else: # scalar / stack
184
262
  dv = inp.get("denominatorValue")
185
263
  if not isinstance(dv, int) or isinstance(dv, bool) or dv < 0:
186
264
  return f"{source} branch requires integer `denominatorValue` >= 0"
187
265
  if not inp.get("skillPackagePath"):
188
- return f"{source} branch requires `skillPackagePath` to grep SKILL.md ∪ references"
266
+ return f"{source} branch requires `skillPackagePath` to read SKILL.md ∪ references"
189
267
  if source == "stack" and inp.get("compositionNames") is None:
190
- return "stack branch requires `compositionNames` (composition-surface names to grep)"
268
+ return "stack branch requires `compositionNames` (composition-surface names to look up)"
269
+ verified = inp.get("verifiedNumerator")
270
+ if verified is not None and (not isinstance(verified, int) or isinstance(verified, bool) or verified < 0):
271
+ return "verifiedNumerator must be an integer >= 0"
191
272
  return None
192
273
 
193
274
 
275
+ def _docs_only(inp):
276
+ tally = _inventory().completeness(inp["exports"])
277
+ total = tally["total"]
278
+ if total == 0:
279
+ return make_error(
280
+ "docsOnly branch: the inventory documents no item, so Export Coverage is "
281
+ "undefined; the §2b docs-only guard should HALT"
282
+ )
283
+ return {
284
+ "branch": "docsOnly",
285
+ "denominatorSource": "docsOnly",
286
+ "denominator": total,
287
+ "documented": tally["complete"],
288
+ "missing": [],
289
+ "missingCount": total - tally["complete"],
290
+ "stale": [],
291
+ "staleCount": 0,
292
+ "staleApplicable": False,
293
+ "exportCoverage": round2(tally["complete"] / total * 100),
294
+ "incomplete": tally["incomplete"],
295
+ }
296
+
297
+
194
298
  def reconcile(inp, doc_text=None):
195
299
  """Pure reconciliation. `doc_text` may be injected (tests); else loaded from disk."""
196
300
  err = _validate(inp)
@@ -199,6 +303,9 @@ def reconcile(inp, doc_text=None):
199
303
 
200
304
  source = inp["denominatorSource"]
201
305
 
306
+ if source == "docsOnly":
307
+ return _docs_only(inp)
308
+
202
309
  if source == "barrel":
203
310
  documented_set = build_documented_set(inp.get("exports"))
204
311
  barrel_set = build_barrel_set(inp)
@@ -217,6 +324,7 @@ def reconcile(inp, doc_text=None):
217
324
  "denominatorSource": source,
218
325
  "denominator": denominator,
219
326
  "documented": documented,
327
+ "documentedNames": sorted(documented_names),
220
328
  "missing": missing,
221
329
  "missingCount": len(missing),
222
330
  "stale": stale,
@@ -225,39 +333,43 @@ def reconcile(inp, doc_text=None):
225
333
  "exportCoverage": round2(documented / denominator * 100),
226
334
  }
227
335
 
228
- # scalar / stack — grep-based numerator against SKILL.md ∪ references
336
+ # scalar / stack: looked-up numerator against SKILL.md ∪ references
229
337
  denominator = inp["denominatorValue"]
230
- if doc_text is None:
231
- doc_text = load_doc_text(inp["skillPackagePath"])
232
-
233
- if source == "scalar":
234
- candidates = sorted(build_documented_set(inp.get("exports")))
235
- else: # stack
236
- candidates = [n for n in inp["compositionNames"] if isinstance(n, str) and n]
237
-
238
- present = names_present(candidates, doc_text)
239
- documented = len(present)
240
-
241
338
  if denominator == 0:
242
339
  return make_error(
243
340
  f"{source} branch: denominatorValue is 0 — Export Coverage is undefined; "
244
341
  "upstream §2b guard should HALT before reconciliation"
245
342
  )
246
343
 
247
- # The grep numerator and the resolved denominator are independent measures,
248
- # so `documented` can legitimately exceed `denominator` — a consumer-surface
249
- # denominator counts one surface while the documented body may also name
250
- # migration aliases, re-exported sibling symbols, and other extras. Left
251
- # unbounded that yields a negative Missing and >100% coverage (which
252
- # compute-score.py then rejects outright as out of range).
344
+ numerator_source = "lookup"
345
+ if source == "scalar" and inp.get("verifiedNumerator") is not None:
346
+ # §2b's numerator ground truth found the declared count padded: its
347
+ # verified count is authoritative and replaces the lookup.
348
+ documented = inp["verifiedNumerator"]
349
+ numerator_source = "verified"
350
+ else:
351
+ if doc_text is None:
352
+ doc_text = load_doc_text(inp["skillPackagePath"])
353
+ if source == "scalar":
354
+ candidates = sorted(build_documented_set(inp.get("exports")))
355
+ else: # stack
356
+ candidates = [n for n in inp["compositionNames"] if isinstance(n, str) and n]
357
+ documented = len(names_present(candidates, doc_text))
358
+
359
+ # The looked-up numerator and the resolved denominator are independent
360
+ # measures, so `documented` can legitimately exceed `denominator`: a
361
+ # consumer-surface denominator counts one surface while the documented body
362
+ # may also name migration aliases, re-exported sibling symbols, and other
363
+ # extras. Left unbounded that yields a negative Missing and >100% coverage
364
+ # (which compute-score.py then rejects outright as out of range).
253
365
  #
254
- # `documented` stays the true grep count: it is reported as "Documented in
366
+ # `documented` stays the true count: it is reported as "Documented in
255
367
  # SKILL.md", so capping it would print a number that was never measured.
256
368
  # The derived ratio is what gets bounded, and the surplus is surfaced rather
257
369
  # than swallowed so a deflated denominator stays visible.
258
370
  surplus = max(0, documented - denominator)
259
371
  coverage_uncapped = round2(documented / denominator * 100)
260
- return {
372
+ result = {
261
373
  "branch": source,
262
374
  "denominatorSource": source,
263
375
  "denominator": denominator,
@@ -272,6 +384,302 @@ def reconcile(inp, doc_text=None):
272
384
  "coverageUncapped": coverage_uncapped,
273
385
  "coverageCapped": surplus > 0,
274
386
  }
387
+ if source == "scalar":
388
+ result["numeratorSource"] = numerator_source
389
+ return result
390
+
391
+
392
+ # --- The Coverage Analysis section (render) -----------------------------------
393
+
394
+ BRANCHES = ("enumerated", "scalar", "stack", "docsOnly")
395
+ # Why a branch names no export: the line it writes in place of the table, and its stale count.
396
+ NO_NAMES = {
397
+ "scalar": "the scalar denominator (`stats.effective_denominator`) counts the exports without naming them",
398
+ "stack": "the stack denominator counts the composition surface without naming each export",
399
+ "docsOnly": "a docs-only run has no source surface",
400
+ }
401
+ NO_TABLE = {
402
+ "scalar": f"No per-export table: {NO_NAMES['scalar']}. The summary below gives the counts.",
403
+ "stack": f"No per-export table: {NO_NAMES['stack']}. The summary below gives the counts.",
404
+ "docsOnly": f"No per-export table: {NO_NAMES['docsOnly']} to compare against. The summary below gives its "
405
+ "documentation completeness.",
406
+ }
407
+
408
+ _GAP_LEDGER_MODULE = None
409
+
410
+
411
+ class RenderError(Exception):
412
+ """A render input that is not the result of the script that writes it."""
413
+
414
+
415
+ def _gap_ledger():
416
+ """gap-ledger.py beside this script: its rule for a missing export's Source gives the Source Location."""
417
+ global _GAP_LEDGER_MODULE
418
+ if _GAP_LEDGER_MODULE is None:
419
+ sibling = Path(__file__).resolve().parent / "gap-ledger.py"
420
+ spec = importlib.util.spec_from_file_location("skf_gap_ledger_render", sibling)
421
+ if spec is None or spec.loader is None or not sibling.is_file():
422
+ raise RenderError(f"gap-ledger.py not found beside {Path(__file__).name}")
423
+ module = importlib.util.module_from_spec(spec)
424
+ spec.loader.exec_module(module)
425
+ _GAP_LEDGER_MODULE = module
426
+ return _GAP_LEDGER_MODULE
427
+
428
+
429
+ def _pct(value):
430
+ """A score as the report writes it.
431
+
432
+ >>> _pct(60.0), _pct(66.67), _pct(None)
433
+ ('60%', '66.67%', 'N/A')
434
+ """
435
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
436
+ return "N/A"
437
+ return f"{int(value)}%" if float(value).is_integer() else f"{value}%"
438
+
439
+
440
+ def _cell(text):
441
+ """A table cell: a `|` would end it."""
442
+ return str(text).replace("|", "\\|")
443
+
444
+
445
+ def _string_list(data, key, label):
446
+ value = data.get(key)
447
+ if value is None:
448
+ return []
449
+ if not isinstance(value, list):
450
+ raise RenderError(f"{label}: `{key}` is not a list")
451
+ return [v for v in value if isinstance(v, str) and v]
452
+
453
+
454
+ def _incomplete(item):
455
+ """A docs-only item and the fields it lacks.
456
+
457
+ >>> _incomplete({"name": "load", "kind": "function", "missing": ["params", "return_type"]})
458
+ '`load` (function) lacks `params`, `return_type`'
459
+ """
460
+ fields = ", ".join(f"`{field}`" for field in item.get("missing") or [])
461
+ return f"`{item.get('name')}` ({item.get('kind')}) lacks {fields}"
462
+
463
+
464
+ def _left_out(surface):
465
+ """The Coverage Summary lines for the names the surface leaves out or puts back, each only when it names one.
466
+
467
+ >>> for line in _left_out({
468
+ ... "excluded": {"outsideScope": [{"name": "gen", "file": "src/gen.ts"}],
469
+ ... "nestedEntries": [{"entry": "pkg/api/__init__.py", "within": "pkg/__init__.py",
470
+ ... "names": ["router"]}]},
471
+ ... "guards": {"staleScope": {"restored": [{"name": "old", "file": "src/old.ts", "line": 4}]}}}):
472
+ ... print(line)
473
+ - **Scoped Out by the Brief:** `gen` (src/gen.ts)
474
+ - **Nested Entry Points:** `pkg/api/__init__.py` inside `pkg/__init__.py`: `router`
475
+ - **Restored by the Stale-Scope Guard:** `old` (src/old.ts:4)
476
+ >>> _left_out({"excluded": {"outsideScope": [], "nestedEntries": []}, "guards": None})
477
+ []
478
+ """
479
+ def items(value):
480
+ return [i for i in value if isinstance(i, dict)] if isinstance(value, list) else []
481
+
482
+ def where(item):
483
+ file, line = item.get("file"), item.get("line")
484
+ if not isinstance(file, str) or not file:
485
+ return ""
486
+ return f" ({file}:{line})" if isinstance(line, int) and not isinstance(line, bool) else f" ({file})"
487
+
488
+ excluded = surface.get("excluded") if isinstance(surface.get("excluded"), dict) else {}
489
+ guards = surface.get("guards") if isinstance(surface.get("guards"), dict) else {}
490
+ stale_scope = guards.get("staleScope") if isinstance(guards.get("staleScope"), dict) else {}
491
+ lines = []
492
+ scoped = [f"`{i['name']}`{where(i)}" for i in items(excluded.get("outsideScope"))
493
+ if isinstance(i.get("name"), str)]
494
+ if scoped:
495
+ lines.append(f"- **Scoped Out by the Brief:** {', '.join(scoped)}")
496
+ nested = [f"`{i['entry']}` inside `{i.get('within')}`: "
497
+ + ", ".join(f"`{n}`" for n in i.get("names") or [] if isinstance(n, str))
498
+ for i in items(excluded.get("nestedEntries")) if isinstance(i.get("entry"), str)]
499
+ if nested:
500
+ lines.append(f"- **Nested Entry Points:** {'; '.join(nested)}")
501
+ restored = [f"`{i['name']}`{where(i)}" for i in items(stale_scope.get("restored"))
502
+ if isinstance(i.get("name"), str)]
503
+ if restored:
504
+ lines.append(f"- **Restored by the Stale-Scope Guard:** {', '.join(restored)}")
505
+ return lines
506
+
507
+
508
+ def _rows(coverage, surface, signatures):
509
+ """The Export Coverage table of the enumerated branch."""
510
+ if "documentedNames" not in coverage:
511
+ raise RenderError("--coverage has no `documentedNames`: write it again with this reconcile-coverage.py")
512
+ documented = set(_string_list(coverage, "documentedNames", "--coverage"))
513
+ missing = set(_string_list(coverage, "missing", "--coverage"))
514
+ stale = set(_string_list(coverage, "stale", "--coverage"))
515
+ mismatched, compared = set(), set()
516
+ if signatures is not None:
517
+ mismatched = {m["name"] for m in signatures.get("mismatches") or []
518
+ if isinstance(m, dict) and isinstance(m.get("name"), str)}
519
+ compared = set(_string_list(signatures, "comparedNames", "--signatures"))
520
+ kinds, sources = {}, {}
521
+ if surface is not None:
522
+ for record in surface["exports"]:
523
+ if isinstance(record, dict) and isinstance(record.get("name"), str) and record.get("kind"):
524
+ kinds.setdefault(record["name"], str(record["kind"]))
525
+ sources = _gap_ledger()._surface_sources(surface)
526
+ rows = ["| Export | Type | Documented | Signature | Source Location | Status |",
527
+ "|--------|------|------------|-----------|-----------------|--------|"]
528
+ for name in sorted(documented | missing | stale | mismatched):
529
+ if name in mismatched:
530
+ signature = "no"
531
+ elif name in compared:
532
+ signature = "yes"
533
+ elif signatures is None and name not in missing:
534
+ signature = "unverified"
535
+ else:
536
+ signature = "-"
537
+ status = "FAIL" if name in mismatched else "WARN" if name in missing or name in stale else "PASS"
538
+ rows.append(f"| `{_cell(name)}` | {_cell(kinds.get(name, '-'))} | {'no' if name in missing else 'yes'} "
539
+ f"| {signature} | {_cell(sources.get(name, '-'))} | {status} |")
540
+ return rows
541
+
542
+
543
+ def render(coverage, surface=None, signatures=None):
544
+ """The Coverage Analysis section after its header lines, as Markdown (see the module docstring)."""
545
+ if not isinstance(coverage, dict) or coverage.get("branch") not in BRANCHES:
546
+ raise RenderError("--coverage is not reconcile-coverage.py's result (no known `branch`)")
547
+ if surface is not None and not (isinstance(surface, dict) and isinstance(surface.get("exports"), list)):
548
+ raise RenderError("--surface is not load-coverage-inputs.py surface's result (no `exports` list)")
549
+ if signatures is not None and not (isinstance(signatures, dict) and signatures.get("valid") is True):
550
+ raise RenderError("--signatures is not score-signatures.py score's result (no `valid: true`)")
551
+ branch = coverage["branch"]
552
+ docs_only = branch == "docsOnly"
553
+
554
+ if surface is None:
555
+ files = "N/A: no source surface"
556
+ else:
557
+ files = len({r["file"] for r in surface["exports"]
558
+ if isinstance(r, dict) and isinstance(r.get("file"), str) and r["file"]})
559
+ lines = [f"**Files Analyzed:** {files}", "", "### Export Coverage", ""]
560
+ lines += _rows(coverage, surface, signatures) if branch == "enumerated" else [NO_TABLE[branch]]
561
+
562
+ denominator, documented = coverage.get("denominator"), coverage.get("documented")
563
+ missing_count, coverage_pct = coverage.get("missingCount"), _pct(coverage.get("exportCoverage"))
564
+ lines += ["", "### Coverage Summary", ""]
565
+ if docs_only:
566
+ incomplete = [_incomplete(item) for item in coverage.get("incomplete") or [] if isinstance(item, dict)]
567
+ lines += [f"- **Exports Found:** {denominator} documented items",
568
+ f"- **Documented:** {documented} complete ({coverage_pct})",
569
+ f"- **Missing Documentation:** {missing_count} incomplete"
570
+ + (f": {'; '.join(incomplete)}" if incomplete else "")]
571
+ else:
572
+ capped = coverage.get("coverageCapped")
573
+ floored = " (a floored residual: the numerator exceeds the denominator)" if capped else ""
574
+ lines += [f"- **Exports Found:** {denominator}",
575
+ f"- **Documented:** {documented} ({coverage_pct})",
576
+ f"- **Missing Documentation:** {missing_count}{floored}"]
577
+ mismatches = signatures.get("mismatches") if signatures is not None else None
578
+ lines.append(f"- **Signature Mismatches:** {len(mismatches) if isinstance(mismatches, list) else 'N/A'}")
579
+ if coverage.get("staleApplicable"):
580
+ lines.append(f"- **Stale Documentation:** {coverage.get('staleCount')}, documented extras included")
581
+ else:
582
+ lines.append(f"- **Stale Documentation:** N/A: {NO_NAMES.get(branch, 'the denominator names no stale export')}")
583
+ warnings = _string_list(signatures, "warnings", "--signatures") if signatures is not None else []
584
+ if warnings:
585
+ lines.append("- **Scoring Warnings:**")
586
+ lines += [f" - {w}" for w in warnings]
587
+ if coverage.get("coverageCapped"):
588
+ lines.append(f"- **Numerator Surplus:** {coverage.get('numeratorSurplus')}, uncapped coverage "
589
+ f"{_pct(coverage.get('coverageUncapped'))}")
590
+ if surface is not None:
591
+ lines += _left_out(surface)
592
+
593
+ scores = signatures or {}
594
+ lines += ["", "### Category Scores", "", "| Category | Score |", "|----------|-------|",
595
+ f"| Export Coverage | {coverage_pct} |",
596
+ f"| Signature Accuracy | {_pct(scores.get('signatureAccuracy'))} |",
597
+ f"| Type Coverage | {_pct(scores.get('typeCoverage'))} |"]
598
+ return "\n".join(lines) + "\n"
599
+
600
+
601
+ def _render_main(argv):
602
+ parser = argparse.ArgumentParser(
603
+ prog="reconcile-coverage render",
604
+ description="Print the Coverage Analysis section's Markdown from the run files (coverage-check.md §5).",
605
+ )
606
+ parser.add_argument("--coverage", required=True, metavar="PATH", help="this script's result (coverage.json)")
607
+ parser.add_argument("--surface", metavar="PATH", help="load-coverage-inputs.py surface output (surface.json)")
608
+ parser.add_argument("--signatures", metavar="PATH", help="score-signatures.py score output (signatures.json)")
609
+ args = parser.parse_args(argv)
610
+ try:
611
+ text = render(_read_json_file(args.coverage, "--coverage"),
612
+ _read_json_file(args.surface, "--surface") if args.surface else None,
613
+ _read_json_file(args.signatures, "--signatures") if args.signatures else None)
614
+ except (InputFileError, RenderError) as exc:
615
+ print(f"error: {exc}", file=sys.stderr)
616
+ return 1
617
+ sys.stdout.write(text)
618
+ return 0
619
+
620
+
621
+ # --- Run-folder inputs --------------------------------------------------------
622
+
623
+
624
+ class InputFileError(Exception):
625
+ """A file flag names a file that cannot be read or does not hold what it should."""
626
+
627
+
628
+ def _read_json_file(path, flag):
629
+ try:
630
+ return json.loads(Path(path).read_bytes().decode("utf-8-sig"))
631
+ except (OSError, UnicodeDecodeError) as exc:
632
+ raise InputFileError(f"cannot read {flag} {path}: {exc}") from exc
633
+ except json.JSONDecodeError as exc:
634
+ raise InputFileError(f"{flag} {path} is not JSON: {exc.msg}") from exc
635
+
636
+
637
+ def apply_file_inputs(inp, args):
638
+ """Fill the input object from the file flags; each flag wins over the JSON field."""
639
+ if args.denominator_source is not None:
640
+ inp["denominatorSource"] = args.denominator_source
641
+ if args.denominator_value is not None:
642
+ inp["denominatorValue"] = args.denominator_value
643
+ if args.skill_dir is not None:
644
+ inp["skillPackagePath"] = args.skill_dir
645
+ if args.inventory is not None:
646
+ data = _read_json_file(args.inventory, "--inventory")
647
+ if isinstance(data, dict) and isinstance(data.get("inventory"), dict):
648
+ data = data["inventory"] # validate-inventory.py's whole result
649
+ if not isinstance(data, dict) or not isinstance(data.get("exports"), list):
650
+ raise InputFileError(f"--inventory {args.inventory} holds no `exports` list")
651
+ inp["exports"] = data["exports"]
652
+ if args.surface is not None:
653
+ data = _read_json_file(args.surface, "--surface")
654
+ sets = data.get("sets") if isinstance(data, dict) else None
655
+ names = sets.get(args.surface_set) if isinstance(sets, dict) else None
656
+ if not isinstance(names, list):
657
+ raise InputFileError(f"--surface {args.surface} has no name set `{args.surface_set}`")
658
+ inp["barrelSet"] = names
659
+ if args.coverage_inputs is not None:
660
+ data = _read_json_file(args.coverage_inputs, "--coverage-inputs")
661
+ if not isinstance(data, dict):
662
+ raise InputFileError(f"--coverage-inputs {args.coverage_inputs} is not a JSON object")
663
+ if inp.get("denominatorSource") == "scalar":
664
+ if args.denominator_value is None:
665
+ if data.get("effectiveDenominator") is None:
666
+ raise InputFileError(f"--coverage-inputs {args.coverage_inputs} has no "
667
+ "`effectiveDenominator`: metadata.json has no stats.effective_denominator")
668
+ inp["denominatorValue"] = data["effectiveDenominator"]
669
+ else:
670
+ stack = data.get("stack")
671
+ if not isinstance(stack, dict):
672
+ raise InputFileError(f"--coverage-inputs {args.coverage_inputs} has no `stack` object")
673
+ inp["compositionNames"] = stack.get("compositionNames")
674
+ if args.denominator_value is None:
675
+ inp["denominatorValue"] = stack.get("denominator")
676
+ if args.verified is not None:
677
+ data = _read_json_file(args.verified, "--verified")
678
+ if not isinstance(data, dict):
679
+ raise InputFileError(f"--verified {args.verified} is not a verify-declared-numerator.py result")
680
+ if data.get("inflated") is True:
681
+ inp["verifiedNumerator"] = data.get("verified")
682
+ return inp
275
683
 
276
684
 
277
685
  # --- CLI --------------------------------------------------------------------
@@ -283,17 +691,19 @@ def _build_parser():
283
691
  description=(
284
692
  "Deterministic Documented-vs-Source coverage reconciliation "
285
693
  "(coverage-check.md §2c). Consumes the §1 documented inventory and "
286
- "the §2 source barrel (or a resolved scalar/stack denominator + the "
287
- "skill package path) and emits {documented, missing, stale, "
288
- "exportCoverage} so the coverage numerator is reproducible."
694
+ "the §2 source surface (or a resolved scalar/stack denominator + the "
695
+ "skill package path, or the inventory alone for a docs-only skill) and "
696
+ "emits {documented, missing, stale, exportCoverage} so the coverage "
697
+ "numerator is reproducible."
289
698
  ),
290
699
  formatter_class=argparse.RawDescriptionHelpFormatter,
291
700
  epilog=(
292
701
  "Example (barrel branch):\n"
293
- " uv run reconcile-coverage.py "
294
- "'{\"denominatorSource\":\"barrel\","
295
- "\"exports\":[{\"name\":\"a\",\"kind\":\"function\"}],"
296
- "\"barrelSet\":[\"a\",\"b\"]}'"
702
+ " uv run reconcile-coverage.py --denominator-source barrel "
703
+ "--inventory run/inventory.json --surface run/surface.json --output run/coverage.json\n"
704
+ "Render the Coverage Analysis section (see `render --help`):\n"
705
+ " uv run reconcile-coverage.py render --coverage run/coverage.json "
706
+ "[--surface run/surface.json] [--signatures run/signatures.json]"
297
707
  ),
298
708
  )
299
709
  src = parser.add_mutually_exclusive_group()
@@ -312,9 +722,27 @@ def _build_parser():
312
722
  action="store_true",
313
723
  help="Read the JSON object from stdin.",
314
724
  )
725
+ files = parser.add_argument_group("inputs read from files (each wins over the same JSON field)")
726
+ files.add_argument("--denominator-source", choices=VALID_SOURCES, help="the §2c branch")
727
+ files.add_argument("--denominator-value", type=int, metavar="N",
728
+ help="the resolved scalar denominator")
729
+ files.add_argument("--skill-dir", metavar="DIR", help="the skill package: SKILL.md and references/")
730
+ files.add_argument("--inventory", metavar="PATH", help="the validated inventory file")
731
+ files.add_argument("--surface", metavar="PATH", help="load-coverage-inputs.py surface output")
732
+ files.add_argument("--surface-set", default="all", metavar="NAME",
733
+ help="the name set of --surface to intersect against (default: all)")
734
+ files.add_argument("--coverage-inputs", metavar="PATH",
735
+ help="load-coverage-inputs.py metadata output (scalar and stack branches)")
736
+ files.add_argument("--verified", metavar="PATH",
737
+ help="verify-declared-numerator.py output (scalar branch)")
738
+ parser.add_argument("--output", metavar="PATH", help="also write the result to this file")
315
739
  return parser
316
740
 
317
741
 
742
+ FILE_FLAGS = ("denominator_source", "denominator_value", "skill_dir", "inventory", "surface",
743
+ "coverage_inputs", "verified")
744
+
745
+
318
746
  def _resolve_input(args):
319
747
  if args.stdin:
320
748
  return sys.stdin.read()
@@ -325,30 +753,70 @@ def _resolve_input(args):
325
753
  return ""
326
754
 
327
755
 
756
+ def _write_output(path, result):
757
+ target = Path(path)
758
+ if result.get("code") == "INVALID_INPUT":
759
+ target.unlink(missing_ok=True)
760
+ return
761
+ target.parent.mkdir(parents=True, exist_ok=True)
762
+ target.write_bytes((json.dumps(result, indent=2, ensure_ascii=False) + "\n").encode("utf-8"))
763
+
764
+
328
765
  def main(argv=None):
766
+ argv = sys.argv[1:] if argv is None else list(argv)
767
+ if argv[:1] == ["render"]:
768
+ return _render_main(argv[1:])
329
769
  parser = _build_parser()
330
770
  args = parser.parse_args(argv)
331
771
  raw = _resolve_input(args)
332
- if not raw.strip():
772
+ from_files = any(getattr(args, flag) is not None for flag in FILE_FLAGS)
773
+ if not raw.strip() and not from_files:
333
774
  parser.print_usage(file=sys.stderr)
334
775
  print(
335
- "error: no input provided (positional arg, --json-input, or --stdin)",
776
+ "error: no input provided (positional arg, --json-input, --stdin or the file flags)",
336
777
  file=sys.stderr,
337
778
  )
338
779
  return 1
339
780
 
340
- try:
341
- data = json.loads(raw)
342
- except json.JSONDecodeError as exc:
343
- print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
344
- return 1
781
+ data = {}
782
+ if raw.strip():
783
+ try:
784
+ data = json.loads(raw)
785
+ except json.JSONDecodeError as exc:
786
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
787
+ return 1
788
+
789
+ if isinstance(data, dict):
790
+ try:
791
+ data = apply_file_inputs(data, args)
792
+ except InputFileError as exc:
793
+ print(f"error: {exc}", file=sys.stderr)
794
+ return 1
345
795
 
346
796
  result = reconcile(data)
797
+ if args.output:
798
+ _write_output(args.output, result)
347
799
  print(json.dumps(result, indent=2))
348
800
  if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
349
801
  return 2
350
802
  return 0
351
803
 
352
804
 
805
+ def _force_utf8(*streams) -> None:
806
+ """Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
807
+
808
+ A Windows console pipes them as cp1252, which cannot print every character
809
+ of the --help text, so --help would stop with UnicodeEncodeError.
810
+ """
811
+ for stream in streams:
812
+ if hasattr(stream, "reconfigure"):
813
+ errors = getattr(stream, "errors", None)
814
+ if errors is None:
815
+ stream.reconfigure(encoding="utf-8")
816
+ else:
817
+ stream.reconfigure(encoding="utf-8", errors=errors)
818
+
819
+
353
820
  if __name__ == "__main__":
821
+ _force_utf8(sys.stdout, sys.stderr)
354
822
  raise SystemExit(main())