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,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,113 @@
1
+ import { describe, expect, onTestFinished, test } from "bun:test"
2
+ import { createHash } from "node:crypto"
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"
4
+ import { tmpdir } from "node:os"
5
+ import { join } from "node:path"
6
+
7
+ import { snapshotFromHtml, writeSnapshot } from "./source-snapshot.mjs"
8
+
9
+ const FIXTURES = join(import.meta.dir, "tests", "fixtures", "fidelity", "sources")
10
+ const S4_HTML = readFileSync(join(FIXTURES, "S4.html"), "utf8")
11
+ const S9_TEXT = readFileSync(join(FIXTURES, "S9.txt"), "utf8")
12
+
13
+ function tempDir(): string {
14
+ const dir = mkdtempSync(join(tmpdir(), "ulw-snapshot-"))
15
+ onTestFinished(() => rmSync(dir, { recursive: true, force: true }))
16
+ return dir
17
+ }
18
+
19
+ type Sentence = { sid: string; start: number; end: number; text: string }
20
+
21
+ function readSentences(dir: string): Sentence[] {
22
+ return readFileSync(join(dir, "sentences.jsonl"), "utf8")
23
+ .split("\n")
24
+ .filter((line) => line !== "")
25
+ .map((line) => JSON.parse(line))
26
+ }
27
+
28
+ describe("snapshotFromHtml", () => {
29
+ test("#given a page with prose, a table, a script and a comment #when snapshotted #then blocks are lines and each table cell is its own line", () => {
30
+ const lines = snapshotFromHtml(S4_HTML).split("\n")
31
+ expect(lines).toContain("Span length: 320 m")
32
+ expect(lines).toContain("After the delay, we estimate completion no earlier than 2027.")
33
+ const header = lines.indexOf("Metric")
34
+ expect(lines.slice(header, header + 4)).toEqual(["Metric", "Length", "Span", "320 m"])
35
+ const text = lines.join("\n")
36
+ expect(text).not.toContain("not prose")
37
+ expect(text).not.toContain("internal draft comment")
38
+ expect(text).not.toContain("p { margin")
39
+ })
40
+ })
41
+
42
+ describe("writeSnapshot", () => {
43
+ test("#given html #when written #then text.txt, source.html, meta.json, sentences.jsonl and acronyms.json exist and sha256 covers text.txt", () => {
44
+ const sources = tempDir()
45
+ const result = writeSnapshot(sources, "S4", { url: "https://example.test/bridge", html: S4_HTML, extractor: "tool:source-snapshot@1", observed_at: "2026-10-23T00:00:00Z" })
46
+ expect(result.dir).toBe(join(sources, "S4"))
47
+ for (const name of ["text.txt", "source.html", "meta.json", "sentences.jsonl", "acronyms.json"]) expect(existsSync(join(result.dir, name))).toBe(true)
48
+ const text = readFileSync(join(result.dir, "text.txt"))
49
+ expect(result.sha256).toBe(createHash("sha256").update(text).digest("hex"))
50
+ expect(readFileSync(join(result.dir, "source.html"), "utf8")).toBe(S4_HTML)
51
+ const meta = JSON.parse(readFileSync(join(result.dir, "meta.json"), "utf8"))
52
+ expect(meta).toEqual({
53
+ source_id: "S4",
54
+ url: "https://example.test/bridge",
55
+ observed_at: "2026-10-23T00:00:00Z",
56
+ sha256: result.sha256,
57
+ extractor: "tool:source-snapshot@1",
58
+ revision: null,
59
+ kind: "page",
60
+ provenance: "unknown",
61
+ })
62
+ })
63
+
64
+ test("#given a written snapshot #when sentences.jsonl is read #then every row slices text.txt and carries an S<n>:s<k> id", () => {
65
+ const sources = tempDir()
66
+ const { dir } = writeSnapshot(sources, "S4", { url: "https://example.test/bridge", html: S4_HTML, extractor: "tool:source-snapshot@1" })
67
+ const text = readFileSync(join(dir, "text.txt"), "utf8")
68
+ const sentences = readSentences(dir)
69
+ expect(sentences.length).toBeGreaterThanOrEqual(4)
70
+ sentences.forEach((row, index) => {
71
+ expect(row.sid).toBe(`S4:s${index + 1}`)
72
+ expect(text.slice(row.start, row.end)).toBe(row.text)
73
+ })
74
+ })
75
+
76
+ test("#given both acronym forms #when written #then acronyms.json lists each expansion with its sentence and span", () => {
77
+ const sources = tempDir()
78
+ const text = "The deck uses High Bandwidth Memory (HBM) sensors.\nA second note cites TSV (Through Silicon Via) bonding."
79
+ const { dir, sha256 } = writeSnapshot(sources, "S2", { path: "notes/memo.txt", text, extractor: "model" })
80
+ const acronyms = JSON.parse(readFileSync(join(dir, "acronyms.json"), "utf8"))
81
+ expect(acronyms.source_id).toBe("S2")
82
+ expect(acronyms.snapshot_sha256).toBe(sha256)
83
+ const entries = acronyms.entries.map((entry: { acronym: string; expansion: string; sentence_id: string; span: number[] }) => [entry.acronym, entry.expansion, entry.sentence_id, text.slice(entry.span[0], entry.span[1])])
84
+ expect(entries).toEqual([
85
+ ["HBM", "High Bandwidth Memory", "S2:s1", "High Bandwidth Memory (HBM)"],
86
+ ["TSV", "Through Silicon Via", "S2:s2", "TSV (Through Silicon Via)"],
87
+ ])
88
+ expect(JSON.parse(readFileSync(join(dir, "meta.json"), "utf8")).kind).toBe("document")
89
+ expect(existsSync(join(dir, "source.html"))).toBe(false)
90
+ })
91
+
92
+ test("#given pages #when written #then pages are joined by a form feed and no sentence crosses a page", () => {
93
+ const sources = tempDir()
94
+ const { dir } = writeSnapshot(sources, "S7", { path: "report.pdf", pages: ["First page ends here", "second page starts here."], extractor: "extractor:pdftotext" })
95
+ expect(readFileSync(join(dir, "text.txt"), "utf8")).toBe("First page ends here\fsecond page starts here.")
96
+ expect(readSentences(dir).map((row) => row.text)).toEqual(["First page ends here", "second page starts here."])
97
+ })
98
+
99
+ test("#given Korean text #when written #then the sentence index covers it", () => {
100
+ const sources = tempDir()
101
+ const { dir } = writeSnapshot(sources, "S9", { path: "S9.txt", text: S9_TEXT, extractor: "tool:source-snapshot@1" })
102
+ expect(readSentences(dir).map((row) => row.sid)).toEqual(["S9:s1"])
103
+ })
104
+
105
+ test("#given an invalid extractor, source id, or a different text over an existing snapshot #when written #then it throws", () => {
106
+ const sources = tempDir()
107
+ expect(() => writeSnapshot(sources, "S1", { path: "a.txt", text: "x", extractor: "guess" })).toThrow()
108
+ expect(() => writeSnapshot(sources, "source1", { path: "a.txt", text: "x", extractor: "model" })).toThrow()
109
+ writeSnapshot(sources, "S1", { path: "a.txt", text: "first", extractor: "model" })
110
+ expect(() => writeSnapshot(sources, "S1", { path: "a.txt", text: "first", extractor: "model" })).not.toThrow()
111
+ expect(() => writeSnapshot(sources, "S1", { path: "a.txt", text: "second", extractor: "model" })).toThrow(/snapshot_exists/)
112
+ })
113
+ })
@@ -0,0 +1,3 @@
1
+ fact_id,plot_value,unit
2
+ F5,12,%
3
+ F6,27,%
@@ -0,0 +1,89 @@
1
+ // gen-legacy-dev.mjs - regenerates ledger-legacy-dev.json with dev's own pre-registry writer, so the
2
+ // dimension 31 tests load every value shape dev could write, not hand-picked rows.
3
+ // Pinned dev commit: 99220c4e95f9ef3a667834486f2caa03873deeab (origin/dev when the fixture was generated).
4
+ // Usage: node gen-legacy-dev.mjs [dev-sha]. Needs git and the commit in the local clone; node: builtins only.
5
+
6
+ import { execFileSync } from "node:child_process"
7
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
8
+ import { tmpdir } from "node:os"
9
+ import { dirname, join } from "node:path"
10
+ import { fileURLToPath, pathToFileURL } from "node:url"
11
+
12
+ const DEV_SHA = process.argv[2] ?? "99220c4e95f9ef3a667834486f2caa03873deeab"
13
+ const SCRIPTS = "packages/shared-skills/skills/ulw-research/scripts"
14
+ // fact-ledger.mjs and every module it imports at the pinned commit.
15
+ const WRITER_FILES = ["fact-ledger.mjs", "fact-ledger-kinds.mjs", "fact-ledger-schema.mjs", "cli-support.mjs", "entry-guard.mjs"]
16
+ const HERE = dirname(fileURLToPath(import.meta.url))
17
+ const OUT = join(HERE, "ledger-legacy-dev.json")
18
+
19
+ const at = { artifact: "sources/S1/text.txt", line: 2, char_start: 0, char_end: 30 }
20
+ const sha = "a".repeat(64)
21
+ const base = { extracted_by: "fact add --find", observed_at: "2026-10-20T08:00:00Z" }
22
+
23
+ function measured(label, value, unit) {
24
+ const raw = `${value} ${unit}`
25
+ return { ...base, kind: "measured", label, value, unit, raw, quote: `The figure is ${raw}.`, value_span: [14, 14 + value.length], source_id: "S1", locator: at, snapshot_sha256: sha }
26
+ }
27
+
28
+ function assumed(label, value, unit, exact) {
29
+ return { ...base, kind: "assumed", label, value, unit, exact, raw: "", quote: "", extracted_by: "fact add --assumed", rationale: "planning assumption from the brief" }
30
+ }
31
+
32
+ function legacy(kind, label, fields) {
33
+ return { ...base, kind, label, raw: "", quote: "", extracted_by: "fact derive", ...fields }
34
+ }
35
+
36
+ // Every kind and every value shape dev's addRow accepts: alias units (English and Korean count and
37
+ // currency words, written as escapes so this file stays ASCII), leading zeros, negative values,
38
+ // assumed rows with unreduced or zero-padded exacts, and a legacy derived and converted row. The
39
+ // last three are shapes dev accepted that the strict dimensions do not (empty metadata, an operation
40
+ // on a source row, an extra key in exact): they must load and be refused only as operands.
41
+ const SEEDS = [
42
+ measured("north-span", "12", "kilometers"),
43
+ measured("south-span", "8.5", "kilometers"),
44
+ measured("share-online", "41.2", "per cent"),
45
+ measured("share-gap", "1.5", "pp"),
46
+ measured("staff-count", "320", "\uBA85"),
47
+ measured("unit-price", "750", "\uCC9C\uC6D0"),
48
+ measured("padded-span", "007", "km"),
49
+ measured("net-change", "-2.5", "per cent"),
50
+ { ...base, kind: "quote", label: "quote-claim", raw: "grew fast", quote: "The market grew fast.", value_span: [11, 20], source_id: "S1", locator: at, snapshot_sha256: sha },
51
+ assumed("growth-assumption", "3", "%", null),
52
+ assumed("whole-ratio", "1", "%", { num: "2", den: "2" }),
53
+ assumed("quarter-ratio", "2.5", "kilometers", { num: "10", den: "4" }),
54
+ assumed("padded-exact", "7", "km", { num: "007", den: "01" }),
55
+ assumed("negative-exact", "-2.5", "per cent", { num: "-10", den: "04" }),
56
+ legacy("derived", "total-span", { value: "20.5", unit: "kilometers", exact: { num: "82", den: "4" }, operation: "sum", operands: ["F1", "F2"], derived_from: ["F1", "F2"], formula: "F1 + F2" }),
57
+ legacy("converted", "span-metres", { value: "12000", unit: "m", exact: { num: "24000", den: "2" }, operation: "convert", operands: ["F1"], derived_from: ["F1"], formula: "F1 * 1000", factor: "1000" }),
58
+ { ...measured("blank-entity", "5", "km"), entity: "" },
59
+ { ...measured("tagged-sum", "6", "km"), operation: "sum" },
60
+ assumed("keyed-exact", "3", "km", { num: "3", den: "1", note: "from the brief" }),
61
+ ]
62
+
63
+ /** JSON with every non-ASCII character as a \u escape, tab-indented like the other fixtures. */
64
+ function asciiJson(value) {
65
+ return `${JSON.stringify(value, null, "\t").replace(/[\u0080-\uffff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`)}\n`
66
+ }
67
+
68
+ async function main() {
69
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: HERE, encoding: "utf8" }).trim()
70
+ const work = mkdtempSync(join(tmpdir(), "gen-legacy-dev-"))
71
+ try {
72
+ mkdirSync(join(work, "scripts"))
73
+ for (const file of WRITER_FILES) {
74
+ const source = execFileSync("git", ["show", `${DEV_SHA}:${SCRIPTS}/${file}`], { cwd: root, encoding: "utf8" })
75
+ writeFileSync(join(work, "scripts", file), source)
76
+ }
77
+ const dev = await import(pathToFileURL(join(work, "scripts", "fact-ledger.mjs")).href)
78
+ const ledger = dev.emptyLedger()
79
+ for (const seed of SEEDS) dev.addRow(ledger, seed)
80
+ const errors = dev.validateLedger(ledger)
81
+ if (errors.length > 0) throw new Error(`dev's validateLedger rejected the generated ledger: ${errors.join("; ")}`)
82
+ writeFileSync(OUT, asciiJson(ledger))
83
+ process.stdout.write(`wrote ${OUT} (${ledger.rows.length} rows, dev ${DEV_SHA})\n`)
84
+ } finally {
85
+ rmSync(work, { recursive: true, force: true })
86
+ }
87
+ }
88
+
89
+ await main()
@@ -0,0 +1,223 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "rows": [
4
+ {
5
+ "fact_id": "F12",
6
+ "kind": "measured",
7
+ "raw": "320 m",
8
+ "quote": "The bridge has a main span of up to 320 m.",
9
+ "extracted_by": "fixture",
10
+ "observed_at": "2026-10-23T00:00:00Z",
11
+ "label": "span_len",
12
+ "value": "320",
13
+ "unit": "m",
14
+ "value_span": null,
15
+ "source_id": "S4",
16
+ "locator": {
17
+ "artifact": "sources/S4/text.txt",
18
+ "char_start": 0,
19
+ "char_end": 42
20
+ },
21
+ "locate_hint": null,
22
+ "snapshot_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
23
+ "valid_at": null,
24
+ "qualifier": "upper_inclusive",
25
+ "entity": null,
26
+ "sentence_id": null,
27
+ "observation_id": null,
28
+ "fuzzy": false,
29
+ "outside_floor": false,
30
+ "population": null,
31
+ "missing_rule": null,
32
+ "accounting_basis": null,
33
+ "snapshot_window": null,
34
+ "revision": null,
35
+ "exact": {
36
+ "num": "320",
37
+ "den": "1"
38
+ },
39
+ "render": null,
40
+ "operation": null,
41
+ "operands": [],
42
+ "derived_from": [],
43
+ "formula": null,
44
+ "factor": null,
45
+ "rationale": null,
46
+ "status": "active"
47
+ },
48
+ {
49
+ "fact_id": "F13",
50
+ "kind": "measured",
51
+ "raw": "15.2%",
52
+ "quote": "The rate reached 15.2% in May.",
53
+ "extracted_by": "fixture",
54
+ "observed_at": "2026-10-23T00:00:00Z",
55
+ "label": "rate",
56
+ "value": "15.2",
57
+ "unit": "%",
58
+ "value_span": null,
59
+ "source_id": "S4",
60
+ "locator": {
61
+ "artifact": "sources/S4/text.txt",
62
+ "char_start": 43,
63
+ "char_end": 73
64
+ },
65
+ "locate_hint": null,
66
+ "snapshot_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
67
+ "valid_at": null,
68
+ "qualifier": null,
69
+ "entity": null,
70
+ "sentence_id": null,
71
+ "observation_id": null,
72
+ "fuzzy": false,
73
+ "outside_floor": false,
74
+ "population": null,
75
+ "missing_rule": null,
76
+ "accounting_basis": null,
77
+ "snapshot_window": null,
78
+ "revision": null,
79
+ "exact": {
80
+ "num": "76",
81
+ "den": "5"
82
+ },
83
+ "render": null,
84
+ "operation": null,
85
+ "operands": [],
86
+ "derived_from": [],
87
+ "formula": null,
88
+ "factor": null,
89
+ "rationale": null,
90
+ "status": "active"
91
+ },
92
+ {
93
+ "fact_id": "Q7",
94
+ "kind": "quote",
95
+ "raw": "we estimate completion no earlier than 2027",
96
+ "quote": "we estimate completion no earlier than 2027",
97
+ "extracted_by": "fixture",
98
+ "observed_at": "2026-10-23T00:00:00Z",
99
+ "label": "completion",
100
+ "value": null,
101
+ "unit": null,
102
+ "value_span": null,
103
+ "source_id": "S4",
104
+ "locator": {
105
+ "artifact": "sources/S4/text.txt",
106
+ "char_start": 74,
107
+ "char_end": 117
108
+ },
109
+ "locate_hint": null,
110
+ "snapshot_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
111
+ "valid_at": null,
112
+ "qualifier": null,
113
+ "entity": null,
114
+ "sentence_id": null,
115
+ "observation_id": null,
116
+ "fuzzy": false,
117
+ "outside_floor": false,
118
+ "population": null,
119
+ "missing_rule": null,
120
+ "accounting_basis": null,
121
+ "snapshot_window": null,
122
+ "revision": null,
123
+ "exact": null,
124
+ "render": null,
125
+ "operation": null,
126
+ "operands": [],
127
+ "derived_from": [],
128
+ "formula": null,
129
+ "factor": null,
130
+ "rationale": null,
131
+ "status": "active"
132
+ },
133
+ {
134
+ "fact_id": "F21",
135
+ "kind": "measured",
136
+ "raw": "4\uc870 2,000\uc5b5 \uc6d0",
137
+ "quote": "3\ubd84\uae30 HBM \ub9e4\ucd9c\uc740 4\uc870 2,000\uc5b5 \uc6d0\uc744 \uae30\ub85d\ud588\ub2e4.",
138
+ "extracted_by": "fixture",
139
+ "observed_at": "2026-10-23T00:00:00Z",
140
+ "label": "hbm_rev",
141
+ "value": "4200000000000",
142
+ "unit": "KRW",
143
+ "value_span": null,
144
+ "source_id": "S5",
145
+ "locator": {
146
+ "artifact": "sources/S5/text.txt",
147
+ "char_start": 0,
148
+ "char_end": 30
149
+ },
150
+ "locate_hint": null,
151
+ "snapshot_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
152
+ "valid_at": "2026 Q3",
153
+ "qualifier": null,
154
+ "entity": "HBM3E",
155
+ "sentence_id": null,
156
+ "observation_id": null,
157
+ "fuzzy": false,
158
+ "outside_floor": false,
159
+ "population": null,
160
+ "missing_rule": null,
161
+ "accounting_basis": null,
162
+ "snapshot_window": null,
163
+ "revision": null,
164
+ "exact": {
165
+ "num": "4200000000000",
166
+ "den": "1"
167
+ },
168
+ "render": null,
169
+ "operation": null,
170
+ "operands": [],
171
+ "derived_from": [],
172
+ "formula": null,
173
+ "factor": null,
174
+ "rationale": null,
175
+ "status": "active"
176
+ },
177
+ {
178
+ "fact_id": "F40",
179
+ "kind": "converted",
180
+ "raw": "320000 mm",
181
+ "quote": "The bridge has a main span of up to 320 m.",
182
+ "extracted_by": "fixture",
183
+ "observed_at": "2026-10-23T00:00:00Z",
184
+ "label": "span_mm",
185
+ "value": "320000",
186
+ "unit": "mm",
187
+ "value_span": null,
188
+ "source_id": null,
189
+ "locator": null,
190
+ "locate_hint": null,
191
+ "snapshot_sha256": null,
192
+ "valid_at": null,
193
+ "qualifier": null,
194
+ "entity": null,
195
+ "sentence_id": null,
196
+ "observation_id": null,
197
+ "fuzzy": false,
198
+ "outside_floor": false,
199
+ "population": null,
200
+ "missing_rule": null,
201
+ "accounting_basis": null,
202
+ "snapshot_window": null,
203
+ "revision": null,
204
+ "exact": {
205
+ "num": "320000",
206
+ "den": "1"
207
+ },
208
+ "render": null,
209
+ "operation": "convert",
210
+ "operands": [
211
+ "F12"
212
+ ],
213
+ "derived_from": [
214
+ "F12"
215
+ ],
216
+ "formula": null,
217
+ "factor": "1000",
218
+ "rationale": null,
219
+ "status": "active"
220
+ }
221
+ ],
222
+ "events": []
223
+ }