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,118 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
import { locateFind, locateMatch, locatePageLine, locateTable } from "./fact-locate.mjs"
|
|
6
|
+
import { snapshotFromHtml } from "./source-snapshot.mjs"
|
|
7
|
+
|
|
8
|
+
const FIXTURES = join(import.meta.dir, "tests", "fixtures", "fidelity", "sources")
|
|
9
|
+
const S4_HTML = readFileSync(join(FIXTURES, "S4.html"), "utf8")
|
|
10
|
+
const S4_TEXT = snapshotFromHtml(S4_HTML)
|
|
11
|
+
const S9_TEXT = readFileSync(join(FIXTURES, "S9.txt"), "utf8")
|
|
12
|
+
const MAX = "\uCD5C\uB300"
|
|
13
|
+
|
|
14
|
+
type Located = {
|
|
15
|
+
value: string | null
|
|
16
|
+
unit: string | null
|
|
17
|
+
raw: string
|
|
18
|
+
quote: string
|
|
19
|
+
value_span: [number, number] | null
|
|
20
|
+
sentence_id: string
|
|
21
|
+
qualifier: string | null
|
|
22
|
+
fuzzy?: boolean
|
|
23
|
+
locator: { artifact: string; char_start: number; char_end: number; page?: number; line: number; table?: string; row_label?: string; col_label?: string }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function expectAnchored(text: string, result: Located): void {
|
|
27
|
+
expect(text.slice(result.locator.char_start, result.locator.char_end)).toBe(result.quote)
|
|
28
|
+
if (result.value_span) expect(result.quote.slice(result.value_span[0], result.value_span[1])).toBe(result.raw)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("locateFind", () => {
|
|
32
|
+
test("#given an anchor followed by a quantity #when located #then the value, unit, sentence and locator bind to the snapshot", () => {
|
|
33
|
+
const result = locateFind(S4_TEXT, { find: "Span length", sourceId: "S4" })
|
|
34
|
+
expect(result).toMatchObject({ value: "320", unit: "m", raw: "320 m", quote: "Span length: 320 m", qualifier: null })
|
|
35
|
+
expect(result.sentence_id).toMatch(/^S4:s\d+$/)
|
|
36
|
+
expect(result.locator.artifact).toBe("S4/text.txt")
|
|
37
|
+
expectAnchored(S4_TEXT, result)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("#given an expected unit that differs #when located #then unit_expected_mismatch", () => {
|
|
41
|
+
expect(locateFind(S4_TEXT, { find: "Span length", unitExpected: "mm", sourceId: "S4" })).toMatchObject({ error: "unit_expected_mismatch" })
|
|
42
|
+
expect(locateFind(S4_TEXT, { find: "Span length", unitExpected: "meters", sourceId: "S4" })).toMatchObject({ value: "320" })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("#given a Korean anchor before a scaled amount #when located #then the first numeral after the anchor is the value and the anchor is the qualifier", () => {
|
|
46
|
+
const result = locateFind(S9_TEXT, { find: MAX, sourceId: "S9" })
|
|
47
|
+
expect(result).toMatchObject({ value: "4200000000000", unit: "KRW", qualifier: MAX, sentence_id: "S9:s1" })
|
|
48
|
+
expectAnchored(S9_TEXT, result)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("#given an English bounding phrase before the value #when located #then it is the qualifier", () => {
|
|
52
|
+
const text = "The contract costs up to 40 million USD in total."
|
|
53
|
+
expect(locateFind(text, { find: "costs", sourceId: "S3" })).toMatchObject({ value: "40000000", unit: "USD", qualifier: "up to" })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("#given an anchor that occurs twice #when located #then fact_ambiguous lists both candidates", () => {
|
|
57
|
+
const result = locateFind(S4_TEXT, { find: "span cost", sourceId: "S4" })
|
|
58
|
+
expect(result.error).toBe("fact_ambiguous")
|
|
59
|
+
expect(result.candidates).toHaveLength(2)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("#given a missing anchor, or no numeral within the anchor's sentence #when located #then fact_not_located", () => {
|
|
63
|
+
expect(locateFind(S4_TEXT, { find: "nonexistent", sourceId: "S4" })).toMatchObject({ error: "fact_not_located", candidates: [] })
|
|
64
|
+
expect(locateFind("Total reached.\nIt was 12 m.", { find: "Total", sourceId: "S1" })).toMatchObject({ error: "fact_not_located" })
|
|
65
|
+
expect(locateFind(`Delta ${"x".repeat(90)} 7 m`, { find: "Delta", sourceId: "S1" })).toMatchObject({ error: "fact_not_located" })
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe("locateTable", () => {
|
|
70
|
+
test("#given a table found by a header label #when the row and column labels match #then the cell value binds to its text.txt line", () => {
|
|
71
|
+
const result = locateTable(S4_HTML, { table: "Length", row: "Span", col: "Length", sourceId: "S4" })
|
|
72
|
+
expect(result).toMatchObject({ value: "320", unit: "m", quote: "320 m", locator: { table: "Length", row_label: "Span", col_label: "Length" } })
|
|
73
|
+
expect(result.sentence_id).toMatch(/^S4:s\d+$/)
|
|
74
|
+
expectAnchored(S4_TEXT, result)
|
|
75
|
+
expect(locateTable(S4_HTML, { table: "dimensions", row: "Approach", col: "Length", sourceId: "S4" })).toMatchObject({ value: "85" })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("#given an unknown table, row or column, or a duplicated row label #when located #then it fails without guessing", () => {
|
|
79
|
+
expect(locateTable(S4_HTML, { table: "Width", row: "Span", col: "Length", sourceId: "S4" })).toMatchObject({ error: "fact_not_located" })
|
|
80
|
+
expect(locateTable(S4_HTML, { table: "Length", row: "Deck", col: "Length", sourceId: "S4" })).toMatchObject({ error: "fact_not_located" })
|
|
81
|
+
const twice = "<table><tr><th>Item</th><th>Cost</th></tr><tr><td>Span</td><td>3</td></tr><tr><td>Span</td><td>4</td></tr></table>"
|
|
82
|
+
expect(locateTable(twice, { table: "Cost", row: "Span", col: "Cost", sourceId: "S5" })).toMatchObject({ error: "fact_ambiguous" })
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe("locatePageLine", () => {
|
|
87
|
+
const text = "Intro line\nRevenue: 15 %\fCost: 3 m\nBetween 1 m and 2 m"
|
|
88
|
+
|
|
89
|
+
test("#given a page and line holding one quantity #when located #then it binds with its page and line", () => {
|
|
90
|
+
const result = locatePageLine(text, { page: 1, line: 2, sourceId: "S6" })
|
|
91
|
+
expect(result).toMatchObject({ value: "15", unit: "%", quote: "Revenue: 15 %", locator: { line: 2 } })
|
|
92
|
+
expectAnchored(text, result)
|
|
93
|
+
expect(locatePageLine(text, { page: 2, line: 1, sourceId: "S6" })).toMatchObject({ value: "3", locator: { page: 2, line: 1 } })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("#given a line with two quantities or a missing page #when located #then ambiguous or not located", () => {
|
|
97
|
+
expect(locatePageLine(text, { page: 2, line: 2, sourceId: "S6" })).toMatchObject({ error: "fact_ambiguous" })
|
|
98
|
+
expect(locatePageLine(text, { page: 3, line: 1, sourceId: "S6" })).toMatchObject({ error: "fact_not_located" })
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe("locateMatch", () => {
|
|
103
|
+
test("#given an exact match #when located #then raw is the snapshot bytes, the quote is the whole sentence, and there is no value span", () => {
|
|
104
|
+
const result = locateMatch(S4_TEXT, { match: "no earlier than 2027", sourceId: "S4" })
|
|
105
|
+
expect(result).toMatchObject({ raw: "no earlier than 2027", quote: "After the delay, we estimate completion no earlier than 2027.", value_span: null, value: null, fuzzy: false })
|
|
106
|
+
expectAnchored(S4_TEXT, result)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test("#given a near match at or above 0.95 similarity #when located #then the window returns with fuzzy true; below it, fact_not_located", () => {
|
|
110
|
+
const near = locateMatch(S4_TEXT, { match: "we estimate completion no earlier then 2027", sourceId: "S4" })
|
|
111
|
+
expect(near).toMatchObject({ raw: "we estimate completion no earlier than 2027", fuzzy: true })
|
|
112
|
+
expect(locateMatch(S4_TEXT, { match: "completion is certain by 2026", sourceId: "S4" })).toMatchObject({ error: "fact_not_located" })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test("#given a phrase that occurs twice #when located #then fact_ambiguous", () => {
|
|
116
|
+
expect(locateMatch(S4_TEXT, { match: "span cost", sourceId: "S4" })).toMatchObject({ error: "fact_ambiguous" })
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// fact-pairs.mjs - (source, report sentence) pairs for the meaning verifier.
|
|
2
|
+
//
|
|
3
|
+
// extractPairs walks the prose sentences of a filled HTML report and emits, in document order:
|
|
4
|
+
// a quote pair per distinct ledger quote cited by a findings sentence (facts sharing one quote
|
|
5
|
+
// share one pair), a claim pair per sentence bound only by data-claim (the statement comes from
|
|
6
|
+
// the claim index), and a translation pair per data-translates span. force_strong routes a pair
|
|
7
|
+
// to the strong reader when the lexicon raised name_outside_quote or scope_widened_warn, when the
|
|
8
|
+
// hedge-position heuristic fired, or when the source hedges and the sentence's last six tokens do
|
|
9
|
+
// not. No model, no IO: the readers run as separate lanes. Plain Node ESM; no Hangul here.
|
|
10
|
+
|
|
11
|
+
import { isExemptKind, proseSentences } from "./fidelity-binding.mjs"
|
|
12
|
+
import { isKoreanTerm, lexicalPattern, lexiconChecks } from "./fidelity-lexicon.mjs"
|
|
13
|
+
import { sentenceKey } from "./fidelity-state.mjs"
|
|
14
|
+
import { buildContext } from "./gates-static.mjs"
|
|
15
|
+
import { parseHtml, proseText } from "./html-lite.mjs"
|
|
16
|
+
import { HEDGES } from "./lexicon.mjs"
|
|
17
|
+
import { normalizeText } from "./text-normalize.mjs"
|
|
18
|
+
|
|
19
|
+
/** Every pair kind the producers below emit; consumers count and schema pairs by this list. */
|
|
20
|
+
export const PAIR_KINDS = Object.freeze(["quote", "claim", "translation"])
|
|
21
|
+
const TAIL = 6
|
|
22
|
+
|
|
23
|
+
const lang = (text) => (isKoreanTerm(text) ? "ko" : "en")
|
|
24
|
+
/** Sentence text with its fact spans cut out, so a Korean value does not make an English sentence Korean. */
|
|
25
|
+
const outsideFacts = (sentence) => sentence.facts.reduceRight((text, fact) => text.slice(0, fact.start - sentence.start) + text.slice(fact.end - sentence.start), sentence.text)
|
|
26
|
+
|
|
27
|
+
/** Latin hedges match whole words ("exact"); Korean hedges match as substrings, one syllable as a token prefix ("stem"). */
|
|
28
|
+
const HEDGE_RES = HEDGES.map((term) => lexicalPattern(term, isKoreanTerm(term) ? "stem" : "exact"))
|
|
29
|
+
|
|
30
|
+
/** Hedge terms that are also month names; capitalized and followed by a date number, they are months. */
|
|
31
|
+
const MONTH_WORDS = new Set(["may"])
|
|
32
|
+
|
|
33
|
+
/** A capitalized Latin match after the first word is a name or a month ("in May"), not a hedge. */
|
|
34
|
+
const isHedgeAt = (text, match) => {
|
|
35
|
+
if (!/^\p{Lu}/u.test(match[0]) || isKoreanTerm(match[0])) return true
|
|
36
|
+
if (match.index !== 0) return false
|
|
37
|
+
// At the start of a text a capital is only sentence case, so the word still hedges - unless it is a
|
|
38
|
+
// month name followed by a date number ("May 2024 sales rose.").
|
|
39
|
+
return !(MONTH_WORDS.has(match[0].toLowerCase()) && /^\s+\p{Nd}/u.test(text.slice(match.index + match[0].length)))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const hasHedge = (text) => {
|
|
43
|
+
const normal = normalizeText(text)
|
|
44
|
+
return HEDGE_RES.some((re) => [...normal.matchAll(re)].some((match) => isHedgeAt(normal, match)))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The source hedges and none of the sentence's last six tokens carries a hedge. */
|
|
48
|
+
function tailDropsHedge(source, sentence) {
|
|
49
|
+
if (!hasHedge(source)) return false
|
|
50
|
+
return !hasHedge(normalizeText(sentence).split(/\s+/u).filter(Boolean).slice(-TAIL).join(" "))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function slugOf(scope) {
|
|
54
|
+
const heading = scope.section?.heading
|
|
55
|
+
if (!heading) return ""
|
|
56
|
+
return (heading.attrs.id ?? proseText(heading))
|
|
57
|
+
.toLowerCase()
|
|
58
|
+
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
|
59
|
+
.replace(/^-+|-+$/gu, "")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function claimMap(claims) {
|
|
63
|
+
const list = Array.isArray(claims) ? claims : (claims?.claims ?? [])
|
|
64
|
+
return new Map(list.map((claim) => [claim.claim_id, claim]))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const idsOf = (attr) => String(attr ?? "").split(/[\s,]+/u).filter(Boolean)
|
|
68
|
+
|
|
69
|
+
/** Claim ids that bind a sentence: on the block or an ancestor, or on an inner element whose text the sentence holds. */
|
|
70
|
+
function claimIdsFor(ctx, block, sentence) {
|
|
71
|
+
const { closest, descendants } = ctx.util
|
|
72
|
+
const up = block.owner.attrs["data-claim"] !== undefined ? block.owner : closest(block.owner, (node) => node.attrs?.["data-claim"] !== undefined)
|
|
73
|
+
const ids = up ? idsOf(up.attrs["data-claim"]) : []
|
|
74
|
+
const text = normalizeText(sentence.text)
|
|
75
|
+
for (const node of descendants(block.owner)) {
|
|
76
|
+
if (node.attrs?.["data-claim"] === undefined) continue
|
|
77
|
+
const inner = normalizeText(proseText(node))
|
|
78
|
+
if (inner !== "" && text.includes(inner)) ids.push(...idsOf(node.attrs["data-claim"]))
|
|
79
|
+
}
|
|
80
|
+
return [...new Set(ids)]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function exempt(ctx, node) {
|
|
84
|
+
return isExemptKind(node.attrs["data-kind"]) || Boolean(ctx.util.closest(node, (cur) => isExemptKind(cur.attrs?.["data-kind"])))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Lexicon routing for every fact span of one quote group in one sentence. */
|
|
88
|
+
function lexiconRouted(group, sentence) {
|
|
89
|
+
const probes = group.facts.map((fact, index) => ({
|
|
90
|
+
pair_id: String(index),
|
|
91
|
+
quote: group.quote,
|
|
92
|
+
sentence: sentence.text,
|
|
93
|
+
span: [fact.start - sentence.start, fact.end - sentence.start],
|
|
94
|
+
qualifier: group.row.kind === "quote" ? null : (group.rows.get(fact.id)?.qualifier ?? null),
|
|
95
|
+
}))
|
|
96
|
+
return lexiconChecks(probes).forceStrong.length > 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function quotePairs(sentence, rows, filled, slug, reportLang) {
|
|
100
|
+
const groups = new Map()
|
|
101
|
+
for (const fact of sentence.facts) {
|
|
102
|
+
const row = rows.get(fact.id)
|
|
103
|
+
if (!row?.quote || (filled && !filled.has(fact.id))) continue
|
|
104
|
+
const group = groups.get(row.quote) ?? { quote: row.quote, row, rows, facts: [], ids: [] }
|
|
105
|
+
group.facts.push(fact)
|
|
106
|
+
if (!group.ids.includes(fact.id)) group.ids.push(fact.id)
|
|
107
|
+
groups.set(row.quote, group)
|
|
108
|
+
}
|
|
109
|
+
const allIds = [...new Set(sentence.facts.map((fact) => fact.id))]
|
|
110
|
+
return [...groups.values()].map((group) => ({
|
|
111
|
+
kind: "quote",
|
|
112
|
+
sentence_key: sentenceKey(slug, allIds),
|
|
113
|
+
fact_ids: [...group.ids].sort(),
|
|
114
|
+
quote: group.quote,
|
|
115
|
+
sentence: sentence.text,
|
|
116
|
+
source_lang: lang(group.quote),
|
|
117
|
+
report_lang: reportLang ?? lang(outsideFacts(sentence)),
|
|
118
|
+
force_strong: lexiconRouted(group, sentence) || tailDropsHedge(group.quote, sentence.text),
|
|
119
|
+
}))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function claimPairs(ctx, block, sentence, byClaim, slug, reportLang) {
|
|
123
|
+
if (sentence.facts.length > 0 || exempt(ctx, block.owner)) return []
|
|
124
|
+
return claimIdsFor(ctx, block, sentence)
|
|
125
|
+
.map((id) => byClaim.get(id))
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.map((claim) => ({
|
|
128
|
+
kind: "claim",
|
|
129
|
+
sentence_key: sentenceKey(slug, [claim.claim_id]),
|
|
130
|
+
claim_id: claim.claim_id,
|
|
131
|
+
statement: claim.statement,
|
|
132
|
+
sentence: sentence.text,
|
|
133
|
+
source_lang: lang(claim.statement),
|
|
134
|
+
report_lang: reportLang ?? lang(sentence.text),
|
|
135
|
+
force_strong: lexiconChecks([{ pair_id: "c", quote: claim.statement, sentence: sentence.text }]).forceStrong.length > 0 || tailDropsHedge(claim.statement, sentence.text),
|
|
136
|
+
}))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function translationPair(node, rows, filled, slug, reportLang) {
|
|
140
|
+
const id = node.attrs["data-translates"]
|
|
141
|
+
const row = rows.get(id)
|
|
142
|
+
const text = proseText(node)
|
|
143
|
+
if (!row?.quote || text === "" || (filled && !filled.has(id))) return null
|
|
144
|
+
return {
|
|
145
|
+
kind: "translation",
|
|
146
|
+
sentence_key: sentenceKey(slug, [id]),
|
|
147
|
+
fact_ids: [id],
|
|
148
|
+
quote: row.quote,
|
|
149
|
+
sentence: text,
|
|
150
|
+
source_lang: lang(row.quote),
|
|
151
|
+
report_lang: reportLang ?? lang(text),
|
|
152
|
+
force_strong: tailDropsHedge(row.quote, text),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} html the filled report
|
|
158
|
+
* @param {{ rows: object[] }} ledger
|
|
159
|
+
* @param {{ claims: object[] } | object[] | null} claims indexClaims(...) output or its claims array
|
|
160
|
+
* @param {{ fact_id: string }[]} [fillMap] fact fill's map; when given, only facts it filled are paired
|
|
161
|
+
* @returns {object[]} pairs with pair_id P1, P2, ... in document order
|
|
162
|
+
*/
|
|
163
|
+
export function extractPairs(html, ledger, claims, fillMap) {
|
|
164
|
+
const ctx = buildContext(parseHtml(html), {})
|
|
165
|
+
const rows = new Map((ledger?.rows ?? []).map((row) => [row.fact_id, row]))
|
|
166
|
+
const filled = Array.isArray(fillMap) ? new Set(fillMap.map((entry) => entry.fact_id)) : null
|
|
167
|
+
const byClaim = claimMap(claims)
|
|
168
|
+
const htmlLang = ctx.all.find((node) => node.tag === "html")?.attrs.lang
|
|
169
|
+
const reportLang = htmlLang ? htmlLang.toLowerCase().split("-")[0] : null
|
|
170
|
+
const out = []
|
|
171
|
+
const seenSpans = new Set()
|
|
172
|
+
for (const block of proseSentences(ctx)) {
|
|
173
|
+
const findings = Boolean(block.scope.section) && block.kind === null
|
|
174
|
+
if (!findings) continue
|
|
175
|
+
const slug = slugOf(block.scope)
|
|
176
|
+
for (const sentence of block.sentences) {
|
|
177
|
+
out.push(...quotePairs(sentence, rows, filled, slug, reportLang), ...claimPairs(ctx, block, sentence, byClaim, slug, reportLang))
|
|
178
|
+
}
|
|
179
|
+
for (const node of ctx.util.descendants(block.owner)) {
|
|
180
|
+
if (node.attrs?.["data-translates"] === undefined || seenSpans.has(node)) continue
|
|
181
|
+
seenSpans.add(node)
|
|
182
|
+
const pair = translationPair(node, rows, filled, slug, reportLang)
|
|
183
|
+
if (pair) out.push(pair)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out.map((pair, index) => ({ pair_id: `P${index + 1}`, ...pair }))
|
|
187
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
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 { extractPairs, hasHedge, PAIR_KINDS } from "./fact-pairs.mjs"
|
|
7
|
+
import { fill } from "./fact-fill.mjs"
|
|
8
|
+
|
|
9
|
+
type Row = Record<string, unknown> & { fact_id: string }
|
|
10
|
+
type Pair = Record<string, unknown> & { pair_id: string; kind: string; sentence: string }
|
|
11
|
+
|
|
12
|
+
const FIXTURES = join(import.meta.dir, "tests/fixtures/fidelity")
|
|
13
|
+
const BASE = JSON.parse(readFileSync(join(FIXTURES, "ledger-basic.json"), "utf8")) as { schemaVersion: number; rows: Row[] }
|
|
14
|
+
const Q7 = BASE.rows.find((row) => row.fact_id === "Q7")
|
|
15
|
+
if (Q7 === undefined) throw new Error("ledger-basic.json lost Q7")
|
|
16
|
+
const Q8 = { ...Q7, fact_id: "Q8", label: "outlook", quote: "Demand may recover in 2027" }
|
|
17
|
+
const LEDGER = { ...BASE, rows: [...BASE.rows, Q8] }
|
|
18
|
+
const CLAIMS = indexClaims(readFileSync(join(FIXTURES, "claim-graph.md"), "utf8"))
|
|
19
|
+
|
|
20
|
+
const SOURCE = [
|
|
21
|
+
"<!doctype html><html lang=\"en\"><body>",
|
|
22
|
+
"<h1>Bridge report</h1>",
|
|
23
|
+
"<h2 id=\"findings\">Findings</h2>",
|
|
24
|
+
"<p>The span is {{F12:span_len:q}}, or {{F40:span_mm}} [S4].</p>",
|
|
25
|
+
"<p>The rate is {{F13:rate}} [S4].</p>",
|
|
26
|
+
"<p>The operator wrote {{Q8:outlook}}, so the recovery is certain and fully funded now.</p>",
|
|
27
|
+
"<p>The note says {{Q7:completion}}<span data-translates=\"Q7\">completion is not expected before 2027</span>.</p>",
|
|
28
|
+
"<p data-claim=\"C2\">Revenue grew 10 percent in 2025.</p>",
|
|
29
|
+
"<p data-kind=\"opinion\">This looks like a sound plan.</p>",
|
|
30
|
+
"<h2>Sources</h2>",
|
|
31
|
+
"<ol><li id=\"s4\">[S4] Bridge authority page, retrieved 2026-10-01. Its span is {{F13:rate}}.</li></ol>",
|
|
32
|
+
"</body></html>",
|
|
33
|
+
].join("\n")
|
|
34
|
+
|
|
35
|
+
function filled(): { out: string; fillMap: { fact_id: string }[] } {
|
|
36
|
+
const result = fill(SOURCE, LEDGER, { format: "html" })
|
|
37
|
+
if (result.out === null) throw new Error(`fixture failed to fill: ${JSON.stringify(result.errors)}`)
|
|
38
|
+
return { out: result.out, fillMap: result.fillMap as { fact_id: string }[] }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pairs(): Pair[] {
|
|
42
|
+
const { out, fillMap } = filled()
|
|
43
|
+
return extractPairs(out, LEDGER, CLAIMS, fillMap) as Pair[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("extractPairs: a filled report", () => {
|
|
47
|
+
test("#given the filled fixture #when extracted #then pairs come in document order with P ids", () => {
|
|
48
|
+
const result = pairs()
|
|
49
|
+
expect(result.map((pair) => [pair.pair_id, pair.kind])).toEqual([
|
|
50
|
+
["P1", "quote"],
|
|
51
|
+
["P2", "quote"],
|
|
52
|
+
["P3", "quote"],
|
|
53
|
+
["P4", "quote"],
|
|
54
|
+
["P5", "translation"],
|
|
55
|
+
["P6", "claim"],
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("#given the fixture that produces every pair kind #when extracted #then the kinds are exactly PAIR_KINDS", () => {
|
|
60
|
+
expect([...new Set(pairs().map((pair) => pair.kind))].sort()).toEqual([...PAIR_KINDS].sort())
|
|
61
|
+
expect(Object.isFrozen(PAIR_KINDS)).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("#given two facts sharing one source quote #when extracted #then one quote pair names both", () => {
|
|
65
|
+
const [first] = pairs()
|
|
66
|
+
expect(first).toMatchObject({
|
|
67
|
+
kind: "quote",
|
|
68
|
+
sentence_key: "findings#F12,F40",
|
|
69
|
+
fact_ids: ["F12", "F40"],
|
|
70
|
+
quote: "The bridge has a main span of up to 320 m.",
|
|
71
|
+
source_lang: "en",
|
|
72
|
+
report_lang: "en",
|
|
73
|
+
force_strong: false,
|
|
74
|
+
})
|
|
75
|
+
expect(first.sentence).toContain("up to 320 m")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("#given an unhedged quote with its value kept #when extracted #then force_strong stays false", () => {
|
|
79
|
+
expect(pairs()[1]).toMatchObject({ fact_ids: ["F13"], quote: "The rate reached 15.2% in May.", force_strong: false })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("#given a hedged quote whose sentence ends without a hedge #when extracted #then force_strong is true", () => {
|
|
83
|
+
expect(pairs()[2]).toMatchObject({ fact_ids: ["Q8"], quote: "Demand may recover in 2027", force_strong: true })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("#given a data-translates span #when extracted #then a translation pair carries the span text", () => {
|
|
87
|
+
const result = pairs()
|
|
88
|
+
expect(result[3]).toMatchObject({ kind: "quote", fact_ids: ["Q7"] })
|
|
89
|
+
expect(result[4]).toMatchObject({
|
|
90
|
+
kind: "translation",
|
|
91
|
+
fact_ids: ["Q7"],
|
|
92
|
+
quote: "we estimate completion no earlier than 2027",
|
|
93
|
+
sentence: "completion is not expected before 2027",
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("#given a sentence bound only by data-claim #when extracted #then a claim pair carries the node statement", () => {
|
|
98
|
+
expect(pairs()[5]).toMatchObject({
|
|
99
|
+
kind: "claim",
|
|
100
|
+
claim_id: "C2",
|
|
101
|
+
statement: "Revenue grew about 10 percent in 2025.",
|
|
102
|
+
sentence: "Revenue grew 10 percent in 2025.",
|
|
103
|
+
force_strong: true,
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test("#given an opinion paragraph and a sources section #when extracted #then neither yields a pair", () => {
|
|
108
|
+
const sentences = pairs().map((pair) => pair.sentence)
|
|
109
|
+
expect(sentences.some((text) => text.includes("sound plan"))).toBe(false)
|
|
110
|
+
expect(sentences.some((text) => text.includes("Bridge authority"))).toBe(false)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test("#given a fill map that omits a fact #when extracted #then that hand-typed fact yields no pair", () => {
|
|
114
|
+
const { out, fillMap } = filled()
|
|
115
|
+
const kept = fillMap.filter((entry) => entry.fact_id !== "F13")
|
|
116
|
+
const result = extractPairs(out, LEDGER, CLAIMS, kept) as Pair[]
|
|
117
|
+
expect(result.some((pair) => Array.isArray(pair.fact_ids) && (pair.fact_ids as string[]).includes("F13"))).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test("#given no fill map #when extracted #then every data-fact element counts as filled", () => {
|
|
121
|
+
const { out } = filled()
|
|
122
|
+
expect((extractPairs(out, LEDGER, CLAIMS) as Pair[]).length).toBe(6)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("#given a Hangul quote #when extracted #then source_lang is ko", () => {
|
|
126
|
+
const html = "<html><body><h2>Findings</h2><p>HBM revenue reached {{F21:hbm_rev}} [S5].</p></body></html>"
|
|
127
|
+
const result = fill(html, LEDGER, { format: "html" }) as { out: string; fillMap: { fact_id: string }[] }
|
|
128
|
+
const [pair] = extractPairs(result.out, LEDGER, CLAIMS, result.fillMap) as Pair[]
|
|
129
|
+
expect(pair).toMatchObject({ fact_ids: ["F21"], source_lang: "ko", report_lang: "en" })
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe("extractPairs: a fill map over translations", () => {
|
|
134
|
+
test("#given a fill map that omits the translated fact #when extracted #then no translation pair is emitted", () => {
|
|
135
|
+
const { out, fillMap } = filled()
|
|
136
|
+
const kept = fillMap.filter((entry) => entry.fact_id !== "Q7")
|
|
137
|
+
const result = extractPairs(out, LEDGER, CLAIMS, kept) as Pair[]
|
|
138
|
+
expect(result.some((pair) => pair.kind === "translation")).toBe(false)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe("hasHedge: a month is not a hedge", () => {
|
|
143
|
+
test("#given a text that starts with the month May #when checked #then it does not count as hedged", () => {
|
|
144
|
+
expect(hasHedge("May 2024 sales rose.")).toBe(false)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test("#given a sentence-initial modal may #when checked #then it still counts as hedged", () => {
|
|
148
|
+
expect(hasHedge("May the figure hold through 2027.")).toBe(true)
|
|
149
|
+
expect(hasHedge("Demand may recover in 2027.")).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe("extractPairs: determinism", () => {
|
|
154
|
+
test("#given the same filled report twice #when extracted #then both results are byte-for-byte equal", () => {
|
|
155
|
+
const { out, fillMap } = filled()
|
|
156
|
+
const first = JSON.stringify(extractPairs(out, LEDGER, CLAIMS, fillMap))
|
|
157
|
+
const second = JSON.stringify(extractPairs(out, LEDGER, CLAIMS, fillMap))
|
|
158
|
+
expect(second).toBe(first)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("extractPairs: the no-model contract", () => {
|
|
163
|
+
test("#given the module source #when read #then it never fetches or imports a network or model client", () => {
|
|
164
|
+
const text = readFileSync(join(import.meta.dir, "fact-pairs.mjs"), "utf8")
|
|
165
|
+
expect(text).not.toMatch(/\bfetch\s*\(|node:https?|node:net|from\s+"(?!\.\/|node:)/u)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Iterative postorder verifier: one snapshot and one arithmetic evaluation per reachable node.
|
|
2
|
+
import { compute } from "./fact-derive.mjs"
|
|
3
|
+
import { parseExpr } from "./fact-expr.mjs"
|
|
4
|
+
import { isLegacyDerived, kindRuleErrors, readFlaws, readRow } from "./fact-ledger-kinds.mjs"
|
|
5
|
+
import { rowErrors } from "./fact-ledger-schema.mjs"
|
|
6
|
+
import { refuse } from "./unit-algebra.mjs"
|
|
7
|
+
import { refusal } from "./unit-errors.mjs"
|
|
8
|
+
import { object, sameDeclared } from "./unit-metadata.mjs"
|
|
9
|
+
import { expectedUnit, malformed, ruleOf, unitMismatch } from "./unit-rules.mjs"
|
|
10
|
+
|
|
11
|
+
const derived = (row) => row.kind === "derived" || row.kind === "converted"
|
|
12
|
+
const fail = (detail, shape, expected) => ({ ok: false, detail, shape, expected })
|
|
13
|
+
function refuseLegacy(row) {
|
|
14
|
+
if (isLegacyDerived(row)) refuse(`${row.fact_id} was written before precision, compatibility, and context existed; regenerate it with derive`, undefined, "legacy derived row", "a derived row regenerated by derive")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function verifyRow(row, rows) {
|
|
18
|
+
const rule = ruleOf(row.operation)
|
|
19
|
+
const ast = rule.expression ? parseExpr(row.formula) : null
|
|
20
|
+
const context = { ...row.context, rationale: row.rationale }
|
|
21
|
+
const unit = expectedUnit(row.operation, rows, { ast, context })
|
|
22
|
+
const expected = { unit, ...compute(row.operation, rows, unit, ast, context, row.formula), derived_from: rows.map((r) => r.fact_id) }
|
|
23
|
+
if (row.unit !== unit) return fail(`${row.fact_id}: ${unitMismatch(row.operation, rows, row.unit, unit)}`, "result_unit", expected)
|
|
24
|
+
for (const field of Object.keys(expected)) {
|
|
25
|
+
if (!sameDeclared(row[field], expected[field])) return fail(`${row.fact_id}: stored ${field} differs from the recomputed result`, `${field}_mismatch`, expected)
|
|
26
|
+
}
|
|
27
|
+
return { ok: true, expected }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function verifyGraph(ledger, roots, requireActive = false, override = null) {
|
|
31
|
+
try {
|
|
32
|
+
if (!object(ledger) || !Array.isArray(ledger.rows)) refuse("ledger rows must be an array")
|
|
33
|
+
const byId = new Map()
|
|
34
|
+
for (const r of ledger.rows) {
|
|
35
|
+
if (!object(r) || typeof r.fact_id !== "string" || byId.has(r.fact_id)) refuse("ledger rows need distinct fact ids", undefined, "identity", "distinct fact ids")
|
|
36
|
+
byId.set(r.fact_id, r)
|
|
37
|
+
}
|
|
38
|
+
if (override) byId.set(override.fact_id, override)
|
|
39
|
+
const states = new Map()
|
|
40
|
+
const snapshots = new Map()
|
|
41
|
+
const results = new Map()
|
|
42
|
+
const stack = roots.map((id) => ({ id, finish: false }))
|
|
43
|
+
while (stack.length) {
|
|
44
|
+
const { id, finish } = stack.pop()
|
|
45
|
+
if (states.get(id) === "done") continue
|
|
46
|
+
if (!finish) {
|
|
47
|
+
if (states.get(id) === "open") refuse(`${id}: operands form a cycle`, undefined, "cycle", "an acyclic dependency graph")
|
|
48
|
+
const original = byId.get(id)
|
|
49
|
+
if (!original) refuse(`no ledger row ${id}`, undefined, "missing_operand", "an existing fact id")
|
|
50
|
+
const row = { ...readRow(original) }
|
|
51
|
+
snapshots.set(id, row)
|
|
52
|
+
if (requireActive && row.status !== "active") refuse(`${id} is ${row.status}`, undefined, "inactive_dependency", "active dependencies at every depth")
|
|
53
|
+
const [flaw] = readFlaws(row)
|
|
54
|
+
if (flaw) refuse(`${id}: ${flaw.detail}`, undefined, flaw.shape, flaw.expected)
|
|
55
|
+
if (derived(row)) {
|
|
56
|
+
refuseLegacy(row)
|
|
57
|
+
const shape = malformed(row)
|
|
58
|
+
if (shape) refuse(`${id}: ${shape}`, undefined, "malformed_row", "the operation's registry row shape")
|
|
59
|
+
}
|
|
60
|
+
const errors = derived(row) ? kindRuleErrors(row, id) : rowErrors(row)
|
|
61
|
+
if (errors.length) refuse(errors.join("; "), undefined, "row_schema", "a canonical row matching its kind and operation")
|
|
62
|
+
if (row.exact === null || row.value === null) refuse(`${id} has no exact value to derive from`)
|
|
63
|
+
states.set(id, "open")
|
|
64
|
+
stack.push({ id, finish: true })
|
|
65
|
+
if (derived(row)) for (const dependency of row.operands) stack.push({ id: dependency, finish: false })
|
|
66
|
+
} else {
|
|
67
|
+
const row = snapshots.get(id)
|
|
68
|
+
if (derived(row)) {
|
|
69
|
+
const result = verifyRow(row, row.operands.map((ref) => snapshots.get(ref)))
|
|
70
|
+
if (!result.ok) return result
|
|
71
|
+
const errors = rowErrors(row)
|
|
72
|
+
if (errors.length) refuse(errors.join("; "), undefined, "row_schema", "a complete canonical row")
|
|
73
|
+
results.set(id, result)
|
|
74
|
+
}
|
|
75
|
+
states.set(id, "done")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return results.get(roots[0]) ?? { ok: true }
|
|
79
|
+
} catch (error) { return { ok: false, ...refusal(error) } }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function recompute(ledger, row) {
|
|
83
|
+
try {
|
|
84
|
+
refuseLegacy(row)
|
|
85
|
+
const shape = malformed(row)
|
|
86
|
+
if (shape) refuse(shape, undefined, "malformed_row", "a valid derived or converted row")
|
|
87
|
+
return verifyGraph(ledger, [row.fact_id], false, row)
|
|
88
|
+
} catch (error) { return { ok: false, ...refusal(error) } }
|
|
89
|
+
}
|