lazycodex-ai 5.1.44 → 5.1.45

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 (273) hide show
  1. package/dist/cli/index.js +948 -416
  2. package/dist/cli/service-auth/keystore.d.ts +4 -2
  3. package/dist/cli-node/index.js +948 -416
  4. package/docs/reference/web-terminal-visual-qa.md +8 -0
  5. package/package.json +1 -1
  6. package/packages/lsp-daemon/dist/cli.js +4 -4
  7. package/packages/lsp-daemon/dist/client.js +4 -4
  8. package/packages/lsp-daemon/dist/index.js +4 -4
  9. package/packages/lsp-daemon/package.json +3 -3
  10. package/packages/lsp-tools-mcp/dist/cli.js +4 -4
  11. package/packages/lsp-tools-mcp/dist/mcp.js +4 -4
  12. package/packages/lsp-tools-mcp/dist/tools.js +4 -4
  13. package/packages/lsp-tools-mcp/package.json +3 -3
  14. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  15. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +62 -21
  16. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  17. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +1 -1
  19. package/packages/omo-codex/plugin/components/comment-checker/biome.json +1 -21
  20. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
  22. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  23. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/biome.json +1 -21
  25. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  26. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +4 -4
  27. package/packages/omo-codex/plugin/components/lsp/biome.json +1 -21
  28. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  29. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +4 -4
  30. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  31. package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
  32. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +0 -1
  33. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +1 -1
  34. package/packages/omo-codex/plugin/components/rules/biome.json +1 -21
  35. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  36. package/packages/omo-codex/plugin/components/rules/package.json +4 -4
  37. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  38. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  39. package/packages/omo-codex/plugin/components/telemetry/biome.json +1 -21
  40. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +90 -1
  41. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +91 -1
  42. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  43. package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
  44. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +2 -2
  45. package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -21
  46. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  47. package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
  48. package/packages/omo-codex/plugin/components/ulw-execute-continuation/biome.json +1 -21
  49. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  50. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  51. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +4 -4
  52. package/packages/omo-codex/plugin/components/ulw-loop/biome.json +1 -21
  53. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +1 -1
  54. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +7 -7
  55. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +5 -5
  56. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +1 -1
  57. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  58. package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
  59. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +1 -1
  60. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
  61. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +1 -1
  62. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +2 -0
  63. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  71. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  72. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  73. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  84. package/packages/omo-codex/plugin/package-lock.json +12 -12
  85. package/packages/omo-codex/plugin/package.json +1 -1
  86. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +7 -7
  87. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +2 -0
  88. package/packages/omo-codex/plugin/skills/browser/SKILL.md +21 -22
  89. package/packages/omo-codex/plugin/skills/browser/references/commands.md +2 -9
  90. package/packages/omo-codex/plugin/skills/browser/references/install.md +1 -2
  91. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  92. package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +2 -2
  93. package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +63 -487
  94. package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +1 -1
  95. package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +1 -10
  96. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +79 -118
  97. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +75 -224
  98. package/packages/omo-codex/plugin/skills/frontend/references/design/stylegallery.md +32 -27
  99. package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -2
  100. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
  101. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
  102. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/collect-files.ts +53 -0
  103. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
  104. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
  105. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
  106. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
  107. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
  108. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
  109. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
  110. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
  111. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
  112. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
  113. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
  114. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
  115. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
  116. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
  117. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
  118. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
  119. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
  120. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
  121. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
  122. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +1 -1
  123. package/packages/omo-codex/plugin/skills/programming/SKILL.md +10 -7
  124. package/packages/omo-codex/plugin/skills/programming/references/typescript/README.md +10 -7
  125. package/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md +9 -23
  126. package/packages/omo-codex/plugin/skills/programming/references/typescript/oxc-lint.md +130 -0
  127. package/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md +6 -62
  128. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
  129. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/new-project.ts +20 -81
  130. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
  131. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
  132. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
  133. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
  134. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
  135. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
  136. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
  137. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +14 -14
  138. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +1 -1
  139. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +2 -2
  140. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +2 -2
  141. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  142. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +9 -3
  143. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
  144. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +7 -6
  145. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
  146. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +51 -3
  147. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claims-index.mjs +125 -0
  148. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +68 -3
  149. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
  150. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
  151. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
  152. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
  153. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
  154. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +1 -1
  155. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +1 -1
  156. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +1 -27
  157. package/packages/omo-codex/plugin/skills/ulw-research/scripts/lexicon.mjs +47 -0
  158. package/packages/omo-codex/plugin/skills/ulw-research/scripts/numerals.mjs +226 -0
  159. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +2 -3
  160. package/packages/omo-codex/plugin/skills/ulw-research/scripts/particles-ko.mjs +108 -0
  161. package/packages/omo-codex/plugin/skills/ulw-research/scripts/rational.mjs +139 -0
  162. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
  163. package/packages/omo-codex/plugin/skills/ulw-research/scripts/sentence-split.mjs +77 -0
  164. package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-normalize.mjs +82 -0
  165. package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-similarity.mjs +28 -0
  166. package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +105 -0
  167. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +71 -64
  168. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +4 -1
  169. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +6 -6
  170. package/packages/omo-codex/scripts/install-dist/install-local.mjs +152 -23
  171. package/packages/shared-skills/skills/browser/SKILL.md +21 -22
  172. package/packages/shared-skills/skills/browser/references/commands.md +2 -9
  173. package/packages/shared-skills/skills/browser/references/install.md +1 -2
  174. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  175. package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +2 -2
  176. package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +63 -487
  177. package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +1 -1
  178. package/packages/shared-skills/skills/browser/scripts/omowright.mjs +1 -10
  179. package/packages/shared-skills/skills/frontend/SKILL.md +79 -118
  180. package/packages/shared-skills/skills/frontend/references/design/README.md +75 -224
  181. package/packages/shared-skills/skills/frontend/references/design/stylegallery.md +32 -27
  182. package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -2
  183. package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
  184. package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.test.ts +101 -0
  185. package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
  186. package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.test.ts +50 -0
  187. package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.ts +53 -0
  188. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
  189. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
  190. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
  191. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
  192. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
  193. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
  194. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
  195. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
  196. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
  197. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
  198. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
  199. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
  200. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
  201. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
  202. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
  203. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
  204. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
  205. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
  206. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
  207. package/packages/shared-skills/skills/programming/SKILL.md +10 -7
  208. package/packages/shared-skills/skills/programming/references/typescript/README.md +10 -7
  209. package/packages/shared-skills/skills/programming/references/typescript/bootstrap.md +9 -23
  210. package/packages/shared-skills/skills/programming/references/typescript/oxc-lint.md +130 -0
  211. package/packages/shared-skills/skills/programming/references/typescript/tsconfig-strict.md +6 -62
  212. package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
  213. package/packages/shared-skills/skills/programming/scripts/typescript/new-project.test.ts +162 -0
  214. package/packages/shared-skills/skills/programming/scripts/typescript/new-project.ts +20 -81
  215. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
  216. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
  217. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
  218. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
  219. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
  220. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
  221. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
  222. package/packages/shared-skills/skills/refactor/SKILL.md +13 -13
  223. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  224. package/packages/shared-skills/skills/ulw-research/AGENTS.md +9 -3
  225. package/packages/shared-skills/skills/ulw-research/SKILL.md +2 -2
  226. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +7 -6
  227. package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
  228. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +51 -3
  229. package/packages/shared-skills/skills/ulw-research/scripts/claims-index.mjs +125 -0
  230. package/packages/shared-skills/skills/ulw-research/scripts/claims-index.test.ts +149 -0
  231. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +68 -3
  232. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +97 -12
  233. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +190 -0
  234. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
  235. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +141 -0
  236. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
  237. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.test.ts +43 -0
  238. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
  239. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +252 -0
  240. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
  241. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.test.ts +179 -0
  242. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
  243. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.test.ts +46 -0
  244. package/packages/shared-skills/skills/ulw-research/scripts/gates-context.test.ts +64 -0
  245. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +1 -1
  246. package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +1 -1
  247. package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +1 -27
  248. package/packages/shared-skills/skills/ulw-research/scripts/lexicon.mjs +47 -0
  249. package/packages/shared-skills/skills/ulw-research/scripts/lexicon.test.ts +57 -0
  250. package/packages/shared-skills/skills/ulw-research/scripts/numerals.mjs +226 -0
  251. package/packages/shared-skills/skills/ulw-research/scripts/numerals.test.ts +120 -0
  252. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +2 -3
  253. package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +29 -0
  254. package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.mjs +108 -0
  255. package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.test.ts +86 -0
  256. package/packages/shared-skills/skills/ulw-research/scripts/rational.mjs +139 -0
  257. package/packages/shared-skills/skills/ulw-research/scripts/rational.test.ts +46 -0
  258. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
  259. package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.mjs +77 -0
  260. package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.test.ts +109 -0
  261. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/claim-graph.md +13 -0
  262. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-locked.md +17 -0
  263. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-pending.md +17 -0
  264. package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.mjs +82 -0
  265. package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.test.ts +73 -0
  266. package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.mjs +28 -0
  267. package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.test.ts +38 -0
  268. package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +105 -0
  269. package/packages/shared-skills/skills/ulw-research/scripts/units.test.ts +83 -0
  270. package/packages/shared-skills/skills/visual-qa/SKILL.md +70 -63
  271. package/packages/shared-skills/skills/visual-qa/scripts/skill-doc-fields.test.ts +34 -0
  272. package/script/qa/web-terminal-visual-qa.mjs +6 -2
  273. package/script/qa/xterm-live-terminal.mjs +26 -3
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { readFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+
5
+ import { parseLineageMode, parsePalette } from "./design-spec.mjs"
6
+ import * as gatesStatic from "./gates-static.mjs"
7
+ import * as gatesStructure from "./gates-structure.mjs"
8
+ import { parseHtml } from "./html-lite.mjs"
9
+
10
+ const FIXTURES = join(import.meta.dir, "tests", "fixtures")
11
+ const GOOD = readFileSync(join(FIXTURES, "good-report.html"), "utf8")
12
+ const SPEC = readFileSync(join(FIXTURES, "design-spec.md"), "utf8")
13
+
14
+ type BuildContext = (doc: unknown, options: Record<string, unknown>) => Record<string, unknown>
15
+
16
+ function buildContextExport(): BuildContext {
17
+ const buildContext = (gatesStatic as Record<string, unknown>).buildContext
18
+ if (typeof buildContext !== "function") throw new Error("buildContext is not exported")
19
+ return buildContext as BuildContext
20
+ }
21
+
22
+ describe("static-gate context exports", () => {
23
+ test("#given gates-static #when imported #then buildContext is an exported function", () => {
24
+ // given
25
+ const exported = (gatesStatic as Record<string, unknown>).buildContext
26
+ // when
27
+ const kind = typeof exported
28
+ // then
29
+ expect(kind).toBe("function")
30
+ })
31
+
32
+ test("#given the passing fixture report #when buildContext runs #then the walk, sections and helpers are present", () => {
33
+ // given
34
+ const buildContext = buildContextExport()
35
+ const options = { palette: parsePalette(SPEC), lineageMode: parseLineageMode(SPEC), requireSections: [], baseDir: FIXTURES }
36
+ // when
37
+ const ctx = buildContext(parseHtml(GOOD), options) as {
38
+ elements: unknown[]
39
+ texts: unknown[]
40
+ sections: { kind: string | null }[]
41
+ util: { hasCitation: unknown; citationMarker: unknown }
42
+ kindOf: unknown
43
+ }
44
+ // then
45
+ expect(ctx.elements.length).toBeGreaterThan(0)
46
+ expect(ctx.texts.length).toBeGreaterThan(0)
47
+ expect(ctx.sections.length).toBeGreaterThan(0)
48
+ expect(ctx.sections.map((section) => section.kind)).toContain("sources")
49
+ expect(typeof ctx.util.hasCitation).toBe("function")
50
+ expect(ctx.util.citationMarker).toBeInstanceOf(RegExp)
51
+ expect(typeof ctx.kindOf).toBe("function")
52
+ })
53
+
54
+ test("#given gates-structure #when imported #then UNCHECKED_KINDS lists the closing and sources kinds", () => {
55
+ // given
56
+ const unchecked = (gatesStructure as Record<string, unknown>).UNCHECKED_KINDS as Set<string> | undefined
57
+ // when
58
+ const kinds = unchecked ? [...unchecked] : []
59
+ // then
60
+ expect(unchecked).toBeInstanceOf(Set)
61
+ expect(kinds).toContain("closing")
62
+ expect(kinds).toContain("sources")
63
+ })
64
+ })
@@ -108,7 +108,7 @@ function walkBody(ctx, node, state) {
108
108
  }
109
109
  }
110
110
 
111
- function buildContext(doc, options) {
111
+ export function buildContext(doc, options) {
112
112
  const lineageMode = options.lineageMode ?? "inline"
113
113
  if (!LINEAGE_MODES.includes(lineageMode)) throw new TypeError(`lineageMode must be one of ${LINEAGE_MODES.join(", ")}`)
114
114
  const closingPattern = options.closingPattern ? toRegExp(options.closingPattern) : null
@@ -9,7 +9,7 @@ import { join } from "node:path"
9
9
  import { proseText } from "./html-lite.mjs"
10
10
 
11
11
  const MIN_SECTION_PROSE = 120
12
- const UNCHECKED_KINDS = new Set(["closing", "sources", "toc", "appendix", "glossary"])
12
+ export const UNCHECKED_KINDS = new Set(["closing", "sources", "toc", "appendix", "glossary"])
13
13
  const DEFINITION_TAGS = new Set(["p", "li", "dd", "dt", "td"])
14
14
  const DEFINITION_LINE = /^\[(S|Source\s*)(\d+)\]/i
15
15
  const MIN_DEFINITIONS = 3
@@ -4,6 +4,7 @@
4
4
  // Plain Node ESM, sibling imports only.
5
5
 
6
6
  import { proseText } from "./html-lite.mjs"
7
+ import { CLAIMS, NOT_CLAIMS } from "./numerals.mjs"
7
8
 
8
9
  const HANGUL = /[\uAC00-\uD7A3]/g
9
10
  const KEEP_ALL = /word-break\s*:\s*keep-all/i
@@ -19,33 +20,6 @@ const MAX_HEADING_WORDS = 12
19
20
  const LINEAGE_TOKEN = /\b(?:MEASURED|ASSUMED|DERIVED)\b/
20
21
  const NUMBER_CONTAINERS = new Set(["figure", "table", "nav", "header", "footer"])
21
22
  const MAX_UNSOURCED = 50
22
- const MONTH = "(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\\.?"
23
- /** Stripped before claim detection: dates, versions, figure refs, times, bare years. */
24
- const NOT_CLAIMS = [
25
- /\b\d{4}[-./]\d{1,2}(?:[-./]\d{1,2})?\b/g,
26
- /\b\d{2}\.\d{2}\.\d{2}\b/g,
27
- /\d{2,4}\s*\uB144\s*\d{1,2}\s*\uC6D4(?:\s*\d{1,2}\s*\uC77C)?/g,
28
- /\d{1,2}\s*\uC6D4(?:\s*\d{1,2}\s*\uC77C)?/g,
29
- /(?:19|20)\d{2}\s*\uB144/g,
30
- new RegExp(`\\b${MONTH}\\s+\\d{1,2}(?:st|nd|rd|th)?(?:,?\\s+\\d{4})?`, "gi"),
31
- new RegExp(`\\b\\d{1,2}(?:st|nd|rd|th)?\\s+${MONTH}(?:\\s+\\d{4})?`, "gi"),
32
- /\bv\d+(?:\.\d+)*\b|\b\d+(?:\.\d+){2,}\b/gi,
33
- /\b(?:version|ver\.?|release)\s*\d+(?:\.\d+)*/gi,
34
- /\b(?:fig(?:ure)?\.?|table|chart|section|step|phase|wave|part)\s*\d+(?:\.\d+)*/gi,
35
- /(?:\uADF8\uB9BC|\uB3C4\uD45C|\uD45C|\uBC84\uC804)\s*\d+(?:\.\d+)*/g,
36
- /\b\d{1,2}:\d{2}\b/g,
37
- /\b(?:19|20)\d{2}s?\b/g,
38
- ]
39
- const NUM = "(?<![\\p{L}\\d.,])\\d+(?:[.,]\\d+)*"
40
- const UNIT =
41
- "(?:%|\\u2030|\\u00D7|\\uD37C\\uC13C\\uD2B8|\\uB2EC\\uB7EC|\\uC720\\uB85C|\\uC5D4|\\uC6D0|\\uB9CC|\\uC5B5|\\uC870|\\uCC9C|\\uCD08|\\uBD84|\\uC2DC\\uAC04|\\uC77C|\\uC8FC|\\uAC1C\\uC6D4|\\uB144|\\uBA85|\\uAC74|\\uAC1C|\\uD68C|\\uBC88|\\uBC30" +
42
- "|(?:percent|pp|bps?|x|k|m|b|bn|million|billion|thousand|trillion|usd|krw|eur|ms|s|secs?|seconds?|mins?|minutes?|h|hrs?|hours?|days?|weeks?|months?|years?|kb|mb|gb|tb|users?|people|employees|engineers|tokens?|requests?|stars?|downloads?|customers?)(?![\\p{L}\\d]))"
43
- const CLAIMS = [
44
- new RegExp(`[$\\u20AC\\u00A3\\u00A5\\u20A9]\\s?${NUM}`, "u"),
45
- new RegExp(`${NUM}\\s?${UNIT}`, "iu"),
46
- /(?<![\d.,])\d{1,3}(?:,\d{3})+(?![\d,])/,
47
- /(?<![\d.])\d+\.\d+(?![\d.])/,
48
- ]
49
23
 
50
24
  function snippet(text, index, span = 24) {
51
25
  return text.slice(Math.max(0, index - span), index + span).trim()
@@ -0,0 +1,47 @@
1
+ // Bilingual fidelity lexicon: data only, read by the fidelity checks and the meaning-pair router.
2
+ // Korean entries are \u escapes (this directory ships no Hangul); each list is frozen.
3
+
4
+ // Hedge tokens: a source hedge the report drops is hedge_dropped.
5
+ export const HEDGES = Object.freeze([
6
+ ...["estimated", "about", "approximately", "may", "might", "expected", "forecast", "provisional", "not yet"],
7
+ ...["\uCD94\uC815", "\uC57D", "\uAC00\uB2A5", "\uC804\uB9DD", "\uC608\uC0C1", "\uC7A0\uC815", "\uC544\uC9C1", "\uBBF8\uD655\uC815", "\uAC83\uC73C\uB85C \uBCF4\uC778\uB2E4"],
8
+ ])
9
+
10
+ // Closed qualifier list for the ledger qualifier field (bounding or approximation words).
11
+ export const QUALIFIERS = Object.freeze([
12
+ ...["up to", "at least", "at most", "about", "approximately", "nearly", "over", "under", "more than", "less than"],
13
+ ...["\uCD5C\uB300", "\uCD5C\uC18C", "\uC57D", "\uC774\uC0C1", "\uC774\uD558", "\uBBF8\uB9CC", "\uCD08\uACFC", "\uB0B4\uC678", "\uAE4C\uC9C0"],
14
+ ])
15
+
16
+ // Direction classes: a source in one class and an assertion in the other is direction_flipped.
17
+ export const DIRECTION = Object.freeze({
18
+ up: Object.freeze([...["increase", "rise", "grow", "up"], ...["\uC99D\uAC00", "\uC0C1\uC2B9", "\uB298"]]),
19
+ down: Object.freeze([...["decrease", "fall", "decline", "down"], ...["\uAC10\uC18C", "\uD558\uB77D", "\uC904"]]),
20
+ })
21
+
22
+ // Causal connectives: one in the assertion but in no supporting quote is causality_added.
23
+ export const CAUSAL = Object.freeze([
24
+ ...["because", "due to", "leads to", "results in", "causes", "therefore"],
25
+ ...["\uB54C\uBB38\uC5D0", "\uB85C \uC778\uD574", "\uB530\uB77C\uC11C", "\uADF8\uB798\uC11C", "\uCD08\uB798"],
26
+ ])
27
+
28
+ // Concessive connectives for the hedge-position routing heuristic.
29
+ export const CONCESSIVE = Object.freeze([
30
+ ...["although", "but", "however", "despite", "while"],
31
+ ...["\uC73C\uB098", "\uC9C0\uB9CC", "\uC5D0\uB3C4 \uBD88\uAD6C\uD558\uACE0", "\uBC18\uBA74", "\uC640 \uB2EC\uB9AC"],
32
+ ])
33
+
34
+ // Narrow-to-broad scope pairs [narrow, broad] for scope_widened_warn.
35
+ export const SCOPE_NARROW_TO_BROAD = Object.freeze([
36
+ Object.freeze(["\uBD81\uBBF8", "\uC804 \uC138\uACC4"]),
37
+ Object.freeze(["pilot", "system"]),
38
+ Object.freeze(["one region", "all regions"]),
39
+ Object.freeze(["\uC77C\uBD80", "\uC804\uCCB4"]),
40
+ Object.freeze(["\uD55C \uC9C0\uC5ED", "\uC804\uAD6D"]),
41
+ ])
42
+
43
+ // Negation tokens; negated direction cases route to a reader instead of the lexicon.
44
+ export const NEGATION = Object.freeze([
45
+ ...["not", "no", "never"],
46
+ ...["\uC54A", "\uC5C6", "\uBABB"],
47
+ ])
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from "bun:test"
2
+
3
+ import { CAUSAL, CONCESSIVE, DIRECTION, HEDGES, NEGATION, QUALIFIERS, SCOPE_NARROW_TO_BROAD } from "./lexicon.mjs"
4
+
5
+ const LISTS = { HEDGES, QUALIFIERS, CAUSAL, CONCESSIVE, NEGATION, SCOPE_NARROW_TO_BROAD } as const
6
+
7
+ describe("lexicon", () => {
8
+ test("given each exported list when read then it is non-empty and frozen", () => {
9
+ for (const [name, list] of Object.entries(LISTS)) {
10
+ expect({ name, length: list.length > 0 }).toEqual({ name, length: true })
11
+ expect({ name, frozen: Object.isFrozen(list) }).toEqual({ name, frozen: true })
12
+ }
13
+ expect(Object.isFrozen(DIRECTION)).toBe(true)
14
+ expect(Object.isFrozen(DIRECTION.up)).toBe(true)
15
+ expect(Object.isFrozen(DIRECTION.down)).toBe(true)
16
+ for (const pair of SCOPE_NARROW_TO_BROAD) expect(Object.isFrozen(pair)).toBe(true)
17
+ })
18
+
19
+ test("given HEDGES when read then it carries the named English and Korean hedges", () => {
20
+ const english = ["estimated", "about", "approximately", "may", "might", "expected", "forecast", "provisional", "not yet"]
21
+ const korean = ["추정", "약", "가능", "전망", "예상", "잠정", "아직", "미확정", "것으로 보인다"]
22
+ expect(HEDGES).toEqual(expect.arrayContaining([...english, ...korean]))
23
+ })
24
+
25
+ test("given QUALIFIERS when read then it is exactly the closed bilingual list", () => {
26
+ const korean = ["최대", "최소", "약", "이상", "이하", "미만", "초과", "내외", "까지"]
27
+ const english = ["up to", "at least", "at most", "about", "approximately", "nearly", "over", "under", "more than", "less than"]
28
+ expect([...QUALIFIERS].sort()).toEqual([...korean, ...english].sort())
29
+ })
30
+
31
+ test("given DIRECTION when read then up and down carry both languages", () => {
32
+ expect(DIRECTION.up).toEqual(expect.arrayContaining(["증가", "상승", "늘", "increase", "rise", "grow", "up"]))
33
+ expect(DIRECTION.down).toEqual(expect.arrayContaining(["감소", "하락", "줄", "decrease", "fall", "decline", "down"]))
34
+ })
35
+
36
+ test("given CAUSAL, CONCESSIVE and NEGATION when read then they carry the named connectives", () => {
37
+ expect(CAUSAL).toEqual(
38
+ expect.arrayContaining(["because", "due to", "leads to", "results in", "causes", "therefore", "때문에", "로 인해", "따라서", "그래서", "초래"]),
39
+ )
40
+ expect(CONCESSIVE).toEqual(
41
+ expect.arrayContaining(["although", "but", "however", "despite", "while", "으나", "지만", "에도 불구하고", "반면", "와 달리"]),
42
+ )
43
+ expect(NEGATION).toEqual(expect.arrayContaining(["not", "no", "never", "않", "없", "못"]))
44
+ })
45
+
46
+ test("given SCOPE_NARROW_TO_BROAD when read then it carries the named narrow-to-broad pairs", () => {
47
+ expect(SCOPE_NARROW_TO_BROAD).toEqual(
48
+ expect.arrayContaining([
49
+ ["북미", "전 세계"],
50
+ ["pilot", "system"],
51
+ ["one region", "all regions"],
52
+ ["일부", "전체"],
53
+ ["한 지역", "전국"],
54
+ ]),
55
+ )
56
+ })
57
+ })
@@ -0,0 +1,226 @@
1
+ // numerals.mjs - shared numeral patterns and the numeral parser.
2
+ //
3
+ // NUM, UNIT, NOT_CLAIMS and CLAIMS are the claim-detection patterns the unsourced-number gate uses
4
+ // (gates-text.mjs imports them from here). parseNumeral reads one numeral into an exact decimal
5
+ // string: thousands separators, Korean and Latin scale words, percent and percentage points, the
6
+ // English number words one to twelve, an adjacent currency symbol or unit, and an adjacent bounding
7
+ // qualifier. No numeral is ever passed through Number(). Plain Node ESM, sibling imports only.
8
+
9
+ import { add, fromDecimal, mul, toDecimal } from "./rational.mjs"
10
+ import { normalizeUnit } from "./units.mjs"
11
+
12
+ const MONTH = "(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\\.?"
13
+ /** Stripped before claim detection: dates, versions, figure refs, times, bare years. */
14
+ export const NOT_CLAIMS = [
15
+ /\b\d{4}[-./]\d{1,2}(?:[-./]\d{1,2})?\b/g,
16
+ /\b\d{2}\.\d{2}\.\d{2}\b/g,
17
+ /\d{2,4}\s*\uB144\s*\d{1,2}\s*\uC6D4(?:\s*\d{1,2}\s*\uC77C)?/g,
18
+ /\d{1,2}\s*\uC6D4(?:\s*\d{1,2}\s*\uC77C)?/g,
19
+ /(?:19|20)\d{2}\s*\uB144/g,
20
+ new RegExp(`\\b${MONTH}\\s+\\d{1,2}(?:st|nd|rd|th)?(?:,?\\s+\\d{4})?`, "gi"),
21
+ new RegExp(`\\b\\d{1,2}(?:st|nd|rd|th)?\\s+${MONTH}(?:\\s+\\d{4})?`, "gi"),
22
+ /\bv\d+(?:\.\d+)*\b|\b\d+(?:\.\d+){2,}\b/gi,
23
+ /\b(?:version|ver\.?|release)\s*\d+(?:\.\d+)*/gi,
24
+ /\b(?:fig(?:ure)?\.?|table|chart|section|step|phase|wave|part)\s*\d+(?:\.\d+)*/gi,
25
+ /(?:\uADF8\uB9BC|\uB3C4\uD45C|\uD45C|\uBC84\uC804)\s*\d+(?:\.\d+)*/g,
26
+ /\b\d{1,2}:\d{2}\b/g,
27
+ /\b(?:19|20)\d{2}s?\b/g,
28
+ ]
29
+ export const NUM = "(?<![\\p{L}\\d.,])\\d+(?:[.,]\\d+)*"
30
+ export const UNIT =
31
+ "(?:%|\\u2030|\\u00D7|\\uD37C\\uC13C\\uD2B8|\\uB2EC\\uB7EC|\\uC720\\uB85C|\\uC5D4|\\uC6D0|\\uB9CC|\\uC5B5|\\uC870|\\uCC9C|\\uCD08|\\uBD84|\\uC2DC\\uAC04|\\uC77C|\\uC8FC|\\uAC1C\\uC6D4|\\uB144|\\uBA85|\\uAC74|\\uAC1C|\\uD68C|\\uBC88|\\uBC30" +
32
+ "|(?:percent|pp|bps?|x|k|m|b|bn|million|billion|thousand|trillion|usd|krw|eur|ms|s|secs?|seconds?|mins?|minutes?|h|hrs?|hours?|days?|weeks?|months?|years?|kb|mb|gb|tb|users?|people|employees|engineers|tokens?|requests?|stars?|downloads?|customers?)(?![\\p{L}\\d]))"
33
+ export const CLAIMS = [
34
+ new RegExp(`[$\\u20AC\\u00A3\\u00A5\\u20A9]\\s?${NUM}`, "u"),
35
+ new RegExp(`${NUM}\\s?${UNIT}`, "iu"),
36
+ /(?<![\d.,])\d{1,3}(?:,\d{3})+(?![\d,])/,
37
+ /(?<![\d.])\d+\.\d+(?![\d.])/,
38
+ ]
39
+
40
+ const WORDS = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"]
41
+ const WORD_RE = new RegExp(`\\b(?:${WORDS.join("|")})\\b`, "gi")
42
+ /** Korean scale words: cheon (1e3) inside a group, then man, eok, jo as group scales. */
43
+ const CHEON = "\uCC9C"
44
+ const BIG = new Map([["\uB9CC", 10000n], ["\uC5B5", 100000000n], ["\uC870", 1000000000000n]])
45
+ /** A scale word that starts a longer word is not a scale: jogak (pieces), mankeum (as much as). */
46
+ const KO_GROUP = /^(\s?)(?:(\uCC9C)?([\uB9CC\uC5B5\uC870])|(\uCC9C))(?![\uAC01\uD07C])/
47
+ const KO_NEXT = new RegExp(`^\\s?(${NUM})(\uCC9C)?([\uB9CC\uC5B5\uC870])?`, "u")
48
+ const LATIN_SCALE = /^(?:(k|K|M|bn|B)|\s(thousand|million|billion|trillion|bn))(?![\p{L}\d])/u
49
+ const LATIN_FACTOR = { k: 1000n, K: 1000n, M: 1000000n, bn: 1000000000n, B: 1000000000n, thousand: 1000n, million: 1000000n, billion: 1000000000n, trillion: 1000000000000n }
50
+ const CURRENCY = "$\u20AC\u00A3\u00A5\u20A9"
51
+ const SYMBOL_UNIT = /^\s?(%p|%|\u2030|\u00D7|\u00B0C|\u00B0F|\u2103|\u2109)/
52
+ const LATIN_UNIT = /^\s?([A-Za-z]+)(?![\p{L}\d])/u
53
+ const HANGUL_UNIT = /^\s?([\uAC00-\uD7A3]+)/
54
+ /** "3 bun-ui 1" is the fraction one third, not 3 minutes. */
55
+ const FRACTION = /^\s?\uBD84\uC758\s?\d/
56
+ const UNIT_AFTER = new RegExp(`^\\s?${UNIT}`, "iu")
57
+ const NUM_AT = new RegExp(`^${NUM}`, "u")
58
+ /** Range separators: hyphen, en dash, tilde, "to". */
59
+ const RANGE_SEP = /^(?:\s?[-\u2013~]\s?|\sto\s)/
60
+ const SIGN = "-\u2212"
61
+ /** Bounding qualifiers: before the value (Korean and English) and right after it (Korean). */
62
+ const QUALIFIERS_BEFORE = ["up to", "at least", "at most", "more than", "less than", "about", "approximately", "nearly", "over", "under", "\uCD5C\uB300", "\uCD5C\uC18C", "\uC57D"]
63
+ const QUALIFIER_AFTER = /^\s?(\uC774\uC0C1|\uC774\uD558|\uBBF8\uB9CC|\uCD08\uACFC|\uB0B4\uC678|\uAE4C\uC9C0)/
64
+
65
+ /** "1,237" -> "1237"; a malformed group ("1,23") keeps only the leading plain number. */
66
+ function cleanDigits(raw) {
67
+ if (/^\d{1,3}(?:,\d{3})+(?:\.\d+)?$/.test(raw)) return { digits: raw.replace(/,/g, ""), length: raw.length }
68
+ const plain = /^\d+(?:\.\d+)?/.exec(raw)[0]
69
+ return { digits: plain, length: plain.length }
70
+ }
71
+
72
+ /** The bare-year and four-digit-date patterns: a numeral they cover can still be a value. */
73
+ const YEAR_SHAPED = new Set([NOT_CLAIMS[0], NOT_CLAIMS[12]])
74
+
75
+ function excludedRanges(text) {
76
+ const ranges = []
77
+ for (const re of NOT_CLAIMS) {
78
+ for (const match of text.matchAll(re)) ranges.push([match.index, match.index + match[0].length, YEAR_SHAPED.has(re)])
79
+ }
80
+ return ranges
81
+ }
82
+
83
+ /** A year-shaped numeral is a value when it has a decimal part or a currency, scale word or unit touches it. */
84
+ function anchored(text, at, end, digits) {
85
+ if (digits.includes(".") || (at > 0 && CURRENCY.includes(text[at - 1]))) return true
86
+ return UNIT_AFTER.test(text.slice(end)) || unitAfter(text, end) !== null
87
+ }
88
+
89
+ /**
90
+ * First digit run or number word at or after `from` that no NOT_CLAIMS pattern covers. A year-shaped
91
+ * match exactly over the numeral (not "2020s", not "2020-03") is lifted when the numeral is anchored.
92
+ */
93
+ function findStart(text, from, ranges) {
94
+ const covering = (at) => ranges.filter(([s, e]) => at >= s && at < e)
95
+ let digit = null
96
+ for (const match of text.slice(from).matchAll(new RegExp(NUM, "gu"))) {
97
+ const at = from + match.index
98
+ const candidate = { at, ...cleanDigits(match[0]) }
99
+ const end = at + candidate.length
100
+ const cover = covering(at)
101
+ const lifted = cover.every(([s, e, yearShaped]) => yearShaped && s === at && e <= end)
102
+ if (cover.length === 0 || (lifted && anchored(text, at, end, candidate.digits))) {
103
+ digit = candidate
104
+ break
105
+ }
106
+ }
107
+ const free = (at) => covering(at).length === 0
108
+ for (const match of text.slice(from).matchAll(WORD_RE)) {
109
+ const at = from + match.index
110
+ if (digit && digit.at < at) break
111
+ if (free(at)) return { at, digits: String(WORDS.indexOf(match[0].toLowerCase()) + 1), length: match[0].length }
112
+ }
113
+ return digit
114
+ }
115
+
116
+ /** Korean groups such as "4jo 2,000eok": each later group needs a smaller scale. */
117
+ function koreanScale(text, at, digits) {
118
+ const first = KO_GROUP.exec(text.slice(at))
119
+ if (!first) return null
120
+ const big = first[3] ? BIG.get(first[3]) : 1n
121
+ const firstFactor = (first[2] || first[4] ? 1000n : 1n) * big
122
+ let value = mul(fromDecimal(digits), firstFactor)
123
+ let end = at + first[0].length
124
+ let last = first[3] ? big : 0n
125
+ let groups = 1
126
+ while (last > 1n) {
127
+ const next = KO_NEXT.exec(text.slice(end))
128
+ if (!next) break
129
+ const factor = (next[2] ? 1000n : 1n) * (next[3] ? BIG.get(next[3]) : 1n)
130
+ if (factor <= 1n || factor >= last) break
131
+ value = add(value, mul(fromDecimal(cleanDigits(next[1]).digits), factor))
132
+ end += next[0].length
133
+ last = next[3] ? BIG.get(next[3]) : 0n
134
+ groups++
135
+ }
136
+ return { value, end, factor: groups === 1 ? firstFactor : null }
137
+ }
138
+
139
+ /** Scale words after a numeral: { value (rational) | null, end, factor | null (one plain factor) }. */
140
+ function scaled(text, end, digits) {
141
+ const korean = koreanScale(text, end, digits)
142
+ if (korean) return korean
143
+ const latin = LATIN_SCALE.exec(text.slice(end))
144
+ if (!latin) return { value: null, end, factor: null }
145
+ const factor = LATIN_FACTOR[latin[1] ?? latin[2].toLowerCase()]
146
+ return { value: mul(fromDecimal(digits), factor), end: end + latin[0].length, factor }
147
+ }
148
+
149
+ /** The second endpoint of "5-10", "5 to 10", "5~10" or "5\u201310", or null. */
150
+ function rangeEnd(text, at, ranges) {
151
+ const sep = RANGE_SEP.exec(text.slice(at))
152
+ if (!sep) return null
153
+ const pos = at + sep[0].length
154
+ const num = NUM_AT.exec(text.slice(pos))
155
+ if (!num || ranges.some(([s, e]) => pos >= s && pos < e)) return null
156
+ const { digits, length } = cleanDigits(num[0])
157
+ const scale = scaled(text, pos + length, digits)
158
+ return { value: scale.value ? toDecimal(scale.value) : digits, end: scale.end, factor: scale.factor }
159
+ }
160
+
161
+ function unitAfter(text, at) {
162
+ const rest = text.slice(at)
163
+ if (FRACTION.test(rest)) return null
164
+ const symbol = SYMBOL_UNIT.exec(rest)
165
+ if (symbol) return { unitRaw: symbol[1], end: at + symbol[0].length }
166
+ const latin = LATIN_UNIT.exec(rest)
167
+ if (latin) return normalizeUnit(latin[1]) ? { unitRaw: latin[1], end: at + latin[0].length } : null
168
+ const hangul = HANGUL_UNIT.exec(rest)
169
+ if (!hangul) return null
170
+ const lead = hangul[0].length - hangul[1].length
171
+ for (let size = hangul[1].length; size > 0; size--) {
172
+ const candidate = hangul[1].slice(0, size)
173
+ if (!BIG.has(candidate) && candidate !== CHEON && normalizeUnit(candidate)) return { unitRaw: candidate, end: at + lead + size }
174
+ }
175
+ return null
176
+ }
177
+
178
+ function qualifierBefore(text, at) {
179
+ const tokens = text.slice(0, at).trim().split(/\s+/).slice(-2).map((token) => token.replace(/^[^\p{L}]+/u, ""))
180
+ for (const phrase of QUALIFIERS_BEFORE) {
181
+ const words = phrase.split(" ")
182
+ for (let skip = 0; skip + words.length <= tokens.length; skip++) {
183
+ const window = tokens.slice(tokens.length - skip - words.length, tokens.length - skip)
184
+ if (window.join(" ").toLowerCase() === phrase) return window.join(" ")
185
+ }
186
+ }
187
+ return null
188
+ }
189
+
190
+ /**
191
+ * The first numeral at or after `from`: { value, raw, span: [start, end], unitRaw, qualifierRaw },
192
+ * or null. `value` is an exact decimal string with scale words folded in ("3eok won" -> "300000000",
193
+ * unit "won") and a leading minus sign ("-" or U+2212) kept as "-"; `span` covers the sign, an
194
+ * adjacent currency symbol, the numeral, scale words and the unit. A range ("5-10 m", "5 to 10 m",
195
+ * "5~10%") adds `valueEnd` and `range: true`; the unit, and a scale word written only on the second
196
+ * endpoint ("3~5eok"), apply to both endpoints.
197
+ */
198
+ export function parseNumeral(text, from = 0) {
199
+ const ranges = excludedRanges(text)
200
+ const found = findStart(text, from, ranges)
201
+ if (!found) return null
202
+ let start = found.at
203
+ const first = scaled(text, found.at + found.length, found.digits)
204
+ let value = first.value
205
+ let end = first.end
206
+ const second = rangeEnd(text, end, ranges)
207
+ if (second) {
208
+ end = second.end
209
+ if (!value && second.factor) value = mul(fromDecimal(found.digits), second.factor)
210
+ }
211
+ let unitRaw = null
212
+ if (start > 0 && CURRENCY.includes(text[start - 1])) [unitRaw, start] = [text[start - 1], start - 1]
213
+ const negative = start > 0 && SIGN.includes(text[start - 1]) && !/[\p{L}\p{N}]/u.test(text[start - 2] ?? "")
214
+ if (negative) start--
215
+ const unit = unitRaw ? null : unitAfter(text, end)
216
+ if (unit) [unitRaw, end] = [unit.unitRaw, unit.end]
217
+ const after = QUALIFIER_AFTER.exec(text.slice(end))
218
+ return {
219
+ value: (negative ? "-" : "") + (value ? toDecimal(value) : found.digits),
220
+ ...(second ? { valueEnd: second.value, range: true } : {}),
221
+ raw: text.slice(start, end),
222
+ span: [start, end],
223
+ unitRaw,
224
+ qualifierRaw: qualifierBefore(text, start) ?? (after ? after[1] : null),
225
+ }
226
+ }
@@ -0,0 +1,120 @@
1
+ import { describe, expect, test } from "bun:test"
2
+
3
+ import { CLAIMS, NOT_CLAIMS, NUM, parseNumeral, UNIT } from "./numerals.mjs"
4
+ import { normalizeUnit } from "./units.mjs"
5
+
6
+ describe("shared claim patterns", () => {
7
+ test("exports the gate patterns", () => {
8
+ expect(typeof NUM).toBe("string")
9
+ expect(typeof UNIT).toBe("string")
10
+ expect(NOT_CLAIMS.length).toBe(13)
11
+ expect(CLAIMS.length).toBe(4)
12
+ })
13
+ })
14
+
15
+ describe("parseNumeral", () => {
16
+ test("a length with its unit", () => {
17
+ expect(parseNumeral("320 m")).toEqual({ value: "320", raw: "320 m", span: [0, 5], unitRaw: "m", qualifierRaw: null })
18
+ expect(parseNumeral("320 mm")?.unitRaw).toBe("mm")
19
+ })
20
+
21
+ test("thousands separators", () => {
22
+ const parsed = parseNumeral("we shipped 1,237 units")
23
+ expect(parsed?.value).toBe("1237")
24
+ expect(parsed?.span).toEqual([11, 16])
25
+ })
26
+
27
+ test("Korean scale words fold into the value and leave the currency unit", () => {
28
+ const parsed = parseNumeral("3억 원")
29
+ expect(parsed?.value).toBe("300000000")
30
+ expect(parsed?.raw).toBe("3억 원")
31
+ expect(normalizeUnit(parsed?.unitRaw as string)).toBe(normalizeUnit("원"))
32
+ expect(parseNumeral("4조 2,000억 원")?.value).toBe("4200000000000")
33
+ expect(parseNumeral("3천만 원")?.value).toBe("30000000")
34
+ })
35
+
36
+ test("Latin suffixes and scale words", () => {
37
+ expect(parseNumeral("1.2k")?.value).toBe("1200")
38
+ expect(parseNumeral("$3M")).toMatchObject({ value: "3000000", raw: "$3M", unitRaw: "$" })
39
+ expect(parseNumeral("2.5bn")?.value).toBe("2500000000")
40
+ expect(parseNumeral("$4B")?.value).toBe("4000000000")
41
+ expect(parseNumeral("3 million users")?.value).toBe("3000000")
42
+ expect(parseNumeral("5 km")).toMatchObject({ value: "5", unitRaw: "km" })
43
+ })
44
+
45
+ test("percent and percentage points stay distinct", () => {
46
+ expect(parseNumeral("15.2%")).toMatchObject({ value: "15.2", unitRaw: "%" })
47
+ expect(parseNumeral("2%p")).toMatchObject({ value: "2", unitRaw: "%p" })
48
+ })
49
+
50
+ test("long decimals keep every digit", () => {
51
+ expect(parseNumeral("10000000000000000.1")?.value).toBe("10000000000000000.1")
52
+ })
53
+
54
+ test("English number words one to twelve only", () => {
55
+ expect(parseNumeral("three engineers")?.value).toBe("3")
56
+ expect(parseNumeral("Twelve")?.value).toBe("12")
57
+ expect(parseNumeral("thirteen")).toBeNull()
58
+ })
59
+
60
+ test("qualifiers before and after the value", () => {
61
+ expect(parseNumeral("최대 4조 2,000억 원")).toMatchObject({ value: "4200000000000", qualifierRaw: "최대", span: [3, 14] })
62
+ expect(parseNumeral("up to 320 m")?.qualifierRaw).toBe("up to")
63
+ expect(parseNumeral("320 m 이상")?.qualifierRaw).toBe("이상")
64
+ expect(parseNumeral("the 320 m mast")?.qualifierRaw).toBeNull()
65
+ })
66
+
67
+ test("dates, versions and figure references are not numerals", () => {
68
+ expect(parseNumeral("v2.3.1")).toBeNull()
69
+ expect(parseNumeral("see figure 3")).toBeNull()
70
+ expect(parseNumeral("2026-10-23")).toBeNull()
71
+ expect(parseNumeral("v2.3.1 shipped 40 fixes")?.value).toBe("40")
72
+ })
73
+
74
+ test("year-shaped values next to a currency, scale word or unit, or with a decimal part, are numerals", () => {
75
+ expect(parseNumeral("2000억 원")).toMatchObject({ value: "200000000000", unitRaw: "원" })
76
+ expect(parseNumeral("1900만 원")).toMatchObject({ value: "19000000", unitRaw: "원" })
77
+ expect(parseNumeral("2050명")).toMatchObject({ value: "2050", unitRaw: "명" })
78
+ expect(parseNumeral("$2000")).toMatchObject({ value: "2000", raw: "$2000", unitRaw: "$" })
79
+ expect(parseNumeral("1999 users")?.value).toBe("1999")
80
+ expect(parseNumeral("1234.56 m")).toMatchObject({ value: "1234.56", unitRaw: "m" })
81
+ expect(parseNumeral("$1500.25")).toMatchObject({ value: "1500.25", unitRaw: "$" })
82
+ expect(parseNumeral("2020.5억")?.value).toBe("202050000000")
83
+ })
84
+
85
+ test("bare years and dates stay excluded", () => {
86
+ expect(parseNumeral("in 2020")).toBeNull()
87
+ expect(parseNumeral("2020-03-01")).toBeNull()
88
+ expect(parseNumeral("the 2020s")).toBeNull()
89
+ expect(parseNumeral("in 2020, 5 users")?.value).toBe("5")
90
+ })
91
+
92
+ test("a leading minus sign is part of the value and the span", () => {
93
+ expect(parseNumeral("-5%")).toMatchObject({ value: "-5", raw: "-5%", span: [0, 3], unitRaw: "%" })
94
+ expect(parseNumeral("fell \u22123.2%")).toMatchObject({ value: "-3.2", span: [5, 10], unitRaw: "%" })
95
+ expect(parseNumeral("COVID-19 cases")?.value).toBe("19")
96
+ })
97
+
98
+ test("ranges carry both endpoints and share the unit", () => {
99
+ expect(parseNumeral("5-10 m")).toMatchObject({ value: "5", valueEnd: "10", range: true, raw: "5-10 m", span: [0, 6], unitRaw: "m" })
100
+ expect(parseNumeral("5 to 10 m")).toMatchObject({ value: "5", valueEnd: "10", range: true, raw: "5 to 10 m", unitRaw: "m" })
101
+ expect(parseNumeral("5~10%")).toMatchObject({ value: "5", valueEnd: "10", range: true, unitRaw: "%" })
102
+ expect(parseNumeral("5\u201310%")).toMatchObject({ value: "5", valueEnd: "10", range: true, unitRaw: "%" })
103
+ expect(parseNumeral("3~5억 원")).toMatchObject({ value: "300000000", valueEnd: "500000000", range: true, unitRaw: "원" })
104
+ expect(parseNumeral("320 m")).not.toHaveProperty("valueEnd")
105
+ })
106
+
107
+ test("Korean scale words and units need a word boundary", () => {
108
+ expect(parseNumeral("5조각")).toMatchObject({ value: "5", raw: "5", unitRaw: null })
109
+ expect(parseNumeral("3분의 1")).toMatchObject({ value: "3", raw: "3", unitRaw: null })
110
+ expect(parseNumeral("3분의 휴식")?.unitRaw).toBe("분")
111
+ expect(parseNumeral("매출 10억은")?.value).toBe("1000000000")
112
+ })
113
+
114
+ test("from skips earlier numerals", () => {
115
+ const text = "320 m and 15%"
116
+ const first = parseNumeral(text)
117
+ expect(parseNumeral(text, first?.span[1])).toMatchObject({ value: "15", unitRaw: "%" })
118
+ expect(parseNumeral("no numbers here")).toBeNull()
119
+ })
120
+ })
@@ -5,8 +5,7 @@
5
5
  import * as nodeFs from "node:fs"
6
6
  import { basename, dirname, join } from "node:path"
7
7
 
8
- import { DELIVERABLE_FORMATS, DELIVERABLE_STATUSES, GATE_NAMES, GATE_STATUSES, LANES } from "./contracts.mjs"
9
- import { MANIFEST_CODES, SCHEMA_VERSION, validateOutcomeManifest } from "./contracts.mjs"
8
+ import { DELIVERABLE_FORMATS, DELIVERABLE_STATUSES, GATE_NAMES, GATE_STATUSES, LANES, MANIFEST_CODES, normalizeManifest, SCHEMA_VERSION, validateOutcomeManifest } from "./contracts.mjs"
10
9
 
11
10
  export const MANIFEST_FILENAME = "outcome.json"
12
11
  const MISSING = MANIFEST_CODES[0]
@@ -226,7 +225,7 @@ function assertValid(manifest, where) {
226
225
  export function readManifest(file) {
227
226
  let manifest
228
227
  try {
229
- manifest = JSON.parse(nodeFs.readFileSync(file, "utf8"))
228
+ manifest = normalizeManifest(JSON.parse(nodeFs.readFileSync(file, "utf8")))
230
229
  } catch (error) {
231
230
  if (error instanceof SyntaxError) throw new OutcomeError("invalid_manifest", `${file}: not valid JSON (${error.message})`)
232
231
  throw error
@@ -258,6 +258,35 @@ describe("readManifest and writeManifest", () => {
258
258
  writeFileSync(file, "{not json")
259
259
  expect(() => readManifest(file)).toThrow(OutcomeError)
260
260
  })
261
+
262
+ test("#given a manifest file written before the fidelity gate (four gates) #when read and written back #then it loads and the file gains fidelity not_run", () => {
263
+ // given
264
+ const dir = sessionDir()
265
+ const file = join(dir, MANIFEST_FILENAME)
266
+ const old = {
267
+ schemaVersion: 1,
268
+ sessionDir: dir,
269
+ startedAt: START_LOCAL.toISOString(),
270
+ finishedAt: null,
271
+ elapsedMinutes: null,
272
+ lane: "no-format",
273
+ interview: null,
274
+ deliverables: [{ format: "pdf", status: "pending", promised: true, updatedAt: START_LOCAL.toISOString() }],
275
+ renders: [],
276
+ gates: { static: "pass", layout: "not_run", visual: "fail", proofread: "not_run" },
277
+ residualDefects: [],
278
+ repair: null,
279
+ sources: null,
280
+ }
281
+ writeFileSync(file, JSON.stringify(old))
282
+ // when
283
+ const manifest = readManifest(file)
284
+ writeManifest(file, manifest)
285
+ // then
286
+ const written = JSON.parse(readFileSync(file, "utf8"))
287
+ expect(written.gates).toEqual({ static: "pass", fidelity: "not_run", layout: "not_run", visual: "fail", proofread: "not_run" })
288
+ expect(written.schemaVersion).toBe(1)
289
+ })
261
290
  })
262
291
 
263
292
  describe("round trip in a stamped session dir", () => {