lazycodex-ai 5.1.43 → 5.1.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +948 -416
- package/dist/cli/service-auth/keystore.d.ts +4 -2
- package/dist/cli-node/index.js +948 -416
- package/docs/reference/web-terminal-visual-qa.md +8 -0
- package/package.json +1 -1
- package/packages/lsp-daemon/dist/cli.js +4 -4
- package/packages/lsp-daemon/dist/client.js +4 -4
- package/packages/lsp-daemon/dist/index.js +4 -4
- package/packages/lsp-daemon/package.json +3 -3
- package/packages/lsp-tools-mcp/dist/cli.js +4 -4
- package/packages/lsp-tools-mcp/dist/mcp.js +4 -4
- package/packages/lsp-tools-mcp/dist/tools.js +4 -4
- package/packages/lsp-tools-mcp/package.json +3 -3
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +62 -21
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/biome.json +1 -21
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/biome.json +1 -21
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/biome.json +1 -21
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +4 -4
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +0 -1
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +1 -1
- package/packages/omo-codex/plugin/components/rules/biome.json +1 -21
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +4 -4
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/biome.json +1 -21
- package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +90 -1
- package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +91 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +7 -7
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +2 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +7 -7
- package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +2 -0
- package/packages/omo-codex/plugin/skills/browser/SKILL.md +21 -22
- package/packages/omo-codex/plugin/skills/browser/references/commands.md +2 -9
- package/packages/omo-codex/plugin/skills/browser/references/install.md +1 -2
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +2 -2
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +63 -487
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +1 -1
- package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +1 -10
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +79 -118
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +75 -224
- package/packages/omo-codex/plugin/skills/frontend/references/design/stylegallery.md +32 -27
- package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -2
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/collect-files.ts +53 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +10 -7
- package/packages/omo-codex/plugin/skills/programming/references/typescript/README.md +10 -7
- package/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md +9 -23
- package/packages/omo-codex/plugin/skills/programming/references/typescript/oxc-lint.md +130 -0
- package/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md +6 -62
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/new-project.ts +20 -81
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +14 -14
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +9 -3
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +7 -6
- package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +51 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/claims-index.mjs +125 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +68 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +1 -27
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/lexicon.mjs +47 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/numerals.mjs +226 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +2 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/particles-ko.mjs +108 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/rational.mjs +139 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/sentence-split.mjs +77 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-normalize.mjs +82 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-similarity.mjs +28 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +105 -0
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +71 -64
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +4 -1
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +6 -6
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +152 -23
- package/packages/shared-skills/skills/browser/SKILL.md +21 -22
- package/packages/shared-skills/skills/browser/references/commands.md +2 -9
- package/packages/shared-skills/skills/browser/references/install.md +1 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +2 -2
- package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +63 -487
- package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +1 -1
- package/packages/shared-skills/skills/browser/scripts/omowright.mjs +1 -10
- package/packages/shared-skills/skills/frontend/SKILL.md +79 -118
- package/packages/shared-skills/skills/frontend/references/design/README.md +75 -224
- package/packages/shared-skills/skills/frontend/references/design/stylegallery.md +32 -27
- package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -2
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.test.ts +101 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.test.ts +50 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.ts +53 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
- package/packages/shared-skills/skills/programming/SKILL.md +10 -7
- package/packages/shared-skills/skills/programming/references/typescript/README.md +10 -7
- package/packages/shared-skills/skills/programming/references/typescript/bootstrap.md +9 -23
- package/packages/shared-skills/skills/programming/references/typescript/oxc-lint.md +130 -0
- package/packages/shared-skills/skills/programming/references/typescript/tsconfig-strict.md +6 -62
- package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
- package/packages/shared-skills/skills/programming/scripts/typescript/new-project.test.ts +162 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/new-project.ts +20 -81
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
- package/packages/shared-skills/skills/refactor/SKILL.md +13 -13
- package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
- package/packages/shared-skills/skills/ulw-research/AGENTS.md +9 -3
- package/packages/shared-skills/skills/ulw-research/SKILL.md +2 -2
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +7 -6
- package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
- package/packages/shared-skills/skills/ulw-research/references/report-gates.md +51 -3
- package/packages/shared-skills/skills/ulw-research/scripts/claims-index.mjs +125 -0
- package/packages/shared-skills/skills/ulw-research/scripts/claims-index.test.ts +149 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +68 -3
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +97 -12
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +190 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +141 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.test.ts +43 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +252 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.test.ts +179 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.test.ts +46 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-context.test.ts +64 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +1 -27
- package/packages/shared-skills/skills/ulw-research/scripts/lexicon.mjs +47 -0
- package/packages/shared-skills/skills/ulw-research/scripts/lexicon.test.ts +57 -0
- package/packages/shared-skills/skills/ulw-research/scripts/numerals.mjs +226 -0
- package/packages/shared-skills/skills/ulw-research/scripts/numerals.test.ts +120 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +2 -3
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +29 -0
- package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.mjs +108 -0
- package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.test.ts +86 -0
- package/packages/shared-skills/skills/ulw-research/scripts/rational.mjs +139 -0
- package/packages/shared-skills/skills/ulw-research/scripts/rational.test.ts +46 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
- package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.mjs +77 -0
- package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.test.ts +109 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/claim-graph.md +13 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-locked.md +17 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-pending.md +17 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.mjs +82 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.test.ts +73 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.mjs +28 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.test.ts +38 -0
- package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +105 -0
- package/packages/shared-skills/skills/ulw-research/scripts/units.test.ts +83 -0
- package/packages/shared-skills/skills/visual-qa/SKILL.md +70 -63
- package/packages/shared-skills/skills/visual-qa/scripts/skill-doc-fields.test.ts +34 -0
- package/script/qa/web-terminal-visual-qa.mjs +6 -2
- package/script/qa/xterm-live-terminal.mjs +26 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { spawnSync } from "node:child_process"
|
|
3
|
+
import { readFileSync } from "node:fs"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
import { KIND_COLUMNS, KIND_RULES as RULES_TABLE, KIND_TABLE_BEGIN, KIND_TABLE_END, exactFromDecimal, renderKindTable } from "./fact-ledger-kinds.mjs"
|
|
7
|
+
import { FACT_KINDS, LEDGER_SCHEMA_VERSION, ROW_DEFAULTS, validateLedger } from "./fact-ledger-schema.mjs"
|
|
8
|
+
|
|
9
|
+
// Property tests generated from KIND_RULES: for every kind, rows built from each column's rule
|
|
10
|
+
// validate, and breaking exactly one column's rule is rejected with an error naming that column.
|
|
11
|
+
|
|
12
|
+
/** mulberry32: a fixed seed gives the same rows on every run. */
|
|
13
|
+
function prng(seed: number) {
|
|
14
|
+
let state = seed >>> 0
|
|
15
|
+
return () => {
|
|
16
|
+
state = (state + 0x6d2b79f5) >>> 0
|
|
17
|
+
let t = state
|
|
18
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
19
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
20
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const KIND_RULES = RULES_TABLE as Record<string, Record<string, string>>
|
|
25
|
+
|
|
26
|
+
type Random = () => number
|
|
27
|
+
const int = (random: Random, max: number) => Math.floor(random() * max)
|
|
28
|
+
const pick = <T>(random: Random, list: T[]): T => list[int(random, list.length)]
|
|
29
|
+
|
|
30
|
+
function decimal(random: Random) {
|
|
31
|
+
const whole = `${pick(random, ["-", ""])}${int(random, 5000)}`
|
|
32
|
+
return random() < 0.5 ? whole : `${whole}.${String(int(random, 1000)).padStart(pick(random, [1, 2, 3]), "0").slice(0, 3)}`
|
|
33
|
+
}
|
|
34
|
+
const rational = (random: Random) => ({ num: `${int(random, 200) - 100}`, den: `${1 + int(random, 50)}` })
|
|
35
|
+
const locator = (random: Random) => ({ artifact: `sources/S${1 + int(random, 9)}/text.txt`, char_start: 3, char_end: 3 + int(random, 80) })
|
|
36
|
+
const sha = (random: Random) => Array.from({ length: 64 }, () => "0123456789abcdef"[int(random, 16)]).join("")
|
|
37
|
+
|
|
38
|
+
type Row = Record<string, unknown> & { value: string }
|
|
39
|
+
type Gen = { valid: (random: Random, row: Row) => unknown; invalid: (random: Random, row: Row) => unknown }
|
|
40
|
+
|
|
41
|
+
/** One valid and one invalid generator per rule name; an unknown rule name fails the test below. */
|
|
42
|
+
const GENERATORS: Record<string, Gen> = {
|
|
43
|
+
f_id: { valid: (r) => `F${1 + int(r, 99)}`, invalid: (r) => `Q${1 + int(r, 99)}` },
|
|
44
|
+
q_id: { valid: (r) => `Q${1 + int(r, 99)}`, invalid: (r) => `F${1 + int(r, 99)}` },
|
|
45
|
+
decimal: { valid: decimal, invalid: (r) => pick(r, ["1e3", "", null, 12, "1,000"]) },
|
|
46
|
+
null: { valid: () => null, invalid: (r) => pick(r, ["0", { num: "1", den: "1" }, "S1", "why"]) },
|
|
47
|
+
source: { valid: (r) => `S${1 + int(r, 9)}`, invalid: (r) => pick(r, [null, "F1", "S"]) },
|
|
48
|
+
source_or_null: { valid: (r) => pick(r, [null, `S${1 + int(r, 9)}`]), invalid: (r) => pick(r, ["F1", "S", 3]) },
|
|
49
|
+
locator: { valid: locator, invalid: (r) => pick(r, [null, { artifact: "", char_start: 0, char_end: 1 }, { artifact: "a", char_start: 5, char_end: 1 }]) },
|
|
50
|
+
locator_or_null: { valid: (r) => pick(r, [null, locator(r)]), invalid: (r) => pick(r, ["here", { artifact: "", char_start: 0, char_end: 1 }]) },
|
|
51
|
+
sha256: { valid: sha, invalid: (r) => pick(r, [null, "abc", "A".repeat(64)]) },
|
|
52
|
+
sha256_or_null: { valid: (r) => pick(r, [null, sha(r)]), invalid: (r) => pick(r, ["abc", 7]) },
|
|
53
|
+
string_or_null: { valid: (r) => pick(r, [null, "", "a reason"]), invalid: (r) => pick(r, [5, false, {}]) },
|
|
54
|
+
non_empty: { valid: () => "assumed from the filing year", invalid: (r) => pick(r, ["", null]) },
|
|
55
|
+
from_value: {
|
|
56
|
+
valid: (_r, row) => exactFromDecimal(row.value),
|
|
57
|
+
invalid: (r, row) => {
|
|
58
|
+
const exact = exactFromDecimal(row.value)
|
|
59
|
+
return pick(r, [null, { num: `${BigInt(exact.num) + 7n}`, den: exact.den }, { num: `${BigInt(exact.num) * 2n}`, den: `${BigInt(exact.den) * 2n}` }])
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
rational_or_null: { valid: (r) => pick(r, [null, rational(r)]), invalid: (r) => pick(r, [{ num: "1", den: "0" }, { num: 1, den: "2" }, "1/2"]) },
|
|
63
|
+
by_derive: { valid: (r) => pick(r, [null, rational(r)]), invalid: (r) => pick(r, [{ num: "1", den: "0" }, { num: "x", den: "2" }, "1/2"]) },
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function baseRow(kind: string): Row {
|
|
67
|
+
const row: Record<string, unknown> = { ...structuredClone(ROW_DEFAULTS), kind, raw: "raw", quote: "quoted text", extracted_by: "fact add", observed_at: "2026-10-23T08:00:00Z", label: "x" }
|
|
68
|
+
return row as Row
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A row whose every KIND_COLUMNS field follows its kind's rule, in column order (exact after value). */
|
|
72
|
+
function validRow(kind: string, random: Random) {
|
|
73
|
+
const row = baseRow(kind)
|
|
74
|
+
for (const column of KIND_COLUMNS) row[column] = GENERATORS[KIND_RULES[kind][column]].valid(random, row)
|
|
75
|
+
return row
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ledgerOf = (row: object) => ({ schemaVersion: LEDGER_SCHEMA_VERSION, rows: [row], events: [] })
|
|
79
|
+
const SEEDS = Array.from({ length: 60 }, (_, index) => index + 1)
|
|
80
|
+
|
|
81
|
+
describe("KIND_RULES covers the grammar", () => {
|
|
82
|
+
test("every kind has a rule for every column, and every rule has a generator", () => {
|
|
83
|
+
expect(Object.keys(KIND_RULES)).toEqual([...FACT_KINDS])
|
|
84
|
+
for (const kind of FACT_KINDS) {
|
|
85
|
+
expect(Object.keys(KIND_RULES[kind])).toEqual([...KIND_COLUMNS])
|
|
86
|
+
for (const column of KIND_COLUMNS) expect(Object.hasOwn(GENERATORS, KIND_RULES[kind][column])).toBe(true)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe("rows generated from KIND_RULES", () => {
|
|
92
|
+
for (const kind of FACT_KINDS) {
|
|
93
|
+
test(`${kind}: seeded valid rows validate`, () => {
|
|
94
|
+
for (const seed of SEEDS) {
|
|
95
|
+
const row = validRow(kind, prng(seed))
|
|
96
|
+
expect({ seed, errors: validateLedger(ledgerOf(row)) }).toEqual({ seed, errors: [] })
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
for (const column of KIND_COLUMNS) {
|
|
101
|
+
test(`${kind}: breaking only the ${column} rule (${KIND_RULES[kind][column]}) is rejected`, () => {
|
|
102
|
+
for (const seed of SEEDS) {
|
|
103
|
+
const random = prng(seed * 7919)
|
|
104
|
+
const row = validRow(kind, random)
|
|
105
|
+
row[column] = GENERATORS[KIND_RULES[kind][column]].invalid(random, row)
|
|
106
|
+
const errors = validateLedger(ledgerOf(row))
|
|
107
|
+
expect({ seed, value: row[column], named: errors.some((error: string) => error.startsWith(`rows[0].${column}`)) }).toEqual({ seed, value: row[column], named: true })
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
test("review MEDIUM: a measured row with value -0.50 and exact 7/1 is rejected", () => {
|
|
114
|
+
const row = { ...validRow("measured", prng(1)), value: "-0.50", exact: { num: "7", den: "1" } }
|
|
115
|
+
expect(validateLedger(ledgerOf(row)).some((error: string) => error.startsWith("rows[0].exact"))).toBe(true)
|
|
116
|
+
expect(validateLedger(ledgerOf({ ...row, exact: { num: "-1", den: "2" } }))).toEqual([])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test("review LOW 1: a quote row with a non-null exact is rejected", () => {
|
|
120
|
+
const row = { ...validRow("quote", prng(2)), exact: { num: "1", den: "1" } }
|
|
121
|
+
expect(validateLedger(ledgerOf(row)).some((error: string) => error.startsWith("rows[0].exact"))).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe("the grammar's per-kind table is rendered from KIND_RULES", () => {
|
|
126
|
+
const grammar = readFileSync(join(import.meta.dir, "..", "references", "fact-ledger-grammar.md"), "utf8")
|
|
127
|
+
|
|
128
|
+
test("the committed table section equals renderKindTable() byte for byte", () => {
|
|
129
|
+
const begin = grammar.indexOf(KIND_TABLE_BEGIN)
|
|
130
|
+
const end = grammar.indexOf(KIND_TABLE_END)
|
|
131
|
+
expect(begin).toBeGreaterThanOrEqual(0)
|
|
132
|
+
expect(end).toBeGreaterThan(begin)
|
|
133
|
+
expect(grammar.slice(begin + KIND_TABLE_BEGIN.length + 1, end)).toBe(renderKindTable())
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test("node fact-ledger-kinds.mjs --render-grammar prints the same table", () => {
|
|
137
|
+
const result = spawnSync("node", [join(import.meta.dir, "fact-ledger-kinds.mjs"), "--render-grammar"], { encoding: "utf8" })
|
|
138
|
+
expect(result.status).toBe(0)
|
|
139
|
+
expect(result.stdout).toBe(renderKindTable())
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
|
|
3
|
+
import { LEDGER_SCHEMA_VERSION, ROW_DEFAULTS, validateLedger } from "./fact-ledger-schema.mjs"
|
|
4
|
+
|
|
5
|
+
function measuredRow(factId: string, label: string) {
|
|
6
|
+
return {
|
|
7
|
+
...structuredClone(ROW_DEFAULTS),
|
|
8
|
+
fact_id: factId,
|
|
9
|
+
kind: "measured",
|
|
10
|
+
value: "320",
|
|
11
|
+
unit: "m",
|
|
12
|
+
raw: "320 m",
|
|
13
|
+
quote: "The span is 320 m.",
|
|
14
|
+
value_span: [12, 15],
|
|
15
|
+
source_id: "S1",
|
|
16
|
+
locator: { artifact: "sources/S1/text.txt", line: 3, char_start: 40, char_end: 62 },
|
|
17
|
+
snapshot_sha256: "a".repeat(64),
|
|
18
|
+
extracted_by: "fact add --find",
|
|
19
|
+
observed_at: "2026-10-23T08:00:00Z",
|
|
20
|
+
label,
|
|
21
|
+
exact: { num: "320", den: "1" },
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ledgerOf(...rows: object[]) {
|
|
26
|
+
return { schemaVersion: LEDGER_SCHEMA_VERSION, rows, events: [] }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("validateLedger (schema module)", () => {
|
|
30
|
+
test("given a complete measured row, when validated, then there are no errors", () => {
|
|
31
|
+
expect(validateLedger(ledgerOf(measuredRow("F1", "span")))).toEqual([])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test("given two rows with one label, when validated, then the duplicate label is reported", () => {
|
|
35
|
+
const errors = validateLedger(ledgerOf(measuredRow("F1", "span"), measuredRow("F2", "span")))
|
|
36
|
+
expect(errors.some((e: string) => e.includes("duplicate label"))).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("given an assumed row without rationale, when validated, then rationale is required", () => {
|
|
40
|
+
const errors = validateLedger(ledgerOf({ ...measuredRow("F1", "span"), kind: "assumed" }))
|
|
41
|
+
expect(errors.some((e: string) => e.includes("rationale"))).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// fact-ledger.mjs - the verbatim fact ledger: one row per measured, quoted, derived, assumed,
|
|
2
|
+
// or converted value, with its source locator and exact rational. Rows are never deleted:
|
|
3
|
+
// retract and supersede change status and append an event. Validation and the field vocabulary
|
|
4
|
+
// live in fact-ledger-schema.mjs and are re-exported here. node: builtins only.
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "node:fs"
|
|
7
|
+
import { join } from "node:path"
|
|
8
|
+
|
|
9
|
+
import { CliError, readJsonFile, writeJsonAtomic } from "./cli-support.mjs"
|
|
10
|
+
import { DECIMAL, exactComesFromValue, exactFromDecimal, idPrefix, isString } from "./fact-ledger-kinds.mjs"
|
|
11
|
+
import { LEDGER_SCHEMA_VERSION, ROW_DEFAULTS, ROW_FIELDS, transitionError, validateLedger } from "./fact-ledger-schema.mjs"
|
|
12
|
+
|
|
13
|
+
export { exactFromDecimal } from "./fact-ledger-kinds.mjs"
|
|
14
|
+
export { FACT_KINDS, FACT_STATUSES, LEDGER_SCHEMA_VERSION, OPERATIONS, QUALIFIERS, ROW_FIELDS, TRANSITIONS, transitionError, validateLedger } from "./fact-ledger-schema.mjs"
|
|
15
|
+
|
|
16
|
+
export const LEDGER_FILENAME = "fact-ledger.json"
|
|
17
|
+
|
|
18
|
+
export function emptyLedger() {
|
|
19
|
+
return { schemaVersion: LEDGER_SCHEMA_VERSION, rows: [], events: [] }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function assertValid(ledger) {
|
|
23
|
+
const errors = validateLedger(ledger)
|
|
24
|
+
if (errors.length > 0) throw new CliError(`invalid fact ledger: ${errors.join("; ")}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readLedger(dir) {
|
|
28
|
+
const file = join(dir, LEDGER_FILENAME)
|
|
29
|
+
if (!existsSync(file)) return emptyLedger()
|
|
30
|
+
const ledger = readJsonFile(file)
|
|
31
|
+
assertValid(ledger)
|
|
32
|
+
return ledger
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function writeLedger(dir, ledger) {
|
|
36
|
+
assertValid(ledger)
|
|
37
|
+
const file = join(dir, LEDGER_FILENAME)
|
|
38
|
+
writeJsonAtomic(file, ledger)
|
|
39
|
+
return file
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ID_NUMBER = Object.freeze({ F: /^F(\d+)$/, Q: /^Q(\d+)$/ })
|
|
43
|
+
|
|
44
|
+
/** The next id with the kind's prefix from KIND_RULES (F<n> for value rows, Q<n> for quotes). */
|
|
45
|
+
export function nextId(ledger, kind) {
|
|
46
|
+
const prefix = idPrefix(kind)
|
|
47
|
+
let max = 0
|
|
48
|
+
for (const row of ledger.rows) {
|
|
49
|
+
const match = ID_NUMBER[prefix].exec(row.fact_id ?? "")
|
|
50
|
+
if (match) max = Math.max(max, Number(match[1]))
|
|
51
|
+
}
|
|
52
|
+
return `${prefix}${max + 1}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fill defaults, assign the next id, validate the whole ledger (provenance included), then append.
|
|
57
|
+
* Only kinds whose KIND_RULES exact is from_value (measured) get exact from their decimal:
|
|
58
|
+
* derived and converted rows carry the exact that derive computed from the operands and factor,
|
|
59
|
+
* never one read back from the rounded value.
|
|
60
|
+
*/
|
|
61
|
+
export function addRow(ledger, row) {
|
|
62
|
+
const filled = { ...structuredClone(ROW_DEFAULTS), ...row, fact_id: row.fact_id ?? nextId(ledger, row.kind) }
|
|
63
|
+
if (exactComesFromValue(filled.kind) && filled.exact === null && isString(filled.value) && DECIMAL.test(filled.value)) {
|
|
64
|
+
filled.exact = exactFromDecimal(filled.value)
|
|
65
|
+
}
|
|
66
|
+
const ordered = Object.fromEntries(ROW_FIELDS.filter((key) => Object.hasOwn(filled, key)).map((key) => [key, filled[key]]))
|
|
67
|
+
for (const key of Object.keys(filled)) if (!ROW_FIELDS.includes(key)) ordered[key] = filled[key]
|
|
68
|
+
assertValid({ ...ledger, rows: [...ledger.rows, ordered] })
|
|
69
|
+
ledger.rows.push(ordered)
|
|
70
|
+
return ordered
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Build the event from the row's current status and append it only if transitionError allows it. */
|
|
74
|
+
function changeStatus(ledger, id, to, replacement, reason) {
|
|
75
|
+
const row = ledger.rows.find((candidate) => candidate.fact_id === id)
|
|
76
|
+
const event = { fact_id: id, from: row?.status ?? null, to, replacement: replacement ?? null, reason }
|
|
77
|
+
const problem = transitionError(new Map(ledger.rows.map((candidate) => [candidate.fact_id, candidate.status])), event)
|
|
78
|
+
if (problem) throw new CliError(problem)
|
|
79
|
+
ledger.events.push(event)
|
|
80
|
+
row.status = to
|
|
81
|
+
return row
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function retractRow(ledger, id, reason) {
|
|
85
|
+
return changeStatus(ledger, id, "retracted", null, reason)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function supersedeRow(ledger, id, replacementId, reason) {
|
|
89
|
+
return changeStatus(ledger, id, "superseded", replacementId, reason)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function cell(value) {
|
|
93
|
+
return value === null || value === undefined ? "" : String(value).replace(/\|/g, "\\|").replace(/\n/g, " ")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The `--view` table: one line per row, retracted and superseded rows included. */
|
|
97
|
+
export function renderLedgerMarkdown(ledger) {
|
|
98
|
+
const lines = ["| fact_id | kind | label | value | unit | source | status | raw |", "|---|---|---|---|---|---|---|---|"]
|
|
99
|
+
for (const row of ledger.rows) {
|
|
100
|
+
lines.push(`| ${[row.fact_id, row.kind, row.label, row.value, row.unit, row.source_id, row.status, row.raw].map(cell).join(" | ")} |`)
|
|
101
|
+
}
|
|
102
|
+
return `${lines.join("\n")}\n`
|
|
103
|
+
}
|