lazycodex-ai 5.1.44 → 5.1.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +948 -416
- package/dist/cli/service-auth/keystore.d.ts +4 -2
- package/dist/cli-node/index.js +948 -416
- package/docs/reference/web-terminal-visual-qa.md +8 -0
- package/package.json +1 -1
- package/packages/lsp-daemon/dist/cli.js +4 -4
- package/packages/lsp-daemon/dist/client.js +4 -4
- package/packages/lsp-daemon/dist/index.js +4 -4
- package/packages/lsp-daemon/package.json +3 -3
- package/packages/lsp-tools-mcp/dist/cli.js +4 -4
- package/packages/lsp-tools-mcp/dist/mcp.js +4 -4
- package/packages/lsp-tools-mcp/dist/tools.js +4 -4
- package/packages/lsp-tools-mcp/package.json +3 -3
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +62 -21
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/biome.json +1 -21
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/biome.json +1 -21
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/biome.json +1 -21
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +4 -4
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +0 -1
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +1 -1
- package/packages/omo-codex/plugin/components/rules/biome.json +1 -21
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +4 -4
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/biome.json +1 -21
- package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +90 -1
- package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +91 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +7 -7
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +2 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +7 -7
- package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +2 -0
- package/packages/omo-codex/plugin/skills/browser/SKILL.md +21 -22
- package/packages/omo-codex/plugin/skills/browser/references/commands.md +2 -9
- package/packages/omo-codex/plugin/skills/browser/references/install.md +1 -2
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +2 -2
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +63 -487
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +1 -1
- package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +1 -10
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +79 -118
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +75 -224
- package/packages/omo-codex/plugin/skills/frontend/references/design/stylegallery.md +32 -27
- package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -2
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/collect-files.ts +53 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +10 -7
- package/packages/omo-codex/plugin/skills/programming/references/typescript/README.md +10 -7
- package/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md +9 -23
- package/packages/omo-codex/plugin/skills/programming/references/typescript/oxc-lint.md +130 -0
- package/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md +6 -62
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/new-project.ts +20 -81
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +14 -14
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +9 -3
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +7 -6
- package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +51 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/claims-index.mjs +125 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +68 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +1 -27
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/lexicon.mjs +47 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/numerals.mjs +226 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +2 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/particles-ko.mjs +108 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/rational.mjs +139 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/sentence-split.mjs +77 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-normalize.mjs +82 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-similarity.mjs +28 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +105 -0
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +71 -64
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +4 -1
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +6 -6
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +152 -23
- package/packages/shared-skills/skills/browser/SKILL.md +21 -22
- package/packages/shared-skills/skills/browser/references/commands.md +2 -9
- package/packages/shared-skills/skills/browser/references/install.md +1 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +2 -2
- package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +63 -487
- package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +1 -1
- package/packages/shared-skills/skills/browser/scripts/omowright.mjs +1 -10
- package/packages/shared-skills/skills/frontend/SKILL.md +79 -118
- package/packages/shared-skills/skills/frontend/references/design/README.md +75 -224
- package/packages/shared-skills/skills/frontend/references/design/stylegallery.md +32 -27
- package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -2
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.test.ts +101 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.test.ts +50 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.ts +53 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
- package/packages/shared-skills/skills/programming/SKILL.md +10 -7
- package/packages/shared-skills/skills/programming/references/typescript/README.md +10 -7
- package/packages/shared-skills/skills/programming/references/typescript/bootstrap.md +9 -23
- package/packages/shared-skills/skills/programming/references/typescript/oxc-lint.md +130 -0
- package/packages/shared-skills/skills/programming/references/typescript/tsconfig-strict.md +6 -62
- package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
- package/packages/shared-skills/skills/programming/scripts/typescript/new-project.test.ts +162 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/new-project.ts +20 -81
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
- package/packages/shared-skills/skills/refactor/SKILL.md +13 -13
- package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
- package/packages/shared-skills/skills/ulw-research/AGENTS.md +9 -3
- package/packages/shared-skills/skills/ulw-research/SKILL.md +2 -2
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +7 -6
- package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
- package/packages/shared-skills/skills/ulw-research/references/report-gates.md +51 -3
- package/packages/shared-skills/skills/ulw-research/scripts/claims-index.mjs +125 -0
- package/packages/shared-skills/skills/ulw-research/scripts/claims-index.test.ts +149 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +68 -3
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +97 -12
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +190 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +141 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.test.ts +43 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +252 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.test.ts +179 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.test.ts +46 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-context.test.ts +64 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +1 -27
- package/packages/shared-skills/skills/ulw-research/scripts/lexicon.mjs +47 -0
- package/packages/shared-skills/skills/ulw-research/scripts/lexicon.test.ts +57 -0
- package/packages/shared-skills/skills/ulw-research/scripts/numerals.mjs +226 -0
- package/packages/shared-skills/skills/ulw-research/scripts/numerals.test.ts +120 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +2 -3
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +29 -0
- package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.mjs +108 -0
- package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.test.ts +86 -0
- package/packages/shared-skills/skills/ulw-research/scripts/rational.mjs +139 -0
- package/packages/shared-skills/skills/ulw-research/scripts/rational.test.ts +46 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
- package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.mjs +77 -0
- package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.test.ts +109 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/claim-graph.md +13 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-locked.md +17 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-pending.md +17 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.mjs +82 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.test.ts +73 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.mjs +28 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.test.ts +38 -0
- package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +105 -0
- package/packages/shared-skills/skills/ulw-research/scripts/units.test.ts +83 -0
- package/packages/shared-skills/skills/visual-qa/SKILL.md +70 -63
- package/packages/shared-skills/skills/visual-qa/scripts/skill-doc-fields.test.ts +34 -0
- package/script/qa/web-terminal-visual-qa.mjs +6 -2
- package/script/qa/xterm-live-terminal.mjs +26 -3
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// text-normalize.mjs - text normalization and raw-offset matching for fidelity checks.
|
|
2
|
+
//
|
|
3
|
+
// normalizeText applies NFC, maps curly and CJK quote glyphs to straight quotes, collapses
|
|
4
|
+
// whitespace runs to one space, and trims. findNormalized matches a needle against the
|
|
5
|
+
// normalized haystack and returns offsets into the RAW haystack through an offset map built
|
|
6
|
+
// during normalization. Plain Node ESM, no dependencies.
|
|
7
|
+
|
|
8
|
+
const DOUBLE_QUOTES = new Set([
|
|
9
|
+
"\u201C", "\u201D", "\u201E", "\u201F", "\u00AB", "\u00BB", "\u300C", "\u300D", "\u300E", "\u300F",
|
|
10
|
+
"\u301D", "\u301E", "\u301F", "\uFF02", "\u2033",
|
|
11
|
+
])
|
|
12
|
+
const SINGLE_QUOTES = new Set(["\u2018", "\u2019", "\u201A", "\u201B", "\u2039", "\u203A", "\uFF07", "\u2032"])
|
|
13
|
+
// Combining marks and conjoining Hangul medial/final jamo attach to the preceding code point,
|
|
14
|
+
// so a decomposed syllable is normalized as one unit.
|
|
15
|
+
const JOINS_PREVIOUS = /^[\p{M}\u1160-\u11FF\uD7B0-\uD7FF]$/u
|
|
16
|
+
const WHITESPACE = /^\s+$/u
|
|
17
|
+
const TOKEN_SEPARATOR = /[^\p{L}\p{N}\p{M}]+/u
|
|
18
|
+
|
|
19
|
+
function mapQuote(ch) {
|
|
20
|
+
if (DOUBLE_QUOTES.has(ch)) return '"'
|
|
21
|
+
if (SINGLE_QUOTES.has(ch)) return "'"
|
|
22
|
+
return ch
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function clusters(raw) {
|
|
26
|
+
const out = []
|
|
27
|
+
let at = 0
|
|
28
|
+
for (const ch of raw) {
|
|
29
|
+
const last = out[out.length - 1]
|
|
30
|
+
if (last && JOINS_PREVIOUS.test(ch)) last[1] += ch.length
|
|
31
|
+
else out.push([at, at + ch.length])
|
|
32
|
+
at += ch.length
|
|
33
|
+
}
|
|
34
|
+
return out
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Returns { text, starts, ends }: text[k] came from raw.slice(starts[k], ends[k]).
|
|
38
|
+
function normalizeWithMap(raw) {
|
|
39
|
+
const source = String(raw ?? "")
|
|
40
|
+
let text = ""
|
|
41
|
+
const starts = []
|
|
42
|
+
const ends = []
|
|
43
|
+
for (const [start, end] of clusters(source)) {
|
|
44
|
+
const piece = Array.from(source.slice(start, end).normalize("NFC"), mapQuote).join("")
|
|
45
|
+
if (WHITESPACE.test(piece)) {
|
|
46
|
+
if (text !== "" && !text.endsWith(" ")) {
|
|
47
|
+
text += " "
|
|
48
|
+
starts.push(start)
|
|
49
|
+
ends.push(end)
|
|
50
|
+
}
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
for (let k = 0; k < piece.length; k++) {
|
|
54
|
+
starts.push(start)
|
|
55
|
+
ends.push(end)
|
|
56
|
+
}
|
|
57
|
+
text += piece
|
|
58
|
+
}
|
|
59
|
+
if (text.endsWith(" ")) {
|
|
60
|
+
text = text.slice(0, -1)
|
|
61
|
+
starts.pop()
|
|
62
|
+
ends.pop()
|
|
63
|
+
}
|
|
64
|
+
return { text, starts, ends }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function normalizeText(s) {
|
|
68
|
+
return normalizeWithMap(s).text
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function tokenize(s) {
|
|
72
|
+
return normalizeText(s).split(TOKEN_SEPARATOR).filter((token) => token !== "")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function findNormalized(hay, needle) {
|
|
76
|
+
const target = normalizeText(needle)
|
|
77
|
+
if (target === "") return null
|
|
78
|
+
const map = normalizeWithMap(hay)
|
|
79
|
+
const at = map.text.indexOf(target)
|
|
80
|
+
if (at === -1) return null
|
|
81
|
+
return { start: map.starts[at], end: map.ends[at + target.length - 1] }
|
|
82
|
+
}
|
|
@@ -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,105 @@
|
|
|
1
|
+
// units.mjs - unit table: normalization, dimensions, exact conversion factors, Korean readings.
|
|
2
|
+
//
|
|
3
|
+
// normalizeUnit maps a raw unit (any spacing or spelling variant) to one canonical token. Each token
|
|
4
|
+
// has a dimension, an exact factor to the dimension's base unit (base = value * factor + offset), and
|
|
5
|
+
// the final-sound class of its Korean reading for the particle rule: "open" (no final consonant),
|
|
6
|
+
// "l" (final rieul), or "closed". Currency is its own dimension per currency, so no exchange rate is
|
|
7
|
+
// ever implied. Data units are decimal (1 KB = 1000 B). Plain Node ESM, sibling imports only.
|
|
8
|
+
|
|
9
|
+
import { div, fromDecimal, sub } from "./rational.mjs"
|
|
10
|
+
|
|
11
|
+
/** [token, dimension, factor, reading, aliases, offset?]; the Korean reading is noted per row. */
|
|
12
|
+
const TABLE = [
|
|
13
|
+
["mm", "length", "0.001", "open", ["millimeter", "millimeters", "\uBC00\uB9AC\uBBF8\uD130"]], // millimiteo
|
|
14
|
+
["cm", "length", "0.01", "open", ["centimeter", "centimeters", "\uC13C\uD2F0\uBBF8\uD130"]], // sentimiteo
|
|
15
|
+
["m", "length", "1", "open", ["meter", "meters", "metre", "metres", "\uBBF8\uD130"]], // miteo
|
|
16
|
+
["km", "length", "1000", "open", ["kilometer", "kilometers", "\uD0AC\uB85C\uBBF8\uD130"]], // killomiteo
|
|
17
|
+
["g", "mass", "1", "closed", ["gram", "grams", "\uADF8\uB7A8"]], // geuraem
|
|
18
|
+
["kg", "mass", "1000", "closed", ["kilogram", "kilograms", "\uD0AC\uB85C\uADF8\uB7A8"]], // killogeuraem
|
|
19
|
+
["t", "mass", "1000000", "closed", ["ton", "tons", "tonne", "tonnes", "\uD1A4"]], // ton
|
|
20
|
+
["ms", "time", "0.001", "open", ["millisecond", "milliseconds", "\uBC00\uB9AC\uCD08"]], // millicho
|
|
21
|
+
["s", "time", "1", "open", ["sec", "secs", "second", "seconds", "\uCD08"]], // cho
|
|
22
|
+
["min", "time", "60", "closed", ["mins", "minute", "minutes", "\uBD84"]], // bun
|
|
23
|
+
["h", "time", "3600", "closed", ["hr", "hrs", "hour", "hours", "\uC2DC\uAC04"]], // sigan
|
|
24
|
+
["B", "data", "1", "open", ["byte", "bytes", "\uBC14\uC774\uD2B8"]], // baiteu
|
|
25
|
+
["KB", "data", "1000", "open", ["\uD0AC\uB85C\uBC14\uC774\uD2B8"]],
|
|
26
|
+
["MB", "data", "1000000", "open", ["\uBA54\uAC00\uBC14\uC774\uD2B8"]],
|
|
27
|
+
["GB", "data", "1000000000", "open", ["\uAE30\uAC00\uBC14\uC774\uD2B8"]],
|
|
28
|
+
["TB", "data", "1000000000000", "open", ["\uD14C\uB77C\uBC14\uC774\uD2B8"]],
|
|
29
|
+
["C", "temperature", "1", "open", ["\u00B0C", "\u2103", "celsius", "\uC12D\uC528"], "0"], // do
|
|
30
|
+
["F", "temperature", "5/9", "open", ["\u00B0F", "\u2109", "fahrenheit", "\uD654\uC528"], "-160/9"], // do
|
|
31
|
+
["%", "percent", "1", "open", ["percent", "\uD37C\uC13C\uD2B8", "\uD504\uB85C"]], // peosenteu
|
|
32
|
+
["\u2030", "percent", "0.1", "l", ["permille", "\uD37C\uBC00"]], // peomil
|
|
33
|
+
["%p", "percentage-point", "1", "open", ["pp", "%pt", "percentage point", "percentage points", "\uD37C\uC13C\uD2B8\uD3EC\uC778\uD2B8", "%\uD3EC\uC778\uD2B8"]],
|
|
34
|
+
["x", "multiple", "1", "open", ["\u00D7", "times", "\uBC30"]], // bae
|
|
35
|
+
["e3", "scale", "1000", "closed", ["k", "thousand", "\uCC9C"]], // cheon
|
|
36
|
+
["e4", "scale", "10000", "closed", ["\uB9CC"]], // man
|
|
37
|
+
["e6", "scale", "1000000", "closed", ["M", "million", "\uBC31\uB9CC"]], // baengman
|
|
38
|
+
["e8", "scale", "100000000", "closed", ["\uC5B5"]], // eok
|
|
39
|
+
["e9", "scale", "1000000000", "closed", ["bn", "billion", "\uC2ED\uC5B5"]], // sibeok
|
|
40
|
+
["e12", "scale", "1000000000000", "open", ["trillion", "\uC870"]], // jo
|
|
41
|
+
["person", "count:person", "1", "closed", ["persons", "people", "\uBA85"]], // myeong
|
|
42
|
+
["case", "count:case", "1", "closed", ["cases", "\uAC74"]], // geon
|
|
43
|
+
["KRW", "currency:KRW", "1", "closed", ["\uC6D0", "\u20A9", "won"]], // won
|
|
44
|
+
["KRW-e3", "currency:KRW", "1000", "closed", ["\uCC9C\uC6D0"]],
|
|
45
|
+
["KRW-e4", "currency:KRW", "10000", "closed", ["\uB9CC\uC6D0"]],
|
|
46
|
+
["KRW-e8", "currency:KRW", "100000000", "closed", ["\uC5B5\uC6D0"]],
|
|
47
|
+
["KRW-e12", "currency:KRW", "1000000000000", "closed", ["\uC870\uC6D0"]],
|
|
48
|
+
["USD", "currency:USD", "1", "open", ["$", "US$", "dollar", "dollars", "\uB2EC\uB7EC"]], // dalleo
|
|
49
|
+
["EUR", "currency:EUR", "1", "open", ["\u20AC", "euro", "euros", "\uC720\uB85C"]], // yuro
|
|
50
|
+
["JPY", "currency:JPY", "1", "closed", ["\u00A5", "yen", "\uC5D4"]], // en
|
|
51
|
+
["GBP", "currency:GBP", "1", "open", ["\u00A3", "pound", "pounds", "\uD30C\uC6B4\uB4DC"]], // paundeu
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const parse = (text) => (text.includes("/") ? div(fromDecimal(text.split("/")[0]), fromDecimal(text.split("/")[1])) : fromDecimal(text))
|
|
55
|
+
const squash = (raw) => raw.replace(/\s+/g, "")
|
|
56
|
+
|
|
57
|
+
const ENTRIES = new Map()
|
|
58
|
+
/** Exact-case keys: every token and alias with whitespace removed. */
|
|
59
|
+
const EXACT = new Map()
|
|
60
|
+
/** Case-insensitive keys: multi-letter ASCII aliases only, so m/M and B/b never collide. */
|
|
61
|
+
const WORD = new Map()
|
|
62
|
+
for (const [token, dimension, factor, reading, aliases, offset] of TABLE) {
|
|
63
|
+
const entry = Object.freeze({ token, dimension, factor: parse(factor), offset: parse(offset ?? "0"), reading })
|
|
64
|
+
ENTRIES.set(token, entry)
|
|
65
|
+
for (const alias of [token, ...aliases]) {
|
|
66
|
+
const key = squash(alias)
|
|
67
|
+
EXACT.set(key, entry)
|
|
68
|
+
if (key.length > 1 && /^[A-Za-z]+$/.test(key) && !WORD.has(key.toLowerCase())) WORD.set(key.toLowerCase(), entry)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function entryOf(raw) {
|
|
73
|
+
if (typeof raw !== "string") return null
|
|
74
|
+
const key = squash(raw)
|
|
75
|
+
if (!key) return null
|
|
76
|
+
return EXACT.get(key) ?? WORD.get(key.toLowerCase()) ?? null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Canonical token for a raw unit, or null when the unit is not in the table. */
|
|
80
|
+
export function normalizeUnit(raw) {
|
|
81
|
+
return entryOf(raw)?.token ?? null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Dimension of a unit ("length", "currency:KRW", ...), or null. */
|
|
85
|
+
export function dimensionOf(token) {
|
|
86
|
+
return entryOf(token)?.dimension ?? null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Exact factor from one unit to another of the same dimension: to = from * num / den (+ offset for
|
|
91
|
+
* affine scales). Null across dimensions, across currencies, or for an unknown unit.
|
|
92
|
+
*/
|
|
93
|
+
export function convertFactor(from, to) {
|
|
94
|
+
const a = entryOf(from)
|
|
95
|
+
const b = entryOf(to)
|
|
96
|
+
if (!a || !b || a.dimension !== b.dimension) return null
|
|
97
|
+
const { num, den } = div(a.factor, b.factor)
|
|
98
|
+
const offset = div(sub(a.offset, b.offset), b.factor)
|
|
99
|
+
return offset.num === 0n ? { num, den } : { num, den, offset: { num: offset.num, den: offset.den } }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Final-sound class of the unit's Korean reading: "open", "closed", or "l"; null when unknown. */
|
|
103
|
+
export function koreanReadingOf(token) {
|
|
104
|
+
return entryOf(token)?.reading ?? null
|
|
105
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
|
|
3
|
+
import { convertFactor, dimensionOf, koreanReadingOf, normalizeUnit } from "./units.mjs"
|
|
4
|
+
|
|
5
|
+
describe("normalizeUnit", () => {
|
|
6
|
+
test("ignores whitespace and spelling variants", () => {
|
|
7
|
+
expect(normalizeUnit("억 원")).toBe(normalizeUnit("억원"))
|
|
8
|
+
expect(normalizeUnit("억원")).not.toBeNull()
|
|
9
|
+
expect(normalizeUnit("million")).toBe(normalizeUnit("M"))
|
|
10
|
+
expect(normalizeUnit("USD")).toBe(normalizeUnit("$"))
|
|
11
|
+
expect(normalizeUnit("달러")).toBe(normalizeUnit("USD"))
|
|
12
|
+
expect(normalizeUnit("%")).not.toBe(normalizeUnit("%p"))
|
|
13
|
+
expect(normalizeUnit("m")).not.toBe(normalizeUnit("M"))
|
|
14
|
+
expect(normalizeUnit("furlongs")).toBeNull()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("a normalized token normalizes to itself", () => {
|
|
18
|
+
for (const raw of ["억 원", "m", "kg", "GB", "%p", "USD", "C", "F"]) {
|
|
19
|
+
const token = normalizeUnit(raw)
|
|
20
|
+
expect(normalizeUnit(token as string)).toBe(token)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe("dimensionOf and convertFactor", () => {
|
|
26
|
+
test("length, mass, time and data convert exactly", () => {
|
|
27
|
+
expect(convertFactor("m", "mm")).toEqual({ num: 1000n, den: 1n })
|
|
28
|
+
expect(convertFactor("mm", "m")).toEqual({ num: 1n, den: 1000n })
|
|
29
|
+
expect(convertFactor("km", "cm")).toEqual({ num: 100000n, den: 1n })
|
|
30
|
+
expect(convertFactor("kg", "t")).toEqual({ num: 1n, den: 1000n })
|
|
31
|
+
expect(convertFactor("h", "s")).toEqual({ num: 3600n, den: 1n })
|
|
32
|
+
expect(convertFactor("GB", "MB")).toEqual({ num: 1000n, den: 1n })
|
|
33
|
+
expect(dimensionOf(normalizeUnit("cm") as string)).toBe("length")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("the Korean currency scales convert within one currency", () => {
|
|
37
|
+
expect(convertFactor("억 원", "원")).toEqual({ num: 100000000n, den: 1n })
|
|
38
|
+
expect(convertFactor("조원", "억원")).toEqual({ num: 10000n, den: 1n })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("currency is never converted across currencies, and dimensions never mix", () => {
|
|
42
|
+
expect(convertFactor("USD", "KRW")).toBeNull()
|
|
43
|
+
expect(convertFactor("m", "kg")).toBeNull()
|
|
44
|
+
expect(convertFactor("%", "%p")).toBeNull()
|
|
45
|
+
expect(convertFactor("m", "furlongs")).toBeNull()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("temperature is affine", () => {
|
|
49
|
+
expect(convertFactor("C", "F")).toEqual({ num: 9n, den: 5n, offset: { num: 32n, den: 1n } })
|
|
50
|
+
expect(convertFactor("F", "C")).toEqual({ num: 5n, den: 9n, offset: { num: -160n, den: 9n } })
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe("koreanReadingOf", () => {
|
|
55
|
+
const cases: Array<[string, string]> = [
|
|
56
|
+
["%", "open"],
|
|
57
|
+
["%p", "open"],
|
|
58
|
+
["m", "open"],
|
|
59
|
+
["km", "open"],
|
|
60
|
+
["원", "closed"],
|
|
61
|
+
["달러", "open"],
|
|
62
|
+
["유로", "open"],
|
|
63
|
+
["엔", "closed"],
|
|
64
|
+
["배", "open"],
|
|
65
|
+
["건", "closed"],
|
|
66
|
+
["명", "closed"],
|
|
67
|
+
["조", "open"],
|
|
68
|
+
["억", "closed"],
|
|
69
|
+
["만", "closed"],
|
|
70
|
+
["천", "closed"],
|
|
71
|
+
["kg", "closed"],
|
|
72
|
+
["GB", "open"],
|
|
73
|
+
]
|
|
74
|
+
for (const [raw, reading] of cases) {
|
|
75
|
+
test(`${raw} reads ${reading}`, () => {
|
|
76
|
+
expect(koreanReadingOf(raw)).toBe(reading)
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
test("unknown units have no reading", () => {
|
|
81
|
+
expect(koreanReadingOf("furlongs")).toBeNull()
|
|
82
|
+
})
|
|
83
|
+
})
|