lazycodex-ai 5.1.43 → 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,103 @@
|
|
|
1
|
+
// fact-ledger.mjs - the verbatim fact ledger: one row per measured, quoted, derived, assumed,
|
|
2
|
+
// or converted value, with its source locator and exact rational. Rows are never deleted:
|
|
3
|
+
// retract and supersede change status and append an event. Validation and the field vocabulary
|
|
4
|
+
// live in fact-ledger-schema.mjs and are re-exported here. node: builtins only.
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "node:fs"
|
|
7
|
+
import { join } from "node:path"
|
|
8
|
+
|
|
9
|
+
import { CliError, readJsonFile, writeJsonAtomic } from "./cli-support.mjs"
|
|
10
|
+
import { DECIMAL, exactComesFromValue, exactFromDecimal, idPrefix, isString } from "./fact-ledger-kinds.mjs"
|
|
11
|
+
import { LEDGER_SCHEMA_VERSION, ROW_DEFAULTS, ROW_FIELDS, transitionError, validateLedger } from "./fact-ledger-schema.mjs"
|
|
12
|
+
|
|
13
|
+
export { exactFromDecimal } from "./fact-ledger-kinds.mjs"
|
|
14
|
+
export { FACT_KINDS, FACT_STATUSES, LEDGER_SCHEMA_VERSION, OPERATIONS, QUALIFIERS, ROW_FIELDS, TRANSITIONS, transitionError, validateLedger } from "./fact-ledger-schema.mjs"
|
|
15
|
+
|
|
16
|
+
export const LEDGER_FILENAME = "fact-ledger.json"
|
|
17
|
+
|
|
18
|
+
export function emptyLedger() {
|
|
19
|
+
return { schemaVersion: LEDGER_SCHEMA_VERSION, rows: [], events: [] }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function assertValid(ledger) {
|
|
23
|
+
const errors = validateLedger(ledger)
|
|
24
|
+
if (errors.length > 0) throw new CliError(`invalid fact ledger: ${errors.join("; ")}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readLedger(dir) {
|
|
28
|
+
const file = join(dir, LEDGER_FILENAME)
|
|
29
|
+
if (!existsSync(file)) return emptyLedger()
|
|
30
|
+
const ledger = readJsonFile(file)
|
|
31
|
+
assertValid(ledger)
|
|
32
|
+
return ledger
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function writeLedger(dir, ledger) {
|
|
36
|
+
assertValid(ledger)
|
|
37
|
+
const file = join(dir, LEDGER_FILENAME)
|
|
38
|
+
writeJsonAtomic(file, ledger)
|
|
39
|
+
return file
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ID_NUMBER = Object.freeze({ F: /^F(\d+)$/, Q: /^Q(\d+)$/ })
|
|
43
|
+
|
|
44
|
+
/** The next id with the kind's prefix from KIND_RULES (F<n> for value rows, Q<n> for quotes). */
|
|
45
|
+
export function nextId(ledger, kind) {
|
|
46
|
+
const prefix = idPrefix(kind)
|
|
47
|
+
let max = 0
|
|
48
|
+
for (const row of ledger.rows) {
|
|
49
|
+
const match = ID_NUMBER[prefix].exec(row.fact_id ?? "")
|
|
50
|
+
if (match) max = Math.max(max, Number(match[1]))
|
|
51
|
+
}
|
|
52
|
+
return `${prefix}${max + 1}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fill defaults, assign the next id, validate the whole ledger (provenance included), then append.
|
|
57
|
+
* Only kinds whose KIND_RULES exact is from_value (measured) get exact from their decimal:
|
|
58
|
+
* derived and converted rows carry the exact that derive computed from the operands and factor,
|
|
59
|
+
* never one read back from the rounded value.
|
|
60
|
+
*/
|
|
61
|
+
export function addRow(ledger, row) {
|
|
62
|
+
const filled = { ...structuredClone(ROW_DEFAULTS), ...row, fact_id: row.fact_id ?? nextId(ledger, row.kind) }
|
|
63
|
+
if (exactComesFromValue(filled.kind) && filled.exact === null && isString(filled.value) && DECIMAL.test(filled.value)) {
|
|
64
|
+
filled.exact = exactFromDecimal(filled.value)
|
|
65
|
+
}
|
|
66
|
+
const ordered = Object.fromEntries(ROW_FIELDS.filter((key) => Object.hasOwn(filled, key)).map((key) => [key, filled[key]]))
|
|
67
|
+
for (const key of Object.keys(filled)) if (!ROW_FIELDS.includes(key)) ordered[key] = filled[key]
|
|
68
|
+
assertValid({ ...ledger, rows: [...ledger.rows, ordered] })
|
|
69
|
+
ledger.rows.push(ordered)
|
|
70
|
+
return ordered
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Build the event from the row's current status and append it only if transitionError allows it. */
|
|
74
|
+
function changeStatus(ledger, id, to, replacement, reason) {
|
|
75
|
+
const row = ledger.rows.find((candidate) => candidate.fact_id === id)
|
|
76
|
+
const event = { fact_id: id, from: row?.status ?? null, to, replacement: replacement ?? null, reason }
|
|
77
|
+
const problem = transitionError(new Map(ledger.rows.map((candidate) => [candidate.fact_id, candidate.status])), event)
|
|
78
|
+
if (problem) throw new CliError(problem)
|
|
79
|
+
ledger.events.push(event)
|
|
80
|
+
row.status = to
|
|
81
|
+
return row
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function retractRow(ledger, id, reason) {
|
|
85
|
+
return changeStatus(ledger, id, "retracted", null, reason)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function supersedeRow(ledger, id, replacementId, reason) {
|
|
89
|
+
return changeStatus(ledger, id, "superseded", replacementId, reason)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function cell(value) {
|
|
93
|
+
return value === null || value === undefined ? "" : String(value).replace(/\|/g, "\\|").replace(/\n/g, " ")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The `--view` table: one line per row, retracted and superseded rows included. */
|
|
97
|
+
export function renderLedgerMarkdown(ledger) {
|
|
98
|
+
const lines = ["| fact_id | kind | label | value | unit | source | status | raw |", "|---|---|---|---|---|---|---|---|"]
|
|
99
|
+
for (const row of ledger.rows) {
|
|
100
|
+
lines.push(`| ${[row.fact_id, row.kind, row.label, row.value, row.unit, row.source_id, row.status, row.raw].map(cell).join(" | ")} |`)
|
|
101
|
+
}
|
|
102
|
+
return `${lines.join("\n")}\n`
|
|
103
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// fidelity-brief.mjs - parse the `## Fidelity` block of brief.md and decide whether it is locked.
|
|
2
|
+
//
|
|
3
|
+
// Grammar (fable-interview.md section 3, "Parser contract"): the block starts at a line
|
|
4
|
+
// `## Fidelity` and ends at the next `## ` heading. It holds the seven field lines below, in any
|
|
5
|
+
// order, each `<field>: <value> (answered_by: user|default|request|pending)`, plus an optional
|
|
6
|
+
// `asked: <field>, <field>` history line. Values carry no parentheses, and `answered_by: pending`
|
|
7
|
+
// is legal only with the value `pending`. Trailing whitespace on a line is ignored; a second
|
|
8
|
+
// `## Fidelity` block is a parse error. This module parses; it never interprets a value, which
|
|
9
|
+
// is each consumer's job. Plain Node ESM, node: builtins only.
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "node:fs"
|
|
12
|
+
|
|
13
|
+
export const FIDELITY_FIELDS = Object.freeze([
|
|
14
|
+
"as of",
|
|
15
|
+
"meaning check",
|
|
16
|
+
"source floor",
|
|
17
|
+
"derived numbers",
|
|
18
|
+
"languages",
|
|
19
|
+
"identifier classes",
|
|
20
|
+
"fallback",
|
|
21
|
+
])
|
|
22
|
+
export const FIDELITY_ANSWERED_BY = Object.freeze(["user", "default", "request", "pending"])
|
|
23
|
+
const LOCKED_ANSWERED_BY = new Set(["user", "default", "request"])
|
|
24
|
+
|
|
25
|
+
export const FIDELITY_INCOMPLETE = "fidelity_interview_incomplete"
|
|
26
|
+
const INCOMPLETE_HINT =
|
|
27
|
+
"A fidelity field is pending or missing in brief.md; answer the open question or record its default, then rerun."
|
|
28
|
+
|
|
29
|
+
const BLOCK_HEADING = /^## Fidelity\s*$/
|
|
30
|
+
const NEXT_HEADING = /^## /
|
|
31
|
+
const FIELD_LINE = /^([a-z][a-z ]*[a-z]): ([^()]+) \(answered_by: (user|default|request|pending)\)$/
|
|
32
|
+
const ASKED_LINE = /^asked:(.*)$/
|
|
33
|
+
|
|
34
|
+
function blockLines(markdown) {
|
|
35
|
+
const lines = String(markdown)
|
|
36
|
+
.split(/\r?\n/)
|
|
37
|
+
.map((line) => line.trimEnd())
|
|
38
|
+
const headings = lines.flatMap((line, index) => (BLOCK_HEADING.test(line) ? [index] : []))
|
|
39
|
+
if (headings.length === 0) return null
|
|
40
|
+
const [start, ...duplicates] = headings
|
|
41
|
+
const body = []
|
|
42
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
43
|
+
if (NEXT_HEADING.test(lines[index])) break
|
|
44
|
+
body.push({ text: lines[index], line: index + 1 })
|
|
45
|
+
}
|
|
46
|
+
return { body, duplicates: duplicates.map((index) => index + 1) }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseAsked(rest, line, text, errors) {
|
|
50
|
+
const names = rest
|
|
51
|
+
.split(",")
|
|
52
|
+
.map((name) => name.trim())
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
const unknown = names.filter((name) => !FIDELITY_FIELDS.includes(name))
|
|
55
|
+
if (unknown.length > 0) {
|
|
56
|
+
errors.push(`line ${line}: malformed asked line, unknown field ${unknown.join(", ")}: ${text}`)
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
return names
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseFieldLine({ text, line }, parsed, errors) {
|
|
63
|
+
const match = FIELD_LINE.exec(text)
|
|
64
|
+
if (!match) return errors.push(`line ${line}: malformed fidelity line: ${text}`)
|
|
65
|
+
const [, field, rawValue, answeredBy] = match
|
|
66
|
+
const value = rawValue.trim()
|
|
67
|
+
if (!FIDELITY_FIELDS.includes(field)) return errors.push(`line ${line}: malformed fidelity line, unknown field "${field}": ${text}`)
|
|
68
|
+
if (Object.hasOwn(parsed.fields, field)) return errors.push(`line ${line}: malformed fidelity line, duplicate field "${field}": ${text}`)
|
|
69
|
+
if ((answeredBy === "pending") !== (value === "pending")) {
|
|
70
|
+
return errors.push(`line ${line}: malformed fidelity line, value pending and answered_by: pending go together: ${text}`)
|
|
71
|
+
}
|
|
72
|
+
parsed.fields[field] = value
|
|
73
|
+
parsed.answered_by[field] = answeredBy
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} briefMd the whole brief.md text
|
|
78
|
+
* @returns {{ present: boolean, fields: Record<string, string>, answered_by: Record<string, string>,
|
|
79
|
+
* asked: string[], missing: string[], pending: string[], errors: string[] }}
|
|
80
|
+
*/
|
|
81
|
+
export function parseFidelityBlock(briefMd) {
|
|
82
|
+
const parsed = { present: false, fields: {}, answered_by: {}, asked: [], missing: [], pending: [], errors: [] }
|
|
83
|
+
const block = blockLines(briefMd)
|
|
84
|
+
if (block !== null) {
|
|
85
|
+
const { body, duplicates } = block
|
|
86
|
+
parsed.present = true
|
|
87
|
+
for (const line of duplicates) parsed.errors.push(`line ${line}: duplicate ## Fidelity block, only one is allowed`)
|
|
88
|
+
let sawAsked = false
|
|
89
|
+
for (const entry of body) {
|
|
90
|
+
if (entry.text.trim() === "") continue
|
|
91
|
+
const asked = ASKED_LINE.exec(entry.text)
|
|
92
|
+
if (!asked) {
|
|
93
|
+
parseFieldLine(entry, parsed, parsed.errors)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
if (sawAsked) parsed.errors.push(`line ${entry.line}: malformed asked line, duplicate: ${entry.text}`)
|
|
97
|
+
else parsed.asked = parseAsked(asked[1], entry.line, entry.text, parsed.errors)
|
|
98
|
+
sawAsked = true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
parsed.missing = FIDELITY_FIELDS.filter((field) => !Object.hasOwn(parsed.fields, field))
|
|
102
|
+
parsed.pending = FIDELITY_FIELDS.filter((field) => parsed.answered_by[field] === "pending")
|
|
103
|
+
return parsed
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Locked = no parse error and all seven fields present with answered_by user, default, or request. */
|
|
107
|
+
export function isLocked(parsed) {
|
|
108
|
+
if (!parsed || parsed.errors.length > 0) return false
|
|
109
|
+
return FIDELITY_FIELDS.every((field) => Object.hasOwn(parsed.fields, field) && LOCKED_ANSWERED_BY.has(parsed.answered_by[field]))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function incompleteDefect(briefPath, parsed) {
|
|
113
|
+
const reasons = []
|
|
114
|
+
if (!parsed.present) reasons.push("no ## Fidelity block")
|
|
115
|
+
else if (parsed.missing.length > 0) reasons.push(`missing: ${parsed.missing.join(", ")}`)
|
|
116
|
+
if (parsed.pending.length > 0) reasons.push(`pending: ${parsed.pending.join(", ")}`)
|
|
117
|
+
return {
|
|
118
|
+
code: FIDELITY_INCOMPLETE,
|
|
119
|
+
severity: "blocker",
|
|
120
|
+
integrity: true,
|
|
121
|
+
selector: null,
|
|
122
|
+
line: null,
|
|
123
|
+
message: `${briefPath}: fidelity interview incomplete (${reasons.join("; ")})`,
|
|
124
|
+
hint: INCOMPLETE_HINT,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The lock check the reader-facing CLI stages run before writing anything.
|
|
130
|
+
* exitCode 0 = locked; 1 = pending, missing, or no brief (one `fidelity_interview_incomplete`
|
|
131
|
+
* defect); 2 = malformed block (usage error, `errors` name the lines, no defect).
|
|
132
|
+
* @param {string} briefPath
|
|
133
|
+
*/
|
|
134
|
+
export function requireLockedBrief(briefPath) {
|
|
135
|
+
let markdown = ""
|
|
136
|
+
try {
|
|
137
|
+
markdown = readFileSync(briefPath, "utf8")
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error?.code !== "ENOENT") throw error
|
|
140
|
+
}
|
|
141
|
+
const parsed = parseFidelityBlock(markdown)
|
|
142
|
+
if (parsed.errors.length > 0) return { locked: false, exitCode: 2, defects: [], errors: parsed.errors, parsed }
|
|
143
|
+
if (isLocked(parsed)) return { locked: true, exitCode: 0, defects: [], errors: [], parsed }
|
|
144
|
+
return { locked: false, exitCode: 1, defects: [incompleteDefect(briefPath, parsed)], errors: [], parsed }
|
|
145
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// fidelity-state.mjs - per-sentence failure counts for the fidelity repair loop. A sentence is
|
|
2
|
+
// keyed by its section slug and the sorted fact ids it cites, so a reworded sentence that
|
|
3
|
+
// cites the same facts keeps its count. node: builtins only.
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs"
|
|
6
|
+
import { join } from "node:path"
|
|
7
|
+
|
|
8
|
+
import { CliError, readJsonFile, writeJsonAtomic } from "./cli-support.mjs"
|
|
9
|
+
|
|
10
|
+
export const STATE_FILENAME = "fidelity-state.json"
|
|
11
|
+
export const STATE_SCHEMA_VERSION = 1
|
|
12
|
+
|
|
13
|
+
/** slug + '#' + the fact ids sorted and joined by ','. */
|
|
14
|
+
export function sentenceKey(sectionSlug, factIds) {
|
|
15
|
+
return `${sectionSlug}#${[...factIds].sort().join(",")}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function emptyState() {
|
|
19
|
+
return { schemaVersion: STATE_SCHEMA_VERSION, failures: {}, replaced: [] }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isValid(state) {
|
|
23
|
+
return (
|
|
24
|
+
state !== null &&
|
|
25
|
+
typeof state === "object" &&
|
|
26
|
+
state.schemaVersion === STATE_SCHEMA_VERSION &&
|
|
27
|
+
state.failures !== null &&
|
|
28
|
+
typeof state.failures === "object" &&
|
|
29
|
+
!Array.isArray(state.failures) &&
|
|
30
|
+
Object.values(state.failures).every((count) => Number.isInteger(count) && count >= 0) &&
|
|
31
|
+
Array.isArray(state.replaced) &&
|
|
32
|
+
state.replaced.every((key) => typeof key === "string")
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readState(dir) {
|
|
37
|
+
const file = join(dir, STATE_FILENAME)
|
|
38
|
+
if (!existsSync(file)) return emptyState()
|
|
39
|
+
const state = readJsonFile(file)
|
|
40
|
+
if (!isValid(state)) throw new CliError(`${file} is not a valid fidelity state`)
|
|
41
|
+
return state
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function recordFailure(state, key) {
|
|
45
|
+
state.failures[key] = (state.failures[key] ?? 0) + 1
|
|
46
|
+
return state.failures[key]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function failureCount(state, key) {
|
|
50
|
+
return state.failures[key] ?? 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function markReplaced(state, key) {
|
|
54
|
+
if (!state.replaced.includes(key)) state.replaced.push(key)
|
|
55
|
+
return state
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function writeState(dir, state) {
|
|
59
|
+
if (!isValid(state)) throw new CliError("refusing to write an invalid fidelity state")
|
|
60
|
+
const file = join(dir, STATE_FILENAME)
|
|
61
|
+
writeJsonAtomic(file, state)
|
|
62
|
+
return file
|
|
63
|
+
}
|
|
@@ -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,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
|
+
}
|
|
@@ -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
|