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
|
@@ -39,12 +39,69 @@ export const DEFECT_CODES = deepFreeze({
|
|
|
39
39
|
layout_scroll_container: { severity: "minor", integrity: false, hint: "Scrolling content prints clipped; make the content fit or wrap the table in a scroll wrapper only for HTML deliverables." },
|
|
40
40
|
layout_image_distorted: { severity: "major", integrity: false, hint: "Preserve the image aspect ratio (object-fit: contain inside a fixed container)." },
|
|
41
41
|
layout_sibling_overlap: { severity: "major", integrity: false, hint: "Separate the overlapping elements (margins, grid, or a slide split)." },
|
|
42
|
+
unresolved_placeholder: { severity: "blocker", integrity: true, hint: "Register the fact with fact add or remove the sentence; a placeholder is never filled with a guess." },
|
|
43
|
+
value_mismatch: { severity: "blocker", integrity: true, hint: "Re-run fact fill; never edit a filled value by hand." },
|
|
44
|
+
unit_mismatch: { severity: "blocker", integrity: true, hint: "Re-run fact fill, or derive a converted row with fact derive --op convert." },
|
|
45
|
+
quote_not_verbatim: { severity: "blocker", integrity: true, hint: "Re-insert the quote through its {{Q<n>}} placeholder; after a second failure the sentence is replaced by the quote." },
|
|
46
|
+
claim_status_invalid: { severity: "blocker", integrity: true, hint: "The claim id is unknown, refuted, unresolved, or not in verified-claims; move the sentence to Gaps or Contradictions, or remove it." },
|
|
47
|
+
incompatible_operands: { severity: "blocker", integrity: true, hint: "The operands differ in unit, population, period, or basis for this operation; fix the rows or declare the operation that fits." },
|
|
48
|
+
pointer_label_mismatch: { severity: "blocker", integrity: true, hint: "The placeholder's label is not the row's label; check the id, then the label." },
|
|
49
|
+
quote_anchor_unresolved: { severity: "blocker", integrity: true, hint: "A from or to anchor is missing or occurs more than once in the quote; pick unique anchor words." },
|
|
50
|
+
unit_duplicated: { severity: "blocker", integrity: true, hint: "The unit after the placeholder repeats the row's unit; use :value or remove the unit." },
|
|
51
|
+
polish_diff_fail: { severity: "blocker", integrity: true, hint: "The polish pass changed numbers, tickers, or table rows; revert to the pre-polish artifact and apply the language fixes only." },
|
|
52
|
+
fidelity_interview_incomplete: { severity: "blocker", integrity: true, hint: "A fidelity field is pending or missing in brief.md; answer the open question or record its default, then rerun." },
|
|
53
|
+
derived_not_allowed: { severity: "blocker", integrity: true, hint: "The brief allows no derived or assumed numbers; cite a source row directly or change the brief with the requester." },
|
|
54
|
+
source_outside_floor: { severity: "blocker", integrity: true, hint: "The row's source is outside the source floor the brief records; use a source the brief allows or remove the fact." },
|
|
55
|
+
snapshot_hash_mismatch: { severity: "blocker", integrity: true, hint: "The source snapshot changed after it was pinned; re-snapshot the source and re-register its rows." },
|
|
56
|
+
chart_value_untraceable: { severity: "blocker", integrity: true, hint: "A chart value, tick, or data file does not match its ledger-backed data source; re-render the chart from the exported data." },
|
|
57
|
+
quote_unbound: { severity: "blocker", integrity: true, hint: "A direct quotation has no ledger quote row; register it with fact quote and insert it through its placeholder." },
|
|
58
|
+
edition_unverified: { severity: "blocker", integrity: true, hint: "The delivered file has no matching fact verify record; run fact verify on that edition and record it with --verified-by." },
|
|
59
|
+
untraceable_number: { severity: "major", integrity: false, hint: "Replace the typed number with a {{F<n>}} placeholder backed by a ledger row, or register it as assumed with a rationale." },
|
|
60
|
+
unsupported_verification: { severity: "major", integrity: false, hint: "The sentence claims a tool check that no recorded command output backs; link the recorded output row or remove the claim." },
|
|
61
|
+
conflicting_readings: { severity: "major", integrity: false, hint: "Two ledger rows with different periods or qualifiers are cited as one fact; split the sentence or retire one row." },
|
|
62
|
+
hedge_dropped: { severity: "major", integrity: false, hint: "Restore the qualifier the source uses (estimated, about, not yet)." },
|
|
63
|
+
direction_flipped: { severity: "major", integrity: false, hint: "The source states the opposite direction; rewrite from the quote." },
|
|
64
|
+
causality_added: { severity: "major", integrity: false, hint: "The source states no cause; state the correlation or quote the source." },
|
|
65
|
+
qualifier_dropped: { severity: "major", integrity: false, hint: "The source bounds this value (up to, at least, about); restore the qualifier or use :q." },
|
|
66
|
+
unbound_sentence: { severity: "major", integrity: false, hint: "Bind the sentence to a fact, a quote, a citation, or a claim-graph node with data-claim." },
|
|
67
|
+
meaning_drift_confirmed: { severity: "major", integrity: false, hint: "Two independent readers found the same drift; rewrite from the quote or dispute with a reason." },
|
|
68
|
+
polish_edit_rejected: { severity: "major", integrity: false, hint: "The edit changed a number, a hedge, a qualifier, a link, or a claim id; re-propose the language fix only." },
|
|
69
|
+
polish_hedging_decreased: { severity: "major", integrity: false, hint: "The polish pass removed qualifiers; restore them." },
|
|
70
|
+
polish_text_churn: { severity: "major", integrity: false, hint: "More than half the text changed during polish; the pass composed instead of proofreading." },
|
|
71
|
+
snapshot_mismatch: { severity: "major", integrity: false, hint: "The operands come from different collection windows; use rows from one window or state the windows separately." },
|
|
72
|
+
join_integrity_fail: { severity: "major", integrity: false, hint: "A table row combines facts from different source observations; keep each row to one observation or split it." },
|
|
73
|
+
quote_stale_version: { severity: "major", integrity: false, hint: "The quoted source revision differs from the revision the report targets; re-snapshot the target revision or date the quote." },
|
|
74
|
+
quote_fuzzy_match: { severity: "minor", integrity: false, hint: "The source text is OCR-noisy; confirm the snapshot and prefer the raw document." },
|
|
75
|
+
name_outside_quote: { severity: "minor", integrity: false, hint: "A name in the sentence is not in the cited quote; add the source or remove the name." },
|
|
76
|
+
scope_widened_warn: { severity: "minor", integrity: false, hint: "The sentence uses a broader scope word than the quote; confirm the scope or narrow it." },
|
|
77
|
+
particle_unverified: { severity: "minor", integrity: false, hint: "The particle after this value could not be checked; read it aloud and fix it by hand if wrong." },
|
|
78
|
+
fallback_without_entity: { severity: "minor", integrity: false, hint: "The replacement quote has no entity for attribution; set one on the row." },
|
|
79
|
+
meaning_drift_warn: { severity: "minor", integrity: false, hint: "A separate reader found the sentence changes scope, subject, or certainty; rewrite from the quote." },
|
|
80
|
+
polish_edit_unlocated: { severity: "minor", integrity: false, hint: "The edit's before-text was not found exactly once; re-propose against the current text." },
|
|
81
|
+
polish_link_changed: { severity: "minor", integrity: false, hint: "A link was altered or removed during polish; restore it." },
|
|
82
|
+
polish_name_changed: { severity: "minor", integrity: false, hint: "A proper noun changed during polish; restore it." },
|
|
42
83
|
})
|
|
43
84
|
|
|
85
|
+
/** Codes only the fidelity verifier raises (fact ledger, fill, verify, meaning, polish). */
|
|
86
|
+
export const FIDELITY_CODES = Object.freeze([
|
|
87
|
+
"unresolved_placeholder", "value_mismatch", "unit_mismatch", "quote_not_verbatim", "claim_status_invalid",
|
|
88
|
+
"incompatible_operands", "pointer_label_mismatch", "quote_anchor_unresolved", "unit_duplicated", "polish_diff_fail",
|
|
89
|
+
"fidelity_interview_incomplete", "derived_not_allowed", "source_outside_floor", "snapshot_hash_mismatch",
|
|
90
|
+
"chart_value_untraceable", "quote_unbound", "edition_unverified", "untraceable_number", "unsupported_verification",
|
|
91
|
+
"conflicting_readings", "hedge_dropped", "direction_flipped", "causality_added", "qualifier_dropped", "unbound_sentence",
|
|
92
|
+
"meaning_drift_confirmed", "polish_edit_rejected", "polish_hedging_decreased", "polish_text_churn", "snapshot_mismatch",
|
|
93
|
+
"join_integrity_fail", "quote_stale_version", "quote_fuzzy_match", "name_outside_quote", "scope_widened_warn",
|
|
94
|
+
"particle_unverified", "fallback_without_entity", "meaning_drift_warn", "polish_edit_unlocated", "polish_link_changed",
|
|
95
|
+
"polish_name_changed",
|
|
96
|
+
])
|
|
97
|
+
const FIDELITY_SET = new Set(FIDELITY_CODES)
|
|
98
|
+
|
|
44
99
|
const LAYOUT_PREFIX = "layout_"
|
|
45
100
|
/** Codes checkStatic can raise (G1-G17). */
|
|
46
101
|
export const STATIC_CODES = Object.freeze(
|
|
47
|
-
Object.keys(DEFECT_CODES).filter(
|
|
102
|
+
Object.keys(DEFECT_CODES).filter(
|
|
103
|
+
(code) => !code.startsWith(LAYOUT_PREFIX) && code !== "missing_promised_deliverable" && !FIDELITY_SET.has(code),
|
|
104
|
+
),
|
|
48
105
|
)
|
|
49
106
|
/** Codes checkLayout can raise (L1-L5). */
|
|
50
107
|
export const LAYOUT_CODES = Object.freeze(Object.keys(DEFECT_CODES).filter((code) => code.startsWith(LAYOUT_PREFIX)))
|
|
@@ -59,10 +116,10 @@ export const LANES = Object.freeze(["template-strict", "template-vibe", "no-form
|
|
|
59
116
|
export const STATES = Object.freeze(["none", "partial", "complete"])
|
|
60
117
|
export const DELIVERABLE_FORMATS = Object.freeze(["pdf", "docx", "html", "md", "latex", "slides", "post"])
|
|
61
118
|
export const DELIVERABLE_STATUSES = Object.freeze(["pending", "delivered", "blocked_capability", "skipped", "failed"])
|
|
62
|
-
export const GATE_NAMES = Object.freeze(["static", "layout", "visual", "proofread"])
|
|
119
|
+
export const GATE_NAMES = Object.freeze(["static", "fidelity", "layout", "visual", "proofread"])
|
|
63
120
|
export const GATE_STATUSES = Object.freeze(["pass", "fail", "not_run"])
|
|
64
121
|
export const LINEAGE_MODES = Object.freeze(["inline", "section"])
|
|
65
|
-
export const ANSWERED_BY = Object.freeze(["user", "default", "request"])
|
|
122
|
+
export const ANSWERED_BY = Object.freeze(["user", "default", "request", "pending"])
|
|
66
123
|
export const REPAIR_DEFAULTS = Object.freeze({ maxAttempts: 3, plateauLimit: 2, wallClockMs: 15 * 60 * 1000 })
|
|
67
124
|
export const DESIGN_SPEC_SECTIONS = Object.freeze([
|
|
68
125
|
"Extracted from",
|
|
@@ -134,6 +191,14 @@ export function validateOutcomeManifest(manifest) {
|
|
|
134
191
|
return { ok: errors.length === 0, errors }
|
|
135
192
|
}
|
|
136
193
|
|
|
194
|
+
/** A copy of a manifest written before a gate existed: every absent gate key becomes not_run. */
|
|
195
|
+
export function normalizeManifest(manifest) {
|
|
196
|
+
if (!isObject(manifest) || !isObject(manifest.gates)) return manifest
|
|
197
|
+
const gates = { ...manifest.gates }
|
|
198
|
+
for (const gate of GATE_NAMES) if (!Object.hasOwn(gates, gate)) gates[gate] = "not_run"
|
|
199
|
+
return { ...manifest, gates }
|
|
200
|
+
}
|
|
201
|
+
|
|
137
202
|
function isMeasure(value) {
|
|
138
203
|
return Array.isArray(value) && value.length === 3 && value.every((n) => Number.isInteger(n) && n >= 0)
|
|
139
204
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test"
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
ANSWERED_BY,
|
|
4
5
|
DEFECT_CODES,
|
|
5
6
|
DELIVERABLE_FORMATS,
|
|
6
7
|
DELIVERABLE_STATUSES,
|
|
7
8
|
DESIGN_SPEC_SECTIONS,
|
|
9
|
+
FIDELITY_CODES,
|
|
8
10
|
GATE_NAMES,
|
|
9
11
|
GATE_STATUSES,
|
|
10
12
|
INTEGRITY_CODES,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
SCHEMA_VERSION,
|
|
17
19
|
STATES,
|
|
18
20
|
STATIC_CODES,
|
|
21
|
+
normalizeManifest,
|
|
19
22
|
validateOutcomeManifest,
|
|
20
23
|
validateRepairState,
|
|
21
24
|
} from "./contracts.mjs"
|
|
@@ -49,6 +52,61 @@ const LAYOUT_CODES_EXPECTED = [
|
|
|
49
52
|
"layout_image_distorted",
|
|
50
53
|
"layout_sibling_overlap",
|
|
51
54
|
]
|
|
55
|
+
// Fidelity vocabulary contract (#11009): severity and integrity per code are fixed here.
|
|
56
|
+
const FIDELITY_BLOCKERS = [
|
|
57
|
+
"unresolved_placeholder",
|
|
58
|
+
"value_mismatch",
|
|
59
|
+
"unit_mismatch",
|
|
60
|
+
"quote_not_verbatim",
|
|
61
|
+
"claim_status_invalid",
|
|
62
|
+
"incompatible_operands",
|
|
63
|
+
"pointer_label_mismatch",
|
|
64
|
+
"quote_anchor_unresolved",
|
|
65
|
+
"unit_duplicated",
|
|
66
|
+
"polish_diff_fail",
|
|
67
|
+
"fidelity_interview_incomplete",
|
|
68
|
+
"derived_not_allowed",
|
|
69
|
+
"source_outside_floor",
|
|
70
|
+
"snapshot_hash_mismatch",
|
|
71
|
+
"chart_value_untraceable",
|
|
72
|
+
"quote_unbound",
|
|
73
|
+
"edition_unverified",
|
|
74
|
+
]
|
|
75
|
+
const FIDELITY_MAJORS = [
|
|
76
|
+
"untraceable_number",
|
|
77
|
+
"unsupported_verification",
|
|
78
|
+
"conflicting_readings",
|
|
79
|
+
"hedge_dropped",
|
|
80
|
+
"direction_flipped",
|
|
81
|
+
"causality_added",
|
|
82
|
+
"qualifier_dropped",
|
|
83
|
+
"unbound_sentence",
|
|
84
|
+
"meaning_drift_confirmed",
|
|
85
|
+
"polish_edit_rejected",
|
|
86
|
+
"polish_hedging_decreased",
|
|
87
|
+
"polish_text_churn",
|
|
88
|
+
"snapshot_mismatch",
|
|
89
|
+
"join_integrity_fail",
|
|
90
|
+
"quote_stale_version",
|
|
91
|
+
]
|
|
92
|
+
const FIDELITY_MINORS = [
|
|
93
|
+
"quote_fuzzy_match",
|
|
94
|
+
"name_outside_quote",
|
|
95
|
+
"scope_widened_warn",
|
|
96
|
+
"particle_unverified",
|
|
97
|
+
"fallback_without_entity",
|
|
98
|
+
"meaning_drift_warn",
|
|
99
|
+
"polish_edit_unlocated",
|
|
100
|
+
"polish_link_changed",
|
|
101
|
+
"polish_name_changed",
|
|
102
|
+
]
|
|
103
|
+
const FIDELITY_CODES_EXPECTED = [...FIDELITY_BLOCKERS, ...FIDELITY_MAJORS, ...FIDELITY_MINORS]
|
|
104
|
+
|
|
105
|
+
function fourGateManifest() {
|
|
106
|
+
const manifest = sampleManifest()
|
|
107
|
+
const { fidelity: _dropped, ...gates } = manifest.gates
|
|
108
|
+
return { ...manifest, gates }
|
|
109
|
+
}
|
|
52
110
|
|
|
53
111
|
function sampleManifest() {
|
|
54
112
|
return {
|
|
@@ -63,7 +121,7 @@ function sampleManifest() {
|
|
|
63
121
|
{ format: "pdf", status: "pending", promised: true, updatedAt: "2026-01-01T12:00:00.000Z" },
|
|
64
122
|
],
|
|
65
123
|
renders: [],
|
|
66
|
-
gates: { static: "not_run", layout: "not_run", visual: "not_run", proofread: "not_run" },
|
|
124
|
+
gates: { static: "not_run", fidelity: "not_run", layout: "not_run", visual: "not_run", proofread: "not_run" },
|
|
67
125
|
residualDefects: [],
|
|
68
126
|
repair: null,
|
|
69
127
|
sources: null,
|
|
@@ -72,7 +130,9 @@ function sampleManifest() {
|
|
|
72
130
|
|
|
73
131
|
describe("DEFECT_CODES", () => {
|
|
74
132
|
test("#given the code table #when every code is inspected #then it has a severity, a hint, and an integrity flag", () => {
|
|
75
|
-
expect(Object.keys(DEFECT_CODES).sort()).toEqual(
|
|
133
|
+
expect(Object.keys(DEFECT_CODES).sort()).toEqual(
|
|
134
|
+
[...STATIC_CODES_EXPECTED, "missing_promised_deliverable", ...LAYOUT_CODES_EXPECTED, ...FIDELITY_CODES_EXPECTED].sort(),
|
|
135
|
+
)
|
|
76
136
|
for (const [code, entry] of Object.entries(DEFECT_CODES)) {
|
|
77
137
|
expect(SEVERITIES.has(entry.severity), `${code} severity`).toBe(true)
|
|
78
138
|
expect(typeof entry.hint).toBe("string")
|
|
@@ -81,14 +141,17 @@ describe("DEFECT_CODES", () => {
|
|
|
81
141
|
}
|
|
82
142
|
})
|
|
83
143
|
|
|
84
|
-
test("#given the code table #when integrity codes are derived #then they are exactly the
|
|
85
|
-
expect([...INTEGRITY_CODES].sort()).toEqual(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
144
|
+
test("#given the code table #when integrity codes are derived #then they are exactly the blocker codes", () => {
|
|
145
|
+
expect([...INTEGRITY_CODES].sort()).toEqual(
|
|
146
|
+
[
|
|
147
|
+
"broken_asset_reference",
|
|
148
|
+
"missing_citation_section",
|
|
149
|
+
"missing_closing_section",
|
|
150
|
+
"missing_promised_deliverable",
|
|
151
|
+
"missing_required_section",
|
|
152
|
+
...FIDELITY_BLOCKERS,
|
|
153
|
+
].sort(),
|
|
154
|
+
)
|
|
92
155
|
for (const code of INTEGRITY_CODES) expect(DEFECT_CODES[code].severity).toBe("blocker")
|
|
93
156
|
for (const [code, entry] of Object.entries(DEFECT_CODES)) {
|
|
94
157
|
if (entry.severity === "blocker") expect(INTEGRITY_CODES.has(code), `${code} must be integrity`).toBe(true)
|
|
@@ -104,6 +167,13 @@ describe("DEFECT_CODES", () => {
|
|
|
104
167
|
}).toThrow()
|
|
105
168
|
expect(DEFECT_CODES.korean_no_keep_all.severity).toBe("major")
|
|
106
169
|
})
|
|
170
|
+
|
|
171
|
+
test("#given the fidelity codes #when inspected #then each carries its fixed severity and integrity flag", () => {
|
|
172
|
+
for (const code of FIDELITY_BLOCKERS) expect([code, DEFECT_CODES[code]?.severity, DEFECT_CODES[code]?.integrity]).toEqual([code, "blocker", true])
|
|
173
|
+
for (const code of FIDELITY_MAJORS) expect([code, DEFECT_CODES[code]?.severity, DEFECT_CODES[code]?.integrity]).toEqual([code, "major", false])
|
|
174
|
+
for (const code of FIDELITY_MINORS) expect([code, DEFECT_CODES[code]?.severity, DEFECT_CODES[code]?.integrity]).toEqual([code, "minor", false])
|
|
175
|
+
expect(DEFECT_CODES).not.toHaveProperty("incompatible_populations")
|
|
176
|
+
})
|
|
107
177
|
})
|
|
108
178
|
|
|
109
179
|
describe("code groups", () => {
|
|
@@ -111,10 +181,12 @@ describe("code groups", () => {
|
|
|
111
181
|
expect([...STATIC_CODES].sort()).toEqual(STATIC_CODES_EXPECTED.slice().sort())
|
|
112
182
|
expect([...LAYOUT_CODES].sort()).toEqual(LAYOUT_CODES_EXPECTED.slice().sort())
|
|
113
183
|
expect([...MANIFEST_CODES]).toEqual(["missing_promised_deliverable"])
|
|
114
|
-
|
|
184
|
+
expect([...FIDELITY_CODES].sort()).toEqual(FIDELITY_CODES_EXPECTED.slice().sort())
|
|
185
|
+
const union = [...STATIC_CODES, ...LAYOUT_CODES, ...MANIFEST_CODES, ...FIDELITY_CODES].sort()
|
|
115
186
|
expect(union).toEqual(Object.keys(DEFECT_CODES).sort())
|
|
116
187
|
expect(new Set(union).size).toBe(union.length)
|
|
117
188
|
expect(Object.isFrozen(STATIC_CODES)).toBe(true)
|
|
189
|
+
expect(Object.isFrozen(FIDELITY_CODES)).toBe(true)
|
|
118
190
|
})
|
|
119
191
|
})
|
|
120
192
|
|
|
@@ -124,7 +196,8 @@ describe("enums", () => {
|
|
|
124
196
|
expect(STATES).toEqual(["none", "partial", "complete"])
|
|
125
197
|
expect(DELIVERABLE_FORMATS).toEqual(["pdf", "docx", "html", "md", "latex", "slides", "post"])
|
|
126
198
|
expect(DELIVERABLE_STATUSES).toEqual(["pending", "delivered", "blocked_capability", "skipped", "failed"])
|
|
127
|
-
expect(GATE_NAMES).toEqual(["static", "layout", "visual", "proofread"])
|
|
199
|
+
expect(GATE_NAMES).toEqual(["static", "fidelity", "layout", "visual", "proofread"])
|
|
200
|
+
expect(ANSWERED_BY).toEqual(["user", "default", "request", "pending"])
|
|
128
201
|
expect(GATE_STATUSES).toEqual(["pass", "fail", "not_run"])
|
|
129
202
|
expect(LINEAGE_MODES).toEqual(["inline", "section"])
|
|
130
203
|
expect(DESIGN_SPEC_SECTIONS).toEqual([
|
|
@@ -155,6 +228,18 @@ describe("validateOutcomeManifest", () => {
|
|
|
155
228
|
expect(result.errors.join("\n")).toMatch(/deliverables\[0\]\.status/)
|
|
156
229
|
})
|
|
157
230
|
|
|
231
|
+
test("#given a manifest written before the fidelity gate #when validated raw and after normalizeManifest #then only the normalized copy passes, with fidelity not_run", () => {
|
|
232
|
+
const old = fourGateManifest()
|
|
233
|
+
expect(validateOutcomeManifest(old).ok).toBe(false)
|
|
234
|
+
const normalized = normalizeManifest(old)
|
|
235
|
+
expect(normalized.gates.fidelity).toBe("not_run")
|
|
236
|
+
expect(normalized.gates.static).toBe("not_run")
|
|
237
|
+
expect(validateOutcomeManifest(normalized)).toEqual({ ok: true, errors: [] })
|
|
238
|
+
expect(old.gates).not.toHaveProperty("fidelity")
|
|
239
|
+
const failed = { ...old, gates: { ...old.gates, visual: "fail" } }
|
|
240
|
+
expect(normalizeManifest(failed).gates.visual).toBe("fail")
|
|
241
|
+
})
|
|
242
|
+
|
|
158
243
|
test("#given a gate with an unknown value #when validated #then it fails", () => {
|
|
159
244
|
const m = sampleManifest()
|
|
160
245
|
m.gates.static = "ok"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
|
|
3
|
+
import { TRANSITIONS, addRow, emptyLedger, nextId, retractRow, supersedeRow, transitionError, validateLedger } from "./fact-ledger.mjs"
|
|
4
|
+
|
|
5
|
+
// Property-style checks of references/fact-ledger-grammar.md: every ledger the writers build is
|
|
6
|
+
// valid, and every single-invariant break the validator rejects is also refused by the writers.
|
|
7
|
+
|
|
8
|
+
type Row = { fact_id: string; status: string; kind: string; label: string; operands: string[]; derived_from: string[] }
|
|
9
|
+
type LedgerEvent = { fact_id: string; from: string; to: string; replacement?: string | null; reason?: string }
|
|
10
|
+
type Ledger = { rows: Row[]; events: LedgerEvent[] }
|
|
11
|
+
|
|
12
|
+
function measured(label: string) {
|
|
13
|
+
return {
|
|
14
|
+
kind: "measured",
|
|
15
|
+
value: "320",
|
|
16
|
+
unit: "m",
|
|
17
|
+
raw: "320 m",
|
|
18
|
+
quote: "The span is 320 m.",
|
|
19
|
+
value_span: [12, 15],
|
|
20
|
+
source_id: "S1",
|
|
21
|
+
locator: { artifact: "sources/S1/text.txt", line: 3, char_start: 40, char_end: 62 },
|
|
22
|
+
snapshot_sha256: "a".repeat(64),
|
|
23
|
+
extracted_by: "fact add --find",
|
|
24
|
+
observed_at: "2026-10-23T08:00:00Z",
|
|
25
|
+
label,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function quoteRow(label: string) {
|
|
30
|
+
return { ...measured(label), kind: "quote", value: null, unit: null, value_span: null }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function derivedRow(label: string, from: string[], kind = "derived") {
|
|
34
|
+
return {
|
|
35
|
+
kind,
|
|
36
|
+
value: "2",
|
|
37
|
+
raw: "2",
|
|
38
|
+
quote: "",
|
|
39
|
+
extracted_by: "fact derive",
|
|
40
|
+
observed_at: "2026-10-23T08:00:00Z",
|
|
41
|
+
label,
|
|
42
|
+
operation: kind === "converted" ? "convert" : "sum",
|
|
43
|
+
operands: [...from],
|
|
44
|
+
derived_from: [...from],
|
|
45
|
+
factor: kind === "converted" ? "1000" : null,
|
|
46
|
+
exact: { num: "2", den: "1" },
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** mulberry32: a fixed seed gives the same ledger on every run. */
|
|
51
|
+
function prng(seed: number) {
|
|
52
|
+
let state = seed >>> 0
|
|
53
|
+
return () => {
|
|
54
|
+
state = (state + 0x6d2b79f5) >>> 0
|
|
55
|
+
let t = state
|
|
56
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
57
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
58
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pick<T>(random: () => number, list: T[]): T {
|
|
63
|
+
return list[Math.floor(random() * list.length)]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ids = (ledger: Ledger, keep: (row: Row) => boolean) => ledger.rows.filter(keep).map((row) => row.fact_id)
|
|
67
|
+
|
|
68
|
+
/** Writes the writers must refuse on any ledger; each returns nothing and must throw. */
|
|
69
|
+
function illegalWrites(ledger: Ledger, random: () => number): (() => unknown)[] {
|
|
70
|
+
const active = ids(ledger, (row) => row.status === "active")
|
|
71
|
+
const ended = ids(ledger, (row) => row.status !== "active")
|
|
72
|
+
const writes: (() => unknown)[] = [() => addRow(ledger, derivedRow("dangling", ["F999"])), () => addRow(ledger, measured(pick(random, ledger.rows).label))]
|
|
73
|
+
if (active.length > 0) {
|
|
74
|
+
const id = pick(random, active)
|
|
75
|
+
writes.push(() => supersedeRow(ledger, id, id, "self"), () => supersedeRow(ledger, id, null, "none"), () => retractRow(ledger, id, ""))
|
|
76
|
+
if (ended.length > 0) writes.push(() => supersedeRow(ledger, id, pick(random, ended), "to an ended row"))
|
|
77
|
+
}
|
|
78
|
+
if (ended.length > 0) writes.push(() => retractRow(ledger, pick(random, ended), "again"))
|
|
79
|
+
return writes
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Random addRow / retractRow / supersedeRow sequences from an empty ledger, with refused writes mixed in. */
|
|
83
|
+
function randomLedger(seed: number) {
|
|
84
|
+
const random = prng(seed)
|
|
85
|
+
const ledger: Ledger = emptyLedger()
|
|
86
|
+
const steps = 4 + Math.floor(random() * 14)
|
|
87
|
+
for (let step = 0; step < steps; step += 1) {
|
|
88
|
+
const active = ids(ledger, (row) => row.status === "active")
|
|
89
|
+
const values = ids(ledger, (row) => row.fact_id.startsWith("F"))
|
|
90
|
+
const roll = random()
|
|
91
|
+
if (roll < 0.3 || values.length < 2) addRow(ledger, measured(`m${step}`))
|
|
92
|
+
else if (roll < 0.4) addRow(ledger, quoteRow(`q${step}`))
|
|
93
|
+
else if (roll < 0.6) addRow(ledger, derivedRow(`d${step}`, [...new Set([pick(random, values), pick(random, values)])], pick(random, ["derived", "converted"])))
|
|
94
|
+
else if (roll < 0.8 && active.length > 0) retractRow(ledger, pick(random, active), "wrong page")
|
|
95
|
+
else if (active.length > 1) {
|
|
96
|
+
const id = pick(random, active)
|
|
97
|
+
supersedeRow(ledger, id, pick(random, active.filter((other: string) => other !== id)), "newer filing")
|
|
98
|
+
}
|
|
99
|
+
const before = JSON.stringify(ledger)
|
|
100
|
+
const write = pick(random, illegalWrites(ledger, random))
|
|
101
|
+
expect(write).toThrow()
|
|
102
|
+
expect(JSON.stringify(ledger)).toBe(before)
|
|
103
|
+
}
|
|
104
|
+
return ledger
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const LEDGERS = Array.from({ length: 200 }, (_, index) => randomLedger(index + 1))
|
|
108
|
+
|
|
109
|
+
describe("every ledger the writers build is valid", () => {
|
|
110
|
+
test("200 seeded random write sequences all validate, and refused writes leave the ledger unchanged", () => {
|
|
111
|
+
for (const ledger of LEDGERS) expect(validateLedger(ledger)).toEqual([])
|
|
112
|
+
const events = LEDGERS.flatMap((ledger) => ledger.events)
|
|
113
|
+
expect(events.some((event) => event.to === "superseded")).toBe(true)
|
|
114
|
+
expect(events.some((event) => event.to === "retracted")).toBe(true)
|
|
115
|
+
expect(LEDGERS.some((ledger) => ledger.rows.some((row) => row.kind === "converted"))).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
type Break = { name: string; mutate: (ledger: Ledger) => string | null; rejects: RegExp; write: (ledger: Ledger) => unknown; refuses: RegExp }
|
|
120
|
+
|
|
121
|
+
const firstEvent = (ledger: Ledger, to?: string) => ledger.events.find((event) => to === undefined || event.to === to) as LedgerEvent
|
|
122
|
+
const firstRow = (ledger: Ledger, keep: (row: Row) => boolean) => ledger.rows.find(keep) as Row
|
|
123
|
+
const activeId = (ledger: Ledger) => firstRow(ledger, (row) => row.status === "active")?.fact_id
|
|
124
|
+
|
|
125
|
+
const BREAKS: Break[] = [
|
|
126
|
+
{ name: "superseded event with a null replacement", mutate: (l) => { const e = firstEvent(l, "superseded"); if (!e || !activeId(l)) return null; e.replacement = null; return e.fact_id }, rejects: /needs a replacement row/, write: (l) => supersedeRow(l, activeId(l), null, "x"), refuses: /needs a replacement row/ },
|
|
127
|
+
{ name: "self replacement", mutate: (l) => { const e = firstEvent(l, "superseded"); if (!e || !activeId(l)) return null; e.replacement = e.fact_id; return e.fact_id }, rejects: /cannot be its own replacement/, write: (l) => supersedeRow(l, activeId(l), activeId(l), "x"), refuses: /cannot be its own replacement/ },
|
|
128
|
+
{ name: "duplicate transition", mutate: (l) => { const e = firstEvent(l); if (!e) return null; l.events.push({ ...e }); return e.fact_id }, rejects: /but the event moves it from active/, write: (l) => retractRow(l, firstEvent(l).fact_id, "x"), refuses: /cannot move from (retracted|superseded)/ },
|
|
129
|
+
{ name: "from mismatch", mutate: (l) => { const e = firstEvent(l); if (!e) return null; e.from = e.to; return e.fact_id }, rejects: /is active, but the event moves it from/, write: (l) => supersedeRow(l, firstEvent(l).fact_id, activeId(l) ?? "F1", "x"), refuses: /cannot move from|own replacement/ },
|
|
130
|
+
{ name: "active row with events", mutate: (l) => { const r = firstRow(l, (row) => row.status !== "active"); if (!r) return null; r.status = "active"; return r.fact_id }, rejects: /has status active but its last event moves it to/, write: (l) => { const r = firstRow(l, (row) => row.status !== "active"); const problem = transitionError(new Map([[r.fact_id, r.status]]), { fact_id: r.fact_id, from: r.status, to: "active", replacement: null, reason: "x" }); if (problem) throw new Error(problem) }, refuses: /cannot move from (retracted|superseded) to active/ },
|
|
131
|
+
{ name: "non-active row without events", mutate: (l) => { const e = firstEvent(l); if (!e) return null; l.events = l.events.filter((event) => event.fact_id !== e.fact_id); return e.fact_id }, rejects: /but has no status event/, write: (l) => addRow(l, { ...measured("fresh"), status: "retracted" }), refuses: /but has no status event/ },
|
|
132
|
+
{ name: "dangling operand", mutate: (l) => { const r = firstRow(l, (row) => row.operands.length > 0); if (!r) return null; r.operands[0] = "F999"; return r.fact_id }, rejects: /references F999, which has no ledger row/, write: (l) => addRow(l, derivedRow("fresh", ["F999"])), refuses: /references F999, which has no ledger row/ },
|
|
133
|
+
{ name: "self derived_from", mutate: (l) => { const r = firstRow(l, (row) => row.derived_from.length > 0); if (!r) return null; r.derived_from.push(r.fact_id); return r.fact_id }, rejects: /references itself/, write: (l) => addRow(l, { ...derivedRow("fresh", []), derived_from: [nextId(l, "derived")] }), refuses: /references itself/ },
|
|
134
|
+
{ name: "cyclic derived_from", mutate: (l) => { const r = firstRow(l, (row) => row.derived_from.length > 0); if (!r) return null; firstRow(l, (row) => row.fact_id === r.derived_from[0]).derived_from.push(r.fact_id); return r.fact_id }, rejects: /derives from itself through/, write: (l) => addRow(l, derivedRow("fresh", [`F${l.rows.length + 5}`])), refuses: /which has no ledger row/ },
|
|
135
|
+
{ name: "duplicate label", mutate: (l) => { if (l.rows.length < 2) return null; l.rows[1].label = l.rows[0].label; return l.rows[1].fact_id }, rejects: /has duplicate label/, write: (l) => addRow(l, measured(l.rows[0].label)), refuses: /has duplicate label/ },
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
describe("every single-invariant break is rejected by the validator and refused by the writers", () => {
|
|
139
|
+
for (const broken of BREAKS) {
|
|
140
|
+
test(broken.name, () => {
|
|
141
|
+
let applied = 0
|
|
142
|
+
for (const valid of LEDGERS) {
|
|
143
|
+
const ledger = structuredClone(valid)
|
|
144
|
+
const factId = broken.mutate(ledger)
|
|
145
|
+
if (factId === null) continue
|
|
146
|
+
applied += 1
|
|
147
|
+
const errors = validateLedger(ledger)
|
|
148
|
+
expect(errors.some((error: string) => broken.rejects.test(error) && error.includes(factId))).toBe(true)
|
|
149
|
+
const untouched = structuredClone(valid)
|
|
150
|
+
expect(() => broken.write(untouched)).toThrow(broken.refuses)
|
|
151
|
+
expect(untouched).toEqual(valid)
|
|
152
|
+
}
|
|
153
|
+
expect(applied).toBeGreaterThan(10)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
test("no transition leads back to active", () => {
|
|
158
|
+
for (const targets of Object.values(TRANSITIONS)) expect(targets).not.toContain("active")
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
function twoRows() {
|
|
163
|
+
const ledger = emptyLedger()
|
|
164
|
+
addRow(ledger, measured("a"))
|
|
165
|
+
addRow(ledger, measured("b"))
|
|
166
|
+
return ledger
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
describe("validateLedger is as strict as the writers (review r5 MEDIUM)", () => {
|
|
170
|
+
test("a superseded event with a null replacement is rejected", () => {
|
|
171
|
+
const ledger = twoRows()
|
|
172
|
+
supersedeRow(ledger, "F1", "F2", "newer filing")
|
|
173
|
+
ledger.events[0].replacement = null
|
|
174
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("F1") && e.includes("replacement"))).toBe(true)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test("a row superseded by itself is rejected", () => {
|
|
178
|
+
const ledger = twoRows()
|
|
179
|
+
supersedeRow(ledger, "F1", "F2", "newer filing")
|
|
180
|
+
ledger.events[0].replacement = "F1"
|
|
181
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("F1") && e.includes("replacement"))).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test("two events that both move F1 from active to retracted are rejected", () => {
|
|
185
|
+
const ledger = twoRows()
|
|
186
|
+
retractRow(ledger, "F1", "wrong page")
|
|
187
|
+
ledger.events.push({ ...ledger.events[0] })
|
|
188
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("events[1]") && e.includes("F1"))).toBe(true)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// fact-ledger-kinds.mjs - the per-kind column rules of the fact ledger as one table, KIND_RULES.
|
|
2
|
+
// validateLedger checks every per-kind column through kindRuleErrors, the writers read their
|
|
3
|
+
// per-kind choices (id prefix, measured exact) from it, and the per-kind table in
|
|
4
|
+
// references/fact-ledger-grammar.md is rendered from it (`node fact-ledger-kinds.mjs --render-grammar`).
|
|
5
|
+
// Also the value primitives the schema shares. Pure functions with no IO; node: builtins only.
|
|
6
|
+
|
|
7
|
+
import { isCliEntry } from "./entry-guard.mjs"
|
|
8
|
+
|
|
9
|
+
export const DECIMAL = /^-?\d+(?:\.\d+)?$/
|
|
10
|
+
const INTEGER = /^-?\d+$/
|
|
11
|
+
const SHA256 = /^[0-9a-f]{64}$/
|
|
12
|
+
const SOURCE_ID = /^S\d+$/
|
|
13
|
+
|
|
14
|
+
export const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
15
|
+
export const isString = (value) => typeof value === "string"
|
|
16
|
+
export const isNonEmpty = (value) => typeof value === "string" && value.length > 0
|
|
17
|
+
export const isCount = (value) => Number.isInteger(value) && value >= 0
|
|
18
|
+
const isDecimal = (value) => isString(value) && DECIMAL.test(value)
|
|
19
|
+
const isRational = (exact) => isObject(exact) && isString(exact.num) && isString(exact.den) && INTEGER.test(exact.num) && /^\d+$/.test(exact.den) && !/^0+$/.test(exact.den)
|
|
20
|
+
|
|
21
|
+
/** The exact rational of a decimal string, reduced. */
|
|
22
|
+
export function exactFromDecimal(value) {
|
|
23
|
+
const [whole, fraction = ""] = value.replace(/^-/, "").split(".")
|
|
24
|
+
let num = BigInt(whole + fraction)
|
|
25
|
+
let den = 10n ** BigInt(fraction.length)
|
|
26
|
+
let a = num
|
|
27
|
+
let b = den
|
|
28
|
+
while (b !== 0n) [a, b] = [b, a % b]
|
|
29
|
+
if (a > 1n) [num, den] = [num / a, den / a]
|
|
30
|
+
return { num: `${value.startsWith("-") && num !== 0n ? "-" : ""}${num}`, den: `${den}` }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @returns {string[]} what is wrong with a locator object, each message prefixed with `at`. */
|
|
34
|
+
export function locatorErrors(locator, at) {
|
|
35
|
+
const errors = []
|
|
36
|
+
if (!isObject(locator)) return errors
|
|
37
|
+
if (!isNonEmpty(locator.artifact)) errors.push(`${at}.artifact must be a non-empty string`)
|
|
38
|
+
if (!(isCount(locator.char_start) && isCount(locator.char_end) && locator.char_start <= locator.char_end)) errors.push(`${at}.char_start/char_end must be integers with start <= end`)
|
|
39
|
+
for (const key of ["page", "line"]) {
|
|
40
|
+
if (locator[key] !== undefined && !(Number.isInteger(locator[key]) && locator[key] >= 1)) errors.push(`${at}.${key} must be an integer >= 1`)
|
|
41
|
+
}
|
|
42
|
+
for (const key of ["table", "row_label", "col_label"]) {
|
|
43
|
+
if (locator[key] !== undefined && !isString(locator[key])) errors.push(`${at}.${key} must be a string`)
|
|
44
|
+
}
|
|
45
|
+
return errors
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isLocator = (value) => isObject(value) && locatorErrors(value, "").length === 0
|
|
49
|
+
const orNull = (test) => (value, row) => value === null || test(value, row)
|
|
50
|
+
const locatorDetail = (value, at) => locatorErrors(value, at)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Every rule a KIND_RULES cell may name: `cell` is its text in the grammar table, `says` ends the
|
|
54
|
+
* error message, `test(value, row)` decides it, and `prefix` (id rules only) is what nextId uses.
|
|
55
|
+
*/
|
|
56
|
+
export const RULES = Object.freeze({
|
|
57
|
+
f_id: { cell: "`F<n>`", says: "F<n>", prefix: "F", test: (v) => isString(v) && /^F\d+$/.test(v) },
|
|
58
|
+
q_id: { cell: "`Q<n>`", says: "Q<n>", prefix: "Q", test: (v) => isString(v) && /^Q\d+$/.test(v) },
|
|
59
|
+
decimal: { cell: "decimal", says: "a decimal string", test: isDecimal },
|
|
60
|
+
null: { cell: "null", says: "null", test: (v) => v === null },
|
|
61
|
+
source: { cell: "`S<n>`", says: "S<n>", test: (v) => isString(v) && SOURCE_ID.test(v) },
|
|
62
|
+
source_or_null: { cell: "`S<n>` or null", says: "S<n> or null", test: orNull((v) => isString(v) && SOURCE_ID.test(v)) },
|
|
63
|
+
locator: { cell: "object", says: "a locator object", test: isLocator, detail: locatorDetail },
|
|
64
|
+
locator_or_null: { cell: "object or null", says: "a locator object or null", test: orNull(isLocator), detail: locatorDetail },
|
|
65
|
+
sha256: { cell: "sha256", says: "a sha256 hex digest", test: (v) => isString(v) && SHA256.test(v) },
|
|
66
|
+
sha256_or_null: { cell: "sha256 or null", says: "a sha256 hex digest or null", test: orNull((v) => isString(v) && SHA256.test(v)) },
|
|
67
|
+
string_or_null: { cell: "string or null", says: "null or a string", test: orNull(isString) },
|
|
68
|
+
non_empty: { cell: "non-empty", says: "a non-empty string (required for this kind)", test: isNonEmpty },
|
|
69
|
+
from_value: {
|
|
70
|
+
cell: "from `value`",
|
|
71
|
+
says: "the reduced exact rational of value",
|
|
72
|
+
test: (v, row) => isRational(v) && (!isDecimal(row.value) || (v.num === exactFromDecimal(row.value).num && v.den === exactFromDecimal(row.value).den)),
|
|
73
|
+
},
|
|
74
|
+
rational_or_null: { cell: "rational or null", says: "null or { num: integer string, den: positive integer string }", test: orNull(isRational) },
|
|
75
|
+
by_derive: { cell: "written by derive (rational or null)", says: "null or { num: integer string, den: positive integer string }", test: orNull(isRational) },
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
/** The grammar's per-kind columns, in table order; `exact` follows `value` because it reads it. */
|
|
79
|
+
export const KIND_COLUMNS = Object.freeze(["fact_id", "value", "source_id", "locator", "snapshot_sha256", "rationale", "exact"])
|
|
80
|
+
|
|
81
|
+
const sourced = { source_id: "source", locator: "locator", snapshot_sha256: "sha256" }
|
|
82
|
+
const unsourced = { source_id: "source_or_null", locator: "locator_or_null", snapshot_sha256: "sha256_or_null" }
|
|
83
|
+
const row = (cells) => Object.freeze(Object.fromEntries(KIND_COLUMNS.map((column) => [column, cells[column]])))
|
|
84
|
+
|
|
85
|
+
export const KIND_RULES = Object.freeze({
|
|
86
|
+
measured: row({ fact_id: "f_id", value: "decimal", ...sourced, rationale: "string_or_null", exact: "from_value" }),
|
|
87
|
+
quote: row({ fact_id: "q_id", value: "null", ...sourced, rationale: "string_or_null", exact: "null" }),
|
|
88
|
+
derived: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "string_or_null", exact: "by_derive" }),
|
|
89
|
+
assumed: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "non_empty", exact: "rational_or_null" }),
|
|
90
|
+
converted: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "string_or_null", exact: "by_derive" }),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
/** The id prefix nextId assigns for a kind; an unknown kind gets F and fails validation later. */
|
|
94
|
+
export function idPrefix(kind) {
|
|
95
|
+
return RULES[KIND_RULES[kind]?.fact_id ?? "f_id"].prefix
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Whether addRow computes the row's exact from its decimal value. */
|
|
99
|
+
export function exactComesFromValue(kind) {
|
|
100
|
+
return KIND_RULES[kind]?.exact === "from_value"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @returns {string[]} every per-kind column error of one row; empty for an unknown kind. */
|
|
104
|
+
export function kindRuleErrors(candidate, at) {
|
|
105
|
+
const rules = KIND_RULES[candidate.kind]
|
|
106
|
+
if (rules === undefined) return []
|
|
107
|
+
const errors = []
|
|
108
|
+
for (const column of KIND_COLUMNS) {
|
|
109
|
+
const value = candidate[column]
|
|
110
|
+
if (value === undefined) continue
|
|
111
|
+
const rule = RULES[rules[column]]
|
|
112
|
+
if (rule.test(value, candidate)) continue
|
|
113
|
+
errors.push(`${at}.${column} must be ${rule.says} for a ${candidate.kind} row`)
|
|
114
|
+
if (rule.detail) errors.push(...rule.detail(value, `${at}.${column}`))
|
|
115
|
+
}
|
|
116
|
+
return errors
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const KIND_TABLE_BEGIN = "<!-- kind-rules:begin (rendered from KIND_RULES; do not edit by hand) -->"
|
|
120
|
+
export const KIND_TABLE_END = "<!-- kind-rules:end -->"
|
|
121
|
+
|
|
122
|
+
/** The grammar's per-kind Markdown table, one line per column, ending in a newline. */
|
|
123
|
+
export function renderKindTable() {
|
|
124
|
+
const kinds = Object.keys(KIND_RULES)
|
|
125
|
+
const lines = [`| field | ${kinds.join(" | ")} |`, `|${"---|".repeat(kinds.length + 1)}`]
|
|
126
|
+
for (const column of KIND_COLUMNS) lines.push(`| \`${column}\` | ${kinds.map((kind) => RULES[KIND_RULES[kind][column]].cell).join(" | ")} |`)
|
|
127
|
+
return `${lines.join("\n")}\n`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (isCliEntry(import.meta.url)) {
|
|
131
|
+
if (process.argv[2] === "--render-grammar" && process.argv.length === 3) process.stdout.write(renderKindTable())
|
|
132
|
+
else {
|
|
133
|
+
process.stderr.write("usage: node fact-ledger-kinds.mjs --render-grammar\n")
|
|
134
|
+
process.exitCode = 2
|
|
135
|
+
}
|
|
136
|
+
}
|