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,73 @@
1
+ import { describe, expect, test } from "bun:test"
2
+
3
+ import { findNormalized, normalizeText, tokenize } from "./text-normalize.mjs"
4
+
5
+ describe("normalizeText", () => {
6
+ test("#given NFD and NFC Korean #when normalized #then both are equal NFC strings", () => {
7
+ // given
8
+ const nfc = "매출은 한국어로 적었다"
9
+ const nfd = nfc.normalize("NFD")
10
+ // when
11
+ const a = normalizeText(nfd)
12
+ const b = normalizeText(nfc)
13
+ // then
14
+ expect(nfd).not.toBe(nfc)
15
+ expect(a).toBe(b)
16
+ expect(a).toBe(nfc)
17
+ })
18
+
19
+ test("#given curly and CJK quote glyphs #when normalized #then they become straight quotes", () => {
20
+ // given
21
+ const raw = "\u201Cgrowth\u201D \u2018ok\u2019 \u300Cquoted\u300D \u300Etitle\u300F \u00ABfr\u00BB"
22
+ // when
23
+ const out = normalizeText(raw)
24
+ // then
25
+ expect(out).toBe("\"growth\" 'ok' \"quoted\" \"title\" \"fr\"")
26
+ })
27
+
28
+ test("#given runs of mixed whitespace #when normalized #then they collapse to single spaces and are trimmed", () => {
29
+ // when
30
+ const out = normalizeText(" a \t\n b\u00A0\u00A0c ")
31
+ // then
32
+ expect(out).toBe("a b c")
33
+ })
34
+ })
35
+
36
+ describe("tokenize", () => {
37
+ test("#given Latin words and Hangul eojeol with punctuation #when tokenized #then tokens split on whitespace and punctuation", () => {
38
+ // when
39
+ const tokens = tokenize("Revenue rose, 매출은 100억 원이다. (\u201Cok\u201D)")
40
+ // then
41
+ expect(tokens).toEqual(["Revenue", "rose", "매출은", "100억", "원이다", "ok"])
42
+ })
43
+ })
44
+
45
+ describe("findNormalized", () => {
46
+ test("#given raw text with double spaces and curly quotes #when a straight-quoted needle is found #then RAW offsets slice back to the raw substring", () => {
47
+ // given
48
+ const hay = "He said \u201Cgrowth is\n strong\u201D today."
49
+ // when
50
+ const hit = findNormalized(hay, "\"growth is strong\"")
51
+ // then
52
+ if (hit === null) throw new Error("needle not found")
53
+ expect(hay.slice(hit.start, hit.end)).toBe("\u201Cgrowth is\n strong\u201D")
54
+ })
55
+
56
+ test("#given NFD Korean raw text with double spaces #when an NFC needle is found #then RAW offsets slice the NFD substring", () => {
57
+ // given
58
+ const rawNfc = "앞 문장. 매출은 100억 원이다. 끝"
59
+ const hay = rawNfc.normalize("NFD")
60
+ const expected = "매출은 100억 원이다".normalize("NFD")
61
+ // when
62
+ const hit = findNormalized(hay, "매출은 100억 원이다")
63
+ // then
64
+ if (hit === null) throw new Error("needle not found")
65
+ expect(hay.slice(hit.start, hit.end)).toBe(expected)
66
+ })
67
+
68
+ test("#given a needle absent from the text #when searched #then null is returned", () => {
69
+ // when / then
70
+ expect(findNormalized("abc", "zzz")).toBeNull()
71
+ expect(findNormalized("abc", " ")).toBeNull()
72
+ })
73
+ })
@@ -0,0 +1,28 @@
1
+ // text-similarity.mjs - similarity in [0, 1] from normalized Levenshtein distance.
2
+ //
3
+ // Both inputs go through normalizeText first (NFC, quote glyphs, whitespace), then the
4
+ // edit distance is computed over code points and divided by the longer length.
5
+ // Plain Node ESM, no dependencies.
6
+
7
+ import { normalizeText } from "./text-normalize.mjs"
8
+
9
+ function levenshtein(a, b) {
10
+ let previous = Array.from({ length: b.length + 1 }, (_, k) => k)
11
+ for (let i = 1; i <= a.length; i++) {
12
+ const current = [i]
13
+ for (let k = 1; k <= b.length; k++) {
14
+ const cost = a[i - 1] === b[k - 1] ? 0 : 1
15
+ current.push(Math.min(previous[k] + 1, current[k - 1] + 1, previous[k - 1] + cost))
16
+ }
17
+ previous = current
18
+ }
19
+ return previous[b.length]
20
+ }
21
+
22
+ export function similarity(a, b) {
23
+ const left = Array.from(normalizeText(a))
24
+ const right = Array.from(normalizeText(b))
25
+ const longest = Math.max(left.length, right.length)
26
+ if (longest === 0) return 1
27
+ return 1 - levenshtein(left, right) / longest
28
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from "bun:test"
2
+
3
+ import { similarity } from "./text-similarity.mjs"
4
+
5
+ describe("similarity", () => {
6
+ test("#given identical strings #when compared #then similarity is exactly 1", () => {
7
+ // when / then
8
+ expect(similarity("a", "a")).toBe(1)
9
+ expect(similarity("", "")).toBe(1)
10
+ })
11
+
12
+ test("#given an OCR-noisy copy of a sentence #when compared #then similarity is at least 0.95", () => {
13
+ // given
14
+ const clean = "The company reported revenue of 100 billion won in the third quarter of 2025."
15
+ const noisy = "The cornpany reported revenue of 100 bi1lion won in the third quarter of 2025. "
16
+ // when
17
+ const score = similarity(clean, noisy)
18
+ // then
19
+ expect(score).toBeGreaterThanOrEqual(0.95)
20
+ expect(score).toBeLessThan(1)
21
+ })
22
+
23
+ test("#given NFD vs NFC and curly vs straight quotes #when compared #then normalization makes them equal", () => {
24
+ // when / then
25
+ expect(similarity("\u201C매출은 늘었다\u201D".normalize("NFD"), "\"매출은 늘었다\"")).toBe(1)
26
+ })
27
+
28
+ test("#given a different sentence #when compared #then similarity is below 0.8", () => {
29
+ // given
30
+ const a = "The company reported revenue of 100 billion won in the third quarter of 2025."
31
+ const b = "Analysts expect margins to shrink next year as input costs keep rising."
32
+ // when
33
+ const score = similarity(a, b)
34
+ // then
35
+ expect(score).toBeLessThan(0.8)
36
+ expect(score).toBeGreaterThanOrEqual(0)
37
+ })
38
+ })
@@ -0,0 +1,101 @@
1
+ // Canonical factor algebra. Exponents are bigint; aliases enter only through normalizeInput.
2
+ import { fromDecimal, mul, sigFigsOf, toDecimal } from "./rational.mjs"
3
+ import { dimensionOf, normalizeUnit } from "./units.mjs"
4
+ import { refuse } from "./unit-errors.mjs"
5
+ export { refuse, RuleError } from "./unit-errors.mjs"
6
+
7
+ const SCALED = new Set(["bp", "basispoint", "basispoints"])
8
+ export const isPercentish = (unit) => unit === "%" || unit === "%p"
9
+ const scaled = (text) => SCALED.has(text.replace(/\s+/g, "").toLowerCase())
10
+
11
+ function atomOf(text) {
12
+ if (text === "1") return null
13
+ const atom = scaled(text) ? "%p" : normalizeUnit(text) ?? text
14
+ if (dimensionOf(atom) === "scale") refuse(`"${text}" is a scale word (${atom}), not a unit`, undefined, "scale_atom", "a counted unit with scale in the numeral")
15
+ return atom
16
+ }
17
+
18
+ const combine = (a, b, sign) => {
19
+ for (const [atom, power] of b) a.set(atom, (a.get(atom) ?? 0n) + sign * power)
20
+ return a
21
+ }
22
+
23
+ /** Iterative unit parser: factor maps plus the input's decimal scaling exponent. */
24
+ function parse(raw) {
25
+ if (raw === null) return { factors: new Map(), shift: 0n }
26
+ if (typeof raw !== "string" || !raw.trim()) refuse("unit must be text or explicit null", undefined, "unit_text", "canonical unit text or null")
27
+ const tokens = raw.match(/[*/()^]|[^*/()^]+/g).map((x) => x.trim()).filter(Boolean)
28
+ const frames = [{ factors: new Map(), shift: 0n, sign: 1n, need: true }]
29
+ let affine = false
30
+ const apply = (value, i) => {
31
+ if (tokens[i] === "^") {
32
+ const power = tokens[i + 1]
33
+ if (!/^[+-]?\d+$/.test(power ?? "")) refuse(`unit "${raw}" has a bad exponent`, undefined, "exponent", "an integer exponent")
34
+ const n = BigInt(power)
35
+ value = { factors: new Map([...value.factors].map(([a, p]) => [a, p * n])), shift: value.shift * n }
36
+ i += 2
37
+ }
38
+ const frame = frames.at(-1)
39
+ combine(frame.factors, value.factors, frame.sign)
40
+ frame.shift += frame.sign * value.shift
41
+ frame.need = false
42
+ return i
43
+ }
44
+ for (let i = 0; i < tokens.length;) {
45
+ const frame = frames.at(-1)
46
+ const token = tokens[i]
47
+ if (frame.need) {
48
+ if (token === "(") {
49
+ frames.push({ factors: new Map(), shift: 0n, sign: 1n, need: true })
50
+ i++
51
+ } else {
52
+ if (/^[*/()^]$/.test(token)) refuse(`unexpected "${token}" in unit "${raw}"`)
53
+ const atom = atomOf(token)
54
+ affine ||= dimensionOf(atom) === "temperature"
55
+ i = apply({ factors: new Map(atom === null ? [] : [[atom, 1n]]), shift: scaled(token) ? -2n : 0n }, i + 1)
56
+ }
57
+ } else if (token === ")" && frames.length > 1) {
58
+ const value = frames.pop()
59
+ i = apply(value, i + 1)
60
+ } else if (token === "*" || token === "/") {
61
+ frame.sign = token === "*" ? 1n : -1n
62
+ frame.need = true
63
+ i++
64
+ } else refuse(`unexpected "${token}" in unit "${raw}"`)
65
+ }
66
+ if (frames.length !== 1 || frames[0].need) refuse(`unit "${raw}" ends early or is missing a closing parenthesis`)
67
+ // Refuse affine composites before cancellation loses the atom that made them unsafe.
68
+ if (affine && (tokens.length !== 1 || frames[0].shift !== 0n)) refuse("affine (temperature) units cannot occur in composites", undefined, "affine_units", "a non-affine composite or bare temperature for convert")
69
+ return frames[0]
70
+ }
71
+
72
+ function format(factors) {
73
+ const side = (sign) => [...factors].filter(([, p]) => sign * p > 0n).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
74
+ .map(([a, p]) => { const n = sign * p; return n === 1n ? a : `${a}^${n}` })
75
+ const top = side(1n)
76
+ const bottom = side(-1n)
77
+ if (!top.length && !bottom.length) return null
78
+ return `${top.join("*") || "1"}${bottom.length ? `/${bottom.length === 1 ? bottom[0] : `(${bottom.join("*")})`}` : ""}`
79
+ }
80
+
81
+ export const canonicalUnit = (raw) => format(parse(raw).factors)
82
+ export function inputScale(raw) {
83
+ const { shift } = parse(raw)
84
+ if (shift === 0n) return null
85
+ if (shift > 10000n || shift < -10000n) refuse("scaled alias exponent exceeds exact input capacity", undefined, "scale_range", "an absolute decimal scale exponent <= 10000")
86
+ return shift > 0n ? `1${"0".repeat(Number(shift))}` : `0.${"0".repeat(Number(-shift - 1n))}1`
87
+ }
88
+
89
+ export function normalizeInput(unit, value) {
90
+ const canonical = canonicalUnit(unit)
91
+ const factor = inputScale(unit)
92
+ return { unit: canonical, value: factor ? toDecimal(mul(fromDecimal(value), fromDecimal(factor)), sigFigsOf(value)) : value }
93
+ }
94
+
95
+ export const unitTimes = (a, b) => format(combine(parse(a).factors, parse(b).factors, 1n))
96
+ export const unitOver = (a, b) => format(combine(parse(a).factors, parse(b).factors, -1n))
97
+ export const isAffine = (unit) => typeof unit === "string" && dimensionOf(unit) === "temperature"
98
+ export function isCurrencyRate(unit) {
99
+ const currencies = [...parse(unit).factors].filter(([a, n]) => n !== 0n && dimensionOf(a)?.startsWith("currency:"))
100
+ return currencies.some(([, n]) => n > 0n) && currencies.some(([, n]) => n < 0n)
101
+ }
@@ -0,0 +1,23 @@
1
+ // Classified refusals shared by public derivation boundaries and the registry.
2
+ export class RuleError extends Error {
3
+ constructor(detail, hint, shape = "invalid_input", expected = "a valid ledger and derivation specification") {
4
+ super(detail)
5
+ this.detail = detail
6
+ this.shape = shape
7
+ this.expected = expected
8
+ if (hint) this.hint = hint
9
+ }
10
+ }
11
+
12
+ export const refuse = (detail, hint, shape, expected) => {
13
+ throw new RuleError(detail, hint, shape, expected)
14
+ }
15
+
16
+ export function refusal(error) {
17
+ return {
18
+ shape: error instanceof RuleError ? error.shape : "invalid_input",
19
+ expected: error instanceof RuleError ? error.expected : "valid JSON fields matching the fact ledger grammar",
20
+ detail: error instanceof Error ? error.message : "invalid input",
21
+ ...(error instanceof RuleError && error.hint ? { hint: error.hint } : {}),
22
+ }
23
+ }
@@ -0,0 +1,31 @@
1
+ // Metadata contracts and order-independent JSON equality, shared by schema and algebra.
2
+ import { isDeepStrictEqual } from "node:util"
3
+ import { refuse } from "./unit-errors.mjs"
4
+
5
+ export const METADATA = ["population", "accounting_basis", "snapshot_window", "entity", "valid_at"]
6
+ export const sameDeclared = isDeepStrictEqual
7
+ export const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0
8
+ export const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
9
+
10
+ export function metadataErrors(row) {
11
+ return METADATA.filter((field) => {
12
+ const value = row[field]
13
+ return value !== null && !(nonEmpty(value) || (["population", "snapshot_window"].includes(field) && object(value)))
14
+ }).map((field) => `${field} must be explicitly null or valid non-empty metadata`)
15
+ }
16
+
17
+ export function checkMetadata(rows, fields) {
18
+ for (const row of rows) {
19
+ const errors = metadataErrors(row)
20
+ if (errors.length) refuse(`${row.fact_id}: ${errors.join("; ")}`, undefined, "metadata", "present, type-valid metadata; explicit null when unknown")
21
+ }
22
+ for (const field of fields) {
23
+ if (rows.some((row) => !sameDeclared(row[field], rows[0][field]))) {
24
+ refuse(`${field} differs across ${rows.map((row) => row.fact_id).join(", ")}`, undefined, "metadata", `equal ${field}`)
25
+ }
26
+ }
27
+ }
28
+
29
+ export function propagatedMetadata(rows) {
30
+ return Object.fromEntries(METADATA.map((field) => [field, rows.every((row) => sameDeclared(row[field], rows[0][field])) ? structuredClone(rows[0][field]) : null]))
31
+ }
@@ -0,0 +1,72 @@
1
+ // unit-pairs.mjs - the typed operand-unit rules shared by the operation registry and expr nodes (references/unit-rules.md).
2
+ // equalUnits and notAffine guard whole operations; minusUnit and plusUnit are the typed-pair rules that diff and sum
3
+ // use and that expr's `-` and `+` nodes follow; inferExpr assigns a canonical unit to every node of an expr tree.
4
+
5
+ import { isAffine, isPercentish, refuse, unitOver, unitTimes } from "./unit-algebra.mjs"
6
+ import { postorder } from "./fact-expr.mjs"
7
+ import { checkMetadata, propagatedMetadata } from "./unit-metadata.mjs"
8
+
9
+ export const show = (unit) => JSON.stringify(unit ?? null)
10
+ export const listed = (rows, units) => rows.map((row, index) => `${row.fact_id} ${units[index] ?? "null"}`).join(", ")
11
+
12
+ export function equalUnits(rows, units) {
13
+ if (units.some((unit) => unit !== units[0])) refuse(`unit differs across ${listed(rows, units)}`)
14
+ return units[0]
15
+ }
16
+
17
+ export function notAffine(op, rows, units) {
18
+ if (units.some(isAffine)) refuse(`${op} refuses affine (temperature) units: ${listed(rows, units)}`)
19
+ }
20
+
21
+ /** The typed-pair rule shared by diff and expr `-`: U-U is U; %-% is %p; %-%p is %; %p-%p is %p; %p-% is refused. */
22
+ export function minusUnit(a, b, where) {
23
+ if (isAffine(a) || isAffine(b)) refuse(`${where}: subtracting affine (temperature) values as absolute values has no meaning`)
24
+ if (a === b) return a === "%" ? "%p" : a
25
+ if (a === "%" && b === "%p") return "%"
26
+ refuse(`${where}: ${show(a)} - ${show(b)} is not a typed pair (U-U, %-%, %-%p, %p-%p)`)
27
+ }
28
+
29
+ export function plusUnit(a, b, where) {
30
+ if (isAffine(a) || isAffine(b)) refuse(`${where}: adding affine (temperature) values has no meaning`)
31
+ if (a === b && a === "%") refuse(`${where}: adding % values needs disjoint shares of one reference: use --op sum`)
32
+ if (a === b) return a
33
+ if ((a === "%" && b === "%p") || (a === "%p" && b === "%")) return "%"
34
+ refuse(`${where}: ${show(a)} + ${show(b)} has no common unit`)
35
+ }
36
+
37
+ /** Unit of every node of an expr tree; refuses when a node's unit cannot be inferred. */
38
+ export function inferExpr(ast, unitsById, rowsById = null) {
39
+ const units = new Map()
40
+ const metadata = new Map()
41
+ for (const node of postorder(ast)) {
42
+ let unit
43
+ if (node.id) unit = unitsById.get(node.id)
44
+ else if (node.literal) unit = null
45
+ else if (node.fn === "pct") {
46
+ unit = units.get(node.arg)
47
+ if (unit !== null) refuse(`pct() takes a dimensionless value, got ${show(unit)}`)
48
+ unit = "%"
49
+ } else {
50
+ const left = units.get(node.left)
51
+ const right = units.get(node.right)
52
+ if (node.op === "+") unit = plusUnit(left, right, "expr")
53
+ else if (node.op === "-") unit = minusUnit(left, right, "expr")
54
+ else {
55
+ if (isAffine(left) || isAffine(right)) refuse(`expr: ${node.op} on affine (temperature) units has no meaning`)
56
+ const fraction = (value) => (isPercentish(value) ? null : value)
57
+ unit = (node.op === "*" ? unitTimes : unitOver)(fraction(left), fraction(right))
58
+ }
59
+ }
60
+ units.set(node, unit)
61
+ if (rowsById) {
62
+ const children = node.id ? [rowsById.get(node.id)] : node.literal ? [] : node.fn ? [metadata.get(node.arg)] : [metadata.get(node.left), metadata.get(node.right)]
63
+ const rows = children.filter(Boolean)
64
+ if (rows.length) {
65
+ const fields = node.op === "+" || node.op === "-" ? ["population", "snapshot_window", "accounting_basis"] : node.op === "/" ? ["population", "snapshot_window"] : []
66
+ checkMetadata(rows, fields)
67
+ metadata.set(node, { fact_id: "expression", ...propagatedMetadata(rows) })
68
+ }
69
+ }
70
+ }
71
+ return { unit: units.get(ast), units, metadata: metadata.get(ast) }
72
+ }
@@ -0,0 +1,44 @@
1
+ // Precision is stored independently from display strings; generated fields are rederived on replay.
2
+ import { sigFigsOf, toDecimal } from "./rational.mjs"
3
+ import { refuse } from "./unit-errors.mjs"
4
+
5
+ export const toBig = (r) => ({ num: BigInt(r.num), den: BigInt(r.den) })
6
+ export const toText = (r) => ({ num: String(r.num), den: String(r.den) })
7
+ export const placesOf = (decimal) => (String(decimal).split(".")[1] ?? "").length
8
+ export function terminates(r) {
9
+ let den = r.den
10
+ for (const prime of [2n, 5n]) while (den % prime === 0n) den /= prime
11
+ return den === 1n
12
+ }
13
+ export function precisionOf(row) {
14
+ if (row.precision !== null && row.precision !== undefined) {
15
+ const p = row.precision
16
+ if (!p || typeof p !== "object" || Object.keys(p).sort().join(",") !== "places,sigfigs" || !Number.isSafeInteger(p.places) || p.places < 0 || !Number.isSafeInteger(p.sigfigs) || p.sigfigs < 1) refuse("invalid stored precision", undefined, "precision_field", "{ places: nonnegative integer, sigfigs: positive integer }")
17
+ return p
18
+ }
19
+ return { places: placesOf(row.value), sigfigs: sigFigsOf(row.value) }
20
+ }
21
+ export function resultPrecision(rows, literals, exact, mode) {
22
+ const inputs = [...rows.map(precisionOf), ...literals.map((value) => precisionOf({ value }))]
23
+ const places = Math.min(...inputs.map((p) => p.places))
24
+ const sigfigs = Math.min(...inputs.map((p) => p.sigfigs))
25
+ if (mode === "places") return { places, sigfigs: sigFigsOf(roundPlaces(exact, places)) }
26
+ const rendered = toDecimal(exact, sigfigs)
27
+ return { places: placesOf(rendered), sigfigs }
28
+ }
29
+ export function roundPlaces(r, places) {
30
+ const digits = ((r.num < 0n ? -r.num : r.num) * 10n ** BigInt(places) * 2n + r.den) / (r.den * 2n)
31
+ const text = digits.toString().padStart(places + 1, "0")
32
+ return (r.num < 0n && digits !== 0n ? "-" : "") + (places ? `${text.slice(0, -places)}.${text.slice(-places)}` : text)
33
+ }
34
+ export function renderValue(exact, precision, mode) {
35
+ const plain = terminates(exact) ? toDecimal(exact) : null
36
+ const n = precision[mode]
37
+ if (plain !== null && (mode === "places" ? placesOf(plain) <= n : sigFigsOf(plain) < n)) return plain
38
+ return mode === "places" ? roundPlaces(exact, n) : toDecimal(exact, n)
39
+ }
40
+ export function display(value, unit) {
41
+ const [whole, fraction] = value.split(".")
42
+ const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (fraction === undefined ? "" : `.${fraction}`)
43
+ return unit === null ? grouped : unit === "%" ? `${grouped}%` : `${grouped} ${unit}`
44
+ }
@@ -0,0 +1,64 @@
1
+ // Structural rules used by the operation table, shared by schema and recomputation.
2
+ import { leaves, parseExpr } from "./fact-expr.mjs"
3
+ import { isRational } from "./fact-ledger-kinds.mjs"
4
+ import { canonicalUnit, RuleError } from "./unit-algebra.mjs"
5
+ import { nonEmpty, object } from "./unit-metadata.mjs"
6
+ import { show } from "./unit-pairs.mjs"
7
+
8
+ export const COMMON = ["operands null", "operands empty", "wrong arity", "missing operand id", "invalid rational", "no operation", "repeated operand", "self reference",
9
+ "kind operation", "audit fields", "precision field", "compatibility field", "metadata fields", "noncanonical operand"]
10
+ export const shapes = (...own) => Object.freeze([...COMMON, ...own])
11
+ export const needsContext = (row) => object(row.context) ? null : `${row.operation} rows carry a context object, got ${show(row.context)}`
12
+ export const noContext = (row) => row.context === null ? null : `${row.operation} rows carry no context, got ${show(row.context)}`
13
+ export const repeatedOperand = (ids) => new Set(ids).size !== ids.length ? `operand ${ids.find((id, i) => ids.indexOf(id) !== i)} is repeated: the operands of one row are distinct facts` : null
14
+
15
+ export function sumShape(row) {
16
+ const missing = needsContext(row)
17
+ if (missing) return missing
18
+ const c = row.context
19
+ if (Object.keys(c).sort().join(",") !== "disjoint,reference" || typeof c.disjoint !== "boolean" || !(c.reference === null || nonEmpty(c.reference) || object(c.reference))) return "sum context needs exactly boolean disjoint and null, text, or map reference"
20
+ if (object(c.reference) && (Object.keys(c.reference).sort().join(",") !== [...row.operands].sort().join(",") || !Object.values(c.reference).every(nonEmpty))) return "reference map must name exactly the supplied operands"
21
+ return null
22
+ }
23
+
24
+ export function convertShape(row) {
25
+ const target = object(row.context) ? row.context.target : undefined
26
+ if (!nonEmpty(target)) return needsContext(row) ?? "a convert row carries its target in context.target"
27
+ if (Object.keys(row.context).join(",") !== "target") return "convert context has exactly target"
28
+ try {
29
+ const canonical = canonicalUnit(target)
30
+ return canonical === target ? null : `context.target ${show(target)} is not canonical unit text (canonical: ${show(canonical)})`
31
+ } catch (error) {
32
+ if (error instanceof RuleError) return `context.target ${show(target)} is not unit text: ${error.detail}`
33
+ throw error
34
+ }
35
+ }
36
+
37
+ export function exprShape(row) {
38
+ try {
39
+ const ids = leaves(parseExpr(row.formula)).ids
40
+ if (ids.length !== row.operands.length || ids.some((id) => !row.operands.includes(id))) return `formula ids ${ids.join(", ")} do not match operands ${row.operands.join(", ")}`
41
+ return null
42
+ } catch (error) {
43
+ if (error instanceof RuleError) return error.detail
44
+ throw error
45
+ }
46
+ }
47
+
48
+ export function rowShape(row, rules) {
49
+ if (!object(row)) return "the row is not an object"
50
+ if (!Object.hasOwn(rules, row.operation)) return `operation ${show(row.operation)} is not one of ${Object.keys(rules).join(", ")}`
51
+ const rule = rules[row.operation]
52
+ const { operands, exact } = row
53
+ if (!Array.isArray(operands) || !operands.length || !operands.every(nonEmpty)) return `operands must be a non-empty array of fact ids, got ${show(operands)}`
54
+ const repeated = repeatedOperand(operands)
55
+ if (repeated) return repeated
56
+ if (operands.includes(row.fact_id)) return `${row.fact_id} cites itself as an operand`
57
+ if (!rule.arity(operands.length)) return `${row.operation} takes ${rule.arityText}, got ${operands.length}`
58
+ if (!isRational(exact)) return `exact must be { num, den } reduced integer strings, got ${show(exact)}`
59
+ if (row.kind !== rule.kind) return `kind ${row.kind} must be ${rule.kind} for ${row.operation}`
60
+ const own = rule.shape(row)
61
+ if (own) return own
62
+ if (!nonEmpty(row.formula) || !Array.isArray(row.derived_from) || row.derived_from.length !== operands.length) return "formula and derived_from are required audit fields"
63
+ return null
64
+ }
@@ -0,0 +1,173 @@
1
+ // The sole per-operation registry: guards, context, arithmetic, precision, audit and compatibility.
2
+ import { evaluate, leaves } from "./fact-expr.mjs"
3
+ import { add, div, mul, sub, toDecimal } from "./rational.mjs"
4
+ import { canonicalUnit, inputScale, isAffine, isCurrencyRate, isPercentish, refuse, RuleError, unitOver } from "./unit-algebra.mjs"
5
+ import { checkMetadata, nonEmpty, object, propagatedMetadata } from "./unit-metadata.mjs"
6
+ import { equalUnits, inferExpr, listed, minusUnit, notAffine, show } from "./unit-pairs.mjs"
7
+ import { display, terminates, toBig } from "./unit-precision.mjs"
8
+ import { convertShape, exprShape, noContext, repeatedOperand, rowShape, shapes, sumShape } from "./unit-rules-shapes.mjs"
9
+ import { convertFactor, dimensionOf } from "./units.mjs"
10
+ export { canonicalUnit, inferExpr, isAffine, RuleError }
11
+
12
+ const EXPR_HINT = "currencies never convert by table: add a dated rate row and use --op expr"
13
+ const compare = (a, b) => a.num * b.den - b.num * a.den
14
+ const SNAPSHOT = ["population", "snapshot_window"]
15
+ const ADDITIVE = [...SNAPSHOT, "accounting_basis"]
16
+ const pair = (a, b) => (spec) => [spec[a], spec[b]]
17
+ const listOf = (spec) => spec.operands
18
+ const generated = (exact, formula, factor = null) => ({ exact, formula, factor })
19
+ const idsOf = (rows) => rows.map((r) => r.fact_id)
20
+ const valuesOf = (rows) => rows.map((r) => toBig(r.exact))
21
+ const ordinary = {
22
+ kind: "derived", compatibility: "checked", context: () => null, shape: noContext,
23
+ precision: () => "sigfigs", render: (shown) => shown, metadataResult: propagatedMetadata,
24
+ }
25
+ const binary = { ...ordinary, arity: (n) => n === 2, arityText: "two operands" }
26
+
27
+ function sharedReference(rows, reference) {
28
+ if (nonEmpty(reference)) return
29
+ if (object(reference)) {
30
+ const named = rows.map((r) => reference[r.fact_id])
31
+ if (Object.keys(reference).length === rows.length && named.every(nonEmpty) && named.every((v) => v === named[0])) return
32
+ refuse("% operands name different references or missing/extra ids", undefined, "reference", "one denominator for exactly the operands")
33
+ }
34
+ refuse("summing % operands needs one shared reference (denominator): pass reference")
35
+ }
36
+
37
+ /** Registry rows own every operation-specific choice; consumers only dispatch through these rows. */
38
+ export const OPERATION_RULES = Object.freeze({
39
+ sum: Object.freeze({
40
+ ...ordinary, arity: (n) => n >= 2, arityText: "two or more operands", operandIds: listOf,
41
+ inputFields: ["operands", "disjoint", "reference"],
42
+ context: (s) => ({ disjoint: Object.hasOwn(s, "disjoint") ? s.disjoint : false, reference: Object.hasOwn(s, "reference") ? s.reference : null }),
43
+ metadata: ADDITIVE, precision: () => "places", shape: sumShape,
44
+ resultUnit(rows, units, context) {
45
+ notAffine("sum", rows, units)
46
+ const unit = equalUnits(rows, units)
47
+ if (unit === "%") {
48
+ if (context.disjoint !== true) refuse("summing % operands needs disjoint shares: pass disjoint")
49
+ sharedReference(rows, context.reference)
50
+ if (!nonEmpty(context.rationale)) refuse("summing % operands needs a rationale for the disjoint shares")
51
+ const values = valuesOf(rows)
52
+ if (values.some((r) => r.num < 0n || r.num > 100n * r.den) || compare(values.reduce(add), { num: 100n, den: 1n }) > 0n) refuse("disjoint shares must each be in [0, 100] and total <= 100", undefined, "share_total", "valid disjoint shares totaling at most 100%")
53
+ }
54
+ return unit
55
+ },
56
+ calculate: (rows) => generated(valuesOf(rows).reduce(add), idsOf(rows).join(" + ")),
57
+ malformed: shapes("no context", "% without disjoint", "% without reference", "% without rationale", "unequal units", "affine units", "closed context", "share total"),
58
+ }),
59
+ diff: Object.freeze({
60
+ ...binary, operandIds: listOf, metadata: ADDITIVE, precision: () => "places",
61
+ inputFields: ["operands"],
62
+ resultUnit: (rows, units) => minusUnit(units[0], units[1], `diff ${listed(rows, units)}`),
63
+ calculate: (rows) => generated(sub(...valuesOf(rows)), idsOf(rows).join(" - ")),
64
+ malformed: shapes("%p - % pair", "unit - other unit pair", "affine units", "stray context"),
65
+ }),
66
+ growth: Object.freeze({
67
+ ...binary, arityText: "two operands (from, to)", operandIds: pair("from", "to"), metadata: ["population", "accounting_basis"],
68
+ inputFields: ["from", "to"],
69
+ resultUnit(rows, units) {
70
+ const [from, to] = rows
71
+ notAffine("growth", rows, units)
72
+ equalUnits(rows, units)
73
+ if (!nonEmpty(from.entity) || from.entity !== to.entity) refuse(`growth needs one declared entity: ${from.entity} vs ${to.entity}`)
74
+ if (!nonEmpty(from.valid_at) || !nonEmpty(to.valid_at) || from.valid_at === to.valid_at) refuse(`growth needs two different valid_at periods: ${from.valid_at} vs ${to.valid_at}`)
75
+ if (toBig(from.exact).num <= 0n) refuse(`growth needs a positive starting value in ${from.fact_id}`)
76
+ return "%"
77
+ },
78
+ calculate(rows) { const [a, b] = valuesOf(rows); const [x, y] = idsOf(rows); return generated(mul(div(sub(b, a), a), 100n), `(${y} - ${x}) / ${x} * 100`) },
79
+ malformed: shapes("affine units", "unequal units", "entity mismatch", "one period", "from <= 0", "stray context"),
80
+ }),
81
+ share: Object.freeze({
82
+ ...binary, arityText: "two operands (part, whole)", operandIds: pair("part", "whole"), metadata: SNAPSHOT,
83
+ inputFields: ["part", "whole"],
84
+ resultUnit(rows, units) {
85
+ notAffine("share", rows, units)
86
+ equalUnits(rows, units)
87
+ const [part, whole] = valuesOf(rows)
88
+ if (whole.num <= 0n) refuse(`share needs a positive whole in ${rows[1].fact_id}`)
89
+ if (part.num < 0n || compare(part, whole) > 0n) refuse(`share needs 0 <= part <= whole: ${rows[0].fact_id} is outside [0, ${rows[1].fact_id}]`)
90
+ return "%"
91
+ },
92
+ calculate: (rows) => generated(mul(div(...valuesOf(rows)), 100n), `${idsOf(rows).join(" / ")} * 100`),
93
+ malformed: shapes("affine units", "unequal units", "whole <= 0", "part < 0", "part above whole", "stray context"),
94
+ }),
95
+ ratio: Object.freeze({
96
+ ...binary, arityText: "two operands (num, den)", operandIds: pair("num", "den"), metadata: SNAPSHOT,
97
+ inputFields: ["num", "den"],
98
+ resultUnit(rows, units) {
99
+ notAffine("ratio", rows, units)
100
+ if (toBig(rows[1].exact).num === 0n) refuse(`ratio denominator ${rows[1].fact_id} is zero`)
101
+ if (units[0] === units[1]) return null
102
+ if (units.some(isPercentish)) refuse(`a ratio over % or %p needs equal units: ${listed(rows, units)}; use expr`)
103
+ return unitOver(units[0], units[1])
104
+ },
105
+ calculate: (rows) => generated(div(...valuesOf(rows)), idsOf(rows).join(" / ")),
106
+ malformed: shapes("affine units", "divides by zero", "stray context"),
107
+ }),
108
+ convert: Object.freeze({
109
+ ...ordinary, kind: "converted", arity: (n) => n === 1, arityText: "one operand", operandIds: listOf, metadata: [],
110
+ inputFields: ["operands", "to"],
111
+ context: (s) => ({ target: canonicalUnit(s.to) }), shape: convertShape,
112
+ defaultUnit: (s) => s.to,
113
+ resultUnit(rows, units, context) {
114
+ if (!nonEmpty(context.target)) refuse("convert needs a target unit")
115
+ const [from] = units
116
+ const target = context.target
117
+ if (from === target) refuse(`${rows[0].fact_id} is already in ${target}`)
118
+ if ([from, target].some((u) => dimensionOf(u)?.startsWith("currency:"))) refuse(`${rows[0].fact_id} in ${show(from)} has no table conversion to ${target} (a currency)`, EXPR_HINT)
119
+ if (!convertFactor(from, target)) refuse(`${rows[0].fact_id} in ${show(from)} has no table conversion to ${target}`)
120
+ return target
121
+ },
122
+ calculate(rows, context) {
123
+ const row = rows[0]
124
+ const f = convertFactor(row.unit, context.target)
125
+ const scale = { num: f.num, den: f.den }
126
+ const text = terminates(scale) ? toDecimal(scale) : `${scale.num}/${scale.den}`
127
+ return generated(add(mul(toBig(row.exact), scale), f.offset ?? 0n), `${row.fact_id} * ${text}${f.offset ? ` + ${f.offset.num}/${f.offset.den}` : ""}`, terminates(scale) ? text : null)
128
+ },
129
+ render: (shown, rows) => `${shown} (${display(rows[0].value, rows[0].unit)})`,
130
+ malformed: shapes("no context", "empty target", "alias target", "currency target", "no factor to target", "closed context"),
131
+ }),
132
+ expr: Object.freeze({
133
+ ...ordinary, arity: (n) => n >= 1, arityText: "one or more operands", compatibility: "unchecked",
134
+ expression: true, operandIds: (s, ast) => Object.hasOwn(s, "operands") ? s.operands : leaves(ast).ids,
135
+ inputFields: ["formula", "operands"],
136
+ metadata: [], precision: (ast) => ast?.op === "+" || ast?.op === "-" ? "places" : "sigfigs",
137
+ resultUnit(rows, units, _context, ast) {
138
+ for (const r of rows) if (isCurrencyRate(r.unit) && !nonEmpty(r.valid_at)) refuse(`FX rate ${r.fact_id} needs a dated valid_at; date compatibility remains unchecked`, EXPR_HINT, "undated_rate", "a dated FX rate row")
139
+ const inferred = inferExpr(ast, new Map(rows.map((r, i) => [r.fact_id, units[i]])), new Map(rows.map((r) => [r.fact_id, r]))).unit
140
+ notAffine("expr", rows, units)
141
+ return inferred
142
+ },
143
+ calculate(rows, _context, ast, formula) {
144
+ const { units } = inferExpr(ast, new Map(rows.map((r) => [r.fact_id, r.unit])))
145
+ return generated(evaluate(ast, new Map(rows.map((r) => [r.fact_id, toBig(r.exact)])), units), formula.trim())
146
+ },
147
+ shape: (row) => noContext(row) ?? exprShape(row),
148
+ malformed: shapes("formula id mismatch", "parse error", "divides by zero", "uninferable unit", "affine sum", "stray context", "undated rate"),
149
+ }),
150
+ })
151
+
152
+ export function ruleOf(op) {
153
+ if (!Object.hasOwn(OPERATION_RULES, op)) refuse(`unknown operation ${String(op)}; use one of ${Object.keys(OPERATION_RULES).join(", ")}`, undefined, "operation", Object.keys(OPERATION_RULES))
154
+ return OPERATION_RULES[op]
155
+ }
156
+ export function operandUnit(row) {
157
+ if (canonicalUnit(row.unit) !== row.unit || inputScale(row.unit)) refuse(`${row.fact_id} unit must be canonical stored text`, undefined, "noncanonical_operand", { unit: canonicalUnit(row.unit) })
158
+ return row.unit
159
+ }
160
+ export function expectedUnit(op, rows, { ast = null, context = null } = {}) {
161
+ const rule = ruleOf(op)
162
+ const repeated = repeatedOperand(idsOf(rows))
163
+ if (repeated) refuse(repeated, undefined, "repeated_operand", "distinct operand ids")
164
+ if (!rule.arity(rows.length)) refuse(`${op} takes ${rule.arityText}, got ${rows.length}`, undefined, "arity", rule.arityText)
165
+ const units = rows.map(operandUnit)
166
+ checkMetadata(rows, rule.metadata)
167
+ return rule.resultUnit(rows, units, context ?? {}, ast)
168
+ }
169
+ export function renderPrecision(op, unit, ast = null) {
170
+ return unit === "%p" ? "places" : ruleOf(op).precision(ast)
171
+ }
172
+ export const unitMismatch = (op, rows, unit, expected) => `unit ${show(unit)} does not follow from ${op} over ${rows.map((r) => `${r.fact_id} ${r.unit ?? "null"}`).join(", ")}: expected unit ${show(expected)}`
173
+ export const malformed = (row) => rowShape(row, OPERATION_RULES)