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
@@ -5,6 +5,11 @@
5
5
  // Also the value primitives the schema shares. Pure functions with no IO; node: builtins only.
6
6
 
7
7
  import { isCliEntry } from "./entry-guard.mjs"
8
+ import { canonicalUnit, inputScale, RuleError } from "./unit-algebra.mjs"
9
+ import { metadataErrors } from "./unit-metadata.mjs"
10
+ import { mul } from "./rational.mjs"
11
+ import { placesOf, roundPlaces, toBig } from "./unit-precision.mjs"
12
+ import { OPERATION_RULES } from "./unit-rules.mjs"
8
13
 
9
14
  export const DECIMAL = /^-?\d+(?:\.\d+)?$/
10
15
  const INTEGER = /^-?\d+$/
@@ -16,7 +21,15 @@ export const isString = (value) => typeof value === "string"
16
21
  export const isNonEmpty = (value) => typeof value === "string" && value.length > 0
17
22
  export const isCount = (value) => Number.isInteger(value) && value >= 0
18
23
  const isDecimal = (value) => isString(value) && DECIMAL.test(value)
19
- const isRational = (exact) => isObject(exact) && isString(exact.num) && isString(exact.den) && INTEGER.test(exact.num) && /^\d+$/.test(exact.den) && !/^0+$/.test(exact.den)
24
+ export function isRational(exact) {
25
+ if (!isObject(exact) || Object.keys(exact).sort().join(",") !== "den,num" || !isString(exact.num) || !isString(exact.den) || !INTEGER.test(exact.num) || !/^[1-9]\d*$/.test(exact.den)) return false
26
+ let a = BigInt(exact.num)
27
+ let b = BigInt(exact.den)
28
+ if (String(a) !== exact.num) return false
29
+ if (a < 0n) a = -a
30
+ while (b !== 0n) [a, b] = [b, a % b]
31
+ return a === 1n
32
+ }
20
33
 
21
34
  /** The exact rational of a decimal string, reduced. */
22
35
  export function exactFromDecimal(value) {
@@ -72,22 +85,27 @@ export const RULES = Object.freeze({
72
85
  test: (v, row) => isRational(v) && (!isDecimal(row.value) || (v.num === exactFromDecimal(row.value).num && v.den === exactFromDecimal(row.value).den)),
73
86
  },
74
87
  rational_or_null: { cell: "rational or null", says: "null or { num: integer string, den: positive integer string }", test: orNull(isRational) },
75
- by_derive: { cell: "written by derive (rational or null)", says: "null or { num: integer string, den: positive integer string }", test: orNull(isRational) },
88
+ by_derive: { cell: "reduced rational", says: "a reduced { num: integer string, den: positive integer string }", test: isRational },
89
+ context_by_derive: { cell: "written by derive (object or null)", says: "null or an object", test: orNull(isObject) },
90
+ derived_operation: { cell: "sum/diff/growth/share/ratio/expr", says: "a derived operation", test: (v) => Object.hasOwn(OPERATION_RULES, v) && OPERATION_RULES[v].kind === "derived" },
91
+ convert_operation: { cell: "`convert`", says: "convert", test: (v) => Object.hasOwn(OPERATION_RULES, v) && OPERATION_RULES[v].kind === "converted" },
92
+ precision: { cell: "`{ places, sigfigs }`", says: "nonnegative places and positive sigfigs", test: (v) => isObject(v) && Object.keys(v).sort().join(",") === "places,sigfigs" && Number.isSafeInteger(v.places) && v.places >= 0 && Number.isSafeInteger(v.sigfigs) && v.sigfigs >= 1 },
93
+ compatibility: { cell: "checked or unchecked (expr)", says: "the operation's compatibility", test: (v, r) => Object.hasOwn(OPERATION_RULES, r.operation) && v === OPERATION_RULES[r.operation].compatibility },
76
94
  })
77
95
 
78
96
  /** The grammar's per-kind columns, in table order; `exact` follows `value` because it reads it. */
79
- export const KIND_COLUMNS = Object.freeze(["fact_id", "value", "source_id", "locator", "snapshot_sha256", "rationale", "exact"])
97
+ export const KIND_COLUMNS = Object.freeze(["fact_id", "value", "source_id", "locator", "snapshot_sha256", "rationale", "exact", "context", "operation", "precision", "compatibility"])
80
98
 
81
99
  const sourced = { source_id: "source", locator: "locator", snapshot_sha256: "sha256" }
82
100
  const unsourced = { source_id: "source_or_null", locator: "locator_or_null", snapshot_sha256: "sha256_or_null" }
83
- const row = (cells) => Object.freeze(Object.fromEntries(KIND_COLUMNS.map((column) => [column, cells[column]])))
101
+ const row = (cells) => Object.freeze(Object.fromEntries(KIND_COLUMNS.map((column) => [column, cells[column] ?? (["precision", "compatibility"].includes(column) && cells.operation !== "null" ? column : "null")])))
84
102
 
85
103
  export const KIND_RULES = Object.freeze({
86
- measured: row({ fact_id: "f_id", value: "decimal", ...sourced, rationale: "string_or_null", exact: "from_value" }),
87
- quote: row({ fact_id: "q_id", value: "null", ...sourced, rationale: "string_or_null", exact: "null" }),
88
- derived: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "string_or_null", exact: "by_derive" }),
89
- assumed: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "non_empty", exact: "rational_or_null" }),
90
- converted: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "string_or_null", exact: "by_derive" }),
104
+ measured: row({ fact_id: "f_id", value: "decimal", ...sourced, rationale: "string_or_null", exact: "from_value", context: "null", operation: "null" }),
105
+ quote: row({ fact_id: "q_id", value: "null", ...sourced, rationale: "string_or_null", exact: "null", context: "null", operation: "null" }),
106
+ derived: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "string_or_null", exact: "by_derive", context: "context_by_derive", operation: "derived_operation" }),
107
+ assumed: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "non_empty", exact: "rational_or_null", context: "null", operation: "null" }),
108
+ converted: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "string_or_null", exact: "by_derive", context: "context_by_derive", operation: "convert_operation" }),
91
109
  })
92
110
 
93
111
  /** The id prefix nextId assigns for a kind; an unknown kind gets F and fails validation later. */
@@ -100,15 +118,101 @@ export function exactComesFromValue(kind) {
100
118
  return KIND_RULES[kind]?.exact === "from_value"
101
119
  }
102
120
 
121
+ /** Columns added after schema version 1 shipped; a ledger written before them still loads (dimension 31). */
122
+ export const LATER_COLUMNS = Object.freeze(["context", "precision", "compatibility"])
123
+ const DERIVED_KINDS = Object.freeze(["derived", "converted"])
124
+ const PRE_REGISTRY_COLUMNS = KIND_COLUMNS.slice(0, KIND_COLUMNS.indexOf("exact") + 1)
125
+
126
+ /** A derived or converted row written before the later columns existed: loadable, never an operand. */
127
+ export const isLegacyDerived = (row) => isObject(row) && DERIVED_KINDS.includes(row.kind) && LATER_COLUMNS.every((column) => !Object.hasOwn(row, column))
128
+ /** A derived or converted row carrying this version's columns: the only rows checked strictly at read time. */
129
+ export const isCurrentDerived = (row) => isObject(row) && DERIVED_KINDS.includes(row.kind) && !isLegacyDerived(row)
130
+
131
+ /** An exact carrying keys beyond num and den: never normalized, never silently stripped, flagged on use. */
132
+ export const hasUnknownExactKeys = (exact) => isObject(exact) && isString(exact.num) && isString(exact.den) && Object.keys(exact).sort().join(",") !== "den,num"
133
+ /** Any integer pair dev's writer accepted, reduced: sign on num, no leading zeros, gcd 1; anything else as given. */
134
+ function readExact(exact) {
135
+ if (!isObject(exact) || hasUnknownExactKeys(exact) || !isString(exact.num) || !isString(exact.den) || !INTEGER.test(exact.num) || !/^\d+$/.test(exact.den) || /^0+$/.test(exact.den)) return exact
136
+ const { num, den } = mul({ num: BigInt(exact.num), den: BigInt(exact.den) }, 1n)
137
+ return { num: String(num), den: String(den) }
138
+ }
139
+
140
+ const readDecimal = (value) => (isDecimal(value) ? value.replace(/^(-?)0+(?=\d)/, "$1") : value)
141
+
142
+ /** A source unit read through the alias table; unknown, scaled, or unparseable text stays as given. */
143
+ export function sourceUnit(unit) {
144
+ try {
145
+ const canonical = canonicalUnit(unit)
146
+ return isString(canonical) && !inputScale(unit) ? canonical : unit
147
+ } catch (error) {
148
+ if (error instanceof RuleError) return unit
149
+ throw error
150
+ }
151
+ }
152
+
153
+ /**
154
+ * A row as every reader sees it (dimension 31 wins at read time): rows not written by derive in this
155
+ * version get reduced exacts and decimals; source rows also get canonical units and null later columns.
156
+ * Current derived rows stay as stored so tampering is still detected. Idempotent.
157
+ */
158
+ export function readRow(row) {
159
+ if (!isObject(row) || !Object.hasOwn(KIND_RULES, row.kind) || isCurrentDerived(row)) return row
160
+ const read = { ...row, value: readDecimal(row.value), exact: exactComesFromValue(row.kind) ? row.exact : readExact(row.exact) }
161
+ if (DERIVED_KINDS.includes(row.kind)) return read
162
+ read.unit = sourceUnit(row.unit)
163
+ for (const column of LATER_COLUMNS) if (!Object.hasOwn(read, column)) read[column] = null
164
+ return read
165
+ }
166
+
167
+ const carriesDerivation = (row) => row.operation !== null || row.operands?.length || row.derived_from?.length || row.formula !== null || row.factor !== null
168
+
169
+ /** Dimension 32: a stored exact must equal the row's own value at the value's decimal places. */
170
+ function valueExactDisagreement(row) {
171
+ const exact = readExact(row.exact)
172
+ const value = readDecimal(row.value)
173
+ if (!isRational(exact) || !isDecimal(value) || roundPlaces(toBig(exact), placesOf(value)) === value) return null
174
+ return `value ${value} disagrees with exact ${exact.num}/${exact.den} at ${placesOf(value)} decimal places`
175
+ }
176
+ function unitFlaw(unit) {
177
+ try {
178
+ return isString(unit) && (canonicalUnit(unit) !== unit || inputScale(unit)) ? `unit ${unit} has no canonical stored text` : null
179
+ } catch (error) {
180
+ if (error instanceof RuleError) return `unit ${unit}: ${error.message}`
181
+ throw error
182
+ }
183
+ }
184
+
185
+ /**
186
+ * What a loadable source row (read through readRow) still breaks under the strict dimensions: dev's
187
+ * writer accepted it, so it loads, but derive and recompute refuse it as an operand with this shape.
188
+ * addRow refuses a new row that has any. Empty for derived and converted rows.
189
+ */
190
+ export function readFlaws(row) {
191
+ if (!isObject(row) || DERIVED_KINDS.includes(row.kind)) return []
192
+ const flaws = []
193
+ if (carriesDerivation(row)) flaws.push({ shape: "dimension 1", detail: "a source row carries an operation or derivation fields", expected: "null operation and empty derivation fields on measured, quote, and assumed rows" })
194
+ const unit = unitFlaw(row.unit)
195
+ if (unit) flaws.push({ shape: "dimension 6", detail: unit, expected: "a registered or canonical unit without scale" })
196
+ for (const detail of metadataErrors(row)) flaws.push({ shape: "dimension 18", detail, expected: "present, type-valid metadata; explicit null when unknown" })
197
+ const unknownKeys = hasUnknownExactKeys(row.exact)
198
+ const disagreement = unknownKeys ? null : valueExactDisagreement(row)
199
+ if (unknownKeys) flaws.push({ shape: "dimension 7", detail: `exact has unknown keys: ${Object.keys(row.exact).filter((key) => key !== "num" && key !== "den").join(", ")}`, expected: "an exact with exactly num and den (the stored row is kept as written)" })
200
+ if (disagreement) flaws.push({ shape: "dimension 32", detail: disagreement, expected: "an exact equal to value at the value's decimal places" })
201
+ return flaws
202
+ }
203
+
103
204
  /** @returns {string[]} every per-kind column error of one row; empty for an unknown kind. */
104
205
  export function kindRuleErrors(candidate, at) {
105
206
  const rules = KIND_RULES[candidate.kind]
106
207
  if (rules === undefined) return []
107
208
  const errors = []
108
- for (const column of KIND_COLUMNS) {
109
- const value = candidate[column]
110
- if (value === undefined) continue
111
- const rule = RULES[rules[column]]
209
+ const legacy = isLegacyDerived(candidate)
210
+ const columns = legacy ? PRE_REGISTRY_COLUMNS : isCurrentDerived(candidate) ? KIND_COLUMNS : KIND_COLUMNS.filter((column) => column !== "operation")
211
+ for (const column of columns) {
212
+ const value = candidate[column] === undefined && LATER_COLUMNS.includes(column) ? null : candidate[column]
213
+ const rule = RULES[legacy && column === "exact" ? "rational_or_null" : rules[column]]
214
+ // An exact with unknown keys loads as written (never stripped); readFlaws refuses it at use and at write.
215
+ if (column === "exact" && hasUnknownExactKeys(value) && !isCurrentDerived(candidate)) continue
112
216
  if (rule.test(value, candidate)) continue
113
217
  errors.push(`${at}.${column} must be ${rule.says} for a ${candidate.kind} row`)
114
218
  if (rule.detail) errors.push(...rule.detail(value, `${at}.${column}`))
@@ -116,6 +220,16 @@ export function kindRuleErrors(candidate, at) {
116
220
  return errors
117
221
  }
118
222
 
223
+ /** At-rest rules of current derived rows; source rows answer to readFlaws at use and at write instead. */
224
+ export function storedErrors(row) {
225
+ if (!isCurrentDerived(row)) return []
226
+ const errors = metadataErrors(row)
227
+ try {
228
+ if (canonicalUnit(row.unit) !== row.unit || inputScale(row.unit)) errors.push("unit must be canonical stored text")
229
+ } catch (error) { errors.push(error.message) }
230
+ return errors
231
+ }
232
+
119
233
  export const KIND_TABLE_BEGIN = "<!-- kind-rules:begin (rendered from KIND_RULES; do not edit by hand) -->"
120
234
  export const KIND_TABLE_END = "<!-- kind-rules:end -->"
121
235
 
@@ -4,7 +4,8 @@
4
4
  // KIND_RULES (fact-ledger-kinds.mjs), never here.
5
5
  // Pure functions with no IO; fact-ledger.mjs owns reading, writing, and row changes. node: builtins only.
6
6
 
7
- import { DECIMAL, isCount, isNonEmpty, isObject, isString, kindRuleErrors } from "./fact-ledger-kinds.mjs"
7
+ import { DECIMAL, isCount, isCurrentDerived, isNonEmpty, isObject, isString, kindRuleErrors, LATER_COLUMNS, readRow, storedErrors } from "./fact-ledger-kinds.mjs"
8
+ import { malformed } from "./unit-rules.mjs"
8
9
 
9
10
  export { DECIMAL, isNonEmpty, isString } from "./fact-ledger-kinds.mjs"
10
11
 
@@ -50,8 +51,13 @@ export const ROW_DEFAULTS = Object.freeze({
50
51
  formula: null,
51
52
  factor: null,
52
53
  rationale: null,
54
+ context: null,
55
+ precision: null,
56
+ compatibility: null,
53
57
  status: "active",
54
58
  })
59
+ /** Fields added after schema version 1 shipped: written on every new row, optional when reading an older ledger. */
60
+ const OPTIONAL_FIELDS = new Set(LATER_COLUMNS)
55
61
  const REQUIRED = ["fact_id", "kind", "raw", "quote", "extracted_by", "observed_at", "label"]
56
62
  export const ROW_FIELDS = Object.freeze([...REQUIRED, ...Object.keys(ROW_DEFAULTS)])
57
63
 
@@ -70,10 +76,16 @@ function nullOr(value, test) {
70
76
  function validateRow(row, index, errors) {
71
77
  const at = `rows[${index}]`
72
78
  if (!isObject(row)) return errors.push(`${at} must be an object`)
73
- for (const key of ROW_FIELDS) check(errors, Object.hasOwn(row, key), `${at}.${key} is required`)
79
+ for (const key of ROW_FIELDS) check(errors, OPTIONAL_FIELDS.has(key) || Object.hasOwn(row, key), `${at}.${key} is required`)
74
80
  for (const key of Object.keys(row)) check(errors, ROW_FIELDS.includes(key), `${at}.${key} is not a ledger field`)
75
81
  check(errors, FACT_KINDS.includes(row.kind), `${at}.kind must be one of ${FACT_KINDS.join(", ")}`)
76
- errors.push(...kindRuleErrors(row, at))
82
+ // Dimension 31 wins at read time: per-kind rules see the row as readRow normalizes it.
83
+ errors.push(...kindRuleErrors(readRow(row), at))
84
+ errors.push(...storedErrors(row).map((e) => `${at}.${e}`))
85
+ if (isCurrentDerived(row)) {
86
+ const problem = malformed(row)
87
+ if (problem) errors.push(`${at}: ${problem}`)
88
+ }
77
89
  check(errors, row.unit === undefined || nullOr(row.unit, isNonEmpty), `${at}.unit must be null or a non-empty token`)
78
90
  for (const key of ["raw", "quote"]) check(errors, row[key] === undefined || isString(row[key]), `${at}.${key} must be a string`)
79
91
  check(errors, row.extracted_by === undefined || isNonEmpty(row.extracted_by), `${at}.extracted_by must be a non-empty string`)
@@ -100,6 +112,12 @@ function validateRow(row, index, errors) {
100
112
  check(errors, row.status === undefined || FACT_STATUSES.includes(row.status), `${at}.status must be one of ${FACT_STATUSES.join(", ")}`)
101
113
  }
102
114
 
115
+ export function rowErrors(row) {
116
+ const errors = []
117
+ validateRow(row, 0, errors)
118
+ return errors
119
+ }
120
+
103
121
  // The status machine and event rule of references/fact-ledger-grammar.md. validateLedger replays
104
122
  // events through transitionError, and every writer in fact-ledger.mjs calls it before appending.
105
123
  export const TRANSITIONS = Object.freeze({ active: Object.freeze(["retracted", "superseded"]), retracted: Object.freeze([]), superseded: Object.freeze([]) })
@@ -163,20 +181,19 @@ export function provenanceErrors(rows) {
163
181
  }
164
182
  })
165
183
  const state = new Map()
166
- const visit = (id, path) => {
167
- if (state.get(id) === "done") return null
168
- if (state.get(id) === "open") return [...path.slice(path.indexOf(id)), id]
169
- state.set(id, "open")
170
- for (const next of refs.get(id)) {
171
- const cycle = next !== id && refs.has(next) ? visit(next, [...path, id]) : null
172
- if (cycle) return cycle
184
+ for (const root of refs.keys()) {
185
+ const stack = [{ id: root, finish: false }]
186
+ const path = []
187
+ while (stack.length) {
188
+ const { id, finish } = stack.pop()
189
+ if (state.get(id) === "done") continue
190
+ if (finish) { state.set(id, "done"); path.pop(); continue }
191
+ if (state.get(id) === "open") return [...errors, `${id} derives from itself through cycle ${[...path, id].join(" -> ")}`]
192
+ state.set(id, "open")
193
+ path.push(id)
194
+ stack.push({ id, finish: true })
195
+ for (const next of refs.get(id)) if (next !== id && refs.has(next)) stack.push({ id: next, finish: false })
173
196
  }
174
- state.set(id, "done")
175
- return null
176
- }
177
- for (const id of refs.keys()) {
178
- const cycle = visit(id, [])
179
- if (cycle) return [...errors, `${cycle[0]} derives from itself through ${cycle.join(" -> ")}`]
180
197
  }
181
198
  return errors
182
199
  }
@@ -7,8 +7,9 @@ import { existsSync } from "node:fs"
7
7
  import { join } from "node:path"
8
8
 
9
9
  import { CliError, readJsonFile, writeJsonAtomic } from "./cli-support.mjs"
10
- import { DECIMAL, exactComesFromValue, exactFromDecimal, idPrefix, isString } from "./fact-ledger-kinds.mjs"
10
+ import { DECIMAL, exactComesFromValue, exactFromDecimal, idPrefix, isString, readFlaws, readRow } from "./fact-ledger-kinds.mjs"
11
11
  import { LEDGER_SCHEMA_VERSION, ROW_DEFAULTS, ROW_FIELDS, transitionError, validateLedger } from "./fact-ledger-schema.mjs"
12
+ import { normalizeInput } from "./unit-algebra.mjs"
12
13
 
13
14
  export { exactFromDecimal } from "./fact-ledger-kinds.mjs"
14
15
  export { FACT_KINDS, FACT_STATUSES, LEDGER_SCHEMA_VERSION, OPERATIONS, QUALIFIERS, ROW_FIELDS, TRANSITIONS, transitionError, validateLedger } from "./fact-ledger-schema.mjs"
@@ -24,11 +25,24 @@ function assertValid(ledger) {
24
25
  if (errors.length > 0) throw new CliError(`invalid fact ledger: ${errors.join("; ")}`)
25
26
  }
26
27
 
28
+ /** The row's fields in ROW_FIELDS order, then any unknown field (which validation reports). */
29
+ function ordered(row) {
30
+ const out = Object.fromEntries(ROW_FIELDS.filter((key) => Object.hasOwn(row, key)).map((key) => [key, row[key]]))
31
+ for (const key of Object.keys(row)) if (!ROW_FIELDS.includes(key)) out[key] = row[key]
32
+ return out
33
+ }
34
+
35
+ /**
36
+ * Every row comes back as readRow normalizes it (reduced exacts and decimals; source rows with canonical
37
+ * units and null later columns) before any strict rule runs; the file changes on the next write.
38
+ */
27
39
  export function readLedger(dir) {
28
40
  const file = join(dir, LEDGER_FILENAME)
29
41
  if (!existsSync(file)) return emptyLedger()
30
42
  const ledger = readJsonFile(file)
31
43
  assertValid(ledger)
44
+ ledger.rows = ledger.rows.map((row) => ordered(readRow(row)))
45
+ assertValid(ledger)
32
46
  return ledger
33
47
  }
34
48
 
@@ -44,12 +58,12 @@ const ID_NUMBER = Object.freeze({ F: /^F(\d+)$/, Q: /^Q(\d+)$/ })
44
58
  /** The next id with the kind's prefix from KIND_RULES (F<n> for value rows, Q<n> for quotes). */
45
59
  export function nextId(ledger, kind) {
46
60
  const prefix = idPrefix(kind)
47
- let max = 0
61
+ let max = 0n
48
62
  for (const row of ledger.rows) {
49
63
  const match = ID_NUMBER[prefix].exec(row.fact_id ?? "")
50
- if (match) max = Math.max(max, Number(match[1]))
64
+ if (match && BigInt(match[1]) > max) max = BigInt(match[1])
51
65
  }
52
- return `${prefix}${max + 1}`
66
+ return `${prefix}${max + 1n}`
53
67
  }
54
68
 
55
69
  /**
@@ -60,14 +74,20 @@ export function nextId(ledger, kind) {
60
74
  */
61
75
  export function addRow(ledger, row) {
62
76
  const filled = { ...structuredClone(ROW_DEFAULTS), ...row, fact_id: row.fact_id ?? nextId(ledger, row.kind) }
77
+ if (!["derived", "converted"].includes(filled.kind)) {
78
+ const normalized = normalizeInput(filled.unit, filled.value)
79
+ if (normalized.value !== filled.value && filled.exact !== null) throw new CliError("scaled input must omit exact; it is computed after normalization")
80
+ Object.assign(filled, readRow({ ...filled, ...normalized }))
81
+ }
63
82
  if (exactComesFromValue(filled.kind) && filled.exact === null && isString(filled.value) && DECIMAL.test(filled.value)) {
64
83
  filled.exact = exactFromDecimal(filled.value)
65
84
  }
66
- const ordered = Object.fromEntries(ROW_FIELDS.filter((key) => Object.hasOwn(filled, key)).map((key) => [key, filled[key]]))
67
- for (const key of Object.keys(filled)) if (!ROW_FIELDS.includes(key)) ordered[key] = filled[key]
68
- assertValid({ ...ledger, rows: [...ledger.rows, ordered] })
69
- ledger.rows.push(ordered)
70
- return ordered
85
+ const added = ordered(filled)
86
+ const flaws = readFlaws(added)
87
+ if (flaws.length > 0) throw new CliError(`invalid fact ledger: ${flaws.map((flaw) => `${flaw.shape}: ${flaw.detail}`).join("; ")}`)
88
+ assertValid({ ...ledger, rows: [...ledger.rows, added] })
89
+ ledger.rows.push(added)
90
+ return added
71
91
  }
72
92
 
73
93
  /** Build the event from the row's current status and append it only if transitionError allows it. */
@@ -0,0 +1,231 @@
1
+ // fact-locate.mjs - deterministic fact locators over a source snapshot.
2
+ //
3
+ // Each locator returns { value, unit, raw, quote, value_span, sentence_id, qualifier, locator } or
4
+ // { error: "fact_not_located" | "fact_ambiguous", candidates }; it never guesses. `quote` is the
5
+ // whole containing sentence, cell, or line of text.txt, `value_span` is [start, end] inside
6
+ // `quote` with quote.slice(...value_span) === raw, and locator.char_start/char_end place `quote`
7
+ // in text.txt. Every call takes `sourceId` (S<n>) so sentence ids match sentences.jsonl.
8
+ // Plain Node ESM, sibling imports only; no network, no OCR.
9
+
10
+ import { proseText } from "./html-lite.mjs"
11
+ import { QUALIFIERS } from "./lexicon.mjs"
12
+ import { parseNumeral } from "./numerals.mjs"
13
+ import { indexSentences, snapshotBlocks } from "./source-snapshot.mjs"
14
+ import { findNormalized, normalizeText, tokenize } from "./text-normalize.mjs"
15
+ import { similarity } from "./text-similarity.mjs"
16
+ import { normalizeUnit } from "./units.mjs"
17
+
18
+ const FIND_WINDOW = 80
19
+ const CONTEXT = 60
20
+ const FUZZY_FLOOR = 0.95
21
+ const PHRASES = QUALIFIERS.map((phrase) => phrase.split(" "))
22
+ const SINGLE_QUALIFIERS = new Set(QUALIFIERS.filter((phrase) => !phrase.includes(" ")))
23
+
24
+ const notLocated = (candidates = []) => ({ error: "fact_not_located", candidates })
25
+ const ambiguous = (candidates) => ({ error: "fact_ambiguous", candidates })
26
+ const label = (text) => normalizeText(text).toLowerCase()
27
+ const clean = (token) => token.replace(/^[^\p{L}\p{N}%]+|[^\p{L}\p{N}%]+$/gu, "").toLowerCase()
28
+ const sentenceAt = (sentences, offset) => sentences.find((row) => row.start <= offset && offset < row.end) ?? null
29
+
30
+ function requireSource(sourceId) {
31
+ if (!/^S[1-9]\d*$/.test(String(sourceId))) throw new TypeError(`sourceId must be S<n>, got ${sourceId}`)
32
+ }
33
+
34
+ /** Every normalized occurrence of `needle`, as raw [start, end) offsets. */
35
+ function findAll(text, needle) {
36
+ const hits = []
37
+ for (let from = 0; from < text.length; ) {
38
+ const hit = findNormalized(text.slice(from), needle)
39
+ if (!hit) break
40
+ hits.push({ start: from + hit.start, end: from + hit.end })
41
+ from += hit.start + 1
42
+ }
43
+ return hits
44
+ }
45
+
46
+ function locatorFor(text, sourceId, start, end, extra = {}) {
47
+ const before = text.slice(0, start)
48
+ const pageStart = before.lastIndexOf("\f") + 1
49
+ const page = text.includes("\f") ? { page: before.split("\f").length } : {}
50
+ return { artifact: `${sourceId}/text.txt`, ...page, line: before.slice(pageStart).split("\n").length, char_start: start, char_end: end, ...extra }
51
+ }
52
+
53
+ function candidate(text, sentences, start, end) {
54
+ return { sentence_id: sentenceAt(sentences, start)?.sid ?? null, char_start: start, char_end: end, context: text.slice(Math.max(0, start - CONTEXT), end + CONTEXT) }
55
+ }
56
+
57
+ /** A QUALIFIERS token ending within the 2 tokens before the value, or the 1 token right after it. */
58
+ function qualifierAround(quote, [start, end]) {
59
+ const before = quote.slice(0, start).split(/\s+/).filter(Boolean).map(clean)
60
+ for (let skip = 0; skip < 2; skip++) {
61
+ for (const words of PHRASES) {
62
+ const stop = before.length - skip
63
+ if (stop - words.length >= 0 && before.slice(stop - words.length, stop).join(" ") === words.join(" ")) return words.join(" ")
64
+ }
65
+ }
66
+ const next = /^\s*(\S+)/.exec(quote.slice(end))
67
+ return next && SINGLE_QUALIFIERS.has(clean(next[1])) ? clean(next[1]) : null
68
+ }
69
+
70
+ /** A located quantity: `holder` is the quote's { sid, start, end, text }, `numeral` is parsed from holder.text. */
71
+ function quantityResult(text, sourceId, holder, numeral, extra = {}, unitFallback = null) {
72
+ const unit = numeral.unitRaw ? (normalizeUnit(numeral.unitRaw) ?? numeral.unitRaw) : unitFallback
73
+ return {
74
+ value: numeral.value,
75
+ unit,
76
+ raw: numeral.raw,
77
+ quote: holder.text,
78
+ value_span: [numeral.span[0], numeral.span[1]],
79
+ sentence_id: holder.sid,
80
+ qualifier: qualifierAround(holder.text, numeral.span),
81
+ locator: locatorFor(text, sourceId, holder.start, holder.end, extra),
82
+ }
83
+ }
84
+
85
+ /** --find: one anchor occurrence, then the first numeral starting within 80 chars after it in the same sentence. */
86
+ export function locateFind(text, { find, unitExpected, sourceId }) {
87
+ requireSource(sourceId)
88
+ const sentences = indexSentences(text, sourceId)
89
+ const hits = findAll(text, find)
90
+ if (hits.length === 0) return notLocated()
91
+ if (hits.length > 1) return ambiguous(hits.map((hit) => candidate(text, sentences, hit.start, hit.end)))
92
+ const [hit] = hits
93
+ const sentence = sentenceAt(sentences, hit.start)
94
+ if (!sentence || hit.end > sentence.end) return notLocated()
95
+ const from = hit.end - sentence.start
96
+ const numeral = parseNumeral(sentence.text, from)
97
+ if (!numeral || numeral.span[0] - from > FIND_WINDOW) return notLocated([candidate(text, sentences, hit.start, hit.end)])
98
+ const result = quantityResult(text, sourceId, sentence, numeral)
99
+ if (unitExpected === undefined) return result
100
+ const expected = normalizeUnit(unitExpected) ?? unitExpected
101
+ return result.unit === expected ? result : { error: "unit_expected_mismatch", expected, found: result.unit, candidates: [result] }
102
+ }
103
+
104
+ function cellsOf(row) {
105
+ const cells = []
106
+ for (const cell of row.children.filter((child) => child.tag === "td" || child.tag === "th")) {
107
+ const span = Math.max(1, Number.parseInt(cell.attrs.colspan ?? "1", 10) || 1)
108
+ for (let k = 0; k < span; k++) cells.push(cell)
109
+ }
110
+ return cells
111
+ }
112
+
113
+ function rowsOf(table) {
114
+ const rows = []
115
+ const walk = (node) => {
116
+ for (const child of node.children) {
117
+ if (child.tag === "tr") rows.push(child)
118
+ else if (child.tag !== "table" && child.tag !== "#text") walk(child)
119
+ }
120
+ }
121
+ walk(table)
122
+ return rows
123
+ }
124
+
125
+ function tablesOf(root) {
126
+ const tables = []
127
+ const walk = (node) => {
128
+ for (const child of node.children) {
129
+ if (child.tag === "table") tables.push(child)
130
+ if (child.tag !== "#text") walk(child)
131
+ }
132
+ }
133
+ walk(root)
134
+ return tables.map((table) => {
135
+ const rows = rowsOf(table)
136
+ const header = rows.find((row) => row.children.some((child) => child.tag === "th")) ?? rows[0]
137
+ const caption = table.children.find((child) => child.tag === "caption")
138
+ return { table, header, rows: rows.filter((row) => row !== header), caption: caption ? label(proseText(caption)) : null }
139
+ })
140
+ }
141
+
142
+ const cellLabel = (cell) => label(proseText(cell))
143
+ const indexesOf = (labels, wanted) => labels.flatMap((value, index) => (value === wanted ? [index] : []))
144
+
145
+ /** --table/--row/--col: the table by id, caption, or a header label; row and column by normalized label, never by index. */
146
+ export function locateTable(sourceHtml, { table, row, col, sourceId }) {
147
+ requireSource(sourceId)
148
+ const { root, text, blocks } = snapshotBlocks(sourceHtml)
149
+ const key = label(table)
150
+ const tables = tablesOf(root).filter((entry) => entry.table.attrs.id === table || entry.caption === key || (entry.header && cellsOf(entry.header).map(cellLabel).includes(key)))
151
+ if (tables.length !== 1) return tables.length === 0 ? notLocated() : ambiguous(tables.map((entry) => ({ table: entry.table.attrs.id ?? entry.caption })))
152
+ const [{ header, rows }] = tables
153
+ const headerCells = header ? cellsOf(header) : []
154
+ const columns = indexesOf(headerCells.map(cellLabel), label(col))
155
+ const matches = rows.filter((candidateRow) => cellsOf(candidateRow).length > 0 && cellLabel(cellsOf(candidateRow)[0]) === label(row))
156
+ if (columns.length > 1 || matches.length > 1) return ambiguous([...columns.map((index) => ({ col: index })), ...matches.map((match) => ({ row_line: match.line }))])
157
+ const cell = columns.length === 1 && matches.length === 1 ? cellsOf(matches[0])[columns[0]] : null
158
+ const block = cell ? blocks.find((entry) => entry.node === cell) : null
159
+ if (!block) return notLocated()
160
+ const holder = { sid: sentenceAt(indexSentences(text, sourceId), block.start)?.sid ?? null, start: block.start, end: block.end, text: text.slice(block.start, block.end) }
161
+ const numeral = parseNumeral(holder.text)
162
+ if (!numeral) return notLocated()
163
+ const headerUnit = /\(([^()]+)\)\s*$/.exec(cellLabel(headerCells[columns[0]]))
164
+ return quantityResult(text, sourceId, holder, numeral, { table, row_label: row, col_label: col }, headerUnit ? normalizeUnit(headerUnit[1]) : null)
165
+ }
166
+
167
+ /** --page/--line: exactly one quantity on that line of that page (pages split on a form feed). */
168
+ export function locatePageLine(text, { page, line, sourceId }) {
169
+ requireSource(sourceId)
170
+ const pages = text.split("\f")
171
+ if (!Number.isInteger(page) || page < 1 || page > pages.length) return notLocated()
172
+ const lines = pages[page - 1].split("\n")
173
+ if (!Number.isInteger(line) || line < 1 || line > lines.length) return notLocated()
174
+ let start = pages.slice(0, page - 1).reduce((sum, body) => sum + body.length + 1, 0)
175
+ for (const body of lines.slice(0, line - 1)) start += body.length + 1
176
+ const holder = { start, end: start + lines[line - 1].length, text: lines[line - 1] }
177
+ const numerals = []
178
+ for (let numeral = parseNumeral(holder.text); numeral; numeral = parseNumeral(holder.text, numeral.span[1])) numerals.push(numeral)
179
+ const sentences = indexSentences(text, sourceId)
180
+ if (numerals.length !== 1) return numerals.length === 0 ? notLocated() : ambiguous(numerals.map((numeral) => candidate(text, sentences, start + numeral.span[0], start + numeral.span[1])))
181
+ holder.sid = sentenceAt(sentences, start + numerals[0].span[0])?.sid ?? null
182
+ return quantityResult(text, sourceId, holder, numerals[0])
183
+ }
184
+
185
+ /** Token-aligned windows of `sentence` whose similarity to `match` reaches the fuzzy floor; best first. */
186
+ function fuzzyWindow(sentence, match) {
187
+ const target = normalizeText(match).length
188
+ const wanted = new Set(tokenize(match))
189
+ const present = new Set(tokenize(sentence.text))
190
+ if (wanted.size === 0 || [...wanted].filter((token) => present.has(token)).length / wanted.size < 0.5) return null
191
+ const bounds = [...sentence.text.matchAll(/[\p{L}\p{N}]+/gu)].map((m) => [m.index, m.index + m[0].length])
192
+ let best = null
193
+ for (let i = 0; i < bounds.length; i++) {
194
+ for (let j = i; j < bounds.length; j++) {
195
+ const window = sentence.text.slice(bounds[i][0], bounds[j][1])
196
+ if (window.length > target * 1.06 + 2) break
197
+ if (window.length < target * 0.94 - 2) continue
198
+ const score = similarity(window, match)
199
+ if (score >= FUZZY_FLOOR && (!best || score > best.score)) best = { score, start: sentence.start + bounds[i][0], end: sentence.start + bounds[j][1] }
200
+ }
201
+ }
202
+ return best
203
+ }
204
+
205
+ function quoteResult(text, sourceId, sentences, start, end, fuzzy) {
206
+ const first = sentenceAt(sentences, start)
207
+ const last = sentenceAt(sentences, end - 1) ?? first
208
+ return {
209
+ value: null,
210
+ unit: null,
211
+ raw: text.slice(start, end),
212
+ quote: text.slice(first.start, last.end),
213
+ value_span: null,
214
+ sentence_id: first.sid,
215
+ qualifier: null,
216
+ fuzzy,
217
+ locator: locatorFor(text, sourceId, first.start, last.end),
218
+ }
219
+ }
220
+
221
+ /** --match: a normalized exact match (fuzzy false), else the one window with similarity >= 0.95 (fuzzy true). */
222
+ export function locateMatch(text, { match, sourceId }) {
223
+ requireSource(sourceId)
224
+ const sentences = indexSentences(text, sourceId)
225
+ const hits = findAll(text, match)
226
+ if (hits.length > 1) return ambiguous(hits.map((hit) => candidate(text, sentences, hit.start, hit.end)))
227
+ if (hits.length === 1) return quoteResult(text, sourceId, sentences, hits[0].start, hits[0].end, false)
228
+ const windows = sentences.map((sentence) => fuzzyWindow(sentence, match)).filter(Boolean)
229
+ if (windows.length > 1) return ambiguous(windows.map((window) => candidate(text, sentences, window.start, window.end)))
230
+ return windows.length === 1 ? quoteResult(text, sourceId, sentences, windows[0].start, windows[0].end, true) : notLocated()
231
+ }