oh-my-opencode 5.1.47 → 5.1.51

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 (497) hide show
  1. package/dist/agents/refusal-rule.d.ts +7 -0
  2. package/dist/cli/index.js +1194 -752
  3. package/dist/cli/service-auth/accounts-gate.d.ts +11 -0
  4. package/dist/cli/service-auth/commands.d.ts +2 -4
  5. package/dist/cli/service-auth/index.d.ts +1 -1
  6. package/dist/cli/service-auth/keystore.d.ts +26 -2
  7. package/dist/cli/service-auth/protocol.d.ts +7 -1
  8. package/dist/cli/service-auth/session.d.ts +6 -0
  9. package/dist/cli/service-auth/signed-out-marker.d.ts +11 -0
  10. package/dist/cli-node/index.js +1194 -752
  11. package/dist/config/schema/hooks.d.ts +1 -0
  12. package/dist/create-hooks.d.ts +1 -0
  13. package/dist/features/boulder-state/storage.d.ts +1 -1
  14. package/dist/features/boulder-state/types.d.ts +1 -1
  15. package/dist/hooks/atlas/idle-continuation.d.ts +2 -1
  16. package/dist/hooks/atlas/types.d.ts +9 -1
  17. package/dist/hooks/index.d.ts +1 -0
  18. package/dist/hooks/momus-review-persistence/hook.d.ts +19 -0
  19. package/dist/hooks/momus-review-persistence/index.d.ts +1 -0
  20. package/dist/hooks/momus-review-persistence/review-resolution.d.ts +20 -0
  21. package/dist/hooks/prometheus-md-only/constants.d.ts +2 -0
  22. package/dist/hooks/prometheus-md-only/fixtures/denial-log.d.ts +1 -0
  23. package/dist/hooks/prometheus-md-only/fixtures/guarded-hashline.d.ts +1 -0
  24. package/dist/hooks/prometheus-md-only/fixtures/invoke-before.d.ts +7 -0
  25. package/dist/hooks/prometheus-md-only/fixtures/opencode-patch-parser.d.ts +2 -0
  26. package/dist/hooks/prometheus-md-only/hook.d.ts +5 -1
  27. package/dist/hooks/prometheus-md-only/path-policy.d.ts +2 -9
  28. package/dist/hooks/prometheus-md-only/vendor/opencode-patch-parser.d.ts +22 -0
  29. package/dist/index.js +2322 -1045
  30. package/dist/plugin/hooks/create-core-hooks.d.ts +1 -0
  31. package/dist/plugin/hooks/create-tool-guard-hooks.d.ts +2 -1
  32. package/dist/shared/posthog-activity-state.d.ts +1 -0
  33. package/dist/shared/posthog.d.ts +1 -2
  34. package/dist/shared/resolve-file-write-path.d.ts +6 -0
  35. package/dist/skills/ast-grep/README.md +1 -1
  36. package/dist/skills/ast-grep/install.ps1 +5 -3
  37. package/dist/skills/ast-grep/install.sh +4 -5
  38. package/dist/skills/ast-grep/references/install.md +2 -2
  39. package/dist/skills/ast-grep/scripts/ast_grep_helper.py +13 -3
  40. package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
  41. package/dist/skills/coding-agent-sessions/AGENTS.md +5 -4
  42. package/dist/skills/coding-agent-sessions/SKILL.md +11 -11
  43. package/dist/skills/coding-agent-sessions/references/all-platforms.md +2 -2
  44. package/dist/skills/coding-agent-sessions/references/codex.md +4 -4
  45. package/dist/skills/coding-agent-sessions/scripts/agent_sessions/cli.py +1 -1
  46. package/dist/skills/coding-agent-sessions/scripts/find-agent-sessions.py +6 -3
  47. package/dist/skills/frontend/ATTRIBUTION.md +1 -1
  48. package/dist/skills/frontend/SKILL.md +34 -12
  49. package/dist/skills/frontend/references/design/README.md +2 -2
  50. package/dist/skills/frontend/references/design/_INDEX.md +2 -1
  51. package/dist/skills/frontend/references/design/component-catalogs.md +14 -10
  52. package/dist/skills/frontend/references/design/design-system-architecture.md +19 -6
  53. package/dist/skills/frontend/references/design/grounding.md +29 -0
  54. package/dist/skills/frontend/references/ui-ux-db/scripts/design_system.py +2 -0
  55. package/dist/skills/frontend/references/ui-ux-db/scripts/search.py +2 -0
  56. package/dist/skills/ultimate-browsing/SKILL.md +5 -4
  57. package/dist/skills/ultimate-browsing/engine/AGENTS.md +2 -2
  58. package/dist/skills/ultimate-browsing/engine/__main__.py +9 -6
  59. package/dist/skills/ultimate-browsing/engine/summary.py +1 -1
  60. package/dist/skills/ultimate-browsing/references/insane-search/README.md +4 -4
  61. package/dist/skills/ultimate-browsing/scripts/extract_cookies.py +4 -2
  62. package/dist/skills/ulw-research/AGENTS.md +8 -4
  63. package/dist/skills/ulw-research/SKILL.md +7 -259
  64. package/dist/skills/ulw-research/references/deliverable-phase.md +68 -24
  65. package/dist/skills/ulw-research/references/fact-ledger-grammar.md +7 -3
  66. package/dist/skills/ulw-research/references/fact-ledger.md +42 -8
  67. package/dist/skills/ulw-research/references/fidelity-claim-kinds.md +1 -1
  68. package/dist/skills/ulw-research/references/markdown-blocks.md +12 -0
  69. package/dist/skills/ulw-research/references/report-gates.md +14 -6
  70. package/dist/skills/ulw-research/references/research-collection.md +195 -0
  71. package/dist/skills/ulw-research/references/research-delivery.md +123 -0
  72. package/dist/skills/ulw-research/scripts/claim-kinds.mjs +33 -5
  73. package/dist/skills/ulw-research/scripts/claim-kinds.test.ts +37 -2
  74. package/dist/skills/ulw-research/scripts/claim-prefix.test.ts +76 -0
  75. package/dist/skills/ulw-research/scripts/contracts-validation.test.ts +83 -0
  76. package/dist/skills/ulw-research/scripts/contracts.mjs +16 -15
  77. package/dist/skills/ulw-research/scripts/contracts.test.ts +6 -109
  78. package/dist/skills/ulw-research/scripts/conversion-altered.test.ts +154 -0
  79. package/dist/skills/ulw-research/scripts/conversion-originals.test.ts +297 -0
  80. package/dist/skills/ulw-research/scripts/conversion-probe-classes.test.ts +361 -0
  81. package/dist/skills/ulw-research/scripts/conversion-probe.mjs +229 -0
  82. package/dist/skills/ulw-research/scripts/conversion-probe.test.ts +218 -0
  83. package/dist/skills/ulw-research/scripts/copy-returns-fidelity-policy.test.ts +156 -0
  84. package/dist/skills/ulw-research/scripts/copy-returns-polish.test.ts +10 -9
  85. package/dist/skills/ulw-research/scripts/css-hiding.mjs +158 -0
  86. package/dist/skills/ulw-research/scripts/css-lite.mjs +4 -2
  87. package/dist/skills/ulw-research/scripts/delivery-barrier-meaning.test.ts +186 -0
  88. package/dist/skills/ulw-research/scripts/delivery-barrier.test.ts +40 -87
  89. package/dist/skills/ulw-research/scripts/entities.mjs +13 -4
  90. package/dist/skills/ulw-research/scripts/fact-commands-fill.mjs +96 -0
  91. package/dist/skills/ulw-research/scripts/fact-commands-meaning.mjs +26 -30
  92. package/dist/skills/ulw-research/scripts/fact-commands-polish.mjs +46 -19
  93. package/dist/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -6
  94. package/dist/skills/ulw-research/scripts/fact-commands.mjs +40 -62
  95. package/dist/skills/ulw-research/scripts/fact-derive-2.test.ts +149 -0
  96. package/dist/skills/ulw-research/scripts/fact-derive.mjs +7 -2
  97. package/dist/skills/ulw-research/scripts/fact-derive.test.ts +7 -192
  98. package/dist/skills/ulw-research/scripts/fact-fill.mjs +9 -6
  99. package/dist/skills/ulw-research/scripts/fact-ledger-kinds.mjs +6 -7
  100. package/dist/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +2 -1
  101. package/dist/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +2 -1
  102. package/dist/skills/ulw-research/scripts/fact-locate.mjs +15 -9
  103. package/dist/skills/ulw-research/scripts/fact-unit-dimensions-2.test.ts +149 -0
  104. package/dist/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +4 -168
  105. package/dist/skills/ulw-research/scripts/fact-unit-rules.test.ts +3 -229
  106. package/dist/skills/ulw-research/scripts/fidelity-claims.mjs +4 -3
  107. package/dist/skills/ulw-research/scripts/fidelity-csv.mjs +32 -0
  108. package/dist/skills/ulw-research/scripts/fidelity-lexicon.mjs +1 -1
  109. package/dist/skills/ulw-research/scripts/fidelity-originals.mjs +161 -0
  110. package/dist/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  111. package/dist/skills/ulw-research/scripts/fidelity-records.mjs +81 -14
  112. package/dist/skills/ulw-research/scripts/fidelity-snapshot-readability.mjs +49 -0
  113. package/dist/skills/ulw-research/scripts/fidelity-sources.mjs +76 -0
  114. package/dist/skills/ulw-research/scripts/fidelity-trace.mjs +25 -11
  115. package/dist/skills/ulw-research/scripts/fidelity-value-unit.mjs +202 -0
  116. package/dist/skills/ulw-research/scripts/fidelity-values.mjs +40 -39
  117. package/dist/skills/ulw-research/scripts/fidelity-values.test.ts +8 -29
  118. package/dist/skills/ulw-research/scripts/gates-fidelity.mjs +8 -6
  119. package/dist/skills/ulw-research/scripts/gates-static-headings.test.ts +17 -0
  120. package/dist/skills/ulw-research/scripts/gates-static.mjs +10 -1
  121. package/dist/skills/ulw-research/scripts/html-lite.mjs +6 -2
  122. package/dist/skills/ulw-research/scripts/html-markup.mjs +2 -2
  123. package/dist/skills/ulw-research/scripts/html-text-map.mjs +140 -0
  124. package/dist/skills/ulw-research/scripts/html-text-map.test.ts +148 -0
  125. package/dist/skills/ulw-research/scripts/markdown-closers.mjs +126 -0
  126. package/dist/skills/ulw-research/scripts/md-lite-boundaries.test.ts +46 -0
  127. package/dist/skills/ulw-research/scripts/md-lite.mjs +1 -1
  128. package/dist/skills/ulw-research/scripts/number-grammar.mjs +33 -0
  129. package/dist/skills/ulw-research/scripts/number-grammar.test.ts +36 -0
  130. package/dist/skills/ulw-research/scripts/numerals.mjs +18 -7
  131. package/dist/skills/ulw-research/scripts/numerals.test.ts +3 -0
  132. package/dist/skills/ulw-research/scripts/outcome-start.mjs +66 -0
  133. package/dist/skills/ulw-research/scripts/outcome-start.test.ts +109 -0
  134. package/dist/skills/ulw-research/scripts/outcome.mjs +11 -22
  135. package/dist/skills/ulw-research/scripts/page-source-locator.test.ts +187 -0
  136. package/dist/skills/ulw-research/scripts/placeholder.mjs +18 -6
  137. package/dist/skills/ulw-research/scripts/polish-apply.mjs +3 -2
  138. package/dist/skills/ulw-research/scripts/polish-markup.test.ts +15 -1
  139. package/dist/skills/ulw-research/scripts/polish-partial.test.ts +42 -0
  140. package/dist/skills/ulw-research/scripts/raw-decode.mjs +110 -0
  141. package/dist/skills/ulw-research/scripts/raw-locator.mjs +47 -0
  142. package/dist/skills/ulw-research/scripts/raw-snapshot.test.ts +139 -0
  143. package/dist/skills/ulw-research/scripts/report-tools-2.test.ts +233 -0
  144. package/dist/skills/ulw-research/scripts/report-tools-3.test.ts +141 -0
  145. package/dist/skills/ulw-research/scripts/report-tools-4.test.ts +135 -0
  146. package/dist/skills/ulw-research/scripts/report-tools-5.test.ts +231 -0
  147. package/dist/skills/ulw-research/scripts/report-tools-commands.mjs +1 -1
  148. package/dist/skills/ulw-research/scripts/report-tools.mjs +7 -1
  149. package/dist/skills/ulw-research/scripts/report-tools.test.ts +6 -560
  150. package/dist/skills/ulw-research/scripts/snapshot-readability.test.ts +116 -0
  151. package/dist/skills/ulw-research/scripts/source-commands.mjs +87 -29
  152. package/dist/skills/ulw-research/scripts/source-integrity.test.ts +243 -0
  153. package/dist/skills/ulw-research/scripts/source-refresh.mjs +82 -0
  154. package/dist/skills/ulw-research/scripts/source-snapshot.mjs +73 -69
  155. package/dist/skills/ulw-research/scripts/source-snapshot.test.ts +7 -4
  156. package/dist/skills/ulw-research/scripts/source-view.mjs +89 -0
  157. package/dist/skills/ulw-research/scripts/sources-citation.mjs +203 -0
  158. package/dist/skills/ulw-research/scripts/sources-citation.test.ts +236 -0
  159. package/dist/skills/ulw-research/scripts/tests/contracts-support.ts +147 -0
  160. package/dist/skills/ulw-research/scripts/tests/delivery-barrier-session.ts +59 -0
  161. package/dist/skills/ulw-research/scripts/tests/derive-clock.ts +4 -0
  162. package/dist/skills/ulw-research/scripts/tests/fact-derive-support.ts +65 -0
  163. package/dist/skills/ulw-research/scripts/tests/fact-unit-dimensions-support.ts +41 -0
  164. package/dist/skills/ulw-research/scripts/tests/fact-unit-rules-support.ts +241 -0
  165. package/dist/skills/ulw-research/scripts/tests/fidelity-values-support.ts +30 -0
  166. package/dist/skills/ulw-research/scripts/tests/fixtures/conversion-probe.html +14 -0
  167. package/dist/skills/ulw-research/scripts/tests/fixtures/sources-shapes.html +20 -0
  168. package/dist/skills/ulw-research/scripts/tests/numeral-classes.ts +196 -0
  169. package/dist/skills/ulw-research/scripts/tests/report-tools-support.ts +182 -0
  170. package/dist/skills/ulw-research/scripts/tests/value-mode-support.ts +48 -0
  171. package/dist/skills/ulw-research/scripts/units.mjs +14 -3
  172. package/dist/skills/ulw-research/scripts/units.test.ts +10 -0
  173. package/dist/skills/ulw-research/scripts/value-mode-unit-classes.test.ts +255 -0
  174. package/dist/skills/ulw-research/scripts/value-mode-unit.test.ts +179 -0
  175. package/dist/skills/ulw-research/scripts/value-rules-cli.test.ts +157 -0
  176. package/dist/skills/ulw-research/scripts/value-rules-followups.test.ts +72 -0
  177. package/dist/skills/ulw-research/scripts/value-rules-priority.test.ts +80 -0
  178. package/dist/tools/delegate-task/executor-types.d.ts +1 -0
  179. package/dist/tools/delegate-task/sync-session-existence.d.ts +4 -0
  180. package/dist/tools/delegate-task/types.d.ts +1 -0
  181. package/dist/tools/skill/description-formatter.d.ts +2 -1
  182. package/dist/tools/skill/types.d.ts +6 -0
  183. package/dist/tui.js +131 -33
  184. package/package.json +18 -15
  185. package/packages/git-bash-mcp/package.json +1 -1
  186. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  187. package/packages/omo-codex/plugin/README.md +1 -1
  188. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  189. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  190. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  191. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  192. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  193. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  194. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  195. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  196. package/packages/omo-codex/plugin/components/lcx/skills/lcx-contribute-bug-fix/SKILL.md +51 -55
  197. package/packages/omo-codex/plugin/components/lcx/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  198. package/packages/omo-codex/plugin/components/lcx/skills/lcx-report-bug/SKILL.md +7 -7
  199. package/packages/omo-codex/plugin/components/lcx/skills/lcx-report-bug/agents/openai.yaml +1 -1
  200. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  201. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  202. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  203. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  204. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  205. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  206. package/packages/omo-codex/plugin/components/teammode/package.json +2 -2
  207. package/packages/omo-codex/plugin/components/telemetry/README.md +2 -2
  208. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +52 -15
  209. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +52 -19
  210. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  211. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  212. package/packages/omo-codex/plugin/components/telemetry/src/codex-hook.ts +2 -9
  213. package/packages/omo-codex/plugin/components/telemetry/src/posthog.ts +4 -13
  214. package/packages/omo-codex/plugin/components/telemetry/src/product-identity.ts +0 -2
  215. package/packages/omo-codex/plugin/components/telemetry/test/codex-hook-diagnostics.test.ts +0 -3
  216. package/packages/omo-codex/plugin/components/telemetry/test/codex-hook.test.ts +3 -8
  217. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +3 -0
  218. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  219. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  220. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  221. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  222. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  223. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +3 -0
  224. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  225. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  226. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  227. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  228. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  229. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  230. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  231. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  232. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  233. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  234. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  235. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  236. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  237. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  238. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  239. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  240. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  241. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  242. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  243. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  244. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  245. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  246. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  247. package/packages/omo-codex/plugin/package-lock.json +16 -16
  248. package/packages/omo-codex/plugin/package.json +1 -1
  249. package/packages/omo-codex/plugin/shared/package.json +1 -1
  250. package/packages/omo-codex/plugin/skills/ast-grep/README.md +1 -1
  251. package/packages/omo-codex/plugin/skills/ast-grep/install.ps1 +5 -3
  252. package/packages/omo-codex/plugin/skills/ast-grep/install.sh +4 -5
  253. package/packages/omo-codex/plugin/skills/ast-grep/references/install.md +2 -2
  254. package/packages/omo-codex/plugin/skills/ast-grep/scripts/ast_grep_helper.py +13 -3
  255. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  256. package/packages/omo-codex/plugin/skills/coding-agent-sessions/AGENTS.md +5 -4
  257. package/packages/omo-codex/plugin/skills/coding-agent-sessions/SKILL.md +11 -11
  258. package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/all-platforms.md +2 -2
  259. package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/codex.md +4 -4
  260. package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/agent_sessions/cli.py +1 -1
  261. package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/find-agent-sessions.py +6 -3
  262. package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +1 -1
  263. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +34 -12
  264. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +2 -2
  265. package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +2 -1
  266. package/packages/omo-codex/plugin/skills/frontend/references/design/component-catalogs.md +14 -10
  267. package/packages/omo-codex/plugin/skills/frontend/references/design/design-system-architecture.md +19 -6
  268. package/packages/omo-codex/plugin/skills/frontend/references/design/grounding.md +29 -0
  269. package/packages/omo-codex/plugin/skills/frontend/references/ui-ux-db/scripts/design_system.py +2 -0
  270. package/packages/omo-codex/plugin/skills/frontend/references/ui-ux-db/scripts/search.py +2 -0
  271. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +51 -55
  272. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  273. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +7 -7
  274. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  275. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +5 -4
  276. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -2
  277. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/__main__.py +9 -6
  278. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/summary.py +1 -1
  279. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +4 -4
  280. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +4 -2
  281. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +3 -0
  282. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +8 -4
  283. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +7 -259
  284. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +68 -24
  285. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +7 -3
  286. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger.md +42 -8
  287. package/packages/omo-codex/plugin/skills/ulw-research/references/fidelity-claim-kinds.md +1 -1
  288. package/packages/omo-codex/plugin/skills/ulw-research/references/markdown-blocks.md +12 -0
  289. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +14 -6
  290. package/packages/omo-codex/plugin/skills/ulw-research/references/research-collection.md +195 -0
  291. package/packages/omo-codex/plugin/skills/ulw-research/references/research-delivery.md +123 -0
  292. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claim-kinds.mjs +33 -5
  293. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +16 -15
  294. package/packages/omo-codex/plugin/skills/ulw-research/scripts/conversion-probe.mjs +229 -0
  295. package/packages/omo-codex/plugin/skills/ulw-research/scripts/css-hiding.mjs +158 -0
  296. package/packages/omo-codex/plugin/skills/ulw-research/scripts/css-lite.mjs +4 -2
  297. package/packages/omo-codex/plugin/skills/ulw-research/scripts/entities.mjs +13 -4
  298. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-fill.mjs +96 -0
  299. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +26 -30
  300. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-polish.mjs +46 -19
  301. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -6
  302. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +40 -62
  303. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-derive.mjs +7 -2
  304. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +9 -6
  305. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +6 -7
  306. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-locate.mjs +15 -9
  307. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-claims.mjs +4 -3
  308. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-csv.mjs +32 -0
  309. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-lexicon.mjs +1 -1
  310. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-originals.mjs +161 -0
  311. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  312. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-records.mjs +81 -14
  313. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-snapshot-readability.mjs +49 -0
  314. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-sources.mjs +76 -0
  315. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-trace.mjs +25 -11
  316. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-value-unit.mjs +202 -0
  317. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-values.mjs +40 -39
  318. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-fidelity.mjs +8 -6
  319. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +10 -1
  320. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +6 -2
  321. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-markup.mjs +2 -2
  322. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-text-map.mjs +140 -0
  323. package/packages/omo-codex/plugin/skills/ulw-research/scripts/markdown-closers.mjs +126 -0
  324. package/packages/omo-codex/plugin/skills/ulw-research/scripts/md-lite.mjs +1 -1
  325. package/packages/omo-codex/plugin/skills/ulw-research/scripts/number-grammar.mjs +33 -0
  326. package/packages/omo-codex/plugin/skills/ulw-research/scripts/numerals.mjs +18 -7
  327. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome-start.mjs +66 -0
  328. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +11 -22
  329. package/packages/omo-codex/plugin/skills/ulw-research/scripts/placeholder.mjs +18 -6
  330. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-apply.mjs +3 -2
  331. package/packages/omo-codex/plugin/skills/ulw-research/scripts/raw-decode.mjs +110 -0
  332. package/packages/omo-codex/plugin/skills/ulw-research/scripts/raw-locator.mjs +47 -0
  333. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +1 -1
  334. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +7 -1
  335. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +87 -29
  336. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-refresh.mjs +82 -0
  337. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-snapshot.mjs +73 -69
  338. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-view.mjs +89 -0
  339. package/packages/omo-codex/plugin/skills/ulw-research/scripts/sources-citation.mjs +203 -0
  340. package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +14 -3
  341. package/packages/omo-codex/plugin/test/payload-equivalence.test.mjs +6 -4
  342. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +4 -3
  343. package/packages/omo-codex/scripts/install-dist/install-local.mjs +338 -237
  344. package/packages/prompts-core/prompts/ultrawork/codex.md +3 -0
  345. package/packages/shared-skills/skill-source-filter.mjs +6 -2
  346. package/packages/shared-skills/skills/ast-grep/README.md +1 -1
  347. package/packages/shared-skills/skills/ast-grep/install.ps1 +5 -3
  348. package/packages/shared-skills/skills/ast-grep/install.sh +4 -5
  349. package/packages/shared-skills/skills/ast-grep/references/install.md +2 -2
  350. package/packages/shared-skills/skills/ast-grep/scripts/ast_grep_helper.py +13 -3
  351. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  352. package/packages/shared-skills/skills/coding-agent-sessions/AGENTS.md +5 -4
  353. package/packages/shared-skills/skills/coding-agent-sessions/SKILL.md +11 -11
  354. package/packages/shared-skills/skills/coding-agent-sessions/references/all-platforms.md +2 -2
  355. package/packages/shared-skills/skills/coding-agent-sessions/references/codex.md +4 -4
  356. package/packages/shared-skills/skills/coding-agent-sessions/scripts/agent_sessions/cli.py +1 -1
  357. package/packages/shared-skills/skills/coding-agent-sessions/scripts/find-agent-sessions.py +6 -3
  358. package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +1 -1
  359. package/packages/shared-skills/skills/frontend/SKILL.md +34 -12
  360. package/packages/shared-skills/skills/frontend/references/design/README.md +2 -2
  361. package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +2 -1
  362. package/packages/shared-skills/skills/frontend/references/design/component-catalogs.md +14 -10
  363. package/packages/shared-skills/skills/frontend/references/design/design-system-architecture.md +19 -6
  364. package/packages/shared-skills/skills/frontend/references/design/grounding.md +29 -0
  365. package/packages/shared-skills/skills/frontend/references/ui-ux-db/scripts/design_system.py +2 -0
  366. package/packages/shared-skills/skills/frontend/references/ui-ux-db/scripts/search.py +2 -0
  367. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +5 -4
  368. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -2
  369. package/packages/shared-skills/skills/ultimate-browsing/engine/__main__.py +9 -6
  370. package/packages/shared-skills/skills/ultimate-browsing/engine/summary.py +1 -1
  371. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +4 -4
  372. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +4 -2
  373. package/packages/shared-skills/skills/ulw-research/AGENTS.md +8 -4
  374. package/packages/shared-skills/skills/ulw-research/SKILL.md +7 -259
  375. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +68 -24
  376. package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +7 -3
  377. package/packages/shared-skills/skills/ulw-research/references/fact-ledger.md +42 -8
  378. package/packages/shared-skills/skills/ulw-research/references/fidelity-claim-kinds.md +1 -1
  379. package/packages/shared-skills/skills/ulw-research/references/markdown-blocks.md +12 -0
  380. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +14 -6
  381. package/packages/shared-skills/skills/ulw-research/references/research-collection.md +195 -0
  382. package/packages/shared-skills/skills/ulw-research/references/research-delivery.md +123 -0
  383. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.mjs +33 -5
  384. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.test.ts +37 -2
  385. package/packages/shared-skills/skills/ulw-research/scripts/claim-prefix.test.ts +76 -0
  386. package/packages/shared-skills/skills/ulw-research/scripts/contracts-validation.test.ts +83 -0
  387. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +16 -15
  388. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +6 -109
  389. package/packages/shared-skills/skills/ulw-research/scripts/conversion-altered.test.ts +154 -0
  390. package/packages/shared-skills/skills/ulw-research/scripts/conversion-originals.test.ts +297 -0
  391. package/packages/shared-skills/skills/ulw-research/scripts/conversion-probe-classes.test.ts +361 -0
  392. package/packages/shared-skills/skills/ulw-research/scripts/conversion-probe.mjs +229 -0
  393. package/packages/shared-skills/skills/ulw-research/scripts/conversion-probe.test.ts +218 -0
  394. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-fidelity-policy.test.ts +156 -0
  395. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-polish.test.ts +10 -9
  396. package/packages/shared-skills/skills/ulw-research/scripts/css-hiding.mjs +158 -0
  397. package/packages/shared-skills/skills/ulw-research/scripts/css-lite.mjs +4 -2
  398. package/packages/shared-skills/skills/ulw-research/scripts/delivery-barrier-meaning.test.ts +186 -0
  399. package/packages/shared-skills/skills/ulw-research/scripts/delivery-barrier.test.ts +40 -87
  400. package/packages/shared-skills/skills/ulw-research/scripts/entities.mjs +13 -4
  401. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-fill.mjs +96 -0
  402. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +26 -30
  403. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-polish.mjs +46 -19
  404. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -6
  405. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +40 -62
  406. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive-2.test.ts +149 -0
  407. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.mjs +7 -2
  408. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.test.ts +7 -192
  409. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +9 -6
  410. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +6 -7
  411. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +2 -1
  412. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +2 -1
  413. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.mjs +15 -9
  414. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-dimensions-2.test.ts +149 -0
  415. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +4 -168
  416. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-rules.test.ts +3 -229
  417. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-claims.mjs +4 -3
  418. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-csv.mjs +32 -0
  419. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.mjs +1 -1
  420. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-originals.mjs +161 -0
  421. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  422. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-records.mjs +81 -14
  423. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-snapshot-readability.mjs +49 -0
  424. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-sources.mjs +76 -0
  425. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.mjs +25 -11
  426. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-value-unit.mjs +202 -0
  427. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.mjs +40 -39
  428. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.test.ts +8 -29
  429. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.mjs +8 -6
  430. package/packages/shared-skills/skills/ulw-research/scripts/gates-static-headings.test.ts +17 -0
  431. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +10 -1
  432. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +6 -2
  433. package/packages/shared-skills/skills/ulw-research/scripts/html-markup.mjs +2 -2
  434. package/packages/shared-skills/skills/ulw-research/scripts/html-text-map.mjs +140 -0
  435. package/packages/shared-skills/skills/ulw-research/scripts/html-text-map.test.ts +148 -0
  436. package/packages/shared-skills/skills/ulw-research/scripts/markdown-closers.mjs +126 -0
  437. package/packages/shared-skills/skills/ulw-research/scripts/md-lite-boundaries.test.ts +46 -0
  438. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.mjs +1 -1
  439. package/packages/shared-skills/skills/ulw-research/scripts/number-grammar.mjs +33 -0
  440. package/packages/shared-skills/skills/ulw-research/scripts/number-grammar.test.ts +36 -0
  441. package/packages/shared-skills/skills/ulw-research/scripts/numerals.mjs +18 -7
  442. package/packages/shared-skills/skills/ulw-research/scripts/numerals.test.ts +3 -0
  443. package/packages/shared-skills/skills/ulw-research/scripts/outcome-start.mjs +66 -0
  444. package/packages/shared-skills/skills/ulw-research/scripts/outcome-start.test.ts +109 -0
  445. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +11 -22
  446. package/packages/shared-skills/skills/ulw-research/scripts/page-source-locator.test.ts +187 -0
  447. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.mjs +18 -6
  448. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.mjs +3 -2
  449. package/packages/shared-skills/skills/ulw-research/scripts/polish-markup.test.ts +15 -1
  450. package/packages/shared-skills/skills/ulw-research/scripts/polish-partial.test.ts +42 -0
  451. package/packages/shared-skills/skills/ulw-research/scripts/raw-decode.mjs +110 -0
  452. package/packages/shared-skills/skills/ulw-research/scripts/raw-locator.mjs +47 -0
  453. package/packages/shared-skills/skills/ulw-research/scripts/raw-snapshot.test.ts +139 -0
  454. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-2.test.ts +233 -0
  455. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-3.test.ts +141 -0
  456. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-4.test.ts +135 -0
  457. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-5.test.ts +231 -0
  458. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +1 -1
  459. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +7 -1
  460. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +6 -560
  461. package/packages/shared-skills/skills/ulw-research/scripts/snapshot-readability.test.ts +116 -0
  462. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +87 -29
  463. package/packages/shared-skills/skills/ulw-research/scripts/source-integrity.test.ts +243 -0
  464. package/packages/shared-skills/skills/ulw-research/scripts/source-refresh.mjs +82 -0
  465. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.mjs +73 -69
  466. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.test.ts +7 -4
  467. package/packages/shared-skills/skills/ulw-research/scripts/source-view.mjs +89 -0
  468. package/packages/shared-skills/skills/ulw-research/scripts/sources-citation.mjs +203 -0
  469. package/packages/shared-skills/skills/ulw-research/scripts/sources-citation.test.ts +236 -0
  470. package/packages/shared-skills/skills/ulw-research/scripts/tests/contracts-support.ts +147 -0
  471. package/packages/shared-skills/skills/ulw-research/scripts/tests/delivery-barrier-session.ts +59 -0
  472. package/packages/shared-skills/skills/ulw-research/scripts/tests/derive-clock.ts +4 -0
  473. package/packages/shared-skills/skills/ulw-research/scripts/tests/fact-derive-support.ts +65 -0
  474. package/packages/shared-skills/skills/ulw-research/scripts/tests/fact-unit-dimensions-support.ts +41 -0
  475. package/packages/shared-skills/skills/ulw-research/scripts/tests/fact-unit-rules-support.ts +241 -0
  476. package/packages/shared-skills/skills/ulw-research/scripts/tests/fidelity-values-support.ts +30 -0
  477. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/conversion-probe.html +14 -0
  478. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/sources-shapes.html +20 -0
  479. package/packages/shared-skills/skills/ulw-research/scripts/tests/numeral-classes.ts +196 -0
  480. package/packages/shared-skills/skills/ulw-research/scripts/tests/report-tools-support.ts +182 -0
  481. package/packages/shared-skills/skills/ulw-research/scripts/tests/value-mode-support.ts +48 -0
  482. package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +14 -3
  483. package/packages/shared-skills/skills/ulw-research/scripts/units.test.ts +10 -0
  484. package/packages/shared-skills/skills/ulw-research/scripts/value-mode-unit-classes.test.ts +255 -0
  485. package/packages/shared-skills/skills/ulw-research/scripts/value-mode-unit.test.ts +179 -0
  486. package/packages/shared-skills/skills/ulw-research/scripts/value-rules-cli.test.ts +157 -0
  487. package/packages/shared-skills/skills/ulw-research/scripts/value-rules-followups.test.ts +72 -0
  488. package/packages/shared-skills/skills/ulw-research/scripts/value-rules-priority.test.ts +80 -0
  489. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/amp_redirect_stub.html +0 -7
  490. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/search_interstitial.html +0 -19
  491. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/wayback_available.json +0 -1
  492. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/wayback_snapshot.html +0 -1128
  493. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_fetch_chain.py +0 -77
  494. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +0 -86
  495. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +0 -279
  496. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_surrogate.py +0 -252
  497. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_surrogate_validators.py +0 -78
@@ -0,0 +1,182 @@
1
+ import { describe, expect, onTestFinished, test } from "bun:test"
2
+ import { createHash } from "node:crypto"
3
+ import { copyFileSync, existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
4
+ import { tmpdir } from "node:os"
5
+ import { delimiter, join } from "node:path"
6
+ import { PROMOTION } from "../fact-commands-meaning.mjs"
7
+ import { writeFidelityRecord } from "../fidelity-records.mjs"
8
+ import { PAIR_KINDS } from "../fact-pairs.mjs"
9
+ import { VERDICT_SCHEMA } from "../meaning-check.mjs"
10
+ import { POLISH_MARKUP_REASON } from "../polish-apply.mjs"
11
+ import { COMMANDS, run } from "../report-tools.mjs"
12
+
13
+
14
+ const SCRIPT = join(join(import.meta.dir, ".."), "report-tools.mjs")
15
+
16
+ const FIXTURES = join(join(import.meta.dir, ".."), "tests", "fixtures")
17
+
18
+ /** The caller-owned observation time every CLI derive passes (derive never reads the clock). */
19
+ const NOW = "2026-10-23T08:00:00Z"
20
+
21
+
22
+ function sessionDir() {
23
+ const root = mkdtempSync(join(tmpdir(), "report-tools-cli-"))
24
+ onTestFinished(() => rmSync(root, { recursive: true, force: true }))
25
+ const dir = join(root, "20260101-120000")
26
+ mkdirSync(dir)
27
+ return dir
28
+ }
29
+
30
+
31
+ function nodeWithoutBun(args: string[]) {
32
+ const nodePath = Bun.which("node")
33
+ if (nodePath === null) throw new Error("node not found on PATH")
34
+ const path = (process.env.PATH ?? "").split(delimiter).filter((entry) => !entry.includes(".bun")).join(delimiter)
35
+ return Bun.spawnSync({ cmd: [nodePath, SCRIPT, ...args], env: { ...process.env, PATH: path } })
36
+ }
37
+
38
+
39
+ function defectsFile(dir: string, name: string, codes: string[]) {
40
+ const file = join(dir, name)
41
+ writeFileSync(file, JSON.stringify(codes.map((code) => ({ code, message: code }))))
42
+ return file
43
+ }
44
+
45
+
46
+ const FIDELITY = join(FIXTURES, "fidelity")
47
+
48
+ const sha256 = (path: string) => createHash("sha256").update(readFileSync(path)).digest("hex")
49
+
50
+
51
+ type CliRow = { fact_id: string; kind: string; value: string | null }
52
+
53
+ type CliJson = { data: { row: CliRow; context: { match: string } }; defects: { code: string }[] }
54
+
55
+
56
+ function nodeJson(args: string[]) {
57
+ const proc = nodeWithoutBun(args)
58
+ const stdout = proc.stdout.toString()
59
+ let json = {} as CliJson
60
+ try {
61
+ json = JSON.parse(stdout) as CliJson
62
+ } catch {}
63
+ return { code: proc.exitCode, json, stdout, stderr: proc.stderr.toString() }
64
+ }
65
+
66
+
67
+ /** A session with the locked brief, S4 snapshotted, F1 (span), Q1 (completion), and F<n> (span in mm). */
68
+ function fidelitySession() {
69
+ const dir = sessionDir()
70
+ copyFileSync(join(FIXTURES, "fidelity-brief-locked.md"), join(dir, "brief.md"))
71
+ const s = ["--session-dir", dir]
72
+ const snap = nodeJson(["source", "snapshot", "--id", "S4", "--html", join(FIDELITY, "sources", "S4.html"), ...s])
73
+ const add = nodeJson(["fact", "add", "--source", "S4", "--find", "Span length", "--label", "span_len", "--json", ...s])
74
+ const quote = nodeJson(["fact", "quote", "--source", "S4", "--match", "no earlier than 2027", "--label", "completion", "--json", ...s])
75
+ const derive = nodeJson(["fact", "derive", "--op", "convert", "F1", "--to", "mm", "--label", "span_mm", "--now", NOW, "--json", ...s])
76
+ const mm = derive.json.data?.row?.fact_id
77
+ const source = [
78
+ '<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Bridge</title></head><body><h1>Bridge</h1>',
79
+ "<h2>Findings</h2>",
80
+ "<p>The span length is {{F1:span_len}} [S4].</p>",
81
+ `<p>In millimetres the span is {{${mm}:span_mm}} [S4].</p>`,
82
+ "<p>The operator wrote {{Q1:completion}} [S4].</p>",
83
+ "<h2>How this report was made</h2><p>Collected from one cited public page [S4].</p>",
84
+ '<h2>Sources</h2><ol><li id="s4">[S4] Bridge page, retrieved 2026-10-01.</li></ol></body></html>',
85
+ ].join("\n")
86
+ writeFileSync(join(dir, "report.src.html"), source)
87
+ return { dir, s, snap, add, quote, derive, mm }
88
+ }
89
+
90
+
91
+ function verify(dir: string, report: string, extra: string[] = []) {
92
+ return nodeJson(["fact", "verify", join(dir, report), "--sources", join(dir, "sources"), "--brief", join(dir, "brief.md"), "--session-dir", dir, ...extra])
93
+ }
94
+
95
+
96
+ const codesOf = (json: { defects?: { code: string }[] }) => (json.defects ?? []).map((d) => d.code)
97
+
98
+
99
+ type Pair = { pair_id: string; kind: string; fact_ids?: string[]; sentence: string; quote?: string }
100
+
101
+
102
+ /** A filled session plus the pair artifact `fact pairs` wrote for it. */
103
+ function pairedSession() {
104
+ const session = fidelitySession()
105
+ const { dir, s } = session
106
+ nodeJson(["fact", "fill", join(dir, "report.src.html"), "--out", join(dir, "report.html"), ...s])
107
+ const pairs = nodeJson(["fact", "pairs", join(dir, "report.html"), "--ledger", join(dir, "fact-ledger.json"), "--out", join(dir, "pairs.json"), ...s])
108
+ return { ...session, pairs }
109
+ }
110
+
111
+
112
+ function verdicts(dir: string, name: string, tier: string, list: object[]) {
113
+ const file = join(dir, name)
114
+ writeFileSync(file, JSON.stringify({ model_tier: tier, verdicts: list }))
115
+ return file
116
+ }
117
+
118
+
119
+ const calibrationRows = (dir: string) =>
120
+ readFileSync(join(dir, "fidelity-calibration.jsonl"), "utf8")
121
+ .trim()
122
+ .split("\n")
123
+ .map((line) => JSON.parse(line))
124
+ .filter((row) => row.header !== true && row.run !== true)
125
+
126
+
127
+ const runLines = (dir: string) =>
128
+ readFileSync(join(dir, "fidelity-calibration.jsonl"), "utf8")
129
+ .trim()
130
+ .split("\n")
131
+ .map((line) => JSON.parse(line))
132
+ .filter((row) => row.run === true)
133
+
134
+
135
+ type PolishJson = { data: { out: string; applied: number; rejected: number; unlocated: number }; defects: { code: string; index: number; reason?: string }[] }
136
+
137
+
138
+ /** A filled session whose first finding names an acquirer and a target around F1. */
139
+ function polishSession() {
140
+ const session = fidelitySession()
141
+ const { dir, s } = session
142
+ const source = readFileSync(join(dir, "report.src.html"), "utf8").replace("<p>The span length is", "<p>Acme acquired Bolt before the span length reached")
143
+ writeFileSync(join(dir, "report.src.html"), source)
144
+ nodeJson(["fact", "fill", join(dir, "report.src.html"), "--out", join(dir, "report.html"), ...s])
145
+ return session
146
+ }
147
+
148
+
149
+ function polishApply(dir: string, edits: string) {
150
+ const args = ["fact", "polish-apply", join(dir, "report.html"), "--edits", edits, "--ledger", join(dir, "fact-ledger.json"), "--out", join(dir, "report.polished.html"), "--session-dir", dir]
151
+ const proc = nodeJson(args)
152
+ return { ...proc, json: proc.json as unknown as PolishJson }
153
+ }
154
+
155
+
156
+ /** Rewrite one `## Fidelity` field line of the session's brief.md. */
157
+ function setField(dir: string, field: string, value: string, answeredBy = "user") {
158
+ const path = join(dir, "brief.md")
159
+ const line = new RegExp(`^${field}: .*$`, "mu")
160
+ writeFileSync(path, readFileSync(path, "utf8").replace(line, `${field}: ${value} (answered_by: ${answeredBy})`))
161
+ }
162
+
163
+
164
+ function fillTemplate(dir: string, body: string, extra: string[] = []) {
165
+ const source = ['<!doctype html><html lang="en"><head><meta charset="utf-8"><title>T</title></head><body><h1>T</h1>', "<h2>Findings</h2>", body, "</body></html>"].join("\n")
166
+ writeFileSync(join(dir, "t.src.html"), source)
167
+ return nodeJson(["fact", "fill", join(dir, "t.src.html"), "--out", join(dir, "t.html"), "--session-dir", dir, ...extra])
168
+ }
169
+
170
+
171
+ /** Snapshot a one-line text source under `id` with the given provenance (omitted: unknown) and register its span row. */
172
+ function textSource(dir: string, id: string, provenance?: string) {
173
+ writeFileSync(join(dir, `${id}.txt`), "Commentary: the span length is 320 m according to this review.\n")
174
+ const prov = provenance === undefined ? [] : ["--provenance", provenance]
175
+ const snap = nodeJson(["source", "snapshot", "--id", id, "--raw", join(dir, `${id}.txt`), "--kind", "document", ...prov, "--session-dir", dir])
176
+ const add = nodeJson(["fact", "add", "--source", id, "--find", "span length", "--label", `span_${id.toLowerCase()}`, "--json", "--session-dir", dir])
177
+ return { snap, row: add.json.data?.row }
178
+ }
179
+
180
+
181
+ const ledgerRow = (dir: string, id: string) => JSON.parse(readFileSync(join(dir, "fact-ledger.json"), "utf8")).rows.find((r: { fact_id: string }) => r.fact_id === id)
182
+ export { SCRIPT, FIXTURES, NOW, sessionDir, nodeWithoutBun, defectsFile, FIDELITY, sha256, CliRow, CliJson, nodeJson, fidelitySession, verify, codesOf, Pair, pairedSession, verdicts, calibrationRows, runLines, PolishJson, polishSession, polishApply, setField, fillTemplate, textSource, ledgerRow }
@@ -0,0 +1,48 @@
1
+ import { expect, onTestFinished } from "bun:test"
2
+ import { copyFileSync, cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
5
+
6
+ import { run } from "../report-tools.mjs"
7
+
8
+ // Shared by the value-mode unit tests: the verify fixture ledger plus value rows, and one fill -> verify session.
9
+ export type Row = Record<string, unknown> & { fact_id: string }
10
+ export type VerifyResult = { exitCode: number; json: { defects: { code: string; severity: string }[] } }
11
+ export const FIX = join(import.meta.dir, "fixtures")
12
+ const BASE: { rows: Row[] } = JSON.parse(readFileSync(join(FIX, "fidelity", "ledger-verify.json"), "utf8"))
13
+ export const F1 = BASE.rows.find((r) => r.fact_id === "F1") as Row
14
+ const extra = (fact_id: string, label: string, raw: string, value: string, unit: string, num: string, den = "1"): Row => ({ ...F1, fact_id, label, raw, value, unit, exact: { num, den } })
15
+ // Korean units and particles as \u escapes: pp (peosenteu pointeu), eok won, myeong, geon, dalleo, particles ro/euro/ui/ida.
16
+ export const LEDGER = {
17
+ ...BASE,
18
+ rows: [
19
+ ...BASE.rows,
20
+ extra("F20", "gap_pp", "3.2%p", "3.2", "%p", "16", "5"),
21
+ extra("F21", "rev_eok", "3,234 KRW-e8", "3234", "KRW-e8", "3234"),
22
+ extra("F22", "staff", "5\uBA85", "5", "person", "5"),
23
+ extra("F23", "price", "$12", "12", "USD", "12"),
24
+ extra("F24", "deck_len", "1.2 km", "1.2", "km", "6", "5"),
25
+ ],
26
+ }
27
+
28
+ /** Fill `body` in a fresh session; verify re-checks the edition, write replaces its text (the fill map stays). */
29
+ export async function filled(body: string, format = "html", ledger: { rows: Row[] } = LEDGER) {
30
+ const dir = mkdtempSync(join(tmpdir(), "value-mode-unit-"))
31
+ onTestFinished(() => rmSync(dir, { recursive: true, force: true }))
32
+ writeFileSync(join(dir, "fact-ledger.json"), JSON.stringify(ledger))
33
+ copyFileSync(join(FIX, "fidelity-brief-locked.md"), join(dir, "brief.md"))
34
+ cpSync(join(FIX, "fidelity", "sources"), join(dir, "sources"), { recursive: true })
35
+ const source = join(dir, `report.src.${format}`)
36
+ const report = join(dir, `report.${format}`)
37
+ writeFileSync(source, body)
38
+ const result = await run(["fact", "fill", source, "--out", report, "--session-dir", dir])
39
+ expect(result.exitCode).toBe(0)
40
+ const verify = async () => (await run(["fact", "verify", report, "--session-dir", dir])) as VerifyResult
41
+ const edit = (from: string, to: string) => writeFileSync(report, readFileSync(report, "utf8").replace(from, to))
42
+ const text = readFileSync(report, "utf8")
43
+ const write = (next: string) => writeFileSync(report, next)
44
+ return { verify, edit, text, write }
45
+ }
46
+
47
+ export const codes = (result: VerifyResult) => result.json.defects.map((d) => d.code)
48
+ export const prose = (pointer: string, tail: string) => `<h2>Findings</h2><p>Up to ${pointer}${tail} [S4].</p>`
@@ -21,6 +21,9 @@ const TABLE = [
21
21
  ["s", "time", "1", "open", ["sec", "secs", "second", "seconds", "\uCD08"]], // cho
22
22
  ["min", "time", "60", "closed", ["mins", "minute", "minutes", "\uBD84"]], // bun
23
23
  ["h", "time", "3600", "closed", ["hr", "hrs", "hour", "hours", "\uC2DC\uAC04"]], // sigan
24
+ // Months and years are a calendar dimension of their own (a year is no fixed number of seconds).
25
+ ["mo", "calendar", "1", "l", []], // gaewol
26
+ ["yr", "calendar", "12", "closed", ["yrs"]], // nyeon
24
27
  ["B", "data", "1", "open", ["byte", "bytes", "\uBC14\uC774\uD2B8"]], // baiteu
25
28
  ["KB", "data", "1000", "open", ["\uD0AC\uB85C\uBC14\uC774\uD2B8"]],
26
29
  ["MB", "data", "1000000", "open", ["\uBA54\uAC00\uBC14\uC774\uD2B8"]],
@@ -28,13 +31,13 @@ const TABLE = [
28
31
  ["TB", "data", "1000000000000", "open", ["\uD14C\uB77C\uBC14\uC774\uD2B8"]],
29
32
  ["C", "temperature", "1", "open", ["\u00B0C", "\u2103", "celsius", "\uC12D\uC528"], "0"], // do
30
33
  ["F", "temperature", "5/9", "open", ["\u00B0F", "\u2109", "fahrenheit", "\uD654\uC528"], "-160/9"], // do
31
- ["%", "percent", "1", "open", ["percent", "\uD37C\uC13C\uD2B8", "\uD504\uB85C"]], // peosenteu
34
+ ["%", "percent", "1", "open", ["percent", "pct", "\uD37C\uC13C\uD2B8", "\uD504\uB85C"]], // peosenteu
32
35
  ["\u2030", "percent", "0.1", "l", ["permille", "\uD37C\uBC00"]], // peomil
33
- ["%p", "percentage-point", "1", "open", ["pp", "%pt", "percentage point", "percentage points", "\uD37C\uC13C\uD2B8\uD3EC\uC778\uD2B8", "%\uD3EC\uC778\uD2B8"]],
36
+ ["%p", "percentage-point", "1", "open", ["pp", "%pt", "percentage point", "percentage points", "percent point", "percent points", "% point", "% points", "\uD37C\uC13C\uD2B8\uD3EC\uC778\uD2B8", "%\uD3EC\uC778\uD2B8"]],
34
37
  ["x", "multiple", "1", "open", ["\u00D7", "times", "\uBC30"]], // bae
35
38
  ["e3", "scale", "1000", "closed", ["k", "thousand", "\uCC9C"]], // cheon
36
39
  ["e4", "scale", "10000", "closed", ["\uB9CC"]], // man
37
- ["e6", "scale", "1000000", "closed", ["M", "million", "\uBC31\uB9CC"]], // baengman
40
+ ["e6", "scale", "1000000", "closed", ["M", "mn", "million", "\uBC31\uB9CC"]], // baengman
38
41
  ["e8", "scale", "100000000", "closed", ["\uC5B5"]], // eok
39
42
  ["e9", "scale", "1000000000", "closed", ["bn", "billion", "\uC2ED\uC5B5"]], // sibeok
40
43
  ["e12", "scale", "1000000000000", "open", ["trillion", "\uC870"]], // jo
@@ -59,6 +62,8 @@ const ENTRIES = new Map()
59
62
  const EXACT = new Map()
60
63
  /** Case-insensitive keys: multi-letter ASCII aliases only, so m/M and B/b never collide. */
61
64
  const WORD = new Map()
65
+ /** Aliases written with their own spacing ("percentage points"), by lower-case text. */
66
+ const SPACED = new Map()
62
67
  for (const [token, dimension, factor, reading, aliases, offset] of TABLE) {
63
68
  const entry = Object.freeze({ token, dimension, factor: parse(factor), offset: parse(offset ?? "0"), reading })
64
69
  ENTRIES.set(token, entry)
@@ -66,6 +71,7 @@ for (const [token, dimension, factor, reading, aliases, offset] of TABLE) {
66
71
  const key = squash(alias)
67
72
  EXACT.set(key, entry)
68
73
  if (alias !== token && key.length > 2 && /^[a-z]+$/.test(key) && !WORD.has(key.toLowerCase())) WORD.set(key.toLowerCase(), entry)
74
+ if (/\s/.test(alias)) SPACED.set(alias.toLowerCase(), entry)
69
75
  }
70
76
  }
71
77
 
@@ -81,6 +87,11 @@ export function normalizeUnit(raw) {
81
87
  return entryOf(raw)?.token ?? null
82
88
  }
83
89
 
90
+ /** Canonical token for a multi-word unit written with its alias's own spacing, or null (never a squashed join). */
91
+ export function spacedAliasUnit(raw) {
92
+ return SPACED.get(String(raw).trim().replace(/\s+/g, " ").toLowerCase())?.token ?? null
93
+ }
94
+
84
95
  /** Dimension of a unit ("length", "currency:KRW", ...), or null. */
85
96
  export function dimensionOf(token) {
86
97
  return entryOf(token)?.dimension ?? null
@@ -14,6 +14,16 @@ describe("normalizeUnit", () => {
14
14
  expect(normalizeUnit("furlongs")).toBeNull()
15
15
  })
16
16
 
17
+ test("common measure suffixes read as units: mn, pct, hrs, yr(s), mo", () => {
18
+ expect(normalizeUnit("mn")).toBe(normalizeUnit("M"))
19
+ expect(normalizeUnit("pct")).toBe(normalizeUnit("%"))
20
+ expect(normalizeUnit("hrs")).toBe(normalizeUnit("h"))
21
+ expect(normalizeUnit("yrs")).toBe(normalizeUnit("yr"))
22
+ expect(normalizeUnit("mo")).not.toBeNull()
23
+ expect(convertFactor("yr", "mo")).toEqual({ num: 12n, den: 1n })
24
+ expect(convertFactor("yr", "s")).toBeNull()
25
+ })
26
+
17
27
  test("a normalized token normalizes to itself", () => {
18
28
  for (const raw of ["억 원", "m", "kg", "GB", "%p", "USD", "C", "F"]) {
19
29
  const token = normalizeUnit(raw)
@@ -0,0 +1,255 @@
1
+ import { describe, expect, test } from "bun:test"
2
+
3
+ import { valueChecks } from "./fidelity-values.mjs"
4
+ import { buildContext } from "./gates-static.mjs"
5
+ import { parseHtml } from "./html-lite.mjs"
6
+ import { escapeHtml } from "./placeholder.mjs"
7
+ import { F1, codes, filled, prose } from "./tests/value-mode-support"
8
+ import { dimensionOf, normalizeUnit } from "./units.mjs"
9
+
10
+ // One table per class of value-mode unit reading; every row is { input, expected } and a run lists every row that
11
+ // disagrees. Korean text is written as \u escapes (romanized in the comments).
12
+ type Verdict = "pass" | "unit_mismatch" | "unit_unbound"
13
+ const verdictOf = (found: string[]): Verdict => (found.includes("unit_mismatch") ? "unit_mismatch" : found.includes("unit_unbound") ? "unit_unbound" : "pass")
14
+
15
+ // Hand-written oracle: every alias units.mjs accepts, by canonical token (internal scale tokens such as e3 omitted).
16
+ const ALIASES: [string, string[]][] = [
17
+ ["mm", ["mm", "millimeter", "millimeters", "millimetre", "millimetres", "\uBC00\uB9AC\uBBF8\uD130"]],
18
+ ["cm", ["cm", "centimeter", "centimeters", "centimetre", "centimetres", "\uC13C\uD2F0\uBBF8\uD130"]],
19
+ ["m", ["m", "meter", "meters", "metre", "metres", "\uBBF8\uD130"]],
20
+ ["km", ["km", "kilometer", "kilometers", "kilometre", "kilometres", "\uD0AC\uB85C\uBBF8\uD130"]],
21
+ ["g", ["g", "gram", "grams", "\uADF8\uB7A8"]],
22
+ ["kg", ["kg", "kilogram", "kilograms", "\uD0AC\uB85C\uADF8\uB7A8"]],
23
+ ["t", ["t", "ton", "tons", "tonne", "tonnes", "\uD1A4"]],
24
+ ["ms", ["ms", "millisecond", "milliseconds", "\uBC00\uB9AC\uCD08"]],
25
+ ["s", ["s", "sec", "secs", "second", "seconds", "\uCD08"]],
26
+ ["min", ["min", "mins", "minute", "minutes", "\uBD84"]],
27
+ ["h", ["h", "hr", "hrs", "hour", "hours", "\uC2DC\uAC04"]],
28
+ ["B", ["B", "byte", "bytes", "\uBC14\uC774\uD2B8"]],
29
+ ["KB", ["KB", "\uD0AC\uB85C\uBC14\uC774\uD2B8"]],
30
+ ["MB", ["MB", "\uBA54\uAC00\uBC14\uC774\uD2B8"]],
31
+ ["GB", ["GB", "\uAE30\uAC00\uBC14\uC774\uD2B8"]],
32
+ ["TB", ["TB", "\uD14C\uB77C\uBC14\uC774\uD2B8"]],
33
+ ["C", ["C", "\u00B0C", "\u2103", "celsius", "\uC12D\uC528"]],
34
+ ["F", ["F", "\u00B0F", "\u2109", "fahrenheit", "\uD654\uC528"]],
35
+ ["%", ["%", "percent", "\uD37C\uC13C\uD2B8", "\uD504\uB85C"]],
36
+ ["\u2030", ["\u2030", "permille", "\uD37C\uBC00"]],
37
+ ["%p", ["%p", "pp", "%pt", "percentage point", "percentage points", "percent point", "percent points", "% point", "% points", "\uD37C\uC13C\uD2B8\uD3EC\uC778\uD2B8", "%\uD3EC\uC778\uD2B8"]],
38
+ ["x", ["x", "\u00D7", "times", "\uBC30"]],
39
+ ["e3", ["k", "thousand", "\uCC9C"]],
40
+ ["e4", ["\uB9CC"]],
41
+ ["e6", ["M", "million", "\uBC31\uB9CC"]],
42
+ ["e8", ["\uC5B5"]],
43
+ ["e9", ["bn", "billion", "\uC2ED\uC5B5"]],
44
+ ["e12", ["trillion", "\uC870"]],
45
+ ["person", ["person", "persons", "people", "\uBA85"]],
46
+ ["case", ["case", "cases", "\uAC74"]],
47
+ ["KRW", ["KRW", "\uC6D0", "\u20A9", "won"]],
48
+ ["KRW-e3", ["\uCC9C\uC6D0"]],
49
+ ["KRW-e4", ["\uB9CC\uC6D0"]],
50
+ ["KRW-e8", ["\uC5B5\uC6D0"]],
51
+ ["KRW-e12", ["\uC870\uC6D0"]],
52
+ ["USD", ["USD", "$", "US$", "dollar", "dollars", "\uB2EC\uB7EC"]],
53
+ ["EUR", ["EUR", "\u20AC", "euro", "euros", "\uC720\uB85C"]],
54
+ ["JPY", ["JPY", "\u00A5", "yen", "\uC5D4"]],
55
+ ["GBP", ["GBP", "\u00A3", "pound", "pounds", "\uD30C\uC6B4\uB4DC"]],
56
+ ]
57
+ // i, ga, eun, neun, eul, reul, ui, gwa, wa, ro, euro, e, eseo, do, man, buteo, kkaji.
58
+ const PARTICLES = ["\uC774", "\uAC00", "\uC740", "\uB294", "\uC744", "\uB97C", "\uC758", "\uACFC", "\uC640", "\uB85C", "\uC73C\uB85C", "\uC5D0", "\uC5D0\uC11C", "\uB3C4", "\uB9CC", "\uBD80\uD130", "\uAE4C\uC9C0"]
59
+ // chogwa, isang, miman, iha, jeongdo, garyang, jjeum: written glued after a unit.
60
+ const GLUED_QUALIFIERS = ["\uCD08\uACFC", "\uC774\uC0C1", "\uBBF8\uB9CC", "\uC774\uD558", "\uC815\uB3C4", "\uAC00\uB7C9", "\uCBE4"]
61
+ // The same plus gwaban (majority) and yak (about): whole words standing alone after the value.
62
+ const QUALIFIER_WORDS = [...GLUED_QUALIFIERS, "\uACFC\uBC18", "\uC57D"]
63
+ const CHOGWA = "\uCD08\uACFC"
64
+ const ORDINARY = new Set(["won", "second", "pound", "pounds"])
65
+
66
+ const element = (unit: string) => `<data class="fact" value="320" data-fact="F1" data-unit="${escapeHtml(unit)}" data-fact-mode="value">320</data>`
67
+ /** valueChecks over a Findings section holding one value-mode element of a row in `unit`. */
68
+ function readHtml(html: string, unit: string): Verdict {
69
+ const ledger = { rows: [{ ...F1, unit, raw: `320 ${unit}` }] }
70
+ return verdictOf((valueChecks(buildContext(parseHtml(`<h2>Findings</h2>${html}`), {}), ledger, null, null) as { code: string }[]).map((d) => d.code))
71
+ }
72
+ /** The same element with `after` written right after it. */
73
+ const readAfter = (after: string, unit: string) => readHtml(`<p>Up to ${element(unit)}${escapeHtml(after)} [S4].</p>`, unit)
74
+
75
+ type Case = { input: string; unit: string; expected: Verdict }
76
+ function tokenizationCases(): Case[] {
77
+ const cases: Case[] = []
78
+ for (const [token, aliases] of ALIASES) {
79
+ const other = dimensionOf(token) === "length" ? "KRW" : "m"
80
+ for (const alias of aliases) {
81
+ const suffixes = [...["", ...PARTICLES].flatMap((p) => [` ${alias}${p}`, `${alias}${p}`]), ...GLUED_QUALIFIERS.map((q) => ` ${alias}${q}`)]
82
+ for (const input of suffixes) {
83
+ const qualifier = input.trim() === CHOGWA
84
+ cases.push({ input, unit: token, expected: qualifier ? "unit_unbound" : "pass" })
85
+ cases.push({ input, unit: other, expected: qualifier || ORDINARY.has(alias) ? "unit_unbound" : "unit_mismatch" })
86
+ }
87
+ }
88
+ }
89
+ for (const word of QUALIFIER_WORDS) cases.push({ input: ` ${word}`, unit: "m", expected: "unit_unbound" }, { input: ` m ${word}`, unit: "m", expected: "pass" }, { input: ` mm ${word}`, unit: "m", expected: "unit_mismatch" })
90
+ // English words that collide with unit aliases or read like them; "3 bun-ui 1" is a fraction, not minutes.
91
+ const words: [string, string, Verdict][] = [
92
+ [" in total", "m", "unit_unbound"], [" a year", "m", "unit_unbound"], [" or more", "m", "unit_unbound"], [" or less", "m", "unit_unbound"],
93
+ [" min", "min", "pass"], [" min", "m", "unit_mismatch"], [" sec", "s", "pass"], [" sec", "m", "unit_mismatch"], [" SEC filings", "m", "unit_unbound"],
94
+ [" second", "m", "unit_unbound"], [" second", "s", "pass"], [" won awards", "person", "unit_unbound"], [" won", "KRW", "pass"],
95
+ [" pound", "m", "unit_unbound"], [" times", "x", "pass"], [" times", "m", "unit_mismatch"],
96
+ ["\uBD84\uC758 1", "%", "unit_unbound"], ["\uBD84\uC758 1", "min", "unit_unbound"], [" (m)", "m", "pass"], [" (mm)", "m", "unit_mismatch"], [" (approx.)", "m", "unit_unbound"],
97
+ ]
98
+ for (const [input, unit, expected] of words) cases.push({ input, unit, expected })
99
+ return cases
100
+ }
101
+
102
+ describe("class 1: the unit token written after a value", () => {
103
+ test("#given every alias with every particle and qualifier, and the colliding words #when read #then each row gets its expected verdict", () => {
104
+ const cases = tokenizationCases()
105
+ for (const [token, aliases] of ALIASES) for (const alias of aliases) expect([alias, normalizeUnit(alias)]).toEqual([alias, token])
106
+ const wrong = cases.map((row) => ({ ...row, actual: readAfter(row.input, row.unit) })).filter((row) => row.actual !== row.expected)
107
+ expect(cases.length).toBeGreaterThan(9000)
108
+ expect(wrong).toEqual([])
109
+ }, 120000)
110
+ })
111
+
112
+ type Form = { name: string; format: "html" | "md"; body: string; at: (unit: string) => string; units: [same: string, known: string, unknown: string] }
113
+ const m3: [string, string, string] = ["m", "mm", "ft"]
114
+ const P = "{{F1:span_len:value}}"
115
+ const head = (th: string) => `<h2>Findings</h2><table><thead><tr><th>${th}</th></tr></thead><tbody><tr><td>${P}</td></tr></tbody></table>`
116
+ const FORMS: Form[] = [
117
+ { name: "glued", format: "html", body: prose(P, "m"), at: (u) => `</data>${u} [S4]`, units: m3 },
118
+ { name: "spaced", format: "html", body: prose(P, " m"), at: (u) => `</data> ${u} [S4]`, units: m3 },
119
+ ...["b", "span", "em", "code"].map((tag): Form => ({ name: `wrapped in ${tag}`, format: "html", body: prose(P, ` <${tag}>m</${tag}>`), at: (u) => `</data> <${tag}>${u}</${tag}>`, units: m3 })),
120
+ { name: "parenthesized", format: "html", body: prose(P, " (m)"), at: (u) => `</data> (${u})`, units: m3 },
121
+ { name: "after a line break", format: "html", body: prose(P, "\nm"), at: (u) => `</data>\n${u} [S4]`, units: m3 },
122
+ { name: "after a br", format: "html", body: prose(P, "<br>m"), at: (u) => `</data><br>${u} [S4]`, units: m3 },
123
+ { name: "header-bound column", format: "html", body: head("Span (m)"), at: (u) => `<th>Span${u ? ` (${u})` : ""}</th>`, units: m3 },
124
+ { name: "currency sign before", format: "html", body: prose("${{F23:price:value}}", ""), at: (u) => `Up to ${u}<data`, units: ["$", "\u20AC", "Rs"] },
125
+ ...([["glued", "m"], ["spaced", " m"], ["parenthesized", " (m)"], ["after a line break", "\nm"]] as const).map(([name, tail]): Form => ({ name: `md ${name}`, format: "md", body: `## Findings\n\nUp to ${P}${tail} [S4].`, at: (u) => `320${tail.replace("m", u)} [S4]`, units: m3 })),
126
+ ]
127
+
128
+ describe("class 2: a unit hand-edited after fill", () => {
129
+ test("#given every form a bound unit takes #when it is kept, changed to a known or unknown unit, or deleted, with and without data-fact-mode #then each row gets its expected verdict", async () => {
130
+ const rows: { form: string; edit: string; attr: string; expected: Verdict; actual?: Verdict; exit?: number }[] = []
131
+ for (const form of FORMS) {
132
+ const { verify, text, write } = await filled(form.body, form.format)
133
+ const [same, known, unknown] = form.units
134
+ expect(text).toContain(form.at(same))
135
+ for (const [edit, unit, expected] of [["kept", same, "pass"], ["known", known, "unit_mismatch"], ["unknown", unknown, "unit_mismatch"], ["deleted", "", "unit_mismatch"]] as const) {
136
+ for (const attr of form.format === "html" ? ["kept", "deleted"] : ["n/a"]) {
137
+ const edited = text.replace(form.at(same), form.at(unit))
138
+ write(attr === "deleted" ? edited.replace(' data-fact-mode="value"', "") : edited)
139
+ const result = await verify()
140
+ rows.push({ form: form.name, edit, attr, expected, actual: verdictOf(codes(result)), exit: result.exitCode })
141
+ }
142
+ }
143
+ }
144
+ for (const [name, body, edit, expected] of [["no unit at fill", prose(P, " long"), ["</data> long", "</data> long"], "unit_unbound"], ["no unit at fill, mm added", prose(P, " long"), ["</data> long", "</data> mm long"], "unit_mismatch"], ["unknown unit at fill", prose(P, " ft"), ["</data> ft", "</data> ft"], "unit_unbound"], ["unknown unit at fill, m written", prose(P, " ft"), ["</data> ft", "</data> m"], "pass"]] as const) {
145
+ const { verify, text, write } = await filled(body)
146
+ write(text.replace(edit[0], edit[1]))
147
+ const result = await verify()
148
+ rows.push({ form: name, edit: "as named", attr: "kept", expected, actual: verdictOf(codes(result)), exit: result.exitCode })
149
+ }
150
+ const wrong = rows.filter((row) => row.actual !== row.expected || row.exit !== (row.expected === "unit_mismatch" ? 1 : 0))
151
+ expect(rows.length).toBe(FORMS.filter((f) => f.format === "html").length * 8 + FORMS.filter((f) => f.format === "md").length * 4 + 4)
152
+ expect(wrong).toEqual([])
153
+ }, 120000)
154
+ })
155
+
156
+ // Class 2b: a currency sign on both sides of the element's edge. Fill renders a prefix currency value from its raw text,
157
+ // so the sign may sit inside the element ($<data>$12</data>) as well as outside it. Every bound sign, outside before the
158
+ // element or inside it (prefix or suffix), must agree with the row's currency; two signs that disagree flag.
159
+ // EN forms $, US$, USD, euro sign, pound sign; KO forms won sign, won, dalleo, man-won. A form of the row's own currency at
160
+ // another scale (won sign under a man-won row) is the element numeral's value and scale reading, not a sign position: skipped.
161
+ const PREFIX_SIGNS: [form: string, unit: string][] = [["$", "USD"], ["US$", "USD"], ["USD ", "USD"], ["\u20AC", "EUR"], ["\u00A3", "GBP"], ["\u20A9", "KRW"]]
162
+ const SUFFIX_SIGNS: [form: string, unit: string][] = [["\u20AC", "EUR"], [" \u20AC", "EUR"], [" USD", "USD"], ["\u00A3", "GBP"], ["\uC6D0", "KRW"], [" \uC6D0", "KRW"], ["\uB2EC\uB7EC", "USD"], [" \uB2EC\uB7EC", "USD"], ["\uB9CC\uC6D0", "KRW"]]
163
+ const SIGN_ROW_UNITS = ["USD", "EUR", "GBP", "KRW", "KRW-e4"]
164
+ const KO = /[\u20A9\uAC00-\uD7A3]/u
165
+ type SignCase = { position: string; outside: string; inside: string; unit: string; expected: Verdict }
166
+ function signCases(): SignCase[] {
167
+ const cases: SignCase[] = []
168
+ const insides: [form: string, unit: string, where: string][] = [["", "", ""], ...PREFIX_SIGNS.map(([f, u]): [string, string, string] => [`${f}320`, u, "prefix"]), ...SUFFIX_SIGNS.map(([f, u]): [string, string, string] => [`320${f}`, u, "suffix"])]
169
+ for (const [outside, outsideUnit] of [["", ""], ...PREFIX_SIGNS]) for (const [inside, insideUnit, where] of insides) for (const unit of SIGN_ROW_UNITS) {
170
+ const bound = [outsideUnit, insideUnit].filter(Boolean)
171
+ if (bound.some((u) => u !== unit && dimensionOf(u) === dimensionOf(unit))) continue
172
+ const agree = bound.every((u) => dimensionOf(u) === dimensionOf(unit))
173
+ const position = !outsideUnit && !insideUnit ? "no sign" : !insideUnit ? "outside-before only" : !outsideUnit ? `inside-${where} only` : dimensionOf(outsideUnit) !== dimensionOf(insideUnit) ? "outside vs inside conflicting" : "outside and inside agreeing"
174
+ const label = outsideUnit && !insideUnit && !agree ? "outside disagreeing with the row" : position
175
+ cases.push({ position: label, outside, inside: inside || "320", unit, expected: bound.length === 0 ? "unit_unbound" : agree ? "pass" : "unit_mismatch" })
176
+ }
177
+ return cases
178
+ }
179
+ /** The row's value is what the element's text reads ("320 man-won" inside is 3,200,000 won), so the value check passes and the unit is what is asked. */
180
+ function signed(outside: string, inside: string, unit: string): Verdict {
181
+ const value = inside.includes("\uB9CC") ? "3200000" : "320"
182
+ const html = `<h2>Findings</h2><p>Up to ${escapeHtml(outside)}<data class="fact" value="${value}" data-fact="F1" data-unit="${escapeHtml(unit)}" data-fact-mode="value">${escapeHtml(inside)}</data> [S4].</p>`
183
+ const ledger = { rows: [{ ...F1, unit, value, raw: `${value} ${unit}`, exact: { num: value, den: "1" } }] }
184
+ return verdictOf((valueChecks(buildContext(parseHtml(html), {}), ledger, null, null) as { code: string }[]).map((d) => d.code))
185
+ }
186
+
187
+ describe("class 2b: a currency sign outside and inside a value-mode element", () => {
188
+ test("#given every EN and KO sign outside before, inside as prefix or suffix, both, or none #when read #then every bound sign must agree with the row and with each other", () => {
189
+ const cases = signCases()
190
+ const positions = ["outside-before only", "inside-prefix only", "inside-suffix only", "outside and inside agreeing", "outside vs inside conflicting", "outside disagreeing with the row", "no sign"]
191
+ for (const position of positions) {
192
+ const here = cases.filter((row) => row.position === position)
193
+ if (position !== "no sign") expect([position, here.some((row) => KO.test(row.outside + row.inside)), here.some((row) => !KO.test(row.outside + row.inside) && row.outside + row.inside !== "320")]).toEqual([position, true, true])
194
+ else expect(here.length).toBe(SIGN_ROW_UNITS.length)
195
+ }
196
+ const wrong = cases.map((row) => ({ ...row, actual: signed(row.outside, row.inside, row.unit) })).filter((row) => row.actual !== row.expected)
197
+ expect(wrong).toEqual([])
198
+ }, 120000)
199
+ })
200
+
201
+ // Class 3: unit prefix shadowing. Every pair of aliases of different units where one (spaces removed) starts the other
202
+ // ("%" / "%p", "m" / "mm" / "min", "k" / "km", "M" / "MB", cheon / cheon-won, peosenteu / peosenteu pointeu) and every alias that
203
+ // starts a qualifier word (cho / chogwa). The longer unit is read whole, never as its prefix.
204
+ const squashed = (alias: string) => alias.replace(/\s+/g, "")
205
+ const LETTERS = /^[A-Za-z]+$/
206
+ const scaleOf = (unit: string) => /-(e\d+)$/.exec(unit)?.[1] ?? null
207
+ type Pair = { short: string; shortUnit: string; long: string; longUnit: string }
208
+ function prefixPairs(): Pair[] {
209
+ const all = ALIASES.flatMap(([token, aliases]) => aliases.map((alias) => ({ alias, token })))
210
+ const longer = (short: string, long: string) => squashed(long) !== squashed(short) && squashed(long).startsWith(squashed(short))
211
+ return all.flatMap((s) => all.filter((l) => l.token !== s.token && longer(s.alias, l.alias)).map((l) => ({ short: s.alias, shortUnit: s.token, long: l.alias, longUnit: l.token })))
212
+ }
213
+ function prefixCases(): Case[] {
214
+ const cases: Case[] = []
215
+ const add = (input: string, unit: string, expected: Verdict) => cases.push({ input, unit, expected })
216
+ for (const { short, shortUnit, long, longUnit } of prefixPairs()) {
217
+ // A scale word alone agrees with a scaled currency row (the currency may be a sign, "$12 million"): cheon under a cheon-won row.
218
+ const scaleOnly = dimensionOf(shortUnit) === "scale" && scaleOf(longUnit) === shortUnit
219
+ for (const gap of ["", " "]) {
220
+ add(`${gap}${short}`, shortUnit, "pass")
221
+ add(`${gap}${long}`, shortUnit, "unit_mismatch")
222
+ add(`${gap}${short}`, longUnit, scaleOnly ? "pass" : "unit_mismatch")
223
+ add(`${gap}${long}`, longUnit, "pass")
224
+ }
225
+ if (/\s/.test(long)) continue
226
+ // Split at the prefix: English words stay apart ("m in" is m, then in); a symbol or Hangul joins ("% p", cheon won).
227
+ const rest = squashed(long).slice(squashed(short).length)
228
+ const english = LETTERS.test(short) && LETTERS.test(rest)
229
+ add(` ${short} ${rest}`, shortUnit, english ? "pass" : "unit_mismatch")
230
+ add(` ${short} ${rest}`, longUnit, english ? "unit_mismatch" : "pass")
231
+ }
232
+ for (const [token, aliases] of ALIASES) for (const alias of aliases) for (const word of [...QUALIFIER_WORDS, "\uB0B4\uC678", "\uAE4C\uC9C0"]) {
233
+ if (word !== alias && word.startsWith(alias)) for (const gap of ["", " "]) add(`${gap}${word}`, token, "unit_unbound")
234
+ }
235
+ return cases
236
+ }
237
+
238
+ // An ordinary word that is also a unit ("pounds", "won", "second") is read when a sign or header binds a unit of its kind.
239
+ const SIGNED: [html: string, unit: string, expected: Verdict][] = [
240
+ ["<p>Up to ${V} pounds [S4].</p>", "USD", "unit_mismatch"], ["<p>Up to ${V} pound [S4].</p>", "USD", "unit_mismatch"], ["<p>Up to ${V} won [S4].</p>", "USD", "unit_mismatch"],
241
+ ["<p>Up to \u20AC{V} pounds [S4].</p>", "EUR", "unit_mismatch"], ["<p>Up to \u20A9{V} pounds [S4].</p>", "KRW", "unit_mismatch"], ["<p>Up to \u00A3{V} pounds [S4].</p>", "GBP", "pass"],
242
+ ["<p>Up to ${V} SEC filings [S4].</p>", "USD", "pass"], ["<p>Up to ${V} second [S4].</p>", "USD", "pass"], ["<p>Up to {V} pounds [S4].</p>", "USD", "unit_unbound"],
243
+ ["<table><thead><tr><th>Cost ($)</th></tr></thead><tbody><tr><td>{V} pounds</td></tr></tbody></table>", "USD", "unit_mismatch"],
244
+ ["<table><thead><tr><th>Time (min)</th></tr></thead><tbody><tr><td>{V} second</td></tr></tbody></table>", "min", "unit_mismatch"],
245
+ ["<table><thead><tr><th>Span (m)</th></tr></thead><tbody><tr><td>{V} second</td></tr></tbody></table>", "m", "pass"],
246
+ ]
247
+
248
+ describe("class 3: a longer unit that starts with a shorter one", () => {
249
+ test("#given every prefix pair of aliases, spaced, glued and split, and the signed ordinary words #when read #then each row gets its expected verdict", () => {
250
+ const pairs = prefixPairs().map((p) => `${p.short}/${p.long}`)
251
+ for (const pair of ["%/%p", "%/%\uD3EC\uC778\uD2B8", "\uD37C\uC13C\uD2B8/\uD37C\uC13C\uD2B8\uD3EC\uC778\uD2B8", "percent/percent points", "m/mm", "m/min", "k/km", "M/MB", "\uCC9C/\uCC9C\uC6D0"]) expect(pairs).toContain(pair)
252
+ const rows = [...prefixCases().map((row) => ({ ...row, actual: readAfter(row.input, row.unit) })), ...SIGNED.map(([html, unit, expected]) => ({ input: html, unit, expected, actual: readHtml(html.replace("{V}", element(unit)), unit) }))]
253
+ expect(rows.filter((row) => row.actual !== row.expected)).toEqual([])
254
+ }, 120000)
255
+ })