lazycodex-ai 5.1.44 → 5.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (412) hide show
  1. package/README.de.md +0 -5
  2. package/README.es.md +0 -5
  3. package/README.fr.md +0 -5
  4. package/README.it.md +0 -5
  5. package/README.ja.md +0 -5
  6. package/README.ko.md +0 -5
  7. package/README.md +4 -5
  8. package/README.pt-br.md +0 -5
  9. package/README.ru.md +0 -6
  10. package/README.zh-cn.md +0 -5
  11. package/dist/cli/index.js +865 -413
  12. package/dist/cli/service-auth/keystore.d.ts +4 -2
  13. package/dist/cli-node/index.js +865 -413
  14. package/docs/reference/web-terminal-visual-qa.md +8 -0
  15. package/package.json +1 -1
  16. package/packages/lsp-daemon/dist/cli.js +7 -7
  17. package/packages/lsp-daemon/dist/client.js +7 -7
  18. package/packages/lsp-daemon/dist/index.js +7 -7
  19. package/packages/lsp-daemon/package.json +3 -3
  20. package/packages/lsp-tools-mcp/dist/cli.js +7 -7
  21. package/packages/lsp-tools-mcp/dist/mcp.js +7 -7
  22. package/packages/lsp-tools-mcp/dist/tools.js +4 -4
  23. package/packages/lsp-tools-mcp/package.json +3 -3
  24. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  25. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +77 -26
  26. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +1 -1
  29. package/packages/omo-codex/plugin/components/comment-checker/biome.json +1 -21
  30. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
  32. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  33. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  34. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/biome.json +1 -21
  35. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  36. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +4 -4
  37. package/packages/omo-codex/plugin/components/lsp/biome.json +1 -21
  38. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  39. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +4 -4
  40. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  41. package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
  42. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +0 -1
  43. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +1 -1
  44. package/packages/omo-codex/plugin/components/rules/biome.json +1 -21
  45. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  46. package/packages/omo-codex/plugin/components/rules/package.json +4 -4
  47. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  48. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  49. package/packages/omo-codex/plugin/components/telemetry/biome.json +1 -21
  50. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +90 -1
  51. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +91 -1
  52. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  53. package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
  54. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +2 -2
  55. package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -21
  56. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  57. package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
  58. package/packages/omo-codex/plugin/components/ulw-execute-continuation/biome.json +1 -21
  59. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  60. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  61. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +4 -4
  62. package/packages/omo-codex/plugin/components/ulw-loop/biome.json +1 -21
  63. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +1 -1
  64. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +7 -7
  65. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +5 -5
  66. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +1 -1
  67. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  68. package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
  69. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +1 -1
  70. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
  71. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +1 -1
  72. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +2 -0
  73. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  85. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  86. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  87. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  88. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  92. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  93. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  94. package/packages/omo-codex/plugin/package-lock.json +12 -12
  95. package/packages/omo-codex/plugin/package.json +1 -1
  96. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +14 -9
  97. package/packages/omo-codex/plugin/skills/ast-grep/AGENTS.md +1 -1
  98. package/packages/omo-codex/plugin/skills/browser/SKILL.md +21 -22
  99. package/packages/omo-codex/plugin/skills/browser/references/commands.md +2 -9
  100. package/packages/omo-codex/plugin/skills/browser/references/install.md +1 -2
  101. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  102. package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +2 -2
  103. package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +63 -487
  104. package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +1 -1
  105. package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +1 -10
  106. package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +2 -2
  107. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +79 -118
  108. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +75 -224
  109. package/packages/omo-codex/plugin/skills/frontend/references/design/stylegallery.md +32 -27
  110. package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -2
  111. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
  112. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
  113. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/collect-files.ts +53 -0
  114. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
  115. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
  116. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
  117. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
  118. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
  119. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
  120. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
  121. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
  122. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
  123. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
  124. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
  125. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
  126. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
  127. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
  128. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
  129. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
  130. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
  131. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
  132. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
  133. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +1 -1
  134. package/packages/omo-codex/plugin/skills/programming/SKILL.md +10 -7
  135. package/packages/omo-codex/plugin/skills/programming/references/typescript/README.md +10 -7
  136. package/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md +9 -23
  137. package/packages/omo-codex/plugin/skills/programming/references/typescript/oxc-lint.md +130 -0
  138. package/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md +6 -62
  139. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
  140. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/new-project.ts +20 -81
  141. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
  142. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
  143. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
  144. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
  145. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
  146. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
  147. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
  148. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +14 -14
  149. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +1 -1
  150. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +2 -2
  151. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  152. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +2 -2
  153. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  154. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +21 -3
  155. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +37 -12
  156. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +77 -16
  157. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +134 -0
  158. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger.md +95 -0
  159. package/packages/omo-codex/plugin/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  160. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +68 -3
  161. package/packages/omo-codex/plugin/skills/ulw-research/references/report-visualization.md +2 -0
  162. package/packages/omo-codex/plugin/skills/ulw-research/references/unit-rules.md +157 -0
  163. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  164. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claims-index.mjs +125 -0
  165. package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +19 -5
  166. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +89 -3
  167. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
  168. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
  169. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +239 -0
  170. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  171. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  172. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  173. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +250 -0
  174. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +221 -0
  175. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +123 -0
  176. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  177. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
  178. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  179. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  180. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +150 -0
  181. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  182. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  183. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  184. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +64 -0
  185. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  186. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  187. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  188. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +29 -9
  189. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +1 -1
  190. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +1 -27
  191. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +2 -2
  192. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-markup.mjs +182 -0
  193. package/packages/omo-codex/plugin/skills/ulw-research/scripts/lexicon.mjs +47 -0
  194. package/packages/omo-codex/plugin/skills/ulw-research/scripts/md-lite.mjs +193 -0
  195. package/packages/omo-codex/plugin/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  196. package/packages/omo-codex/plugin/skills/ulw-research/scripts/numerals.mjs +226 -0
  197. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +8 -8
  198. package/packages/omo-codex/plugin/skills/ulw-research/scripts/particles-ko.mjs +108 -0
  199. package/packages/omo-codex/plugin/skills/ulw-research/scripts/placeholder.mjs +184 -0
  200. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  201. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  202. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-project.mjs +125 -0
  203. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  204. package/packages/omo-codex/plugin/skills/ulw-research/scripts/rational.mjs +139 -0
  205. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +21 -10
  206. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +15 -0
  207. package/packages/omo-codex/plugin/skills/ulw-research/scripts/sentence-split.mjs +77 -0
  208. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +140 -0
  209. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  210. package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-normalize.mjs +82 -0
  211. package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-similarity.mjs +28 -0
  212. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  213. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  214. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  215. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  216. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  217. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  218. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  219. package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +105 -0
  220. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +71 -64
  221. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +4 -1
  222. package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +19 -0
  223. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +12 -8
  224. package/packages/omo-codex/scripts/install-dist/install-local.mjs +169 -30
  225. package/packages/shared-skills/skills/ast-grep/AGENTS.md +1 -1
  226. package/packages/shared-skills/skills/browser/SKILL.md +21 -22
  227. package/packages/shared-skills/skills/browser/references/commands.md +2 -9
  228. package/packages/shared-skills/skills/browser/references/install.md +1 -2
  229. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  230. package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +2 -2
  231. package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +63 -487
  232. package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +1 -1
  233. package/packages/shared-skills/skills/browser/scripts/omowright.mjs +1 -10
  234. package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +2 -2
  235. package/packages/shared-skills/skills/frontend/SKILL.md +79 -118
  236. package/packages/shared-skills/skills/frontend/references/design/README.md +75 -224
  237. package/packages/shared-skills/skills/frontend/references/design/stylegallery.md +32 -27
  238. package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -2
  239. package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
  240. package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.test.ts +101 -0
  241. package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
  242. package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.test.ts +50 -0
  243. package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.ts +53 -0
  244. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
  245. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
  246. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
  247. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
  248. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
  249. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
  250. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
  251. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
  252. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
  253. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
  254. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
  255. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
  256. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
  257. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
  258. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
  259. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
  260. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
  261. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
  262. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
  263. package/packages/shared-skills/skills/programming/SKILL.md +10 -7
  264. package/packages/shared-skills/skills/programming/references/typescript/README.md +10 -7
  265. package/packages/shared-skills/skills/programming/references/typescript/bootstrap.md +9 -23
  266. package/packages/shared-skills/skills/programming/references/typescript/oxc-lint.md +130 -0
  267. package/packages/shared-skills/skills/programming/references/typescript/tsconfig-strict.md +6 -62
  268. package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
  269. package/packages/shared-skills/skills/programming/scripts/typescript/new-project.test.ts +162 -0
  270. package/packages/shared-skills/skills/programming/scripts/typescript/new-project.ts +20 -81
  271. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
  272. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
  273. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
  274. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
  275. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
  276. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
  277. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
  278. package/packages/shared-skills/skills/refactor/SKILL.md +13 -13
  279. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  280. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  281. package/packages/shared-skills/skills/ulw-research/AGENTS.md +21 -3
  282. package/packages/shared-skills/skills/ulw-research/SKILL.md +37 -12
  283. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +77 -16
  284. package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +134 -0
  285. package/packages/shared-skills/skills/ulw-research/references/fact-ledger.md +95 -0
  286. package/packages/shared-skills/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  287. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +68 -3
  288. package/packages/shared-skills/skills/ulw-research/references/report-visualization.md +2 -0
  289. package/packages/shared-skills/skills/ulw-research/references/unit-rules.md +157 -0
  290. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  291. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.test.ts +193 -0
  292. package/packages/shared-skills/skills/ulw-research/scripts/claims-index.mjs +125 -0
  293. package/packages/shared-skills/skills/ulw-research/scripts/claims-index.test.ts +149 -0
  294. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +19 -5
  295. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +13 -2
  296. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +89 -3
  297. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +100 -12
  298. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns.test.ts +183 -0
  299. package/packages/shared-skills/skills/ulw-research/scripts/delivery-barrier.test.ts +245 -0
  300. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
  301. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
  302. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +239 -0
  303. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  304. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.test.ts +375 -0
  305. package/packages/shared-skills/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  306. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  307. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.test.ts +221 -0
  308. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +197 -0
  309. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +250 -0
  310. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +156 -0
  311. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +160 -0
  312. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +221 -0
  313. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.test.ts +43 -0
  314. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +123 -0
  315. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +254 -0
  316. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  317. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.test.ts +118 -0
  318. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
  319. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.test.ts +167 -0
  320. package/packages/shared-skills/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  321. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +409 -0
  322. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-rules.test.ts +388 -0
  323. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  324. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.test.ts +81 -0
  325. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +150 -0
  326. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.test.ts +179 -0
  327. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  328. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  329. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.test.ts +117 -0
  330. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  331. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +64 -0
  332. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.test.ts +46 -0
  333. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  334. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.test.ts +58 -0
  335. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  336. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.test.ts +270 -0
  337. package/packages/shared-skills/skills/ulw-research/scripts/gates-context.test.ts +64 -0
  338. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  339. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.test.ts +73 -0
  340. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +29 -9
  341. package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +1 -1
  342. package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +1 -27
  343. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +2 -2
  344. package/packages/shared-skills/skills/ulw-research/scripts/html-markup.mjs +182 -0
  345. package/packages/shared-skills/skills/ulw-research/scripts/lexicon.mjs +47 -0
  346. package/packages/shared-skills/skills/ulw-research/scripts/lexicon.test.ts +57 -0
  347. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.mjs +193 -0
  348. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.test.ts +271 -0
  349. package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  350. package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.test.ts +228 -0
  351. package/packages/shared-skills/skills/ulw-research/scripts/numerals.mjs +226 -0
  352. package/packages/shared-skills/skills/ulw-research/scripts/numerals.test.ts +120 -0
  353. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +8 -8
  354. package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +29 -0
  355. package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.mjs +108 -0
  356. package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.test.ts +86 -0
  357. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.mjs +184 -0
  358. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.test.ts +77 -0
  359. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  360. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.test.ts +363 -0
  361. package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  362. package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.test.ts +168 -0
  363. package/packages/shared-skills/skills/ulw-research/scripts/polish-markup.test.ts +187 -0
  364. package/packages/shared-skills/skills/ulw-research/scripts/polish-project.mjs +125 -0
  365. package/packages/shared-skills/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  366. package/packages/shared-skills/skills/ulw-research/scripts/rational.mjs +139 -0
  367. package/packages/shared-skills/skills/ulw-research/scripts/rational.test.ts +46 -0
  368. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +21 -10
  369. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +15 -0
  370. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +458 -2
  371. package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.mjs +77 -0
  372. package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.test.ts +109 -0
  373. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +140 -0
  374. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  375. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.test.ts +113 -0
  376. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/assets/share.csv +3 -0
  377. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/claim-graph.md +13 -0
  378. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/gen-legacy-dev.mjs +89 -0
  379. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-basic.json +223 -0
  380. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-legacy-dev.json +859 -0
  381. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-verify.json +420 -0
  382. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-bad.html +19 -0
  383. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-good.html +39 -0
  384. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/acronyms.json +3 -0
  385. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/meta.json +9 -0
  386. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/sentences.jsonl +7 -0
  387. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/text.txt +7 -0
  388. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4.html +19 -0
  389. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/acronyms.json +1 -0
  390. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/meta.json +9 -0
  391. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/sentences.jsonl +1 -0
  392. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/text.txt +1 -0
  393. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S9.txt +1 -0
  394. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-locked.md +17 -0
  395. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-pending.md +17 -0
  396. package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.mjs +82 -0
  397. package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.test.ts +73 -0
  398. package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.mjs +28 -0
  399. package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.test.ts +38 -0
  400. package/packages/shared-skills/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  401. package/packages/shared-skills/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  402. package/packages/shared-skills/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  403. package/packages/shared-skills/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  404. package/packages/shared-skills/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  405. package/packages/shared-skills/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  406. package/packages/shared-skills/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  407. package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +105 -0
  408. package/packages/shared-skills/skills/ulw-research/scripts/units.test.ts +83 -0
  409. package/packages/shared-skills/skills/visual-qa/SKILL.md +70 -63
  410. package/packages/shared-skills/skills/visual-qa/scripts/skill-doc-fields.test.ts +34 -0
  411. package/script/qa/web-terminal-visual-qa.mjs +6 -2
  412. package/script/qa/xterm-live-terminal.mjs +26 -3
@@ -0,0 +1,228 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
5
+
6
+ import { DRIFT_KINDS, ingestVerdicts } from "./meaning-check.mjs"
7
+
8
+ type Verdict = Record<string, unknown>
9
+ type Result = {
10
+ defects: { code: string; severity: string; integrity: boolean; pair_id: string; drift_kind: string; fragment: string; source_fragment: string }[]
11
+ dropped: { pair_id: string | null; reason: string; model_tier: string | null; file: string }[]
12
+ unverified: { pair_id: string; reason: string }[]
13
+ calibrationLines: string[]
14
+ }
15
+
16
+ const PAIRS = [
17
+ { pair_id: "P1", kind: "quote", sentence_key: "findings#Q1", fact_ids: ["Q1"], quote: "The pilot covered North America.", sentence: "The rollout was worldwide.", force_strong: true },
18
+ { pair_id: "P2", kind: "claim", claim_id: "C2", statement: "Revenue may grow in 2026.", sentence: "Revenue will grow in 2026.", force_strong: true },
19
+ ]
20
+
21
+ const base = (tier: string, extra: Verdict = {}): Verdict => ({
22
+ pair_id: "P1",
23
+ verdict: "drift",
24
+ drift_kind: "scope_widened",
25
+ fragment: "worldwide",
26
+ source_fragment: "North America",
27
+ explanation: "The assertion broadens the reported region.",
28
+ model_tier: tier,
29
+ ...extra,
30
+ })
31
+
32
+ let dir = ""
33
+ beforeEach(() => {
34
+ dir = mkdtempSync(join(tmpdir(), "meaning-check-"))
35
+ })
36
+ afterEach(() => {
37
+ rmSync(dir, { recursive: true, force: true })
38
+ })
39
+
40
+ function file(name: string, verdicts: Verdict[]): string {
41
+ const path = join(dir, name)
42
+ writeFileSync(path, JSON.stringify(verdicts))
43
+ return path
44
+ }
45
+
46
+ function run(cheap: Verdict[], strong: Verdict[] = [], options: object = {}): Result {
47
+ return ingestVerdicts(PAIRS, [file("cheap.json", cheap), file("strong.json", strong)], options) as Result
48
+ }
49
+
50
+ describe("ingestVerdicts: tier agreement", () => {
51
+ test("#given cheap and strong agree on scope_widened #when ingested #then meaning_drift_confirmed (major, non-integrity)", () => {
52
+ const result = run([base("cheap")], [base("strong")])
53
+ expect(result.defects).toHaveLength(1)
54
+ expect(result.defects[0]).toMatchObject({ code: "meaning_drift_confirmed", severity: "major", integrity: false, pair_id: "P1", drift_kind: "scope_widened" })
55
+ expect(result.dropped).toEqual([])
56
+ })
57
+
58
+ test("#given only the cheap tier flags #when ingested #then meaning_drift_warn (minor)", () => {
59
+ const result = run([base("cheap")])
60
+ expect(result.defects.map((d) => [d.code, d.severity])).toEqual([["meaning_drift_warn", "minor"]])
61
+ })
62
+
63
+ test("#given both tiers agree on other #when ingested #then warn only, never confirmed", () => {
64
+ const result = run([base("cheap", { drift_kind: "other" })], [base("strong", { drift_kind: "other" })])
65
+ expect(result.defects.map((d) => d.code)).toEqual(["meaning_drift_warn"])
66
+ })
67
+
68
+ test("#given the tiers name different drift kinds #when ingested #then warn only", () => {
69
+ const result = run([base("cheap")], [base("strong", { drift_kind: "subject_changed" })])
70
+ expect(result.defects.map((d) => d.code)).toEqual(["meaning_drift_warn"])
71
+ })
72
+
73
+ test("#given an ok verdict #when ingested #then no defect and no calibration line", () => {
74
+ const result = run([{ pair_id: "P1", verdict: "ok", model_tier: "cheap" }])
75
+ expect(result.defects).toEqual([])
76
+ expect(result.calibrationLines).toEqual([])
77
+ })
78
+
79
+ test("#given a claim pair #when both tiers agree on evidence_grade_raised #then confirmed against the statement", () => {
80
+ const verdict = { pair_id: "P2", verdict: "drift", drift_kind: "evidence_grade_raised", fragment: "will grow", source_fragment: "may grow", explanation: "certainty raised" }
81
+ const result = run([{ ...verdict, model_tier: "cheap" }], [{ ...verdict, model_tier: "strong" }])
82
+ expect(result.defects.map((d) => [d.code, d.pair_id])).toEqual([["meaning_drift_confirmed", "P2"]])
83
+ })
84
+ })
85
+
86
+ describe("ingestVerdicts: grounding drops", () => {
87
+ test("#given a fragment that is not in the sentence #when ingested #then dropped and counted, no defect", () => {
88
+ const result = run([base("cheap", { fragment: "Europe" })])
89
+ expect(result.defects).toEqual([])
90
+ expect(result.dropped).toHaveLength(1)
91
+ expect(result.dropped[0]).toMatchObject({ pair_id: "P1", reason: "fragment_not_found", model_tier: "cheap" })
92
+ })
93
+
94
+ test("#given a source fragment that is not in the quote #when ingested #then dropped", () => {
95
+ const result = run([base("cheap", { source_fragment: "Asia" })])
96
+ expect(result.defects).toEqual([])
97
+ expect(result.dropped.map((d) => d.reason)).toEqual(["source_fragment_not_found"])
98
+ })
99
+
100
+ test("#given empty or blank fragments #when ingested #then dropped as empty", () => {
101
+ const result = run([base("cheap", { fragment: "" })], [base("strong", { source_fragment: " " })])
102
+ expect(result.defects).toEqual([])
103
+ expect(result.dropped.map((d) => d.reason)).toEqual(["empty_fragment", "empty_fragment"])
104
+ })
105
+
106
+ test("#given an unknown drift_kind #when ingested #then dropped", () => {
107
+ const result = run([base("cheap", { drift_kind: "tone_shifted" })])
108
+ expect(result.defects).toEqual([])
109
+ expect(result.dropped.map((d) => d.reason)).toEqual(["unknown_drift_kind"])
110
+ })
111
+
112
+ test("#given an unknown pair id or tier #when ingested #then dropped", () => {
113
+ const result = run([base("cheap", { pair_id: "P99" }), base("medium")])
114
+ expect(result.dropped.map((d) => d.reason)).toEqual(["unknown_pair", "invalid_model_tier"])
115
+ })
116
+
117
+ test("#given extra whitespace in a fragment #when ingested #then it still grounds", () => {
118
+ const result = run([base("cheap", { fragment: "rollout \n was", source_fragment: " North America " })])
119
+ expect(result.dropped).toEqual([])
120
+ expect(result.defects.map((d) => d.code)).toEqual(["meaning_drift_warn"])
121
+ })
122
+
123
+ test("#given the closed drift_kind set #when read #then it holds exactly the nine kinds", () => {
124
+ expect([...DRIFT_KINDS]).toEqual([
125
+ "subject_changed", "scope_widened", "hedge_dropped", "certainty_added", "causality_added",
126
+ "direction_flipped", "population_changed", "evidence_grade_raised", "other",
127
+ ])
128
+ })
129
+ })
130
+
131
+ describe("ingestVerdicts: calibration log", () => {
132
+ test("#given a dispute on a confirmed pair #when ingested #then the defect stays and the line records disputed:true", () => {
133
+ const result = run([base("cheap")], [base("strong")], { dispute: { pair_id: "P1", reason: "Scope is justified by a second observation." } })
134
+ expect(result.defects.map((d) => d.code)).toEqual(["meaning_drift_confirmed"])
135
+ expect(result.calibrationLines.map((line) => JSON.parse(line))).toEqual([
136
+ { kind: "quote", pair_id: "P1", tiers: ["cheap", "strong"], drift_kind: "scope_widened", confirmed: true, disputed: true, reason: "Scope is justified by a second observation." },
137
+ ])
138
+ })
139
+
140
+ test("#given two flagged pairs and one ok verdict #when ingested with a calibration path #then exactly one line per flagged pair is appended", () => {
141
+ const calibrationPath = join(dir, "fidelity-calibration.jsonl")
142
+ const claimDrift = { pair_id: "P2", verdict: "drift", drift_kind: "certainty_added", fragment: "will", source_fragment: "may", explanation: "x", model_tier: "cheap" }
143
+ const cheap = [base("cheap"), claimDrift, { pair_id: "P1", verdict: "ok", model_tier: "cheap" }]
144
+ const first = run(cheap, [], { calibrationPath })
145
+ expect(first.calibrationLines).toHaveLength(2)
146
+ const lines = () => readFileSync(calibrationPath, "utf8").split("\n").filter(Boolean)
147
+ expect(lines()).toHaveLength(2)
148
+ expect(JSON.parse(lines()[1])).toEqual({ kind: "claim", pair_id: "P2", tiers: ["cheap"], drift_kind: "certainty_added", confirmed: false, disputed: false, reason: null })
149
+ run(cheap, [], { calibrationPath })
150
+ expect(lines()).toHaveLength(4)
151
+ })
152
+ })
153
+
154
+ describe("ingestVerdicts: coverage", () => {
155
+ test("#given a pair no reader judged #when ingested #then it is reported as unverified, never as clean", () => {
156
+ const result = run([base("cheap")], [base("strong")])
157
+ expect(result.unverified).toEqual([{ pair_id: "P2", reason: "no_verdict" }])
158
+ })
159
+
160
+ test("#given a force_strong pair with only a cheap verdict #when ingested #then unverified says no_strong_verdict", () => {
161
+ const result = run([base("cheap"), { pair_id: "P2", verdict: "ok", model_tier: "cheap" }])
162
+ expect(result.unverified).toEqual([
163
+ { pair_id: "P1", reason: "no_strong_verdict" },
164
+ { pair_id: "P2", reason: "no_strong_verdict" },
165
+ ])
166
+ })
167
+
168
+ test("#given every pair read by both tiers #when ingested #then unverified is empty", () => {
169
+ const ok = (pair_id: string, tier: string): Verdict => ({ pair_id, verdict: "ok", model_tier: tier })
170
+ const result = run([ok("P1", "cheap"), ok("P2", "cheap")], [ok("P1", "strong"), ok("P2", "strong")])
171
+ expect(result.unverified).toEqual([])
172
+ expect(result.defects).toEqual([])
173
+ })
174
+
175
+ test("#given only dropped verdicts on a pair #when ingested #then the pair counts as unread", () => {
176
+ const result = run([base("cheap", { fragment: "Europe" })], [base("strong", { fragment: "Europe" })])
177
+ expect(result.unverified.map((entry) => entry.pair_id)).toEqual(["P1", "P2"])
178
+ })
179
+ })
180
+
181
+ describe("ingestVerdicts: the reported fragment", () => {
182
+ test("#given an earlier other verdict #when the tiers agree on a later kind #then the fragment comes from the agreed verdict", () => {
183
+ const other = base("cheap", { drift_kind: "other", fragment: "The rollout", source_fragment: "The pilot" })
184
+ const result = run([other, base("cheap")], [base("strong")])
185
+ expect(result.defects).toHaveLength(1)
186
+ expect(result.defects[0]).toMatchObject({ code: "meaning_drift_confirmed", drift_kind: "scope_widened", fragment: "worldwide", source_fragment: "North America" })
187
+ })
188
+ })
189
+
190
+ describe("ingestVerdicts: determinism", () => {
191
+ test("#given the same verdicts twice #when ingested #then both results are byte-for-byte equal", () => {
192
+ const cheap = [base("cheap"), { pair_id: "P2", verdict: "drift", drift_kind: "certainty_added", fragment: "will grow", source_fragment: "may grow", model_tier: "cheap" }, base("medium")]
193
+ const strong = [base("strong")]
194
+ const first = JSON.stringify(run(cheap, strong))
195
+ const second = JSON.stringify(run(cheap, strong))
196
+ expect(second).toBe(first)
197
+ })
198
+ })
199
+
200
+ describe("ingestVerdicts: unreadable verdict files", () => {
201
+ test("#given a missing verdict file #when ingested #then a short typed error names the file", () => {
202
+ const missing = join(dir, "absent.json")
203
+ expect(() => ingestVerdicts(PAIRS, [missing])).toThrow(TypeError)
204
+ expect(() => ingestVerdicts(PAIRS, [missing])).toThrow(/absent\.json: cannot read the verdict file/u)
205
+ })
206
+
207
+ test("#given invalid JSON #when ingested #then a short typed error names the file", () => {
208
+ const path = join(dir, "broken.json")
209
+ writeFileSync(path, "{ not json")
210
+ expect(() => ingestVerdicts(PAIRS, [path])).toThrow(/broken\.json: the verdict file is not valid JSON/u)
211
+ })
212
+ })
213
+
214
+ describe("ingestVerdicts: malformed pairs input", () => {
215
+ test("#given a missing or malformed pairs input #when ingested #then it refuses instead of reporting a clean result", () => {
216
+ for (const bad of [undefined, null, {}, { pairs: "P1" }, "pairs.json", 7]) {
217
+ expect(() => ingestVerdicts(bad as never, []), JSON.stringify(bad)).toThrow(TypeError)
218
+ }
219
+ expect(ingestVerdicts({ pairs: [] }, []).unverified).toEqual([])
220
+ })
221
+ })
222
+
223
+ describe("ingestVerdicts: the no-model contract", () => {
224
+ test("#given the module source #when read #then it never fetches or imports a network or model client", () => {
225
+ const text = readFileSync(join(import.meta.dir, "meaning-check.mjs"), "utf8")
226
+ expect(text).not.toMatch(/\bfetch\s*\(|node:https?|node:net|from\s+"(?!\.\/|node:)/u)
227
+ })
228
+ })
@@ -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
+ })
@@ -1,12 +1,10 @@
1
- // outcome.mjs - the per-run outcome manifest (`outcome.json`) and the closing briefing.
2
- // Manifest functions are pure (they return a new manifest); the briefing reads only the
3
- // manifest, the sources-ledger text, and an injected `now`. node: builtins only.
1
+ // outcome.mjs - the per-run outcome manifest (`outcome.json`) and the closing briefing. Manifest functions are pure (they return a new manifest).
2
+ // The briefing reads only the manifest, the sources-ledger text, and an injected `now`. node: builtins only.
4
3
 
5
4
  import * as nodeFs from "node:fs"
6
5
  import { basename, dirname, join } from "node:path"
7
6
 
8
- import { DELIVERABLE_FORMATS, DELIVERABLE_STATUSES, GATE_NAMES, GATE_STATUSES, LANES } from "./contracts.mjs"
9
- import { MANIFEST_CODES, SCHEMA_VERSION, validateOutcomeManifest } from "./contracts.mjs"
7
+ import { DELIVERABLE_FORMATS, DELIVERABLE_STATUSES, GATE_NAMES, GATE_STATUSES, LANES, MANIFEST_CODES, normalizeManifest, SCHEMA_VERSION, validateOutcomeManifest } from "./contracts.mjs"
10
8
 
11
9
  export const MANIFEST_FILENAME = "outcome.json"
12
10
  const MISSING = MANIFEST_CODES[0]
@@ -87,7 +85,7 @@ export function setDeliverable(manifest, format, status, detail = {}) {
87
85
  const next = clone(manifest)
88
86
  const index = next.deliverables.findIndex((row) => row.format === format)
89
87
  const row = { format, status, promised: index >= 0 ? next.deliverables[index].promised : false, updatedAt: stamp(detail.now) }
90
- for (const key of ["path", "reason", "pages", "bytes"]) {
88
+ for (const key of ["path", "reason", "pages", "bytes", "verifiedBy", "renderedFrom"]) {
91
89
  if (detail[key] !== undefined) row[key] = detail[key]
92
90
  }
93
91
  if (index >= 0) next.deliverables[index] = row
@@ -95,11 +93,13 @@ export function setDeliverable(manifest, format, status, detail = {}) {
95
93
  return next
96
94
  }
97
95
 
98
- export function setGate(manifest, gate, status) {
96
+ export function setGate(manifest, gate, status, detail = {}) {
99
97
  assertOneOf("unknown_gate", "gate", gate, GATE_NAMES)
100
98
  assertOneOf("unknown_gate_status", "gate status", status, GATE_STATUSES)
99
+ if (gate === "fidelity" && status === "not_run" && (typeof detail.reason !== "string" || !detail.reason.trim())) throw new OutcomeError("gate_reason_required", "fidelity not_run requires a non-empty --reason")
101
100
  const next = clone(manifest)
102
101
  next.gates[gate] = status
102
+ next.gateHistory = [...(next.gateHistory ?? []), { gate, status, ...(detail.reason !== undefined ? { reason: detail.reason.trim() } : {}), at: stamp(detail.now) }]
103
103
  return next
104
104
  }
105
105
 
@@ -226,7 +226,7 @@ function assertValid(manifest, where) {
226
226
  export function readManifest(file) {
227
227
  let manifest
228
228
  try {
229
- manifest = JSON.parse(nodeFs.readFileSync(file, "utf8"))
229
+ manifest = normalizeManifest(JSON.parse(nodeFs.readFileSync(file, "utf8")))
230
230
  } catch (error) {
231
231
  if (error instanceof SyntaxError) throw new OutcomeError("invalid_manifest", `${file}: not valid JSON (${error.message})`)
232
232
  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", () => {