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