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,252 @@
|
|
|
1
|
+
import { describe, expect, onTestFinished, test } from "bun:test"
|
|
2
|
+
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
LEDGER_FILENAME,
|
|
8
|
+
LEDGER_SCHEMA_VERSION,
|
|
9
|
+
addRow,
|
|
10
|
+
emptyLedger,
|
|
11
|
+
nextId,
|
|
12
|
+
readLedger,
|
|
13
|
+
renderLedgerMarkdown,
|
|
14
|
+
retractRow,
|
|
15
|
+
supersedeRow,
|
|
16
|
+
validateLedger,
|
|
17
|
+
writeLedger,
|
|
18
|
+
} from "./fact-ledger.mjs"
|
|
19
|
+
|
|
20
|
+
const HASH = "a".repeat(64)
|
|
21
|
+
|
|
22
|
+
function tempDir(): string {
|
|
23
|
+
const dir = mkdtempSync(join(tmpdir(), "ulw-ledger-"))
|
|
24
|
+
onTestFinished(() => rmSync(dir, { recursive: true, force: true }))
|
|
25
|
+
return dir
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function measured(label: string, value = "320") {
|
|
29
|
+
return {
|
|
30
|
+
kind: "measured",
|
|
31
|
+
value,
|
|
32
|
+
unit: "m",
|
|
33
|
+
raw: `${value} m`,
|
|
34
|
+
quote: `The span is ${value} m.`,
|
|
35
|
+
value_span: [12, 12 + value.length],
|
|
36
|
+
source_id: "S1",
|
|
37
|
+
locator: { artifact: "sources/S1/text.txt", line: 3, char_start: 40, char_end: 62 },
|
|
38
|
+
snapshot_sha256: HASH,
|
|
39
|
+
extracted_by: "fact add --find",
|
|
40
|
+
observed_at: "2026-10-23T08:00:00Z",
|
|
41
|
+
label,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function quoteRow(label: string) {
|
|
46
|
+
return { ...measured(label), kind: "quote", value: null, unit: null, value_span: null }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ledgerWith(...rows: object[]) {
|
|
50
|
+
const ledger = emptyLedger()
|
|
51
|
+
for (const row of rows) addRow(ledger, row)
|
|
52
|
+
return ledger
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("validateLedger", () => {
|
|
56
|
+
test("accepts a ledger built by addRow", () => {
|
|
57
|
+
expect(validateLedger(ledgerWith(measured("span"), quoteRow("claim")))).toEqual([])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("rejects a row missing label", () => {
|
|
61
|
+
const ledger = ledgerWith(measured("span"))
|
|
62
|
+
delete (ledger.rows[0] as Record<string, unknown>).label
|
|
63
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("label"))).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test("rejects a duplicate label", () => {
|
|
67
|
+
const ledger = ledgerWith(measured("span"))
|
|
68
|
+
ledger.rows.push({ ...ledger.rows[0], fact_id: "F2" })
|
|
69
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("duplicate label"))).toBe(true)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("rejects an assumed row without rationale", () => {
|
|
73
|
+
const ledger = ledgerWith(measured("span"))
|
|
74
|
+
ledger.rows[0].kind = "assumed"
|
|
75
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("rationale"))).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("rejects a value row with a non-decimal value", () => {
|
|
79
|
+
const ledger = ledgerWith(measured("span"))
|
|
80
|
+
ledger.rows[0].value = "320m"
|
|
81
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("value"))).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("rejects an exact with a non-string num", () => {
|
|
85
|
+
const ledger = ledgerWith(measured("span"))
|
|
86
|
+
ledger.rows[0].exact = { num: 1, den: "3" }
|
|
87
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("exact"))).toBe(true)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test("rejects operands and derived_from ids with no ledger row", () => {
|
|
91
|
+
const ledger = ledgerWith(measured("span"))
|
|
92
|
+
ledger.rows.push({ ...ledger.rows[0], fact_id: "F2", kind: "derived", label: "ratio", operation: "ratio", operands: ["F9"], derived_from: ["F1", "Q7"] })
|
|
93
|
+
const errors = validateLedger(ledger)
|
|
94
|
+
expect(errors.some((e: string) => e.includes("operands") && e.includes("F9"))).toBe(true)
|
|
95
|
+
expect(errors.some((e: string) => e.includes("derived_from") && e.includes("Q7"))).toBe(true)
|
|
96
|
+
expect(errors.some((e: string) => e.includes("F1"))).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("a bare row with only fact_id returns at least three errors", () => {
|
|
100
|
+
expect(validateLedger({ rows: [{ fact_id: "F1" }] }).length).toBeGreaterThanOrEqual(3)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe("ids and status", () => {
|
|
105
|
+
test("nextId sequences F1, F2 then Q1, and a derived row gets F3", () => {
|
|
106
|
+
const ledger = emptyLedger()
|
|
107
|
+
expect(addRow(ledger, measured("a")).fact_id).toBe("F1")
|
|
108
|
+
expect(addRow(ledger, measured("b")).fact_id).toBe("F2")
|
|
109
|
+
expect(addRow(ledger, quoteRow("c")).fact_id).toBe("Q1")
|
|
110
|
+
expect(nextId(ledger, "derived")).toBe("F3")
|
|
111
|
+
const derived = addRow(ledger, {
|
|
112
|
+
kind: "derived",
|
|
113
|
+
value: "0.333",
|
|
114
|
+
unit: null,
|
|
115
|
+
raw: "0.333",
|
|
116
|
+
quote: "",
|
|
117
|
+
extracted_by: "fact derive",
|
|
118
|
+
observed_at: "2026-10-23T08:00:00Z",
|
|
119
|
+
label: "ratio",
|
|
120
|
+
operation: "ratio",
|
|
121
|
+
operands: ["F1", "F2"],
|
|
122
|
+
derived_from: ["F1", "F2"],
|
|
123
|
+
exact: { num: "1", den: "3" },
|
|
124
|
+
})
|
|
125
|
+
expect(derived.fact_id).toBe("F3")
|
|
126
|
+
expect(nextId(ledger, "quote")).toBe("Q2")
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("measured rows store exact from their decimal", () => {
|
|
130
|
+
const ledger = ledgerWith(measured("span", "12.50"))
|
|
131
|
+
expect(ledger.rows[0].exact).toEqual({ num: "25", den: "2" })
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("retractRow keeps the row with status retracted", () => {
|
|
135
|
+
const ledger = ledgerWith(measured("a"), measured("b"))
|
|
136
|
+
retractRow(ledger, "F1", "wrong page")
|
|
137
|
+
expect(ledger.rows).toHaveLength(2)
|
|
138
|
+
expect(ledger.rows[0].status).toBe("retracted")
|
|
139
|
+
expect(ledger.events[0]).toMatchObject({ fact_id: "F1", to: "retracted", reason: "wrong page" })
|
|
140
|
+
expect(validateLedger(ledger)).toEqual([])
|
|
141
|
+
expect(() => retractRow(ledger, "F9", "x")).toThrow()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test("supersedeRow points at the replacement", () => {
|
|
145
|
+
const ledger = ledgerWith(measured("a"), measured("b"))
|
|
146
|
+
supersedeRow(ledger, "F1", "F2", "newer filing")
|
|
147
|
+
expect(ledger.rows[0].status).toBe("superseded")
|
|
148
|
+
expect(ledger.events[0]).toMatchObject({ fact_id: "F1", replacement: "F2" })
|
|
149
|
+
expect(validateLedger(ledger)).toEqual([])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test("supersedeRow refuses a replacement that is not active", () => {
|
|
153
|
+
const ledger = ledgerWith(measured("a"), measured("b"))
|
|
154
|
+
retractRow(ledger, "F2", "wrong page")
|
|
155
|
+
expect(() => supersedeRow(ledger, "F1", "F2", "newer filing")).toThrow(/F2 is retracted/)
|
|
156
|
+
expect(ledger.rows[0].status).toBe("active")
|
|
157
|
+
expect(ledger.events).toHaveLength(1)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("supersedeRow without a replacement throws and points at retract", () => {
|
|
161
|
+
const ledger = ledgerWith(measured("a"))
|
|
162
|
+
expect(() => supersedeRow(ledger, "F1", null, "newer filing")).toThrow(/supersede F1 needs a replacement row; use retract/)
|
|
163
|
+
expect(ledger.rows[0].status).toBe("active")
|
|
164
|
+
expect(ledger.events).toHaveLength(0)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe("read and write", () => {
|
|
169
|
+
test("write then read round-trips byte-identically", () => {
|
|
170
|
+
const dir = tempDir()
|
|
171
|
+
const ledger = ledgerWith(measured("span"), quoteRow("claim"))
|
|
172
|
+
writeLedger(dir, ledger)
|
|
173
|
+
const first = readFileSync(join(dir, LEDGER_FILENAME), "utf8")
|
|
174
|
+
const loaded = readLedger(dir)
|
|
175
|
+
expect(loaded).toEqual(ledger)
|
|
176
|
+
writeLedger(dir, loaded)
|
|
177
|
+
expect(readFileSync(join(dir, LEDGER_FILENAME), "utf8")).toBe(first)
|
|
178
|
+
expect(readdirSync(dir)).toEqual([LEDGER_FILENAME])
|
|
179
|
+
expect(loaded.schemaVersion).toBe(LEDGER_SCHEMA_VERSION)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test("a derived row with an exact rational round-trips and validates", () => {
|
|
183
|
+
const dir = tempDir()
|
|
184
|
+
const ledger = ledgerWith(measured("a"), measured("b"))
|
|
185
|
+
addRow(ledger, {
|
|
186
|
+
kind: "derived",
|
|
187
|
+
value: "0.333",
|
|
188
|
+
raw: "0.333",
|
|
189
|
+
quote: "",
|
|
190
|
+
extracted_by: "fact derive",
|
|
191
|
+
observed_at: "2026-10-23T08:00:00Z",
|
|
192
|
+
label: "third",
|
|
193
|
+
operation: "ratio",
|
|
194
|
+
operands: ["F1", "F2"],
|
|
195
|
+
derived_from: ["F1", "F2"],
|
|
196
|
+
exact: { num: "1", den: "3" },
|
|
197
|
+
})
|
|
198
|
+
writeLedger(dir, ledger)
|
|
199
|
+
const loaded = readLedger(dir)
|
|
200
|
+
expect(loaded.rows[2].exact).toEqual({ num: "1", den: "3" })
|
|
201
|
+
expect(loaded.rows[2].value).toBe("0.333")
|
|
202
|
+
expect(validateLedger(loaded)).toEqual([])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test("readLedger of a missing file is an empty ledger; writeLedger refuses an invalid one", () => {
|
|
206
|
+
const dir = tempDir()
|
|
207
|
+
expect(readLedger(dir)).toEqual(emptyLedger())
|
|
208
|
+
const ledger = ledgerWith(measured("a"))
|
|
209
|
+
ledger.rows[0].value = "abc"
|
|
210
|
+
expect(() => writeLedger(dir, ledger)).toThrow()
|
|
211
|
+
expect(readdirSync(dir)).toEqual([])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test("renderLedgerMarkdown lists every row", () => {
|
|
215
|
+
const text = renderLedgerMarkdown(ledgerWith(measured("span"), quoteRow("claim")))
|
|
216
|
+
expect(text).toContain("| F1 | measured | span |")
|
|
217
|
+
expect(text).toContain("| Q1 | quote | claim |")
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe("validateLedger checks events against rows", () => {
|
|
222
|
+
test("a retracted event that carries a replacement is an error", () => {
|
|
223
|
+
const ledger = ledgerWith(measured("a"), measured("b"))
|
|
224
|
+
retractRow(ledger, "F1", "wrong page")
|
|
225
|
+
ledger.events[0].replacement = "F2"
|
|
226
|
+
expect(validateLedger(ledger).some((e: string) => /F1 is retracted but names replacement F2/.test(e))).toBe(true)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test("the last event for a fact must match its row status", () => {
|
|
230
|
+
const ledger = ledgerWith(measured("a"), measured("b"))
|
|
231
|
+
retractRow(ledger, "F1", "wrong page")
|
|
232
|
+
ledger.rows[0].status = "active"
|
|
233
|
+
expect(validateLedger(ledger).some((e: string) => /F1 has status active but its last event moves it to retracted/.test(e))).toBe(true)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test("a non-active row with no event is an error", () => {
|
|
237
|
+
const ledger = ledgerWith(measured("a"))
|
|
238
|
+
ledger.rows[0].status = "superseded"
|
|
239
|
+
expect(validateLedger(ledger).some((e: string) => /F1 is superseded but has no status event/.test(e))).toBe(true)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test("an event with an invalid from or to status is an error naming the fact", () => {
|
|
243
|
+
const ledger = ledgerWith(measured("a"))
|
|
244
|
+
retractRow(ledger, "F1", "wrong page")
|
|
245
|
+
ledger.events[0].from = "gone"
|
|
246
|
+
const errors = validateLedger(ledger)
|
|
247
|
+
expect(errors.some((e: string) => /events\[0\]: F1 is active, but the event moves it from gone/.test(e))).toBe(true)
|
|
248
|
+
ledger.events[0].from = "active"
|
|
249
|
+
ledger.events[0].to = "active"
|
|
250
|
+
expect(validateLedger(ledger).some((e: string) => /events\[0\]: F1 cannot move from active to active/.test(e))).toBe(true)
|
|
251
|
+
})
|
|
252
|
+
})
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// fidelity-brief.mjs - parse the `## Fidelity` block of brief.md and decide whether it is locked.
|
|
2
|
+
//
|
|
3
|
+
// Grammar (fable-interview.md section 3, "Parser contract"): the block starts at a line
|
|
4
|
+
// `## Fidelity` and ends at the next `## ` heading. It holds the seven field lines below, in any
|
|
5
|
+
// order, each `<field>: <value> (answered_by: user|default|request|pending)`, plus an optional
|
|
6
|
+
// `asked: <field>, <field>` history line. Values carry no parentheses, and `answered_by: pending`
|
|
7
|
+
// is legal only with the value `pending`. Trailing whitespace on a line is ignored; a second
|
|
8
|
+
// `## Fidelity` block is a parse error. This module parses; it never interprets a value, which
|
|
9
|
+
// is each consumer's job. Plain Node ESM, node: builtins only.
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "node:fs"
|
|
12
|
+
|
|
13
|
+
export const FIDELITY_FIELDS = Object.freeze([
|
|
14
|
+
"as of",
|
|
15
|
+
"meaning check",
|
|
16
|
+
"source floor",
|
|
17
|
+
"derived numbers",
|
|
18
|
+
"languages",
|
|
19
|
+
"identifier classes",
|
|
20
|
+
"fallback",
|
|
21
|
+
])
|
|
22
|
+
export const FIDELITY_ANSWERED_BY = Object.freeze(["user", "default", "request", "pending"])
|
|
23
|
+
const LOCKED_ANSWERED_BY = new Set(["user", "default", "request"])
|
|
24
|
+
|
|
25
|
+
export const FIDELITY_INCOMPLETE = "fidelity_interview_incomplete"
|
|
26
|
+
const INCOMPLETE_HINT =
|
|
27
|
+
"A fidelity field is pending or missing in brief.md; answer the open question or record its default, then rerun."
|
|
28
|
+
|
|
29
|
+
const BLOCK_HEADING = /^## Fidelity\s*$/
|
|
30
|
+
const NEXT_HEADING = /^## /
|
|
31
|
+
const FIELD_LINE = /^([a-z][a-z ]*[a-z]): ([^()]+) \(answered_by: (user|default|request|pending)\)$/
|
|
32
|
+
const ASKED_LINE = /^asked:(.*)$/
|
|
33
|
+
|
|
34
|
+
function blockLines(markdown) {
|
|
35
|
+
const lines = String(markdown)
|
|
36
|
+
.split(/\r?\n/)
|
|
37
|
+
.map((line) => line.trimEnd())
|
|
38
|
+
const headings = lines.flatMap((line, index) => (BLOCK_HEADING.test(line) ? [index] : []))
|
|
39
|
+
if (headings.length === 0) return null
|
|
40
|
+
const [start, ...duplicates] = headings
|
|
41
|
+
const body = []
|
|
42
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
43
|
+
if (NEXT_HEADING.test(lines[index])) break
|
|
44
|
+
body.push({ text: lines[index], line: index + 1 })
|
|
45
|
+
}
|
|
46
|
+
return { body, duplicates: duplicates.map((index) => index + 1) }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseAsked(rest, line, text, errors) {
|
|
50
|
+
const names = rest
|
|
51
|
+
.split(",")
|
|
52
|
+
.map((name) => name.trim())
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
const unknown = names.filter((name) => !FIDELITY_FIELDS.includes(name))
|
|
55
|
+
if (unknown.length > 0) {
|
|
56
|
+
errors.push(`line ${line}: malformed asked line, unknown field ${unknown.join(", ")}: ${text}`)
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
return names
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseFieldLine({ text, line }, parsed, errors) {
|
|
63
|
+
const match = FIELD_LINE.exec(text)
|
|
64
|
+
if (!match) return errors.push(`line ${line}: malformed fidelity line: ${text}`)
|
|
65
|
+
const [, field, rawValue, answeredBy] = match
|
|
66
|
+
const value = rawValue.trim()
|
|
67
|
+
if (!FIDELITY_FIELDS.includes(field)) return errors.push(`line ${line}: malformed fidelity line, unknown field "${field}": ${text}`)
|
|
68
|
+
if (Object.hasOwn(parsed.fields, field)) return errors.push(`line ${line}: malformed fidelity line, duplicate field "${field}": ${text}`)
|
|
69
|
+
if ((answeredBy === "pending") !== (value === "pending")) {
|
|
70
|
+
return errors.push(`line ${line}: malformed fidelity line, value pending and answered_by: pending go together: ${text}`)
|
|
71
|
+
}
|
|
72
|
+
parsed.fields[field] = value
|
|
73
|
+
parsed.answered_by[field] = answeredBy
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} briefMd the whole brief.md text
|
|
78
|
+
* @returns {{ present: boolean, fields: Record<string, string>, answered_by: Record<string, string>,
|
|
79
|
+
* asked: string[], missing: string[], pending: string[], errors: string[] }}
|
|
80
|
+
*/
|
|
81
|
+
export function parseFidelityBlock(briefMd) {
|
|
82
|
+
const parsed = { present: false, fields: {}, answered_by: {}, asked: [], missing: [], pending: [], errors: [] }
|
|
83
|
+
const block = blockLines(briefMd)
|
|
84
|
+
if (block !== null) {
|
|
85
|
+
const { body, duplicates } = block
|
|
86
|
+
parsed.present = true
|
|
87
|
+
for (const line of duplicates) parsed.errors.push(`line ${line}: duplicate ## Fidelity block, only one is allowed`)
|
|
88
|
+
let sawAsked = false
|
|
89
|
+
for (const entry of body) {
|
|
90
|
+
if (entry.text.trim() === "") continue
|
|
91
|
+
const asked = ASKED_LINE.exec(entry.text)
|
|
92
|
+
if (!asked) {
|
|
93
|
+
parseFieldLine(entry, parsed, parsed.errors)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
if (sawAsked) parsed.errors.push(`line ${entry.line}: malformed asked line, duplicate: ${entry.text}`)
|
|
97
|
+
else parsed.asked = parseAsked(asked[1], entry.line, entry.text, parsed.errors)
|
|
98
|
+
sawAsked = true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
parsed.missing = FIDELITY_FIELDS.filter((field) => !Object.hasOwn(parsed.fields, field))
|
|
102
|
+
parsed.pending = FIDELITY_FIELDS.filter((field) => parsed.answered_by[field] === "pending")
|
|
103
|
+
return parsed
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Locked = no parse error and all seven fields present with answered_by user, default, or request. */
|
|
107
|
+
export function isLocked(parsed) {
|
|
108
|
+
if (!parsed || parsed.errors.length > 0) return false
|
|
109
|
+
return FIDELITY_FIELDS.every((field) => Object.hasOwn(parsed.fields, field) && LOCKED_ANSWERED_BY.has(parsed.answered_by[field]))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function incompleteDefect(briefPath, parsed) {
|
|
113
|
+
const reasons = []
|
|
114
|
+
if (!parsed.present) reasons.push("no ## Fidelity block")
|
|
115
|
+
else if (parsed.missing.length > 0) reasons.push(`missing: ${parsed.missing.join(", ")}`)
|
|
116
|
+
if (parsed.pending.length > 0) reasons.push(`pending: ${parsed.pending.join(", ")}`)
|
|
117
|
+
return {
|
|
118
|
+
code: FIDELITY_INCOMPLETE,
|
|
119
|
+
severity: "blocker",
|
|
120
|
+
integrity: true,
|
|
121
|
+
selector: null,
|
|
122
|
+
line: null,
|
|
123
|
+
message: `${briefPath}: fidelity interview incomplete (${reasons.join("; ")})`,
|
|
124
|
+
hint: INCOMPLETE_HINT,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The lock check the reader-facing CLI stages run before writing anything.
|
|
130
|
+
* exitCode 0 = locked; 1 = pending, missing, or no brief (one `fidelity_interview_incomplete`
|
|
131
|
+
* defect); 2 = malformed block (usage error, `errors` name the lines, no defect).
|
|
132
|
+
* @param {string} briefPath
|
|
133
|
+
*/
|
|
134
|
+
export function requireLockedBrief(briefPath) {
|
|
135
|
+
let markdown = ""
|
|
136
|
+
try {
|
|
137
|
+
markdown = readFileSync(briefPath, "utf8")
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error?.code !== "ENOENT") throw error
|
|
140
|
+
}
|
|
141
|
+
const parsed = parseFidelityBlock(markdown)
|
|
142
|
+
if (parsed.errors.length > 0) return { locked: false, exitCode: 2, defects: [], errors: parsed.errors, parsed }
|
|
143
|
+
if (isLocked(parsed)) return { locked: true, exitCode: 0, defects: [], errors: [], parsed }
|
|
144
|
+
return { locked: false, exitCode: 1, defects: [incompleteDefect(briefPath, parsed)], errors: [], parsed }
|
|
145
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
import { FIDELITY_FIELDS, isLocked, parseFidelityBlock, requireLockedBrief } from "./fidelity-brief.mjs"
|
|
7
|
+
|
|
8
|
+
const fixture = (name: string) => join(import.meta.dir, "tests", "fixtures", name)
|
|
9
|
+
const lockedMd = await Bun.file(fixture("fidelity-brief-locked.md")).text()
|
|
10
|
+
const pendingMd = await Bun.file(fixture("fidelity-brief-pending.md")).text()
|
|
11
|
+
|
|
12
|
+
function withoutLine(markdown: string, prefix: string): string {
|
|
13
|
+
return markdown
|
|
14
|
+
.split("\n")
|
|
15
|
+
.filter((line) => !line.startsWith(prefix))
|
|
16
|
+
.join("\n")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("parseFidelityBlock", () => {
|
|
20
|
+
test("#given a complete block #when parsed #then seven fields are read and the brief is locked", () => {
|
|
21
|
+
// when
|
|
22
|
+
const parsed = parseFidelityBlock(lockedMd)
|
|
23
|
+
// then
|
|
24
|
+
expect(parsed.errors).toEqual([])
|
|
25
|
+
expect(Object.keys(parsed.fields).sort()).toEqual([...FIDELITY_FIELDS].sort())
|
|
26
|
+
expect(parsed.fields["as of"]).toBe("latest at run start, 2026-10-23")
|
|
27
|
+
expect(parsed.fields.languages).toBe("report ko; sources ko, en; quotes original with translation beside")
|
|
28
|
+
expect(parsed.answered_by["derived numbers"]).toBe("user")
|
|
29
|
+
expect(parsed.answered_by.fallback).toBe("request")
|
|
30
|
+
expect(parsed.asked).toEqual(["derived numbers"])
|
|
31
|
+
expect(isLocked(parsed)).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test("#given a block missing as of #when parsed #then it is not locked and the missing field is named", () => {
|
|
35
|
+
// when
|
|
36
|
+
const parsed = parseFidelityBlock(withoutLine(lockedMd, "as of:"))
|
|
37
|
+
// then
|
|
38
|
+
expect(isLocked(parsed)).toBe(false)
|
|
39
|
+
expect(parsed.missing).toEqual(["as of"])
|
|
40
|
+
expect(parsed.errors).toEqual([])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("#given a field answered_by pending #when parsed #then it is not locked and not malformed", () => {
|
|
44
|
+
// when
|
|
45
|
+
const parsed = parseFidelityBlock(pendingMd)
|
|
46
|
+
// then
|
|
47
|
+
expect(parsed.errors).toEqual([])
|
|
48
|
+
expect(parsed.answered_by["derived numbers"]).toBe("pending")
|
|
49
|
+
expect(parsed.pending).toEqual(["derived numbers"])
|
|
50
|
+
expect(isLocked(parsed)).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("#given a malformed field line #when parsed #then errors name the line as malformed", () => {
|
|
54
|
+
// given
|
|
55
|
+
const markdown = lockedMd.replace("meaning check: on (answered_by: default)", "meaning check: on")
|
|
56
|
+
// when
|
|
57
|
+
const parsed = parseFidelityBlock(markdown)
|
|
58
|
+
// then
|
|
59
|
+
expect(parsed.errors.length).toBe(1)
|
|
60
|
+
expect(parsed.errors[0]).toContain("malformed")
|
|
61
|
+
expect(parsed.errors[0]).toContain("meaning check: on")
|
|
62
|
+
expect(isLocked(parsed)).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("#given grammar violations #when parsed #then each is a malformed error", () => {
|
|
66
|
+
const cases = [
|
|
67
|
+
lockedMd.replace("fallback: block-quote (answered_by: request)", "fallback: block-quote (answered_by: someone)"),
|
|
68
|
+
lockedMd.replace("meaning check: on (answered_by: default)", "meaning check: on (maybe) (answered_by: default)"),
|
|
69
|
+
lockedMd.replace("meaning check: on (answered_by: default)", "meaning check: on (answered_by: pending)"),
|
|
70
|
+
lockedMd.replace("asked: derived numbers", "asked: derived numbers\ntone: formal (answered_by: user)"),
|
|
71
|
+
lockedMd.replace("asked: derived numbers", "asked: derived numbers\nfallback: omit (answered_by: user)"),
|
|
72
|
+
lockedMd.replace("asked: derived numbers", "asked: tone"),
|
|
73
|
+
]
|
|
74
|
+
for (const markdown of cases) expect(parseFidelityBlock(markdown).errors.length).toBe(1)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("#given a brief with no Fidelity block #when parsed #then it is not locked and every field is missing", () => {
|
|
78
|
+
// when
|
|
79
|
+
const parsed = parseFidelityBlock("# Brief\n\n## Deliverable\nformat: pdf (answered_by: request)\n")
|
|
80
|
+
// then
|
|
81
|
+
expect(parsed.present).toBe(false)
|
|
82
|
+
expect(parsed.missing).toEqual([...FIDELITY_FIELDS])
|
|
83
|
+
expect(isLocked(parsed)).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("#given field lines after the next heading #when parsed #then they are outside the block", () => {
|
|
87
|
+
// given
|
|
88
|
+
const markdown = withoutLine(lockedMd, "fallback:") + "\nfallback: omit (answered_by: user)\n"
|
|
89
|
+
// then
|
|
90
|
+
expect(parseFidelityBlock(markdown).missing).toEqual(["fallback"])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("#given a second Fidelity block #when parsed #then the duplicate is an error", () => {
|
|
94
|
+
// given
|
|
95
|
+
const markdown = lockedMd + "\n## Fidelity\nas of: x (answered_by: user)\n"
|
|
96
|
+
// when
|
|
97
|
+
const parsed = parseFidelityBlock(markdown)
|
|
98
|
+
// then
|
|
99
|
+
expect(parsed.errors.length).toBe(1)
|
|
100
|
+
expect(parsed.errors[0]).toContain("duplicate")
|
|
101
|
+
expect(parsed.fields["as of"]).toBe("latest at run start, 2026-10-23")
|
|
102
|
+
expect(isLocked(parsed)).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe("requireLockedBrief", () => {
|
|
107
|
+
function briefFile(contents: string | null): { path: string; cleanup: () => void } {
|
|
108
|
+
const dir = mkdtempSync(join(tmpdir(), "fidelity-brief-"))
|
|
109
|
+
const path = join(dir, "brief.md")
|
|
110
|
+
if (contents !== null) writeFileSync(path, contents)
|
|
111
|
+
return { path, cleanup: () => rmSync(dir, { recursive: true, force: true }) }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
test("#given a locked brief #when required #then exit 0 and no defect", () => {
|
|
115
|
+
const brief = briefFile(lockedMd)
|
|
116
|
+
try {
|
|
117
|
+
expect(requireLockedBrief(brief.path)).toMatchObject({ locked: true, exitCode: 0, defects: [], errors: [] })
|
|
118
|
+
} finally {
|
|
119
|
+
brief.cleanup()
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test("#given field lines with trailing spaces, a tab, and CRLF endings #when required #then they parse like trimmed lines", () => {
|
|
124
|
+
// given
|
|
125
|
+
const markdown = lockedMd
|
|
126
|
+
.replace("meaning check: on (answered_by: default)", "meaning check: on (answered_by: default) ")
|
|
127
|
+
.replace("fallback: block-quote (answered_by: request)", "fallback: block-quote (answered_by: request)\t")
|
|
128
|
+
.replace("as of: latest at run start, 2026-10-23 (answered_by: default)\n", "as of: latest at run start, 2026-10-23 (answered_by: default) \r\n")
|
|
129
|
+
const brief = briefFile(markdown)
|
|
130
|
+
try {
|
|
131
|
+
// when
|
|
132
|
+
const result = requireLockedBrief(brief.path)
|
|
133
|
+
// then
|
|
134
|
+
expect(result).toMatchObject({ locked: true, exitCode: 0, defects: [], errors: [] })
|
|
135
|
+
expect(result.parsed.fields.fallback).toBe("block-quote")
|
|
136
|
+
} finally {
|
|
137
|
+
brief.cleanup()
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test("#given a second Fidelity block #when required #then exit 2 naming the duplicate", () => {
|
|
142
|
+
const brief = briefFile(lockedMd + "\n## Fidelity\nas of: x (answered_by: user)\n")
|
|
143
|
+
try {
|
|
144
|
+
const result = requireLockedBrief(brief.path)
|
|
145
|
+
expect(result).toMatchObject({ locked: false, exitCode: 2, defects: [] })
|
|
146
|
+
expect(result.errors[0]).toContain("duplicate ## Fidelity block")
|
|
147
|
+
} finally {
|
|
148
|
+
brief.cleanup()
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test("#given a pending, partial, or absent brief #when required #then exit 1 with the single lock defect", () => {
|
|
153
|
+
for (const contents of [pendingMd, withoutLine(lockedMd, "as of:"), "# Brief\n", null]) {
|
|
154
|
+
const brief = briefFile(contents)
|
|
155
|
+
try {
|
|
156
|
+
const result = requireLockedBrief(brief.path)
|
|
157
|
+
expect(result.locked).toBe(false)
|
|
158
|
+
expect(result.exitCode).toBe(1)
|
|
159
|
+
expect(result.defects.length).toBe(1)
|
|
160
|
+
expect(result.defects[0]).toMatchObject({ code: "fidelity_interview_incomplete", severity: "blocker", integrity: true })
|
|
161
|
+
expect(typeof result.defects[0].message).toBe("string")
|
|
162
|
+
expect(typeof result.defects[0].hint).toBe("string")
|
|
163
|
+
} finally {
|
|
164
|
+
brief.cleanup()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test("#given a malformed brief #when required #then exit 2 with the parse errors and no defect", () => {
|
|
170
|
+
const brief = briefFile(lockedMd.replace("meaning check: on (answered_by: default)", "meaning check: on"))
|
|
171
|
+
try {
|
|
172
|
+
const result = requireLockedBrief(brief.path)
|
|
173
|
+
expect(result).toMatchObject({ locked: false, exitCode: 2, defects: [] })
|
|
174
|
+
expect(result.errors[0]).toContain("malformed")
|
|
175
|
+
} finally {
|
|
176
|
+
brief.cleanup()
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// fidelity-state.mjs - per-sentence failure counts for the fidelity repair loop. A sentence is
|
|
2
|
+
// keyed by its section slug and the sorted fact ids it cites, so a reworded sentence that
|
|
3
|
+
// cites the same facts keeps its count. node: builtins only.
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs"
|
|
6
|
+
import { join } from "node:path"
|
|
7
|
+
|
|
8
|
+
import { CliError, readJsonFile, writeJsonAtomic } from "./cli-support.mjs"
|
|
9
|
+
|
|
10
|
+
export const STATE_FILENAME = "fidelity-state.json"
|
|
11
|
+
export const STATE_SCHEMA_VERSION = 1
|
|
12
|
+
|
|
13
|
+
/** slug + '#' + the fact ids sorted and joined by ','. */
|
|
14
|
+
export function sentenceKey(sectionSlug, factIds) {
|
|
15
|
+
return `${sectionSlug}#${[...factIds].sort().join(",")}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function emptyState() {
|
|
19
|
+
return { schemaVersion: STATE_SCHEMA_VERSION, failures: {}, replaced: [] }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isValid(state) {
|
|
23
|
+
return (
|
|
24
|
+
state !== null &&
|
|
25
|
+
typeof state === "object" &&
|
|
26
|
+
state.schemaVersion === STATE_SCHEMA_VERSION &&
|
|
27
|
+
state.failures !== null &&
|
|
28
|
+
typeof state.failures === "object" &&
|
|
29
|
+
!Array.isArray(state.failures) &&
|
|
30
|
+
Object.values(state.failures).every((count) => Number.isInteger(count) && count >= 0) &&
|
|
31
|
+
Array.isArray(state.replaced) &&
|
|
32
|
+
state.replaced.every((key) => typeof key === "string")
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readState(dir) {
|
|
37
|
+
const file = join(dir, STATE_FILENAME)
|
|
38
|
+
if (!existsSync(file)) return emptyState()
|
|
39
|
+
const state = readJsonFile(file)
|
|
40
|
+
if (!isValid(state)) throw new CliError(`${file} is not a valid fidelity state`)
|
|
41
|
+
return state
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function recordFailure(state, key) {
|
|
45
|
+
state.failures[key] = (state.failures[key] ?? 0) + 1
|
|
46
|
+
return state.failures[key]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function failureCount(state, key) {
|
|
50
|
+
return state.failures[key] ?? 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function markReplaced(state, key) {
|
|
54
|
+
if (!state.replaced.includes(key)) state.replaced.push(key)
|
|
55
|
+
return state
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function writeState(dir, state) {
|
|
59
|
+
if (!isValid(state)) throw new CliError("refusing to write an invalid fidelity state")
|
|
60
|
+
const file = join(dir, STATE_FILENAME)
|
|
61
|
+
writeJsonAtomic(file, state)
|
|
62
|
+
return file
|
|
63
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, onTestFinished, test } from "bun:test"
|
|
2
|
+
import { mkdtempSync, readdirSync, rmSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
import { failureCount, markReplaced, readState, recordFailure, sentenceKey, writeState } from "./fidelity-state.mjs"
|
|
7
|
+
|
|
8
|
+
function tempDir(): string {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), "ulw-fidelity-"))
|
|
10
|
+
onTestFinished(() => rmSync(dir, { recursive: true, force: true }))
|
|
11
|
+
return dir
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("fidelity state", () => {
|
|
15
|
+
test("sentenceKey is order-independent on ids", () => {
|
|
16
|
+
expect(sentenceKey("findings", ["F3", "Q1", "F12"])).toBe(sentenceKey("findings", ["F12", "F3", "Q1"]))
|
|
17
|
+
expect(sentenceKey("findings", ["F2", "F1"])).toBe("findings#F1,F2")
|
|
18
|
+
expect(sentenceKey("findings", ["F1"])).not.toBe(sentenceKey("other", ["F1"]))
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("failureCount increments per key", () => {
|
|
22
|
+
const dir = tempDir()
|
|
23
|
+
const state = readState(dir)
|
|
24
|
+
const key = sentenceKey("findings", ["F1"])
|
|
25
|
+
expect(failureCount(state, key)).toBe(0)
|
|
26
|
+
recordFailure(state, key)
|
|
27
|
+
recordFailure(state, key)
|
|
28
|
+
expect(failureCount(state, key)).toBe(2)
|
|
29
|
+
expect(failureCount(state, sentenceKey("findings", ["F2"]))).toBe(0)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("markReplaced records the key and survives a write/read", () => {
|
|
33
|
+
const dir = tempDir()
|
|
34
|
+
const state = readState(dir)
|
|
35
|
+
const key = sentenceKey("findings", ["Q1"])
|
|
36
|
+
recordFailure(state, key)
|
|
37
|
+
markReplaced(state, key)
|
|
38
|
+
markReplaced(state, key)
|
|
39
|
+
expect(state.replaced).toEqual([key])
|
|
40
|
+
writeState(dir, state)
|
|
41
|
+
expect(readdirSync(dir)).toEqual(["fidelity-state.json"])
|
|
42
|
+
const loaded = readState(dir)
|
|
43
|
+
expect(loaded).toEqual(state)
|
|
44
|
+
expect(failureCount(loaded, key)).toBe(1)
|
|
45
|
+
})
|
|
46
|
+
})
|