oh-my-opencode 5.1.48 → 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 (495) 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 -316
  64. package/dist/skills/ulw-research/references/deliverable-phase.md +23 -21
  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 +12 -5
  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 +13 -4
  77. package/dist/skills/ulw-research/scripts/contracts.test.ts +6 -110
  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-polish.test.ts +4 -3
  84. package/dist/skills/ulw-research/scripts/css-hiding.mjs +158 -0
  85. package/dist/skills/ulw-research/scripts/css-lite.mjs +4 -2
  86. package/dist/skills/ulw-research/scripts/delivery-barrier-meaning.test.ts +186 -0
  87. package/dist/skills/ulw-research/scripts/delivery-barrier.test.ts +40 -87
  88. package/dist/skills/ulw-research/scripts/entities.mjs +13 -4
  89. package/dist/skills/ulw-research/scripts/fact-commands-fill.mjs +11 -1
  90. package/dist/skills/ulw-research/scripts/fact-commands-meaning.mjs +13 -9
  91. package/dist/skills/ulw-research/scripts/fact-commands-polish.mjs +40 -18
  92. package/dist/skills/ulw-research/scripts/fact-commands-verify.mjs +25 -8
  93. package/dist/skills/ulw-research/scripts/fact-commands.mjs +35 -16
  94. package/dist/skills/ulw-research/scripts/fact-derive-2.test.ts +149 -0
  95. package/dist/skills/ulw-research/scripts/fact-derive.mjs +7 -2
  96. package/dist/skills/ulw-research/scripts/fact-derive.test.ts +7 -192
  97. package/dist/skills/ulw-research/scripts/fact-fill.mjs +6 -4
  98. package/dist/skills/ulw-research/scripts/fact-ledger-kinds.mjs +6 -7
  99. package/dist/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +2 -1
  100. package/dist/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +2 -1
  101. package/dist/skills/ulw-research/scripts/fact-locate.mjs +15 -9
  102. package/dist/skills/ulw-research/scripts/fact-unit-dimensions-2.test.ts +149 -0
  103. package/dist/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +4 -168
  104. package/dist/skills/ulw-research/scripts/fact-unit-rules.test.ts +3 -229
  105. package/dist/skills/ulw-research/scripts/fidelity-claims.mjs +4 -3
  106. package/dist/skills/ulw-research/scripts/fidelity-csv.mjs +32 -0
  107. package/dist/skills/ulw-research/scripts/fidelity-lexicon.mjs +1 -1
  108. package/dist/skills/ulw-research/scripts/fidelity-originals.mjs +161 -0
  109. package/dist/skills/ulw-research/scripts/fidelity-policy.mjs +4 -4
  110. package/dist/skills/ulw-research/scripts/fidelity-records.mjs +81 -14
  111. package/dist/skills/ulw-research/scripts/fidelity-snapshot-readability.mjs +49 -0
  112. package/dist/skills/ulw-research/scripts/fidelity-sources.mjs +76 -0
  113. package/dist/skills/ulw-research/scripts/fidelity-trace.mjs +25 -11
  114. package/dist/skills/ulw-research/scripts/fidelity-value-unit.mjs +202 -0
  115. package/dist/skills/ulw-research/scripts/fidelity-values.mjs +40 -39
  116. package/dist/skills/ulw-research/scripts/fidelity-values.test.ts +8 -29
  117. package/dist/skills/ulw-research/scripts/gates-fidelity.mjs +8 -6
  118. package/dist/skills/ulw-research/scripts/gates-static-headings.test.ts +17 -0
  119. package/dist/skills/ulw-research/scripts/gates-static.mjs +10 -1
  120. package/dist/skills/ulw-research/scripts/html-lite.mjs +6 -2
  121. package/dist/skills/ulw-research/scripts/html-markup.mjs +2 -2
  122. package/dist/skills/ulw-research/scripts/html-text-map.mjs +140 -0
  123. package/dist/skills/ulw-research/scripts/html-text-map.test.ts +148 -0
  124. package/dist/skills/ulw-research/scripts/markdown-closers.mjs +126 -0
  125. package/dist/skills/ulw-research/scripts/md-lite-boundaries.test.ts +46 -0
  126. package/dist/skills/ulw-research/scripts/md-lite.mjs +1 -1
  127. package/dist/skills/ulw-research/scripts/number-grammar.mjs +33 -0
  128. package/dist/skills/ulw-research/scripts/number-grammar.test.ts +36 -0
  129. package/dist/skills/ulw-research/scripts/numerals.mjs +18 -7
  130. package/dist/skills/ulw-research/scripts/numerals.test.ts +3 -0
  131. package/dist/skills/ulw-research/scripts/outcome-start.mjs +66 -0
  132. package/dist/skills/ulw-research/scripts/outcome-start.test.ts +109 -0
  133. package/dist/skills/ulw-research/scripts/outcome.mjs +11 -22
  134. package/dist/skills/ulw-research/scripts/page-source-locator.test.ts +187 -0
  135. package/dist/skills/ulw-research/scripts/placeholder.mjs +18 -6
  136. package/dist/skills/ulw-research/scripts/polish-apply.mjs +3 -2
  137. package/dist/skills/ulw-research/scripts/polish-markup.test.ts +15 -1
  138. package/dist/skills/ulw-research/scripts/polish-partial.test.ts +42 -0
  139. package/dist/skills/ulw-research/scripts/raw-decode.mjs +110 -0
  140. package/dist/skills/ulw-research/scripts/raw-locator.mjs +47 -0
  141. package/dist/skills/ulw-research/scripts/raw-snapshot.test.ts +139 -0
  142. package/dist/skills/ulw-research/scripts/report-tools-2.test.ts +233 -0
  143. package/dist/skills/ulw-research/scripts/report-tools-3.test.ts +141 -0
  144. package/dist/skills/ulw-research/scripts/report-tools-4.test.ts +135 -0
  145. package/dist/skills/ulw-research/scripts/report-tools-5.test.ts +231 -0
  146. package/dist/skills/ulw-research/scripts/report-tools-commands.mjs +1 -1
  147. package/dist/skills/ulw-research/scripts/report-tools.mjs +7 -1
  148. package/dist/skills/ulw-research/scripts/report-tools.test.ts +5 -800
  149. package/dist/skills/ulw-research/scripts/snapshot-readability.test.ts +116 -0
  150. package/dist/skills/ulw-research/scripts/source-commands.mjs +79 -26
  151. package/dist/skills/ulw-research/scripts/source-integrity.test.ts +243 -0
  152. package/dist/skills/ulw-research/scripts/source-refresh.mjs +82 -0
  153. package/dist/skills/ulw-research/scripts/source-snapshot.mjs +73 -69
  154. package/dist/skills/ulw-research/scripts/source-snapshot.test.ts +7 -4
  155. package/dist/skills/ulw-research/scripts/source-view.mjs +89 -0
  156. package/dist/skills/ulw-research/scripts/sources-citation.mjs +203 -0
  157. package/dist/skills/ulw-research/scripts/sources-citation.test.ts +236 -0
  158. package/dist/skills/ulw-research/scripts/tests/contracts-support.ts +147 -0
  159. package/dist/skills/ulw-research/scripts/tests/delivery-barrier-session.ts +59 -0
  160. package/dist/skills/ulw-research/scripts/tests/derive-clock.ts +4 -0
  161. package/dist/skills/ulw-research/scripts/tests/fact-derive-support.ts +65 -0
  162. package/dist/skills/ulw-research/scripts/tests/fact-unit-dimensions-support.ts +41 -0
  163. package/dist/skills/ulw-research/scripts/tests/fact-unit-rules-support.ts +241 -0
  164. package/dist/skills/ulw-research/scripts/tests/fidelity-values-support.ts +30 -0
  165. package/dist/skills/ulw-research/scripts/tests/fixtures/conversion-probe.html +14 -0
  166. package/dist/skills/ulw-research/scripts/tests/fixtures/sources-shapes.html +20 -0
  167. package/dist/skills/ulw-research/scripts/tests/numeral-classes.ts +196 -0
  168. package/dist/skills/ulw-research/scripts/tests/report-tools-support.ts +182 -0
  169. package/dist/skills/ulw-research/scripts/tests/value-mode-support.ts +48 -0
  170. package/dist/skills/ulw-research/scripts/units.mjs +14 -3
  171. package/dist/skills/ulw-research/scripts/units.test.ts +10 -0
  172. package/dist/skills/ulw-research/scripts/value-mode-unit-classes.test.ts +255 -0
  173. package/dist/skills/ulw-research/scripts/value-mode-unit.test.ts +179 -0
  174. package/dist/skills/ulw-research/scripts/value-rules-cli.test.ts +157 -0
  175. package/dist/skills/ulw-research/scripts/value-rules-followups.test.ts +72 -0
  176. package/dist/skills/ulw-research/scripts/value-rules-priority.test.ts +80 -0
  177. package/dist/tools/delegate-task/executor-types.d.ts +1 -0
  178. package/dist/tools/delegate-task/sync-session-existence.d.ts +4 -0
  179. package/dist/tools/delegate-task/types.d.ts +1 -0
  180. package/dist/tools/skill/description-formatter.d.ts +2 -1
  181. package/dist/tools/skill/types.d.ts +6 -0
  182. package/dist/tui.js +131 -33
  183. package/package.json +18 -15
  184. package/packages/git-bash-mcp/package.json +1 -1
  185. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  186. package/packages/omo-codex/plugin/README.md +1 -1
  187. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  188. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  189. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  190. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  191. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  192. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  193. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  194. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  195. package/packages/omo-codex/plugin/components/lcx/skills/lcx-contribute-bug-fix/SKILL.md +51 -55
  196. package/packages/omo-codex/plugin/components/lcx/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  197. package/packages/omo-codex/plugin/components/lcx/skills/lcx-report-bug/SKILL.md +7 -7
  198. package/packages/omo-codex/plugin/components/lcx/skills/lcx-report-bug/agents/openai.yaml +1 -1
  199. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  200. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  201. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  202. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  203. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  204. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  205. package/packages/omo-codex/plugin/components/teammode/package.json +2 -2
  206. package/packages/omo-codex/plugin/components/telemetry/README.md +2 -2
  207. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +52 -15
  208. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +52 -19
  209. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  210. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  211. package/packages/omo-codex/plugin/components/telemetry/src/codex-hook.ts +2 -9
  212. package/packages/omo-codex/plugin/components/telemetry/src/posthog.ts +4 -13
  213. package/packages/omo-codex/plugin/components/telemetry/src/product-identity.ts +0 -2
  214. package/packages/omo-codex/plugin/components/telemetry/test/codex-hook-diagnostics.test.ts +0 -3
  215. package/packages/omo-codex/plugin/components/telemetry/test/codex-hook.test.ts +3 -8
  216. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +3 -0
  217. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  218. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  219. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  220. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  221. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  222. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +3 -0
  223. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  224. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  225. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  226. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  227. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  228. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  229. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  230. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  231. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  232. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  233. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  234. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  235. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  236. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  237. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  238. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  239. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  240. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  241. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  242. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  243. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  244. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  245. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  246. package/packages/omo-codex/plugin/package-lock.json +16 -16
  247. package/packages/omo-codex/plugin/package.json +1 -1
  248. package/packages/omo-codex/plugin/shared/package.json +1 -1
  249. package/packages/omo-codex/plugin/skills/ast-grep/README.md +1 -1
  250. package/packages/omo-codex/plugin/skills/ast-grep/install.ps1 +5 -3
  251. package/packages/omo-codex/plugin/skills/ast-grep/install.sh +4 -5
  252. package/packages/omo-codex/plugin/skills/ast-grep/references/install.md +2 -2
  253. package/packages/omo-codex/plugin/skills/ast-grep/scripts/ast_grep_helper.py +13 -3
  254. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  255. package/packages/omo-codex/plugin/skills/coding-agent-sessions/AGENTS.md +5 -4
  256. package/packages/omo-codex/plugin/skills/coding-agent-sessions/SKILL.md +11 -11
  257. package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/all-platforms.md +2 -2
  258. package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/codex.md +4 -4
  259. package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/agent_sessions/cli.py +1 -1
  260. package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/find-agent-sessions.py +6 -3
  261. package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +1 -1
  262. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +34 -12
  263. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +2 -2
  264. package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +2 -1
  265. package/packages/omo-codex/plugin/skills/frontend/references/design/component-catalogs.md +14 -10
  266. package/packages/omo-codex/plugin/skills/frontend/references/design/design-system-architecture.md +19 -6
  267. package/packages/omo-codex/plugin/skills/frontend/references/design/grounding.md +29 -0
  268. package/packages/omo-codex/plugin/skills/frontend/references/ui-ux-db/scripts/design_system.py +2 -0
  269. package/packages/omo-codex/plugin/skills/frontend/references/ui-ux-db/scripts/search.py +2 -0
  270. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +51 -55
  271. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  272. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +7 -7
  273. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  274. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +5 -4
  275. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -2
  276. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/__main__.py +9 -6
  277. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/summary.py +1 -1
  278. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +4 -4
  279. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +4 -2
  280. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +3 -0
  281. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +8 -4
  282. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +7 -316
  283. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +23 -21
  284. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +7 -3
  285. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger.md +42 -8
  286. package/packages/omo-codex/plugin/skills/ulw-research/references/fidelity-claim-kinds.md +1 -1
  287. package/packages/omo-codex/plugin/skills/ulw-research/references/markdown-blocks.md +12 -0
  288. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +12 -5
  289. package/packages/omo-codex/plugin/skills/ulw-research/references/research-collection.md +195 -0
  290. package/packages/omo-codex/plugin/skills/ulw-research/references/research-delivery.md +123 -0
  291. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claim-kinds.mjs +33 -5
  292. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +13 -4
  293. package/packages/omo-codex/plugin/skills/ulw-research/scripts/conversion-probe.mjs +229 -0
  294. package/packages/omo-codex/plugin/skills/ulw-research/scripts/css-hiding.mjs +158 -0
  295. package/packages/omo-codex/plugin/skills/ulw-research/scripts/css-lite.mjs +4 -2
  296. package/packages/omo-codex/plugin/skills/ulw-research/scripts/entities.mjs +13 -4
  297. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-fill.mjs +11 -1
  298. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +13 -9
  299. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-polish.mjs +40 -18
  300. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +25 -8
  301. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +35 -16
  302. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-derive.mjs +7 -2
  303. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +6 -4
  304. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +6 -7
  305. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-locate.mjs +15 -9
  306. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-claims.mjs +4 -3
  307. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-csv.mjs +32 -0
  308. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-lexicon.mjs +1 -1
  309. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-originals.mjs +161 -0
  310. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-policy.mjs +4 -4
  311. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-records.mjs +81 -14
  312. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-snapshot-readability.mjs +49 -0
  313. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-sources.mjs +76 -0
  314. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-trace.mjs +25 -11
  315. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-value-unit.mjs +202 -0
  316. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-values.mjs +40 -39
  317. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-fidelity.mjs +8 -6
  318. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +10 -1
  319. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +6 -2
  320. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-markup.mjs +2 -2
  321. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-text-map.mjs +140 -0
  322. package/packages/omo-codex/plugin/skills/ulw-research/scripts/markdown-closers.mjs +126 -0
  323. package/packages/omo-codex/plugin/skills/ulw-research/scripts/md-lite.mjs +1 -1
  324. package/packages/omo-codex/plugin/skills/ulw-research/scripts/number-grammar.mjs +33 -0
  325. package/packages/omo-codex/plugin/skills/ulw-research/scripts/numerals.mjs +18 -7
  326. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome-start.mjs +66 -0
  327. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +11 -22
  328. package/packages/omo-codex/plugin/skills/ulw-research/scripts/placeholder.mjs +18 -6
  329. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-apply.mjs +3 -2
  330. package/packages/omo-codex/plugin/skills/ulw-research/scripts/raw-decode.mjs +110 -0
  331. package/packages/omo-codex/plugin/skills/ulw-research/scripts/raw-locator.mjs +47 -0
  332. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +1 -1
  333. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +7 -1
  334. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +79 -26
  335. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-refresh.mjs +82 -0
  336. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-snapshot.mjs +73 -69
  337. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-view.mjs +89 -0
  338. package/packages/omo-codex/plugin/skills/ulw-research/scripts/sources-citation.mjs +203 -0
  339. package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +14 -3
  340. package/packages/omo-codex/plugin/test/payload-equivalence.test.mjs +6 -4
  341. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +4 -3
  342. package/packages/omo-codex/scripts/install-dist/install-local.mjs +338 -237
  343. package/packages/prompts-core/prompts/ultrawork/codex.md +3 -0
  344. package/packages/shared-skills/skill-source-filter.mjs +6 -2
  345. package/packages/shared-skills/skills/ast-grep/README.md +1 -1
  346. package/packages/shared-skills/skills/ast-grep/install.ps1 +5 -3
  347. package/packages/shared-skills/skills/ast-grep/install.sh +4 -5
  348. package/packages/shared-skills/skills/ast-grep/references/install.md +2 -2
  349. package/packages/shared-skills/skills/ast-grep/scripts/ast_grep_helper.py +13 -3
  350. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  351. package/packages/shared-skills/skills/coding-agent-sessions/AGENTS.md +5 -4
  352. package/packages/shared-skills/skills/coding-agent-sessions/SKILL.md +11 -11
  353. package/packages/shared-skills/skills/coding-agent-sessions/references/all-platforms.md +2 -2
  354. package/packages/shared-skills/skills/coding-agent-sessions/references/codex.md +4 -4
  355. package/packages/shared-skills/skills/coding-agent-sessions/scripts/agent_sessions/cli.py +1 -1
  356. package/packages/shared-skills/skills/coding-agent-sessions/scripts/find-agent-sessions.py +6 -3
  357. package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +1 -1
  358. package/packages/shared-skills/skills/frontend/SKILL.md +34 -12
  359. package/packages/shared-skills/skills/frontend/references/design/README.md +2 -2
  360. package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +2 -1
  361. package/packages/shared-skills/skills/frontend/references/design/component-catalogs.md +14 -10
  362. package/packages/shared-skills/skills/frontend/references/design/design-system-architecture.md +19 -6
  363. package/packages/shared-skills/skills/frontend/references/design/grounding.md +29 -0
  364. package/packages/shared-skills/skills/frontend/references/ui-ux-db/scripts/design_system.py +2 -0
  365. package/packages/shared-skills/skills/frontend/references/ui-ux-db/scripts/search.py +2 -0
  366. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +5 -4
  367. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -2
  368. package/packages/shared-skills/skills/ultimate-browsing/engine/__main__.py +9 -6
  369. package/packages/shared-skills/skills/ultimate-browsing/engine/summary.py +1 -1
  370. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +4 -4
  371. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +4 -2
  372. package/packages/shared-skills/skills/ulw-research/AGENTS.md +8 -4
  373. package/packages/shared-skills/skills/ulw-research/SKILL.md +7 -316
  374. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +23 -21
  375. package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +7 -3
  376. package/packages/shared-skills/skills/ulw-research/references/fact-ledger.md +42 -8
  377. package/packages/shared-skills/skills/ulw-research/references/fidelity-claim-kinds.md +1 -1
  378. package/packages/shared-skills/skills/ulw-research/references/markdown-blocks.md +12 -0
  379. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +12 -5
  380. package/packages/shared-skills/skills/ulw-research/references/research-collection.md +195 -0
  381. package/packages/shared-skills/skills/ulw-research/references/research-delivery.md +123 -0
  382. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.mjs +33 -5
  383. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.test.ts +37 -2
  384. package/packages/shared-skills/skills/ulw-research/scripts/claim-prefix.test.ts +76 -0
  385. package/packages/shared-skills/skills/ulw-research/scripts/contracts-validation.test.ts +83 -0
  386. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +13 -4
  387. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +6 -110
  388. package/packages/shared-skills/skills/ulw-research/scripts/conversion-altered.test.ts +154 -0
  389. package/packages/shared-skills/skills/ulw-research/scripts/conversion-originals.test.ts +297 -0
  390. package/packages/shared-skills/skills/ulw-research/scripts/conversion-probe-classes.test.ts +361 -0
  391. package/packages/shared-skills/skills/ulw-research/scripts/conversion-probe.mjs +229 -0
  392. package/packages/shared-skills/skills/ulw-research/scripts/conversion-probe.test.ts +218 -0
  393. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-polish.test.ts +4 -3
  394. package/packages/shared-skills/skills/ulw-research/scripts/css-hiding.mjs +158 -0
  395. package/packages/shared-skills/skills/ulw-research/scripts/css-lite.mjs +4 -2
  396. package/packages/shared-skills/skills/ulw-research/scripts/delivery-barrier-meaning.test.ts +186 -0
  397. package/packages/shared-skills/skills/ulw-research/scripts/delivery-barrier.test.ts +40 -87
  398. package/packages/shared-skills/skills/ulw-research/scripts/entities.mjs +13 -4
  399. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-fill.mjs +11 -1
  400. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +13 -9
  401. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-polish.mjs +40 -18
  402. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +25 -8
  403. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +35 -16
  404. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive-2.test.ts +149 -0
  405. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.mjs +7 -2
  406. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.test.ts +7 -192
  407. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +6 -4
  408. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +6 -7
  409. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +2 -1
  410. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +2 -1
  411. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.mjs +15 -9
  412. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-dimensions-2.test.ts +149 -0
  413. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +4 -168
  414. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-rules.test.ts +3 -229
  415. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-claims.mjs +4 -3
  416. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-csv.mjs +32 -0
  417. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.mjs +1 -1
  418. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-originals.mjs +161 -0
  419. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-policy.mjs +4 -4
  420. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-records.mjs +81 -14
  421. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-snapshot-readability.mjs +49 -0
  422. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-sources.mjs +76 -0
  423. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.mjs +25 -11
  424. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-value-unit.mjs +202 -0
  425. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.mjs +40 -39
  426. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.test.ts +8 -29
  427. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.mjs +8 -6
  428. package/packages/shared-skills/skills/ulw-research/scripts/gates-static-headings.test.ts +17 -0
  429. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +10 -1
  430. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +6 -2
  431. package/packages/shared-skills/skills/ulw-research/scripts/html-markup.mjs +2 -2
  432. package/packages/shared-skills/skills/ulw-research/scripts/html-text-map.mjs +140 -0
  433. package/packages/shared-skills/skills/ulw-research/scripts/html-text-map.test.ts +148 -0
  434. package/packages/shared-skills/skills/ulw-research/scripts/markdown-closers.mjs +126 -0
  435. package/packages/shared-skills/skills/ulw-research/scripts/md-lite-boundaries.test.ts +46 -0
  436. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.mjs +1 -1
  437. package/packages/shared-skills/skills/ulw-research/scripts/number-grammar.mjs +33 -0
  438. package/packages/shared-skills/skills/ulw-research/scripts/number-grammar.test.ts +36 -0
  439. package/packages/shared-skills/skills/ulw-research/scripts/numerals.mjs +18 -7
  440. package/packages/shared-skills/skills/ulw-research/scripts/numerals.test.ts +3 -0
  441. package/packages/shared-skills/skills/ulw-research/scripts/outcome-start.mjs +66 -0
  442. package/packages/shared-skills/skills/ulw-research/scripts/outcome-start.test.ts +109 -0
  443. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +11 -22
  444. package/packages/shared-skills/skills/ulw-research/scripts/page-source-locator.test.ts +187 -0
  445. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.mjs +18 -6
  446. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.mjs +3 -2
  447. package/packages/shared-skills/skills/ulw-research/scripts/polish-markup.test.ts +15 -1
  448. package/packages/shared-skills/skills/ulw-research/scripts/polish-partial.test.ts +42 -0
  449. package/packages/shared-skills/skills/ulw-research/scripts/raw-decode.mjs +110 -0
  450. package/packages/shared-skills/skills/ulw-research/scripts/raw-locator.mjs +47 -0
  451. package/packages/shared-skills/skills/ulw-research/scripts/raw-snapshot.test.ts +139 -0
  452. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-2.test.ts +233 -0
  453. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-3.test.ts +141 -0
  454. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-4.test.ts +135 -0
  455. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-5.test.ts +231 -0
  456. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +1 -1
  457. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +7 -1
  458. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +5 -800
  459. package/packages/shared-skills/skills/ulw-research/scripts/snapshot-readability.test.ts +116 -0
  460. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +79 -26
  461. package/packages/shared-skills/skills/ulw-research/scripts/source-integrity.test.ts +243 -0
  462. package/packages/shared-skills/skills/ulw-research/scripts/source-refresh.mjs +82 -0
  463. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.mjs +73 -69
  464. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.test.ts +7 -4
  465. package/packages/shared-skills/skills/ulw-research/scripts/source-view.mjs +89 -0
  466. package/packages/shared-skills/skills/ulw-research/scripts/sources-citation.mjs +203 -0
  467. package/packages/shared-skills/skills/ulw-research/scripts/sources-citation.test.ts +236 -0
  468. package/packages/shared-skills/skills/ulw-research/scripts/tests/contracts-support.ts +147 -0
  469. package/packages/shared-skills/skills/ulw-research/scripts/tests/delivery-barrier-session.ts +59 -0
  470. package/packages/shared-skills/skills/ulw-research/scripts/tests/derive-clock.ts +4 -0
  471. package/packages/shared-skills/skills/ulw-research/scripts/tests/fact-derive-support.ts +65 -0
  472. package/packages/shared-skills/skills/ulw-research/scripts/tests/fact-unit-dimensions-support.ts +41 -0
  473. package/packages/shared-skills/skills/ulw-research/scripts/tests/fact-unit-rules-support.ts +241 -0
  474. package/packages/shared-skills/skills/ulw-research/scripts/tests/fidelity-values-support.ts +30 -0
  475. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/conversion-probe.html +14 -0
  476. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/sources-shapes.html +20 -0
  477. package/packages/shared-skills/skills/ulw-research/scripts/tests/numeral-classes.ts +196 -0
  478. package/packages/shared-skills/skills/ulw-research/scripts/tests/report-tools-support.ts +182 -0
  479. package/packages/shared-skills/skills/ulw-research/scripts/tests/value-mode-support.ts +48 -0
  480. package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +14 -3
  481. package/packages/shared-skills/skills/ulw-research/scripts/units.test.ts +10 -0
  482. package/packages/shared-skills/skills/ulw-research/scripts/value-mode-unit-classes.test.ts +255 -0
  483. package/packages/shared-skills/skills/ulw-research/scripts/value-mode-unit.test.ts +179 -0
  484. package/packages/shared-skills/skills/ulw-research/scripts/value-rules-cli.test.ts +157 -0
  485. package/packages/shared-skills/skills/ulw-research/scripts/value-rules-followups.test.ts +72 -0
  486. package/packages/shared-skills/skills/ulw-research/scripts/value-rules-priority.test.ts +80 -0
  487. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/amp_redirect_stub.html +0 -7
  488. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/search_interstitial.html +0 -19
  489. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/wayback_available.json +0 -1
  490. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/wayback_snapshot.html +0 -1128
  491. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_fetch_chain.py +0 -77
  492. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +0 -86
  493. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +0 -279
  494. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_surrogate.py +0 -252
  495. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_surrogate_validators.py +0 -78
@@ -0,0 +1,229 @@
1
+ // conversion-probe.mjs - numeric multiset differences after source conversion.
2
+ // Inline markup may split one number; superscripts must not merge into it. Numbers are read with the
3
+ // one grouped-number grammar (number-grammar.mjs). A cell, row, list-item, block or line break is a
4
+ // hard boundary: a space never groups digits across it. A sign counts when no Latin, Greek or
5
+ // Cyrillic letter, digit or underscore touches it (an en dash before a digit is a minus too), and a
6
+ // "-" or U+2212 counts after ONE such letter ("\u0394\u22125") or after an exponent ("1e-5"); a run of
7
+ // two or more ("cite_note-7", "AB-12", "UTF-8", "2019-2023") is an identifier or range and carries no
8
+ // negative. Markdown markup a converter writes beside a sign never decides it: the closer run before it
9
+ // (read with the CommonMark inline grammar in markdown-closers.mjs: emphasis, code, strike, titled and
10
+ // parenthesised link and image targets, reference endings, backslash-escaped closers) is skipped to
11
+ // the first character it closes ("**2019**-2023" and '[1990](/wiki/1990 "1990")-2000' are ranges), and
12
+ // an opener run between the sign and its digits ("-**5**") keeps the sign. A raw-side character
13
+ // reference (`−`, ` `) is decoded on BOTH sides with the one entities.mjs table, so a
14
+ // verbatim copy probes clean. Raw inline HTML a converter leaves in the Markdown (pandoc's gfm writer
15
+ // emits `<sup>`, `<sub>`, `<img ... width=320>`, `<span>`, `<br>`) is read with the html-lite tag
16
+ // reader: sup, sub and break tags are boundaries, as on the raw side, and any other tag or comment is
17
+ // dropped with its attributes, so it is a closer or opener like Markdown markup. Digits a converter
18
+ // writes out from a link target, an image source, alt text, or a title cover a converted number only
19
+ // inside Markdown link or image syntax (`](...)` with any title and balanced parentheses,
20
+ // `![...](...)`, `<https://...>`, `[ref]: url "title"` with the title on the same or the next line),
21
+ // never in prose; a footnote
22
+ // definition's text ("[^1]: ...") is prose. Converter syntax is not a number: a line-start list marker
23
+ // ("1. ", "2) ", "(3) ", indented or after "> ") is exempt only while the source has an <ol> item with
24
+ // that number and the markers so far do not outnumber the <ol> items (so lazy "1. 1. 1." numbering
25
+ // over three items passes), a numeric reference label ("[x][1]" with its "[1]: url") only while both
26
+ // halves are present, and a footnote definition label ("[^1]:") only while its "[^1]" is used;
27
+ // otherwise each is a candidate. A converter's hard wrap may split a space-grouped number over one line
28
+ // break; the two halves read as one number only when the raw side holds that number and not both
29
+ // halves, so raw "12 345 and 12 and 345" wrapped inside "12 345" reports both halves (it fails safe).
30
+ // The built-in snapshot writes no attribute text, syntax, wraps or tags, and its text is already
31
+ // decoded, so it gets none of these allowances. Constructs outside this grammar are declared out of
32
+ // scope in references/fact-ledger.md: they report as altered (the safe direction).
33
+
34
+ import { existsSync, readFileSync } from "node:fs"
35
+ import { join } from "node:path"
36
+
37
+ import { CliError } from "./cli-support.mjs"
38
+ import { decodeEntities, parseHtml, PROSE_BREAKS, readTag } from "./html-lite.mjs"
39
+ import { escapedAt, inlineLinkSpans, stripClosers } from "./markdown-closers.mjs"
40
+ import { NUMBER_SOURCE, plainDecimal } from "./number-grammar.mjs"
41
+
42
+ const LETTER = "\\p{Script=Latin}\\p{Script=Greek}\\p{Script=Cyrillic}"
43
+ const WORD = new RegExp(`[${LETTER}\\p{N}_]`, "u")
44
+ const ALPHA = new RegExp(`[${LETTER}]`, "u")
45
+ const EXPONENT = new RegExp(`(?<![${LETTER}\\p{N}_.,])\\d+(?:[.,]\\d+)?[eE]$`, "u")
46
+ /** A dash, then any Markdown opener run, then a number; signed() decides whether the dash is a minus. */
47
+ const NUMBERS = new RegExp(`(?:([-\\u2212\\u2013])([*_~\`[]*))?(${NUMBER_SOURCE})`, "gu")
48
+ /** Sup, sub, datetime chunks and every break join no neighbour: neither a digit, a separator, nor whitespace. */
49
+ const BOUNDARY = "\u0001"
50
+ const CARRIED = ["href", "src", "alt", "title"]
51
+ const LIST_MARKER = /(?:^|\u0001)(?: ?>)* ?(?:\d+[.)]|\(\d+\)) /gu
52
+ const REF_USE = /\]\[(\d+)\]/gu
53
+ const REF_DEF = /(?:^|\u0001)(?: ?>)* ?\[(\d+)\]:/gu
54
+ const NOTE_USE = /\[\^(\d+)\](?!:)/gu
55
+ const NOTE_DEF = /(?:^|\u0001)(?: ?>)* ?\[\^(\d+)\]:/gu
56
+ const TITLE = `(?:"[^"\u0001]*"|'[^'\u0001]*'|\\([^)\u0001]*\\))`
57
+ /** A reference definition's title may sit alone on the next line (only whitespace after it). */
58
+ const REF_DEFINITION = `(?:^|\u0001) ?\\[(?!\\^)[^\\]\u0001]+\\]: ?[^\\s\u0001]*(?: ${TITLE}| ?\u0001 ?${TITLE}(?= ?(?:\u0001|$)))?`
59
+ const LINK_SYNTAX = new RegExp(`!\\[[^\\]\u0001]*\\](?:\\([^)\u0001]*\\))?|\\]\\([^)\u0001]*\\)|<https?:\\/\\/[^>\\s\u0001]+>|${REF_DEFINITION}`, "gu")
60
+ const TAG_NAME = /^[a-z][a-z0-9-]*$/u
61
+ /** What a tag leaves in the numeric text on either side: sup, sub and every break join no neighbour. */
62
+ const edgeOf = (tag) => (tag === "sup" || tag === "sub" || PROSE_BREAKS.has(tag) ? BOUNDARY : "")
63
+
64
+ /** Prose text, attribute text and <ol> item numbers of an HTML document; a non-HTML document is prose only. */
65
+ function numericText(source, html) {
66
+ if (!html) return { prose: decodeEntities(source), carried: "", listed: [] }
67
+ const chunks = []
68
+ const carried = []
69
+ const listed = []
70
+ const walk = (node) => {
71
+ if (node.tag === "#text") { chunks.push(node.text.replace(/\s+/gu, " ")); return }
72
+ for (const name of CARRIED) if (node.attrs?.[name] !== undefined) carried.push(node.attrs[name])
73
+ if (node.tag === "ol") {
74
+ const start = /^\s*\d+\s*$/u.test(node.attrs.start ?? "") ? Number(node.attrs.start) : 1
75
+ node.children.filter((child) => child.tag === "li").forEach((_, index) => listed.push(String(start + index)))
76
+ }
77
+ const edge = edgeOf(node.tag)
78
+ chunks.push(edge)
79
+ if (node.tag === "time" && node.attrs.datetime !== undefined) chunks.push(`${BOUNDARY}${node.attrs.datetime}${BOUNDARY}`)
80
+ for (const child of node.children) walk(child)
81
+ chunks.push(edge)
82
+ }
83
+ walk(parseHtml(source))
84
+ return { prose: chunks.join(""), carried: carried.join(BOUNDARY), listed }
85
+ }
86
+
87
+ /** Raw inline HTML in converted Markdown: sup, sub and break tags become boundaries, other tags and comments go. */
88
+ function inlineHtml(text) {
89
+ let out = ""
90
+ let kept = 0
91
+ for (let lt = text.indexOf("<"); lt !== -1; lt = text.indexOf("<", Math.max(lt + 1, kept))) {
92
+ const comment = text.startsWith("<!--", lt)
93
+ const name = lt + (text[lt + 1] === "/" ? 2 : 1)
94
+ if (escapedAt(text, lt) || (!comment && !/[a-z]/iu.test(text[name] ?? ""))) continue
95
+ const tag = comment ? null : readTag(text, name)
96
+ const end = comment ? text.indexOf("-->", lt + 4) + 3 : tag.end
97
+ if (end <= lt || (tag && !(TAG_NAME.test(tag.name) && text[end - 1] === ">")) || /\n[ \t\r]*\n/u.test(text.slice(lt, end))) continue
98
+ out += text.slice(kept, lt) + (tag ? edgeOf(tag.name) : "")
99
+ kept = end
100
+ }
101
+ return out + text.slice(kept)
102
+ }
103
+
104
+ /**
105
+ * Start offsets of numeric reference labels whose use ("][1]") and definition ("[1]:") are both present,
106
+ * and of footnote definition labels ("[^1]:") whose "[^1]" is used (the use itself mirrors the source).
107
+ */
108
+ function referenceLabels(flat) {
109
+ const uses = [...flat.matchAll(REF_USE)]
110
+ const defs = [...flat.matchAll(REF_DEF)]
111
+ const used = new Set(uses.map((match) => match[1]))
112
+ const defined = new Set(defs.map((match) => match[1]))
113
+ const paired = [...uses.filter((match) => defined.has(match[1])), ...defs.filter((match) => used.has(match[1]))]
114
+ const noted = new Set([...flat.matchAll(NOTE_USE)].map((match) => match[1]))
115
+ const notes = [...flat.matchAll(NOTE_DEF)].filter((match) => noted.has(match[1]))
116
+ return new Set([...paired, ...notes].map((match) => match.index + match[0].search(/\d/u)))
117
+ }
118
+
119
+ /** Whether the dash at `at` is a minus: Markdown closers (and a backslash escaping the dash) before it are skipped, as is an opener run before a lone letter. */
120
+ function signed(flat, at, dash) {
121
+ const before = stripClosers(flat.slice(Math.max(0, at - 512), escapedAt(flat, at) ? at - 1 : at))
122
+ const last = before.at(-1) ?? ""
123
+ if (!WORD.test(last)) return true
124
+ if (dash === "\u2013") return false
125
+ if (EXPONENT.test(before)) return true
126
+ return ALPHA.test(last) && !WORD.test(before.slice(0, -1).replace(/[*_~`[]+$/u, "").at(-1) ?? "")
127
+ }
128
+
129
+ /**
130
+ * Line breaks become boundaries, then whitespace runs one space; `linked` marks tokens inside link
131
+ * syntax, `marker` a line-start list marker, `label` a paired reference label, `gap` the text since
132
+ * the previous number.
133
+ */
134
+ function tokensOf(text, { links = false } = {}) {
135
+ const flat = text.replace(/\r\n?|\n|\f/gu, BOUNDARY).replace(/\s+/gu, " ")
136
+ const spans = links ? [...[...flat.matchAll(LINK_SYNTAX)].map((match) => [match.index, match.index + match[0].length]), ...inlineLinkSpans(flat)] : []
137
+ const markers = new Set([...flat.matchAll(LIST_MARKER)].map((match) => match.index + match[0].search(/\d/u)))
138
+ const labels = referenceLabels(flat)
139
+ let previousEnd = 0
140
+ return [...flat.matchAll(NUMBERS)].map((match, n) => {
141
+ const sign = match[1] !== undefined && signed(flat, match.index, match[1])
142
+ const at = sign ? match.index : match.index + match[0].length - match[3].length
143
+ const gap = flat.slice(previousEnd, at)
144
+ previousEnd = match.index + match[0].length
145
+ return {
146
+ n,
147
+ gap,
148
+ text: sign ? match[0] : match[3],
149
+ key: `${sign ? "-" : ""}${plainDecimal(match[3])}`,
150
+ linked: spans.some(([start, end]) => at >= start && at < end),
151
+ marker: markers.has(at),
152
+ label: labels.has(at),
153
+ }
154
+ })
155
+ }
156
+
157
+ /** The comparison key of one number token, so other tools can match a converted number against a probe record. */
158
+ export const numberKey = (token) => tokensOf(String(token))[0]?.key ?? null
159
+
160
+ /** The numbers in `texts` that the source's conversion probe recorded as altered (sources/<id>/conversion.json). */
161
+ export function alteredIn(sourceDir, texts) {
162
+ const record = join(sourceDir, "conversion.json")
163
+ let altered = []
164
+ if (existsSync(record)) {
165
+ try {
166
+ altered = JSON.parse(readFileSync(record, "utf8")).conversion_altered_numbers ?? []
167
+ } catch (error) {
168
+ throw new CliError(`${record} is not a readable conversion record (${error.message}); snapshot the source again`)
169
+ }
170
+ }
171
+ const keys = new Set(altered.map(numberKey))
172
+ return texts.flatMap((text) => tokensOf(String(text ?? ""))).filter((token) => keys.has(token.key)).map((token) => token.text)
173
+ }
174
+
175
+ /** Rejoin a grouped number a hard wrap split over one line break, when only the joined reading is in the raw counts. */
176
+ function joinWrapped(tokens, raw) {
177
+ const out = []
178
+ for (const token of tokens) {
179
+ const prev = out.at(-1)
180
+ const wrapped = prev?.n === token.n - 1 && /^ ?\u0001 ?$/u.test(token.gap) && !prev.linked && !token.linked
181
+ const [joined, ...more] = wrapped ? tokensOf(`${prev.text} ${token.text}`) : []
182
+ if (joined && more.length === 0 && raw.has(joined.key) && !(raw.has(prev.key) && raw.has(token.key))) {
183
+ out[out.length - 1] = { ...joined, n: token.n, text: `${prev.text}\n${token.text}` }
184
+ continue
185
+ }
186
+ out.push(token)
187
+ }
188
+ return out
189
+ }
190
+
191
+ const countOf = (tokens) => tokens.reduce((counts, token) => counts.set(token.key, (counts.get(token.key) ?? 0) + 1), new Map())
192
+
193
+ /** The tokens `counts` cannot cover; each covered token uses up one count. */
194
+ function uncovered(tokens, counts) {
195
+ return tokens.filter((token) => {
196
+ const remaining = counts.get(token.key) ?? 0
197
+ if (remaining === 0) return true
198
+ counts.set(token.key, remaining - 1)
199
+ return false
200
+ })
201
+ }
202
+
203
+ /**
204
+ * Preserve token spelling in diagnostics; the raw side is read as HTML, the converted side as Markdown
205
+ * with raw inline tags, both decoded with one entity table. `attributes: false`
206
+ * (the built-in snapshot) gives attribute digits no cover, converter syntax no exemption and no wrap rejoin. An exempt
207
+ * marker or label is not a converted number at all: it neither covers a raw number nor counts as altered.
208
+ */
209
+ export function probeConversion(raw, converted, { attributes = true } = {}) {
210
+ const source = numericText(raw, /<[a-z!/]/i.test(raw))
211
+ const before = tokensOf(source.prose)
212
+ const listed = new Set(source.listed)
213
+ let items = attributes ? source.listed.length : 0
214
+ const syntax = tokensOf(attributes ? decodeEntities(inlineHtml(converted)) : converted, { links: attributes })
215
+ .filter((token) => !(attributes && token.label) && !(token.marker && listed.has(token.key) && items-- > 0))
216
+ const after = attributes ? joinWrapped(syntax, countOf(before)) : syntax
217
+ const counts = countOf(before)
218
+ const extra = new Set(uncovered(after.filter((token) => !token.linked), counts))
219
+ for (const token of tokensOf(source.carried)) counts.set(token.key, (counts.get(token.key) ?? 0) + 1)
220
+ for (const token of uncovered(after.filter((token) => token.linked), counts)) extra.add(token)
221
+ const lost = uncovered(before, countOf(after)).map((token) => token.text)
222
+ return { altered: after.filter((token) => extra.has(token)).map((token) => token.text), lost, raw_count: before.length, converted_count: after.length }
223
+ }
224
+
225
+ export function runConversionProbe(argv) {
226
+ if (argv.length !== 2) throw new CliError("usage: source probe-conversion <raw-file> <converted-file>")
227
+ const result = probeConversion(readFileSync(argv[0], "utf8"), readFileSync(argv[1], "utf8"))
228
+ return { json: result, summary: `source probe-conversion: ${result.altered.length} altered, ${result.lost.length} lost`, exitCode: result.altered.length === 0 ? 0 : 1 }
229
+ }
@@ -0,0 +1,218 @@
1
+ import { describe, expect, onTestFinished, test } from "bun:test"
2
+ import { spawnSync } from "node:child_process"
3
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
4
+ import { tmpdir } from "node:os"
5
+ import { join } from "node:path"
6
+
7
+ import { numberKey } from "./conversion-probe.mjs"
8
+ import { readFidelityRecord } from "./fidelity-records.mjs"
9
+ import { writeSnapshot } from "./source-snapshot.mjs"
10
+
11
+ const CLI = join(import.meta.dir, "report-tools.mjs")
12
+ const FIXTURE = readFileSync(join(import.meta.dir, "tests/fixtures/conversion-probe.html"), "utf8")
13
+ const FLATTENED = FIXTURE.replace(/<[^>]*>/g, "")
14
+ // Superscript boundaries preserve 4.2 + 2, 27.3 + 1 and 10 + 9 in raw.
15
+ const ALTERED = ["4.22", "27.31", "109"]
16
+
17
+ function sandbox(): string {
18
+ const dir = mkdtempSync(join(tmpdir(), "conversion-probe-"))
19
+ onTestFinished(() => rmSync(dir, { recursive: true, force: true }))
20
+ return dir
21
+ }
22
+
23
+ function probe(raw: string, converted: string) {
24
+ const dir = sandbox()
25
+ const rawFile = join(dir, "raw.html")
26
+ const convertedFile = join(dir, "converted.txt")
27
+ writeFileSync(rawFile, raw)
28
+ writeFileSync(convertedFile, converted)
29
+ const proc = spawnSync("node", [CLI, "source", "probe-conversion", rawFile, convertedFile], { encoding: "utf8" })
30
+ expect(proc.error).toBeUndefined()
31
+ expect(proc.stdout, proc.stderr).not.toBe("")
32
+ return { exitCode: proc.status, json: JSON.parse(proc.stdout) }
33
+ }
34
+
35
+ describe("source probe-conversion", () => {
36
+ test("#given the probe page #when naively flattened #then only fabricated numbers are altered", () => {
37
+ const result = probe(FIXTURE, FLATTENED)
38
+ expect(result.exitCode).toBe(1)
39
+ // Stripping the adjacent table tags also joins Total's 2 to the first cell.
40
+ // The flatten glues Total's 2 onto 12,345 and 12,345 onto 15,678: "212,34515,678".
41
+ expect(result.json.altered).toEqual([...ALTERED, "212,34515", "678"])
42
+ expect(result.json.lost).toContain("2026")
43
+ expect(result.json.lost).not.toContain("99.9")
44
+ expect(result.json.altered).not.toContain("99.9")
45
+ })
46
+
47
+ test("#given a plain text #when compared to itself #then identity passes without alterations", () => {
48
+ const text = "USD 4.2 billion; 1,234.5 units; 1 234 567 people; -3.5 and 2019-2023."
49
+ const result = probe(text, text)
50
+ expect(result.exitCode).toBe(0)
51
+ expect(result.json.altered).toEqual([])
52
+ expect(result.json.lost).toEqual([])
53
+ expect(result.json.converted_count).toBe(result.json.raw_count)
54
+ })
55
+
56
+ test("#given a tag-split decimal #when compared with text #then each side has one number", () => {
57
+ const result = probe("<span>1</span><span>,</span><span>234.5</span>", "1,234.5")
58
+ expect(result).toEqual({ exitCode: 0, json: { altered: [], lost: [], raw_count: 1, converted_count: 1 } })
59
+ })
60
+
61
+ test.each(["\u202f", "\u00a0"])("#given %s thousands separators #when converted to spaces #then grouping is equivalent", (separator: string) => {
62
+ const result = probe(`1${separator}234${separator}567`, "1 234 567")
63
+ expect(result).toEqual({ exitCode: 0, json: { altered: [], lost: [], raw_count: 1, converted_count: 1 } })
64
+ })
65
+
66
+ test("#given a machine date #when its attribute is removed #then it is lost, not altered", () => {
67
+ const result = probe('<time datetime="2026-10-01T09:00:00Z">1 Oct</time>', "1 Oct")
68
+ expect(result.exitCode).toBe(0)
69
+ expect(result.json.altered).toEqual([])
70
+ expect(result.json.lost).toEqual(["2026", "10", "01", "09", "00", "00"])
71
+ })
72
+
73
+ test("#given repeated signed decimals #when one count increases #then multiset subtraction preserves the excess", () => {
74
+ const result = probe("-4.2 +7 2", "-4.2 +7 2; 2")
75
+ expect(result.exitCode).toBe(1)
76
+ expect(result.json.altered).toEqual(["2"])
77
+ expect(result.json.lost).toEqual([])
78
+ })
79
+
80
+ test("#given hidden text #when omitted by conversion #then the number is only lost", () => {
81
+ const result = probe('<span style="display:none">99.9</span>', "")
82
+ expect(result.exitCode).toBe(0)
83
+ expect(result.json.altered).toEqual([])
84
+ expect(result.json.lost).toEqual(["99.9"])
85
+ })
86
+ })
87
+
88
+ describe("one grouped-number grammar: a digit glued after the last group is an alteration", () => {
89
+ test("#given a footnote glued onto a grouped number #when probed #then it is altered", () => {
90
+ expect(probe("$1,234<sup>5</sup> million", "$1,2345 million").json.altered).toEqual(["1,2345"])
91
+ expect(probe("<table><tr><td>12,345<sup>1</sup></td></tr></table>", "12,3451").json.altered).toEqual(["12,3451"])
92
+ })
93
+
94
+ test("#given the same cell through the built-in snapshot #when written #then conversion.json lists it", () => {
95
+ const { dir } = writeSnapshot(join(sandbox(), "sources"), "S1", { path: "t.html", html: "<table><tr><td>12,345<sup>1</sup></td></tr></table>", extractor: "model", observed_at: "2026-10-25T00:00:00Z" })
96
+ expect(readFidelityRecord(join(dir, "conversion.json"), "source_conversion").conversion_altered_numbers).toEqual(["12,3451"])
97
+ })
98
+ })
99
+
100
+ describe("Markdown-shaped converter output reports only real artifacts", () => {
101
+ const RAW = [
102
+ "<p>Revenue was 4.2<sup>2</sup> billion.<sup><a href=\"#cite_note-7\">[7]</a></sup></p>",
103
+ "<p><a href=\"https://example.com/2024/03/report-4\">Report</a> covers q4-2023.</p>",
104
+ "<p><img src=\"chart-640x480.png\" alt=\"Chart 2023\" title=\"Fig 4\"></p>",
105
+ "<table><tr><td>12,345<sup>1</sup></td><td>1&#8201;234&#8201;567</td></tr></table>",
106
+ "<p>From 2019\u20132023 the index fell \u22123.5 points.</p>",
107
+ ].join("\n")
108
+ const MARKDOWN = [
109
+ "Revenue was 4.22 billion.[[7]](#cite_note-7)",
110
+ "",
111
+ "[Report](https://example.com/2024/03/report-4) covers q4-2023.",
112
+ "",
113
+ "![Chart 2023](chart-640x480.png \"Fig 4\")",
114
+ "",
115
+ "| 12,3451 | 1\u2009234\u2009567 |",
116
+ "",
117
+ "From 2019-2023 the index fell -3.5 points.",
118
+ ].join("\n")
119
+
120
+ test("#given markdownify-shaped text #when probed #then altered is exactly 4.22 and 12,3451", () => {
121
+ expect(probe(RAW, MARKDOWN).json.altered).toEqual(["4.22", "12,3451"])
122
+ })
123
+
124
+ test("#given the same page through the built-in snapshot #when written #then only the real artifacts are altered", () => {
125
+ const { dir } = writeSnapshot(join(sandbox(), "sources"), "S1", { path: "m.html", html: RAW, extractor: "model", observed_at: "2026-10-25T00:00:00Z" })
126
+ expect(readFidelityRecord(join(dir, "conversion.json"), "source_conversion").conversion_altered_numbers).toEqual(["4.22", "12,3451"])
127
+ })
128
+
129
+ test("#given a converted text that looks like a tag #when probed #then it is never parsed as HTML", () => {
130
+ expect(probe("<p>if a&lt;b then revenue was 4.2<sup>2</sup> billion</p>", "if a<b then revenue was 4.22 billion").json.altered).toEqual(["4.22"])
131
+ })
132
+ })
133
+
134
+ describe("whitespace and separators match the snapshot side", () => {
135
+ for (const [raw, converted] of [
136
+ ["<p>1&#8201;234&#8201;567</p>", "1 234 567"],
137
+ ["<p>1&#8199;234</p>", "1 234"],
138
+ ["<table><tr><td>2,500<br>300</td></tr></table>", "2,500 300"],
139
+ ["<ul><li><p>in 2023</p><p>500 sites</p></li></ul>", "in 2023 500 sites"],
140
+ ["<p>1,250\n300</p>", "1,250 300"],
141
+ ["<p><span>1</span>\n<span>234</span></p>", "1 234"],
142
+ ]) {
143
+ test(`#given ${JSON.stringify(raw)} #when converted to ${JSON.stringify(converted)} #then nothing is altered`, () => {
144
+ const result = probe(raw, converted)
145
+ expect(result.json.altered).toEqual([])
146
+ expect(result.exitCode).toBe(0)
147
+ })
148
+ }
149
+ })
150
+
151
+ test("#given HTML snapshot conversion #when written #then source warnings use the fidelity envelope", () => {
152
+ const { dir } = writeSnapshot(join(sandbox(), "sources"), "S1", {
153
+ path: "fixture.html", html: FIXTURE, extractor: "model", observed_at: "2026-10-25T00:00:00Z",
154
+ })
155
+ const record = readFidelityRecord(join(dir, "conversion.json"), "source_conversion")
156
+ expect(record.conversion_altered_numbers).toEqual(ALTERED)
157
+ expect(record.record_kind).toBe("source_conversion")
158
+ expect(record.artifact_sha256).toMatch(/^[0-9a-f]{64}$/)
159
+ expect(record.report).toBe(join(dir, "source.html"))
160
+ expect(record.written_at).toBe("2026-10-25T00:00:00.000Z")
161
+ expect(record.defects).toEqual([])
162
+ })
163
+
164
+ describe("attribute digits cover only a number inside Markdown link or image syntax", () => {
165
+ const GLUED = "<p>Output reached 10<sup>9</sup> kWh</p>"
166
+ const BOOK = '<p><a href="https://books.example/b?id=x&amp;pg=PA109">book</a></p>'
167
+
168
+ test("#given an href page number 109 #when the prose shows a glued 10<sup>9</sup> #then 109 is altered in flat and Markdown output", () => {
169
+ expect(probe(`${GLUED}${BOOK}`, "Output reached 109 kWh\n\nbook").json.altered).toEqual(["109"])
170
+ expect(probe(`${GLUED}${BOOK}`, "Output reached 109 kWh\n\n[book](https://books.example/b?id=x&pg=PA109)").json.altered).toEqual(["109"])
171
+ })
172
+
173
+ test("#given #ref1..#ref45 links #when 4<sup>2</sup> is glued into 42 #then 42 is altered", () => {
174
+ const links = Array.from({ length: 45 }, (_, k) => `<a href="#ref${k + 1}">note</a>`).join(" ")
175
+ const markdown = Array.from({ length: 45 }, (_, k) => `[note](#ref${k + 1})`).join(" ")
176
+ expect(probe(`<p>Area grew 4<sup>2</sup> times.</p><p>${links}</p>`, `Area grew 42 times.\n\n${markdown}`).json.altered).toEqual(["42"])
177
+ })
178
+
179
+ test("#given an image named chart-109.png #when the built-in snapshot writes the page #then 109 is altered", () => {
180
+ const { dir } = writeSnapshot(join(sandbox(), "sources"), "S1", { path: "g.html", html: `${GLUED}<p><img src="chart-109.png"></p>`, extractor: "model", observed_at: "2026-10-25T00:00:00Z" })
181
+ expect(readFidelityRecord(join(dir, "conversion.json"), "source_conversion").conversion_altered_numbers).toEqual(["109"])
182
+ })
183
+ })
184
+
185
+ describe("cell, row, list-item and line breaks never group digits", () => {
186
+ for (const [raw, converted] of [
187
+ ["<table><tr><td>3</td><td>250</td><td>100</td></tr></table>", "| 3 | 250 | 100 |"],
188
+ ["<table><tr><td>12</td><td>345</td></tr></table>", "| 12 | 345 |"],
189
+ ["<ul><li>3</li><li>250</li></ul>", "- 3\n- 250"],
190
+ ["<p>3</p><p>250</p>", "3\n250"],
191
+ ]) {
192
+ test(`#given ${JSON.stringify(raw)} #when converted to ${JSON.stringify(converted)} #then nothing is altered or lost`, () => {
193
+ expect(probe(raw, converted)).toMatchObject({ exitCode: 0, json: { altered: [], lost: [] } })
194
+ })
195
+ }
196
+
197
+ test("#given 1 234 567 inside one cell #when converted to a Markdown row #then it stays one grouped number", () => {
198
+ expect(probe("<table><tr><td>1 234 567</td><td>8</td></tr></table>", "| 1 234 567 | 8 |")).toEqual({ exitCode: 0, json: { altered: [], lost: [], raw_count: 2, converted_count: 2 } })
199
+ })
200
+ })
201
+
202
+ describe("a period group needs an unambiguous shape; a dropped minus is altered", () => {
203
+ test("#given 1.234 and 1,234 #when keyed #then they differ, and 1.234.567 still groups", () => {
204
+ expect(numberKey("1.234")).not.toBe(numberKey("1,234"))
205
+ expect(numberKey("1.234.567")).toBe(numberKey("1,234,567"))
206
+ expect(numberKey("1.234,5")).toBe(numberKey("1,234.5"))
207
+ })
208
+
209
+ test("#given a glued 12.34<sup>5</sup> beside a real 12,345 #when flattened #then the glued 12.345 is the altered token", () => {
210
+ expect(probe("<p>12.34<sup>5</sup> and 12,345</p>", "12.345 and 12,345").json.altered).toEqual(["12.345"])
211
+ })
212
+
213
+ test("#given a minus touching Hangul or an en dash minus #when the sign is dropped #then the unsigned number is altered", () => {
214
+ expect(probe("\uAC10\uC18C\uC728-3.5%", "\uAC10\uC18C\uC728 3.5%").json.altered).toEqual(["3.5"])
215
+ expect(probe("fell \u20135%", "fell 5%").json.altered).toEqual(["5"])
216
+ expect(probe("cite_note-7 and 2019\u20132023", "cite_note-7 and 2019-2023").json.altered).toEqual([])
217
+ })
218
+ })
@@ -173,7 +173,7 @@ describe("fact polish-apply prints every rejection reason verbatim", () => {
173
173
  })
174
174
 
175
175
  describe("--changed-since stays fail-closed", () => {
176
- const { pairsChangedSince } = polishCommand as unknown as { pairsChangedSince: (pairs: unknown[], mapPath: string, ledger: unknown, claims: unknown) => unknown[] }
176
+ const { pairsChangedSince } = polishCommand as unknown as { pairsChangedSince: (pairs: unknown[], mapPath: string, ledger: unknown, claims: unknown) => unknown }
177
177
  const artifact = file("pre-polish.html", doc("<p>The span is 320 m [S1].</p>"))
178
178
 
179
179
  test("#given a fill map without artifact_sha256 #when --changed-since reads it #then it refuses", () => {
@@ -181,9 +181,10 @@ describe("--changed-since stays fail-closed", () => {
181
181
  expect(outcome(() => pairsChangedSince([], mapPath, LEDGER, null))).toContain("fill map has no artifact_sha256")
182
182
  })
183
183
 
184
- test("#given a fill map whose artifact changed since fill #when read #then it still refuses", () => {
184
+ test("#given a fill map whose artifact changed since fill #when read #then nothing is filtered out: every pair is re-checked", () => {
185
185
  const mapPath = file("fill-map-stale.json", JSON.stringify({ artifact, artifact_sha256: "0".repeat(64), format: "html", entries: [] }))
186
- expect(outcome(() => pairsChangedSince([], mapPath, LEDGER, null))).toContain("changed after fill")
186
+ const pairs = [{ kind: "quote", sentence_key: "s1", claim_id: null, sentence: "The span is 320 m [S1]." }]
187
+ expect(pairsChangedSince(pairs, mapPath, LEDGER, null)).toEqual({ pairs, fill_map: mapPath, narrowed: false })
187
188
  })
188
189
  })
189
190
 
@@ -0,0 +1,158 @@
1
+ // css-hiding.mjs - whether CSS hides an element of a page from every reader, from its inline style and
2
+ // the page's own <style> rules (external stylesheets and scripts are not evaluated).
3
+ //
4
+ // Text counts as reader-visible when it is visible under at least one condition. So a rule can hide
5
+ // only when it certainly applies: it sits outside every at-rule except @layer (and @media all), its
6
+ // <style> element applies everywhere (no media other than all, CSS type, not in <noscript>), and its
7
+ // selector is one tag (case-insensitive), one .class or one #id (case-sensitive). A rule that only
8
+ // might apply (inside @media, @supports, @container, @scope, @starting-style, @document, @keyframes,
9
+ // nested in another style rule, or with a compound, combinator, attribute or pseudo-class selector)
10
+ // never hides, but it can make text visible when its subject could match and it outranks the hiding
11
+ // declaration in the cascade (origin and importance, inline style, layer order, specificity, order).
12
+ // Hiding properties: display:none and content-visibility:hidden remove the subtree;
13
+ // visibility:hidden|collapse is inherited and a descendant's visibility:visible shows it again.
14
+ // The hidden attribute (except hidden="until-found") and a closed <dialog> are the user-agent
15
+ // display:none, which any author display declaration overrides. opacity, clip, zero size, font-size,
16
+ // off-screen positions and colors do not hide: the text still renders or is read aloud.
17
+ // Plain Node ESM, sibling imports only.
18
+
19
+ import { scanRules } from "./css-lite.mjs"
20
+
21
+ const PROPS = new Set(["display", "visibility", "content-visibility"])
22
+ const INHERITED = new Set(["inherit", "unset", "revert", "revert-layer"])
23
+ const IMPORTANT = /!\s*important\s*$/i
24
+ const SIMPLE = /^(?:[.#][^\s.#:[\]>+~,()*"'\\]+|[a-z][a-z0-9-]*)$/iu
25
+ const PSEUDO_ELEMENT = /::|:(?:before|after|first-line|first-letter)\b/i
26
+ const NAME = "[^\\s.#:[\\]>+~,()*\"'\\\\]+"
27
+ const STYLE_IN_SVG = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi
28
+
29
+ /** [tier (0 user agent, 1 author, 2 author !important), inline, layer, specificity, order]: higher wins. */
30
+ const outranks = (a, b) => {
31
+ for (let k = 0; k < a.length; k++) if (a[k] !== b[k]) return a[k] > b[k]
32
+ return false
33
+ }
34
+
35
+ function specificity(selector) {
36
+ const core = selector.replace(/\[[^\]]*\]/g, " [] ")
37
+ const ids = core.match(new RegExp(`#${NAME}`, "gu"))?.length ?? 0
38
+ const classes = (core.match(new RegExp(`\\.${NAME}`, "gu"))?.length ?? 0) + (core.match(/\[\]|:(?!:)[\w-]+/g)?.length ?? 0)
39
+ const tags = core.match(/(?:^|[\s>+~(])[a-z][\w-]*/gi)?.length ?? 0
40
+ return ids * 10000 + classes * 100 + tags
41
+ }
42
+
43
+ /** The subject compound of a selector: its tag, classes and ids (attribute and pseudo-class parts dropped). */
44
+ function subject(selector) {
45
+ const compound = selector.trim().match(/(?:[^\s>+~[\]()]|\[[^\]]*\]|\([^)]*\))+$/)?.[0] ?? ""
46
+ const core = compound.replace(/\([^)]*\)/g, "").replace(/\[[^\]]*\]/g, "").replace(/:+[\w-]+/g, "")
47
+ const tag = core.match(/^[a-z][a-z0-9-]*/i)?.[0].toLowerCase() ?? null
48
+ const classes = [...core.matchAll(new RegExp(`\\.(${NAME})`, "gu"))].map((match) => match[1])
49
+ const ids = [...core.matchAll(new RegExp(`#(${NAME})`, "gu"))].map((match) => match[1])
50
+ return { tag, classes, ids, key: ids[0] ? `#${ids[0]}` : classes[0] ? `.${classes[0]}` : tag ?? "*" }
51
+ }
52
+
53
+ const classList = (node) => String(node.attrs?.class ?? "").split(/[ \t\n\f\r]+/).filter(Boolean)
54
+
55
+ function matches(sub, node) {
56
+ if (sub.tag !== null && sub.tag !== node.tag) return false
57
+ const names = classList(node)
58
+ return sub.classes.every((name) => names.includes(name)) && sub.ids.every((id) => id === node.attrs?.id)
59
+ }
60
+
61
+ /** Whether every enclosing at-rule leaves the rule unconditional (@layer, @media all), its layer name, and keyframe steps. */
62
+ function context(atRule) {
63
+ const entries = atRule ? atRule.split(/\s(?=@)/) : []
64
+ const layers = entries.filter((entry) => /^@layer\b/i.test(entry)).map((entry) => entry.slice(6).trim())
65
+ const conditional = entries.some((entry) => !/^@layer\b/i.test(entry) && !/^@media(?:\s+all)?\s*$/i.test(entry))
66
+ return { conditional, layer: layers.length ? layers.join(".") : null, keyframe: entries.some((entry) => /^@(?:-[a-z]+-)?keyframes\b/i.test(entry)) }
67
+ }
68
+
69
+ const declaration = (value) => ({ value: value.replace(IMPORTANT, "").trim().toLowerCase(), important: IMPORTANT.test(value) })
70
+
71
+ /** Every <style> text that applies to the page, with whether it applies only under a condition. */
72
+ function sheets(root) {
73
+ const found = []
74
+ const visit = (node, conditional) => {
75
+ if (node.tag === "template") return
76
+ const inner = conditional || node.tag === "noscript"
77
+ if (node.tag === "style") {
78
+ const type = String(node.attrs?.type ?? "").trim().toLowerCase()
79
+ const media = String(node.attrs?.media ?? "").trim().toLowerCase()
80
+ if (type === "" || type === "text/css") found.push({ css: String(node.raw ?? ""), conditional: inner || (media !== "" && media !== "all") })
81
+ }
82
+ if (node.tag === "svg") for (const match of String(node.raw ?? "").matchAll(STYLE_IN_SVG)) found.push({ css: match[1], conditional: inner })
83
+ for (const child of node.children ?? []) visit(child, inner)
84
+ }
85
+ visit(root, false)
86
+ return found
87
+ }
88
+
89
+ /** Index every rule declaring display, visibility or content-visibility by its subject key. */
90
+ function indexRules(root) {
91
+ const index = new Map()
92
+ const layers = []
93
+ const layerRank = (name, important) => {
94
+ if (name === null) return important ? -1 : 1e6
95
+ if (!layers.includes(name)) layers.push(name)
96
+ return important ? 1e6 - layers.indexOf(name) : layers.indexOf(name)
97
+ }
98
+ let order = 0
99
+ for (const sheet of sheets(root)) {
100
+ const css = sheet.css.replace(/<!--|-->/g, " ")
101
+ for (const match of css.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, "").matchAll(/@layer\s+([^{;]*)[{;]/gi)) {
102
+ for (const name of match[1].split(",").map((part) => part.trim())) if (name && !layers.includes(name)) layers.push(name)
103
+ }
104
+ for (const rule of scanRules(css)) {
105
+ const decls = rule.declarations.filter((entry) => PROPS.has(entry.prop))
106
+ if (!decls.length || rule.selector.startsWith("@")) continue
107
+ const { conditional, layer, keyframe } = context(rule.atRule)
108
+ if (keyframe) continue
109
+ for (const selector of rule.selector.split(/,(?![^()[\]]*[)\]])/).map((part) => part.trim())) {
110
+ if (!selector || PSEUDO_ELEMENT.test(selector)) continue
111
+ const sub = subject(selector)
112
+ const certain = !sheet.conditional && !conditional && !rule.nested && SIMPLE.test(selector)
113
+ const spec = specificity(selector)
114
+ for (const entry of decls) {
115
+ const { value, important } = declaration(entry.value)
116
+ const rank = [important ? 2 : 1, 0, layerRank(layer, important), spec, order++]
117
+ if (!index.has(sub.key)) index.set(sub.key, [])
118
+ index.get(sub.key).push({ sub, prop: entry.prop, value, rank, certain })
119
+ }
120
+ }
121
+ }
122
+ }
123
+ return index
124
+ }
125
+
126
+ /** The values a property can take on the element: the certain winner, plus every possible rule that outranks it. */
127
+ function possible(candidates, prop) {
128
+ let top = null
129
+ for (const entry of candidates) if (entry.prop === prop && entry.certain && (top === null || outranks(entry.rank, top.rank))) top = entry
130
+ const values = [top === null ? "unset" : top.value]
131
+ for (const entry of candidates) if (entry.prop === prop && !entry.certain && (top === null || outranks(entry.rank, top.rank))) values.push(entry.value)
132
+ return values
133
+ }
134
+
135
+ /**
136
+ * cssVisibility(root) -> (node, parentShown) => null when CSS removes the element and its subtree from every
137
+ * rendering, else whether its own text can be seen (visibility, inherited from parentShown).
138
+ */
139
+ export function cssVisibility(root) {
140
+ const index = indexRules(root)
141
+ return (node, parentShown) => {
142
+ const attrs = node.attrs ?? {}
143
+ const keys = ["*", node.tag, ...classList(node).map((name) => `.${name}`), attrs.id === undefined ? null : `#${attrs.id}`]
144
+ const candidates = []
145
+ for (const key of new Set(keys)) for (const entry of index.get(key) ?? []) if (matches(entry.sub, node)) candidates.push(entry)
146
+ const hiddenAttr = attrs.hidden !== undefined && String(attrs.hidden).trim().toLowerCase() !== "until-found"
147
+ if (hiddenAttr || (node.tag === "dialog" && attrs.open === undefined)) candidates.push({ prop: "display", value: "none", rank: [0, 0, 0, 0, -1], certain: true })
148
+ const inline = attrs.style ? (scanRules(`x{${attrs.style}}`)[0]?.declarations ?? []) : []
149
+ inline.forEach((entry, at) => {
150
+ if (!PROPS.has(entry.prop)) return
151
+ const { value, important } = declaration(entry.value)
152
+ candidates.push({ prop: entry.prop, value, rank: [important ? 2 : 1, 1, 0, 0, at], certain: true })
153
+ })
154
+ if (possible(candidates, "display").every((value) => value === "none")) return null
155
+ if (possible(candidates, "content-visibility").every((value) => value === "hidden")) return null
156
+ return possible(candidates, "visibility").some((value) => (INHERITED.has(value) ? parentShown : value !== "hidden" && value !== "collapse"))
157
+ }
158
+ }
@@ -57,8 +57,9 @@ function parseDeclarations(text) {
57
57
  }
58
58
 
59
59
  /**
60
- * @returns {{ selector: string, declarations: { prop: string, value: string }[], atRule?: string }[]}
61
- * atRule is the chain of enclosing grouping at-rule preludes joined by a space.
60
+ * @returns {{ selector: string, declarations: { prop: string, value: string }[], atRule?: string, nested?: true }[]}
61
+ * atRule is the chain of enclosing grouping at-rule preludes joined by a space; nested is true for a
62
+ * rule written inside another style rule (CSS nesting).
62
63
  */
63
64
  export function scanRules(css) {
64
65
  if (typeof css !== "string") return []
@@ -88,6 +89,7 @@ export function scanRules(css) {
88
89
  const atChain = stack.filter((entry) => entry.group).map((entry) => entry.prelude)
89
90
  const rule = { selector: frame.prelude, declarations: parseDeclarations(frame.body + buffer) }
90
91
  if (atChain.length) rule.atRule = atChain.join(" ")
92
+ if (stack.some((entry) => !entry.group)) rule.nested = true
91
93
  slots[frame.slot] = rule
92
94
  }
93
95
  buffer = ""