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
@@ -15,7 +15,7 @@
15
15
  // fidelity record exists, the barrier refuses the unverified edition regardless of it, and the same
16
16
  // exemption is the only way a required record may be absent.
17
17
 
18
- import { existsSync, readFileSync } from "node:fs"
18
+ import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs"
19
19
  import { join } from "node:path"
20
20
 
21
21
  import { CliError, readJsonFile, sha256Of, writeJsonAtomic } from "./cli-support.mjs"
@@ -24,7 +24,10 @@ import { meaningCheckEnabled, requireLockedBrief } from "./fidelity-brief.mjs"
24
24
  import { blocksDelivery, deliveryBlockReason } from "./gates-fidelity.mjs"
25
25
 
26
26
  export const FIDELITY_RECORD = "fidelity-defects.json"
27
+ /** The legacy session meaning record: a candidate for every edition whose bytes it claims, like any edition record (newest run wins). */
27
28
  export const MEANING_RECORD = "fidelity-meaning-defects.json"
29
+ /** The meaning record of one delivered edition, named by its outcome format (`fact meaning-check --edition`). */
30
+ export const meaningRecordName = (format) => `fidelity-meaning-defects.${format}.json`
28
31
 
29
32
  /** The fields every fidelity record carries, whatever its kind. */
30
33
  export const RECORD_ENVELOPE = Object.freeze(["record_kind", "artifact_sha256", "report", "written_at", "defects"])
@@ -39,13 +42,67 @@ function meaningRequired({ sessionDir }) {
39
42
  }
40
43
 
41
44
  const subjectOf = (row) => row.renderedFrom ?? row.path ?? null
45
+ const hashOf = (path) => (existsSync(path) ? sha256Of(readFileSync(path)) : null)
42
46
 
43
47
  /** One record per delivered edition: the file its row names with --verified-by, bound to that edition's bytes. */
44
48
  const perEdition = (sessionDir, delivered) =>
45
49
  delivered.map((row) => ({ path: row.verifiedBy ?? null, declared: row.verifiedBy !== undefined, format: row.format, at: `${row.format}: `, subjects: [subjectOf(row)].filter((subject) => subject !== null) }))
46
50
 
47
- /** One record per session, in the session directory, bound to any delivered edition's bytes. */
48
- const perSession = (file) => (sessionDir, delivered) => [{ path: join(sessionDir, file), declared: false, format: null, at: "", subjects: delivered.map(subjectOf).filter((subject) => subject !== null) }]
51
+ const sessionTarget = (sessionDir, file) => ({ path: join(sessionDir, file), declared: false, format: null, at: "", subjects: [] })
52
+
53
+ /** Include every existing session fidelity record, even when no delivered row attaches it. */
54
+ const perEditionAndSession = (sessionDir, delivered) => [
55
+ ...perEdition(sessionDir, delivered),
56
+ ...(existsSync(join(sessionDir, FIDELITY_RECORD)) ? [sessionTarget(sessionDir, FIDELITY_RECORD)] : []),
57
+ ]
58
+
59
+ /** Edition meaning records on disk (fidelity-meaning-defects.<format>.json). */
60
+ const EDITION_MEANING = /^fidelity-meaning-defects\.[^.]+\.json$/u
61
+
62
+ /** Verdict severity for equal times: a confirmed drift, then any other blocking defect (unverified), then clean. */
63
+ const severityOf = (defects) =>
64
+ defects.some((defect) => defect?.code === "meaning_drift_confirmed") ? 2 : defects.some((defect) => typeof defect?.code === "string" && blocksDelivery(defect)) ? 1 : 0
65
+
66
+ /**
67
+ * What selection reads from a meaning record: the bytes it claims, when it was written, how severe it is.
68
+ * An unreadable record matches no bytes (its own row still reads and refuses it); a record with no
69
+ * readable written_at sorts newest, so the barrier reads it and refuses it as malformed.
70
+ */
71
+ function claimOf(path) {
72
+ let record
73
+ try {
74
+ record = readJsonFile(path)
75
+ } catch {
76
+ return null
77
+ }
78
+ const at = Date.parse(record?.written_at)
79
+ return { path, hash: record?.artifact_sha256 ?? null, at: Number.isNaN(at) ? Infinity : at, severity: severityOf(Array.isArray(record?.defects) ? record.defects : []) }
80
+ }
81
+
82
+ /** Newest written_at first; on equal times the more severe verdict first. File names never decide. */
83
+ const newestFirst = (a, b) => (a.at === b.at ? b.severity - a.severity : b.at > a.at ? 1 : -1)
84
+
85
+ /**
86
+ * One meaning record per delivered edition, bound to that edition's bytes. Every record whose
87
+ * artifact_sha256 equals the edition's current bytes (a PDF counts its --rendered-from bytes) is a
88
+ * candidate: its own fidelity-meaning-defects.<format>.json, another edition's record (a
89
+ * byte-identical copy, or the html edition a PDF renders), and the legacy session record. The
90
+ * newest reader run wins; on equal times the more severe verdict wins. When no record claims these
91
+ * bytes the target is its own record (refused as for a different edition), else the legacy record,
92
+ * else its own name, so the refusal names the file to write (meaning gate not run).
93
+ */
94
+ function perEditionMeaning(sessionDir, delivered) {
95
+ if (delivered.length === 0) return [sessionTarget(sessionDir, MEANING_RECORD)]
96
+ const legacy = join(sessionDir, MEANING_RECORD)
97
+ const editions = readdirSync(sessionDir).filter((name) => EDITION_MEANING.test(name)).map((name) => join(sessionDir, name))
98
+ const claims = [...editions, ...(existsSync(legacy) ? [legacy] : [])].map(claimOf).filter((claim) => claim !== null)
99
+ return perEdition(sessionDir, delivered).map((target) => {
100
+ const own = join(sessionDir, meaningRecordName(target.format))
101
+ const bytes = target.subjects.length > 0 ? hashOf(target.subjects[0]) : null
102
+ const [newest] = bytes === null ? [] : claims.filter((claim) => claim.hash === bytes).sort(newestFirst)
103
+ return { ...target, declared: false, path: newest?.path ?? (existsSync(own) ? own : existsSync(legacy) ? legacy : own) }
104
+ })
105
+ }
49
106
 
50
107
  /** Every fidelity record the barrier reads, in check order; each column is one rule the barrier applies. */
51
108
  export const FIDELITY_RECORDS = Object.freeze([
@@ -53,7 +110,7 @@ export const FIDELITY_RECORDS = Object.freeze([
53
110
  kind: "fidelity",
54
111
  file: FIDELITY_RECORD,
55
112
  step: "fidelity gate",
56
- targets: perEdition,
113
+ targets: perEditionAndSession,
57
114
  requiredWhen: ({ hasFidelity }, target) => hasFidelity || GATED_FORMATS.includes(target.format),
58
115
  remedy: (target) => `run fact verify on ${target.subjects[0] ?? "the edition"} and set it with --verified-by`,
59
116
  bindsTo: "artifact_sha256",
@@ -64,17 +121,25 @@ export const FIDELITY_RECORDS = Object.freeze([
64
121
  kind: "meaning",
65
122
  file: MEANING_RECORD,
66
123
  step: "meaning gate",
67
- targets: perSession(MEANING_RECORD),
124
+ targets: perEditionMeaning,
68
125
  requiredWhen: meaningRequired,
69
- remedy: () => "run fact pairs and fact meaning-check before delivery",
126
+ remedy: (target) => `run fact pairs and fact meaning-check --edition ${target.format} on ${target.subjects[0] ?? "the edition"} before delivery`,
70
127
  bindsTo: "artifact_sha256",
71
128
  blocking: blocksDelivery,
72
129
  payload: Object.freeze(["command", "status", "exitCode", "run_id", "pairs_sha256", "counts", "dropped", "unverified", "calibration_path", "calibration_written", "calibration_replaced", "promotion_eligibility"]),
73
130
  }),
74
131
  ])
75
132
 
133
+ // Collection warnings use the same envelope, but are not delivery-gate records.
134
+ const SOURCE_RECORDS = Object.freeze([
135
+ Object.freeze({
136
+ kind: "source_conversion",
137
+ payload: Object.freeze(["conversion_altered_numbers", "lost", "raw_count", "converted_count"]),
138
+ }),
139
+ ])
140
+
76
141
  const entryOf = (kind) => {
77
- const entry = FIDELITY_RECORDS.find((row) => row.kind === kind)
142
+ const entry = [...FIDELITY_RECORDS, ...SOURCE_RECORDS].find((row) => row.kind === kind)
78
143
  if (entry === undefined) throw new CliError(`unknown fidelity record kind ${kind}`)
79
144
  return entry
80
145
  }
@@ -124,19 +189,20 @@ export function readFidelityRecord(path, kind) {
124
189
  }
125
190
 
126
191
  const unresolved = (format, codes, path) => ({ code: "fidelity_unresolved", format, codes, message: codes.map((code) => deliveryBlockReason({ code })).join("; "), path })
127
- const hashOf = (path) => (existsSync(path) ? sha256Of(readFileSync(path)) : null)
128
192
 
129
193
  /** Barrier problems for `outcome verify` and `outcome finish`; empty when nothing blocks delivery. */
130
194
  export function fidelityProblems(sessionDir, manifest) {
131
195
  const delivered = manifest.deliverables.filter((row) => row.status === "delivered")
132
- const hasFidelity = existsSync(join(sessionDir, LEDGER_FILENAME)) || FIDELITY_RECORDS.some((entry) => existsSync(join(sessionDir, entry.file)))
196
+ const targets = FIDELITY_RECORDS.map((entry) => [entry, entry.targets(sessionDir, delivered)])
197
+ // Any session record a row did not name (a session fidelity record, a meaning record) is fidelity evidence.
198
+ const hasFidelity = existsSync(join(sessionDir, LEDGER_FILENAME)) || targets.some(([, list]) => list.some((target) => !target.declared && target.path !== null && existsSync(target.path)))
133
199
  const lastFidelity = manifest.gateHistory?.findLast((entry) => entry.gate === "fidelity")
134
200
  const exempt = !hasFidelity && manifest.gates.fidelity === "not_run" && lastFidelity?.status === "not_run" && Boolean(lastFidelity.reason?.trim())
135
201
  const context = { sessionDir, hasFidelity }
136
202
  const problems = []
137
203
  const counted = new Set()
138
- for (const entry of FIDELITY_RECORDS) {
139
- for (const target of entry.targets(sessionDir, delivered)) {
204
+ for (const [entry, list] of targets) {
205
+ for (const target of list) {
140
206
  const refuse = (message, path = target.path) => problems.push({ code: "edition_unverified", format: target.format, message: `${target.at}${message}`, path })
141
207
  if (target.path === null || !existsSync(target.path)) {
142
208
  if (target.declared) refuse(`${entry.kind} record ${target.path} is missing`)
@@ -151,10 +217,11 @@ export function fidelityProblems(sessionDir, manifest) {
151
217
  continue
152
218
  }
153
219
  if (target.subjects.length > 0 && !target.subjects.some((subject) => hashOf(subject) === record[entry.bindsTo])) {
154
- refuse(`${entry.kind} record ${target.path} is for a different edition than ${target.subjects.join(", ")}`)
220
+ refuse(`${entry.kind} record ${target.path} is for a different edition than ${target.subjects.join(", ")}; ${entry.remedy(target)}`)
155
221
  }
156
- if (counted.has(target.path)) continue
157
- counted.add(target.path)
222
+ const path = realpathSync.native(target.path)
223
+ if (counted.has(path)) continue
224
+ counted.add(path)
158
225
  const codes = [...new Set(record.defects.filter(entry.blocking).map((defect) => defect.code))].sort()
159
226
  if (codes.length > 0) problems.push(unresolved(target.format, codes, target.path))
160
227
  }
@@ -0,0 +1,49 @@
1
+ // Availability is checked before hash/locator checks, including the raw-byte snapshot contract.
2
+ // One code (snapshot_hash_mismatch) for every unreadable or changed snapshot; the message names the cause.
3
+ import { existsSync, readFileSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import { sha256Of } from "./cli-support.mjs"
6
+
7
+ function read(dir, file) {
8
+ try {
9
+ return readFileSync(join(dir, file))
10
+ } catch (error) {
11
+ if (error.code === "ENOENT") throw new Error(`${file} is missing`)
12
+ if (error.code === "EISDIR") throw new Error(`${file} is not a readable file`)
13
+ throw new Error(`${file} is unreadable (${error.code ?? error.message})`)
14
+ }
15
+ }
16
+
17
+ /** Why the snapshot behind `row` cannot be trusted, or null when it reads and matches its pinned hash. */
18
+ export function snapshotCause(sourcesDir, row) {
19
+ if (!sourcesDir) return "has no sources directory"
20
+ const dir = join(sourcesDir, row.source_id)
21
+ try {
22
+ let meta
23
+ try {
24
+ meta = JSON.parse(read(dir, "meta.json").toString("utf8"))
25
+ } catch (error) {
26
+ throw error instanceof SyntaxError ? new Error("meta.json is not valid JSON") : error
27
+ }
28
+ if (!meta || typeof meta.sha256 !== "string") return "has invalid snapshot metadata (meta.json has no sha256)"
29
+ const needsRaw = ["bytes", "utf8-bytes"].includes(row.locator?.unit) && meta.kind !== "converted-only"
30
+ const file = existsSync(join(dir, "raw.bin")) || needsRaw ? "raw.bin" : "text.txt"
31
+ return sha256Of(read(dir, file)) === meta.sha256 ? null : `hash mismatch: ${file} changed after its snapshot was pinned`
32
+ } catch (error) {
33
+ return error.message
34
+ }
35
+ }
36
+
37
+ /** Flags each cited row whose own snapshot is unreadable; returns those source ids so healthy sources keep their metadata. */
38
+ export function unreadableSnapshots(ctx, rows, cited, sourcesDir, out) {
39
+ const unreadable = new Set()
40
+ for (const [id, node] of cited) {
41
+ const row = rows.get(id)
42
+ if (!row?.source_id) continue
43
+ const cause = snapshotCause(sourcesDir, row)
44
+ if (cause === null) continue
45
+ unreadable.add(row.source_id)
46
+ out.push({ ...ctx.defect("snapshot_hash_mismatch", node, `${id}: snapshot ${row.source_id} ${cause}`), fact_id: id })
47
+ }
48
+ return unreadable
49
+ }
@@ -0,0 +1,76 @@
1
+ // Source integrity and raw quote anchoring, shared by the fidelity value checks. Fails closed:
2
+ // a cited source-backed row whose sources dir, meta.json, raw.bin, or text.txt cannot be read, or
3
+ // whose snapshot no longer matches its pin, gets snapshot_hash_mismatch (with fact_id) naming the
4
+ // cause; healthy sources keep their metadata and checks. flag is (code, node, message, extra).
5
+ import { join } from "node:path"
6
+
7
+ import { proseText } from "./html-lite.mjs"
8
+ import { reanchor } from "./raw-locator.mjs"
9
+ import { readSnapshot } from "./source-view.mjs"
10
+
11
+ const short = (hash) => String(hash ?? "none").slice(0, 12)
12
+
13
+ function snapshotOf(sourcesDir, id) {
14
+ if (!sourcesDir) throw new Error("has no sources directory")
15
+ return readSnapshot(join(sourcesDir, id))
16
+ }
17
+
18
+ const blocked = (flag, cited, row, id, cause) => flag("snapshot_hash_mismatch", cited.get(row.fact_id), `${row.fact_id}: snapshot ${id} ${cause}`, { fact_id: row.fact_id })
19
+
20
+ /** One source's checks for the rows that cite it; byte-located rows are re-anchored on the view. */
21
+ function rowChecks(snap, id, referenced, cited, flag) {
22
+ const converted = snap.view.mode === "converted"
23
+ for (const row of referenced) {
24
+ const node = cited.get(row.fact_id)
25
+ // No locator unit marks a legacy text.txt row: it holds only on the legacy snapshot it pins.
26
+ if (row.locator?.unit === undefined) {
27
+ if (snap.view.mode !== "legacy") blocked(flag, cited, row, id, `is no longer the legacy text.txt snapshot the row was located on (now ${snap.view.mode}); add the row again from the new snapshot`)
28
+ else if (row.snapshot_sha256 !== snap.sha256) blocked(flag, cited, row, id, `holds ${short(snap.sha256)} but the legacy row pins ${short(row.snapshot_sha256)}`)
29
+ continue
30
+ }
31
+ if (snap.view.mode === "legacy") {
32
+ blocked(flag, cited, row, id, "raw.bin is missing (the row is byte-located)")
33
+ continue
34
+ }
35
+ if (converted && row.kind !== "quote") {
36
+ flag("snapshot_not_raw", node, `${row.fact_id} cannot measure converted-only source ${id}`)
37
+ continue
38
+ }
39
+ if (row.snapshot_sha256 !== snap.sha256) {
40
+ blocked(flag, cited, row, id, `holds ${short(snap.sha256)} but the row pins ${short(row.snapshot_sha256)}; run source refresh ${id} to re-anchor`)
41
+ continue
42
+ }
43
+ const anchor = row.locator.artifact === snap.view.artifact ? reanchor(snap.view, row) : null
44
+ if (!anchor) flag("quote_not_verbatim", node, `${row.fact_id}: no unique exact quote and context in ${id}/${snap.view.artifact}`)
45
+ else if (anchor.relocated) flag("relocated", node, `${row.fact_id}: ${row.locator.char_start}:${row.locator.char_end} -> ${anchor.start}:${anchor.end} (bytes of ${id}/${snap.view.artifact})`)
46
+ if (converted && row.kind === "quote") flag("quote_converted_only", node, `${row.fact_id} quotes converted-only source ${id}, not the original fetched bytes`)
47
+ }
48
+ }
49
+
50
+ export function sourceChecks(ctx, rows, cited, sourcesDir, flag, unreadable = new Set()) {
51
+ const referenced = new Map()
52
+ for (const key of cited.keys()) {
53
+ const row = rows.get(key)
54
+ if (row?.source_id) referenced.set(row.source_id, [...(referenced.get(row.source_id) ?? []), row])
55
+ }
56
+ const ids = new Set(referenced.keys())
57
+ for (const match of proseText(ctx.body).matchAll(/\[S(\d+)\]/g)) ids.add(`S${match[1]}`)
58
+ const meta = new Map()
59
+ for (const id of ids) {
60
+ if (unreadable.has(id)) continue
61
+ const backing = referenced.get(id) ?? []
62
+ let snap
63
+ try {
64
+ snap = snapshotOf(sourcesDir, id)
65
+ } catch (error) {
66
+ // A source only named in prose ([S<n>]) backs no row: an absent snapshot is not a finding.
67
+ for (const row of backing) blocked(flag, cited, row, id, error.message)
68
+ continue
69
+ }
70
+ meta.set(id, snap.meta)
71
+ if (snap.drift.length === 0) rowChecks(snap, id, backing, cited, flag)
72
+ else if (backing.length === 0) flag("snapshot_hash_mismatch", ctx.body, `snapshot ${id} ${snap.drift.join("; ")}`)
73
+ else for (const row of backing) blocked(flag, cited, row, id, snap.drift.join("; "))
74
+ }
75
+ return meta
76
+ }
@@ -13,9 +13,12 @@
13
13
  // so each entry, in array order, is matched to the next untraced occurrence of its rendered text in
14
14
  // document order that is a whole numeral and reads as the entry's row there (fits: "1,237 million"
15
15
  // is not F2's 1,237), and strips that one occurrence only. A copy of the same text that no entry
16
- // accounts for stays in the scan; an entry whose text is gone strips nothing.
16
+ // accounts for stays in the scan; an entry whose text is gone strips nothing. A derived or
17
+ // converted entry is located under the bracket grammar of fidelity-originals.mjs (renderLocator).
17
18
 
19
+ import { PROSE_TAGS } from "./html-lite.mjs"
18
20
  import { NOT_CLAIMS } from "./numerals.mjs"
21
+ import { sourcesClaims, textClaims } from "./sources-citation.mjs"
19
22
 
20
23
  /** Structural numerals: versions, figure/table/section references, times (NOT_CLAIMS 7-11), pages, citations, placeholders. */
21
24
  export const STRUCTURAL = [...NOT_CLAIMS.slice(7, 12), /\b(?:p|pp|page|pages)\.?\s*\d+(?:\s*[-\u2013]\s*\d+)?/gi, /\[(?:S|Source\s*)\d+\]|\[\^\d+\]/gi, /\{\{[^}]*\}\}/g]
@@ -29,7 +32,12 @@ function ownerTexts(ctx, charts) {
29
32
  const owners = new Map()
30
33
  for (const entry of ctx.texts) {
31
34
  const kind = ctx.kindOf(entry.scope)
32
- if (kind === "sources" || kind === "toc") continue
35
+ if (kind === "toc") continue
36
+ if (kind === "sources") {
37
+ if (!PROSE_TAGS.has(entry.owner.tag)) owners.set(entry.owner, `${owners.get(entry.owner) ?? ""} ${textClaims(entry.node.text).join(" ")}`)
38
+ else if (!owners.has(entry.owner)) owners.set(entry.owner, sourcesClaims(entry.owner).join(" "))
39
+ continue
40
+ }
33
41
  if (charts.has(ctx.util.closest(entry.node, (cur) => cur.tag === "figure"))) continue
34
42
  const hidden = ctx.util.closest(entry.node, (cur) => cur.attrs["data-fact"] !== undefined || cur.attrs["data-translates"] !== undefined || cur.tag === "q" || cur.tag === "blockquote")
35
43
  owners.set(entry.owner, (owners.get(entry.owner) ?? "") + (hidden ? ` ${TRACED} ` : entry.node.text))
@@ -52,8 +60,13 @@ function scanOwner(ctx, owner, raw, flag) {
52
60
  for (const match of rest.matchAll(NUMERAL)) flag("untraceable_number", owner, `number "${match[0]}" has no ledger row: "${clip(rest.slice(Math.max(0, match.index - 30), match.index + 30))}"`)
53
61
  }
54
62
 
63
+ const plainSite = (entry, text, from) => {
64
+ const hit = text.indexOf(entry.rendered, from)
65
+ return hit === -1 ? null : [hit, hit + entry.rendered.length]
66
+ }
67
+
55
68
  /** Strip each fill-map entry at its own fill site: the next untraced occurrence after the previous entry's site. */
56
- function traceFills(owners, fillMap, fits) {
69
+ function traceFills(owners, fillMap, fits, locate = plainSite) {
57
70
  const keys = [...owners.keys()]
58
71
  const texts = [...owners.values()]
59
72
  let [index, from] = [0, 0]
@@ -61,12 +74,12 @@ function traceFills(owners, fillMap, fits) {
61
74
  const { rendered } = entry
62
75
  if (!rendered) continue
63
76
  for (let i = index, at = from; i < texts.length; i += 1, at = 0) {
64
- let hit = texts[i].indexOf(rendered, at)
65
- const glued = (pos) => /\d/.test(texts[i][pos - 1] ?? "") || /\d/.test(texts[i][pos + rendered.length] ?? "")
66
- while (hit !== -1 && (glued(hit) || !fits(entry, texts[i].slice(hit)))) hit = texts[i].indexOf(rendered, hit + 1)
67
- if (hit === -1) continue
68
- texts[i] = `${texts[i].slice(0, hit)}${TRACED}${texts[i].slice(hit + rendered.length)}`
69
- ;[index, from] = [i, hit + TRACED.length]
77
+ let hit = locate(entry, texts[i], at)
78
+ const glued = ([start, end]) => (/^\d/.test(rendered) && /\d/.test(texts[i][start - 1] ?? "")) || (/\d$/.test(rendered) && /\d/.test(texts[i][end] ?? ""))
79
+ while (hit && (glued(hit) || !fits(entry, texts[i].slice(hit[0])))) hit = locate(entry, texts[i], hit[0] + 1)
80
+ if (!hit) continue
81
+ texts[i] = `${texts[i].slice(0, hit[0])}${TRACED}${texts[i].slice(hit[1])}`
82
+ ;[index, from] = [i, hit[0] + TRACED.length]
70
83
  break
71
84
  }
72
85
  }
@@ -79,7 +92,8 @@ function traceFills(owners, fillMap, fits) {
79
92
  * @param {{ fact_id: string, rendered: string, start?: number, end?: number }[] | null} fillMap Markdown fill map, or null
80
93
  * @param {(code: string, node: object, message: string) => void} flag
81
94
  * @param {(entry: object, tail: string) => boolean} fits whether the text from a candidate site on reads as the entry's row
95
+ * @param {(entry: object, text: string, from: number) => [number, number] | null} [locate] the next candidate site of an entry's text
82
96
  */
83
- export function traceChecks(ctx, charts, fillMap, flag, fits) {
84
- for (const [owner, text] of traceFills(ownerTexts(ctx, charts), fillMap, fits)) scanOwner(ctx, owner, text, flag)
97
+ export function traceChecks(ctx, charts, fillMap, flag, fits, locate) {
98
+ for (const [owner, text] of traceFills(ownerTexts(ctx, charts), fillMap, fits, locate)) scanOwner(ctx, owner, text, flag)
85
99
  }
@@ -0,0 +1,202 @@
1
+ // Value-mode units live outside the fact element. The documented form is the value then its unit,
2
+ // with or without a space, a Korean particle allowed after it ({{F1:span:value}} m, {{F3:rate:value}}%,
3
+ // {{F21:rev:value}}\uC5B5 \uC6D0\uC73C\uB85C); a currency sign right before the value; the :unit element
4
+ // right after it; or the same visual column's table header. A currency the element's own text carries (fill renders a
5
+ // prefix currency value from its raw text, $<data>$12</data>) binds too: every bound sign, outside or inside, must agree. Unknown words are advisory, never guessed units,
6
+ // unless fill recorded a bound unit for the value: then losing it is a unit edit.
7
+ import { cellsOf, rowsOf } from "./fact-locate.mjs"
8
+ import { unitKey } from "./fidelity-binding.mjs"
9
+ import { parseHtml, proseText } from "./html-lite.mjs"
10
+ import { NUMBER_SOURCE } from "./number-grammar.mjs"
11
+ import { QUALIFIERS_AFTER } from "./numerals.mjs"
12
+ import { dimensionOf, normalizeUnit, spacedAliasUnit } from "./units.mjs"
13
+
14
+ // Korean particles a unit may carry (eun/neun, i/ga, eul/reul, ui, e, eseo, ege, euro/ro, wa/gwa, do, man,
15
+ // kkaji, buteo, boda, ssik, in, ida/yeotda/ieotda, imyeo, igo), longest first.
16
+ const PARTICLES = ["\uC774\uC5C8\uB2E4", "\uC600\uB2E4", "\uC774\uB2E4", "\uC774\uBA70", "\uC774\uACE0", "\uC73C\uB85C", "\uC5D0\uC11C", "\uC5D0\uAC8C", "\uAE4C\uC9C0", "\uBD80\uD130", "\uBCF4\uB2E4", "\uC740", "\uB294", "\uC774", "\uAC00", "\uC744", "\uB97C", "\uC758", "\uC5D0", "\uB85C", "\uC640", "\uACFC", "\uB3C4", "\uB9CC", "\uC529", "\uC778"]
17
+ /** The displayed value, read with the one grouped-number grammar; text around it is sign and unit. */
18
+ const NUMBER = new RegExp(NUMBER_SOURCE, "u")
19
+ const SIGNS = ["US$", "$", "\u20AC", "\u20A9", "\u00A3", "\u00A5", "USD", "EUR", "GBP", "JPY", "KRW"]
20
+
21
+ const withoutSuffix = (word, suffixes) => {
22
+ const suffix = suffixes.find((cur) => word.endsWith(cur) && word.length > cur.length)
23
+ return suffix ? word.slice(0, -suffix.length) : word
24
+ }
25
+ const withoutParticle = (word) => withoutSuffix(word, PARTICLES)
26
+ /** Words written after a value that bound or soften it (the numeral qualifiers, jeongdo, garyang, jjeum, gwaban). */
27
+ const QUALIFIER_WORDS = [...QUALIFIERS_AFTER, "\uC815\uB3C4", "\uAC00\uB7C9", "\uCBE4", "\uACFC\uBC18"]
28
+ /** A unit, with a particle and/or a qualifier glued on ("m\uC774\uC0C1\uC758"); a whole qualifier word is never a unit (chogwa is not cho + gwa). */
29
+ const stemsOf = (word) => {
30
+ const bare = withoutParticle(word)
31
+ return QUALIFIER_WORDS.includes(word) ? [] : [word, bare, withoutSuffix(word, QUALIFIER_WORDS), withoutSuffix(bare, QUALIFIER_WORDS)].filter((stem) => !QUALIFIER_WORDS.includes(stem))
32
+ }
33
+ const stemOf = (word) => stemsOf(word).find((stem) => normalizeUnit(stem)) ?? null
34
+ const unitOf = (word) => normalizeUnit(stemOf(word))
35
+ /** Unit aliases that are also ordinary English words ("won awards", "second place"): read only as the row's own unit. */
36
+ const ORDINARY_WORDS = new Set(["won", "second", "pound", "pounds"])
37
+ /** An ordinary word, or an all-caps acronym that only matches a unit case-insensitively ("SEC filings", "WON"). */
38
+ const ordinary = (word) => ORDINARY_WORDS.has(word.toLowerCase()) || (/^[A-Z]{3,}$/.test(word) && normalizeUnit(word) !== word)
39
+ /** Inline elements the text after a value is read through; any other element, or another fact, ends it. */
40
+ const INLINE = new Set(["a", "abbr", "b", "bdi", "cite", "code", "data", "dfn", "em", "i", "ins", "kbd", "mark", "samp", "small", "span", "strong", "sub", "sup", "time", "u", "var"])
41
+ /** "3 bun-ui 1" is the fraction one third, not 3 minutes. */
42
+ const FRACTION = /^\uBD84\uC758\s?\d/u
43
+ const scaleOf = (unit) => /-(e\d+)$/.exec(unit ?? "")?.[1] ?? null
44
+
45
+ /** English words stay apart: letter-only words join only as a spaced alias ("percentage points"), so "m in" is m, then in. */
46
+ const LETTERS = /^[A-Za-z]+$/u
47
+ /** The unit the words spell together: a spaced alias, or (with a symbol or Hangul in them) the words joined ("% p", "\uD37C\uC13C\uD2B8 \uD3EC\uC778\uD2B8"). */
48
+ function unitOfWords(words) {
49
+ if (words.length === 1) return unitOf(words[0])
50
+ const spaced = stemsOf(words.at(-1)).map((last) => spacedAliasUnit([...words.slice(0, -1), last].join(" "))).find(Boolean)
51
+ return spaced ?? (words.every((word) => LETTERS.test(word)) ? null : unitOf(words.join("")))
52
+ }
53
+
54
+ /** The unit written right after the value: the longest known unit its first words spell ("% \uD3EC\uC778\uD2B8" is %p, not %), else the next word. */
55
+ function unitAfter(text, row) {
56
+ const lead = text.replace(/^[\s\u00A0]+/u, "")
57
+ if (FRACTION.test(lead)) return null
58
+ const bracketed = /^\(([^()]{1,40})\)/u.exec(lead)
59
+ if (bracketed) return unitAfter(bracketed[1], row)
60
+ const clause = lead.split(/[,.;:!?()[\]"]/u)[0]
61
+ const words = clause.split(/[\s\u00A0]+/u).filter(Boolean).slice(0, 3)
62
+ if (words.length === 0) return null
63
+ for (let n = words.length; n > 1; n--) {
64
+ const unit = unitOfWords(words.slice(0, n))
65
+ if (unit) return { token: unit }
66
+ }
67
+ const first = unitOf(words[0])
68
+ if (!first) return null
69
+ // An ordinary word ("pounds", "won awards") counts only as the row's unit or beside a sign or header of its kind (boundUnits).
70
+ if (unitKey(first) !== unitKey(row.unit) && ordinary(stemOf(words[0]))) return { token: first, ordinary: true }
71
+ const currency = words[1] ? unitOf(words[1]) : null
72
+ if (dimensionOf(first) === "scale" && dimensionOf(currency)?.startsWith("currency:")) return { token: `${currency} ${words[0]}`, dimension: dimensionOf(currency), scale: first }
73
+ return { token: first }
74
+ }
75
+
76
+ function signBefore(text) {
77
+ const trimmed = text.replace(/[\s\u00A0]+$/u, "")
78
+ // A letter sign is a whole word: "USD 12", never "XUSD 12".
79
+ const sign = SIGNS.find((s) => trimmed.endsWith(s) && !(/^[A-Z]/u.test(s) && /[A-Za-z]$/u.test(trimmed.slice(0, -s.length))))
80
+ return sign ? { token: normalizeUnit(sign), sign: true } : null
81
+ }
82
+
83
+ /** A currency written after the number inside the element ("12\u20AC", "12\uC6D0"); any other unit there is part of the value. */
84
+ function currencyAfter(text, row) {
85
+ const found = unitAfter(text, row)
86
+ const dimension = found && !found.ordinary ? (found.dimension ?? dimensionOf(found.token)) : null
87
+ return dimension?.startsWith("currency:") ? { token: found.scale ? dimension.slice("currency:".length) : found.token, sign: true } : null
88
+ }
89
+
90
+ function agrees(found, row) {
91
+ if (found.scale) return dimensionOf(row.unit) === found.dimension && scaleOf(row.unit) === found.scale
92
+ if (unitKey(found.token) === unitKey(row.unit)) return true
93
+ if (dimensionOf(found.token) === "scale") return scaleOf(row.unit) === found.token
94
+ if (dimensionOf(found.token)?.startsWith("currency:") && found.sign) return dimensionOf(row.unit) === dimensionOf(found.token)
95
+ return unitKey(found.token) === unitKey(row.unit)
96
+ }
97
+
98
+ const closest = (node, test) => {
99
+ for (let cur = node.parent; cur; cur = cur.parent) if (test(cur)) return cur
100
+ return null
101
+ }
102
+ /** All text under a node, code included: a unit may be set in code ("320 <code>ms</code>"). */
103
+ const plainText = (node) => (node.tag === "#text" ? node.text : node.children.map(plainText).join(""))
104
+
105
+ /** A column-header row: a thead row or a row made only of th cells. A th first in a body row is a row header. */
106
+ const isHeaderRow = (row) => row.parent?.tag === "thead" || (cellsOf(row).length > 0 && cellsOf(row).every((cur) => cur.tag === "th"))
107
+
108
+ /** The header of the cell's visual column; nothing binds in a table with a rowspan or without a header row. */
109
+ function headerUnit(node) {
110
+ const cell = closest(node, (cur) => cur.tag === "td")
111
+ const table = cell && closest(cell, (cur) => cur.tag === "table")
112
+ if (!table || cell.attrs.colspan || cell.attrs.rowspan) return null
113
+ const rows = rowsOf(table)
114
+ if (rows.some((row) => row.children.some((cur) => cur.attrs?.rowspan && cur.attrs.rowspan !== "1"))) return null
115
+ const header = rows.find(isHeaderRow)
116
+ if (!header || header === cell.parent) return null
117
+ const head = cellsOf(header)[cellsOf(cell.parent).indexOf(cell)]
118
+ if (!head) return null
119
+ const text = proseText(head)
120
+ const label = (/\(([^()]+)\)\s*$/u.exec(text)?.[1] ?? text).trim()
121
+ const unit = unitOf(label) ?? unitOf(label.split(/\s+/u).at(-1) ?? "")
122
+ return unit ? { token: unit } : null
123
+ }
124
+
125
+ /** The documented split form: the next element is this fact's own :unit element. */
126
+ function unitElementAfter(siblings, at, row) {
127
+ const next = siblings.slice(at + 1).find((cur) => !(cur.tag === "#text" && !cur.text.trim()))
128
+ return next?.attrs?.["data-fact"] === row.fact_id && unitKey(proseText(next)) === unitKey(row.unit)
129
+ }
130
+
131
+ /** The text after the element within its block, read through inline wrappers and line breaks ("320 <b>m</b>", "<b>320</b> m", "320<br>m"). */
132
+ function textAfter(node) {
133
+ let text = ""
134
+ for (let cur = node; text.length < 80; ) {
135
+ const next = cur.parent?.children[cur.parent.children.indexOf(cur) + 1]
136
+ if (next === undefined) {
137
+ if (!INLINE.has(cur.parent?.tag) || cur.parent.attrs["data-fact"] !== undefined) break
138
+ cur = cur.parent
139
+ } else if (next.tag === "#text" || next.tag === "br" || (INLINE.has(next.tag) && next.attrs["data-fact"] === undefined)) {
140
+ text += next.tag === "br" ? "\n" : plainText(next)
141
+ cur = next
142
+ } else break
143
+ }
144
+ return text
145
+ }
146
+
147
+ const INSIDE = "written inside with"
148
+ /**
149
+ * Every unit bound to a value-mode element: a currency sign before it, the unit after it (or its :unit element), its
150
+ * column header, and a currency sign or word inside it (prefix or suffix of the number).
151
+ */
152
+ function boundUnits(node, row) {
153
+ const siblings = node.parent.children
154
+ const at = siblings.indexOf(node)
155
+ const prev = siblings[at - 1]
156
+ const shown = proseText(node)
157
+ const number = NUMBER.exec(shown)
158
+ const before = signBefore(prev?.tag === "#text" ? prev.text : "")
159
+ // The element's own text is the row's render: a scale word in it ("3,234\uC5B5", "12 million") is part of the value.
160
+ const inside = number ? [signBefore(shown.slice(0, number.index)), currencyAfter(shown.slice(number.index + number[0].length), row)] : []
161
+ const read = unitAfter(textAfter(node), row) ?? (unitElementAfter(siblings, at, row) ? { token: row.unit } : null)
162
+ const header = headerUnit(node)
163
+ const kind = (unit) => dimensionOf(unit.token)?.split(":")[0]
164
+ const after = read?.ordinary && ![before, ...inside, header].some((unit) => unit && kind(unit) === kind(read)) ? null : read
165
+ return [["preceded by", before], ["followed by", after], ["headed with", header], ...inside.map((unit) => [INSIDE, unit])].filter(([, unit]) => unit)
166
+ }
167
+
168
+ /** Each data-fact element's own fill-map entry: fill writes one element per entry, so ids pair up in document order. */
169
+ export function fillEntriesByNode(nodes, entries) {
170
+ const queues = new Map()
171
+ for (const entry of entries ?? []) if (entry.replaced === undefined && entry.translates === undefined) queues.set(entry.fact_id, [...(queues.get(entry.fact_id) ?? []), entry])
172
+ return new Map(nodes.filter((node) => node.attrs["data-replaced"] === undefined).flatMap((node) => {
173
+ const entry = queues.get(node.attrs["data-fact"])?.shift()
174
+ return entry ? [[node, entry]] : []
175
+ }))
176
+ }
177
+
178
+ /** Fill's record of the unit bound to each value-mode pointer (unit_bound), read off the filled edition (HTML, or the Markdown projection). */
179
+ export function withBoundUnits(html, entries, rows) {
180
+ const all = (node) => [node, ...node.children.flatMap(all)]
181
+ const filled = fillEntriesByNode(all(parseHtml(html)).filter((node) => node.attrs?.["data-fact"] !== undefined), entries)
182
+ const units = new Map([...filled].flatMap(([node, entry]) => {
183
+ const row = rows.get(entry.fact_id)
184
+ // Only a unit outside the element: the element's own text is the row's render, so it can be no record of a unit beside it.
185
+ const unit = entry.mode === "value" && row && unitKey(row.unit) ? boundUnits(node, row).find(([where]) => where !== INSIDE)?.[1].token : null
186
+ return unit ? [[entry, unit]] : []
187
+ }))
188
+ return entries.map((entry) => (units.has(entry) ? { ...entry, unit_bound: units.get(entry) } : entry))
189
+ }
190
+
191
+ /**
192
+ * Value mode comes from fill: the element's data-fact-mode, or its fill-map entry when the edition has one
193
+ * (so deleting the attribute changes nothing). `entry.unit_bound` is the unit fill saw bound to the value.
194
+ */
195
+ export function valueUnitProblem(node, row, entry = null) {
196
+ if ((entry?.mode !== "value" && node.attrs["data-fact-mode"] !== "value") || !unitKey(row.unit)) return null
197
+ const found = boundUnits(node, row)
198
+ if (entry?.unit_bound && found.every(([where]) => where === INSIDE)) return ["unit_mismatch", `value-mode ${row.fact_id} had the unit "${entry.unit_bound}" beside it when filled, but no recognizable unit is bound beside it now; its row is in "${row.unit}"`]
199
+ if (found.length === 0) return ["unit_unbound", `${row.fact_id} has unit "${row.unit}" but no recognizable unit right after its value, before it as a currency sign, or in its column header`]
200
+ const wrong = found.find(([, unit]) => !agrees(unit, row))
201
+ return wrong ? ["unit_mismatch", `value-mode ${row.fact_id} is ${wrong[0]} "${wrong[1].token}", but its row is in "${row.unit}"`] : null
202
+ }