lazycodex-ai 5.1.44 → 5.1.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +948 -416
- package/dist/cli/service-auth/keystore.d.ts +4 -2
- package/dist/cli-node/index.js +948 -416
- package/docs/reference/web-terminal-visual-qa.md +8 -0
- package/package.json +1 -1
- package/packages/lsp-daemon/dist/cli.js +4 -4
- package/packages/lsp-daemon/dist/client.js +4 -4
- package/packages/lsp-daemon/dist/index.js +4 -4
- package/packages/lsp-daemon/package.json +3 -3
- package/packages/lsp-tools-mcp/dist/cli.js +4 -4
- package/packages/lsp-tools-mcp/dist/mcp.js +4 -4
- package/packages/lsp-tools-mcp/dist/tools.js +4 -4
- package/packages/lsp-tools-mcp/package.json +3 -3
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +62 -21
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/biome.json +1 -21
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/biome.json +1 -21
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/biome.json +1 -21
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +4 -4
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +0 -1
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +1 -1
- package/packages/omo-codex/plugin/components/rules/biome.json +1 -21
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +4 -4
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/biome.json +1 -21
- package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +90 -1
- package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +91 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +7 -7
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +2 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +7 -7
- package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +2 -0
- package/packages/omo-codex/plugin/skills/browser/SKILL.md +21 -22
- package/packages/omo-codex/plugin/skills/browser/references/commands.md +2 -9
- package/packages/omo-codex/plugin/skills/browser/references/install.md +1 -2
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +2 -2
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +63 -487
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +1 -1
- package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +1 -10
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +79 -118
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +75 -224
- package/packages/omo-codex/plugin/skills/frontend/references/design/stylegallery.md +32 -27
- package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -2
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/collect-files.ts +53 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +10 -7
- package/packages/omo-codex/plugin/skills/programming/references/typescript/README.md +10 -7
- package/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md +9 -23
- package/packages/omo-codex/plugin/skills/programming/references/typescript/oxc-lint.md +130 -0
- package/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md +6 -62
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/new-project.ts +20 -81
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +14 -14
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +9 -3
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +7 -6
- package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +51 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/claims-index.mjs +125 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +68 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +1 -27
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/lexicon.mjs +47 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/numerals.mjs +226 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +2 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/particles-ko.mjs +108 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/rational.mjs +139 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/sentence-split.mjs +77 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-normalize.mjs +82 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-similarity.mjs +28 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +105 -0
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +71 -64
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +4 -1
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +6 -6
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +152 -23
- package/packages/shared-skills/skills/browser/SKILL.md +21 -22
- package/packages/shared-skills/skills/browser/references/commands.md +2 -9
- package/packages/shared-skills/skills/browser/references/install.md +1 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +2 -2
- package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +63 -487
- package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +1 -1
- package/packages/shared-skills/skills/browser/scripts/omowright.mjs +1 -10
- package/packages/shared-skills/skills/frontend/SKILL.md +79 -118
- package/packages/shared-skills/skills/frontend/references/design/README.md +75 -224
- package/packages/shared-skills/skills/frontend/references/design/stylegallery.md +32 -27
- package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -2
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.test.ts +101 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.test.ts +50 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.ts +53 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
- package/packages/shared-skills/skills/programming/SKILL.md +10 -7
- package/packages/shared-skills/skills/programming/references/typescript/README.md +10 -7
- package/packages/shared-skills/skills/programming/references/typescript/bootstrap.md +9 -23
- package/packages/shared-skills/skills/programming/references/typescript/oxc-lint.md +130 -0
- package/packages/shared-skills/skills/programming/references/typescript/tsconfig-strict.md +6 -62
- package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
- package/packages/shared-skills/skills/programming/scripts/typescript/new-project.test.ts +162 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/new-project.ts +20 -81
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
- package/packages/shared-skills/skills/refactor/SKILL.md +13 -13
- package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
- package/packages/shared-skills/skills/ulw-research/AGENTS.md +9 -3
- package/packages/shared-skills/skills/ulw-research/SKILL.md +2 -2
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +7 -6
- package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
- package/packages/shared-skills/skills/ulw-research/references/report-gates.md +51 -3
- package/packages/shared-skills/skills/ulw-research/scripts/claims-index.mjs +125 -0
- package/packages/shared-skills/skills/ulw-research/scripts/claims-index.test.ts +149 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +68 -3
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +97 -12
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +190 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +141 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.test.ts +43 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +252 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.test.ts +179 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.test.ts +46 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-context.test.ts +64 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +1 -27
- package/packages/shared-skills/skills/ulw-research/scripts/lexicon.mjs +47 -0
- package/packages/shared-skills/skills/ulw-research/scripts/lexicon.test.ts +57 -0
- package/packages/shared-skills/skills/ulw-research/scripts/numerals.mjs +226 -0
- package/packages/shared-skills/skills/ulw-research/scripts/numerals.test.ts +120 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +2 -3
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +29 -0
- package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.mjs +108 -0
- package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.test.ts +86 -0
- package/packages/shared-skills/skills/ulw-research/scripts/rational.mjs +139 -0
- package/packages/shared-skills/skills/ulw-research/scripts/rational.test.ts +46 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
- package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.mjs +77 -0
- package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.test.ts +109 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/claim-graph.md +13 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-locked.md +17 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-pending.md +17 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.mjs +82 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.test.ts +73 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.mjs +28 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.test.ts +38 -0
- package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +105 -0
- package/packages/shared-skills/skills/ulw-research/scripts/units.test.ts +83 -0
- package/packages/shared-skills/skills/visual-qa/SKILL.md +70 -63
- package/packages/shared-skills/skills/visual-qa/scripts/skill-doc-fields.test.ts +34 -0
- package/script/qa/web-terminal-visual-qa.mjs +6 -2
- package/script/qa/xterm-live-terminal.mjs +26 -3
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// claims-index.mjs - deterministic index of claim-graph.md: the one Markdown table whose header
|
|
2
|
+
// names claim_id, statement, risk tier, and status, plus the ids listed under the
|
|
3
|
+
// `## verified-claims` digest. Pure function of the text; node: builtins only.
|
|
4
|
+
|
|
5
|
+
const CLAIM_ID = /^C\d+$/
|
|
6
|
+
const REQUIRED_COLUMNS = Object.freeze({ claim_id: "claim_id", statement: "statement", risk: "risk tier", status: "status" })
|
|
7
|
+
|
|
8
|
+
/** Split one table line on unescaped pipes; drops the leading and trailing border cells. */
|
|
9
|
+
function splitRow(line) {
|
|
10
|
+
const cells = []
|
|
11
|
+
let current = ""
|
|
12
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
13
|
+
const char = line[index]
|
|
14
|
+
if (char === "\\" && line[index + 1] === "|") {
|
|
15
|
+
current += "|"
|
|
16
|
+
index += 1
|
|
17
|
+
} else if (char === "|") {
|
|
18
|
+
cells.push(current)
|
|
19
|
+
current = ""
|
|
20
|
+
} else current += char
|
|
21
|
+
}
|
|
22
|
+
cells.push(current)
|
|
23
|
+
const trimmed = cells.map((value) => value.trim())
|
|
24
|
+
if (line.trim().startsWith("|")) trimmed.shift()
|
|
25
|
+
if (line.trim().endsWith("|")) trimmed.pop()
|
|
26
|
+
return trimmed
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeHeader(value) {
|
|
30
|
+
return value.replace(/`/g, "").trim().toLowerCase().replace(/[\s_-]+/g, " ")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function columnIndexes(header) {
|
|
34
|
+
const names = header.map(normalizeHeader)
|
|
35
|
+
const indexes = {}
|
|
36
|
+
for (const [key, name] of Object.entries(REQUIRED_COLUMNS)) indexes[key] = names.indexOf(normalizeHeader(name))
|
|
37
|
+
return Object.values(indexes).every((index) => index >= 0) ? indexes : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isSeparator(line) {
|
|
41
|
+
return /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DIGEST_HEADING = normalizeHeader("verified-claims")
|
|
45
|
+
|
|
46
|
+
/** `offset` is the index of `lines[0]` in the document, for line numbers in errors. */
|
|
47
|
+
function verifiedIds(lines, offset, errors) {
|
|
48
|
+
const ids = []
|
|
49
|
+
let inside = false
|
|
50
|
+
for (const [index, line] of lines.entries()) {
|
|
51
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line)
|
|
52
|
+
if (heading) {
|
|
53
|
+
const name = normalizeHeader(heading[2])
|
|
54
|
+
inside = name === DIGEST_HEADING
|
|
55
|
+
if (!inside && name.startsWith(DIGEST_HEADING)) {
|
|
56
|
+
errors.push(`line ${offset + index + 1}: heading "${line.trim()}" is not the digest heading "## verified-claims"; drop the suffix so its entries are read`)
|
|
57
|
+
}
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if (!inside) continue
|
|
61
|
+
// An entry is a list item or a table row; prose lines never verify. Only the id an entry
|
|
62
|
+
// starts with is verified; ids mentioned later in the line are not.
|
|
63
|
+
const leading = /^\s*(?:(?:[-*+]|\d+[.)])\s+|\|\s*)`?(C\d+)\b/.exec(line)
|
|
64
|
+
if (leading && !ids.includes(leading[1])) ids.push(leading[1])
|
|
65
|
+
}
|
|
66
|
+
return ids
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Blank every line of a fenced code block (``` or ~~~, fences included) so nothing in it is read. */
|
|
70
|
+
function unfenced(lines) {
|
|
71
|
+
let fence = null
|
|
72
|
+
return lines.map((line) => {
|
|
73
|
+
const marker = /^\s{0,3}(`{3,}|~{3,})/.exec(line)
|
|
74
|
+
if (fence === null) {
|
|
75
|
+
if (marker) fence = marker[1]
|
|
76
|
+
return marker ? "" : line
|
|
77
|
+
}
|
|
78
|
+
const closes = marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && line.trim() === marker[1]
|
|
79
|
+
if (closes) fence = null
|
|
80
|
+
return ""
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @returns {{ claims: { claim_id: string, statement: string, risk: string, status: string, in_verified: boolean }[], errors: string[] }} */
|
|
85
|
+
export function indexClaims(markdown) {
|
|
86
|
+
const lines = unfenced(markdown.split(/\r?\n/))
|
|
87
|
+
const errors = []
|
|
88
|
+
const headerAt = lines.findIndex((line) => line.trim().startsWith("|") && columnIndexes(splitRow(line)) !== null)
|
|
89
|
+
if (headerAt < 0) {
|
|
90
|
+
return { claims: [], errors: ["no claim table: a Markdown table with claim_id, statement, risk tier, and status columns is required"] }
|
|
91
|
+
}
|
|
92
|
+
const header = splitRow(lines[headerAt])
|
|
93
|
+
const columns = columnIndexes(header)
|
|
94
|
+
const hasSeparator = isSeparator(lines[headerAt + 1] ?? "")
|
|
95
|
+
if (!hasSeparator) errors.push(`line ${headerAt + 2}: claim table header must be followed by a separator row`)
|
|
96
|
+
const firstRow = headerAt + (hasSeparator ? 2 : 1)
|
|
97
|
+
const rows = []
|
|
98
|
+
let tableEnd = firstRow
|
|
99
|
+
for (let index = firstRow; index < lines.length && lines[index].trim().startsWith("|"); index += 1) {
|
|
100
|
+
tableEnd = index + 1
|
|
101
|
+
const cells = splitRow(lines[index])
|
|
102
|
+
const at = `line ${index + 1}`
|
|
103
|
+
if (cells.length !== header.length) {
|
|
104
|
+
errors.push(`${at}: expected ${header.length} cells, found ${cells.length}`)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
const claimId = cells[columns.claim_id].replace(/`/g, "")
|
|
108
|
+
if (!CLAIM_ID.test(claimId)) {
|
|
109
|
+
errors.push(`${at}: claim_id "${claimId}" must match C<n>`)
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
if (rows.some((row) => row.claim_id === claimId)) {
|
|
113
|
+
errors.push(`${at}: duplicate claim_id ${claimId}`)
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
rows.push({ claim_id: claimId, statement: cells[columns.statement], risk: cells[columns.risk], status: cells[columns.status] })
|
|
117
|
+
}
|
|
118
|
+
// The digest section ends at the claim table: scan each side on its own so it cannot carry across.
|
|
119
|
+
const verified = [...new Set([...verifiedIds(lines.slice(0, headerAt), 0, errors), ...verifiedIds(lines.slice(tableEnd), tableEnd, errors)])]
|
|
120
|
+
for (const id of verified) {
|
|
121
|
+
if (!rows.some((row) => row.claim_id === id)) errors.push(`verified-claims lists ${id}, which has no claim row`)
|
|
122
|
+
}
|
|
123
|
+
const claims = rows.map((row) => ({ ...row, in_verified: verified.includes(row.claim_id) }))
|
|
124
|
+
return { claims, errors }
|
|
125
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// fact-ledger-schema.mjs - the fact ledger's vocabulary and validation: row fields and defaults,
|
|
2
|
+
// kinds, statuses, qualifiers, and validateLedger over rows, status events, and their history.
|
|
3
|
+
// The grammar it encodes is references/fact-ledger-grammar.md; every per-kind column rule lives in
|
|
4
|
+
// KIND_RULES (fact-ledger-kinds.mjs), never here.
|
|
5
|
+
// Pure functions with no IO; fact-ledger.mjs owns reading, writing, and row changes. node: builtins only.
|
|
6
|
+
|
|
7
|
+
import { DECIMAL, isCount, isNonEmpty, isObject, isString, kindRuleErrors } from "./fact-ledger-kinds.mjs"
|
|
8
|
+
|
|
9
|
+
export { DECIMAL, isNonEmpty, isString } from "./fact-ledger-kinds.mjs"
|
|
10
|
+
|
|
11
|
+
export const LEDGER_SCHEMA_VERSION = 1
|
|
12
|
+
export const FACT_KINDS = Object.freeze(["measured", "quote", "derived", "assumed", "converted"])
|
|
13
|
+
export const FACT_STATUSES = Object.freeze(["active", "superseded", "retracted"])
|
|
14
|
+
export const OPERATIONS = Object.freeze(["sum", "diff", "growth", "ratio", "share", "convert", "expr"])
|
|
15
|
+
/** Canonical qualifier terms; surface forms in either language map onto these. */
|
|
16
|
+
export const QUALIFIERS = Object.freeze([
|
|
17
|
+
"upper_inclusive",
|
|
18
|
+
"lower_inclusive",
|
|
19
|
+
"approximate",
|
|
20
|
+
"near",
|
|
21
|
+
"lower_exclusive",
|
|
22
|
+
"upper_exclusive",
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
export const ROW_DEFAULTS = Object.freeze({
|
|
26
|
+
value: null,
|
|
27
|
+
unit: null,
|
|
28
|
+
value_span: null,
|
|
29
|
+
source_id: null,
|
|
30
|
+
locator: null,
|
|
31
|
+
locate_hint: null,
|
|
32
|
+
snapshot_sha256: null,
|
|
33
|
+
valid_at: null,
|
|
34
|
+
qualifier: null,
|
|
35
|
+
entity: null,
|
|
36
|
+
sentence_id: null,
|
|
37
|
+
observation_id: null,
|
|
38
|
+
fuzzy: false,
|
|
39
|
+
outside_floor: false,
|
|
40
|
+
population: null,
|
|
41
|
+
missing_rule: null,
|
|
42
|
+
accounting_basis: null,
|
|
43
|
+
snapshot_window: null,
|
|
44
|
+
revision: null,
|
|
45
|
+
exact: null,
|
|
46
|
+
render: null,
|
|
47
|
+
operation: null,
|
|
48
|
+
operands: [],
|
|
49
|
+
derived_from: [],
|
|
50
|
+
formula: null,
|
|
51
|
+
factor: null,
|
|
52
|
+
rationale: null,
|
|
53
|
+
status: "active",
|
|
54
|
+
})
|
|
55
|
+
const REQUIRED = ["fact_id", "kind", "raw", "quote", "extracted_by", "observed_at", "label"]
|
|
56
|
+
export const ROW_FIELDS = Object.freeze([...REQUIRED, ...Object.keys(ROW_DEFAULTS)])
|
|
57
|
+
|
|
58
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/
|
|
59
|
+
const LABEL = /^[a-z0-9_-]{1,32}$/
|
|
60
|
+
const FACT_REF = /^[FQ]\d+$/
|
|
61
|
+
|
|
62
|
+
function check(errors, condition, message) {
|
|
63
|
+
if (!condition) errors.push(message)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function nullOr(value, test) {
|
|
67
|
+
return value === null || test(value)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateRow(row, index, errors) {
|
|
71
|
+
const at = `rows[${index}]`
|
|
72
|
+
if (!isObject(row)) return errors.push(`${at} must be an object`)
|
|
73
|
+
for (const key of ROW_FIELDS) check(errors, Object.hasOwn(row, key), `${at}.${key} is required`)
|
|
74
|
+
for (const key of Object.keys(row)) check(errors, ROW_FIELDS.includes(key), `${at}.${key} is not a ledger field`)
|
|
75
|
+
check(errors, FACT_KINDS.includes(row.kind), `${at}.kind must be one of ${FACT_KINDS.join(", ")}`)
|
|
76
|
+
errors.push(...kindRuleErrors(row, at))
|
|
77
|
+
check(errors, row.unit === undefined || nullOr(row.unit, isNonEmpty), `${at}.unit must be null or a non-empty token`)
|
|
78
|
+
for (const key of ["raw", "quote"]) check(errors, row[key] === undefined || isString(row[key]), `${at}.${key} must be a string`)
|
|
79
|
+
check(errors, row.extracted_by === undefined || isNonEmpty(row.extracted_by), `${at}.extracted_by must be a non-empty string`)
|
|
80
|
+
check(errors, row.observed_at === undefined || (isString(row.observed_at) && ISO_DATE.test(row.observed_at)), `${at}.observed_at must be an ISO timestamp`)
|
|
81
|
+
check(errors, row.label === undefined || (isString(row.label) && LABEL.test(row.label)), `${at}.label must match [a-z0-9_-]{1,32}`)
|
|
82
|
+
const span = row.value_span
|
|
83
|
+
check(errors, span === undefined || nullOr(span, (v) => Array.isArray(v) && v.length === 2 && v.every(isCount) && v[0] <= v[1]), `${at}.value_span must be null or [start, end]`)
|
|
84
|
+
for (const key of ["locate_hint", "valid_at", "entity", "observation_id", "missing_rule", "accounting_basis", "formula", "render"]) {
|
|
85
|
+
if (row[key] !== undefined) check(errors, nullOr(row[key], isString), `${at}.${key} must be null or a string`)
|
|
86
|
+
}
|
|
87
|
+
for (const key of ["population", "snapshot_window", "revision"]) {
|
|
88
|
+
if (row[key] !== undefined) check(errors, nullOr(row[key], (v) => isString(v) || isObject(v)), `${at}.${key} must be null, a string, or an object`)
|
|
89
|
+
}
|
|
90
|
+
if (row.qualifier !== undefined) check(errors, nullOr(row.qualifier, (v) => QUALIFIERS.includes(v)), `${at}.qualifier must be null or one of ${QUALIFIERS.join(", ")}`)
|
|
91
|
+
if (row.sentence_id !== undefined) check(errors, nullOr(row.sentence_id, (v) => isString(v) && /^S\d+:s\d+$/.test(v)), `${at}.sentence_id must be null or S<n>:s<k>`)
|
|
92
|
+
for (const key of ["fuzzy", "outside_floor"]) {
|
|
93
|
+
if (row[key] !== undefined) check(errors, typeof row[key] === "boolean", `${at}.${key} must be a boolean`)
|
|
94
|
+
}
|
|
95
|
+
if (row.operation !== undefined) check(errors, nullOr(row.operation, (v) => OPERATIONS.includes(v)), `${at}.operation must be null or one of ${OPERATIONS.join(", ")}`)
|
|
96
|
+
for (const key of ["operands", "derived_from"]) {
|
|
97
|
+
if (row[key] !== undefined) check(errors, Array.isArray(row[key]) && row[key].every((v) => isString(v) && FACT_REF.test(v)), `${at}.${key} must be an array of fact ids`)
|
|
98
|
+
}
|
|
99
|
+
if (row.factor !== undefined) check(errors, nullOr(row.factor, (v) => isString(v) && DECIMAL.test(v)), `${at}.factor must be null or a decimal string`)
|
|
100
|
+
check(errors, row.status === undefined || FACT_STATUSES.includes(row.status), `${at}.status must be one of ${FACT_STATUSES.join(", ")}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The status machine and event rule of references/fact-ledger-grammar.md. validateLedger replays
|
|
104
|
+
// events through transitionError, and every writer in fact-ledger.mjs calls it before appending.
|
|
105
|
+
export const TRANSITIONS = Object.freeze({ active: Object.freeze(["retracted", "superseded"]), retracted: Object.freeze([]), superseded: Object.freeze([]) })
|
|
106
|
+
const EVENT_FIELDS = Object.freeze(["fact_id", "from", "to", "replacement", "reason"])
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {Map<string, string>} statuses each fact's status at the event's position
|
|
110
|
+
* @returns {string | null} why the event is not a legal transition, or null
|
|
111
|
+
*/
|
|
112
|
+
export function transitionError(statuses, event) {
|
|
113
|
+
if (!isObject(event)) return "an event must be an object"
|
|
114
|
+
const id = event.fact_id
|
|
115
|
+
if (!statuses.has(id)) return `no ledger row ${id}`
|
|
116
|
+
if (EVENT_FIELDS.some((key) => !Object.hasOwn(event, key)) || Object.keys(event).some((key) => !EVENT_FIELDS.includes(key))) {
|
|
117
|
+
return `${id}: an event has exactly the fields ${EVENT_FIELDS.join(", ")}`
|
|
118
|
+
}
|
|
119
|
+
const current = statuses.get(id)
|
|
120
|
+
if (event.from !== current) return `${id} is ${current}, but the event moves it from ${event.from}`
|
|
121
|
+
if (!TRANSITIONS[current].includes(event.to)) return `${id} cannot move from ${current} to ${event.to}`
|
|
122
|
+
if (event.to === "superseded") {
|
|
123
|
+
if (event.replacement === null) return `supersede ${id} needs a replacement row; use retract for a fact with no replacement`
|
|
124
|
+
if (event.replacement === id) return `${id} cannot be its own replacement`
|
|
125
|
+
if (!statuses.has(event.replacement)) return `no replacement row ${event.replacement} for ${id}`
|
|
126
|
+
if (statuses.get(event.replacement) !== "active") return `replacement row ${event.replacement} is ${statuses.get(event.replacement)} (superseding ${id})`
|
|
127
|
+
} else if (event.replacement !== null) return `${id} is ${event.to} but names replacement ${event.replacement}`
|
|
128
|
+
if (!isNonEmpty(event.reason)) return `a reason is required to mark ${id} ${event.to}`
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Replay every event from all-active; each row's status must be where its event chain leaves it. */
|
|
133
|
+
function validateHistory(ledger, errors) {
|
|
134
|
+
const statuses = new Map(ledger.rows.filter(isObject).map((row) => [row.fact_id, "active"]))
|
|
135
|
+
const counts = new Map()
|
|
136
|
+
ledger.events.forEach((event, index) => {
|
|
137
|
+
const problem = transitionError(statuses, event)
|
|
138
|
+
if (problem) errors.push(`events[${index}]: ${problem}`)
|
|
139
|
+
if (!isObject(event) || !statuses.has(event.fact_id) || !FACT_STATUSES.includes(event.to)) return
|
|
140
|
+
statuses.set(event.fact_id, event.to)
|
|
141
|
+
counts.set(event.fact_id, (counts.get(event.fact_id) ?? 0) + 1)
|
|
142
|
+
})
|
|
143
|
+
ledger.rows.forEach((row, index) => {
|
|
144
|
+
if (!isObject(row) || !FACT_STATUSES.includes(row.status)) return
|
|
145
|
+
const to = statuses.get(row.fact_id)
|
|
146
|
+
if (!counts.has(row.fact_id)) check(errors, row.status === "active", `rows[${index}]: ${row.fact_id} is ${row.status} but has no status event`)
|
|
147
|
+
else check(errors, to === row.status, `rows[${index}]: ${row.fact_id} has status ${row.status} but its last event moves it to ${to}`)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** operands and derived_from name existing rows, never the row itself, and close no cycle. */
|
|
152
|
+
export function provenanceErrors(rows) {
|
|
153
|
+
const errors = []
|
|
154
|
+
const refs = new Map()
|
|
155
|
+
for (const row of rows) if (isObject(row)) refs.set(row.fact_id, [row.operands, row.derived_from].flatMap((list) => (Array.isArray(list) ? list : [])))
|
|
156
|
+
rows.forEach((row, index) => {
|
|
157
|
+
for (const key of ["operands", "derived_from"]) {
|
|
158
|
+
if (!isObject(row) || !Array.isArray(row[key])) continue
|
|
159
|
+
for (const ref of row[key]) {
|
|
160
|
+
if (ref === row.fact_id) errors.push(`rows[${index}].${key}: ${ref} references itself`)
|
|
161
|
+
else check(errors, refs.has(ref), `rows[${index}].${key}: ${row.fact_id} references ${ref}, which has no ledger row`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
const state = new Map()
|
|
166
|
+
const visit = (id, path) => {
|
|
167
|
+
if (state.get(id) === "done") return null
|
|
168
|
+
if (state.get(id) === "open") return [...path.slice(path.indexOf(id)), id]
|
|
169
|
+
state.set(id, "open")
|
|
170
|
+
for (const next of refs.get(id)) {
|
|
171
|
+
const cycle = next !== id && refs.has(next) ? visit(next, [...path, id]) : null
|
|
172
|
+
if (cycle) return cycle
|
|
173
|
+
}
|
|
174
|
+
state.set(id, "done")
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
for (const id of refs.keys()) {
|
|
178
|
+
const cycle = visit(id, [])
|
|
179
|
+
if (cycle) return [...errors, `${cycle[0]} derives from itself through ${cycle.join(" -> ")}`]
|
|
180
|
+
}
|
|
181
|
+
return errors
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @returns {string[]} every schema error; empty when the ledger is valid. */
|
|
185
|
+
export function validateLedger(ledger) {
|
|
186
|
+
const errors = []
|
|
187
|
+
if (!isObject(ledger)) return ["ledger must be an object"]
|
|
188
|
+
check(errors, ledger.schemaVersion === LEDGER_SCHEMA_VERSION, `schemaVersion must be ${LEDGER_SCHEMA_VERSION}`)
|
|
189
|
+
if (!Array.isArray(ledger.rows)) return [...errors, "rows must be an array"]
|
|
190
|
+
ledger.rows.forEach((row, index) => validateRow(row, index, errors))
|
|
191
|
+
const ids = new Set()
|
|
192
|
+
const labels = new Set()
|
|
193
|
+
ledger.rows.forEach((row, index) => {
|
|
194
|
+
if (!isObject(row)) return
|
|
195
|
+
check(errors, !ids.has(row.fact_id), `rows[${index}].fact_id ${row.fact_id} is a duplicate`)
|
|
196
|
+
check(errors, row.label === undefined || !labels.has(row.label), `rows[${index}]: ${row.fact_id} has duplicate label ${row.label}`)
|
|
197
|
+
ids.add(row.fact_id)
|
|
198
|
+
labels.add(row.label)
|
|
199
|
+
})
|
|
200
|
+
errors.push(...provenanceErrors(ledger.rows))
|
|
201
|
+
if (Array.isArray(ledger.events)) validateHistory(ledger, errors)
|
|
202
|
+
else errors.push("events must be an array")
|
|
203
|
+
return errors
|
|
204
|
+
}
|