lazycodex-ai 5.1.44 → 5.1.46
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/README.de.md +0 -5
- package/README.es.md +0 -5
- package/README.fr.md +0 -5
- package/README.it.md +0 -5
- package/README.ja.md +0 -5
- package/README.ko.md +0 -5
- package/README.md +4 -5
- package/README.pt-br.md +0 -5
- package/README.ru.md +0 -6
- package/README.zh-cn.md +0 -5
- package/dist/cli/index.js +865 -413
- package/dist/cli/service-auth/keystore.d.ts +4 -2
- package/dist/cli-node/index.js +865 -413
- package/docs/reference/web-terminal-visual-qa.md +8 -0
- package/package.json +1 -1
- package/packages/lsp-daemon/dist/cli.js +7 -7
- package/packages/lsp-daemon/dist/client.js +7 -7
- package/packages/lsp-daemon/dist/index.js +7 -7
- package/packages/lsp-daemon/package.json +3 -3
- package/packages/lsp-tools-mcp/dist/cli.js +7 -7
- package/packages/lsp-tools-mcp/dist/mcp.js +7 -7
- 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 +77 -26
- 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 +14 -9
- package/packages/omo-codex/plugin/skills/ast-grep/AGENTS.md +1 -1
- 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/ATTRIBUTION.md +2 -2
- 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/ultimate-browsing/engine/AGENTS.md +1 -1
- 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 +21 -3
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +37 -12
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +77 -16
- package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +134 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger.md +95 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +68 -3
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-visualization.md +2 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/unit-rules.md +157 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/claims-index.mjs +125 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +19 -5
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +89 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +239 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-derive.mjs +66 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-expr.mjs +108 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +194 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +250 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +221 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +123 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-locate.mjs +231 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +150 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +64 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +29 -9
- 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/html-lite.mjs +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-markup.mjs +182 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/lexicon.mjs +47 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/md-lite.mjs +193 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/meaning-check.mjs +196 -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 +8 -8
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/particles-ko.mjs +108 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/placeholder.mjs +184 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-apply.mjs +115 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-diff.mjs +31 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-project.mjs +125 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-rules.mjs +170 -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 +21 -10
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +15 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/sentence-split.mjs +77 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +140 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-snapshot.mjs +176 -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/unit-algebra.mjs +101 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-errors.mjs +23 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-precision.mjs +44 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules.mjs +173 -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-codex-compatibility.test.mjs +19 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +12 -8
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +169 -30
- package/packages/shared-skills/skills/ast-grep/AGENTS.md +1 -1
- 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/ATTRIBUTION.md +2 -2
- 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/ultimate-browsing/engine/AGENTS.md +1 -1
- package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
- package/packages/shared-skills/skills/ulw-research/AGENTS.md +21 -3
- package/packages/shared-skills/skills/ulw-research/SKILL.md +37 -12
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +77 -16
- package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +134 -0
- package/packages/shared-skills/skills/ulw-research/references/fact-ledger.md +95 -0
- package/packages/shared-skills/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
- package/packages/shared-skills/skills/ulw-research/references/report-gates.md +68 -3
- package/packages/shared-skills/skills/ulw-research/references/report-visualization.md +2 -0
- package/packages/shared-skills/skills/ulw-research/references/unit-rules.md +157 -0
- package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
- package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.test.ts +193 -0
- 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/cli-support.mjs +19 -5
- package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +13 -2
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +89 -3
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +100 -12
- package/packages/shared-skills/skills/ulw-research/scripts/copy-returns.test.ts +183 -0
- package/packages/shared-skills/skills/ulw-research/scripts/delivery-barrier.test.ts +245 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +239 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.mjs +66 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.test.ts +375 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-expr.mjs +108 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +194 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.test.ts +221 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +197 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +250 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +156 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +160 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +221 -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 +123 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +254 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.mjs +231 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.test.ts +118 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.test.ts +167 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +409 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-rules.test.ts +388 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.test.ts +81 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +150 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.test.ts +179 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.test.ts +117 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +64 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.test.ts +46 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.test.ts +58 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.test.ts +270 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-context.test.ts +64 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.test.ts +73 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +29 -9
- 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/html-lite.mjs +2 -2
- package/packages/shared-skills/skills/ulw-research/scripts/html-markup.mjs +182 -0
- 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/md-lite.mjs +193 -0
- package/packages/shared-skills/skills/ulw-research/scripts/md-lite.test.ts +271 -0
- package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.mjs +196 -0
- package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.test.ts +228 -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 +8 -8
- 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/placeholder.mjs +184 -0
- package/packages/shared-skills/skills/ulw-research/scripts/placeholder.test.ts +77 -0
- package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.mjs +115 -0
- package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.test.ts +363 -0
- package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.mjs +31 -0
- package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.test.ts +168 -0
- package/packages/shared-skills/skills/ulw-research/scripts/polish-markup.test.ts +187 -0
- package/packages/shared-skills/skills/ulw-research/scripts/polish-project.mjs +125 -0
- package/packages/shared-skills/skills/ulw-research/scripts/polish-rules.mjs +170 -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 +21 -10
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +15 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +458 -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/source-commands.mjs +140 -0
- package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
- package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.test.ts +113 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/assets/share.csv +3 -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/gen-legacy-dev.mjs +89 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-basic.json +223 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-legacy-dev.json +859 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-verify.json +420 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-bad.html +19 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-good.html +39 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/acronyms.json +3 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/meta.json +9 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/sentences.jsonl +7 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/text.txt +7 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4.html +19 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/acronyms.json +1 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/meta.json +9 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/sentences.jsonl +1 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/text.txt +1 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S9.txt +1 -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/unit-algebra.mjs +101 -0
- package/packages/shared-skills/skills/ulw-research/scripts/unit-errors.mjs +23 -0
- package/packages/shared-skills/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
- package/packages/shared-skills/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
- package/packages/shared-skills/skills/ulw-research/scripts/unit-precision.mjs +44 -0
- package/packages/shared-skills/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
- package/packages/shared-skills/skills/ulw-research/scripts/unit-rules.mjs +173 -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,194 @@
|
|
|
1
|
+
// fact-fill.mjs - the fill step: substitute every fact pointer in a report source from the
|
|
2
|
+
// ledger, all or nothing. Fatal errors (pointer_label_mismatch, unresolved_placeholder,
|
|
3
|
+
// quote_anchor_unresolved, unit_duplicated) return out null; warnings (particle_unverified,
|
|
4
|
+
// fallback_without_entity) never block. A twice-failed sentence can be replaced by an attributed
|
|
5
|
+
// block quote of its source row (replaceFailed). No IO, but not pure: unless options.view is set,
|
|
6
|
+
// it records each replaced sentence in options.replaceFailed through markReplaced. node: builtins only.
|
|
7
|
+
|
|
8
|
+
import { finalReading, rewriteParticle } from "./particles-ko.mjs"
|
|
9
|
+
import { failureCount, markReplaced, sentenceKey } from "./fidelity-state.mjs"
|
|
10
|
+
import { findPlaceholders, lead, qualifiedText, renderBlockquote, renderQuote, renderValueHtml, subSpan, valueParts } from "./placeholder.mjs"
|
|
11
|
+
import { splitSentences } from "./sentence-split.mjs"
|
|
12
|
+
|
|
13
|
+
const FORMATS = Object.freeze(["html", "md"])
|
|
14
|
+
const TRANSLATION = Object.freeze({
|
|
15
|
+
html: /^\s*<span data-translates="(Q\d+)">[\s\S]*?<\/span>/u,
|
|
16
|
+
md: /^\s*<!-- translates: (Q\d+) -->[^\n]*/u,
|
|
17
|
+
})
|
|
18
|
+
const HEADING = Object.freeze({ html: /<h[1-6]\b([^>]*)>([\s\S]*?)<\/h[1-6]>/giu, md: /^#{1,6}[ \t]+(.+)$/gmu })
|
|
19
|
+
|
|
20
|
+
const lineOf = (text, at) => text.slice(0, at).split("\n").length
|
|
21
|
+
|
|
22
|
+
function resolveValue(row, parsed, format) {
|
|
23
|
+
const { valueText, unitText } = valueParts(row)
|
|
24
|
+
const text = { full: row.raw, value: valueText, unit: unitText, q: qualifiedText(row) }[parsed.mode]
|
|
25
|
+
if (text === null) return { code: "quote_anchor_unresolved", message: `${row.fact_id} has qualifier ${row.qualifier}, but its quote holds no qualifier beside "${row.raw}"` }
|
|
26
|
+
return { row, parsed, visible: text, rendered: format === "html" ? renderValueHtml(row, text) : text }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveQuote(row, parsed, format) {
|
|
30
|
+
const whole = { text: row.quote, start: 0, end: row.quote.length }
|
|
31
|
+
const selection = parsed.mode === "lead" ? lead(row.quote, parsed.lead) : parsed.mode === "span" ? subSpan(row.quote, parsed.from, parsed.to) : whole
|
|
32
|
+
if (selection === null) {
|
|
33
|
+
return { code: "quote_anchor_unresolved", message: `from "${parsed.from}" and to "${parsed.to}" must each occur exactly once in ${row.fact_id}'s quote, in that order` }
|
|
34
|
+
}
|
|
35
|
+
const rendered = renderQuote(row, parsed.selector, selection.text, format)
|
|
36
|
+
return { row, parsed, visible: `[${row.source_id}]`, rendered, selector: parsed.selector, quote_span: [selection.start, selection.end] }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolve(found, rows, format) {
|
|
40
|
+
const { parsed } = found
|
|
41
|
+
if (parsed.error) return { code: "unresolved_placeholder", message: parsed.error }
|
|
42
|
+
const row = rows.get(parsed.id)
|
|
43
|
+
if (row === undefined) return { code: "unresolved_placeholder", message: `${parsed.id} is not in the fact ledger` }
|
|
44
|
+
if (row.status !== "active") return { code: "unresolved_placeholder", message: `${parsed.id} is ${row.status}; cite its replacement` }
|
|
45
|
+
if (row.label !== parsed.label) return { code: "pointer_label_mismatch", message: `${parsed.id} has label "${row.label}", not "${parsed.label}"` }
|
|
46
|
+
return parsed.kind === "Q" ? resolveQuote(row, parsed, format) : resolveValue(row, parsed, format)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A unit typed right after a pointer that already renders it: never absorbed, always an error. */
|
|
50
|
+
function unitDuplicated(text, item) {
|
|
51
|
+
if (item.parsed.kind !== "F" || (item.parsed.mode !== "full" && item.parsed.mode !== "q")) return false
|
|
52
|
+
const suffix = item.row.raw.replace(/^[\s\S]*\d/u, "").trim()
|
|
53
|
+
const units = [item.row.unit, valueParts(item.row).unitText, suffix].filter((unit) => typeof unit === "string" && unit !== "")
|
|
54
|
+
const following = text.slice(item.end).replace(/^[ \t]+/u, "")
|
|
55
|
+
return units.some((unit) => following.startsWith(unit) && !/[A-Za-z0-9]/u.test(following[unit.length] ?? ""))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* Sentence-key rules (fidelity-state.mjs sentenceKey(slug, ids)); any other producer of keys must match:
|
|
60
|
+
* - slug source: an HTML heading's id attribute wins over its text; md headings use their text.
|
|
61
|
+
* - slugify: strip tags, lowercase, turn each run of non-letters/digits (\p{L}\p{N}) into "-",
|
|
62
|
+
* trim leading and trailing "-".
|
|
63
|
+
* - a sentence takes the nearest heading that starts strictly before the sentence's start offset;
|
|
64
|
+
* a sentence before the first heading gets slug "".
|
|
65
|
+
* - fact ids cited in the sentence are de-duplicated before sentenceKey sorts them.
|
|
66
|
+
*/
|
|
67
|
+
function slugify(text) {
|
|
68
|
+
return text
|
|
69
|
+
.replace(/<[^>]*>/gu, "")
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
|
72
|
+
.replace(/^-+|-+$/gu, "")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function headings(text, format) {
|
|
76
|
+
return [...text.matchAll(HEADING[format])].map((match) => {
|
|
77
|
+
const id = format === "html" ? /\bid="([^"]*)"/u.exec(match[1])?.[1] : undefined
|
|
78
|
+
return { at: match.index, slug: slugify(id ?? match[match.length - 1]) }
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Sentence offsets over the source with pointers, comments and (html) tags blanked to equal length. */
|
|
83
|
+
function sentencesOf(text, found, format) {
|
|
84
|
+
let masked = text
|
|
85
|
+
for (const item of found) masked = masked.slice(0, item.start) + "x".repeat(item.end - item.start) + masked.slice(item.end)
|
|
86
|
+
const blank = (part) => part.replace(/[^\n]/gu, " ")
|
|
87
|
+
masked = masked.replace(/<!--[\s\S]*?-->/gu, blank)
|
|
88
|
+
if (format === "html") masked = masked.replace(/<[^>]*>/gu, blank)
|
|
89
|
+
return splitSentences(masked)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function replacements(text, found, items, options, warnings) {
|
|
93
|
+
const { format, replaceFailed: state } = options
|
|
94
|
+
const marks = headings(text, format)
|
|
95
|
+
const out = []
|
|
96
|
+
for (const sentence of sentencesOf(text, found, format)) {
|
|
97
|
+
const inside = items.filter((item) => item.start >= sentence.start && item.end <= sentence.end)
|
|
98
|
+
if (inside.length === 0) continue
|
|
99
|
+
const slug = marks.filter((mark) => mark.at < sentence.start).at(-1)?.slug ?? ""
|
|
100
|
+
const key = sentenceKey(slug, [...new Set(inside.map((item) => item.row.fact_id))])
|
|
101
|
+
if (failureCount(state, key) < 2) continue
|
|
102
|
+
const rows = inside.map((item) => item.row)
|
|
103
|
+
const source = rows.find((row) => row.kind === "quote") ?? rows.find((row) => row.source_id !== null && row.quote !== "")
|
|
104
|
+
if (source === undefined) continue
|
|
105
|
+
let rendered = options.fallback === "omit" ? "" : renderBlockquote(source, key, format)
|
|
106
|
+
if (rendered !== "" && format === "md") {
|
|
107
|
+
if (sentence.start > 0 && text[sentence.start - 1] !== "\n") rendered = `\n\n${rendered}`
|
|
108
|
+
if (sentence.end < text.length && text[sentence.end] !== "\n") rendered = `${rendered}\n\n`
|
|
109
|
+
}
|
|
110
|
+
if (rendered !== "" && source.entity === null) {
|
|
111
|
+
warnings.push({ code: "fallback_without_entity", token: key, line: lineOf(text, sentence.start), message: `${source.fact_id} has no entity; the block quote cites [${source.source_id}] alone` })
|
|
112
|
+
}
|
|
113
|
+
out.push({ start: sentence.start, end: sentence.end, key, source, rendered })
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The text after a pointer: rewrite a following Korean particle, and map a translation span. */
|
|
119
|
+
function afterPlaceholder(segment, item, context) {
|
|
120
|
+
const { format, text, warnings, fillMap } = context
|
|
121
|
+
const probeEnd = format === "html" && segment.includes("<") ? segment.indexOf("<") : segment.length
|
|
122
|
+
const result = rewriteParticle(segment.slice(0, probeEnd), finalReading(item.visible, null))
|
|
123
|
+
let next = segment
|
|
124
|
+
if (result.applied) next = result.text + segment.slice(probeEnd)
|
|
125
|
+
else if (result.candidate) {
|
|
126
|
+
warnings.push({ code: "particle_unverified", token: item.token, line: lineOf(text, item.start), message: `the particle after ${item.token} was not rewritten; check it by eye` })
|
|
127
|
+
}
|
|
128
|
+
const translation = item.parsed.kind === "Q" ? TRANSLATION[format].exec(next) : null
|
|
129
|
+
if (translation !== null && translation[1] === item.row.fact_id) {
|
|
130
|
+
const span = translation[0].trimStart()
|
|
131
|
+
const start = context.out.length + translation[0].length - span.length
|
|
132
|
+
fillMap.push({ token: span, fact_id: translation[1], translates: translation[1], start, end: start + span.length, rendered: span })
|
|
133
|
+
}
|
|
134
|
+
return next
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function assemble(text, parts, context) {
|
|
138
|
+
let cursor = 0
|
|
139
|
+
let previous = null
|
|
140
|
+
const flush = (until) => {
|
|
141
|
+
const segment = text.slice(cursor, until)
|
|
142
|
+
context.out += previous?.parsed ? afterPlaceholder(segment, previous, context) : segment
|
|
143
|
+
}
|
|
144
|
+
for (const part of parts) {
|
|
145
|
+
flush(part.start)
|
|
146
|
+
const start = context.out.length
|
|
147
|
+
if (part.parsed) {
|
|
148
|
+
const extra = part.selector === undefined ? {} : { selector: part.selector, quote_span: part.quote_span }
|
|
149
|
+
context.fillMap.push({ token: part.token, fact_id: part.row.fact_id, start, end: start + part.rendered.length, rendered: part.rendered, ...extra })
|
|
150
|
+
} else if (part.rendered !== "") {
|
|
151
|
+
context.fillMap.push({ token: null, fact_id: part.source.fact_id, replaced: part.key, start, end: start + part.rendered.length, rendered: part.rendered })
|
|
152
|
+
}
|
|
153
|
+
context.out += part.rendered
|
|
154
|
+
cursor = part.end
|
|
155
|
+
previous = part
|
|
156
|
+
}
|
|
157
|
+
flush(text.length)
|
|
158
|
+
return context.out
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} sourceText the report source with {{F..}} / {{Q..}} pointers
|
|
163
|
+
* @param {{ rows: object[] }} ledger
|
|
164
|
+
* @param {{ format?: "html"|"md", replaceFailed?: object, fallback?: "quote"|"omit", view?: boolean }} [options]
|
|
165
|
+
* @returns {{ out: string|null, fillMap: object[], errors: object[], warnings: object[] }}
|
|
166
|
+
*/
|
|
167
|
+
export function fill(sourceText, ledger, options = {}) {
|
|
168
|
+
const format = options.format ?? "html"
|
|
169
|
+
if (!FORMATS.includes(format)) throw new TypeError(`format must be one of ${FORMATS.join(", ")}`)
|
|
170
|
+
const rows = new Map(ledger.rows.map((row) => [row.fact_id, row]))
|
|
171
|
+
const found = findPlaceholders(sourceText)
|
|
172
|
+
const errors = []
|
|
173
|
+
const warnings = []
|
|
174
|
+
const items = []
|
|
175
|
+
for (const pointer of found) {
|
|
176
|
+
const resolved = resolve(pointer, rows, format)
|
|
177
|
+
const line = lineOf(sourceText, pointer.start)
|
|
178
|
+
if (resolved.code) errors.push({ code: resolved.code, token: pointer.token, line, message: resolved.message })
|
|
179
|
+
else {
|
|
180
|
+
const item = { ...pointer, ...resolved }
|
|
181
|
+
if (unitDuplicated(sourceText, item)) {
|
|
182
|
+
errors.push({ code: "unit_duplicated", token: pointer.token, line, message: `${pointer.token} already renders its unit; use {{${item.row.fact_id}:${item.row.label}:value}}` })
|
|
183
|
+
} else items.push(item)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (errors.length > 0) return { out: null, fillMap: [], errors, warnings }
|
|
187
|
+
const replaced = options.replaceFailed ? replacements(sourceText, found, items, { ...options, format }, warnings) : []
|
|
188
|
+
const kept = items.filter((item) => !replaced.some((block) => item.start >= block.start && item.end <= block.end))
|
|
189
|
+
const parts = [...kept, ...replaced].sort((a, b) => a.start - b.start)
|
|
190
|
+
const context = { format, text: sourceText, warnings, fillMap: [], out: "" }
|
|
191
|
+
const out = assemble(sourceText, parts, context)
|
|
192
|
+
if (!options.view) for (const block of replaced) markReplaced(options.replaceFailed, block.key)
|
|
193
|
+
return { out, fillMap: context.fillMap, errors, warnings }
|
|
194
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
import { fill } from "./fact-fill.mjs"
|
|
6
|
+
import { retractRow, supersedeRow, validateLedger } from "./fact-ledger.mjs"
|
|
7
|
+
import { failureCount, sentenceKey } from "./fidelity-state.mjs"
|
|
8
|
+
|
|
9
|
+
const LEDGER = JSON.parse(readFileSync(join(import.meta.dir, "tests/fixtures/fidelity/ledger-basic.json"), "utf8"))
|
|
10
|
+
const F12_DATA = '<data class="fact" value="320" data-fact="F12" data-unit="m" data-lineage="MEASURED">320 m</data>'
|
|
11
|
+
const QUOTE = "we estimate completion no earlier than 2027"
|
|
12
|
+
// Korean particles and words, as escapes: eul, girok, i, ollatda, ga, isang.
|
|
13
|
+
const EUL = "\uC744"
|
|
14
|
+
const GIROK = "\uAE30\uB85D"
|
|
15
|
+
const I = "\uC774"
|
|
16
|
+
const OLLATDA = "\uC62C\uB790\uB2E4"
|
|
17
|
+
const GA = "\uAC00"
|
|
18
|
+
const ISANG = "\uC774\uC0C1"
|
|
19
|
+
|
|
20
|
+
type State = { schemaVersion: number; failures: Record<string, number>; replaced: string[] }
|
|
21
|
+
function failedTwice(key: string): State {
|
|
22
|
+
return { schemaVersion: 1, failures: { [key]: 2 }, replaced: [] }
|
|
23
|
+
}
|
|
24
|
+
const codes = (list: { code: string }[]) => list.map((item) => item.code)
|
|
25
|
+
|
|
26
|
+
describe("fill: fixture", () => {
|
|
27
|
+
test("the ledger fixture is a valid ledger", () => {
|
|
28
|
+
expect(validateLedger(LEDGER)).toEqual([])
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("fill: value placeholders (html)", () => {
|
|
33
|
+
test("{{F12:span_len}} renders the <data> element with the raw text", () => {
|
|
34
|
+
const result = fill("<p>The span is {{F12:span_len}}.</p>", LEDGER, { format: "html" })
|
|
35
|
+
expect(result.errors).toEqual([])
|
|
36
|
+
expect(result.out).toBe(`<p>The span is ${F12_DATA}.</p>`)
|
|
37
|
+
expect(result.fillMap).toHaveLength(1)
|
|
38
|
+
expect(result.fillMap[0]).toMatchObject({ token: "{{F12:span_len}}", fact_id: "F12", rendered: F12_DATA })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test(":q copies the source qualifier with the quantity", () => {
|
|
42
|
+
const result = fill("<p>{{F12:span_len:q}}</p>", LEDGER, { format: "html" })
|
|
43
|
+
expect(result.out).toContain('data-lineage="MEASURED">up to 320 m</data>')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test(":value and :unit render one part each", () => {
|
|
47
|
+
const result = fill("<p>{{F12:span_len:value}} metres, unit {{F12:span_len:unit}}</p>", LEDGER, { format: "md" })
|
|
48
|
+
expect(result.out).toBe("<p>320 metres, unit m</p>")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("a converted row renders DERIVED lineage", () => {
|
|
52
|
+
const result = fill("<p>{{F40:span_mm}}</p>", LEDGER, { format: "html" })
|
|
53
|
+
expect(result.out).toContain('data-fact="F40" data-unit="mm" data-lineage="DERIVED">320000 mm</data>')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("an all-English template fills with no warnings", () => {
|
|
57
|
+
const result = fill("The span is {{F12:span_len}} long.", LEDGER, { format: "html" })
|
|
58
|
+
expect(result.out).not.toBeNull()
|
|
59
|
+
expect(result.warnings).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe("fill: fatal errors write nothing", () => {
|
|
64
|
+
test("a label mismatch is pointer_label_mismatch with out null", () => {
|
|
65
|
+
const result = fill("<p>Line one.</p>\n<p>{{F12:wrong}}</p>", LEDGER, { format: "html" })
|
|
66
|
+
expect(result.out).toBeNull()
|
|
67
|
+
expect(result.errors[0]).toMatchObject({ code: "pointer_label_mismatch", token: "{{F12:wrong}}", line: 2 })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("an unknown id is unresolved_placeholder; no row is guessed", () => {
|
|
71
|
+
const result = fill("<p>{{F99:x}}</p>", LEDGER, { format: "html" })
|
|
72
|
+
expect(result.out).toBeNull()
|
|
73
|
+
expect(codes(result.errors)).toEqual(["unresolved_placeholder"])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test("a malformed pointer is unresolved_placeholder", () => {
|
|
77
|
+
expect(codes(fill("<p>{{F12}}</p>", LEDGER, { format: "html" }).errors)).toEqual(["unresolved_placeholder"])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("a unit typed after a value placeholder is unit_duplicated, never absorbed", () => {
|
|
81
|
+
const result = fill("<p>{{F13:rate}}%</p>", LEDGER, { format: "html" })
|
|
82
|
+
expect(result.out).toBeNull()
|
|
83
|
+
expect(codes(result.errors)).toEqual(["unit_duplicated"])
|
|
84
|
+
expect(fill("<p>{{F13:rate:value}}%</p>", LEDGER, { format: "html" }).errors).toEqual([])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("a retracted row is unresolved_placeholder; fill never renders it", () => {
|
|
88
|
+
const ledger = structuredClone(LEDGER)
|
|
89
|
+
retractRow(ledger, "F12", "source withdrawn")
|
|
90
|
+
expect(validateLedger(ledger)).toEqual([])
|
|
91
|
+
const result = fill("<p>Line one.</p>\n<p>{{F12:span_len}}</p>", ledger, { format: "html" })
|
|
92
|
+
expect(result.out).toBeNull()
|
|
93
|
+
expect(result.fillMap).toEqual([])
|
|
94
|
+
expect(result.errors).toEqual([{ code: "unresolved_placeholder", token: "{{F12:span_len}}", line: 2, message: "F12 is retracted; cite its replacement" }])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("a superseded row is unresolved_placeholder; fill never renders it", () => {
|
|
98
|
+
const ledger = structuredClone(LEDGER)
|
|
99
|
+
supersedeRow(ledger, "F12", "F40", "restated in mm")
|
|
100
|
+
expect(validateLedger(ledger)).toEqual([])
|
|
101
|
+
const result = fill("{{F12:span_len:value}}", ledger, { format: "md" })
|
|
102
|
+
expect(result.out).toBeNull()
|
|
103
|
+
expect(result.fillMap).toEqual([])
|
|
104
|
+
expect(result.errors).toEqual([{ code: "unresolved_placeholder", token: "{{F12:span_len:value}}", line: 1, message: "F12 is superseded; cite its replacement" }])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test("an anchor occurring twice is quote_anchor_unresolved", () => {
|
|
108
|
+
const result = fill('<p>{{Q7:completion:from="ti":to="2027"}}</p>', LEDGER, { format: "html" })
|
|
109
|
+
expect(result.out).toBeNull()
|
|
110
|
+
expect(codes(result.errors)).toEqual(["quote_anchor_unresolved"])
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe("fill: quote placeholders", () => {
|
|
115
|
+
test("from/to renders the sub-span in quotes plus the source marker", () => {
|
|
116
|
+
const result = fill('<p>{{Q7:completion:from="no earlier":to="2027"}}</p>', LEDGER, { format: "html" })
|
|
117
|
+
expect(result.out).toBe('<p><q data-fact="Q7" data-quote-sel="from=no earlier;to=2027">\u2026no earlier than 2027</q> [S4]</p>')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test("full and lead selectors", () => {
|
|
121
|
+
expect(fill("{{Q7:completion}}", LEDGER, { format: "html" }).out).toBe(`<q data-fact="Q7" data-quote-sel="full">${QUOTE}</q> [S4]`)
|
|
122
|
+
expect(fill("{{Q7:completion:lead=2}}", LEDGER, { format: "html" }).out).toBe('<q data-fact="Q7" data-quote-sel="lead=2">we estimate\u2026</q> [S4]')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("md format puts the selector into fillMap", () => {
|
|
126
|
+
const result = fill("{{Q7:completion:lead=2}}", LEDGER, { format: "md" })
|
|
127
|
+
expect(result.out).toBe('"we estimate\u2026" [S4]')
|
|
128
|
+
expect(result.fillMap[0]).toMatchObject({ fact_id: "Q7", selector: "lead=2", quote_span: [0, 11] })
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test("a translation span after a quote placeholder is preserved and mapped", () => {
|
|
132
|
+
const source = '{{Q7:completion}} <span data-translates="Q7">translation</span> rest'
|
|
133
|
+
const result = fill(source, LEDGER, { format: "html" })
|
|
134
|
+
expect(result.out).toContain('<span data-translates="Q7">translation</span> rest')
|
|
135
|
+
const entry = result.fillMap.find((item: { translates?: string }) => item.translates === "Q7")
|
|
136
|
+
expect(entry).toBeDefined()
|
|
137
|
+
expect(result.out.slice(entry.start, entry.end)).toBe(entry.rendered)
|
|
138
|
+
const md = fill("{{Q7:completion}} <!-- translates: Q7 --> translation", LEDGER, { format: "md" })
|
|
139
|
+
expect(md.fillMap.some((item: { translates?: string }) => item.translates === "Q7")).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe("fill: Korean particles", () => {
|
|
144
|
+
test("a closed final keeps eul", () => {
|
|
145
|
+
const result = fill(`<p>{{F21:hbm_rev}}${EUL} ${GIROK}</p>`, LEDGER, { format: "html" })
|
|
146
|
+
expect(result.out).toContain(`</data>${EUL} ${GIROK}`)
|
|
147
|
+
expect(result.warnings).toEqual([])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("percent is open: i becomes ga", () => {
|
|
151
|
+
const result = fill(`<p>{{F13:rate}}${I} ${OLLATDA}</p>`, LEDGER, { format: "html" })
|
|
152
|
+
expect(result.out).toContain(`15.2%</data>${GA} ${OLLATDA}`)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test("a candidate particle with no boundary is untouched and warned", () => {
|
|
156
|
+
const result = fill(`<p>{{F13:rate}}${ISANG}</p>`, LEDGER, { format: "html" })
|
|
157
|
+
expect(result.out).toContain(`</data>${ISANG}</p>`)
|
|
158
|
+
expect(codes(result.warnings)).toEqual(["particle_unverified"])
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("fill: md maps", () => {
|
|
163
|
+
test("fillMap offsets slice to the rendered text", () => {
|
|
164
|
+
const result = fill(`# Findings\n\nSpan {{F12:span_len}}, rate {{F13:rate}}${I} ${OLLATDA}, {{Q7:completion:lead=3}}.\n`, LEDGER, { format: "md" })
|
|
165
|
+
expect(result.errors).toEqual([])
|
|
166
|
+
expect(result.fillMap).toHaveLength(3)
|
|
167
|
+
for (const entry of result.fillMap) expect(result.out.slice(entry.start, entry.end)).toBe(entry.rendered)
|
|
168
|
+
expect(result.fillMap[0].rendered).toBe("320 m")
|
|
169
|
+
expect(result.out).toContain(`15.2%${GA}`)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe("fill: replace-failed", () => {
|
|
174
|
+
const SOURCE = '<h2 id="findings">Findings</h2>\n<p>Completion slips: {{Q7:completion}}. Other text stays.</p>'
|
|
175
|
+
const KEY = sentenceKey("findings", ["Q7"])
|
|
176
|
+
|
|
177
|
+
test("a key that failed twice becomes the attributed blockquote; no entity warns", () => {
|
|
178
|
+
const state = failedTwice(KEY)
|
|
179
|
+
const result = fill(SOURCE, LEDGER, { format: "html", replaceFailed: state })
|
|
180
|
+
expect(result.out).not.toBeNull()
|
|
181
|
+
expect(result.out).toContain(`<blockquote data-fact="Q7" data-replaced="${KEY}">`)
|
|
182
|
+
expect(result.out).toContain(`"${QUOTE}"`)
|
|
183
|
+
expect(result.out).toContain("<footer>[S4]</footer>")
|
|
184
|
+
expect(result.out).toContain("Other text stays.</p>")
|
|
185
|
+
expect(result.out).not.toContain("Completion slips")
|
|
186
|
+
expect(codes(result.warnings)).toEqual(["fallback_without_entity"])
|
|
187
|
+
expect(state.replaced).toEqual([KEY])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test("a row with entity and valid_at carries them in the footer", () => {
|
|
191
|
+
const source = `<h2>Market</h2>\n<p>Revenue was {{F21:hbm_rev}}.</p>`
|
|
192
|
+
const result = fill(source, LEDGER, { format: "html", replaceFailed: failedTwice(sentenceKey("market", ["F21"])) })
|
|
193
|
+
expect(result.out).toContain("<footer>[S5], on HBM3E (2026 Q3)</footer>")
|
|
194
|
+
expect(result.warnings).toEqual([])
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test("a key below two failures is filled normally", () => {
|
|
198
|
+
const state: State = { schemaVersion: 1, failures: { [KEY]: 1 }, replaced: [] }
|
|
199
|
+
const result = fill(SOURCE, LEDGER, { format: "html", replaceFailed: state })
|
|
200
|
+
expect(result.out).toContain("Completion slips:")
|
|
201
|
+
expect(state.replaced).toEqual([])
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test("fallback omit removes the sentence", () => {
|
|
205
|
+
const result = fill(SOURCE, LEDGER, { format: "html", replaceFailed: failedTwice(KEY), fallback: "omit" })
|
|
206
|
+
expect(result.out).toBe('<h2 id="findings">Findings</h2>\n<p> Other text stays.</p>')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test("view leaves the state unchanged", () => {
|
|
210
|
+
const state = failedTwice(KEY)
|
|
211
|
+
const result = fill(SOURCE, LEDGER, { format: "html", replaceFailed: state, view: true })
|
|
212
|
+
expect(result.out).toContain("<blockquote")
|
|
213
|
+
expect(state.replaced).toEqual([])
|
|
214
|
+
expect(failureCount(state, KEY)).toBe(2)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test("md renders a > block with the footer line", () => {
|
|
218
|
+
const result = fill("## Findings\n\nCompletion slips: {{Q7:completion}}.\n", LEDGER, { format: "md", replaceFailed: failedTwice(KEY) })
|
|
219
|
+
expect(result.out).toBe(`## Findings\n\n> "${QUOTE}"\n> [S4]\n`)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
|
|
3
|
+
import { TRANSITIONS, addRow, emptyLedger, nextId, retractRow, supersedeRow, transitionError, validateLedger } from "./fact-ledger.mjs"
|
|
4
|
+
|
|
5
|
+
// Property-style checks of references/fact-ledger-grammar.md: every ledger the writers build is
|
|
6
|
+
// valid, and every single-invariant break the validator rejects is also refused by the writers.
|
|
7
|
+
|
|
8
|
+
type Row = { fact_id: string; status: string; kind: string; label: string; operands: string[]; derived_from: string[] }
|
|
9
|
+
type LedgerEvent = { fact_id: string; from: string; to: string; replacement?: string | null; reason?: string }
|
|
10
|
+
type Ledger = { rows: Row[]; events: LedgerEvent[] }
|
|
11
|
+
|
|
12
|
+
function measured(label: string) {
|
|
13
|
+
return {
|
|
14
|
+
kind: "measured",
|
|
15
|
+
value: "320",
|
|
16
|
+
unit: "m",
|
|
17
|
+
raw: "320 m",
|
|
18
|
+
quote: "The span is 320 m.",
|
|
19
|
+
value_span: [12, 15],
|
|
20
|
+
source_id: "S1",
|
|
21
|
+
locator: { artifact: "sources/S1/text.txt", line: 3, char_start: 40, char_end: 62 },
|
|
22
|
+
snapshot_sha256: "a".repeat(64),
|
|
23
|
+
extracted_by: "fact add --find",
|
|
24
|
+
observed_at: "2026-10-23T08:00:00Z",
|
|
25
|
+
label,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function quoteRow(label: string) {
|
|
30
|
+
return { ...measured(label), kind: "quote", value: null, unit: null, value_span: null }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function derivedRow(label: string, from: string[], kind = "derived") {
|
|
34
|
+
return {
|
|
35
|
+
kind,
|
|
36
|
+
value: "2",
|
|
37
|
+
raw: "2",
|
|
38
|
+
quote: "",
|
|
39
|
+
extracted_by: "fact derive",
|
|
40
|
+
observed_at: "2026-10-23T08:00:00Z",
|
|
41
|
+
label,
|
|
42
|
+
operation: kind === "converted" ? "convert" : "expr",
|
|
43
|
+
operands: [...from],
|
|
44
|
+
derived_from: [...from],
|
|
45
|
+
factor: kind === "converted" ? "1000" : null,
|
|
46
|
+
exact: { num: "2", den: "1" },
|
|
47
|
+
formula: from.join(" + "), render: "2", precision: { places: 0, sigfigs: 1 },
|
|
48
|
+
compatibility: kind === "converted" ? "checked" : "unchecked",
|
|
49
|
+
context: kind === "converted" ? { target: "km" } : null,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** mulberry32: a fixed seed gives the same ledger on every run. */
|
|
54
|
+
function prng(seed: number) {
|
|
55
|
+
let state = seed >>> 0
|
|
56
|
+
return () => {
|
|
57
|
+
state = (state + 0x6d2b79f5) >>> 0
|
|
58
|
+
let t = state
|
|
59
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
60
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
61
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function pick<T>(random: () => number, list: T[]): T {
|
|
66
|
+
return list[Math.floor(random() * list.length)]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ids = (ledger: Ledger, keep: (row: Row) => boolean) => ledger.rows.filter(keep).map((row) => row.fact_id)
|
|
70
|
+
|
|
71
|
+
/** Writes the writers must refuse on any ledger; each returns nothing and must throw. */
|
|
72
|
+
function illegalWrites(ledger: Ledger, random: () => number): (() => unknown)[] {
|
|
73
|
+
const active = ids(ledger, (row) => row.status === "active")
|
|
74
|
+
const ended = ids(ledger, (row) => row.status !== "active")
|
|
75
|
+
const writes: (() => unknown)[] = [() => addRow(ledger, derivedRow("dangling", ["F999"])), () => addRow(ledger, measured(pick(random, ledger.rows).label))]
|
|
76
|
+
if (active.length > 0) {
|
|
77
|
+
const id = pick(random, active)
|
|
78
|
+
writes.push(() => supersedeRow(ledger, id, id, "self"), () => supersedeRow(ledger, id, null, "none"), () => retractRow(ledger, id, ""))
|
|
79
|
+
if (ended.length > 0) writes.push(() => supersedeRow(ledger, id, pick(random, ended), "to an ended row"))
|
|
80
|
+
}
|
|
81
|
+
if (ended.length > 0) writes.push(() => retractRow(ledger, pick(random, ended), "again"))
|
|
82
|
+
return writes
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Random addRow / retractRow / supersedeRow sequences from an empty ledger, with refused writes mixed in. */
|
|
86
|
+
function randomLedger(seed: number) {
|
|
87
|
+
const random = prng(seed)
|
|
88
|
+
const ledger: Ledger = emptyLedger()
|
|
89
|
+
const steps = 4 + Math.floor(random() * 14)
|
|
90
|
+
for (let step = 0; step < steps; step += 1) {
|
|
91
|
+
const active = ids(ledger, (row) => row.status === "active")
|
|
92
|
+
const values = ids(ledger, (row) => row.fact_id.startsWith("F"))
|
|
93
|
+
const roll = random()
|
|
94
|
+
if (roll < 0.3 || values.length < 2) addRow(ledger, measured(`m${step}`))
|
|
95
|
+
else if (roll < 0.4) addRow(ledger, quoteRow(`q${step}`))
|
|
96
|
+
else if (roll < 0.6) {
|
|
97
|
+
const kind = pick(random, ["derived", "converted"])
|
|
98
|
+
const from = kind === "converted" ? [pick(random, values)] : [...new Set([pick(random, values), pick(random, values)])]
|
|
99
|
+
addRow(ledger, derivedRow(`d${step}`, from, kind))
|
|
100
|
+
}
|
|
101
|
+
else if (roll < 0.8 && active.length > 0) retractRow(ledger, pick(random, active), "wrong page")
|
|
102
|
+
else if (active.length > 1) {
|
|
103
|
+
const id = pick(random, active)
|
|
104
|
+
supersedeRow(ledger, id, pick(random, active.filter((other: string) => other !== id)), "newer filing")
|
|
105
|
+
}
|
|
106
|
+
const before = JSON.stringify(ledger)
|
|
107
|
+
const write = pick(random, illegalWrites(ledger, random))
|
|
108
|
+
expect(write).toThrow()
|
|
109
|
+
expect(JSON.stringify(ledger)).toBe(before)
|
|
110
|
+
}
|
|
111
|
+
return ledger
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const LEDGERS = Array.from({ length: 200 }, (_, index) => randomLedger(index + 1))
|
|
115
|
+
|
|
116
|
+
describe("every ledger the writers build is valid", () => {
|
|
117
|
+
test("200 seeded random write sequences all validate, and refused writes leave the ledger unchanged", () => {
|
|
118
|
+
for (const ledger of LEDGERS) expect(validateLedger(ledger)).toEqual([])
|
|
119
|
+
const events = LEDGERS.flatMap((ledger) => ledger.events)
|
|
120
|
+
expect(events.some((event) => event.to === "superseded")).toBe(true)
|
|
121
|
+
expect(events.some((event) => event.to === "retracted")).toBe(true)
|
|
122
|
+
expect(LEDGERS.some((ledger) => ledger.rows.some((row) => row.kind === "converted"))).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
type Break = { name: string; mutate: (ledger: Ledger) => string | null; rejects: RegExp; write: (ledger: Ledger) => unknown; refuses: RegExp }
|
|
127
|
+
|
|
128
|
+
const firstEvent = (ledger: Ledger, to?: string) => ledger.events.find((event) => to === undefined || event.to === to) as LedgerEvent
|
|
129
|
+
const firstRow = (ledger: Ledger, keep: (row: Row) => boolean) => ledger.rows.find(keep) as Row
|
|
130
|
+
const activeId = (ledger: Ledger) => firstRow(ledger, (row) => row.status === "active")?.fact_id
|
|
131
|
+
|
|
132
|
+
const BREAKS: Break[] = [
|
|
133
|
+
{ name: "superseded event with a null replacement", mutate: (l) => { const e = firstEvent(l, "superseded"); if (!e || !activeId(l)) return null; e.replacement = null; return e.fact_id }, rejects: /needs a replacement row/, write: (l) => supersedeRow(l, activeId(l), null, "x"), refuses: /needs a replacement row/ },
|
|
134
|
+
{ name: "self replacement", mutate: (l) => { const e = firstEvent(l, "superseded"); if (!e || !activeId(l)) return null; e.replacement = e.fact_id; return e.fact_id }, rejects: /cannot be its own replacement/, write: (l) => supersedeRow(l, activeId(l), activeId(l), "x"), refuses: /cannot be its own replacement/ },
|
|
135
|
+
{ name: "duplicate transition", mutate: (l) => { const e = firstEvent(l); if (!e) return null; l.events.push({ ...e }); return e.fact_id }, rejects: /but the event moves it from active/, write: (l) => retractRow(l, firstEvent(l).fact_id, "x"), refuses: /cannot move from (retracted|superseded)/ },
|
|
136
|
+
{ name: "from mismatch", mutate: (l) => { const e = firstEvent(l); if (!e) return null; e.from = e.to; return e.fact_id }, rejects: /is active, but the event moves it from/, write: (l) => supersedeRow(l, firstEvent(l).fact_id, activeId(l) ?? "F1", "x"), refuses: /cannot move from|own replacement/ },
|
|
137
|
+
{ name: "active row with events", mutate: (l) => { const r = firstRow(l, (row) => row.status !== "active"); if (!r) return null; r.status = "active"; return r.fact_id }, rejects: /has status active but its last event moves it to/, write: (l) => { const r = firstRow(l, (row) => row.status !== "active"); const problem = transitionError(new Map([[r.fact_id, r.status]]), { fact_id: r.fact_id, from: r.status, to: "active", replacement: null, reason: "x" }); if (problem) throw new Error(problem) }, refuses: /cannot move from (retracted|superseded) to active/ },
|
|
138
|
+
{ name: "non-active row without events", mutate: (l) => { const e = firstEvent(l); if (!e) return null; l.events = l.events.filter((event) => event.fact_id !== e.fact_id); return e.fact_id }, rejects: /but has no status event/, write: (l) => addRow(l, { ...measured("fresh"), status: "retracted" }), refuses: /but has no status event/ },
|
|
139
|
+
{ name: "dangling operand", mutate: (l) => { const r = firstRow(l, (row) => row.operands.length > 0); if (!r) return null; r.operands[0] = "F999"; return r.fact_id }, rejects: /references F999, which has no ledger row/, write: (l) => addRow(l, derivedRow("fresh", ["F999"])), refuses: /references F999, which has no ledger row/ },
|
|
140
|
+
{ name: "self derived_from", mutate: (l) => { const r = firstRow(l, (row) => row.derived_from.length > 0); if (!r) return null; r.derived_from.push(r.fact_id); return r.fact_id }, rejects: /references itself/, write: (l) => addRow(l, { ...derivedRow("fresh", []), derived_from: [nextId(l, "derived")] }), refuses: /references itself/ },
|
|
141
|
+
{ name: "cyclic derived_from", mutate: (l) => { const r = firstRow(l, (row) => row.derived_from.length > 0); if (!r) return null; firstRow(l, (row) => row.fact_id === r.derived_from[0]).derived_from.push(r.fact_id); return r.fact_id }, rejects: /derives from itself through/, write: (l) => addRow(l, derivedRow("fresh", [`F${l.rows.length + 5}`])), refuses: /which has no ledger row/ },
|
|
142
|
+
{ name: "duplicate label", mutate: (l) => { if (l.rows.length < 2) return null; l.rows[1].label = l.rows[0].label; return l.rows[1].fact_id }, rejects: /has duplicate label/, write: (l) => addRow(l, measured(l.rows[0].label)), refuses: /has duplicate label/ },
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
describe("every single-invariant break is rejected by the validator and refused by the writers", () => {
|
|
146
|
+
for (const broken of BREAKS) {
|
|
147
|
+
test(broken.name, () => {
|
|
148
|
+
let applied = 0
|
|
149
|
+
for (const valid of LEDGERS) {
|
|
150
|
+
const ledger = structuredClone(valid)
|
|
151
|
+
const factId = broken.mutate(ledger)
|
|
152
|
+
if (factId === null) continue
|
|
153
|
+
applied += 1
|
|
154
|
+
const errors = validateLedger(ledger)
|
|
155
|
+
expect(errors.some((error: string) => broken.rejects.test(error) && error.includes(factId))).toBe(true)
|
|
156
|
+
const untouched = structuredClone(valid)
|
|
157
|
+
expect(() => broken.write(untouched)).toThrow(broken.refuses)
|
|
158
|
+
expect(untouched).toEqual(valid)
|
|
159
|
+
}
|
|
160
|
+
expect(applied).toBeGreaterThan(10)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
test("no transition leads back to active", () => {
|
|
165
|
+
for (const targets of Object.values(TRANSITIONS)) expect(targets).not.toContain("active")
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
function twoRows() {
|
|
170
|
+
const ledger = emptyLedger()
|
|
171
|
+
addRow(ledger, measured("a"))
|
|
172
|
+
addRow(ledger, measured("b"))
|
|
173
|
+
return ledger
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
describe("validateLedger is as strict as the writers (review r5 MEDIUM)", () => {
|
|
177
|
+
test("a superseded event with a null replacement is rejected", () => {
|
|
178
|
+
const ledger = twoRows()
|
|
179
|
+
supersedeRow(ledger, "F1", "F2", "newer filing")
|
|
180
|
+
ledger.events[0].replacement = null
|
|
181
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("F1") && e.includes("replacement"))).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test("a row superseded by itself is rejected", () => {
|
|
185
|
+
const ledger = twoRows()
|
|
186
|
+
supersedeRow(ledger, "F1", "F2", "newer filing")
|
|
187
|
+
ledger.events[0].replacement = "F1"
|
|
188
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("F1") && e.includes("replacement"))).toBe(true)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test("two events that both move F1 from active to retracted are rejected", () => {
|
|
192
|
+
const ledger = twoRows()
|
|
193
|
+
retractRow(ledger, "F1", "wrong page")
|
|
194
|
+
ledger.events.push({ ...ledger.events[0] })
|
|
195
|
+
expect(validateLedger(ledger).some((e: string) => e.includes("events[1]") && e.includes("F1"))).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
})
|