oh-my-opencode 5.1.45 → 5.1.47

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 (370) hide show
  1. package/.agents/command/publish.md +12 -13
  2. package/.agents/skills/publish/SKILL.md +12 -13
  3. package/.opencode/command/publish.md +12 -13
  4. package/README.de.md +0 -5
  5. package/README.es.md +0 -5
  6. package/README.fr.md +0 -5
  7. package/README.it.md +0 -5
  8. package/README.ja.md +0 -5
  9. package/README.ko.md +0 -5
  10. package/README.md +4 -5
  11. package/README.pt-br.md +0 -5
  12. package/README.ru.md +0 -6
  13. package/README.zh-cn.md +0 -5
  14. package/dist/agents/atlas/agent.d.ts +3 -2
  15. package/dist/agents/sisyphus/index.d.ts +1 -0
  16. package/dist/agents/sisyphus/kimi-k3-1.d.ts +9 -0
  17. package/dist/agents/sisyphus-agent-factory.d.ts +1 -1
  18. package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
  19. package/dist/agents/sisyphus-junior/index.d.ts +1 -0
  20. package/dist/agents/sisyphus-junior/kimi-k3-1.d.ts +6 -0
  21. package/dist/agents/types.d.ts +2 -2
  22. package/dist/cli/index.js +91 -163
  23. package/dist/cli/service-auth/keystore.d.ts +8 -3
  24. package/dist/cli-node/index.js +91 -163
  25. package/dist/config-migration/index.d.ts +2 -2
  26. package/dist/config-migration/migration-plans.d.ts +0 -10
  27. package/dist/index.js +1784 -1204
  28. package/dist/skills/ast-grep/AGENTS.md +1 -1
  29. package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
  30. package/dist/skills/frontend/ATTRIBUTION.md +2 -2
  31. package/dist/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  32. package/dist/skills/ulw-research/AGENTS.md +14 -2
  33. package/dist/skills/ulw-research/SKILL.md +37 -12
  34. package/dist/skills/ulw-research/references/deliverable-phase.md +81 -14
  35. package/dist/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
  36. package/dist/skills/ulw-research/references/fact-ledger.md +95 -0
  37. package/dist/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  38. package/dist/skills/ulw-research/references/report-gates.md +21 -4
  39. package/dist/skills/ulw-research/references/report-visualization.md +2 -0
  40. package/dist/skills/ulw-research/references/unit-rules.md +157 -0
  41. package/dist/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  42. package/dist/skills/ulw-research/scripts/claim-kinds.test.ts +193 -0
  43. package/dist/skills/ulw-research/scripts/cli-support.mjs +19 -5
  44. package/dist/skills/ulw-research/scripts/cli-support.test.ts +13 -2
  45. package/dist/skills/ulw-research/scripts/contracts.mjs +27 -6
  46. package/dist/skills/ulw-research/scripts/contracts.test.ts +3 -0
  47. package/dist/skills/ulw-research/scripts/copy-returns-polish.test.ts +209 -0
  48. package/dist/skills/ulw-research/scripts/copy-returns.test.ts +183 -0
  49. package/dist/skills/ulw-research/scripts/delivery-barrier.test.ts +245 -0
  50. package/dist/skills/ulw-research/scripts/fact-commands-meaning.mjs +176 -0
  51. package/dist/skills/ulw-research/scripts/fact-commands-polish.mjs +127 -0
  52. package/dist/skills/ulw-research/scripts/fact-commands-verify.mjs +183 -0
  53. package/dist/skills/ulw-research/scripts/fact-commands.mjs +241 -0
  54. package/dist/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  55. package/dist/skills/ulw-research/scripts/fact-derive.test.ts +375 -0
  56. package/dist/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  57. package/dist/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  58. package/dist/skills/ulw-research/scripts/fact-fill.test.ts +221 -0
  59. package/dist/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +9 -2
  60. package/dist/skills/ulw-research/scripts/fact-ledger-kinds.mjs +127 -13
  61. package/dist/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +18 -3
  62. package/dist/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +160 -0
  63. package/dist/skills/ulw-research/scripts/fact-ledger-schema.mjs +33 -16
  64. package/dist/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
  65. package/dist/skills/ulw-research/scripts/fact-ledger.test.ts +2 -0
  66. package/dist/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  67. package/dist/skills/ulw-research/scripts/fact-locate.test.ts +118 -0
  68. package/dist/skills/ulw-research/scripts/fact-pairs.mjs +185 -0
  69. package/dist/skills/ulw-research/scripts/fact-pairs.test.ts +151 -0
  70. package/dist/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  71. package/dist/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +409 -0
  72. package/dist/skills/ulw-research/scripts/fact-unit-rules.test.ts +388 -0
  73. package/dist/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  74. package/dist/skills/ulw-research/scripts/fidelity-binding.test.ts +81 -0
  75. package/dist/skills/ulw-research/scripts/fidelity-brief.mjs +5 -0
  76. package/dist/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  77. package/dist/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  78. package/dist/skills/ulw-research/scripts/fidelity-lexicon.test.ts +117 -0
  79. package/dist/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  80. package/dist/skills/ulw-research/scripts/fidelity-state.mjs +2 -1
  81. package/dist/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  82. package/dist/skills/ulw-research/scripts/fidelity-trace.test.ts +58 -0
  83. package/dist/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  84. package/dist/skills/ulw-research/scripts/fidelity-values.test.ts +270 -0
  85. package/dist/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  86. package/dist/skills/ulw-research/scripts/gates-fidelity.test.ts +73 -0
  87. package/dist/skills/ulw-research/scripts/gates-static.mjs +28 -8
  88. package/dist/skills/ulw-research/scripts/html-lite.mjs +2 -2
  89. package/dist/skills/ulw-research/scripts/html-markup.mjs +182 -0
  90. package/dist/skills/ulw-research/scripts/md-lite.mjs +193 -0
  91. package/dist/skills/ulw-research/scripts/md-lite.test.ts +271 -0
  92. package/dist/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  93. package/dist/skills/ulw-research/scripts/meaning-check.test.ts +228 -0
  94. package/dist/skills/ulw-research/scripts/outcome.mjs +6 -5
  95. package/dist/skills/ulw-research/scripts/placeholder.mjs +184 -0
  96. package/dist/skills/ulw-research/scripts/placeholder.test.ts +77 -0
  97. package/dist/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  98. package/dist/skills/ulw-research/scripts/polish-apply.test.ts +363 -0
  99. package/dist/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  100. package/dist/skills/ulw-research/scripts/polish-diff.test.ts +168 -0
  101. package/dist/skills/ulw-research/scripts/polish-markup.test.ts +187 -0
  102. package/dist/skills/ulw-research/scripts/polish-project.mjs +125 -0
  103. package/dist/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  104. package/dist/skills/ulw-research/scripts/references-fences.test.ts +25 -0
  105. package/dist/skills/ulw-research/scripts/report-tools-commands.mjs +19 -8
  106. package/dist/skills/ulw-research/scripts/report-tools.mjs +15 -0
  107. package/dist/skills/ulw-research/scripts/report-tools.test.ts +545 -2
  108. package/dist/skills/ulw-research/scripts/source-commands.mjs +140 -0
  109. package/dist/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  110. package/dist/skills/ulw-research/scripts/source-snapshot.test.ts +113 -0
  111. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/assets/share.csv +3 -0
  112. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/gen-legacy-dev.mjs +89 -0
  113. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-basic.json +223 -0
  114. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-legacy-dev.json +859 -0
  115. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-verify.json +420 -0
  116. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/polish-edits.json +12 -0
  117. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/report-bad.html +19 -0
  118. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/report-good.html +39 -0
  119. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/acronyms.json +3 -0
  120. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/meta.json +9 -0
  121. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/sentences.jsonl +7 -0
  122. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/text.txt +7 -0
  123. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4.html +19 -0
  124. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/acronyms.json +1 -0
  125. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/meta.json +9 -0
  126. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/sentences.jsonl +1 -0
  127. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/text.txt +1 -0
  128. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S9.txt +1 -0
  129. package/dist/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  130. package/dist/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  131. package/dist/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  132. package/dist/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  133. package/dist/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  134. package/dist/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  135. package/dist/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  136. package/dist/skills/ulw-research/scripts/units.mjs +4 -4
  137. package/dist/testing/module-mock-lifecycle.d.ts +1 -1
  138. package/dist/tui.js +32 -34
  139. package/package.json +13 -13
  140. package/packages/lsp-core/src/mcp-protocol-pin.test.ts +29 -0
  141. package/packages/lsp-core/src/mcp.ts +5 -2
  142. package/packages/lsp-daemon/dist/cli.js +3 -3
  143. package/packages/lsp-daemon/dist/client.js +3 -3
  144. package/packages/lsp-daemon/dist/index.js +3 -3
  145. package/packages/lsp-tools-mcp/dist/cli.js +3 -3
  146. package/packages/lsp-tools-mcp/dist/mcp.js +3 -3
  147. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  148. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +23 -14
  149. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  150. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  151. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  152. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  153. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  154. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  155. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  156. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  157. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  158. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  159. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  160. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  161. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  162. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  163. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  164. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  165. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  166. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  167. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  168. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  169. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  170. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  171. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  172. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  173. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  174. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  175. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  176. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  177. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  178. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  179. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  180. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  181. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  182. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  183. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  184. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  185. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  186. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  189. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  190. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  191. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  192. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  193. package/packages/omo-codex/plugin/package-lock.json +12 -12
  194. package/packages/omo-codex/plugin/package.json +1 -1
  195. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +8 -3
  196. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +0 -2
  197. package/packages/omo-codex/plugin/skills/ast-grep/AGENTS.md +1 -1
  198. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  199. package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +2 -2
  200. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  201. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +14 -2
  202. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +36 -11
  203. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +81 -14
  204. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
  205. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger.md +95 -0
  206. package/packages/omo-codex/plugin/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  207. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +21 -4
  208. package/packages/omo-codex/plugin/skills/ulw-research/references/report-visualization.md +2 -0
  209. package/packages/omo-codex/plugin/skills/ulw-research/references/unit-rules.md +157 -0
  210. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  211. package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +19 -5
  212. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +27 -6
  213. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +176 -0
  214. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-polish.mjs +127 -0
  215. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +183 -0
  216. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +241 -0
  217. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  218. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  219. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  220. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +127 -13
  221. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +33 -16
  222. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
  223. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  224. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-pairs.mjs +185 -0
  225. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  226. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  227. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +5 -0
  228. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  229. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  230. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  231. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +2 -1
  232. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  233. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  234. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  235. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +28 -8
  236. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +2 -2
  237. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-markup.mjs +182 -0
  238. package/packages/omo-codex/plugin/skills/ulw-research/scripts/md-lite.mjs +193 -0
  239. package/packages/omo-codex/plugin/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  240. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +6 -5
  241. package/packages/omo-codex/plugin/skills/ulw-research/scripts/placeholder.mjs +184 -0
  242. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  243. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  244. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-project.mjs +125 -0
  245. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  246. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +19 -8
  247. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +15 -0
  248. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +140 -0
  249. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  250. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  251. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  252. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  253. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  254. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  255. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  256. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  257. package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +4 -4
  258. package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +19 -0
  259. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +7 -3
  260. package/packages/omo-codex/scripts/install-dist/install-local.mjs +28 -19
  261. package/packages/shared-skills/skills/ast-grep/AGENTS.md +1 -1
  262. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  263. package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +2 -2
  264. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  265. package/packages/shared-skills/skills/ulw-research/AGENTS.md +14 -2
  266. package/packages/shared-skills/skills/ulw-research/SKILL.md +37 -12
  267. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +81 -14
  268. package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
  269. package/packages/shared-skills/skills/ulw-research/references/fact-ledger.md +95 -0
  270. package/packages/shared-skills/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  271. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +21 -4
  272. package/packages/shared-skills/skills/ulw-research/references/report-visualization.md +2 -0
  273. package/packages/shared-skills/skills/ulw-research/references/unit-rules.md +157 -0
  274. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  275. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.test.ts +193 -0
  276. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +19 -5
  277. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +13 -2
  278. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +27 -6
  279. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +3 -0
  280. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-polish.test.ts +209 -0
  281. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns.test.ts +183 -0
  282. package/packages/shared-skills/skills/ulw-research/scripts/delivery-barrier.test.ts +245 -0
  283. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +176 -0
  284. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-polish.mjs +127 -0
  285. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +183 -0
  286. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +241 -0
  287. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  288. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.test.ts +375 -0
  289. package/packages/shared-skills/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  290. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  291. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.test.ts +221 -0
  292. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +9 -2
  293. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +127 -13
  294. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +18 -3
  295. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +160 -0
  296. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +33 -16
  297. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
  298. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +2 -0
  299. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  300. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.test.ts +118 -0
  301. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.mjs +185 -0
  302. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.test.ts +151 -0
  303. package/packages/shared-skills/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  304. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +409 -0
  305. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-rules.test.ts +388 -0
  306. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  307. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.test.ts +81 -0
  308. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +5 -0
  309. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  310. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  311. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.test.ts +117 -0
  312. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  313. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +2 -1
  314. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  315. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.test.ts +58 -0
  316. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  317. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.test.ts +270 -0
  318. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  319. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.test.ts +73 -0
  320. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +28 -8
  321. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +2 -2
  322. package/packages/shared-skills/skills/ulw-research/scripts/html-markup.mjs +182 -0
  323. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.mjs +193 -0
  324. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.test.ts +271 -0
  325. package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  326. package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.test.ts +228 -0
  327. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +6 -5
  328. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.mjs +184 -0
  329. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.test.ts +77 -0
  330. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  331. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.test.ts +363 -0
  332. package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  333. package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.test.ts +168 -0
  334. package/packages/shared-skills/skills/ulw-research/scripts/polish-markup.test.ts +187 -0
  335. package/packages/shared-skills/skills/ulw-research/scripts/polish-project.mjs +125 -0
  336. package/packages/shared-skills/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  337. package/packages/shared-skills/skills/ulw-research/scripts/references-fences.test.ts +25 -0
  338. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +19 -8
  339. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +15 -0
  340. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +545 -2
  341. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +140 -0
  342. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  343. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.test.ts +113 -0
  344. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/assets/share.csv +3 -0
  345. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/gen-legacy-dev.mjs +89 -0
  346. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-basic.json +223 -0
  347. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-legacy-dev.json +859 -0
  348. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-verify.json +420 -0
  349. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/polish-edits.json +12 -0
  350. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-bad.html +19 -0
  351. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-good.html +39 -0
  352. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/acronyms.json +3 -0
  353. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/meta.json +9 -0
  354. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/sentences.jsonl +7 -0
  355. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/text.txt +7 -0
  356. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4.html +19 -0
  357. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/acronyms.json +1 -0
  358. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/meta.json +9 -0
  359. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/sentences.jsonl +1 -0
  360. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/text.txt +1 -0
  361. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S9.txt +1 -0
  362. package/packages/shared-skills/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  363. package/packages/shared-skills/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  364. package/packages/shared-skills/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  365. package/packages/shared-skills/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  366. package/packages/shared-skills/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  367. package/packages/shared-skills/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  368. package/packages/shared-skills/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  369. package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +4 -4
  370. package/dist/config-migration/dori-section-rename.d.ts +0 -3
@@ -0,0 +1,193 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { spawnSync } from "node:child_process"
3
+ import { readFileSync } from "node:fs"
4
+ import { join } from "node:path"
5
+
6
+ import { CLAIM_ATTRIBUTE, CLAIM_KINDS, CLAIM_TABLE_BEGIN, CLAIM_TABLE_END, renderClaimKindTable, unmarkedKinds } from "./claim-kinds.mjs"
7
+ import { DEFECT_CODES } from "./contracts.mjs"
8
+ import { valueChecks } from "./fidelity-values.mjs"
9
+ import { buildContext } from "./gates-static.mjs"
10
+ import { parseHtml } from "./html-lite.mjs"
11
+
12
+ // Protects the claim-kind contract the writer (fill) and the gate share: a fact marked with a
13
+ // claim kind must cite a row that kind accepts, a mark binds only the facts it owns, and the doc
14
+ // table writers read is rendered from CLAIM_KINDS. Expectations come from ORACLE below, written by
15
+ // hand from the design, never from the table under test.
16
+
17
+ type Defect = { code: string; severity: string }
18
+ type Row = Record<string, unknown> & { fact_id: string; kind: string; source_id: string | null; raw?: string; quote?: string }
19
+
20
+ const FIX = join(import.meta.dir, "tests", "fixtures", "fidelity")
21
+ const BASE: { rows: Row[] } = JSON.parse(readFileSync(join(FIX, "ledger-verify.json"), "utf8"))
22
+ const byId = (id: string) => BASE.rows.find((row) => row.fact_id === id) as Row
23
+ const [F1, F7, F8, Q1] = ["F1", "F7", "F8", "Q1"].map(byId) as [Row, Row, Row, Row]
24
+ const plain = { render: null, operation: null, operands: [], derived_from: [], formula: null, factor: null }
25
+ const F9: Row = { ...F8, ...plain, fact_id: "F9", kind: "assumed", label: "growth_rate", raw: "15%", value: "15", unit: "%", exact: { num: "15", den: "1" }, rationale: "planning assumption" }
26
+ const F10: Row = { ...F8, ...plain, fact_id: "F10", kind: "derived", label: "share_sum", raw: "39%", value: "39", unit: "%", exact: { num: "39", den: "1" }, operation: "sum", operands: ["F5", "F6"] }
27
+ const F11: Row = { ...F7, fact_id: "F11", locator: null }
28
+ const F12: Row = { ...F7, fact_id: "F12", locator: { artifact: "missing.txt", char_start: 0, char_end: 9 } }
29
+ const F13: Row = { ...F7, fact_id: "F13", locator: { artifact: "../S4/text.txt", char_start: 0, char_end: 9 } }
30
+ const LEDGER = { ...BASE, rows: [...BASE.rows, F9, F10, F11, F12, F13] }
31
+ const SUBJECTS = [F1, F7, F8, F9, F10, F11, F12, F13, Q1]
32
+
33
+ /** Hand-written oracle: the ledger row kinds each claim kind accepts, and whether the cited row must be command-backed. */
34
+ const ORACLE: Record<string, { rows: string[]; command: boolean }> = {
35
+ fact: { rows: ["measured", "converted", "derived"], command: false },
36
+ quote: { rows: ["quote"], command: false },
37
+ verified: { rows: ["measured", "quote"], command: true },
38
+ derived: { rows: ["derived", "converted"], command: false },
39
+ assumed: { rows: ["assumed"], command: false },
40
+ }
41
+ /** Rows an unmarked element refuses (an unmarked numeral reads as a fact, so an assumption needs its A mark). */
42
+ const UNMARKED_REFUSED = new Set(["F9"])
43
+ /** Rows whose own locator artifact sits in a snapshot whose meta.json kind is command (S5/text.txt). */
44
+ const COMMAND_BACKED = new Set(["F7"])
45
+ const MAJOR_FOR: Record<string, string> = { fact: "claim_kind_mismatch", quote: "claim_kind_mismatch", verified: "unsupported_verification", derived: "claim_kind_mismatch", assumed: "claim_kind_mismatch" }
46
+ const expected = (kind: string, row: Row) => {
47
+ const rule = ORACLE[kind] as { rows: string[]; command: boolean }
48
+ return rule.rows.includes(row.kind) && (!rule.command || COMMAND_BACKED.has(row.fact_id)) ? [] : [MAJOR_FOR[kind] as string]
49
+ }
50
+
51
+ const CLAIM_CODES = new Set(["claim_kind_mismatch", "unsupported_verification", "verification_claim_unmarked"])
52
+ const run = (body: string) => valueChecks(buildContext(parseHtml(`<h2>Findings</h2>\n${body}`), { baseDir: FIX }), LEDGER, null, join(FIX, "sources"), {}) as Defect[]
53
+ const claims = (body: string) => run(body).filter((d) => CLAIM_CODES.has(d.code))
54
+ const codes = (body: string) => claims(body).map((d) => d.code).sort()
55
+
56
+ const value = (row: Row, kind?: string) => {
57
+ const mark = kind ? ` ${CLAIM_ATTRIBUTE}="${kind}"` : ""
58
+ return row.kind === "quote" ? `<q data-fact="${row.fact_id}"${mark} data-quote-sel="full">${row.quote}</q>` : `<data class="fact"${mark} data-fact="${row.fact_id}">${row.raw}</data>`
59
+ }
60
+ const prose = (inner: string, lead = "The figure is") => `<p>${lead} ${inner} [S4].</p>`
61
+ const cell = (inner: string) => `<table><tr><th>Item</th><td>${inner}</td></tr></table>`
62
+
63
+ describe("every claim kind against the hand-written oracle", () => {
64
+ for (const kind of Object.keys(ORACLE)) {
65
+ for (const row of SUBJECTS) {
66
+ const want = expected(kind, row)
67
+ const label = want.length === 0 ? "no claim defect" : `${want[0]} major`
68
+ test(`#given ${kind} marked on ${row.fact_id} (${row.kind}) in prose and in a table cell #then ${label}`, () => {
69
+ for (const body of [prose(value(row, kind)), cell(value(row, kind))]) {
70
+ expect(codes(body)).toEqual(want)
71
+ for (const defect of claims(body)) expect(defect.severity).toBe("major")
72
+ }
73
+ })
74
+ test(`#given ${kind} marked on an enclosing element over ${row.fact_id} #then the parent's mark binds it: ${label}`, () => {
75
+ expect(codes(`<p><span ${CLAIM_ATTRIBUTE}="${kind}">The figure is ${value(row)}</span> [S4].</p>`)).toEqual(want)
76
+ })
77
+ test(`#given ${kind} marked on ${row.fact_id} beside an unmarked command row #then the neighbour never satisfies it: ${label}`, () => {
78
+ expect(codes(prose(`${value(row, kind)}, and the suite reported ${value(F7)}`))).toEqual(want)
79
+ })
80
+ for (const inner of Object.keys(ORACLE).filter((other) => other !== kind)) {
81
+ test(`#given ${inner} nested inside ${kind} over ${row.fact_id} #then mismatch, the parent owns nothing, and the inner kind is still checked`, () => {
82
+ const body = `<p><span ${CLAIM_ATTRIBUTE}="${kind}">The figure is ${value(row, inner)}</span> [S4].</p>`
83
+ expect(codes(body)).toEqual(["claim_kind_mismatch", MAJOR_FOR[kind] as string, ...expected(inner, row)].sort())
84
+ })
85
+ }
86
+ }
87
+ }
88
+
89
+ for (const row of SUBJECTS) {
90
+ const refused = UNMARKED_REFUSED.has(row.fact_id) ? ["claim_kind_mismatch:major"] : []
91
+ const refusedLabel = refused.length ? "claim_kind_mismatch major" : "no claim code"
92
+ test(`#given ${row.fact_id} unmarked in a first-person run sentence #then the minor verification_claim_unmarked, plus ${refusedLabel}`, () => {
93
+ const found = claims(prose(value(row), "We ran the check and it reported"))
94
+ expect(found.map((d) => `${d.code}:${d.severity}`).sort()).toEqual([...refused, "verification_claim_unmarked:minor"].sort())
95
+ })
96
+ test(`#given ${row.fact_id} unmarked in a plain sentence in prose and in a table cell #then ${refusedLabel}`, () => {
97
+ for (const body of [prose(value(row)), cell(value(row))]) expect(claims(body).map((d) => `${d.code}:${d.severity}`)).toEqual(refused)
98
+ })
99
+ }
100
+
101
+ test("#given an unknown claim kind #then claim_kind_mismatch", () => {
102
+ expect(codes(prose(value(F1, "rumoured")))).toEqual(["claim_kind_mismatch"])
103
+ })
104
+
105
+ test("#given every defect and warning code the table names #then contracts defines it, and warnings are never major", () => {
106
+ for (const entry of Object.values(CLAIM_KINDS) as { defect: string; unmarked: { code: string } | null }[]) {
107
+ expect(DEFECT_CODES[entry.defect]).toBeDefined()
108
+ if (entry.unmarked) expect(DEFECT_CODES[entry.unmarked.code].severity).toBe("minor")
109
+ }
110
+ })
111
+ })
112
+
113
+ describe("design-check regressions", () => {
114
+ test("#given verified wrapping a fact-marked page row (F1 shadowing) #then the parent is unsupported and the nesting is a mismatch", () => {
115
+ expect(codes(`<p><span ${CLAIM_ATTRIBUTE}="verified">The inspection put the span at ${value(F1, "fact")}</span> [S4].</p>`)).toEqual(["claim_kind_mismatch", "unsupported_verification"])
116
+ })
117
+
118
+ test("#given one verified value and an unmarked value in the same run sentence (F2) #then the unmarked one still warns", () => {
119
+ expect(codes(prose(`${value(F7, "verified")}, and the span is ${value(F1)}`, "We ran the suite and it reported"))).toEqual(["verification_claim_unmarked"])
120
+ })
121
+
122
+ test("#given a truthful converted and derived value marked fact (F3) #then no stricter than unmarked", () => {
123
+ for (const row of [F8, F9, F10]) expect(codes(prose(value(row, "fact")))).toEqual(codes(prose(value(row))))
124
+ expect(codes(prose(value(F8, "fact")))).toEqual([])
125
+ })
126
+
127
+ test("#given the assumed row F9 unmarked or marked fact #then both refuse it, and the unmarked message names the A mark", () => {
128
+ expect(codes(prose(value(F9)))).toEqual(["claim_kind_mismatch"])
129
+ expect(codes(prose(value(F9, "fact")))).toEqual(["claim_kind_mismatch"])
130
+ expect(JSON.stringify(claims(prose(value(F9))))).toContain("{{A:F9}}")
131
+ })
132
+
133
+ test("#given the assumed row F9 marked {{A:F9}} (data-claim-kind=assumed) #then no claim defect", () => {
134
+ expect(codes(prose(value(F9, "assumed")))).toEqual([])
135
+ expect(codes(cell(value(F9, "assumed")))).toEqual([])
136
+ })
137
+
138
+ test("#given verified citing a command-sourced row with no, a missing, or an escaping locator artifact (F4) #then unsupported_verification", () => {
139
+ for (const row of [F11, F12, F13]) expect(codes(prose(value(row, "verified")))).toEqual(["unsupported_verification"])
140
+ })
141
+ })
142
+
143
+ describe("unmarked phrases", () => {
144
+ const fires = [
145
+ "We verified the span by running the measurement script.",
146
+ "We ran the suite twice.",
147
+ "We executed the export.",
148
+ "We verified by hand-counting the rows.",
149
+ "We have run the tests.",
150
+ "I ran the suite.",
151
+ "Our numbers were obtained by running the script.",
152
+ "우리가 실행해 확인한 길이",
153
+ "저희가 스크립트를 실행하여 검증한 값",
154
+ "저희가 테스트를 돌렸다",
155
+ "우리 팀이 벤치마크를 돌려 얻은 값",
156
+ "그래서 우리는 스크립트를 실행했다",
157
+ "직접 실행한 결과",
158
+ ]
159
+ const quiet = [
160
+ "The tests showed a 12 % drop.",
161
+ "The bridge, which ran across the river, spans 320 m.",
162
+ "The span, as confirmed with the annual inspection report, is 320 m.",
163
+ "Clinical tests confirmed the diagnosis.",
164
+ "The authors obtained estimates by running simulations.",
165
+ "We ran out of time.",
166
+ "확인된 길이는 최대 320 m",
167
+ "개발자가 직접 실행한 결과를 보고했다",
168
+ "실행해 확인한 길이",
169
+ "우리나라에서 실행된 정책은 320개다",
170
+ "우리 정부가 실행한 예산",
171
+ "우리가 의뢰한 업체가 테스트를 돌렸다",
172
+ "우리는 결과를 확인했다",
173
+ ]
174
+ for (const text of fires) test(`#given "${text}" #then verified`, () => expect(unmarkedKinds(text)).toEqual(["verified"]))
175
+ for (const text of quiet) test(`#given "${text}" #then nothing`, () => expect(unmarkedKinds(text)).toEqual([]))
176
+ })
177
+
178
+ describe("references/fidelity-claim-kinds.md is rendered from CLAIM_KINDS", () => {
179
+ const doc = readFileSync(join(import.meta.dir, "..", "references", "fidelity-claim-kinds.md"), "utf8")
180
+
181
+ test("the committed table section equals renderClaimKindTable() byte for byte", () => {
182
+ const [begin, end] = [doc.indexOf(CLAIM_TABLE_BEGIN), doc.indexOf(CLAIM_TABLE_END)]
183
+ expect(begin).toBeGreaterThanOrEqual(0)
184
+ expect(end).toBeGreaterThan(begin)
185
+ expect(doc.slice(begin + CLAIM_TABLE_BEGIN.length + 1, end)).toBe(renderClaimKindTable())
186
+ })
187
+
188
+ test("node claim-kinds.mjs --render-doc prints the same table", () => {
189
+ const result = spawnSync("node", [join(import.meta.dir, "claim-kinds.mjs"), "--render-doc"], { encoding: "utf8" })
190
+ expect(result.status).toBe(0)
191
+ expect(result.stdout).toBe(renderClaimKindTable())
192
+ })
193
+ })
@@ -1,7 +1,8 @@
1
1
  // cli-support.mjs - argument parsing, typed usage errors, exit-code mapping, and JSON file IO
2
2
  // shared by every report-tools subcommand. node: builtins only.
3
3
 
4
- import { readFileSync, renameSync, writeFileSync } from "node:fs"
4
+ import { createHash } from "node:crypto"
5
+ import { readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
5
6
  import { basename, dirname, join } from "node:path"
6
7
 
7
8
  /** A usage or input error: always exit code 2. */
@@ -87,11 +88,24 @@ export function readJsonFile(path) {
87
88
  }
88
89
  }
89
90
 
90
- /** Write JSON through a sibling temp file + rename, so a crash never leaves a half-written file. */
91
- export function writeJsonAtomic(path, value) {
91
+ /** The sha256 hex digest of a string or bytes: the one hash every record and receipt carries. */
92
+ export const sha256Of = (content) => createHash("sha256").update(content).digest("hex")
93
+
94
+ /** Write text through a sibling temp file + rename; a failed write removes the temp file. */
95
+ export function writeTextAtomic(path, text) {
92
96
  const temp = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.tmp`)
93
- writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`)
94
- renameSync(temp, path)
97
+ try {
98
+ writeFileSync(temp, text)
99
+ renameSync(temp, path)
100
+ } catch (error) {
101
+ rmSync(temp, { force: true })
102
+ throw error
103
+ }
104
+ }
105
+
106
+ /** Write JSON through writeTextAtomic, so a crash never leaves a half-written file. */
107
+ export function writeJsonAtomic(path, value) {
108
+ writeTextAtomic(path, `${JSON.stringify(value, null, 2)}\n`)
95
109
  }
96
110
 
97
111
  /** Print a result: JSON on stdout, one summary line on stderr. */
@@ -1,10 +1,10 @@
1
1
  import { describe, expect, onTestFinished, test } from "bun:test"
2
- import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
2
+ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
3
3
  import { tmpdir } from "node:os"
4
4
  import { join } from "node:path"
5
5
  import { pathToFileURL } from "node:url"
6
6
 
7
- import { CliError, exitCodeFor, parseArgs, readJsonFile, writeJsonAtomic } from "./cli-support.mjs"
7
+ import { CliError, exitCodeFor, parseArgs, readJsonFile, writeJsonAtomic, writeTextAtomic } from "./cli-support.mjs"
8
8
  import { isCliEntry } from "./entry-guard.mjs"
9
9
 
10
10
  function tempDir() {
@@ -65,6 +65,17 @@ describe("JSON files", () => {
65
65
  expect(leftovers).toEqual([])
66
66
  })
67
67
 
68
+ test("#given a target the rename cannot replace #when written atomically #then the write throws and no temp file is left", () => {
69
+ // given: a non-empty directory where the file should go
70
+ const dir = tempDir()
71
+ const target = join(dir, "calibration.jsonl")
72
+ mkdirSync(target)
73
+ writeFileSync(join(target, "keep"), "x")
74
+ // when / then
75
+ expect(() => writeTextAtomic(target, "line\n")).toThrow()
76
+ expect(readdirSync(dir)).toEqual(["calibration.jsonl"])
77
+ })
78
+
68
79
  test("#given a missing or malformed file #when read #then a CliError names the path", () => {
69
80
  const dir = tempDir()
70
81
  expect(() => readJsonFile(join(dir, "missing.json"))).toThrow(CliError)
@@ -6,6 +6,16 @@
6
6
 
7
7
  export const SCHEMA_VERSION = 1
8
8
 
9
+ /** The severity vocabulary in rank order; every DEFECT_CODES entry uses one of these. */
10
+ export const SEVERITIES = Object.freeze(["blocker", "major", "minor"])
11
+
12
+ /** Defect counts per severity, then total. */
13
+ export function countDefects(defects) {
14
+ const counts = Object.fromEntries(SEVERITIES.map((severity) => [severity, 0]))
15
+ for (const defect of defects) counts[defect.severity] += 1
16
+ return { ...counts, total: defects.length }
17
+ }
18
+
9
19
  function deepFreeze(value) {
10
20
  if (value === null || typeof value !== "object") return value
11
21
  for (const key of Object.keys(value)) deepFreeze(value[key])
@@ -48,7 +58,7 @@ export const DEFECT_CODES = deepFreeze({
48
58
  pointer_label_mismatch: { severity: "blocker", integrity: true, hint: "The placeholder's label is not the row's label; check the id, then the label." },
49
59
  quote_anchor_unresolved: { severity: "blocker", integrity: true, hint: "A from or to anchor is missing or occurs more than once in the quote; pick unique anchor words." },
50
60
  unit_duplicated: { severity: "blocker", integrity: true, hint: "The unit after the placeholder repeats the row's unit; use :value or remove the unit." },
51
- polish_diff_fail: { severity: "blocker", integrity: true, hint: "The polish pass changed numbers, tickers, or table rows; revert to the pre-polish artifact and apply the language fixes only." },
61
+ polish_diff_fail: { severity: "blocker", integrity: true, hint: "The polish pass changed numbers, tickers, table rows, claim ids, markup, or hidden content; revert to the pre-polish artifact and apply the language fixes only." },
52
62
  fidelity_interview_incomplete: { severity: "blocker", integrity: true, hint: "A fidelity field is pending or missing in brief.md; answer the open question or record its default, then rerun." },
53
63
  derived_not_allowed: { severity: "blocker", integrity: true, hint: "The brief allows no derived or assumed numbers; cite a source row directly or change the brief with the requester." },
54
64
  source_outside_floor: { severity: "blocker", integrity: true, hint: "The row's source is outside the source floor the brief records; use a source the brief allows or remove the fact." },
@@ -57,7 +67,8 @@ export const DEFECT_CODES = deepFreeze({
57
67
  quote_unbound: { severity: "blocker", integrity: true, hint: "A direct quotation has no ledger quote row; register it with fact quote and insert it through its placeholder." },
58
68
  edition_unverified: { severity: "blocker", integrity: true, hint: "The delivered file has no matching fact verify record; run fact verify on that edition and record it with --verified-by." },
59
69
  untraceable_number: { severity: "major", integrity: false, hint: "Replace the typed number with a {{F<n>}} placeholder backed by a ledger row, or register it as assumed with a rationale." },
60
- unsupported_verification: { severity: "major", integrity: false, hint: "The sentence claims a tool check that no recorded command output backs; link the recorded output row or remove the claim." },
70
+ unsupported_verification: { severity: "major", integrity: false, hint: "A sentence marked verified (data-claim-kind=\"verified\") cites a row that is not recorded command output; cite the recorded output row or drop the verified mark." },
71
+ claim_kind_mismatch: { severity: "major", integrity: false, hint: "An element marked with a claim kind (data-claim-kind) cites a row that kind does not accept, or the kind is unknown; cite a row of that kind or fix the mark (references/fidelity-claim-kinds.md)." },
61
72
  conflicting_readings: { severity: "major", integrity: false, hint: "Two ledger rows with different periods or qualifiers are cited as one fact; split the sentence or retire one row." },
62
73
  hedge_dropped: { severity: "major", integrity: false, hint: "Restore the qualifier the source uses (estimated, about, not yet)." },
63
74
  direction_flipped: { severity: "major", integrity: false, hint: "The source states the opposite direction; rewrite from the quote." },
@@ -65,7 +76,8 @@ export const DEFECT_CODES = deepFreeze({
65
76
  qualifier_dropped: { severity: "major", integrity: false, hint: "The source bounds this value (up to, at least, about); restore the qualifier or use :q." },
66
77
  unbound_sentence: { severity: "major", integrity: false, hint: "Bind the sentence to a fact, a quote, a citation, or a claim-graph node with data-claim." },
67
78
  meaning_drift_confirmed: { severity: "major", integrity: false, hint: "Two independent readers found the same drift; rewrite from the quote or dispute with a reason." },
68
- polish_edit_rejected: { severity: "major", integrity: false, hint: "The edit changed a number, a hedge, a qualifier, a link, or a claim id; re-propose the language fix only." },
79
+ meaning_unverified: { severity: "major", integrity: false, hint: "No meaning reader judged this pair, or no strong reader judged a force_strong pair; run the missing reader on pairs.json and rerun fact meaning-check." },
80
+ polish_edit_rejected: { severity: "major", integrity: false, hint: "The edit changed a number, a hedge, a qualifier, a link, or a claim id, or contained <, > or &; re-propose the language fix only, narrowed to the words around any entity or tag." },
69
81
  polish_hedging_decreased: { severity: "major", integrity: false, hint: "The polish pass removed qualifiers; restore them." },
70
82
  polish_text_churn: { severity: "major", integrity: false, hint: "More than half the text changed during polish; the pass composed instead of proofreading." },
71
83
  snapshot_mismatch: { severity: "major", integrity: false, hint: "The operands come from different collection windows; use rows from one window or state the windows separately." },
@@ -73,6 +85,7 @@ export const DEFECT_CODES = deepFreeze({
73
85
  quote_stale_version: { severity: "major", integrity: false, hint: "The quoted source revision differs from the revision the report targets; re-snapshot the target revision or date the quote." },
74
86
  quote_fuzzy_match: { severity: "minor", integrity: false, hint: "The source text is OCR-noisy; confirm the snapshot and prefer the raw document." },
75
87
  name_outside_quote: { severity: "minor", integrity: false, hint: "A name in the sentence is not in the cited quote; add the source or remove the name." },
88
+ verification_claim_unmarked: { severity: "minor", integrity: false, hint: "The sentence reads as a check we ran but carries no verified mark; mark it {{V:F<n>:label}} on a recorded command-output row, or reword it." },
76
89
  scope_widened_warn: { severity: "minor", integrity: false, hint: "The sentence uses a broader scope word than the quote; confirm the scope or narrow it." },
77
90
  particle_unverified: { severity: "minor", integrity: false, hint: "The particle after this value could not be checked; read it aloud and fix it by hand if wrong." },
78
91
  fallback_without_entity: { severity: "minor", integrity: false, hint: "The replacement quote has no entity for attribution; set one on the row." },
@@ -87,10 +100,10 @@ export const FIDELITY_CODES = Object.freeze([
87
100
  "unresolved_placeholder", "value_mismatch", "unit_mismatch", "quote_not_verbatim", "claim_status_invalid",
88
101
  "incompatible_operands", "pointer_label_mismatch", "quote_anchor_unresolved", "unit_duplicated", "polish_diff_fail",
89
102
  "fidelity_interview_incomplete", "derived_not_allowed", "source_outside_floor", "snapshot_hash_mismatch",
90
- "chart_value_untraceable", "quote_unbound", "edition_unverified", "untraceable_number", "unsupported_verification",
103
+ "chart_value_untraceable", "quote_unbound", "edition_unverified", "untraceable_number", "unsupported_verification", "claim_kind_mismatch",
91
104
  "conflicting_readings", "hedge_dropped", "direction_flipped", "causality_added", "qualifier_dropped", "unbound_sentence",
92
- "meaning_drift_confirmed", "polish_edit_rejected", "polish_hedging_decreased", "polish_text_churn", "snapshot_mismatch",
93
- "join_integrity_fail", "quote_stale_version", "quote_fuzzy_match", "name_outside_quote", "scope_widened_warn",
105
+ "meaning_drift_confirmed", "meaning_unverified", "polish_edit_rejected", "polish_hedging_decreased", "polish_text_churn", "snapshot_mismatch",
106
+ "join_integrity_fail", "quote_stale_version", "quote_fuzzy_match", "name_outside_quote", "verification_claim_unmarked", "scope_widened_warn",
94
107
  "particle_unverified", "fallback_without_entity", "meaning_drift_warn", "polish_edit_unlocated", "polish_link_changed",
95
108
  "polish_name_changed",
96
109
  ])
@@ -184,6 +197,14 @@ export function validateOutcomeManifest(manifest) {
184
197
  if (isObject(manifest.gates)) {
185
198
  for (const gate of GATE_NAMES) check(errors, GATE_STATUSES.includes(manifest.gates[gate]), `gates.${gate} must be one of ${GATE_STATUSES.join(", ")}`)
186
199
  } else errors.push("gates must be an object")
200
+ if (manifest.gateHistory !== undefined) {
201
+ if (!Array.isArray(manifest.gateHistory)) errors.push("gateHistory must be an array")
202
+ else for (const entry of manifest.gateHistory) {
203
+ check(errors, isObject(entry) && GATE_NAMES.includes(entry.gate) && GATE_STATUSES.includes(entry.status)
204
+ && typeof entry.at === "string" && ISO_DATE.test(entry.at)
205
+ && (entry.reason === undefined || typeof entry.reason === "string"), "gateHistory entries must have a known gate, status, timestamp, and optional reason")
206
+ }
207
+ }
187
208
  if (Array.isArray(manifest.residualDefects)) manifest.residualDefects.forEach((defect, index) => validateDefect(defect, `residualDefects[${index}]`, errors))
188
209
  else errors.push("residualDefects must be an array")
189
210
  check(errors, manifest.repair === null || isObject(manifest.repair), "repair must be null or an object")
@@ -75,6 +75,7 @@ const FIDELITY_BLOCKERS = [
75
75
  const FIDELITY_MAJORS = [
76
76
  "untraceable_number",
77
77
  "unsupported_verification",
78
+ "claim_kind_mismatch",
78
79
  "conflicting_readings",
79
80
  "hedge_dropped",
80
81
  "direction_flipped",
@@ -82,6 +83,7 @@ const FIDELITY_MAJORS = [
82
83
  "qualifier_dropped",
83
84
  "unbound_sentence",
84
85
  "meaning_drift_confirmed",
86
+ "meaning_unverified",
85
87
  "polish_edit_rejected",
86
88
  "polish_hedging_decreased",
87
89
  "polish_text_churn",
@@ -92,6 +94,7 @@ const FIDELITY_MAJORS = [
92
94
  const FIDELITY_MINORS = [
93
95
  "quote_fuzzy_match",
94
96
  "name_outside_quote",
97
+ "verification_claim_unmarked",
95
98
  "scope_widened_warn",
96
99
  "particle_unverified",
97
100
  "fallback_without_entity",
@@ -0,0 +1,209 @@
1
+ // Architecture contract for the polish command (#11160 sweep): the edit contract, the ledger
2
+ // loader, the edition-format grammar, and the severity buckets each have one owner export, so a
3
+ // reintroduced local copy in fact-commands-polish.mjs fails one of these tests.
4
+ import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test"
5
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
6
+ import { tmpdir } from "node:os"
7
+ import { join } from "node:path"
8
+
9
+ import { CliError } from "./cli-support.mjs"
10
+ import { countDefects, DEFECT_CODES, SEVERITIES } from "./contracts.mjs"
11
+ import { runFactMeaning } from "./fact-commands-meaning.mjs"
12
+ import * as polishCommand from "./fact-commands-polish.mjs"
13
+ import { editionFormatOf, ledgerFrom, sessionOf } from "./fact-commands-verify.mjs"
14
+ import { applyEdits, POLISH_EDIT_FIELDS, POLISH_MARKUP_REASON, validatePolishEdits } from "./polish-apply.mjs"
15
+
16
+ const { polishDiffFails, runFactPolish } = polishCommand
17
+ const source = (name: string) => readFileSync(join(import.meta.dir, name), "utf8")
18
+ const LEDGER = { schemaVersion: 1, rows: [], events: [] }
19
+ const doc = (body: string) => `<html><body>\n${body}\n</body></html>`
20
+ const BRIEF = join(import.meta.dir, "tests", "fixtures", "fidelity-brief-locked.md")
21
+
22
+ const dir = mkdtempSync(join(tmpdir(), "polish-copy-returns-"))
23
+ const file = (name: string, content: string) => {
24
+ const path = join(dir, name)
25
+ writeFileSync(path, content)
26
+ return path
27
+ }
28
+ const ledgerPath = file("ledger.json", JSON.stringify(LEDGER))
29
+ const badLedgerPath = file("bad-ledger.json", JSON.stringify({ schemaVersion: 1, rows: "nope" }))
30
+ const reportPath = file("report.html", doc("<p>The span is 320 m [S1].</p>"))
31
+ const savedSession = process.env.SESSION_DIR
32
+ beforeEach(() => {
33
+ delete process.env.SESSION_DIR
34
+ })
35
+ afterEach(() => {
36
+ if (savedSession === undefined) delete process.env.SESSION_DIR
37
+ else process.env.SESSION_DIR = savedSession
38
+ })
39
+ afterAll(() => rmSync(dir, { recursive: true, force: true }))
40
+
41
+ function outcome(run: () => unknown) {
42
+ try {
43
+ run()
44
+ return "ok"
45
+ } catch (error) {
46
+ if (!(error instanceof CliError)) throw error
47
+ return error.message
48
+ }
49
+ }
50
+
51
+ describe("the polish edit contract is polish-apply's", () => {
52
+ const SHAPES = [
53
+ ["not an array", { before: "a", after: "b" }],
54
+ ["a null edit", [null]],
55
+ ["an array edit", [["a", "b"]]],
56
+ ["a missing after", [{ before: "span" }]],
57
+ ["a numeric before", [{ before: 3, after: "b" }]],
58
+ ["a numeric reason", [{ before: "span", after: "span", reason: 5 }]],
59
+ ["a valid edit", [{ before: "The span is", after: "The span is", reason: "none" }]],
60
+ ["a valid edit without reason", [{ before: "The span is", after: "The span is" }]],
61
+ ] as const
62
+
63
+ test("#given the command module #when its edit contract is read #then it is the polish-apply export", () => {
64
+ expect(polishCommand.validatePolishEdits).toBe(validatePolishEdits)
65
+ expect(polishCommand.POLISH_EDIT_FIELDS).toBe(POLISH_EDIT_FIELDS)
66
+ expect(source("fact-commands-polish.mjs")).not.toMatch(/typeof edit\?\.(before|after)/u)
67
+ })
68
+
69
+ for (const [name, edits] of SHAPES) {
70
+ test(`#given ${name} #when applied directly and through the CLI #then both accept or both refuse`, () => {
71
+ const editsPath = file(`edits-${name.replaceAll(" ", "-")}.json`, JSON.stringify(edits))
72
+ const direct = (() => {
73
+ try {
74
+ applyEdits(readFileSync(reportPath, "utf8"), edits as never, LEDGER)
75
+ return "ok"
76
+ } catch (error) {
77
+ expect(error).toBeInstanceOf(TypeError)
78
+ return "refused"
79
+ }
80
+ })()
81
+ const cli = outcome(() => runFactPolish("polish-apply", [reportPath, "--edits", editsPath, "--ledger", ledgerPath, "--out", join(dir, "out.html")]))
82
+ expect(cli === "ok" ? "ok" : "refused").toBe(direct)
83
+ if (cli !== "ok") expect(cli).toContain(editsPath)
84
+ })
85
+ }
86
+ })
87
+
88
+ describe("the ledger loader is one shared helper", () => {
89
+ const pairs = (extra: string[]) => () => runFactMeaning("pairs", [reportPath, "--brief", BRIEF, "--out", join(dir, "pairs.json"), ...extra])
90
+ const diff = (extra: string[]) => () => runFactPolish("polish-diff", ["--before", reportPath, "--after", reportPath, ...extra])
91
+
92
+ test("#given the command modules #when read #then neither defines its own ledger loader or session resolver", () => {
93
+ expect(polishCommand.ledgerFrom).toBe(ledgerFrom)
94
+ for (const name of ["fact-commands-meaning.mjs", "fact-commands-polish.mjs"]) {
95
+ const text = source(name)
96
+ expect(text, name).not.toMatch(/function (readLedgerFrom|ledgerFrom)\b|validateLedger\(/u)
97
+ expect(text, name).not.toMatch(/const sessionOf\s*=/u)
98
+ }
99
+ })
100
+
101
+ test("#given the same valid, invalid, and missing ledgers #when fact pairs and fact polish-diff load them #then both give the same outcome", () => {
102
+ for (const extra of [["--ledger", ledgerPath], ["--ledger", badLedgerPath], []]) {
103
+ const expected = outcome(() => ledgerFrom({ ...(extra.length > 0 ? { ledger: extra[1] } : {}) }))
104
+ expect(outcome(pairs(extra)), extra.join(" ")).toBe(expected)
105
+ expect(outcome(diff(extra)), extra.join(" ")).toBe(expected)
106
+ }
107
+ expect(outcome(() => ledgerFrom({ ledger: badLedgerPath }))).toContain("is not a valid fact ledger")
108
+ expect(outcome(() => ledgerFrom({}))).toContain("--ledger is required")
109
+ })
110
+
111
+ test("#given a session dir flag or SESSION_DIR #when resolved #then the flag wins and a bare flag is no session", () => {
112
+ expect(sessionOf({ "session-dir": dir })).toBe(dir)
113
+ expect(sessionOf({ "session-dir": true })).toBeNull()
114
+ process.env.SESSION_DIR = dir
115
+ expect(sessionOf({})).toBe(dir)
116
+ })
117
+ })
118
+
119
+ describe("the edition format comes from verify's grammar", () => {
120
+ test("#given report paths of each case #when polish-apply reads them #then it refuses exactly the Markdown classifications", () => {
121
+ const editsPath = file("edits-ok.json", "[]")
122
+ expect(source("fact-commands-polish.mjs")).not.toMatch(/md\|markdown/u)
123
+ for (const name of ["r.md", "R.MD", "r.markdown", "R.Markdown", "r.html", "r.HTML"]) {
124
+ const path = file(name, readFileSync(reportPath, "utf8"))
125
+ const result = outcome(() => runFactPolish("polish-apply", [path, "--edits", editsPath, "--ledger", ledgerPath, "--out", join(dir, "out.html")]))
126
+ expect(result.includes("takes the filled HTML edition"), name).toBe(editionFormatOf(path, undefined) === "md")
127
+ }
128
+ })
129
+ })
130
+
131
+ describe("polish-diff counts with contracts and exits on blockers only", () => {
132
+ type DiffRun = { exitCode: number; json: { counts: Record<string, number>; defects: { severity: string }[] } }
133
+ const run = (before: string, after: string) => runFactPolish("polish-diff", ["--before", file("b.html", doc(before)), "--after", file("a.html", doc(after)), "--ledger", ledgerPath]) as unknown as DiffRun
134
+
135
+ test("#given every registry code #when polishDiffFails classifies it #then only registry blockers fail", () => {
136
+ for (const [code, entry] of Object.entries(DEFECT_CODES) as [string, { severity: string }][]) expect(polishDiffFails([{ code, severity: "minor" }]), code).toBe(entry.severity === "blocker")
137
+ expect(polishDiffFails([])).toBe(false)
138
+ expect(source("fact-commands-polish.mjs")).not.toMatch(/blocker:\s*0|counts\.blocker > 0/u)
139
+ })
140
+
141
+ test("#given a major-only polish diff #when run #then it is reported with contract buckets and exits 0", () => {
142
+ const result = run("<p>The span may be 320 m [S1].</p>", "<p>The span is 320 m [S1].</p>")
143
+ const { defects } = result.json
144
+ expect(defects.length).toBeGreaterThan(0)
145
+ expect(defects.every((defect) => defect.severity === "major")).toBe(true)
146
+ expect(result.json.counts).toEqual(countDefects(defects))
147
+ expect(Object.keys(result.json.counts)).toEqual([...SEVERITIES, "total"])
148
+ expect(result.exitCode).toBe(0)
149
+ })
150
+
151
+ test("#given a changed numeral #when run #then the blocker exits 1", () => {
152
+ const result = run("<p>Throughput grew 3.5 times [S1].</p>", "<p>Throughput grew 35 times [S1].</p>")
153
+ expect(result.json.counts).toEqual(countDefects(result.json.defects))
154
+ expect(result.exitCode).toBe(1)
155
+ })
156
+ })
157
+
158
+ describe("fact polish-apply prints every rejection reason verbatim", () => {
159
+ type ApplyRun = { summary: string; exitCode: number; json: { defects: { index: number; code: string; reason: string }[]; data: { rejected: number; rejected_reasons: string[] } } }
160
+
161
+ test("#given an edit holding & #when fact polish-apply runs #then the printed line carries polish-apply's own markup reason", () => {
162
+ const before = "Revenue & profit rose across the year"
163
+ const editsPath = file("edits-markup-ampersand.json", JSON.stringify([{ before, after: "Revenue and profit rose across the year", reason: "wording" }]))
164
+ const run = runFactPolish("polish-apply", [reportPath, "--edits", editsPath, "--ledger", ledgerPath, "--out", join(dir, "ampersand.html")]) as unknown as ApplyRun
165
+
166
+ expect(run.exitCode).toBe(0)
167
+ expect(run.json.data.rejected).toBe(1)
168
+ expect(run.json.defects.map((defect) => [defect.index, defect.code, defect.reason])).toEqual([[0, "polish_edit_rejected", POLISH_MARKUP_REASON]])
169
+ const line = ` rejected edit 0 ("${before}"): ${POLISH_MARKUP_REASON}`
170
+ expect(run.json.data.rejected_reasons).toEqual([line])
171
+ expect(run.summary.split("\n")[1]).toBe(line)
172
+ })
173
+ })
174
+
175
+ describe("--changed-since stays fail-closed", () => {
176
+ const { pairsChangedSince } = polishCommand as unknown as { pairsChangedSince: (pairs: unknown[], mapPath: string, ledger: unknown, claims: unknown) => unknown[] }
177
+ const artifact = file("pre-polish.html", doc("<p>The span is 320 m [S1].</p>"))
178
+
179
+ test("#given a fill map without artifact_sha256 #when --changed-since reads it #then it refuses", () => {
180
+ const mapPath = file("fill-map-no-sha.json", JSON.stringify({ artifact, format: "html", entries: [] }))
181
+ expect(outcome(() => pairsChangedSince([], mapPath, LEDGER, null))).toContain("fill map has no artifact_sha256")
182
+ })
183
+
184
+ test("#given a fill map whose artifact changed since fill #when read #then it still refuses", () => {
185
+ const mapPath = file("fill-map-stale.json", JSON.stringify({ artifact, artifact_sha256: "0".repeat(64), format: "html", entries: [] }))
186
+ expect(outcome(() => pairsChangedSince([], mapPath, LEDGER, null))).toContain("changed after fill")
187
+ })
188
+ })
189
+
190
+ describe("polish-diff refuses an unknown defect code", () => {
191
+ test("#given a defect whose code is not in the registry #when classified #then it refuses instead of reading its severity", () => {
192
+ expect(outcome(() => polishDiffFails([{ code: "not_a_real_code", severity: "minor" }]))).toContain("unknown defect code not_a_real_code")
193
+ expect(source("fact-commands-polish.mjs")).not.toMatch(/\?\?\s*defect\.severity/u)
194
+ })
195
+ })
196
+
197
+ describe("polish-apply writes atomically and honours --format", () => {
198
+ test("#given the command module #when read #then it writes the edition through writeTextAtomic", () => {
199
+ const text = source("fact-commands-polish.mjs")
200
+ expect(text).not.toMatch(/writeFileSync\s*\(/u)
201
+ expect(text).toMatch(/writeTextAtomic\(flags\.out/u)
202
+ })
203
+
204
+ test("#given --format md on an .html report #when polish-apply runs #then it refuses the Markdown edition", () => {
205
+ const editsPath = file("edits-format.json", "[]")
206
+ const result = outcome(() => runFactPolish("polish-apply", [reportPath, "--edits", editsPath, "--ledger", ledgerPath, "--out", join(dir, "fmt.html"), "--format", "md"]))
207
+ expect(result).toContain("takes the filled HTML edition")
208
+ })
209
+ })