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,163 @@
1
+ // fidelity-records.mjs - the fidelity record schema and the delivery barrier of `outcome verify`
2
+ // and `outcome finish`.
3
+ //
4
+ // The table and the schema are the spec for the barrier. Every fidelity record is written through
5
+ // writeFidelityRecord, which stamps one envelope (RECORD_ENVELOPE: record_kind, artifact_sha256,
6
+ // report, written_at, defects) plus the fields its kind declares, and read through
7
+ // readFidelityRecord, which refuses a missing, unknown, or ill-typed field (fail closed).
8
+ // FIDELITY_RECORDS holds every per-record rule as a column: where the records are (targets), when a
9
+ // delivered session must carry one (requiredWhen), which envelope field binds it to the delivered
10
+ // bytes (bindsTo), and which defects block delivery (blocking). The barrier is one loop over the
11
+ // table with no record-specific branch; a new fidelity step adds a row here and nothing else.
12
+ //
13
+ // HTML/PDF/DOCX need a gate record or an explicitly logged not_run reason, even without a ledger.
14
+ // That logged exemption covers deliveries this harness never gated: once a ledger or any session
15
+ // fidelity record exists, the barrier refuses the unverified edition regardless of it, and the same
16
+ // exemption is the only way a required record may be absent.
17
+
18
+ import { existsSync, readFileSync } from "node:fs"
19
+ import { join } from "node:path"
20
+
21
+ import { CliError, readJsonFile, sha256Of, writeJsonAtomic } from "./cli-support.mjs"
22
+ import { LEDGER_FILENAME } from "./fact-ledger.mjs"
23
+ import { meaningCheckEnabled, requireLockedBrief } from "./fidelity-brief.mjs"
24
+ import { blocksDelivery, deliveryBlockReason } from "./gates-fidelity.mjs"
25
+
26
+ export const FIDELITY_RECORD = "fidelity-defects.json"
27
+ export const MEANING_RECORD = "fidelity-meaning-defects.json"
28
+
29
+ /** The fields every fidelity record carries, whatever its kind. */
30
+ export const RECORD_ENVELOPE = Object.freeze(["record_kind", "artifact_sha256", "report", "written_at", "defects"])
31
+
32
+ /** Formats that need a fidelity record even in a session with no ledger and no record. */
33
+ const GATED_FORMATS = Object.freeze(["html", "pdf", "docx"])
34
+
35
+ /** The meaning record is required unless the locked brief turns the check off; a malformed block cannot prove off. */
36
+ function meaningRequired({ sessionDir }) {
37
+ const lock = requireLockedBrief(join(sessionDir, "brief.md"))
38
+ return lock.exitCode === 2 || (lock.locked && meaningCheckEnabled(lock.parsed))
39
+ }
40
+
41
+ const subjectOf = (row) => row.renderedFrom ?? row.path ?? null
42
+
43
+ /** One record per delivered edition: the file its row names with --verified-by, bound to that edition's bytes. */
44
+ const perEdition = (sessionDir, delivered) =>
45
+ delivered.map((row) => ({ path: row.verifiedBy ?? null, declared: row.verifiedBy !== undefined, format: row.format, at: `${row.format}: `, subjects: [subjectOf(row)].filter((subject) => subject !== null) }))
46
+
47
+ /** One record per session, in the session directory, bound to any delivered edition's bytes. */
48
+ const perSession = (file) => (sessionDir, delivered) => [{ path: join(sessionDir, file), declared: false, format: null, at: "", subjects: delivered.map(subjectOf).filter((subject) => subject !== null) }]
49
+
50
+ /** Every fidelity record the barrier reads, in check order; each column is one rule the barrier applies. */
51
+ export const FIDELITY_RECORDS = Object.freeze([
52
+ Object.freeze({
53
+ kind: "fidelity",
54
+ file: FIDELITY_RECORD,
55
+ step: "fidelity gate",
56
+ targets: perEdition,
57
+ requiredWhen: ({ hasFidelity }, target) => hasFidelity || GATED_FORMATS.includes(target.format),
58
+ remedy: (target) => `run fact verify on ${target.subjects[0] ?? "the edition"} and set it with --verified-by`,
59
+ bindsTo: "artifact_sha256",
60
+ blocking: blocksDelivery,
61
+ payload: Object.freeze(["command", "status", "exitCode", "format", "counts", "gatesRun"]),
62
+ }),
63
+ Object.freeze({
64
+ kind: "meaning",
65
+ file: MEANING_RECORD,
66
+ step: "meaning gate",
67
+ targets: perSession(MEANING_RECORD),
68
+ requiredWhen: meaningRequired,
69
+ remedy: () => "run fact pairs and fact meaning-check before delivery",
70
+ bindsTo: "artifact_sha256",
71
+ blocking: blocksDelivery,
72
+ payload: Object.freeze(["command", "status", "exitCode", "run_id", "pairs_sha256", "counts", "dropped", "unverified", "calibration_path", "calibration_written", "calibration_replaced", "promotion_eligibility"]),
73
+ }),
74
+ ])
75
+
76
+ const entryOf = (kind) => {
77
+ const entry = FIDELITY_RECORDS.find((row) => row.kind === kind)
78
+ if (entry === undefined) throw new CliError(`unknown fidelity record kind ${kind}`)
79
+ return entry
80
+ }
81
+
82
+ /** Why a value is not a valid record of this kind, or null. */
83
+ function envelopeProblem(record, entry) {
84
+ if (record === null || typeof record !== "object" || Array.isArray(record)) return "not a record object"
85
+ const missing = RECORD_ENVELOPE.filter((key) => !Object.hasOwn(record, key))
86
+ if (missing.length > 0) return `missing ${missing.join(", ")}`
87
+ const unknown = Object.keys(record).filter((key) => !RECORD_ENVELOPE.includes(key) && !entry.payload.includes(key))
88
+ if (unknown.length > 0) return `unknown field(s) ${unknown.join(", ")}`
89
+ if (record.record_kind !== entry.kind) return `record_kind ${JSON.stringify(record.record_kind)} is not ${entry.kind}`
90
+ if (typeof record.artifact_sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(record.artifact_sha256)) return "artifact_sha256 is not a sha256 hex digest"
91
+ if (typeof record.report !== "string" || record.report === "") return "report is not a path"
92
+ if (typeof record.written_at !== "string" || Number.isNaN(Date.parse(record.written_at))) return "written_at is not an ISO time"
93
+ if (!Array.isArray(record.defects) || !record.defects.every((defect) => typeof defect?.code === "string")) return "defects is not a list of coded defects"
94
+ return null
95
+ }
96
+
97
+ /**
98
+ * Write one fidelity record of a kind in the table: the envelope plus that kind's payload fields.
99
+ * @param {string} path
100
+ * @param {string} kind a FIDELITY_RECORDS kind
101
+ * @param {{ artifact_sha256: string, report: string, defects: object[], [field: string]: unknown }} fields the envelope values plus the kind's payload
102
+ */
103
+ export function writeFidelityRecord(path, kind, { artifact_sha256, report, defects, ...payload }, now = new Date()) {
104
+ const entry = entryOf(kind)
105
+ const record = { record_kind: kind, artifact_sha256, report, written_at: now.toISOString(), defects, ...payload }
106
+ const problem = envelopeProblem(record, entry)
107
+ if (problem !== null) throw new CliError(`refusing to write a malformed ${kind} record: ${problem}`)
108
+ writeJsonAtomic(path, record)
109
+ return record
110
+ }
111
+
112
+ /** Read one fidelity record of a kind; anything but a valid envelope is a CliError naming the path. */
113
+ export function readFidelityRecord(path, kind) {
114
+ const entry = entryOf(kind)
115
+ let record
116
+ try {
117
+ record = readJsonFile(path)
118
+ } catch (error) {
119
+ throw new CliError(`${kind} record ${path} is malformed: ${error.message}`)
120
+ }
121
+ const problem = envelopeProblem(record, entry)
122
+ if (problem !== null) throw new CliError(`${kind} record ${path} is malformed: ${problem}`)
123
+ return record
124
+ }
125
+
126
+ const unresolved = (format, codes, path) => ({ code: "fidelity_unresolved", format, codes, message: codes.map((code) => deliveryBlockReason({ code })).join("; "), path })
127
+ const hashOf = (path) => (existsSync(path) ? sha256Of(readFileSync(path)) : null)
128
+
129
+ /** Barrier problems for `outcome verify` and `outcome finish`; empty when nothing blocks delivery. */
130
+ export function fidelityProblems(sessionDir, manifest) {
131
+ const delivered = manifest.deliverables.filter((row) => row.status === "delivered")
132
+ const hasFidelity = existsSync(join(sessionDir, LEDGER_FILENAME)) || FIDELITY_RECORDS.some((entry) => existsSync(join(sessionDir, entry.file)))
133
+ const lastFidelity = manifest.gateHistory?.findLast((entry) => entry.gate === "fidelity")
134
+ const exempt = !hasFidelity && manifest.gates.fidelity === "not_run" && lastFidelity?.status === "not_run" && Boolean(lastFidelity.reason?.trim())
135
+ const context = { sessionDir, hasFidelity }
136
+ const problems = []
137
+ const counted = new Set()
138
+ for (const entry of FIDELITY_RECORDS) {
139
+ for (const target of entry.targets(sessionDir, delivered)) {
140
+ const refuse = (message, path = target.path) => problems.push({ code: "edition_unverified", format: target.format, message: `${target.at}${message}`, path })
141
+ if (target.path === null || !existsSync(target.path)) {
142
+ if (target.declared) refuse(`${entry.kind} record ${target.path} is missing`)
143
+ else if (delivered.length > 0 && !exempt && entry.requiredWhen(context, target)) refuse(`${entry.step} not run; ${entry.remedy(target)}`, target.path ?? target.subjects[0] ?? null)
144
+ continue
145
+ }
146
+ let record
147
+ try {
148
+ record = readFidelityRecord(target.path, entry.kind)
149
+ } catch (error) {
150
+ refuse(error.message)
151
+ continue
152
+ }
153
+ if (target.subjects.length > 0 && !target.subjects.some((subject) => hashOf(subject) === record[entry.bindsTo])) {
154
+ refuse(`${entry.kind} record ${target.path} is for a different edition than ${target.subjects.join(", ")}`)
155
+ }
156
+ if (counted.has(target.path)) continue
157
+ counted.add(target.path)
158
+ const codes = [...new Set(record.defects.filter(entry.blocking).map((defect) => defect.code))].sort()
159
+ if (codes.length > 0) problems.push(unresolved(target.format, codes, target.path))
160
+ }
161
+ }
162
+ return problems
163
+ }
@@ -1,6 +1,7 @@
1
1
  // fidelity-state.mjs - per-sentence failure counts for the fidelity repair loop. A sentence is
2
2
  // keyed by its section slug and the sorted fact ids it cites, so a reworded sentence that
3
- // cites the same facts keeps its count. node: builtins only.
3
+ // cites the same facts keeps its count. The exact slug and id rules are in the comment above
4
+ // slugify in fact-fill.mjs. node: builtins only.
4
5
 
5
6
  import { existsSync } from "node:fs"
6
7
  import { join } from "node:path"
@@ -0,0 +1,85 @@
1
+ // fidelity-trace.mjs - the numeral and quotation trace of the deterministic fidelity gate.
2
+ //
3
+ // Every numeral in prose and table cells outside sources, toc and chart figures (dates included)
4
+ // must sit in a data-fact, data-translates, q or blockquote element, or at the site of a fill-map
5
+ // entry; otherwise untraceable_number. Quoted runs with no Q row are quote_unbound, and leftover
6
+ // {{...}} pointers unresolved_placeholder. No model, no network.
7
+ //
8
+ // Fill-map assumption (fact-fill.mjs fill(), #11141): format "html" wraps every value and quote in
9
+ // <data data-fact> / <q data-fact> / <blockquote data-fact>, which this trace skips by element, so
10
+ // it needs no fill map. Format "md" writes bare text, and each fillMap entry is
11
+ // { fact_id, rendered, start, end } with start/end indexing the filled Markdown, not the HTML this
12
+ // gate reads. fill() pushes entries in output order and the Markdown-to-HTML step keeps text order,
13
+ // so each entry, in array order, is matched to the next untraced occurrence of its rendered text in
14
+ // document order that is a whole numeral and reads as the entry's row there (fits: "1,237 million"
15
+ // is not F2's 1,237), and strips that one occurrence only. A copy of the same text that no entry
16
+ // accounts for stays in the scan; an entry whose text is gone strips nothing.
17
+
18
+ import { NOT_CLAIMS } from "./numerals.mjs"
19
+
20
+ /** Structural numerals: versions, figure/table/section references, times (NOT_CLAIMS 7-11), pages, citations, placeholders. */
21
+ export const STRUCTURAL = [...NOT_CLAIMS.slice(7, 12), /\b(?:p|pp|page|pages)\.?\s*\d+(?:\s*[-\u2013]\s*\d+)?/gi, /\[(?:S|Source\s*)\d+\]|\[\^\d+\]/gi, /\{\{[^}]*\}\}/g]
22
+ const NUMERAL = /\d+(?:[.,\-/:]\d+)*/g
23
+ const TRACED = "\u0000"
24
+ const QUOTE_RUNS = [/"([^"]+)"/g, /\u201C([^\u201C\u201D]+)\u201D/g, /\u300C([^\u300C\u300D]+)\u300D/g, /\u300E([^\u300E\u300F]+)\u300F/g]
25
+
26
+ export const clip = (text) => String(text).replace(/\s+/g, " ").trim().slice(0, 80)
27
+
28
+ function ownerTexts(ctx, charts) {
29
+ const owners = new Map()
30
+ for (const entry of ctx.texts) {
31
+ const kind = ctx.kindOf(entry.scope)
32
+ if (kind === "sources" || kind === "toc") continue
33
+ if (charts.has(ctx.util.closest(entry.node, (cur) => cur.tag === "figure"))) continue
34
+ const hidden = ctx.util.closest(entry.node, (cur) => cur.attrs["data-fact"] !== undefined || cur.attrs["data-translates"] !== undefined || cur.tag === "q" || cur.tag === "blockquote")
35
+ owners.set(entry.owner, (owners.get(entry.owner) ?? "") + (hidden ? ` ${TRACED} ` : entry.node.text))
36
+ }
37
+ return owners
38
+ }
39
+
40
+ function scanOwner(ctx, owner, raw, flag) {
41
+ let text = raw
42
+ if (/^h[1-6]$/.test(owner.tag)) text = text.replace(/^\s*\d+(?:[.-]\d+)*[.):]?\s+/, " ")
43
+ if (text.includes("{{")) flag("unresolved_placeholder", owner, `leftover placeholder: "${clip(text.slice(text.indexOf("{{")))}"`)
44
+ for (const re of QUOTE_RUNS) {
45
+ for (const match of text.matchAll(re)) {
46
+ const inner = match[1].replaceAll(TRACED, " ").trim()
47
+ const after = text.slice(match.index + match[0].length).trim().split(/\s+/).slice(0, 6).join(" ")
48
+ if (inner && (inner.split(/\s+/).length >= 3 || ctx.util.citationMarker.test(after))) flag("quote_unbound", owner, `direct quotation with no Q row: "${clip(match[0])}"`)
49
+ }
50
+ }
51
+ const rest = STRUCTURAL.reduce((acc, re) => acc.replace(re, " "), text)
52
+ for (const match of rest.matchAll(NUMERAL)) flag("untraceable_number", owner, `number "${match[0]}" has no ledger row: "${clip(rest.slice(Math.max(0, match.index - 30), match.index + 30))}"`)
53
+ }
54
+
55
+ /** Strip each fill-map entry at its own fill site: the next untraced occurrence after the previous entry's site. */
56
+ function traceFills(owners, fillMap, fits) {
57
+ const keys = [...owners.keys()]
58
+ const texts = [...owners.values()]
59
+ let [index, from] = [0, 0]
60
+ for (const entry of fillMap ?? []) {
61
+ const { rendered } = entry
62
+ if (!rendered) continue
63
+ for (let i = index, at = from; i < texts.length; i += 1, at = 0) {
64
+ let hit = texts[i].indexOf(rendered, at)
65
+ const glued = (pos) => /\d/.test(texts[i][pos - 1] ?? "") || /\d/.test(texts[i][pos + rendered.length] ?? "")
66
+ while (hit !== -1 && (glued(hit) || !fits(entry, texts[i].slice(hit)))) hit = texts[i].indexOf(rendered, hit + 1)
67
+ if (hit === -1) continue
68
+ texts[i] = `${texts[i].slice(0, hit)}${TRACED}${texts[i].slice(hit + rendered.length)}`
69
+ ;[index, from] = [i, hit + TRACED.length]
70
+ break
71
+ }
72
+ }
73
+ return new Map(keys.map((key, i) => [key, texts[i]]))
74
+ }
75
+
76
+ /**
77
+ * @param {object} ctx buildContext(...) from gates-static.mjs
78
+ * @param {Map<object, unknown>} charts chart figures, whose numerals chart checks own
79
+ * @param {{ fact_id: string, rendered: string, start?: number, end?: number }[] | null} fillMap Markdown fill map, or null
80
+ * @param {(code: string, node: object, message: string) => void} flag
81
+ * @param {(entry: object, tail: string) => boolean} fits whether the text from a candidate site on reads as the entry's row
82
+ */
83
+ export function traceChecks(ctx, charts, fillMap, flag, fits) {
84
+ for (const [owner, text] of traceFills(ownerTexts(ctx, charts), fillMap, fits)) scanOwner(ctx, owner, text, flag)
85
+ }
@@ -0,0 +1,229 @@
1
+ // fidelity-values.mjs - value, quote, and chart checks of the deterministic fidelity gate.
2
+ //
3
+ // Each data-fact element must match its ledger row, and each Q element the script's
4
+ // reconstruction of its selection (report text never gets a fuzzy pass). Cited rows, table rows,
5
+ // snapshots, and chart CSVs are rechecked; the numeral and quotation trace is fidelity-trace.mjs,
6
+ // and the claim-kind checks (data-claim-kind, unsupported_verification) are fidelity-claims.mjs.
7
+ // No model, no network.
8
+
9
+ import { existsSync, readFileSync } from "node:fs"
10
+ import { join, resolve, sep } from "node:path"
11
+
12
+ import { sha256Of } from "./cli-support.mjs"
13
+ import { sameValue, unitKey } from "./fidelity-binding.mjs"
14
+ import { claimChecks } from "./fidelity-claims.mjs"
15
+ import { STRUCTURAL, clip, traceChecks } from "./fidelity-trace.mjs"
16
+ import { parseHtml, proseText } from "./html-lite.mjs"
17
+ import { parseNumeral } from "./numerals.mjs"
18
+ import { div, fromDecimal, sub } from "./rational.mjs"
19
+ import { normalizeText } from "./text-normalize.mjs"
20
+
21
+ const DECIMAL = /^-?\d+(?:\.\d+)?$/
22
+
23
+ const bare = (text) => normalizeText(String(text ?? "").replace(/\s*\[(?:S|Source\s*)\d+\]\s*$/i, "")).replace(/\u2026/g, "...").replace(/\s*\.\.\.\s*/g, "...").replace(/^["']+|["']+$/g, "").trim()
24
+
25
+ /** The text fill renders for a quote selection: full, lead=<n>, or from=<a>;to=<b> (placeholder form accepted). */
26
+ export function selectQuote(quote, selector) {
27
+ const text = String(quote ?? "").trim()
28
+ if (selector === "full") return text
29
+ const lead = /^lead=(\d+)$/.exec(selector)
30
+ const words = text.split(/\s+/)
31
+ if (lead) return words.length <= Number(lead[1]) ? text : `${words.slice(0, Number(lead[1])).join(" ")}...`
32
+ const range = /^from=(.+);to=(.+)$/.exec(selector) ?? /^from="(.+)":to="(.+)"$/.exec(selector)
33
+ if (!range) return null
34
+ const once = (anchor) => text.indexOf(anchor) !== -1 && text.indexOf(anchor) === text.lastIndexOf(anchor)
35
+ if (!once(range[1]) || !once(range[2])) return null
36
+ const start = text.indexOf(range[1])
37
+ const end = text.indexOf(range[2]) + range[2].length
38
+ if (end <= start) return null
39
+ return `${start > 0 ? "..." : ""}${text.slice(start, end)}${end < text.length ? "..." : ""}`
40
+ }
41
+
42
+ function quoteProblem(row, shown, selector) {
43
+ const want = selectQuote(row.quote, selector)
44
+ if (want === null) return ["quote_not_verbatim", `selection "${selector}" does not resolve in ${row.fact_id}'s quote`]
45
+ return bare(shown) === bare(want) ? null : ["quote_not_verbatim", `"${clip(shown)}" is not the ${selector} selection of ${row.fact_id}: "${clip(want)}"`]
46
+ }
47
+
48
+ function factProblem(row, shown, attrs = {}) {
49
+ if (attrs["data-unit"] !== undefined && unitKey(attrs["data-unit"]) !== unitKey(row.unit)) return ["unit_mismatch", `data-unit "${attrs["data-unit"]}" but ${row.fact_id} is in "${row.unit}"`]
50
+ if (attrs.value !== undefined && !sameValue(attrs.value, row.value)) return ["value_mismatch", `value="${attrs.value}" but ${row.fact_id} is ${row.value}`]
51
+ const text = normalizeText(shown)
52
+ const forms = [row.raw, row.render, row.value, row.unit && `${row.value} ${row.unit}`].filter(Boolean).map(normalizeText)
53
+ if (forms.some((form) => text === form || (text.endsWith(` ${form}`) && !/\d/.test(text.slice(0, -form.length))))) return null
54
+ const parsed = parseNumeral(text)
55
+ if (!parsed) return unitKey(text) === unitKey(row.unit) ? null : ["value_mismatch", `"${clip(shown)}" shows no value of ${row.fact_id} (${row.value})`]
56
+ if (!sameValue(parsed.value, row.value)) return ["value_mismatch", `"${clip(shown)}" reads ${parsed.value}, but ${row.fact_id} is ${row.value}`]
57
+ if (parsed.unitRaw && unitKey(parsed.unitRaw) !== unitKey(row.unit)) return ["unit_mismatch", `"${clip(shown)}" is in ${parsed.unitRaw}, but ${row.fact_id} is in ${row.unit}`]
58
+ return null
59
+ }
60
+
61
+ /** Whether a candidate fill site reads as what fill wrote there or as the entry's row ("1,237 million" is neither 1,237 nor F2). */
62
+ const fillSiteFits = (rows) => (entry, tail) => {
63
+ const [row, site, own] = [rows.get(entry.fact_id), parseNumeral(tail, 0), parseNumeral(entry.rendered, 0)]
64
+ if (!row || row.kind === "quote" || !site || !own) return true
65
+ if (sameValue(site.value, own.value) && unitKey(site.unitRaw ?? "") === unitKey(own.unitRaw ?? "")) return true
66
+ return factProblem(row, tail.slice(0, Math.max(entry.rendered.length, site.span[1]))) === null
67
+ }
68
+
69
+ const withoutFooter = (node) => proseText({ children: node.children.filter((child) => child.tag !== "footer") })
70
+
71
+ /** One row per active value row, as `fact export --csv` writes it. */
72
+ export function exportRows(ledger) {
73
+ return (ledger?.rows ?? []).filter((row) => row.status === "active" && row.kind !== "quote" && row.value !== null).map((row) => ({ fact_id: row.fact_id, label: row.label, plot_value: row.value, unit: row.unit ?? "" }))
74
+ }
75
+
76
+ function readCsv(path) {
77
+ if (!existsSync(path)) return null
78
+ const lines = readFileSync(path, "utf8").split(/\r?\n/).filter((line) => line.trim() !== "")
79
+ const cells = (line) => line.split(",").map((cell) => cell.trim().replace(/^"(.*)"$/, "$1"))
80
+ const head = cells(lines[0] ?? "")
81
+ const [id, value, unit] = ["fact_id", "plot_value", "unit"].map((name) => head.indexOf(name))
82
+ if (id < 0 || value < 0 || unit < 0 || lines.length < 2) return null
83
+ return lines.slice(1).map((line) => cells(line)).map((c) => ({ fact_id: c[id], plot_value: c[value], unit: c[unit] ?? "" }))
84
+ }
85
+
86
+ const walk = (node) => node.children.filter((child) => child.tag !== "#text").flatMap((child) => [child, ...walk(child)])
87
+
88
+ /** SVG text nodes of a chart: data labels, or ticks with their declared grid. */
89
+ function svgLabels(raw) {
90
+ return walk(parseHtml(raw)).filter((node) => node.tag === "text").map((node) => {
91
+ let tick = null
92
+ for (let cur = node.parent; cur && !tick; cur = cur.parent) if (cur.tag === "g" && /(?:^|\s)ticks(?:\s|$)/.test(cur.attrs.class ?? "")) tick = cur.attrs
93
+ return { text: proseText(node), tick }
94
+ })
95
+ }
96
+
97
+ function onGrid(value, tick) {
98
+ const [step, min, max] = [tick["data-tick-step"], tick["data-tick-min"], tick["data-tick-max"]]
99
+ if (![value, step, min, max].every((v) => DECIMAL.test(String(v ?? ""))) || /^-?0+(?:\.0+)?$/.test(step)) return false
100
+ const offset = sub(fromDecimal(value), fromDecimal(min))
101
+ return offset.num >= 0n && sub(fromDecimal(max), fromDecimal(value)).num >= 0n && div(offset, fromDecimal(step)).den === 1n
102
+ }
103
+
104
+ function numerals(text) {
105
+ const out = []
106
+ for (let at = 0, n = parseNumeral(text, 0); n; at = n.span[1], n = parseNumeral(text, at)) out.push(n.value)
107
+ return out
108
+ }
109
+
110
+ function chartFigures(ctx) {
111
+ const charts = new Map()
112
+ for (const fig of ctx.all.filter((node) => node.tag === "figure")) {
113
+ const inner = ctx.util.descendants(fig)
114
+ const labels = inner.filter((node) => node.tag === "svg").flatMap((svg) => svgLabels(svg.raw ?? ""))
115
+ const marked = fig.attrs["data-chart-source"] !== undefined || ctx.util.classTokens(fig).includes("chart")
116
+ if (marked || inner.some((node) => node.tag === "img" && ctx.util.classTokens(node).includes("chart")) || labels.some((label) => /\d/.test(label.text))) charts.set(fig, labels)
117
+ }
118
+ return charts
119
+ }
120
+
121
+ function chartProblem(ctx, fig, labels, ledger) {
122
+ const source = fig.attrs["data-chart-source"]
123
+ if (!source) return "chart figure has no data-chart-source"
124
+ const csv = readCsv(resolve(ctx.baseDir ?? ".", source))
125
+ if (!csv) return `chart data ${source} is missing or lacks fact_id, plot_value, unit columns`
126
+ const exported = new Map(exportRows(ledger).map((row) => [row.fact_id, row]))
127
+ const drift = csv.find((row) => exported.get(row.fact_id)?.plot_value !== row.plot_value || exported.get(row.fact_id)?.unit !== row.unit)
128
+ if (drift) return `chart data row ${drift.fact_id} differs from the ledger export`
129
+ const plotted = (value) => csv.some((row) => sameValue(row.plot_value, value))
130
+ for (const label of labels) {
131
+ const bad = numerals(label.text).find((value) => (label.tick ? !onGrid(value, label.tick) : !plotted(value)))
132
+ if (bad !== undefined) return `${label.tick ? "tick" : "label"} "${label.text}" is not ${label.tick ? "on the declared tick grid" : `a plot_value of ${source}`}`
133
+ }
134
+ const caption = ctx.util.descendants(fig).find((node) => node.tag === "figcaption")
135
+ const stray = numerals(caption ? STRUCTURAL.reduce((text, re) => text.replace(re, " "), proseText(caption)) : "").find((value) => !plotted(value))
136
+ return stray === undefined ? null : `caption numeral ${stray} is not a plot_value of ${source}`
137
+ }
138
+
139
+ /**
140
+ * @param {object} ctx buildContext(...) from gates-static.mjs
141
+ * @param {{ rows: object[] }} ledger
142
+ * @param {{ fact_id: string, rendered: string, selector?: string }[] | null} fillMap Markdown fill map, or null
143
+ * @param {string | null} sourcesDir directory of <S-id>/{text.txt, meta.json} snapshots
144
+ * @param {{ recompute?: (ledger: object, row: object) => { ok: boolean }, asOf?: string | null }} options
145
+ */
146
+ export function valueChecks(ctx, ledger, fillMap, sourcesDir, options = {}) {
147
+ const out = []
148
+ const flag = (code, node, message) => out.push(ctx.defect(code, node, message))
149
+ const rows = new Map((ledger?.rows ?? []).map((row) => [row.fact_id, row]))
150
+ const cited = new Map()
151
+ const check = (id, node, problemOf) => {
152
+ const row = rows.get(id)
153
+ if (!row) return flag("unresolved_placeholder", node, `${id} names no ledger row`)
154
+ if (!cited.has(id)) cited.set(id, node)
155
+ const problem = problemOf(row)
156
+ if (problem) flag(problem[0], node, problem[1])
157
+ }
158
+ for (const node of ctx.all.filter((cur) => cur.attrs["data-fact"] !== undefined)) {
159
+ const selector = node.attrs["data-quote-sel"] ?? "full"
160
+ check(node.attrs["data-fact"], node, (row) => (row.kind === "quote" ? quoteProblem(row, withoutFooter(node), selector) : factProblem(row, proseText(node), node.attrs)))
161
+ }
162
+ const docText = bare(proseText(ctx.body))
163
+ for (const entry of fillMap ?? []) {
164
+ check(entry.fact_id, ctx.body, (row) => {
165
+ const problem = row.kind === "quote" ? quoteProblem(row, entry.rendered, entry.selector ?? "full") : factProblem(row, entry.rendered)
166
+ if (problem || docText.includes(bare(entry.rendered))) return problem
167
+ return [row.kind === "quote" ? "quote_not_verbatim" : "value_mismatch", `filled text "${clip(entry.rendered)}" of ${row.fact_id} is no longer in the report`]
168
+ })
169
+ }
170
+ rowChecks(ctx, ledger, rows, cited, options, flag)
171
+ for (const tr of ctx.all.filter((node) => node.tag === "tr" && node.attrs["data-row-kind"] !== "comparison")) {
172
+ const groups = new Set(ctx.util.descendants(tr).map((node) => rows.get(node.attrs["data-fact"])?.observation_id).filter(Boolean))
173
+ if (groups.size > 1) flag("join_integrity_fail", tr, `one table row joins observations ${[...groups].join(", ")}`)
174
+ }
175
+ const meta = sourceChecks(ctx, rows, cited, sourcesDir, flag)
176
+ claimChecks(ctx, rows, (row) => provenance(sourcesDir, meta, row), flag)
177
+ const charts = chartFigures(ctx)
178
+ for (const { node, scope } of ctx.elements) {
179
+ const kind = ctx.kindOf(scope)
180
+ if ((node.tag === "q" || node.tag === "blockquote") && node.attrs["data-fact"] === undefined && kind !== "sources" && kind !== "toc") flag("quote_unbound", node, `<${node.tag}> with no Q row: "${clip(proseText(node))}"`)
181
+ }
182
+ traceChecks(ctx, charts, fillMap, flag, fillSiteFits(rows))
183
+ for (const [fig, labels] of charts) {
184
+ const problem = chartProblem(ctx, fig, labels, ledger)
185
+ if (problem) flag("chart_value_untraceable", fig, problem)
186
+ }
187
+ return out
188
+ }
189
+
190
+ function rowChecks(ctx, ledger, rows, cited, options, flag) {
191
+ // An as-of of "latest ..." or "... per snapshot" names no fixed revision, so quote_stale_version is skipped.
192
+ const asOf = options.asOf && !/^latest\b|per snapshot/i.test(options.asOf) ? options.asOf : null
193
+ for (const [id, node] of cited) {
194
+ const row = rows.get(id)
195
+ if (row.fuzzy === true) flag("quote_fuzzy_match", node, `${id} was registered from a similarity match on an OCR-noisy snapshot`)
196
+ const revision = typeof row.revision === "string" ? row.revision : (row.revision?.id ?? row.revision?.tag ?? null)
197
+ if (asOf && revision && !asOf.includes(revision)) flag("quote_stale_version", node, `${id} is from revision ${revision}; the brief targets "${asOf}"`)
198
+ if ((row.kind === "derived" || row.kind === "converted") && options.recompute?.(ledger, row)?.ok === false) flag("value_mismatch", node, `${id} does not recompute from its operands`)
199
+ const window = row.snapshot_window
200
+ if (window && typeof window === "object" && window.start && window.end) {
201
+ const [start, end] = [Date.parse(window.start), Date.parse(window.end)]
202
+ const outside = (row.operands ?? []).find((op) => !(Date.parse(rows.get(op)?.observed_at) >= start && Date.parse(rows.get(op)?.observed_at) < end))
203
+ if (outside) flag("snapshot_mismatch", node, `operand ${outside} of ${id} was observed outside ${window.start} to ${window.end}`)
204
+ }
205
+ }
206
+ }
207
+
208
+ /** meta.json kind of the snapshot holding the row's own locator artifact; undefined when the artifact is absent or escapes sources/<source_id>/. */
209
+ function provenance(sourcesDir, meta, row) {
210
+ const artifact = row.locator?.artifact
211
+ if (!sourcesDir || typeof row.source_id !== "string" || typeof artifact !== "string") return undefined
212
+ const dir = resolve(sourcesDir, row.source_id)
213
+ const path = resolve(dir, artifact)
214
+ return path.startsWith(`${dir}${sep}`) && existsSync(path) ? meta.get(row.source_id)?.kind : undefined
215
+ }
216
+
217
+ function sourceChecks(ctx, rows, cited, sourcesDir, flag) {
218
+ const ids = new Set([...cited.keys()].map((id) => rows.get(id)?.source_id).filter(Boolean))
219
+ for (const match of proseText(ctx.body).matchAll(/\[S(\d+)\]/g)) ids.add(`S${match[1]}`)
220
+ const meta = new Map()
221
+ for (const id of sourcesDir ? ids : []) {
222
+ const [metaPath, textPath] = [join(sourcesDir, id, "meta.json"), join(sourcesDir, id, "text.txt")]
223
+ if (!existsSync(metaPath) || !existsSync(textPath)) continue
224
+ meta.set(id, JSON.parse(readFileSync(metaPath, "utf8")))
225
+ const actual = sha256Of(readFileSync(textPath))
226
+ if (meta.get(id).sha256 !== actual) flag("snapshot_hash_mismatch", ctx.body, `${id}/text.txt changed after its snapshot was pinned`)
227
+ }
228
+ return meta
229
+ }
@@ -0,0 +1,113 @@
1
+ // gates-fidelity.mjs - the deterministic fidelity gate over a filled HTML report.
2
+ //
3
+ // checkFidelity refuses to judge a report whose brief has no locked `## Fidelity` block (one
4
+ // fidelity_interview_incomplete defect). Otherwise it builds the static-gate document context
5
+ // once and runs the value, binding, and lexicon checks over it, returning defects in the shape
6
+ // `check` emits plus integrity, sorted blocker, major, minor, then line, selector, code. It never
7
+ // calls a model or reads the network; the meaning verifier consumes its pairs separately.
8
+ // Plain Node ESM, sibling imports only.
9
+
10
+ import { existsSync, readFileSync } from "node:fs"
11
+ import { join } from "node:path"
12
+
13
+ import { DEFECT_CODES } from "./contracts.mjs"
14
+ import { bindingChecks, proseSentences } from "./fidelity-binding.mjs"
15
+ import { isLocked, parseFidelityBlock } from "./fidelity-brief.mjs"
16
+ import { lexiconChecks } from "./fidelity-lexicon.mjs"
17
+ import { valueChecks } from "./fidelity-values.mjs"
18
+ import { buildContext } from "./gates-static.mjs"
19
+ import { parseHtml } from "./html-lite.mjs"
20
+
21
+ /** Every code this gate can raise. */
22
+ export const FIDELITY_GATE_CODES = Object.freeze([
23
+ "fidelity_interview_incomplete", "unresolved_placeholder", "value_mismatch", "unit_mismatch", "quote_not_verbatim",
24
+ "quote_unbound", "claim_status_invalid", "snapshot_hash_mismatch", "chart_value_untraceable", "untraceable_number",
25
+ "unsupported_verification", "conflicting_readings", "hedge_dropped", "direction_flipped", "causality_added",
26
+ "qualifier_dropped", "unbound_sentence", "snapshot_mismatch", "join_integrity_fail", "quote_stale_version",
27
+ "quote_fuzzy_match", "name_outside_quote", "scope_widened_warn", "claim_kind_mismatch", "verification_claim_unmarked",
28
+ ])
29
+ const RANK = { blocker: 0, major: 1, minor: 2 }
30
+
31
+ /** The gate and delivery barrier share registry policy; caller severities cannot demote it. */
32
+ export function blocksDelivery(defect) {
33
+ if (!Object.hasOwn(DEFECT_CODES, defect?.code)) return true
34
+ const entry = DEFECT_CODES[defect.code]
35
+ return entry.integrity || entry.severity !== "minor"
36
+ }
37
+
38
+ export function deliveryBlockReason(defect) {
39
+ return Object.hasOwn(DEFECT_CODES, defect?.code) ? `fidelity_unresolved: ${defect.code}` : `unknown defect code ${defect?.code}`
40
+ }
41
+
42
+ function withIntegrity(defect) {
43
+ const { code, severity, selector, line, message, hint } = defect
44
+ return { code, severity, integrity: DEFECT_CODES[code].integrity, selector, line, message, hint }
45
+ }
46
+
47
+ function compareDefects(a, b) {
48
+ const text = (x, y) => (x < y ? -1 : x > y ? 1 : 0)
49
+ return RANK[a.severity] - RANK[b.severity] || (a.line ?? 0) - (b.line ?? 0) || text(a.selector ?? "", b.selector ?? "") || text(a.code, b.code)
50
+ }
51
+
52
+ function readAcronyms(sourcesDir, sourceId, cache) {
53
+ if (!sourcesDir || !sourceId) return {}
54
+ if (!cache.has(sourceId)) {
55
+ const path = join(sourcesDir, sourceId, "acronyms.json")
56
+ cache.set(sourceId, existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {})
57
+ }
58
+ return cache.get(sourceId)
59
+ }
60
+
61
+ /** One (row quote, report sentence) pair per fact cited in a prose sentence outside sources and toc. */
62
+ export function fidelityPairs(ctx, ledger, sourcesDir) {
63
+ const rows = new Map((ledger?.rows ?? []).map((row) => [row.fact_id, row]))
64
+ const cache = new Map()
65
+ const pairs = []
66
+ for (const block of proseSentences(ctx)) {
67
+ if (block.kind === "sources" || block.kind === "toc") continue
68
+ block.sentences.forEach((sentence, index) => {
69
+ for (const fact of sentence.facts) {
70
+ const row = rows.get(fact.id)
71
+ if (!row?.quote) continue
72
+ pairs.push({
73
+ pair_id: `${fact.id}@${block.owner.line}:${index + 1}`,
74
+ fact_id: fact.id,
75
+ quote: row.quote,
76
+ sentence: sentence.text,
77
+ span: [fact.start - sentence.start, fact.end - sentence.start],
78
+ qualifier: row.kind === "quote" ? null : row.qualifier,
79
+ acronyms: readAcronyms(sourcesDir, row.source_id, cache),
80
+ node: block.owner,
81
+ })
82
+ }
83
+ })
84
+ }
85
+ return pairs
86
+ }
87
+
88
+ function incomplete(parsed) {
89
+ const reasons = !parsed?.present ? "no ## Fidelity block" : [...(parsed.errors ?? []), ...parsed.missing.map((f) => `missing: ${f}`), ...parsed.pending.map((f) => `pending: ${f}`)].join("; ")
90
+ const entry = DEFECT_CODES.fidelity_interview_incomplete
91
+ return { code: "fidelity_interview_incomplete", severity: entry.severity, integrity: entry.integrity, selector: null, line: null, message: `fidelity interview incomplete (${reasons})`, hint: entry.hint }
92
+ }
93
+
94
+ /**
95
+ * @param {string} html the filled report
96
+ * @param {{ ledger: { rows: object[] }, sourcesDir?: string | null, claims?: object | null, fillMap?: object[] | null,
97
+ * brief: string | object, baseDir?: string, recompute?: (ledger: object, row: object) => { ok: boolean } }} options
98
+ * `brief` is the brief.md text or parseFidelityBlock output; `baseDir` resolves data-chart-source paths.
99
+ * @returns {{ defects: object[], gatesRun: string[] }}
100
+ */
101
+ export function checkFidelity(html, { ledger, sourcesDir = null, claims = null, fillMap = null, brief, baseDir, recompute } = {}) {
102
+ const parsed = brief && typeof brief === "object" ? brief : parseFidelityBlock(String(brief ?? ""))
103
+ if (!isLocked(parsed)) return { defects: [incomplete(parsed)], gatesRun: ["fidelity_interview_incomplete"] }
104
+ const ctx = buildContext(parseHtml(html), { baseDir })
105
+ const defects = [
106
+ ...valueChecks(ctx, ledger, fillMap, sourcesDir, { recompute, asOf: parsed.fields["as of"] }),
107
+ ...bindingChecks(ctx, claims, ledger),
108
+ ]
109
+ const pairs = fidelityPairs(ctx, ledger, sourcesDir)
110
+ const byId = new Map(pairs.map((pair) => [pair.pair_id, pair]))
111
+ for (const finding of lexiconChecks(pairs).findings) defects.push(ctx.defect(finding.code, byId.get(finding.pair_id).node, `${finding.pair_id}: ${finding.message}`))
112
+ return { defects: defects.map(withIntegrity).sort(compareDefects), gatesRun: [...FIDELITY_GATE_CODES] }
113
+ }