lazycodex-ai 5.1.45 → 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 +76 -156
- package/dist/cli-node/index.js +76 -156
- package/package.json +1 -1
- package/packages/lsp-daemon/dist/cli.js +3 -3
- package/packages/lsp-daemon/dist/client.js +3 -3
- package/packages/lsp-daemon/dist/index.js +3 -3
- package/packages/lsp-tools-mcp/dist/cli.js +3 -3
- package/packages/lsp-tools-mcp/dist/mcp.js +3 -3
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +22 -12
- 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/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- 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/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- 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/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +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 +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 +1 -1
- 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 +8 -3
- package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +0 -2
- package/packages/omo-codex/plugin/skills/ast-grep/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +2 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +14 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +36 -11
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +73 -13
- package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
- 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 +21 -4
- 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/cli-support.mjs +19 -5
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +27 -6
- 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 +127 -13
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +33 -16
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
- 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 +5 -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 +2 -1
- 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 +28 -8
- 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/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/outcome.mjs +6 -5
- 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/report-tools-commands.mjs +19 -8
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +15 -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/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 +4 -4
- 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 +7 -3
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +26 -16
- package/packages/shared-skills/skills/ast-grep/AGENTS.md +1 -1
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +2 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +1 -1
- package/packages/shared-skills/skills/ulw-research/AGENTS.md +14 -2
- package/packages/shared-skills/skills/ulw-research/SKILL.md +37 -12
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +73 -13
- package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
- 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 +21 -4
- 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/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 +27 -6
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +3 -0
- 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 +9 -2
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +127 -13
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +18 -3
- 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 +33 -16
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +2 -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 +5 -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 +2 -1
- 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-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 +28 -8
- 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/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/outcome.mjs +6 -5
- 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/report-tools-commands.mjs +19 -8
- 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/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/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/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 +4 -4
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
import { indexClaims } from "./claims-index.mjs"
|
|
6
|
+
import { checkFidelity } from "./gates-fidelity.mjs"
|
|
7
|
+
|
|
8
|
+
type Defect = { code: string; message: string }
|
|
9
|
+
|
|
10
|
+
const FIXTURES = join(import.meta.dir, "tests", "fixtures")
|
|
11
|
+
const FIX = join(FIXTURES, "fidelity")
|
|
12
|
+
const read = (path: string) => readFileSync(path, "utf8")
|
|
13
|
+
const GOOD = read(join(FIX, "report-good.html"))
|
|
14
|
+
const OPTIONS = {
|
|
15
|
+
ledger: JSON.parse(read(join(FIX, "ledger-verify.json"))),
|
|
16
|
+
sourcesDir: join(FIX, "sources"),
|
|
17
|
+
claims: indexClaims(read(join(FIX, "claim-graph.md"))),
|
|
18
|
+
fillMap: null,
|
|
19
|
+
brief: read(join(FIXTURES, "fidelity-brief-locked.md")),
|
|
20
|
+
baseDir: FIX,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const run = (html: string, extra: Record<string, unknown> = {}) => (checkFidelity(html, { ...OPTIONS, ...extra }) as { defects: Defect[] }).defects
|
|
24
|
+
const codes = (defects: Defect[]) => defects.map((defect) => defect.code)
|
|
25
|
+
const F1_SENTENCE = "long [S4].</p>"
|
|
26
|
+
const F2_FILL = [{ token: "{{F2:units_sold}}", fact_id: "F2", start: 0, end: 5, rendered: "1,237" }]
|
|
27
|
+
|
|
28
|
+
describe("fill-site trace (Markdown fill map)", () => {
|
|
29
|
+
test("#given a filled 1,237 and a second bare 1,237 with no ledger row #when checked #then untraceable_number", () => {
|
|
30
|
+
const html = GOOD.replace("<h2>Appendix</h2>", "<p>Revenue was 1,237 million [S4].</p>\n<h2>Appendix</h2>")
|
|
31
|
+
expect(codes(run(html, { fillMap: F2_FILL }))).toEqual(["untraceable_number"])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test("#given the filled occurrence alone #when checked #then it stays traced", () => {
|
|
35
|
+
const html = GOOD.replace("<h2>Appendix</h2>", "<p>Units sold were 1,237 [S4].</p>\n<h2>Appendix</h2>")
|
|
36
|
+
expect(run(html, { fillMap: F2_FILL })).toEqual([])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("#given one fill entry and two plain 1,237 sites #when checked #then the unfilled copy is untraceable_number", () => {
|
|
40
|
+
const html = GOOD.replace("<h2>Appendix</h2>", "<p>Units sold were 1,237 [S4].</p>\n<p>Again, 1,237 units [S4].</p>\n<h2>Appendix</h2>")
|
|
41
|
+
expect(codes(run(html, { fillMap: F2_FILL }))).toEqual(["untraceable_number"])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test("#given two fill entries for two occurrences #when checked #then both are traced", () => {
|
|
45
|
+
const html = GOOD.replace("<h2>Appendix</h2>", "<p>Units sold were 1,237 [S4].</p>\n<p>Again, 1,237 units [S4].</p>\n<h2>Appendix</h2>")
|
|
46
|
+
expect(run(html, { fillMap: [...F2_FILL, { ...F2_FILL[0], start: 40, end: 45 }] })).toEqual([])
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe("verification claims are judged by their mark, not their wording", () => {
|
|
51
|
+
test("#given the reviewer's prose 'as confirmed with the annual inspection report' #when checked #then no unsupported_verification", () => {
|
|
52
|
+
expect(run(GOOD.replace(F1_SENTENCE, "long, as confirmed with the annual inspection report [S4].</p>"))).toEqual([])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test("#given report-good's F7 marked verified but pointed at a page-sourced row #when checked #then unsupported_verification", () => {
|
|
56
|
+
expect(codes(run(GOOD.replace('value="42" data-fact="F7" data-claim-kind="verified" data-lineage="MEASURED">42 passed', 'value="320" data-fact="F1" data-unit="m" data-claim-kind="verified" data-lineage="MEASURED">up to 320 m')))).toEqual(["unsupported_verification"])
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// fidelity-values.mjs - value, quote, and chart checks of the deterministic fidelity gate.
|
|
2
|
+
//
|
|
3
|
+
// Each data-fact element must match its ledger row, and each Q element the script's
|
|
4
|
+
// reconstruction of its selection (report text never gets a fuzzy pass). Cited rows, table rows,
|
|
5
|
+
// snapshots, and chart CSVs are rechecked; the numeral and quotation trace is fidelity-trace.mjs,
|
|
6
|
+
// and the claim-kind checks (data-claim-kind, unsupported_verification) are fidelity-claims.mjs.
|
|
7
|
+
// No model, no network.
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
10
|
+
import { join, resolve, sep } from "node:path"
|
|
11
|
+
|
|
12
|
+
import { sha256Of } from "./cli-support.mjs"
|
|
13
|
+
import { sameValue, unitKey } from "./fidelity-binding.mjs"
|
|
14
|
+
import { claimChecks } from "./fidelity-claims.mjs"
|
|
15
|
+
import { STRUCTURAL, clip, traceChecks } from "./fidelity-trace.mjs"
|
|
16
|
+
import { parseHtml, proseText } from "./html-lite.mjs"
|
|
17
|
+
import { parseNumeral } from "./numerals.mjs"
|
|
18
|
+
import { div, fromDecimal, sub } from "./rational.mjs"
|
|
19
|
+
import { normalizeText } from "./text-normalize.mjs"
|
|
20
|
+
|
|
21
|
+
const DECIMAL = /^-?\d+(?:\.\d+)?$/
|
|
22
|
+
|
|
23
|
+
const bare = (text) => normalizeText(String(text ?? "").replace(/\s*\[(?:S|Source\s*)\d+\]\s*$/i, "")).replace(/\u2026/g, "...").replace(/\s*\.\.\.\s*/g, "...").replace(/^["']+|["']+$/g, "").trim()
|
|
24
|
+
|
|
25
|
+
/** The text fill renders for a quote selection: full, lead=<n>, or from=<a>;to=<b> (placeholder form accepted). */
|
|
26
|
+
export function selectQuote(quote, selector) {
|
|
27
|
+
const text = String(quote ?? "").trim()
|
|
28
|
+
if (selector === "full") return text
|
|
29
|
+
const lead = /^lead=(\d+)$/.exec(selector)
|
|
30
|
+
const words = text.split(/\s+/)
|
|
31
|
+
if (lead) return words.length <= Number(lead[1]) ? text : `${words.slice(0, Number(lead[1])).join(" ")}...`
|
|
32
|
+
const range = /^from=(.+);to=(.+)$/.exec(selector) ?? /^from="(.+)":to="(.+)"$/.exec(selector)
|
|
33
|
+
if (!range) return null
|
|
34
|
+
const once = (anchor) => text.indexOf(anchor) !== -1 && text.indexOf(anchor) === text.lastIndexOf(anchor)
|
|
35
|
+
if (!once(range[1]) || !once(range[2])) return null
|
|
36
|
+
const start = text.indexOf(range[1])
|
|
37
|
+
const end = text.indexOf(range[2]) + range[2].length
|
|
38
|
+
if (end <= start) return null
|
|
39
|
+
return `${start > 0 ? "..." : ""}${text.slice(start, end)}${end < text.length ? "..." : ""}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function quoteProblem(row, shown, selector) {
|
|
43
|
+
const want = selectQuote(row.quote, selector)
|
|
44
|
+
if (want === null) return ["quote_not_verbatim", `selection "${selector}" does not resolve in ${row.fact_id}'s quote`]
|
|
45
|
+
return bare(shown) === bare(want) ? null : ["quote_not_verbatim", `"${clip(shown)}" is not the ${selector} selection of ${row.fact_id}: "${clip(want)}"`]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function factProblem(row, shown, attrs = {}) {
|
|
49
|
+
if (attrs["data-unit"] !== undefined && unitKey(attrs["data-unit"]) !== unitKey(row.unit)) return ["unit_mismatch", `data-unit "${attrs["data-unit"]}" but ${row.fact_id} is in "${row.unit}"`]
|
|
50
|
+
if (attrs.value !== undefined && !sameValue(attrs.value, row.value)) return ["value_mismatch", `value="${attrs.value}" but ${row.fact_id} is ${row.value}`]
|
|
51
|
+
const text = normalizeText(shown)
|
|
52
|
+
const forms = [row.raw, row.render, row.value, row.unit && `${row.value} ${row.unit}`].filter(Boolean).map(normalizeText)
|
|
53
|
+
if (forms.some((form) => text === form || (text.endsWith(` ${form}`) && !/\d/.test(text.slice(0, -form.length))))) return null
|
|
54
|
+
const parsed = parseNumeral(text)
|
|
55
|
+
if (!parsed) return unitKey(text) === unitKey(row.unit) ? null : ["value_mismatch", `"${clip(shown)}" shows no value of ${row.fact_id} (${row.value})`]
|
|
56
|
+
if (!sameValue(parsed.value, row.value)) return ["value_mismatch", `"${clip(shown)}" reads ${parsed.value}, but ${row.fact_id} is ${row.value}`]
|
|
57
|
+
if (parsed.unitRaw && unitKey(parsed.unitRaw) !== unitKey(row.unit)) return ["unit_mismatch", `"${clip(shown)}" is in ${parsed.unitRaw}, but ${row.fact_id} is in ${row.unit}`]
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether a candidate fill site reads as what fill wrote there or as the entry's row ("1,237 million" is neither 1,237 nor F2). */
|
|
62
|
+
const fillSiteFits = (rows) => (entry, tail) => {
|
|
63
|
+
const [row, site, own] = [rows.get(entry.fact_id), parseNumeral(tail, 0), parseNumeral(entry.rendered, 0)]
|
|
64
|
+
if (!row || row.kind === "quote" || !site || !own) return true
|
|
65
|
+
if (sameValue(site.value, own.value) && unitKey(site.unitRaw ?? "") === unitKey(own.unitRaw ?? "")) return true
|
|
66
|
+
return factProblem(row, tail.slice(0, Math.max(entry.rendered.length, site.span[1]))) === null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const withoutFooter = (node) => proseText({ children: node.children.filter((child) => child.tag !== "footer") })
|
|
70
|
+
|
|
71
|
+
/** One row per active value row, as `fact export --csv` writes it. */
|
|
72
|
+
export function exportRows(ledger) {
|
|
73
|
+
return (ledger?.rows ?? []).filter((row) => row.status === "active" && row.kind !== "quote" && row.value !== null).map((row) => ({ fact_id: row.fact_id, label: row.label, plot_value: row.value, unit: row.unit ?? "" }))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readCsv(path) {
|
|
77
|
+
if (!existsSync(path)) return null
|
|
78
|
+
const lines = readFileSync(path, "utf8").split(/\r?\n/).filter((line) => line.trim() !== "")
|
|
79
|
+
const cells = (line) => line.split(",").map((cell) => cell.trim().replace(/^"(.*)"$/, "$1"))
|
|
80
|
+
const head = cells(lines[0] ?? "")
|
|
81
|
+
const [id, value, unit] = ["fact_id", "plot_value", "unit"].map((name) => head.indexOf(name))
|
|
82
|
+
if (id < 0 || value < 0 || unit < 0 || lines.length < 2) return null
|
|
83
|
+
return lines.slice(1).map((line) => cells(line)).map((c) => ({ fact_id: c[id], plot_value: c[value], unit: c[unit] ?? "" }))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const walk = (node) => node.children.filter((child) => child.tag !== "#text").flatMap((child) => [child, ...walk(child)])
|
|
87
|
+
|
|
88
|
+
/** SVG text nodes of a chart: data labels, or ticks with their declared grid. */
|
|
89
|
+
function svgLabels(raw) {
|
|
90
|
+
return walk(parseHtml(raw)).filter((node) => node.tag === "text").map((node) => {
|
|
91
|
+
let tick = null
|
|
92
|
+
for (let cur = node.parent; cur && !tick; cur = cur.parent) if (cur.tag === "g" && /(?:^|\s)ticks(?:\s|$)/.test(cur.attrs.class ?? "")) tick = cur.attrs
|
|
93
|
+
return { text: proseText(node), tick }
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function onGrid(value, tick) {
|
|
98
|
+
const [step, min, max] = [tick["data-tick-step"], tick["data-tick-min"], tick["data-tick-max"]]
|
|
99
|
+
if (![value, step, min, max].every((v) => DECIMAL.test(String(v ?? ""))) || /^-?0+(?:\.0+)?$/.test(step)) return false
|
|
100
|
+
const offset = sub(fromDecimal(value), fromDecimal(min))
|
|
101
|
+
return offset.num >= 0n && sub(fromDecimal(max), fromDecimal(value)).num >= 0n && div(offset, fromDecimal(step)).den === 1n
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function numerals(text) {
|
|
105
|
+
const out = []
|
|
106
|
+
for (let at = 0, n = parseNumeral(text, 0); n; at = n.span[1], n = parseNumeral(text, at)) out.push(n.value)
|
|
107
|
+
return out
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function chartFigures(ctx) {
|
|
111
|
+
const charts = new Map()
|
|
112
|
+
for (const fig of ctx.all.filter((node) => node.tag === "figure")) {
|
|
113
|
+
const inner = ctx.util.descendants(fig)
|
|
114
|
+
const labels = inner.filter((node) => node.tag === "svg").flatMap((svg) => svgLabels(svg.raw ?? ""))
|
|
115
|
+
const marked = fig.attrs["data-chart-source"] !== undefined || ctx.util.classTokens(fig).includes("chart")
|
|
116
|
+
if (marked || inner.some((node) => node.tag === "img" && ctx.util.classTokens(node).includes("chart")) || labels.some((label) => /\d/.test(label.text))) charts.set(fig, labels)
|
|
117
|
+
}
|
|
118
|
+
return charts
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function chartProblem(ctx, fig, labels, ledger) {
|
|
122
|
+
const source = fig.attrs["data-chart-source"]
|
|
123
|
+
if (!source) return "chart figure has no data-chart-source"
|
|
124
|
+
const csv = readCsv(resolve(ctx.baseDir ?? ".", source))
|
|
125
|
+
if (!csv) return `chart data ${source} is missing or lacks fact_id, plot_value, unit columns`
|
|
126
|
+
const exported = new Map(exportRows(ledger).map((row) => [row.fact_id, row]))
|
|
127
|
+
const drift = csv.find((row) => exported.get(row.fact_id)?.plot_value !== row.plot_value || exported.get(row.fact_id)?.unit !== row.unit)
|
|
128
|
+
if (drift) return `chart data row ${drift.fact_id} differs from the ledger export`
|
|
129
|
+
const plotted = (value) => csv.some((row) => sameValue(row.plot_value, value))
|
|
130
|
+
for (const label of labels) {
|
|
131
|
+
const bad = numerals(label.text).find((value) => (label.tick ? !onGrid(value, label.tick) : !plotted(value)))
|
|
132
|
+
if (bad !== undefined) return `${label.tick ? "tick" : "label"} "${label.text}" is not ${label.tick ? "on the declared tick grid" : `a plot_value of ${source}`}`
|
|
133
|
+
}
|
|
134
|
+
const caption = ctx.util.descendants(fig).find((node) => node.tag === "figcaption")
|
|
135
|
+
const stray = numerals(caption ? STRUCTURAL.reduce((text, re) => text.replace(re, " "), proseText(caption)) : "").find((value) => !plotted(value))
|
|
136
|
+
return stray === undefined ? null : `caption numeral ${stray} is not a plot_value of ${source}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {object} ctx buildContext(...) from gates-static.mjs
|
|
141
|
+
* @param {{ rows: object[] }} ledger
|
|
142
|
+
* @param {{ fact_id: string, rendered: string, selector?: string }[] | null} fillMap Markdown fill map, or null
|
|
143
|
+
* @param {string | null} sourcesDir directory of <S-id>/{text.txt, meta.json} snapshots
|
|
144
|
+
* @param {{ recompute?: (ledger: object, row: object) => { ok: boolean }, asOf?: string | null }} options
|
|
145
|
+
*/
|
|
146
|
+
export function valueChecks(ctx, ledger, fillMap, sourcesDir, options = {}) {
|
|
147
|
+
const out = []
|
|
148
|
+
const flag = (code, node, message) => out.push(ctx.defect(code, node, message))
|
|
149
|
+
const rows = new Map((ledger?.rows ?? []).map((row) => [row.fact_id, row]))
|
|
150
|
+
const cited = new Map()
|
|
151
|
+
const check = (id, node, problemOf) => {
|
|
152
|
+
const row = rows.get(id)
|
|
153
|
+
if (!row) return flag("unresolved_placeholder", node, `${id} names no ledger row`)
|
|
154
|
+
if (!cited.has(id)) cited.set(id, node)
|
|
155
|
+
const problem = problemOf(row)
|
|
156
|
+
if (problem) flag(problem[0], node, problem[1])
|
|
157
|
+
}
|
|
158
|
+
for (const node of ctx.all.filter((cur) => cur.attrs["data-fact"] !== undefined)) {
|
|
159
|
+
const selector = node.attrs["data-quote-sel"] ?? "full"
|
|
160
|
+
check(node.attrs["data-fact"], node, (row) => (row.kind === "quote" ? quoteProblem(row, withoutFooter(node), selector) : factProblem(row, proseText(node), node.attrs)))
|
|
161
|
+
}
|
|
162
|
+
const docText = bare(proseText(ctx.body))
|
|
163
|
+
for (const entry of fillMap ?? []) {
|
|
164
|
+
check(entry.fact_id, ctx.body, (row) => {
|
|
165
|
+
const problem = row.kind === "quote" ? quoteProblem(row, entry.rendered, entry.selector ?? "full") : factProblem(row, entry.rendered)
|
|
166
|
+
if (problem || docText.includes(bare(entry.rendered))) return problem
|
|
167
|
+
return [row.kind === "quote" ? "quote_not_verbatim" : "value_mismatch", `filled text "${clip(entry.rendered)}" of ${row.fact_id} is no longer in the report`]
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
rowChecks(ctx, ledger, rows, cited, options, flag)
|
|
171
|
+
for (const tr of ctx.all.filter((node) => node.tag === "tr" && node.attrs["data-row-kind"] !== "comparison")) {
|
|
172
|
+
const groups = new Set(ctx.util.descendants(tr).map((node) => rows.get(node.attrs["data-fact"])?.observation_id).filter(Boolean))
|
|
173
|
+
if (groups.size > 1) flag("join_integrity_fail", tr, `one table row joins observations ${[...groups].join(", ")}`)
|
|
174
|
+
}
|
|
175
|
+
const meta = sourceChecks(ctx, rows, cited, sourcesDir, flag)
|
|
176
|
+
claimChecks(ctx, rows, (row) => provenance(sourcesDir, meta, row), flag)
|
|
177
|
+
const charts = chartFigures(ctx)
|
|
178
|
+
for (const { node, scope } of ctx.elements) {
|
|
179
|
+
const kind = ctx.kindOf(scope)
|
|
180
|
+
if ((node.tag === "q" || node.tag === "blockquote") && node.attrs["data-fact"] === undefined && kind !== "sources" && kind !== "toc") flag("quote_unbound", node, `<${node.tag}> with no Q row: "${clip(proseText(node))}"`)
|
|
181
|
+
}
|
|
182
|
+
traceChecks(ctx, charts, fillMap, flag, fillSiteFits(rows))
|
|
183
|
+
for (const [fig, labels] of charts) {
|
|
184
|
+
const problem = chartProblem(ctx, fig, labels, ledger)
|
|
185
|
+
if (problem) flag("chart_value_untraceable", fig, problem)
|
|
186
|
+
}
|
|
187
|
+
return out
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function rowChecks(ctx, ledger, rows, cited, options, flag) {
|
|
191
|
+
// An as-of of "latest ..." or "... per snapshot" names no fixed revision, so quote_stale_version is skipped.
|
|
192
|
+
const asOf = options.asOf && !/^latest\b|per snapshot/i.test(options.asOf) ? options.asOf : null
|
|
193
|
+
for (const [id, node] of cited) {
|
|
194
|
+
const row = rows.get(id)
|
|
195
|
+
if (row.fuzzy === true) flag("quote_fuzzy_match", node, `${id} was registered from a similarity match on an OCR-noisy snapshot`)
|
|
196
|
+
const revision = typeof row.revision === "string" ? row.revision : (row.revision?.id ?? row.revision?.tag ?? null)
|
|
197
|
+
if (asOf && revision && !asOf.includes(revision)) flag("quote_stale_version", node, `${id} is from revision ${revision}; the brief targets "${asOf}"`)
|
|
198
|
+
if ((row.kind === "derived" || row.kind === "converted") && options.recompute?.(ledger, row)?.ok === false) flag("value_mismatch", node, `${id} does not recompute from its operands`)
|
|
199
|
+
const window = row.snapshot_window
|
|
200
|
+
if (window && typeof window === "object" && window.start && window.end) {
|
|
201
|
+
const [start, end] = [Date.parse(window.start), Date.parse(window.end)]
|
|
202
|
+
const outside = (row.operands ?? []).find((op) => !(Date.parse(rows.get(op)?.observed_at) >= start && Date.parse(rows.get(op)?.observed_at) < end))
|
|
203
|
+
if (outside) flag("snapshot_mismatch", node, `operand ${outside} of ${id} was observed outside ${window.start} to ${window.end}`)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** meta.json kind of the snapshot holding the row's own locator artifact; undefined when the artifact is absent or escapes sources/<source_id>/. */
|
|
209
|
+
function provenance(sourcesDir, meta, row) {
|
|
210
|
+
const artifact = row.locator?.artifact
|
|
211
|
+
if (!sourcesDir || typeof row.source_id !== "string" || typeof artifact !== "string") return undefined
|
|
212
|
+
const dir = resolve(sourcesDir, row.source_id)
|
|
213
|
+
const path = resolve(dir, artifact)
|
|
214
|
+
return path.startsWith(`${dir}${sep}`) && existsSync(path) ? meta.get(row.source_id)?.kind : undefined
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function sourceChecks(ctx, rows, cited, sourcesDir, flag) {
|
|
218
|
+
const ids = new Set([...cited.keys()].map((id) => rows.get(id)?.source_id).filter(Boolean))
|
|
219
|
+
for (const match of proseText(ctx.body).matchAll(/\[S(\d+)\]/g)) ids.add(`S${match[1]}`)
|
|
220
|
+
const meta = new Map()
|
|
221
|
+
for (const id of sourcesDir ? ids : []) {
|
|
222
|
+
const [metaPath, textPath] = [join(sourcesDir, id, "meta.json"), join(sourcesDir, id, "text.txt")]
|
|
223
|
+
if (!existsSync(metaPath) || !existsSync(textPath)) continue
|
|
224
|
+
meta.set(id, JSON.parse(readFileSync(metaPath, "utf8")))
|
|
225
|
+
const actual = sha256Of(readFileSync(textPath))
|
|
226
|
+
if (meta.get(id).sha256 !== actual) flag("snapshot_hash_mismatch", ctx.body, `${id}/text.txt changed after its snapshot was pinned`)
|
|
227
|
+
}
|
|
228
|
+
return meta
|
|
229
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
import { exportRows, valueChecks } from "./fidelity-values.mjs"
|
|
7
|
+
import { buildContext } from "./gates-static.mjs"
|
|
8
|
+
import { parseHtml } from "./html-lite.mjs"
|
|
9
|
+
import { similarity } from "./text-similarity.mjs"
|
|
10
|
+
|
|
11
|
+
type Defect = { code: string; severity: string; selector: string; line: number; message: string; hint: string }
|
|
12
|
+
type Row = Record<string, unknown> & { fact_id: string }
|
|
13
|
+
type Ledger = { schemaVersion: number; rows: Row[]; events: unknown[] }
|
|
14
|
+
type Options = { ledger?: Ledger; fillMap?: unknown[] | null; sourcesDir?: string | null; baseDir?: string; recompute?: (ledger: Ledger, row: Row) => { ok: boolean }; asOf?: string }
|
|
15
|
+
|
|
16
|
+
const FIX = join(import.meta.dir, "tests", "fixtures", "fidelity")
|
|
17
|
+
const SOURCES = join(FIX, "sources")
|
|
18
|
+
const LEDGER: Ledger = JSON.parse(readFileSync(join(FIX, "ledger-verify.json"), "utf8"))
|
|
19
|
+
const FULL = "we estimate completion no earlier than 2027"
|
|
20
|
+
|
|
21
|
+
function ledgerWith(id: string, patch: Record<string, unknown>): Ledger {
|
|
22
|
+
const copy = structuredClone(LEDGER)
|
|
23
|
+
copy.rows = copy.rows.map((row) => (row.fact_id === id ? { ...row, ...patch } : row))
|
|
24
|
+
return copy
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function run(body: string, options: Options = {}): Defect[] {
|
|
28
|
+
const html = /<h2/.test(body) ? body : `<h2>Findings</h2>\n${body}`
|
|
29
|
+
const ctx = buildContext(parseHtml(html), { baseDir: options.baseDir ?? FIX })
|
|
30
|
+
const sourcesDir = options.sourcesDir === undefined ? SOURCES : options.sourcesDir
|
|
31
|
+
return valueChecks(ctx, options.ledger ?? LEDGER, options.fillMap ?? null, sourcesDir, options) as Defect[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const codes = (defects: Defect[]) => defects.map((defect) => defect.code)
|
|
35
|
+
const fact = (id: string, text: string, unit = "") => `<data class="fact" data-fact="${id}"${unit ? ` data-unit="${unit}"` : ""}>${text}</data>`
|
|
36
|
+
const quote = (sel: string, text: string) => `<p>They wrote <q data-fact="Q1" data-quote-sel="${sel}">${text}</q> [S4].</p>`
|
|
37
|
+
|
|
38
|
+
const TICKS = (labels: string[]) => `<g class="ticks" data-tick-step="10" data-tick-min="0" data-tick-max="30">${labels.map((label) => `<text>${label}</text>`).join("")}</g>`
|
|
39
|
+
const chart = (attrs: string, ticks: string[], labels: string[]) =>
|
|
40
|
+
`<figure ${attrs}><svg viewBox="0 0 200 100">${TICKS(ticks)}${labels.map((label) => `<text>${label}</text>`).join("")}</svg><figcaption>Share by segment [S4].</figcaption></figure>`
|
|
41
|
+
|
|
42
|
+
describe("untraceable numbers", () => {
|
|
43
|
+
test("#given a typed 12% with no ledger row #when checked #then untraceable_number", () => {
|
|
44
|
+
expect(codes(run("<p>Revenue grew 12% [S4].</p>"))).toEqual(["untraceable_number"])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("#given a table cell numeral without data-fact #when checked #then untraceable_number covers table cells", () => {
|
|
48
|
+
const defects = run("<table><tr><th>Metric</th><th>Value</th></tr><tr><td>Span</td><td>320 m</td></tr></table>")
|
|
49
|
+
expect(codes(defects)).toEqual(["untraceable_number"])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test("#given an appendix sentence with a citation but no row #when checked #then the citation does not exempt it", () => {
|
|
53
|
+
expect(codes(run("<h2>Appendix</h2><p>Revenue grew 12% [S4].</p>"))).toEqual(["untraceable_number"])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("#given the same sentence under Sources #when checked #then it is out of scope", () => {
|
|
57
|
+
expect(run("<h2>Sources</h2><p>Revenue grew 12% [S4].</p>")).toEqual([])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("#given a date in prose #when checked #then dates are in scope", () => {
|
|
61
|
+
expect(codes(run("<p>The board met on 2026-03-04 [S4].</p>"))).toEqual(["untraceable_number"])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("#given heading numbering, figure references, versions and times #when checked #then structural numerals are exempt", () => {
|
|
65
|
+
const body = "<h2>2. Findings</h2><p>See Figure 3 and table 2 on page 7; v1.2.3 shipped at 10:30 [S4].</p>"
|
|
66
|
+
expect(run(body)).toEqual([])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("#given the good report's facts #when checked #then numerals inside data-fact and Q elements are traced", () => {
|
|
70
|
+
const body = `<p>The span is ${fact("F1", "up to 320 m", "m")} long [S4].</p>${quote("full", FULL)}`
|
|
71
|
+
expect(run(body)).toEqual([])
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe("protected values", () => {
|
|
76
|
+
test("#given '320 mm' on a 320 m row #when checked #then unit_mismatch", () => {
|
|
77
|
+
expect(codes(run(`<p>Span ${fact("F1", "320 mm", "m")} [S4].</p>`))).toEqual(["unit_mismatch"])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("#given '1,273' on a 1,237 row #when checked #then value_mismatch", () => {
|
|
81
|
+
expect(codes(run(`<p>Units ${fact("F2", "1,273")} [S4].</p>`))).toEqual(["value_mismatch"])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("#given the converted row's render #when checked #then it verifies clean", () => {
|
|
85
|
+
expect(run(`<p>Span ${fact("F8", "320,000 mm (320 m)", "mm")} [S4].</p>`)).toEqual([])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test("#given a data-fact id with no ledger row #when checked #then unresolved_placeholder", () => {
|
|
89
|
+
expect(codes(run(`<p>Span ${fact("F99", "5 m")} [S4].</p>`))).toEqual(["unresolved_placeholder"])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("#given a leftover placeholder #when checked #then unresolved_placeholder", () => {
|
|
93
|
+
expect(codes(run("<p>Next year brings {{F9:growth}} more traffic [S4].</p>"))).toEqual(["unresolved_placeholder"])
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe("quotes", () => {
|
|
98
|
+
test("#given a digit changed inside a rendered quote #when checked #then quote_not_verbatim with no fuzzy downgrade", () => {
|
|
99
|
+
const altered = FULL.replace("2027", "2028")
|
|
100
|
+
expect(similarity(altered, FULL)).toBeGreaterThan(0.95)
|
|
101
|
+
expect(codes(run(quote("full", altered)))).toEqual(["quote_not_verbatim"])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const SELECTIONS: [string, string, string][] = [
|
|
105
|
+
["full", FULL, FULL.replace("estimate", "expect")],
|
|
106
|
+
["lead=4", "we estimate completion no\u2026", "we estimate completion soon\u2026"],
|
|
107
|
+
["from=no earlier;to=2027", "\u2026no earlier than 2027", "\u2026no later than 2027"],
|
|
108
|
+
]
|
|
109
|
+
for (const [sel, rendered, altered] of SELECTIONS) {
|
|
110
|
+
test(`#given fill's ${sel} rendering #when verified #then clean, and one altered word is quote_not_verbatim`, () => {
|
|
111
|
+
expect(run(quote(sel, rendered))).toEqual([])
|
|
112
|
+
expect(codes(run(quote(sel, altered)))).toEqual(["quote_not_verbatim"])
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
test("#given a quote row flagged fuzzy at registration #when cited #then quote_fuzzy_match describes the source", () => {
|
|
117
|
+
expect(codes(run(quote("full", FULL), { ledger: ledgerWith("Q1", { fuzzy: true }) }))).toEqual(["quote_fuzzy_match"])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test("#given a quote revision that differs from the brief's as-of anchor #when checked #then quote_stale_version", () => {
|
|
121
|
+
const ledger = ledgerWith("Q1", { revision: "v3.1" })
|
|
122
|
+
expect(codes(run(quote("full", FULL), { ledger, asOf: "v3.2 (release notes)" }))).toEqual(["quote_stale_version"])
|
|
123
|
+
expect(run(quote("full", FULL), { ledger, asOf: "v3.1" })).toEqual([])
|
|
124
|
+
expect(run(quote("full", FULL), { ledger, asOf: "latest at run start, 2026-10-23" })).toEqual([])
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe("quote binding", () => {
|
|
129
|
+
test("#given a <q> with no Q row #when checked #then quote_unbound (rule a)", () => {
|
|
130
|
+
expect(codes(run("<p><q>Completion is guaranteed.</q> [S4]</p>"))).toEqual(["quote_unbound"])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test("#given a quoted run of three words with a citation and no fillMap Q entry #when checked #then quote_unbound (rule c)", () => {
|
|
134
|
+
expect(codes(run('<p>"Completion is guaranteed." [S4]</p>', { fillMap: [] }))).toEqual(["quote_unbound"])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test("#given the same quote covered by a fillMap Q entry #when checked #then clean", () => {
|
|
138
|
+
const ledger = structuredClone(LEDGER)
|
|
139
|
+
ledger.rows.push({ ...ledger.rows.find((row) => row.fact_id === "Q1"), fact_id: "Q2", label: "guarantee", quote: "Completion is guaranteed.", raw: "Completion is guaranteed." } as Row)
|
|
140
|
+
const fillMap = [{ token: "{{Q2:guarantee}}", fact_id: "Q2", start: 0, end: 27, rendered: '"Completion is guaranteed."', selector: "full" }]
|
|
141
|
+
expect(run('<p>"Completion is guaranteed." [S4]</p>', { ledger, fillMap })).toEqual([])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test("#given a four-word quoted run with no citation #when checked #then quote_unbound (rule b)", () => {
|
|
145
|
+
expect(codes(run('<p>The plan says "no earlier than 2027" for now.</p>'))).toContain("quote_unbound")
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test("#given a single quoted UI label without a citation #when checked #then not a quotation", () => {
|
|
149
|
+
expect(run('<p>Press the "Save" button.</p>')).toEqual([])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test("#given a single quoted term followed by a citation #when checked #then quote_unbound (rule c)", () => {
|
|
153
|
+
expect(codes(run('<p>Press the "Save" button [S4].</p>'))).toEqual(["quote_unbound"])
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe("fill maps (Markdown)", () => {
|
|
158
|
+
test("#given a fillMap F entry whose text was edited after fill #when checked #then value_mismatch", () => {
|
|
159
|
+
const fillMap = [{ token: "{{F2:units_sold}}", fact_id: "F2", start: 0, end: 5, rendered: "1,273" }]
|
|
160
|
+
expect(codes(run("<p>Units sold reached 1,273 [S4].</p>", { fillMap }))).toEqual(["value_mismatch"])
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test("#given a fillMap F entry that matches its row and the text #when checked #then the numeral is traced", () => {
|
|
164
|
+
const fillMap = [{ token: "{{F2:units_sold}}", fact_id: "F2", start: 0, end: 5, rendered: "1,237" }]
|
|
165
|
+
expect(run("<p>Units sold reached 1,237 [S4].</p>", { fillMap })).toEqual([])
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe("derivations, joins, snapshots", () => {
|
|
170
|
+
test("#given recompute reports a tampered derived row #when checked #then value_mismatch", () => {
|
|
171
|
+
const body = `<p>Span ${fact("F8", "320,000 mm (320 m)", "mm")} [S4].</p>`
|
|
172
|
+
expect(codes(run(body, { recompute: () => ({ ok: false }) }))).toEqual(["value_mismatch"])
|
|
173
|
+
expect(run(body, { recompute: () => ({ ok: true }) })).toEqual([])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test("#given a derived row whose operand was observed outside its snapshot window #when checked #then snapshot_mismatch", () => {
|
|
177
|
+
const ledger = ledgerWith("F8", { snapshot_window: { dataset_id: "d1", start: "2026-11-01T00:00:00Z", end: "2026-12-01T00:00:00Z" } })
|
|
178
|
+
expect(codes(run(`<p>Span ${fact("F8", "320,000 mm (320 m)", "mm")} [S4].</p>`, { ledger }))).toEqual(["snapshot_mismatch"])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test("#given a table row joining two observations #when checked #then join_integrity_fail", () => {
|
|
182
|
+
const table = `<table><tr><td>${fact("F1", "320 m", "m")}</td><td>${fact("F3", "12%", "%")}</td></tr></table>`
|
|
183
|
+
expect(codes(run(table))).toEqual(["join_integrity_fail"])
|
|
184
|
+
expect(run(table.replace("<tr>", '<tr data-row-kind="comparison">'))).toEqual([])
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test("#given a snapshot whose text changed after pinning #when checked #then snapshot_hash_mismatch", () => {
|
|
188
|
+
const dir = mkdtempSync(join(tmpdir(), "fidelity-values-"))
|
|
189
|
+
try {
|
|
190
|
+
cpSync(SOURCES, dir, { recursive: true })
|
|
191
|
+
writeFileSync(join(dir, "S4", "text.txt"), "edited\n")
|
|
192
|
+
expect(codes(run(`<p>Span ${fact("F1", "up to 320 m", "m")} [S4].</p>`, { sourcesDir: dir }))).toEqual(["snapshot_hash_mismatch"])
|
|
193
|
+
} finally {
|
|
194
|
+
rmSync(dir, { recursive: true, force: true })
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe("verification claims are structural (data-claim-kind)", () => {
|
|
201
|
+
const verified = (id: string, text: string, unit = "") => fact(id, text, unit).replace("<data ", '<data data-claim-kind="verified" ')
|
|
202
|
+
const severities = (defects: Defect[]) => defects.map((defect) => `${defect.code}:${defect.severity}`)
|
|
203
|
+
|
|
204
|
+
test("#given a claim MARKED verified citing a measured page row #when checked #then unsupported_verification major", () => {
|
|
205
|
+
expect(severities(run(`<p>The span is ${verified("F1", "up to 320 m", "m")} [S4].</p>`))).toEqual(["unsupported_verification:major"])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test("#given a claim MARKED verified citing a recorded command row #when checked #then no defect", () => {
|
|
209
|
+
expect(run(`<p>The suite ran with ${verified("F7", "42 passed")} [S5].</p>`)).toEqual([])
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test("#given an UNMARKED pretend-verification sentence #when checked #then verification_claim_unmarked as a minor warning and no major", () => {
|
|
213
|
+
expect(severities(run("<p>We verified the span by running the measurement script [S4].</p>"))).toEqual(["verification_claim_unmarked:minor"])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const quiet = [
|
|
217
|
+
`<p>The tests showed a ${fact("F3", "12%", "%")} drop [S4].</p>`,
|
|
218
|
+
`<p>The bridge, which ran across the river, spans ${fact("F1", "up to 320 m", "m")} [S4].</p>`,
|
|
219
|
+
`<p>The span is ${fact("F1", "up to 320 m", "m")}, as confirmed with the annual inspection report [S4].</p>`,
|
|
220
|
+
`<p>Clinical tests confirmed the span of ${fact("F1", "up to 320 m", "m")} [S4].</p>`,
|
|
221
|
+
`<p>확인된 길이는 ${fact("F1", "최대 320 m", "m")} [S4].</p>`,
|
|
222
|
+
]
|
|
223
|
+
for (const body of quiet) {
|
|
224
|
+
test(`#given findings prose ${JSON.stringify(body.replace(/<[^>]+>/g, ""))} #when checked #then no warning and no major`, () => expect(run(body)).toEqual([]))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
test("#given an UNMARKED Korean run-and-check sentence #when checked #then verification_claim_unmarked minor only", () => {
|
|
228
|
+
expect(severities(run(`<p>우리가 실행해 확인한 길이는 ${fact("F1", "최대 320 m", "m")} [S4].</p>`))).toEqual(["verification_claim_unmarked:minor"])
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test("#given the same run-and-check sentence MARKED verified on a command row #when checked #then no warning", () => {
|
|
232
|
+
expect(run(`<p>We ran the suite by running the test script, and it reported ${verified("F7", "42 passed")} [S5].</p>`)).toEqual([])
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe("charts", () => {
|
|
237
|
+
const SRC = 'data-chart-source="assets/share.csv"'
|
|
238
|
+
|
|
239
|
+
test("#given a chart with an on-grid ticks group and a data label matching the CSV #when checked #then clean", () => {
|
|
240
|
+
expect(run(chart(SRC, ["0", "10", "20", "30"], ["12%"]))).toEqual([])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test("#given an off-grid tick #when checked #then chart_value_untraceable", () => {
|
|
244
|
+
expect(codes(run(chart(SRC, ["0", "10", "25", "30"], ["12%"])))).toEqual(["chart_value_untraceable"])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test("#given a data label with no CSV plot value #when checked #then chart_value_untraceable", () => {
|
|
248
|
+
expect(codes(run(chart(SRC, ["0", "10", "20", "30"], ["13%"])))).toEqual(["chart_value_untraceable"])
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test("#given a chart figure without data-chart-source #when checked #then chart_value_untraceable", () => {
|
|
252
|
+
expect(codes(run(chart("", ["0", "10", "20", "30"], ["12%"])))).toEqual(["chart_value_untraceable"])
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test("#given a CSV whose row differs from the ledger export #when checked #then chart_value_untraceable", () => {
|
|
256
|
+
const dir = mkdtempSync(join(tmpdir(), "fidelity-chart-"))
|
|
257
|
+
try {
|
|
258
|
+
writeFileSync(join(dir, "share.csv"), "fact_id,plot_value,unit\nF5,13,%\nF6,27,%\n")
|
|
259
|
+
expect(codes(run(chart('data-chart-source="share.csv"', ["0", "10", "20", "30"], ["27%"]), { baseDir: dir }))).toEqual(["chart_value_untraceable"])
|
|
260
|
+
} finally {
|
|
261
|
+
rmSync(dir, { recursive: true, force: true })
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test("#given the ledger #when exported #then each active value row gives fact_id, plot_value and unit", () => {
|
|
266
|
+
const rows = exportRows(LEDGER) as { fact_id: string; plot_value: string; unit: string }[]
|
|
267
|
+
expect(rows.find((row) => row.fact_id === "F5")).toMatchObject({ plot_value: "12", unit: "%" })
|
|
268
|
+
expect(rows.some((row) => row.fact_id === "Q1")).toBe(false)
|
|
269
|
+
})
|
|
270
|
+
})
|