lazycodex-ai 5.1.45 → 5.1.46

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 (236) hide show
  1. package/README.de.md +0 -5
  2. package/README.es.md +0 -5
  3. package/README.fr.md +0 -5
  4. package/README.it.md +0 -5
  5. package/README.ja.md +0 -5
  6. package/README.ko.md +0 -5
  7. package/README.md +4 -5
  8. package/README.pt-br.md +0 -5
  9. package/README.ru.md +0 -6
  10. package/README.zh-cn.md +0 -5
  11. package/dist/cli/index.js +76 -156
  12. package/dist/cli-node/index.js +76 -156
  13. package/package.json +1 -1
  14. package/packages/lsp-daemon/dist/cli.js +3 -3
  15. package/packages/lsp-daemon/dist/client.js +3 -3
  16. package/packages/lsp-daemon/dist/index.js +3 -3
  17. package/packages/lsp-tools-mcp/dist/cli.js +3 -3
  18. package/packages/lsp-tools-mcp/dist/mcp.js +3 -3
  19. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  20. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +22 -12
  21. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  22. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  23. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  24. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  25. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  26. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  28. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  30. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  31. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  33. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  34. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  35. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  36. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  37. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  38. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  39. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  40. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  41. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  42. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  43. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  44. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  45. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  46. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  47. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  48. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  49. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  50. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  51. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  52. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  65. package/packages/omo-codex/plugin/package-lock.json +12 -12
  66. package/packages/omo-codex/plugin/package.json +1 -1
  67. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +8 -3
  68. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +0 -2
  69. package/packages/omo-codex/plugin/skills/ast-grep/AGENTS.md +1 -1
  70. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  71. package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +2 -2
  72. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  73. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +14 -2
  74. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +36 -11
  75. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +73 -13
  76. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
  77. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger.md +95 -0
  78. package/packages/omo-codex/plugin/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  79. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +21 -4
  80. package/packages/omo-codex/plugin/skills/ulw-research/references/report-visualization.md +2 -0
  81. package/packages/omo-codex/plugin/skills/ulw-research/references/unit-rules.md +157 -0
  82. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  83. package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +19 -5
  84. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +27 -6
  85. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
  86. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
  87. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +239 -0
  88. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  89. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  90. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  91. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +127 -13
  92. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +33 -16
  93. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
  94. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  95. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
  96. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  97. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  98. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +5 -0
  99. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  100. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  101. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  102. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +2 -1
  103. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  104. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  105. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  106. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +28 -8
  107. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +2 -2
  108. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-markup.mjs +182 -0
  109. package/packages/omo-codex/plugin/skills/ulw-research/scripts/md-lite.mjs +193 -0
  110. package/packages/omo-codex/plugin/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  111. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +6 -5
  112. package/packages/omo-codex/plugin/skills/ulw-research/scripts/placeholder.mjs +184 -0
  113. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  114. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  115. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-project.mjs +125 -0
  116. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  117. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +19 -8
  118. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +15 -0
  119. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +140 -0
  120. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  121. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  122. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  123. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  124. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  125. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  126. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  127. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  128. package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +4 -4
  129. package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +19 -0
  130. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +7 -3
  131. package/packages/omo-codex/scripts/install-dist/install-local.mjs +26 -16
  132. package/packages/shared-skills/skills/ast-grep/AGENTS.md +1 -1
  133. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  134. package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +2 -2
  135. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  136. package/packages/shared-skills/skills/ulw-research/AGENTS.md +14 -2
  137. package/packages/shared-skills/skills/ulw-research/SKILL.md +37 -12
  138. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +73 -13
  139. package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
  140. package/packages/shared-skills/skills/ulw-research/references/fact-ledger.md +95 -0
  141. package/packages/shared-skills/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  142. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +21 -4
  143. package/packages/shared-skills/skills/ulw-research/references/report-visualization.md +2 -0
  144. package/packages/shared-skills/skills/ulw-research/references/unit-rules.md +157 -0
  145. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  146. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.test.ts +193 -0
  147. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +19 -5
  148. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +13 -2
  149. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +27 -6
  150. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +3 -0
  151. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns.test.ts +183 -0
  152. package/packages/shared-skills/skills/ulw-research/scripts/delivery-barrier.test.ts +245 -0
  153. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
  154. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
  155. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +239 -0
  156. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  157. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.test.ts +375 -0
  158. package/packages/shared-skills/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  159. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  160. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.test.ts +221 -0
  161. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +9 -2
  162. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +127 -13
  163. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +18 -3
  164. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +160 -0
  165. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +33 -16
  166. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
  167. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +2 -0
  168. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  169. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.test.ts +118 -0
  170. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
  171. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.test.ts +167 -0
  172. package/packages/shared-skills/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  173. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +409 -0
  174. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-rules.test.ts +388 -0
  175. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  176. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.test.ts +81 -0
  177. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +5 -0
  178. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  179. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  180. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.test.ts +117 -0
  181. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  182. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +2 -1
  183. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  184. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.test.ts +58 -0
  185. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  186. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.test.ts +270 -0
  187. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  188. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.test.ts +73 -0
  189. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +28 -8
  190. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +2 -2
  191. package/packages/shared-skills/skills/ulw-research/scripts/html-markup.mjs +182 -0
  192. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.mjs +193 -0
  193. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.test.ts +271 -0
  194. package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  195. package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.test.ts +228 -0
  196. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +6 -5
  197. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.mjs +184 -0
  198. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.test.ts +77 -0
  199. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  200. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.test.ts +363 -0
  201. package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  202. package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.test.ts +168 -0
  203. package/packages/shared-skills/skills/ulw-research/scripts/polish-markup.test.ts +187 -0
  204. package/packages/shared-skills/skills/ulw-research/scripts/polish-project.mjs +125 -0
  205. package/packages/shared-skills/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  206. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +19 -8
  207. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +15 -0
  208. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +458 -2
  209. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +140 -0
  210. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  211. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.test.ts +113 -0
  212. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/assets/share.csv +3 -0
  213. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/gen-legacy-dev.mjs +89 -0
  214. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-basic.json +223 -0
  215. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-legacy-dev.json +859 -0
  216. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-verify.json +420 -0
  217. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-bad.html +19 -0
  218. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-good.html +39 -0
  219. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/acronyms.json +3 -0
  220. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/meta.json +9 -0
  221. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/sentences.jsonl +7 -0
  222. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/text.txt +7 -0
  223. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4.html +19 -0
  224. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/acronyms.json +1 -0
  225. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/meta.json +9 -0
  226. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/sentences.jsonl +1 -0
  227. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/text.txt +1 -0
  228. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S9.txt +1 -0
  229. package/packages/shared-skills/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  230. package/packages/shared-skills/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  231. package/packages/shared-skills/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  232. package/packages/shared-skills/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  233. package/packages/shared-skills/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  234. package/packages/shared-skills/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  235. package/packages/shared-skills/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  236. package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +4 -4
@@ -0,0 +1,140 @@
1
+ // source-commands.mjs - the `source snapshot` and `claims index` subcommands of report-tools.
2
+ // A source dir is immutable: the same id with identical text is a no-op, different text fails
3
+ // (exit 1, snapshot_exists), because ledger rows pin the old hash. `--url` fetches with plain
4
+ // fetch (30 s timeout, no browser) and records extractor tool:source-snapshot@1; `--html` and
5
+ // `--text` import a browser-lane capture and record extractor model (lower trust), or the named
6
+ // extractor for `--text --extractor NAME@VERSION`; `--command` runs an argv without a shell and
7
+ // snapshots its stdout, with the argv, cwd, exit status, and output hashes in command.json.
8
+ // node: builtins and sibling imports only.
9
+
10
+ import { spawnSync } from "node:child_process"
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs"
12
+ import { join, resolve } from "node:path"
13
+
14
+ import { indexClaims } from "./claims-index.mjs"
15
+ import { CliError, parseArgs, sha256Of, writeJsonAtomic } from "./cli-support.mjs"
16
+ import { parseFidelityBlock } from "./fidelity-brief.mjs"
17
+ import { writeSnapshot } from "./source-snapshot.mjs"
18
+
19
+ const FETCH_TIMEOUT_MS = 30_000
20
+ const TOOL_EXTRACTOR = "tool:source-snapshot@1"
21
+ const SNAPSHOT_USAGE =
22
+ "usage: source snapshot --id S<n> (--url <u> | --html <file> | --text <file> [--extractor NAME@VERSION] | --command '<argv json>' --cwd DIR) [--kind page|command|document] [--provenance primary|secondary] [--revision R] --session-dir D"
23
+
24
+ function readInput(path) {
25
+ try {
26
+ return readFileSync(path, "utf8")
27
+ } catch (error) {
28
+ throw new CliError(`cannot read ${path}: ${error.code ?? error.message}`)
29
+ }
30
+ }
31
+
32
+ /** The session dir from --session-dir or SESSION_DIR; a usage error when neither is set. */
33
+ export function sessionDirOf(flags) {
34
+ const dir = flags["session-dir"] ?? process.env.SESSION_DIR
35
+ if (!dir || dir === true) throw new CliError("--session-dir is required (or set SESSION_DIR)")
36
+ return resolve(dir)
37
+ }
38
+
39
+ /** The snapshot of one source: meta.json, text.txt, and source.html when the source was a page. */
40
+ export function snapshotOf(sessionDir, id) {
41
+ const dir = join(sessionDir, "sources", String(id))
42
+ if (!existsSync(join(dir, "meta.json"))) throw new CliError(`source ${id} has no snapshot; run source snapshot --id ${id} first`)
43
+ const html = join(dir, "source.html")
44
+ return { meta: JSON.parse(readInput(join(dir, "meta.json"))), text: readInput(join(dir, "text.txt")), html: existsSync(html) ? readInput(html) : null }
45
+ }
46
+
47
+ /** Whether the brief's source floor lists this source as pointed at (`pointed only: a, b`). */
48
+ export function pointedFlag(sessionDir, url) {
49
+ const brief = join(sessionDir, "brief.md")
50
+ const floor = existsSync(brief) ? (parseFidelityBlock(readInput(brief)).fields?.["source floor"] ?? "") : ""
51
+ const match = /^pointed only:\s*(.+)$/i.exec(floor.trim())
52
+ return match ? match[1].split(",").map((entry) => entry.trim()).includes(url) : false
53
+ }
54
+
55
+ async function fetchSource(url) {
56
+ if (!URL.canParse(url)) throw new CliError(`--url is not a URL: ${url}`)
57
+ let response
58
+ try {
59
+ response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
60
+ } catch (error) {
61
+ throw new CliError(`fetch failed for ${url}: ${error.message}`)
62
+ }
63
+ if (!response.ok) {
64
+ await response.body?.cancel()
65
+ throw new CliError(`fetch failed: HTTP ${response.status} for ${url}`)
66
+ }
67
+ const type = response.headers.get("content-type") ?? ""
68
+ if (/pdf/i.test(type)) {
69
+ await response.body?.cancel()
70
+ throw new CliError(`pdf response: ${url}; extract its text and use --text <file> --extractor NAME@VERSION`)
71
+ }
72
+ const body = await response.text()
73
+ return /html|xml/i.test(type) || /^\s*</.test(body) ? { url, html: body } : { url, text: body }
74
+ }
75
+
76
+ function runCommand(argvJson, cwd) {
77
+ let argv
78
+ try {
79
+ argv = JSON.parse(argvJson)
80
+ } catch (error) {
81
+ throw new CliError(`--command must be a JSON array of strings: ${error.message}`)
82
+ }
83
+ if (!Array.isArray(argv) || argv.length === 0 || !argv.every((part) => typeof part === "string" && part.length > 0)) {
84
+ throw new CliError("--command must be a non-empty JSON array of non-empty strings")
85
+ }
86
+ if (!cwd || cwd === true) throw new CliError("--command needs --cwd DIR")
87
+ const proc = spawnSync(argv[0], argv.slice(1), { cwd: resolve(cwd), encoding: "utf8", shell: false, maxBuffer: 64 * 1024 * 1024 })
88
+ if (proc.error) throw new CliError(`--command could not start: ${proc.error.message}`)
89
+ const receipt = { argv, cwd: resolve(cwd), exit_status: proc.status, stdout_sha256: sha256Of(proc.stdout), stderr_sha256: sha256Of(proc.stderr) }
90
+ return { path: `command:${argv.join(" ")}`, text: proc.stdout, kind: "command", receipt }
91
+ }
92
+
93
+ async function acquire(flags) {
94
+ const given = ["url", "html", "text", "command"].filter((name) => flags[name] !== undefined)
95
+ if (given.length !== 1) throw new CliError(SNAPSHOT_USAGE)
96
+ if (flags.extractor !== undefined && flags.text === undefined) throw new CliError("--extractor goes with --text only")
97
+ if (flags.url !== undefined) return { ...(await fetchSource(flags.url)), extractor: TOOL_EXTRACTOR }
98
+ if (flags.html !== undefined) return { path: resolve(flags.html), html: readInput(flags.html), extractor: "model" }
99
+ if (flags.text !== undefined) return { path: resolve(flags.text), text: readInput(flags.text), extractor: flags.extractor === undefined ? "model" : `extractor:${flags.extractor}` }
100
+ return { ...runCommand(flags.command, flags.cwd), extractor: TOOL_EXTRACTOR }
101
+ }
102
+
103
+ export async function runSource(argv) {
104
+ const [sub, ...rest] = argv
105
+ if (sub !== "snapshot") throw new CliError(SNAPSHOT_USAGE)
106
+ const args = parseArgs(rest)
107
+ const id = args.required("id")
108
+ const sourcesDir = join(sessionDirOf(args.flags), "sources")
109
+ const { receipt, ...input } = await acquire(args.flags)
110
+ if (args.flags.kind !== undefined) input.kind = args.flags.kind
111
+ if (args.flags.provenance !== undefined) input.provenance = args.flags.provenance
112
+ if (args.flags.revision !== undefined) input.revision = args.flags.revision
113
+ let written
114
+ try {
115
+ written = writeSnapshot(sourcesDir, id, input)
116
+ } catch (error) {
117
+ if (String(error.message).startsWith("snapshot_exists")) {
118
+ return { json: { command: "source snapshot", status: "fail", exitCode: 1, error: "snapshot_exists", message: error.message }, summary: `source snapshot: ${error.message}`, exitCode: 1 }
119
+ }
120
+ if (error instanceof TypeError) throw new CliError(error.message)
121
+ throw error
122
+ }
123
+ if (receipt) writeFileSync(join(written.dir, "command.json"), `${JSON.stringify(receipt, null, 2)}\n`)
124
+ const meta = JSON.parse(readFileSync(join(written.dir, "meta.json"), "utf8"))
125
+ const data = { source_id: id, directory: written.dir, snapshot_sha256: written.sha256, extractor: meta.extractor, kind: meta.kind, provenance: meta.provenance }
126
+ return { json: { command: "source snapshot", status: "pass", exitCode: 0, data }, summary: `source snapshot: ${id} ${written.sha256.slice(0, 12)} (${meta.extractor})`, exitCode: 0 }
127
+ }
128
+
129
+ export function runClaims(argv) {
130
+ const [sub, ...rest] = argv
131
+ const args = parseArgs(rest)
132
+ const [graph] = args.positionals
133
+ if (sub !== "index" || !graph) throw new CliError("usage: claims index <claim-graph.md> --out claims.json")
134
+ const out = args.required("out")
135
+ const index = indexClaims(readInput(graph))
136
+ if (index.errors.length > 0) throw new CliError(`claims index ${graph}: ${index.errors.join("; ")}`)
137
+ writeJsonAtomic(out, { schemaVersion: 1, graph: resolve(graph), graph_sha256: sha256Of(readFileSync(graph)), ...index })
138
+ const verified = index.claims.filter((claim) => claim.in_verified).length
139
+ return { json: { command: "claims index", out: resolve(out), claims: index.claims.length, verified }, summary: `claims index: ${index.claims.length} claim(s), ${verified} verified`, exitCode: 0 }
140
+ }
@@ -0,0 +1,176 @@
1
+ // source-snapshot.mjs - the per-source snapshot that fact locators read.
2
+ //
3
+ // snapshotFromHtml turns a page into text.txt form: one line per block (prose blocks via
4
+ // proseText, one table cell per line; head, scripts, styles and code are dropped). writeSnapshot
5
+ // writes <sourcesDir>/<S-id>/ with text.txt (pages joined by a form feed), source.html when html
6
+ // was given, meta.json, sentences.jsonl (sid S<n>:s<k>) and acronyms.json. The sha256 covers
7
+ // text.txt only. Nothing here touches the network: the caller supplies the html or text.
8
+ // Plain Node ESM, node: builtins and sibling imports only.
9
+
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
11
+ import { join } from "node:path"
12
+
13
+ import { sha256Of } from "./cli-support.mjs"
14
+ import { parseHtml, PROSE_TAGS, proseText } from "./html-lite.mjs"
15
+ import { splitSentences } from "./sentence-split.mjs"
16
+
17
+ const SKIP = new Set(["head", "script", "style", "svg", "code", "pre", "kbd", "samp", "template", "noscript"])
18
+ const INLINE = new Set([
19
+ "a", "abbr", "b", "bdi", "bdo", "cite", "data", "del", "dfn", "em", "font", "i", "ins", "label", "mark", "q", "s",
20
+ "small", "span", "strong", "sub", "sup", "time", "u", "var",
21
+ ])
22
+ const SOURCE_ID = /^S[1-9]\d*$/
23
+ const EXTRACTOR = /^(?:tool:source-snapshot@1|model|extractor:[\w.@+-]+)$/
24
+ const KINDS = new Set(["page", "command", "document"])
25
+ const PROVENANCE = new Set(["primary", "secondary", "unknown"])
26
+ const ACRONYM_AFTER = /\(([A-Z]{2,6})\)/g
27
+ const ACRONYM_BEFORE = /(?<![\p{L}\p{N}])([A-Z]{2,6})\s?\(([A-Z][^()\n]{2,79})\)/gu
28
+ const NAME_WORD = /^[A-Z][\p{L}\p{N}-]*$/u
29
+ const CONNECTOR = new Set(["of", "and", "for", "the", "in", "on", "to", "&"])
30
+
31
+ /**
32
+ * Block lines of a page: { root, text, blocks: [{ node, start, end }] } where text joins the block
33
+ * texts with "\n" and text.slice(start, end) is one block. fact-locate reads cells through `node`.
34
+ */
35
+ export function snapshotBlocks(html) {
36
+ const root = parseHtml(html)
37
+ const lines = []
38
+ let buffer = ""
39
+ const flush = (node) => {
40
+ const line = buffer.replace(/\s+/g, " ").trim()
41
+ buffer = ""
42
+ if (line) lines.push({ node, text: line })
43
+ }
44
+ const walk = (node) => {
45
+ for (const child of node.children) {
46
+ if (child.tag === "#text") buffer += child.text
47
+ else if (SKIP.has(child.tag)) continue
48
+ else if (child.tag === "br") flush(node)
49
+ else if (INLINE.has(child.tag)) walk(child)
50
+ else if (PROSE_TAGS.has(child.tag)) {
51
+ flush(node)
52
+ const line = proseText(child)
53
+ if (line) lines.push({ node: child, text: line })
54
+ } else {
55
+ flush(node)
56
+ walk(child)
57
+ flush(child)
58
+ }
59
+ }
60
+ }
61
+ walk(root)
62
+ flush(root)
63
+ const blocks = []
64
+ let at = 0
65
+ for (const line of lines) {
66
+ blocks.push({ node: line.node, start: at, end: at + line.text.length })
67
+ at += line.text.length + 1
68
+ }
69
+ return { root, text: lines.map((line) => line.text).join("\n"), blocks }
70
+ }
71
+
72
+ /** text.txt form of an html page. */
73
+ export function snapshotFromHtml(html) {
74
+ return snapshotBlocks(html).text
75
+ }
76
+
77
+ /** Sentence index rows { sid: "S<n>:s<k>", start, end, text }; a form feed (page break) always ends a sentence. */
78
+ export function indexSentences(text, sourceId) {
79
+ return splitSentences(text.replace(/\f/g, "\n")).map((row) => ({
80
+ sid: `${sourceId}:${row.sid}`,
81
+ start: row.start,
82
+ end: row.end,
83
+ text: text.slice(row.start, row.end),
84
+ }))
85
+ }
86
+
87
+ const sentenceIdAt = (sentences, offset) => sentences.find((row) => row.start <= offset && offset < row.end)?.sid ?? null
88
+
89
+ /** "High Bandwidth Memory (HBM)": the capitalized words before the parenthesis, at most one per letter. */
90
+ function nameBefore(text, at, letters) {
91
+ const prefix = text.slice(Math.max(0, at - 160), at)
92
+ if (!/\S[ \t]?$/.test(prefix)) return null
93
+ const words = [...prefix.matchAll(/\S+/g)]
94
+ const picked = []
95
+ let content = 0
96
+ for (let k = words.length - 1; k >= 0 && content < letters; k--) {
97
+ const word = words[k][0]
98
+ if (NAME_WORD.test(word)) content++
99
+ else if (!CONNECTOR.has(word) || picked.length === 0) break
100
+ picked.unshift(words[k])
101
+ }
102
+ while (picked.length > 0 && CONNECTOR.has(picked[0][0])) picked.shift()
103
+ if (picked.length === 0) return null
104
+ const start = at - prefix.length + picked[0].index
105
+ const end = at - prefix.length + picked[picked.length - 1].index + picked[picked.length - 1][0].length
106
+ return { start, expansion: text.slice(start, end) }
107
+ }
108
+
109
+ /** Every `Full Name (ACR)` and `ACR (Full Name)` in the text, ordered by position. */
110
+ export function findAcronyms(text, sentences) {
111
+ const entries = []
112
+ for (const match of text.matchAll(ACRONYM_AFTER)) {
113
+ const name = nameBefore(text, match.index, match[1].length)
114
+ if (!name || name.expansion === match[1]) continue
115
+ entries.push({ acronym: match[1], expansion: name.expansion, span: [name.start, match.index + match[0].length] })
116
+ }
117
+ for (const match of text.matchAll(ACRONYM_BEFORE)) {
118
+ const expansion = match[2].trim()
119
+ if (!/\s/.test(expansion) || expansion === expansion.toUpperCase()) continue
120
+ entries.push({ acronym: match[1], expansion, span: [match.index, match.index + match[0].length] })
121
+ }
122
+ return entries
123
+ .sort((a, b) => a.span[0] - b.span[0])
124
+ .map((entry) => ({ acronym: entry.acronym, expansion: entry.expansion, sentence_id: sentenceIdAt(sentences, entry.span[0]), span: entry.span }))
125
+ }
126
+
127
+ function snapshotText(input) {
128
+ if (Array.isArray(input.pages)) return input.pages.join("\f")
129
+ if (typeof input.text === "string") return input.text
130
+ if (typeof input.html === "string") return snapshotFromHtml(input.html)
131
+ throw new TypeError("writeSnapshot needs text, pages, or html")
132
+ }
133
+
134
+ function checkInput(sourceId, input) {
135
+ if (!SOURCE_ID.test(String(sourceId))) throw new TypeError(`source id must be S<n>, got ${sourceId}`)
136
+ if (!EXTRACTOR.test(String(input.extractor))) throw new TypeError(`extractor must be tool:source-snapshot@1, model, or extractor:<name>, got ${input.extractor}`)
137
+ if (typeof (input.url ?? input.path) !== "string") throw new TypeError("writeSnapshot needs a url or a path")
138
+ if (input.kind !== undefined && !KINDS.has(input.kind)) throw new TypeError(`kind must be page, command, or document, got ${input.kind}`)
139
+ if (input.provenance !== undefined && !PROVENANCE.has(input.provenance)) throw new TypeError(`provenance must be primary, secondary, or unknown, got ${input.provenance}`)
140
+ }
141
+
142
+ /**
143
+ * Write <sourcesDir>/<sourceId>/ for one source; returns { dir, sha256 } with sha256 over text.txt.
144
+ * input: { url | path, text | pages | html, extractor, observed_at?, revision?, kind?, provenance? }.
145
+ * Re-writing the same text is a no-op change; a different text over an existing snapshot throws
146
+ * snapshot_exists, because ledger rows pin the old hash.
147
+ */
148
+ export function writeSnapshot(sourcesDir, sourceId, input) {
149
+ checkInput(sourceId, input)
150
+ const text = snapshotText(input)
151
+ const sha256 = sha256Of(Buffer.from(text, "utf8"))
152
+ const dir = join(sourcesDir, sourceId)
153
+ const textPath = join(dir, "text.txt")
154
+ if (existsSync(textPath) && sha256Of(readFileSync(textPath)) !== sha256) {
155
+ throw new Error(`snapshot_exists: ${sourceId} already holds a different text.txt`)
156
+ }
157
+ mkdirSync(dir, { recursive: true })
158
+ writeFileSync(textPath, text)
159
+ if (typeof input.html === "string") writeFileSync(join(dir, "source.html"), input.html)
160
+ const meta = {
161
+ source_id: sourceId,
162
+ url: input.url ?? input.path,
163
+ observed_at: input.observed_at ?? new Date().toISOString(),
164
+ sha256,
165
+ extractor: input.extractor,
166
+ revision: input.revision ?? null,
167
+ kind: input.kind ?? (input.url !== undefined || typeof input.html === "string" ? "page" : "document"),
168
+ provenance: input.provenance ?? "unknown",
169
+ }
170
+ writeFileSync(join(dir, "meta.json"), `${JSON.stringify(meta, null, 2)}\n`)
171
+ const sentences = indexSentences(text, sourceId)
172
+ writeFileSync(join(dir, "sentences.jsonl"), sentences.map((row) => `${JSON.stringify(row)}\n`).join(""))
173
+ const acronyms = { schemaVersion: 1, source_id: sourceId, snapshot_sha256: sha256, entries: findAcronyms(text, sentences) }
174
+ writeFileSync(join(dir, "acronyms.json"), `${JSON.stringify(acronyms, null, 2)}\n`)
175
+ return { dir, sha256 }
176
+ }
@@ -0,0 +1,101 @@
1
+ // Canonical factor algebra. Exponents are bigint; aliases enter only through normalizeInput.
2
+ import { fromDecimal, mul, sigFigsOf, toDecimal } from "./rational.mjs"
3
+ import { dimensionOf, normalizeUnit } from "./units.mjs"
4
+ import { refuse } from "./unit-errors.mjs"
5
+ export { refuse, RuleError } from "./unit-errors.mjs"
6
+
7
+ const SCALED = new Set(["bp", "basispoint", "basispoints"])
8
+ export const isPercentish = (unit) => unit === "%" || unit === "%p"
9
+ const scaled = (text) => SCALED.has(text.replace(/\s+/g, "").toLowerCase())
10
+
11
+ function atomOf(text) {
12
+ if (text === "1") return null
13
+ const atom = scaled(text) ? "%p" : normalizeUnit(text) ?? text
14
+ if (dimensionOf(atom) === "scale") refuse(`"${text}" is a scale word (${atom}), not a unit`, undefined, "scale_atom", "a counted unit with scale in the numeral")
15
+ return atom
16
+ }
17
+
18
+ const combine = (a, b, sign) => {
19
+ for (const [atom, power] of b) a.set(atom, (a.get(atom) ?? 0n) + sign * power)
20
+ return a
21
+ }
22
+
23
+ /** Iterative unit parser: factor maps plus the input's decimal scaling exponent. */
24
+ function parse(raw) {
25
+ if (raw === null) return { factors: new Map(), shift: 0n }
26
+ if (typeof raw !== "string" || !raw.trim()) refuse("unit must be text or explicit null", undefined, "unit_text", "canonical unit text or null")
27
+ const tokens = raw.match(/[*/()^]|[^*/()^]+/g).map((x) => x.trim()).filter(Boolean)
28
+ const frames = [{ factors: new Map(), shift: 0n, sign: 1n, need: true }]
29
+ let affine = false
30
+ const apply = (value, i) => {
31
+ if (tokens[i] === "^") {
32
+ const power = tokens[i + 1]
33
+ if (!/^[+-]?\d+$/.test(power ?? "")) refuse(`unit "${raw}" has a bad exponent`, undefined, "exponent", "an integer exponent")
34
+ const n = BigInt(power)
35
+ value = { factors: new Map([...value.factors].map(([a, p]) => [a, p * n])), shift: value.shift * n }
36
+ i += 2
37
+ }
38
+ const frame = frames.at(-1)
39
+ combine(frame.factors, value.factors, frame.sign)
40
+ frame.shift += frame.sign * value.shift
41
+ frame.need = false
42
+ return i
43
+ }
44
+ for (let i = 0; i < tokens.length;) {
45
+ const frame = frames.at(-1)
46
+ const token = tokens[i]
47
+ if (frame.need) {
48
+ if (token === "(") {
49
+ frames.push({ factors: new Map(), shift: 0n, sign: 1n, need: true })
50
+ i++
51
+ } else {
52
+ if (/^[*/()^]$/.test(token)) refuse(`unexpected "${token}" in unit "${raw}"`)
53
+ const atom = atomOf(token)
54
+ affine ||= dimensionOf(atom) === "temperature"
55
+ i = apply({ factors: new Map(atom === null ? [] : [[atom, 1n]]), shift: scaled(token) ? -2n : 0n }, i + 1)
56
+ }
57
+ } else if (token === ")" && frames.length > 1) {
58
+ const value = frames.pop()
59
+ i = apply(value, i + 1)
60
+ } else if (token === "*" || token === "/") {
61
+ frame.sign = token === "*" ? 1n : -1n
62
+ frame.need = true
63
+ i++
64
+ } else refuse(`unexpected "${token}" in unit "${raw}"`)
65
+ }
66
+ if (frames.length !== 1 || frames[0].need) refuse(`unit "${raw}" ends early or is missing a closing parenthesis`)
67
+ // Refuse affine composites before cancellation loses the atom that made them unsafe.
68
+ if (affine && (tokens.length !== 1 || frames[0].shift !== 0n)) refuse("affine (temperature) units cannot occur in composites", undefined, "affine_units", "a non-affine composite or bare temperature for convert")
69
+ return frames[0]
70
+ }
71
+
72
+ function format(factors) {
73
+ const side = (sign) => [...factors].filter(([, p]) => sign * p > 0n).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
74
+ .map(([a, p]) => { const n = sign * p; return n === 1n ? a : `${a}^${n}` })
75
+ const top = side(1n)
76
+ const bottom = side(-1n)
77
+ if (!top.length && !bottom.length) return null
78
+ return `${top.join("*") || "1"}${bottom.length ? `/${bottom.length === 1 ? bottom[0] : `(${bottom.join("*")})`}` : ""}`
79
+ }
80
+
81
+ export const canonicalUnit = (raw) => format(parse(raw).factors)
82
+ export function inputScale(raw) {
83
+ const { shift } = parse(raw)
84
+ if (shift === 0n) return null
85
+ if (shift > 10000n || shift < -10000n) refuse("scaled alias exponent exceeds exact input capacity", undefined, "scale_range", "an absolute decimal scale exponent <= 10000")
86
+ return shift > 0n ? `1${"0".repeat(Number(shift))}` : `0.${"0".repeat(Number(-shift - 1n))}1`
87
+ }
88
+
89
+ export function normalizeInput(unit, value) {
90
+ const canonical = canonicalUnit(unit)
91
+ const factor = inputScale(unit)
92
+ return { unit: canonical, value: factor ? toDecimal(mul(fromDecimal(value), fromDecimal(factor)), sigFigsOf(value)) : value }
93
+ }
94
+
95
+ export const unitTimes = (a, b) => format(combine(parse(a).factors, parse(b).factors, 1n))
96
+ export const unitOver = (a, b) => format(combine(parse(a).factors, parse(b).factors, -1n))
97
+ export const isAffine = (unit) => typeof unit === "string" && dimensionOf(unit) === "temperature"
98
+ export function isCurrencyRate(unit) {
99
+ const currencies = [...parse(unit).factors].filter(([a, n]) => n !== 0n && dimensionOf(a)?.startsWith("currency:"))
100
+ return currencies.some(([, n]) => n > 0n) && currencies.some(([, n]) => n < 0n)
101
+ }
@@ -0,0 +1,23 @@
1
+ // Classified refusals shared by public derivation boundaries and the registry.
2
+ export class RuleError extends Error {
3
+ constructor(detail, hint, shape = "invalid_input", expected = "a valid ledger and derivation specification") {
4
+ super(detail)
5
+ this.detail = detail
6
+ this.shape = shape
7
+ this.expected = expected
8
+ if (hint) this.hint = hint
9
+ }
10
+ }
11
+
12
+ export const refuse = (detail, hint, shape, expected) => {
13
+ throw new RuleError(detail, hint, shape, expected)
14
+ }
15
+
16
+ export function refusal(error) {
17
+ return {
18
+ shape: error instanceof RuleError ? error.shape : "invalid_input",
19
+ expected: error instanceof RuleError ? error.expected : "valid JSON fields matching the fact ledger grammar",
20
+ detail: error instanceof Error ? error.message : "invalid input",
21
+ ...(error instanceof RuleError && error.hint ? { hint: error.hint } : {}),
22
+ }
23
+ }
@@ -0,0 +1,31 @@
1
+ // Metadata contracts and order-independent JSON equality, shared by schema and algebra.
2
+ import { isDeepStrictEqual } from "node:util"
3
+ import { refuse } from "./unit-errors.mjs"
4
+
5
+ export const METADATA = ["population", "accounting_basis", "snapshot_window", "entity", "valid_at"]
6
+ export const sameDeclared = isDeepStrictEqual
7
+ export const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0
8
+ export const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
9
+
10
+ export function metadataErrors(row) {
11
+ return METADATA.filter((field) => {
12
+ const value = row[field]
13
+ return value !== null && !(nonEmpty(value) || (["population", "snapshot_window"].includes(field) && object(value)))
14
+ }).map((field) => `${field} must be explicitly null or valid non-empty metadata`)
15
+ }
16
+
17
+ export function checkMetadata(rows, fields) {
18
+ for (const row of rows) {
19
+ const errors = metadataErrors(row)
20
+ if (errors.length) refuse(`${row.fact_id}: ${errors.join("; ")}`, undefined, "metadata", "present, type-valid metadata; explicit null when unknown")
21
+ }
22
+ for (const field of fields) {
23
+ if (rows.some((row) => !sameDeclared(row[field], rows[0][field]))) {
24
+ refuse(`${field} differs across ${rows.map((row) => row.fact_id).join(", ")}`, undefined, "metadata", `equal ${field}`)
25
+ }
26
+ }
27
+ }
28
+
29
+ export function propagatedMetadata(rows) {
30
+ return Object.fromEntries(METADATA.map((field) => [field, rows.every((row) => sameDeclared(row[field], rows[0][field])) ? structuredClone(rows[0][field]) : null]))
31
+ }
@@ -0,0 +1,72 @@
1
+ // unit-pairs.mjs - the typed operand-unit rules shared by the operation registry and expr nodes (references/unit-rules.md).
2
+ // equalUnits and notAffine guard whole operations; minusUnit and plusUnit are the typed-pair rules that diff and sum
3
+ // use and that expr's `-` and `+` nodes follow; inferExpr assigns a canonical unit to every node of an expr tree.
4
+
5
+ import { isAffine, isPercentish, refuse, unitOver, unitTimes } from "./unit-algebra.mjs"
6
+ import { postorder } from "./fact-expr.mjs"
7
+ import { checkMetadata, propagatedMetadata } from "./unit-metadata.mjs"
8
+
9
+ export const show = (unit) => JSON.stringify(unit ?? null)
10
+ export const listed = (rows, units) => rows.map((row, index) => `${row.fact_id} ${units[index] ?? "null"}`).join(", ")
11
+
12
+ export function equalUnits(rows, units) {
13
+ if (units.some((unit) => unit !== units[0])) refuse(`unit differs across ${listed(rows, units)}`)
14
+ return units[0]
15
+ }
16
+
17
+ export function notAffine(op, rows, units) {
18
+ if (units.some(isAffine)) refuse(`${op} refuses affine (temperature) units: ${listed(rows, units)}`)
19
+ }
20
+
21
+ /** The typed-pair rule shared by diff and expr `-`: U-U is U; %-% is %p; %-%p is %; %p-%p is %p; %p-% is refused. */
22
+ export function minusUnit(a, b, where) {
23
+ if (isAffine(a) || isAffine(b)) refuse(`${where}: subtracting affine (temperature) values as absolute values has no meaning`)
24
+ if (a === b) return a === "%" ? "%p" : a
25
+ if (a === "%" && b === "%p") return "%"
26
+ refuse(`${where}: ${show(a)} - ${show(b)} is not a typed pair (U-U, %-%, %-%p, %p-%p)`)
27
+ }
28
+
29
+ export function plusUnit(a, b, where) {
30
+ if (isAffine(a) || isAffine(b)) refuse(`${where}: adding affine (temperature) values has no meaning`)
31
+ if (a === b && a === "%") refuse(`${where}: adding % values needs disjoint shares of one reference: use --op sum`)
32
+ if (a === b) return a
33
+ if ((a === "%" && b === "%p") || (a === "%p" && b === "%")) return "%"
34
+ refuse(`${where}: ${show(a)} + ${show(b)} has no common unit`)
35
+ }
36
+
37
+ /** Unit of every node of an expr tree; refuses when a node's unit cannot be inferred. */
38
+ export function inferExpr(ast, unitsById, rowsById = null) {
39
+ const units = new Map()
40
+ const metadata = new Map()
41
+ for (const node of postorder(ast)) {
42
+ let unit
43
+ if (node.id) unit = unitsById.get(node.id)
44
+ else if (node.literal) unit = null
45
+ else if (node.fn === "pct") {
46
+ unit = units.get(node.arg)
47
+ if (unit !== null) refuse(`pct() takes a dimensionless value, got ${show(unit)}`)
48
+ unit = "%"
49
+ } else {
50
+ const left = units.get(node.left)
51
+ const right = units.get(node.right)
52
+ if (node.op === "+") unit = plusUnit(left, right, "expr")
53
+ else if (node.op === "-") unit = minusUnit(left, right, "expr")
54
+ else {
55
+ if (isAffine(left) || isAffine(right)) refuse(`expr: ${node.op} on affine (temperature) units has no meaning`)
56
+ const fraction = (value) => (isPercentish(value) ? null : value)
57
+ unit = (node.op === "*" ? unitTimes : unitOver)(fraction(left), fraction(right))
58
+ }
59
+ }
60
+ units.set(node, unit)
61
+ if (rowsById) {
62
+ const children = node.id ? [rowsById.get(node.id)] : node.literal ? [] : node.fn ? [metadata.get(node.arg)] : [metadata.get(node.left), metadata.get(node.right)]
63
+ const rows = children.filter(Boolean)
64
+ if (rows.length) {
65
+ const fields = node.op === "+" || node.op === "-" ? ["population", "snapshot_window", "accounting_basis"] : node.op === "/" ? ["population", "snapshot_window"] : []
66
+ checkMetadata(rows, fields)
67
+ metadata.set(node, { fact_id: "expression", ...propagatedMetadata(rows) })
68
+ }
69
+ }
70
+ }
71
+ return { unit: units.get(ast), units, metadata: metadata.get(ast) }
72
+ }
@@ -0,0 +1,44 @@
1
+ // Precision is stored independently from display strings; generated fields are rederived on replay.
2
+ import { sigFigsOf, toDecimal } from "./rational.mjs"
3
+ import { refuse } from "./unit-errors.mjs"
4
+
5
+ export const toBig = (r) => ({ num: BigInt(r.num), den: BigInt(r.den) })
6
+ export const toText = (r) => ({ num: String(r.num), den: String(r.den) })
7
+ export const placesOf = (decimal) => (String(decimal).split(".")[1] ?? "").length
8
+ export function terminates(r) {
9
+ let den = r.den
10
+ for (const prime of [2n, 5n]) while (den % prime === 0n) den /= prime
11
+ return den === 1n
12
+ }
13
+ export function precisionOf(row) {
14
+ if (row.precision !== null && row.precision !== undefined) {
15
+ const p = row.precision
16
+ if (!p || typeof p !== "object" || Object.keys(p).sort().join(",") !== "places,sigfigs" || !Number.isSafeInteger(p.places) || p.places < 0 || !Number.isSafeInteger(p.sigfigs) || p.sigfigs < 1) refuse("invalid stored precision", undefined, "precision_field", "{ places: nonnegative integer, sigfigs: positive integer }")
17
+ return p
18
+ }
19
+ return { places: placesOf(row.value), sigfigs: sigFigsOf(row.value) }
20
+ }
21
+ export function resultPrecision(rows, literals, exact, mode) {
22
+ const inputs = [...rows.map(precisionOf), ...literals.map((value) => precisionOf({ value }))]
23
+ const places = Math.min(...inputs.map((p) => p.places))
24
+ const sigfigs = Math.min(...inputs.map((p) => p.sigfigs))
25
+ if (mode === "places") return { places, sigfigs: sigFigsOf(roundPlaces(exact, places)) }
26
+ const rendered = toDecimal(exact, sigfigs)
27
+ return { places: placesOf(rendered), sigfigs }
28
+ }
29
+ export function roundPlaces(r, places) {
30
+ const digits = ((r.num < 0n ? -r.num : r.num) * 10n ** BigInt(places) * 2n + r.den) / (r.den * 2n)
31
+ const text = digits.toString().padStart(places + 1, "0")
32
+ return (r.num < 0n && digits !== 0n ? "-" : "") + (places ? `${text.slice(0, -places)}.${text.slice(-places)}` : text)
33
+ }
34
+ export function renderValue(exact, precision, mode) {
35
+ const plain = terminates(exact) ? toDecimal(exact) : null
36
+ const n = precision[mode]
37
+ if (plain !== null && (mode === "places" ? placesOf(plain) <= n : sigFigsOf(plain) < n)) return plain
38
+ return mode === "places" ? roundPlaces(exact, n) : toDecimal(exact, n)
39
+ }
40
+ export function display(value, unit) {
41
+ const [whole, fraction] = value.split(".")
42
+ const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (fraction === undefined ? "" : `.${fraction}`)
43
+ return unit === null ? grouped : unit === "%" ? `${grouped}%` : `${grouped} ${unit}`
44
+ }
@@ -0,0 +1,64 @@
1
+ // Structural rules used by the operation table, shared by schema and recomputation.
2
+ import { leaves, parseExpr } from "./fact-expr.mjs"
3
+ import { isRational } from "./fact-ledger-kinds.mjs"
4
+ import { canonicalUnit, RuleError } from "./unit-algebra.mjs"
5
+ import { nonEmpty, object } from "./unit-metadata.mjs"
6
+ import { show } from "./unit-pairs.mjs"
7
+
8
+ export const COMMON = ["operands null", "operands empty", "wrong arity", "missing operand id", "invalid rational", "no operation", "repeated operand", "self reference",
9
+ "kind operation", "audit fields", "precision field", "compatibility field", "metadata fields", "noncanonical operand"]
10
+ export const shapes = (...own) => Object.freeze([...COMMON, ...own])
11
+ export const needsContext = (row) => object(row.context) ? null : `${row.operation} rows carry a context object, got ${show(row.context)}`
12
+ export const noContext = (row) => row.context === null ? null : `${row.operation} rows carry no context, got ${show(row.context)}`
13
+ export const repeatedOperand = (ids) => new Set(ids).size !== ids.length ? `operand ${ids.find((id, i) => ids.indexOf(id) !== i)} is repeated: the operands of one row are distinct facts` : null
14
+
15
+ export function sumShape(row) {
16
+ const missing = needsContext(row)
17
+ if (missing) return missing
18
+ const c = row.context
19
+ if (Object.keys(c).sort().join(",") !== "disjoint,reference" || typeof c.disjoint !== "boolean" || !(c.reference === null || nonEmpty(c.reference) || object(c.reference))) return "sum context needs exactly boolean disjoint and null, text, or map reference"
20
+ if (object(c.reference) && (Object.keys(c.reference).sort().join(",") !== [...row.operands].sort().join(",") || !Object.values(c.reference).every(nonEmpty))) return "reference map must name exactly the supplied operands"
21
+ return null
22
+ }
23
+
24
+ export function convertShape(row) {
25
+ const target = object(row.context) ? row.context.target : undefined
26
+ if (!nonEmpty(target)) return needsContext(row) ?? "a convert row carries its target in context.target"
27
+ if (Object.keys(row.context).join(",") !== "target") return "convert context has exactly target"
28
+ try {
29
+ const canonical = canonicalUnit(target)
30
+ return canonical === target ? null : `context.target ${show(target)} is not canonical unit text (canonical: ${show(canonical)})`
31
+ } catch (error) {
32
+ if (error instanceof RuleError) return `context.target ${show(target)} is not unit text: ${error.detail}`
33
+ throw error
34
+ }
35
+ }
36
+
37
+ export function exprShape(row) {
38
+ try {
39
+ const ids = leaves(parseExpr(row.formula)).ids
40
+ if (ids.length !== row.operands.length || ids.some((id) => !row.operands.includes(id))) return `formula ids ${ids.join(", ")} do not match operands ${row.operands.join(", ")}`
41
+ return null
42
+ } catch (error) {
43
+ if (error instanceof RuleError) return error.detail
44
+ throw error
45
+ }
46
+ }
47
+
48
+ export function rowShape(row, rules) {
49
+ if (!object(row)) return "the row is not an object"
50
+ if (!Object.hasOwn(rules, row.operation)) return `operation ${show(row.operation)} is not one of ${Object.keys(rules).join(", ")}`
51
+ const rule = rules[row.operation]
52
+ const { operands, exact } = row
53
+ if (!Array.isArray(operands) || !operands.length || !operands.every(nonEmpty)) return `operands must be a non-empty array of fact ids, got ${show(operands)}`
54
+ const repeated = repeatedOperand(operands)
55
+ if (repeated) return repeated
56
+ if (operands.includes(row.fact_id)) return `${row.fact_id} cites itself as an operand`
57
+ if (!rule.arity(operands.length)) return `${row.operation} takes ${rule.arityText}, got ${operands.length}`
58
+ if (!isRational(exact)) return `exact must be { num, den } reduced integer strings, got ${show(exact)}`
59
+ if (row.kind !== rule.kind) return `kind ${row.kind} must be ${rule.kind} for ${row.operation}`
60
+ const own = rule.shape(row)
61
+ if (own) return own
62
+ if (!nonEmpty(row.formula) || !Array.isArray(row.derived_from) || row.derived_from.length !== operands.length) return "formula and derived_from are required audit fields"
63
+ return null
64
+ }