lazycodex-ai 5.1.44 → 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 (412) 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 +865 -413
  12. package/dist/cli/service-auth/keystore.d.ts +4 -2
  13. package/dist/cli-node/index.js +865 -413
  14. package/docs/reference/web-terminal-visual-qa.md +8 -0
  15. package/package.json +1 -1
  16. package/packages/lsp-daemon/dist/cli.js +7 -7
  17. package/packages/lsp-daemon/dist/client.js +7 -7
  18. package/packages/lsp-daemon/dist/index.js +7 -7
  19. package/packages/lsp-daemon/package.json +3 -3
  20. package/packages/lsp-tools-mcp/dist/cli.js +7 -7
  21. package/packages/lsp-tools-mcp/dist/mcp.js +7 -7
  22. package/packages/lsp-tools-mcp/dist/tools.js +4 -4
  23. package/packages/lsp-tools-mcp/package.json +3 -3
  24. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  25. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +77 -26
  26. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +1 -1
  29. package/packages/omo-codex/plugin/components/comment-checker/biome.json +1 -21
  30. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
  32. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  33. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  34. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/biome.json +1 -21
  35. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  36. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +4 -4
  37. package/packages/omo-codex/plugin/components/lsp/biome.json +1 -21
  38. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  39. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +4 -4
  40. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  41. package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
  42. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +0 -1
  43. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +1 -1
  44. package/packages/omo-codex/plugin/components/rules/biome.json +1 -21
  45. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  46. package/packages/omo-codex/plugin/components/rules/package.json +4 -4
  47. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  48. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  49. package/packages/omo-codex/plugin/components/telemetry/biome.json +1 -21
  50. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +90 -1
  51. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +91 -1
  52. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  53. package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
  54. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +2 -2
  55. package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -21
  56. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  57. package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
  58. package/packages/omo-codex/plugin/components/ulw-execute-continuation/biome.json +1 -21
  59. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  60. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  61. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +4 -4
  62. package/packages/omo-codex/plugin/components/ulw-loop/biome.json +1 -21
  63. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +1 -1
  64. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +7 -7
  65. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +5 -5
  66. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +1 -1
  67. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  68. package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
  69. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +1 -1
  70. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
  71. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +1 -1
  72. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +2 -0
  73. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  85. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  86. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  87. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  88. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  92. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  93. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  94. package/packages/omo-codex/plugin/package-lock.json +12 -12
  95. package/packages/omo-codex/plugin/package.json +1 -1
  96. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +14 -9
  97. package/packages/omo-codex/plugin/skills/ast-grep/AGENTS.md +1 -1
  98. package/packages/omo-codex/plugin/skills/browser/SKILL.md +21 -22
  99. package/packages/omo-codex/plugin/skills/browser/references/commands.md +2 -9
  100. package/packages/omo-codex/plugin/skills/browser/references/install.md +1 -2
  101. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  102. package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +2 -2
  103. package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +63 -487
  104. package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +1 -1
  105. package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +1 -10
  106. package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +2 -2
  107. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +79 -118
  108. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +75 -224
  109. package/packages/omo-codex/plugin/skills/frontend/references/design/stylegallery.md +32 -27
  110. package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -2
  111. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
  112. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
  113. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/collect-files.ts +53 -0
  114. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
  115. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
  116. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
  117. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
  118. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
  119. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
  120. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
  121. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
  122. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
  123. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
  124. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
  125. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
  126. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
  127. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
  128. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
  129. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
  130. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
  131. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
  132. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
  133. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +1 -1
  134. package/packages/omo-codex/plugin/skills/programming/SKILL.md +10 -7
  135. package/packages/omo-codex/plugin/skills/programming/references/typescript/README.md +10 -7
  136. package/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md +9 -23
  137. package/packages/omo-codex/plugin/skills/programming/references/typescript/oxc-lint.md +130 -0
  138. package/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md +6 -62
  139. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
  140. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/new-project.ts +20 -81
  141. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
  142. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
  143. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
  144. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
  145. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
  146. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
  147. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
  148. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +14 -14
  149. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +1 -1
  150. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +2 -2
  151. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  152. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +2 -2
  153. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  154. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +21 -3
  155. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +37 -12
  156. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +77 -16
  157. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +134 -0
  158. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger.md +95 -0
  159. package/packages/omo-codex/plugin/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  160. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +68 -3
  161. package/packages/omo-codex/plugin/skills/ulw-research/references/report-visualization.md +2 -0
  162. package/packages/omo-codex/plugin/skills/ulw-research/references/unit-rules.md +157 -0
  163. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  164. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claims-index.mjs +125 -0
  165. package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +19 -5
  166. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +89 -3
  167. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
  168. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
  169. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +239 -0
  170. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  171. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  172. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  173. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +250 -0
  174. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +221 -0
  175. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +123 -0
  176. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  177. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
  178. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  179. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  180. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +150 -0
  181. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  182. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  183. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  184. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +64 -0
  185. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  186. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  187. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  188. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +29 -9
  189. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +1 -1
  190. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +1 -27
  191. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +2 -2
  192. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-markup.mjs +182 -0
  193. package/packages/omo-codex/plugin/skills/ulw-research/scripts/lexicon.mjs +47 -0
  194. package/packages/omo-codex/plugin/skills/ulw-research/scripts/md-lite.mjs +193 -0
  195. package/packages/omo-codex/plugin/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  196. package/packages/omo-codex/plugin/skills/ulw-research/scripts/numerals.mjs +226 -0
  197. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +8 -8
  198. package/packages/omo-codex/plugin/skills/ulw-research/scripts/particles-ko.mjs +108 -0
  199. package/packages/omo-codex/plugin/skills/ulw-research/scripts/placeholder.mjs +184 -0
  200. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  201. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  202. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-project.mjs +125 -0
  203. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  204. package/packages/omo-codex/plugin/skills/ulw-research/scripts/rational.mjs +139 -0
  205. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +21 -10
  206. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +15 -0
  207. package/packages/omo-codex/plugin/skills/ulw-research/scripts/sentence-split.mjs +77 -0
  208. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +140 -0
  209. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  210. package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-normalize.mjs +82 -0
  211. package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-similarity.mjs +28 -0
  212. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  213. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  214. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  215. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  216. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  217. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  218. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  219. package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +105 -0
  220. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +71 -64
  221. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +4 -1
  222. package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +19 -0
  223. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +12 -8
  224. package/packages/omo-codex/scripts/install-dist/install-local.mjs +169 -30
  225. package/packages/shared-skills/skills/ast-grep/AGENTS.md +1 -1
  226. package/packages/shared-skills/skills/browser/SKILL.md +21 -22
  227. package/packages/shared-skills/skills/browser/references/commands.md +2 -9
  228. package/packages/shared-skills/skills/browser/references/install.md +1 -2
  229. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  230. package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +2 -2
  231. package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +63 -487
  232. package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +1 -1
  233. package/packages/shared-skills/skills/browser/scripts/omowright.mjs +1 -10
  234. package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +2 -2
  235. package/packages/shared-skills/skills/frontend/SKILL.md +79 -118
  236. package/packages/shared-skills/skills/frontend/references/design/README.md +75 -224
  237. package/packages/shared-skills/skills/frontend/references/design/stylegallery.md +32 -27
  238. package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -2
  239. package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
  240. package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.test.ts +101 -0
  241. package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
  242. package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.test.ts +50 -0
  243. package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.ts +53 -0
  244. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
  245. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
  246. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
  247. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
  248. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
  249. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
  250. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
  251. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
  252. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
  253. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
  254. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
  255. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
  256. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
  257. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
  258. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
  259. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
  260. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
  261. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
  262. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
  263. package/packages/shared-skills/skills/programming/SKILL.md +10 -7
  264. package/packages/shared-skills/skills/programming/references/typescript/README.md +10 -7
  265. package/packages/shared-skills/skills/programming/references/typescript/bootstrap.md +9 -23
  266. package/packages/shared-skills/skills/programming/references/typescript/oxc-lint.md +130 -0
  267. package/packages/shared-skills/skills/programming/references/typescript/tsconfig-strict.md +6 -62
  268. package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
  269. package/packages/shared-skills/skills/programming/scripts/typescript/new-project.test.ts +162 -0
  270. package/packages/shared-skills/skills/programming/scripts/typescript/new-project.ts +20 -81
  271. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
  272. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
  273. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
  274. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
  275. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
  276. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
  277. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
  278. package/packages/shared-skills/skills/refactor/SKILL.md +13 -13
  279. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  280. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  281. package/packages/shared-skills/skills/ulw-research/AGENTS.md +21 -3
  282. package/packages/shared-skills/skills/ulw-research/SKILL.md +37 -12
  283. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +77 -16
  284. package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +134 -0
  285. package/packages/shared-skills/skills/ulw-research/references/fact-ledger.md +95 -0
  286. package/packages/shared-skills/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  287. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +68 -3
  288. package/packages/shared-skills/skills/ulw-research/references/report-visualization.md +2 -0
  289. package/packages/shared-skills/skills/ulw-research/references/unit-rules.md +157 -0
  290. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  291. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.test.ts +193 -0
  292. package/packages/shared-skills/skills/ulw-research/scripts/claims-index.mjs +125 -0
  293. package/packages/shared-skills/skills/ulw-research/scripts/claims-index.test.ts +149 -0
  294. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +19 -5
  295. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +13 -2
  296. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +89 -3
  297. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +100 -12
  298. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns.test.ts +183 -0
  299. package/packages/shared-skills/skills/ulw-research/scripts/delivery-barrier.test.ts +245 -0
  300. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
  301. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
  302. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +239 -0
  303. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  304. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.test.ts +375 -0
  305. package/packages/shared-skills/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  306. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  307. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.test.ts +221 -0
  308. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +197 -0
  309. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +250 -0
  310. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +156 -0
  311. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +160 -0
  312. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +221 -0
  313. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.test.ts +43 -0
  314. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +123 -0
  315. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +254 -0
  316. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  317. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.test.ts +118 -0
  318. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
  319. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.test.ts +167 -0
  320. package/packages/shared-skills/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  321. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +409 -0
  322. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-rules.test.ts +388 -0
  323. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  324. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.test.ts +81 -0
  325. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +150 -0
  326. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.test.ts +179 -0
  327. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  328. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  329. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.test.ts +117 -0
  330. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  331. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +64 -0
  332. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.test.ts +46 -0
  333. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  334. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.test.ts +58 -0
  335. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  336. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.test.ts +270 -0
  337. package/packages/shared-skills/skills/ulw-research/scripts/gates-context.test.ts +64 -0
  338. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  339. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.test.ts +73 -0
  340. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +29 -9
  341. package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +1 -1
  342. package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +1 -27
  343. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +2 -2
  344. package/packages/shared-skills/skills/ulw-research/scripts/html-markup.mjs +182 -0
  345. package/packages/shared-skills/skills/ulw-research/scripts/lexicon.mjs +47 -0
  346. package/packages/shared-skills/skills/ulw-research/scripts/lexicon.test.ts +57 -0
  347. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.mjs +193 -0
  348. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.test.ts +271 -0
  349. package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  350. package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.test.ts +228 -0
  351. package/packages/shared-skills/skills/ulw-research/scripts/numerals.mjs +226 -0
  352. package/packages/shared-skills/skills/ulw-research/scripts/numerals.test.ts +120 -0
  353. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +8 -8
  354. package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +29 -0
  355. package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.mjs +108 -0
  356. package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.test.ts +86 -0
  357. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.mjs +184 -0
  358. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.test.ts +77 -0
  359. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  360. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.test.ts +363 -0
  361. package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  362. package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.test.ts +168 -0
  363. package/packages/shared-skills/skills/ulw-research/scripts/polish-markup.test.ts +187 -0
  364. package/packages/shared-skills/skills/ulw-research/scripts/polish-project.mjs +125 -0
  365. package/packages/shared-skills/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  366. package/packages/shared-skills/skills/ulw-research/scripts/rational.mjs +139 -0
  367. package/packages/shared-skills/skills/ulw-research/scripts/rational.test.ts +46 -0
  368. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +21 -10
  369. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +15 -0
  370. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +458 -2
  371. package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.mjs +77 -0
  372. package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.test.ts +109 -0
  373. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +140 -0
  374. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  375. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.test.ts +113 -0
  376. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/assets/share.csv +3 -0
  377. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/claim-graph.md +13 -0
  378. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/gen-legacy-dev.mjs +89 -0
  379. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-basic.json +223 -0
  380. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-legacy-dev.json +859 -0
  381. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-verify.json +420 -0
  382. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-bad.html +19 -0
  383. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-good.html +39 -0
  384. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/acronyms.json +3 -0
  385. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/meta.json +9 -0
  386. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/sentences.jsonl +7 -0
  387. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/text.txt +7 -0
  388. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4.html +19 -0
  389. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/acronyms.json +1 -0
  390. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/meta.json +9 -0
  391. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/sentences.jsonl +1 -0
  392. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/text.txt +1 -0
  393. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S9.txt +1 -0
  394. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-locked.md +17 -0
  395. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-pending.md +17 -0
  396. package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.mjs +82 -0
  397. package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.test.ts +73 -0
  398. package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.mjs +28 -0
  399. package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.test.ts +38 -0
  400. package/packages/shared-skills/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  401. package/packages/shared-skills/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  402. package/packages/shared-skills/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  403. package/packages/shared-skills/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  404. package/packages/shared-skills/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  405. package/packages/shared-skills/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  406. package/packages/shared-skills/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  407. package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +105 -0
  408. package/packages/shared-skills/skills/ulw-research/scripts/units.test.ts +83 -0
  409. package/packages/shared-skills/skills/visual-qa/SKILL.md +70 -63
  410. package/packages/shared-skills/skills/visual-qa/scripts/skill-doc-fields.test.ts +34 -0
  411. package/script/qa/web-terminal-visual-qa.mjs +6 -2
  412. package/script/qa/xterm-live-terminal.mjs +26 -3
@@ -0,0 +1,231 @@
1
+ // fact-locate.mjs - deterministic fact locators over a source snapshot.
2
+ //
3
+ // Each locator returns { value, unit, raw, quote, value_span, sentence_id, qualifier, locator } or
4
+ // { error: "fact_not_located" | "fact_ambiguous", candidates }; it never guesses. `quote` is the
5
+ // whole containing sentence, cell, or line of text.txt, `value_span` is [start, end] inside
6
+ // `quote` with quote.slice(...value_span) === raw, and locator.char_start/char_end place `quote`
7
+ // in text.txt. Every call takes `sourceId` (S<n>) so sentence ids match sentences.jsonl.
8
+ // Plain Node ESM, sibling imports only; no network, no OCR.
9
+
10
+ import { proseText } from "./html-lite.mjs"
11
+ import { QUALIFIERS } from "./lexicon.mjs"
12
+ import { parseNumeral } from "./numerals.mjs"
13
+ import { indexSentences, snapshotBlocks } from "./source-snapshot.mjs"
14
+ import { findNormalized, normalizeText, tokenize } from "./text-normalize.mjs"
15
+ import { similarity } from "./text-similarity.mjs"
16
+ import { normalizeUnit } from "./units.mjs"
17
+
18
+ const FIND_WINDOW = 80
19
+ const CONTEXT = 60
20
+ const FUZZY_FLOOR = 0.95
21
+ const PHRASES = QUALIFIERS.map((phrase) => phrase.split(" "))
22
+ const SINGLE_QUALIFIERS = new Set(QUALIFIERS.filter((phrase) => !phrase.includes(" ")))
23
+
24
+ const notLocated = (candidates = []) => ({ error: "fact_not_located", candidates })
25
+ const ambiguous = (candidates) => ({ error: "fact_ambiguous", candidates })
26
+ const label = (text) => normalizeText(text).toLowerCase()
27
+ const clean = (token) => token.replace(/^[^\p{L}\p{N}%]+|[^\p{L}\p{N}%]+$/gu, "").toLowerCase()
28
+ const sentenceAt = (sentences, offset) => sentences.find((row) => row.start <= offset && offset < row.end) ?? null
29
+
30
+ function requireSource(sourceId) {
31
+ if (!/^S[1-9]\d*$/.test(String(sourceId))) throw new TypeError(`sourceId must be S<n>, got ${sourceId}`)
32
+ }
33
+
34
+ /** Every normalized occurrence of `needle`, as raw [start, end) offsets. */
35
+ function findAll(text, needle) {
36
+ const hits = []
37
+ for (let from = 0; from < text.length; ) {
38
+ const hit = findNormalized(text.slice(from), needle)
39
+ if (!hit) break
40
+ hits.push({ start: from + hit.start, end: from + hit.end })
41
+ from += hit.start + 1
42
+ }
43
+ return hits
44
+ }
45
+
46
+ function locatorFor(text, sourceId, start, end, extra = {}) {
47
+ const before = text.slice(0, start)
48
+ const pageStart = before.lastIndexOf("\f") + 1
49
+ const page = text.includes("\f") ? { page: before.split("\f").length } : {}
50
+ return { artifact: `${sourceId}/text.txt`, ...page, line: before.slice(pageStart).split("\n").length, char_start: start, char_end: end, ...extra }
51
+ }
52
+
53
+ function candidate(text, sentences, start, end) {
54
+ return { sentence_id: sentenceAt(sentences, start)?.sid ?? null, char_start: start, char_end: end, context: text.slice(Math.max(0, start - CONTEXT), end + CONTEXT) }
55
+ }
56
+
57
+ /** A QUALIFIERS token ending within the 2 tokens before the value, or the 1 token right after it. */
58
+ function qualifierAround(quote, [start, end]) {
59
+ const before = quote.slice(0, start).split(/\s+/).filter(Boolean).map(clean)
60
+ for (let skip = 0; skip < 2; skip++) {
61
+ for (const words of PHRASES) {
62
+ const stop = before.length - skip
63
+ if (stop - words.length >= 0 && before.slice(stop - words.length, stop).join(" ") === words.join(" ")) return words.join(" ")
64
+ }
65
+ }
66
+ const next = /^\s*(\S+)/.exec(quote.slice(end))
67
+ return next && SINGLE_QUALIFIERS.has(clean(next[1])) ? clean(next[1]) : null
68
+ }
69
+
70
+ /** A located quantity: `holder` is the quote's { sid, start, end, text }, `numeral` is parsed from holder.text. */
71
+ function quantityResult(text, sourceId, holder, numeral, extra = {}, unitFallback = null) {
72
+ const unit = numeral.unitRaw ? (normalizeUnit(numeral.unitRaw) ?? numeral.unitRaw) : unitFallback
73
+ return {
74
+ value: numeral.value,
75
+ unit,
76
+ raw: numeral.raw,
77
+ quote: holder.text,
78
+ value_span: [numeral.span[0], numeral.span[1]],
79
+ sentence_id: holder.sid,
80
+ qualifier: qualifierAround(holder.text, numeral.span),
81
+ locator: locatorFor(text, sourceId, holder.start, holder.end, extra),
82
+ }
83
+ }
84
+
85
+ /** --find: one anchor occurrence, then the first numeral starting within 80 chars after it in the same sentence. */
86
+ export function locateFind(text, { find, unitExpected, sourceId }) {
87
+ requireSource(sourceId)
88
+ const sentences = indexSentences(text, sourceId)
89
+ const hits = findAll(text, find)
90
+ if (hits.length === 0) return notLocated()
91
+ if (hits.length > 1) return ambiguous(hits.map((hit) => candidate(text, sentences, hit.start, hit.end)))
92
+ const [hit] = hits
93
+ const sentence = sentenceAt(sentences, hit.start)
94
+ if (!sentence || hit.end > sentence.end) return notLocated()
95
+ const from = hit.end - sentence.start
96
+ const numeral = parseNumeral(sentence.text, from)
97
+ if (!numeral || numeral.span[0] - from > FIND_WINDOW) return notLocated([candidate(text, sentences, hit.start, hit.end)])
98
+ const result = quantityResult(text, sourceId, sentence, numeral)
99
+ if (unitExpected === undefined) return result
100
+ const expected = normalizeUnit(unitExpected) ?? unitExpected
101
+ return result.unit === expected ? result : { error: "unit_expected_mismatch", expected, found: result.unit, candidates: [result] }
102
+ }
103
+
104
+ function cellsOf(row) {
105
+ const cells = []
106
+ for (const cell of row.children.filter((child) => child.tag === "td" || child.tag === "th")) {
107
+ const span = Math.max(1, Number.parseInt(cell.attrs.colspan ?? "1", 10) || 1)
108
+ for (let k = 0; k < span; k++) cells.push(cell)
109
+ }
110
+ return cells
111
+ }
112
+
113
+ function rowsOf(table) {
114
+ const rows = []
115
+ const walk = (node) => {
116
+ for (const child of node.children) {
117
+ if (child.tag === "tr") rows.push(child)
118
+ else if (child.tag !== "table" && child.tag !== "#text") walk(child)
119
+ }
120
+ }
121
+ walk(table)
122
+ return rows
123
+ }
124
+
125
+ function tablesOf(root) {
126
+ const tables = []
127
+ const walk = (node) => {
128
+ for (const child of node.children) {
129
+ if (child.tag === "table") tables.push(child)
130
+ if (child.tag !== "#text") walk(child)
131
+ }
132
+ }
133
+ walk(root)
134
+ return tables.map((table) => {
135
+ const rows = rowsOf(table)
136
+ const header = rows.find((row) => row.children.some((child) => child.tag === "th")) ?? rows[0]
137
+ const caption = table.children.find((child) => child.tag === "caption")
138
+ return { table, header, rows: rows.filter((row) => row !== header), caption: caption ? label(proseText(caption)) : null }
139
+ })
140
+ }
141
+
142
+ const cellLabel = (cell) => label(proseText(cell))
143
+ const indexesOf = (labels, wanted) => labels.flatMap((value, index) => (value === wanted ? [index] : []))
144
+
145
+ /** --table/--row/--col: the table by id, caption, or a header label; row and column by normalized label, never by index. */
146
+ export function locateTable(sourceHtml, { table, row, col, sourceId }) {
147
+ requireSource(sourceId)
148
+ const { root, text, blocks } = snapshotBlocks(sourceHtml)
149
+ const key = label(table)
150
+ const tables = tablesOf(root).filter((entry) => entry.table.attrs.id === table || entry.caption === key || (entry.header && cellsOf(entry.header).map(cellLabel).includes(key)))
151
+ if (tables.length !== 1) return tables.length === 0 ? notLocated() : ambiguous(tables.map((entry) => ({ table: entry.table.attrs.id ?? entry.caption })))
152
+ const [{ header, rows }] = tables
153
+ const headerCells = header ? cellsOf(header) : []
154
+ const columns = indexesOf(headerCells.map(cellLabel), label(col))
155
+ const matches = rows.filter((candidateRow) => cellsOf(candidateRow).length > 0 && cellLabel(cellsOf(candidateRow)[0]) === label(row))
156
+ if (columns.length > 1 || matches.length > 1) return ambiguous([...columns.map((index) => ({ col: index })), ...matches.map((match) => ({ row_line: match.line }))])
157
+ const cell = columns.length === 1 && matches.length === 1 ? cellsOf(matches[0])[columns[0]] : null
158
+ const block = cell ? blocks.find((entry) => entry.node === cell) : null
159
+ if (!block) return notLocated()
160
+ const holder = { sid: sentenceAt(indexSentences(text, sourceId), block.start)?.sid ?? null, start: block.start, end: block.end, text: text.slice(block.start, block.end) }
161
+ const numeral = parseNumeral(holder.text)
162
+ if (!numeral) return notLocated()
163
+ const headerUnit = /\(([^()]+)\)\s*$/.exec(cellLabel(headerCells[columns[0]]))
164
+ return quantityResult(text, sourceId, holder, numeral, { table, row_label: row, col_label: col }, headerUnit ? normalizeUnit(headerUnit[1]) : null)
165
+ }
166
+
167
+ /** --page/--line: exactly one quantity on that line of that page (pages split on a form feed). */
168
+ export function locatePageLine(text, { page, line, sourceId }) {
169
+ requireSource(sourceId)
170
+ const pages = text.split("\f")
171
+ if (!Number.isInteger(page) || page < 1 || page > pages.length) return notLocated()
172
+ const lines = pages[page - 1].split("\n")
173
+ if (!Number.isInteger(line) || line < 1 || line > lines.length) return notLocated()
174
+ let start = pages.slice(0, page - 1).reduce((sum, body) => sum + body.length + 1, 0)
175
+ for (const body of lines.slice(0, line - 1)) start += body.length + 1
176
+ const holder = { start, end: start + lines[line - 1].length, text: lines[line - 1] }
177
+ const numerals = []
178
+ for (let numeral = parseNumeral(holder.text); numeral; numeral = parseNumeral(holder.text, numeral.span[1])) numerals.push(numeral)
179
+ const sentences = indexSentences(text, sourceId)
180
+ if (numerals.length !== 1) return numerals.length === 0 ? notLocated() : ambiguous(numerals.map((numeral) => candidate(text, sentences, start + numeral.span[0], start + numeral.span[1])))
181
+ holder.sid = sentenceAt(sentences, start + numerals[0].span[0])?.sid ?? null
182
+ return quantityResult(text, sourceId, holder, numerals[0])
183
+ }
184
+
185
+ /** Token-aligned windows of `sentence` whose similarity to `match` reaches the fuzzy floor; best first. */
186
+ function fuzzyWindow(sentence, match) {
187
+ const target = normalizeText(match).length
188
+ const wanted = new Set(tokenize(match))
189
+ const present = new Set(tokenize(sentence.text))
190
+ if (wanted.size === 0 || [...wanted].filter((token) => present.has(token)).length / wanted.size < 0.5) return null
191
+ const bounds = [...sentence.text.matchAll(/[\p{L}\p{N}]+/gu)].map((m) => [m.index, m.index + m[0].length])
192
+ let best = null
193
+ for (let i = 0; i < bounds.length; i++) {
194
+ for (let j = i; j < bounds.length; j++) {
195
+ const window = sentence.text.slice(bounds[i][0], bounds[j][1])
196
+ if (window.length > target * 1.06 + 2) break
197
+ if (window.length < target * 0.94 - 2) continue
198
+ const score = similarity(window, match)
199
+ if (score >= FUZZY_FLOOR && (!best || score > best.score)) best = { score, start: sentence.start + bounds[i][0], end: sentence.start + bounds[j][1] }
200
+ }
201
+ }
202
+ return best
203
+ }
204
+
205
+ function quoteResult(text, sourceId, sentences, start, end, fuzzy) {
206
+ const first = sentenceAt(sentences, start)
207
+ const last = sentenceAt(sentences, end - 1) ?? first
208
+ return {
209
+ value: null,
210
+ unit: null,
211
+ raw: text.slice(start, end),
212
+ quote: text.slice(first.start, last.end),
213
+ value_span: null,
214
+ sentence_id: first.sid,
215
+ qualifier: null,
216
+ fuzzy,
217
+ locator: locatorFor(text, sourceId, first.start, last.end),
218
+ }
219
+ }
220
+
221
+ /** --match: a normalized exact match (fuzzy false), else the one window with similarity >= 0.95 (fuzzy true). */
222
+ export function locateMatch(text, { match, sourceId }) {
223
+ requireSource(sourceId)
224
+ const sentences = indexSentences(text, sourceId)
225
+ const hits = findAll(text, match)
226
+ if (hits.length > 1) return ambiguous(hits.map((hit) => candidate(text, sentences, hit.start, hit.end)))
227
+ if (hits.length === 1) return quoteResult(text, sourceId, sentences, hits[0].start, hits[0].end, false)
228
+ const windows = sentences.map((sentence) => fuzzyWindow(sentence, match)).filter(Boolean)
229
+ if (windows.length > 1) return ambiguous(windows.map((window) => candidate(text, sentences, window.start, window.end)))
230
+ return windows.length === 1 ? quoteResult(text, sourceId, sentences, windows[0].start, windows[0].end, true) : notLocated()
231
+ }
@@ -0,0 +1,118 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { readFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+
5
+ import { locateFind, locateMatch, locatePageLine, locateTable } from "./fact-locate.mjs"
6
+ import { snapshotFromHtml } from "./source-snapshot.mjs"
7
+
8
+ const FIXTURES = join(import.meta.dir, "tests", "fixtures", "fidelity", "sources")
9
+ const S4_HTML = readFileSync(join(FIXTURES, "S4.html"), "utf8")
10
+ const S4_TEXT = snapshotFromHtml(S4_HTML)
11
+ const S9_TEXT = readFileSync(join(FIXTURES, "S9.txt"), "utf8")
12
+ const MAX = "\uCD5C\uB300"
13
+
14
+ type Located = {
15
+ value: string | null
16
+ unit: string | null
17
+ raw: string
18
+ quote: string
19
+ value_span: [number, number] | null
20
+ sentence_id: string
21
+ qualifier: string | null
22
+ fuzzy?: boolean
23
+ locator: { artifact: string; char_start: number; char_end: number; page?: number; line: number; table?: string; row_label?: string; col_label?: string }
24
+ }
25
+
26
+ function expectAnchored(text: string, result: Located): void {
27
+ expect(text.slice(result.locator.char_start, result.locator.char_end)).toBe(result.quote)
28
+ if (result.value_span) expect(result.quote.slice(result.value_span[0], result.value_span[1])).toBe(result.raw)
29
+ }
30
+
31
+ describe("locateFind", () => {
32
+ test("#given an anchor followed by a quantity #when located #then the value, unit, sentence and locator bind to the snapshot", () => {
33
+ const result = locateFind(S4_TEXT, { find: "Span length", sourceId: "S4" })
34
+ expect(result).toMatchObject({ value: "320", unit: "m", raw: "320 m", quote: "Span length: 320 m", qualifier: null })
35
+ expect(result.sentence_id).toMatch(/^S4:s\d+$/)
36
+ expect(result.locator.artifact).toBe("S4/text.txt")
37
+ expectAnchored(S4_TEXT, result)
38
+ })
39
+
40
+ test("#given an expected unit that differs #when located #then unit_expected_mismatch", () => {
41
+ expect(locateFind(S4_TEXT, { find: "Span length", unitExpected: "mm", sourceId: "S4" })).toMatchObject({ error: "unit_expected_mismatch" })
42
+ expect(locateFind(S4_TEXT, { find: "Span length", unitExpected: "meters", sourceId: "S4" })).toMatchObject({ value: "320" })
43
+ })
44
+
45
+ test("#given a Korean anchor before a scaled amount #when located #then the first numeral after the anchor is the value and the anchor is the qualifier", () => {
46
+ const result = locateFind(S9_TEXT, { find: MAX, sourceId: "S9" })
47
+ expect(result).toMatchObject({ value: "4200000000000", unit: "KRW", qualifier: MAX, sentence_id: "S9:s1" })
48
+ expectAnchored(S9_TEXT, result)
49
+ })
50
+
51
+ test("#given an English bounding phrase before the value #when located #then it is the qualifier", () => {
52
+ const text = "The contract costs up to 40 million USD in total."
53
+ expect(locateFind(text, { find: "costs", sourceId: "S3" })).toMatchObject({ value: "40000000", unit: "USD", qualifier: "up to" })
54
+ })
55
+
56
+ test("#given an anchor that occurs twice #when located #then fact_ambiguous lists both candidates", () => {
57
+ const result = locateFind(S4_TEXT, { find: "span cost", sourceId: "S4" })
58
+ expect(result.error).toBe("fact_ambiguous")
59
+ expect(result.candidates).toHaveLength(2)
60
+ })
61
+
62
+ test("#given a missing anchor, or no numeral within the anchor's sentence #when located #then fact_not_located", () => {
63
+ expect(locateFind(S4_TEXT, { find: "nonexistent", sourceId: "S4" })).toMatchObject({ error: "fact_not_located", candidates: [] })
64
+ expect(locateFind("Total reached.\nIt was 12 m.", { find: "Total", sourceId: "S1" })).toMatchObject({ error: "fact_not_located" })
65
+ expect(locateFind(`Delta ${"x".repeat(90)} 7 m`, { find: "Delta", sourceId: "S1" })).toMatchObject({ error: "fact_not_located" })
66
+ })
67
+ })
68
+
69
+ describe("locateTable", () => {
70
+ test("#given a table found by a header label #when the row and column labels match #then the cell value binds to its text.txt line", () => {
71
+ const result = locateTable(S4_HTML, { table: "Length", row: "Span", col: "Length", sourceId: "S4" })
72
+ expect(result).toMatchObject({ value: "320", unit: "m", quote: "320 m", locator: { table: "Length", row_label: "Span", col_label: "Length" } })
73
+ expect(result.sentence_id).toMatch(/^S4:s\d+$/)
74
+ expectAnchored(S4_TEXT, result)
75
+ expect(locateTable(S4_HTML, { table: "dimensions", row: "Approach", col: "Length", sourceId: "S4" })).toMatchObject({ value: "85" })
76
+ })
77
+
78
+ test("#given an unknown table, row or column, or a duplicated row label #when located #then it fails without guessing", () => {
79
+ expect(locateTable(S4_HTML, { table: "Width", row: "Span", col: "Length", sourceId: "S4" })).toMatchObject({ error: "fact_not_located" })
80
+ expect(locateTable(S4_HTML, { table: "Length", row: "Deck", col: "Length", sourceId: "S4" })).toMatchObject({ error: "fact_not_located" })
81
+ const twice = "<table><tr><th>Item</th><th>Cost</th></tr><tr><td>Span</td><td>3</td></tr><tr><td>Span</td><td>4</td></tr></table>"
82
+ expect(locateTable(twice, { table: "Cost", row: "Span", col: "Cost", sourceId: "S5" })).toMatchObject({ error: "fact_ambiguous" })
83
+ })
84
+ })
85
+
86
+ describe("locatePageLine", () => {
87
+ const text = "Intro line\nRevenue: 15 %\fCost: 3 m\nBetween 1 m and 2 m"
88
+
89
+ test("#given a page and line holding one quantity #when located #then it binds with its page and line", () => {
90
+ const result = locatePageLine(text, { page: 1, line: 2, sourceId: "S6" })
91
+ expect(result).toMatchObject({ value: "15", unit: "%", quote: "Revenue: 15 %", locator: { line: 2 } })
92
+ expectAnchored(text, result)
93
+ expect(locatePageLine(text, { page: 2, line: 1, sourceId: "S6" })).toMatchObject({ value: "3", locator: { page: 2, line: 1 } })
94
+ })
95
+
96
+ test("#given a line with two quantities or a missing page #when located #then ambiguous or not located", () => {
97
+ expect(locatePageLine(text, { page: 2, line: 2, sourceId: "S6" })).toMatchObject({ error: "fact_ambiguous" })
98
+ expect(locatePageLine(text, { page: 3, line: 1, sourceId: "S6" })).toMatchObject({ error: "fact_not_located" })
99
+ })
100
+ })
101
+
102
+ describe("locateMatch", () => {
103
+ test("#given an exact match #when located #then raw is the snapshot bytes, the quote is the whole sentence, and there is no value span", () => {
104
+ const result = locateMatch(S4_TEXT, { match: "no earlier than 2027", sourceId: "S4" })
105
+ expect(result).toMatchObject({ raw: "no earlier than 2027", quote: "After the delay, we estimate completion no earlier than 2027.", value_span: null, value: null, fuzzy: false })
106
+ expectAnchored(S4_TEXT, result)
107
+ })
108
+
109
+ test("#given a near match at or above 0.95 similarity #when located #then the window returns with fuzzy true; below it, fact_not_located", () => {
110
+ const near = locateMatch(S4_TEXT, { match: "we estimate completion no earlier then 2027", sourceId: "S4" })
111
+ expect(near).toMatchObject({ raw: "we estimate completion no earlier than 2027", fuzzy: true })
112
+ expect(locateMatch(S4_TEXT, { match: "completion is certain by 2026", sourceId: "S4" })).toMatchObject({ error: "fact_not_located" })
113
+ })
114
+
115
+ test("#given a phrase that occurs twice #when located #then fact_ambiguous", () => {
116
+ expect(locateMatch(S4_TEXT, { match: "span cost", sourceId: "S4" })).toMatchObject({ error: "fact_ambiguous" })
117
+ })
118
+ })
@@ -0,0 +1,187 @@
1
+ // fact-pairs.mjs - (source, report sentence) pairs for the meaning verifier.
2
+ //
3
+ // extractPairs walks the prose sentences of a filled HTML report and emits, in document order:
4
+ // a quote pair per distinct ledger quote cited by a findings sentence (facts sharing one quote
5
+ // share one pair), a claim pair per sentence bound only by data-claim (the statement comes from
6
+ // the claim index), and a translation pair per data-translates span. force_strong routes a pair
7
+ // to the strong reader when the lexicon raised name_outside_quote or scope_widened_warn, when the
8
+ // hedge-position heuristic fired, or when the source hedges and the sentence's last six tokens do
9
+ // not. No model, no IO: the readers run as separate lanes. Plain Node ESM; no Hangul here.
10
+
11
+ import { isExemptKind, proseSentences } from "./fidelity-binding.mjs"
12
+ import { isKoreanTerm, lexicalPattern, lexiconChecks } from "./fidelity-lexicon.mjs"
13
+ import { sentenceKey } from "./fidelity-state.mjs"
14
+ import { buildContext } from "./gates-static.mjs"
15
+ import { parseHtml, proseText } from "./html-lite.mjs"
16
+ import { HEDGES } from "./lexicon.mjs"
17
+ import { normalizeText } from "./text-normalize.mjs"
18
+
19
+ /** Every pair kind the producers below emit; consumers count and schema pairs by this list. */
20
+ export const PAIR_KINDS = Object.freeze(["quote", "claim", "translation"])
21
+ const TAIL = 6
22
+
23
+ const lang = (text) => (isKoreanTerm(text) ? "ko" : "en")
24
+ /** Sentence text with its fact spans cut out, so a Korean value does not make an English sentence Korean. */
25
+ const outsideFacts = (sentence) => sentence.facts.reduceRight((text, fact) => text.slice(0, fact.start - sentence.start) + text.slice(fact.end - sentence.start), sentence.text)
26
+
27
+ /** Latin hedges match whole words ("exact"); Korean hedges match as substrings, one syllable as a token prefix ("stem"). */
28
+ const HEDGE_RES = HEDGES.map((term) => lexicalPattern(term, isKoreanTerm(term) ? "stem" : "exact"))
29
+
30
+ /** Hedge terms that are also month names; capitalized and followed by a date number, they are months. */
31
+ const MONTH_WORDS = new Set(["may"])
32
+
33
+ /** A capitalized Latin match after the first word is a name or a month ("in May"), not a hedge. */
34
+ const isHedgeAt = (text, match) => {
35
+ if (!/^\p{Lu}/u.test(match[0]) || isKoreanTerm(match[0])) return true
36
+ if (match.index !== 0) return false
37
+ // At the start of a text a capital is only sentence case, so the word still hedges - unless it is a
38
+ // month name followed by a date number ("May 2024 sales rose.").
39
+ return !(MONTH_WORDS.has(match[0].toLowerCase()) && /^\s+\p{Nd}/u.test(text.slice(match.index + match[0].length)))
40
+ }
41
+
42
+ export const hasHedge = (text) => {
43
+ const normal = normalizeText(text)
44
+ return HEDGE_RES.some((re) => [...normal.matchAll(re)].some((match) => isHedgeAt(normal, match)))
45
+ }
46
+
47
+ /** The source hedges and none of the sentence's last six tokens carries a hedge. */
48
+ function tailDropsHedge(source, sentence) {
49
+ if (!hasHedge(source)) return false
50
+ return !hasHedge(normalizeText(sentence).split(/\s+/u).filter(Boolean).slice(-TAIL).join(" "))
51
+ }
52
+
53
+ function slugOf(scope) {
54
+ const heading = scope.section?.heading
55
+ if (!heading) return ""
56
+ return (heading.attrs.id ?? proseText(heading))
57
+ .toLowerCase()
58
+ .replace(/[^\p{L}\p{N}]+/gu, "-")
59
+ .replace(/^-+|-+$/gu, "")
60
+ }
61
+
62
+ function claimMap(claims) {
63
+ const list = Array.isArray(claims) ? claims : (claims?.claims ?? [])
64
+ return new Map(list.map((claim) => [claim.claim_id, claim]))
65
+ }
66
+
67
+ const idsOf = (attr) => String(attr ?? "").split(/[\s,]+/u).filter(Boolean)
68
+
69
+ /** Claim ids that bind a sentence: on the block or an ancestor, or on an inner element whose text the sentence holds. */
70
+ function claimIdsFor(ctx, block, sentence) {
71
+ const { closest, descendants } = ctx.util
72
+ const up = block.owner.attrs["data-claim"] !== undefined ? block.owner : closest(block.owner, (node) => node.attrs?.["data-claim"] !== undefined)
73
+ const ids = up ? idsOf(up.attrs["data-claim"]) : []
74
+ const text = normalizeText(sentence.text)
75
+ for (const node of descendants(block.owner)) {
76
+ if (node.attrs?.["data-claim"] === undefined) continue
77
+ const inner = normalizeText(proseText(node))
78
+ if (inner !== "" && text.includes(inner)) ids.push(...idsOf(node.attrs["data-claim"]))
79
+ }
80
+ return [...new Set(ids)]
81
+ }
82
+
83
+ function exempt(ctx, node) {
84
+ return isExemptKind(node.attrs["data-kind"]) || Boolean(ctx.util.closest(node, (cur) => isExemptKind(cur.attrs?.["data-kind"])))
85
+ }
86
+
87
+ /** Lexicon routing for every fact span of one quote group in one sentence. */
88
+ function lexiconRouted(group, sentence) {
89
+ const probes = group.facts.map((fact, index) => ({
90
+ pair_id: String(index),
91
+ quote: group.quote,
92
+ sentence: sentence.text,
93
+ span: [fact.start - sentence.start, fact.end - sentence.start],
94
+ qualifier: group.row.kind === "quote" ? null : (group.rows.get(fact.id)?.qualifier ?? null),
95
+ }))
96
+ return lexiconChecks(probes).forceStrong.length > 0
97
+ }
98
+
99
+ function quotePairs(sentence, rows, filled, slug, reportLang) {
100
+ const groups = new Map()
101
+ for (const fact of sentence.facts) {
102
+ const row = rows.get(fact.id)
103
+ if (!row?.quote || (filled && !filled.has(fact.id))) continue
104
+ const group = groups.get(row.quote) ?? { quote: row.quote, row, rows, facts: [], ids: [] }
105
+ group.facts.push(fact)
106
+ if (!group.ids.includes(fact.id)) group.ids.push(fact.id)
107
+ groups.set(row.quote, group)
108
+ }
109
+ const allIds = [...new Set(sentence.facts.map((fact) => fact.id))]
110
+ return [...groups.values()].map((group) => ({
111
+ kind: "quote",
112
+ sentence_key: sentenceKey(slug, allIds),
113
+ fact_ids: [...group.ids].sort(),
114
+ quote: group.quote,
115
+ sentence: sentence.text,
116
+ source_lang: lang(group.quote),
117
+ report_lang: reportLang ?? lang(outsideFacts(sentence)),
118
+ force_strong: lexiconRouted(group, sentence) || tailDropsHedge(group.quote, sentence.text),
119
+ }))
120
+ }
121
+
122
+ function claimPairs(ctx, block, sentence, byClaim, slug, reportLang) {
123
+ if (sentence.facts.length > 0 || exempt(ctx, block.owner)) return []
124
+ return claimIdsFor(ctx, block, sentence)
125
+ .map((id) => byClaim.get(id))
126
+ .filter(Boolean)
127
+ .map((claim) => ({
128
+ kind: "claim",
129
+ sentence_key: sentenceKey(slug, [claim.claim_id]),
130
+ claim_id: claim.claim_id,
131
+ statement: claim.statement,
132
+ sentence: sentence.text,
133
+ source_lang: lang(claim.statement),
134
+ report_lang: reportLang ?? lang(sentence.text),
135
+ force_strong: lexiconChecks([{ pair_id: "c", quote: claim.statement, sentence: sentence.text }]).forceStrong.length > 0 || tailDropsHedge(claim.statement, sentence.text),
136
+ }))
137
+ }
138
+
139
+ function translationPair(node, rows, filled, slug, reportLang) {
140
+ const id = node.attrs["data-translates"]
141
+ const row = rows.get(id)
142
+ const text = proseText(node)
143
+ if (!row?.quote || text === "" || (filled && !filled.has(id))) return null
144
+ return {
145
+ kind: "translation",
146
+ sentence_key: sentenceKey(slug, [id]),
147
+ fact_ids: [id],
148
+ quote: row.quote,
149
+ sentence: text,
150
+ source_lang: lang(row.quote),
151
+ report_lang: reportLang ?? lang(text),
152
+ force_strong: tailDropsHedge(row.quote, text),
153
+ }
154
+ }
155
+
156
+ /**
157
+ * @param {string} html the filled report
158
+ * @param {{ rows: object[] }} ledger
159
+ * @param {{ claims: object[] } | object[] | null} claims indexClaims(...) output or its claims array
160
+ * @param {{ fact_id: string }[]} [fillMap] fact fill's map; when given, only facts it filled are paired
161
+ * @returns {object[]} pairs with pair_id P1, P2, ... in document order
162
+ */
163
+ export function extractPairs(html, ledger, claims, fillMap) {
164
+ const ctx = buildContext(parseHtml(html), {})
165
+ const rows = new Map((ledger?.rows ?? []).map((row) => [row.fact_id, row]))
166
+ const filled = Array.isArray(fillMap) ? new Set(fillMap.map((entry) => entry.fact_id)) : null
167
+ const byClaim = claimMap(claims)
168
+ const htmlLang = ctx.all.find((node) => node.tag === "html")?.attrs.lang
169
+ const reportLang = htmlLang ? htmlLang.toLowerCase().split("-")[0] : null
170
+ const out = []
171
+ const seenSpans = new Set()
172
+ for (const block of proseSentences(ctx)) {
173
+ const findings = Boolean(block.scope.section) && block.kind === null
174
+ if (!findings) continue
175
+ const slug = slugOf(block.scope)
176
+ for (const sentence of block.sentences) {
177
+ out.push(...quotePairs(sentence, rows, filled, slug, reportLang), ...claimPairs(ctx, block, sentence, byClaim, slug, reportLang))
178
+ }
179
+ for (const node of ctx.util.descendants(block.owner)) {
180
+ if (node.attrs?.["data-translates"] === undefined || seenSpans.has(node)) continue
181
+ seenSpans.add(node)
182
+ const pair = translationPair(node, rows, filled, slug, reportLang)
183
+ if (pair) out.push(pair)
184
+ }
185
+ }
186
+ return out.map((pair, index) => ({ pair_id: `P${index + 1}`, ...pair }))
187
+ }