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,194 @@
|
|
|
1
|
+
// fact-fill.mjs - the fill step: substitute every fact pointer in a report source from the
|
|
2
|
+
// ledger, all or nothing. Fatal errors (pointer_label_mismatch, unresolved_placeholder,
|
|
3
|
+
// quote_anchor_unresolved, unit_duplicated) return out null; warnings (particle_unverified,
|
|
4
|
+
// fallback_without_entity) never block. A twice-failed sentence can be replaced by an attributed
|
|
5
|
+
// block quote of its source row (replaceFailed). No IO, but not pure: unless options.view is set,
|
|
6
|
+
// it records each replaced sentence in options.replaceFailed through markReplaced. node: builtins only.
|
|
7
|
+
|
|
8
|
+
import { finalReading, rewriteParticle } from "./particles-ko.mjs"
|
|
9
|
+
import { failureCount, markReplaced, sentenceKey } from "./fidelity-state.mjs"
|
|
10
|
+
import { findPlaceholders, lead, qualifiedText, renderBlockquote, renderQuote, renderValueHtml, subSpan, valueParts } from "./placeholder.mjs"
|
|
11
|
+
import { splitSentences } from "./sentence-split.mjs"
|
|
12
|
+
|
|
13
|
+
const FORMATS = Object.freeze(["html", "md"])
|
|
14
|
+
const TRANSLATION = Object.freeze({
|
|
15
|
+
html: /^\s*<span data-translates="(Q\d+)">[\s\S]*?<\/span>/u,
|
|
16
|
+
md: /^\s*<!-- translates: (Q\d+) -->[^\n]*/u,
|
|
17
|
+
})
|
|
18
|
+
const HEADING = Object.freeze({ html: /<h[1-6]\b([^>]*)>([\s\S]*?)<\/h[1-6]>/giu, md: /^#{1,6}[ \t]+(.+)$/gmu })
|
|
19
|
+
|
|
20
|
+
const lineOf = (text, at) => text.slice(0, at).split("\n").length
|
|
21
|
+
|
|
22
|
+
function resolveValue(row, parsed, format) {
|
|
23
|
+
const { valueText, unitText } = valueParts(row)
|
|
24
|
+
const text = { full: row.raw, value: valueText, unit: unitText, q: qualifiedText(row) }[parsed.mode]
|
|
25
|
+
if (text === null) return { code: "quote_anchor_unresolved", message: `${row.fact_id} has qualifier ${row.qualifier}, but its quote holds no qualifier beside "${row.raw}"` }
|
|
26
|
+
return { row, parsed, visible: text, rendered: format === "html" ? renderValueHtml(row, text) : text }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveQuote(row, parsed, format) {
|
|
30
|
+
const whole = { text: row.quote, start: 0, end: row.quote.length }
|
|
31
|
+
const selection = parsed.mode === "lead" ? lead(row.quote, parsed.lead) : parsed.mode === "span" ? subSpan(row.quote, parsed.from, parsed.to) : whole
|
|
32
|
+
if (selection === null) {
|
|
33
|
+
return { code: "quote_anchor_unresolved", message: `from "${parsed.from}" and to "${parsed.to}" must each occur exactly once in ${row.fact_id}'s quote, in that order` }
|
|
34
|
+
}
|
|
35
|
+
const rendered = renderQuote(row, parsed.selector, selection.text, format)
|
|
36
|
+
return { row, parsed, visible: `[${row.source_id}]`, rendered, selector: parsed.selector, quote_span: [selection.start, selection.end] }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolve(found, rows, format) {
|
|
40
|
+
const { parsed } = found
|
|
41
|
+
if (parsed.error) return { code: "unresolved_placeholder", message: parsed.error }
|
|
42
|
+
const row = rows.get(parsed.id)
|
|
43
|
+
if (row === undefined) return { code: "unresolved_placeholder", message: `${parsed.id} is not in the fact ledger` }
|
|
44
|
+
if (row.status !== "active") return { code: "unresolved_placeholder", message: `${parsed.id} is ${row.status}; cite its replacement` }
|
|
45
|
+
if (row.label !== parsed.label) return { code: "pointer_label_mismatch", message: `${parsed.id} has label "${row.label}", not "${parsed.label}"` }
|
|
46
|
+
return parsed.kind === "Q" ? resolveQuote(row, parsed, format) : resolveValue(row, parsed, format)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A unit typed right after a pointer that already renders it: never absorbed, always an error. */
|
|
50
|
+
function unitDuplicated(text, item) {
|
|
51
|
+
if (item.parsed.kind !== "F" || (item.parsed.mode !== "full" && item.parsed.mode !== "q")) return false
|
|
52
|
+
const suffix = item.row.raw.replace(/^[\s\S]*\d/u, "").trim()
|
|
53
|
+
const units = [item.row.unit, valueParts(item.row).unitText, suffix].filter((unit) => typeof unit === "string" && unit !== "")
|
|
54
|
+
const following = text.slice(item.end).replace(/^[ \t]+/u, "")
|
|
55
|
+
return units.some((unit) => following.startsWith(unit) && !/[A-Za-z0-9]/u.test(following[unit.length] ?? ""))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* Sentence-key rules (fidelity-state.mjs sentenceKey(slug, ids)); any other producer of keys must match:
|
|
60
|
+
* - slug source: an HTML heading's id attribute wins over its text; md headings use their text.
|
|
61
|
+
* - slugify: strip tags, lowercase, turn each run of non-letters/digits (\p{L}\p{N}) into "-",
|
|
62
|
+
* trim leading and trailing "-".
|
|
63
|
+
* - a sentence takes the nearest heading that starts strictly before the sentence's start offset;
|
|
64
|
+
* a sentence before the first heading gets slug "".
|
|
65
|
+
* - fact ids cited in the sentence are de-duplicated before sentenceKey sorts them.
|
|
66
|
+
*/
|
|
67
|
+
function slugify(text) {
|
|
68
|
+
return text
|
|
69
|
+
.replace(/<[^>]*>/gu, "")
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
|
72
|
+
.replace(/^-+|-+$/gu, "")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function headings(text, format) {
|
|
76
|
+
return [...text.matchAll(HEADING[format])].map((match) => {
|
|
77
|
+
const id = format === "html" ? /\bid="([^"]*)"/u.exec(match[1])?.[1] : undefined
|
|
78
|
+
return { at: match.index, slug: slugify(id ?? match[match.length - 1]) }
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Sentence offsets over the source with pointers, comments and (html) tags blanked to equal length. */
|
|
83
|
+
function sentencesOf(text, found, format) {
|
|
84
|
+
let masked = text
|
|
85
|
+
for (const item of found) masked = masked.slice(0, item.start) + "x".repeat(item.end - item.start) + masked.slice(item.end)
|
|
86
|
+
const blank = (part) => part.replace(/[^\n]/gu, " ")
|
|
87
|
+
masked = masked.replace(/<!--[\s\S]*?-->/gu, blank)
|
|
88
|
+
if (format === "html") masked = masked.replace(/<[^>]*>/gu, blank)
|
|
89
|
+
return splitSentences(masked)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function replacements(text, found, items, options, warnings) {
|
|
93
|
+
const { format, replaceFailed: state } = options
|
|
94
|
+
const marks = headings(text, format)
|
|
95
|
+
const out = []
|
|
96
|
+
for (const sentence of sentencesOf(text, found, format)) {
|
|
97
|
+
const inside = items.filter((item) => item.start >= sentence.start && item.end <= sentence.end)
|
|
98
|
+
if (inside.length === 0) continue
|
|
99
|
+
const slug = marks.filter((mark) => mark.at < sentence.start).at(-1)?.slug ?? ""
|
|
100
|
+
const key = sentenceKey(slug, [...new Set(inside.map((item) => item.row.fact_id))])
|
|
101
|
+
if (failureCount(state, key) < 2) continue
|
|
102
|
+
const rows = inside.map((item) => item.row)
|
|
103
|
+
const source = rows.find((row) => row.kind === "quote") ?? rows.find((row) => row.source_id !== null && row.quote !== "")
|
|
104
|
+
if (source === undefined) continue
|
|
105
|
+
let rendered = options.fallback === "omit" ? "" : renderBlockquote(source, key, format)
|
|
106
|
+
if (rendered !== "" && format === "md") {
|
|
107
|
+
if (sentence.start > 0 && text[sentence.start - 1] !== "\n") rendered = `\n\n${rendered}`
|
|
108
|
+
if (sentence.end < text.length && text[sentence.end] !== "\n") rendered = `${rendered}\n\n`
|
|
109
|
+
}
|
|
110
|
+
if (rendered !== "" && source.entity === null) {
|
|
111
|
+
warnings.push({ code: "fallback_without_entity", token: key, line: lineOf(text, sentence.start), message: `${source.fact_id} has no entity; the block quote cites [${source.source_id}] alone` })
|
|
112
|
+
}
|
|
113
|
+
out.push({ start: sentence.start, end: sentence.end, key, source, rendered })
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The text after a pointer: rewrite a following Korean particle, and map a translation span. */
|
|
119
|
+
function afterPlaceholder(segment, item, context) {
|
|
120
|
+
const { format, text, warnings, fillMap } = context
|
|
121
|
+
const probeEnd = format === "html" && segment.includes("<") ? segment.indexOf("<") : segment.length
|
|
122
|
+
const result = rewriteParticle(segment.slice(0, probeEnd), finalReading(item.visible, null))
|
|
123
|
+
let next = segment
|
|
124
|
+
if (result.applied) next = result.text + segment.slice(probeEnd)
|
|
125
|
+
else if (result.candidate) {
|
|
126
|
+
warnings.push({ code: "particle_unverified", token: item.token, line: lineOf(text, item.start), message: `the particle after ${item.token} was not rewritten; check it by eye` })
|
|
127
|
+
}
|
|
128
|
+
const translation = item.parsed.kind === "Q" ? TRANSLATION[format].exec(next) : null
|
|
129
|
+
if (translation !== null && translation[1] === item.row.fact_id) {
|
|
130
|
+
const span = translation[0].trimStart()
|
|
131
|
+
const start = context.out.length + translation[0].length - span.length
|
|
132
|
+
fillMap.push({ token: span, fact_id: translation[1], translates: translation[1], start, end: start + span.length, rendered: span })
|
|
133
|
+
}
|
|
134
|
+
return next
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function assemble(text, parts, context) {
|
|
138
|
+
let cursor = 0
|
|
139
|
+
let previous = null
|
|
140
|
+
const flush = (until) => {
|
|
141
|
+
const segment = text.slice(cursor, until)
|
|
142
|
+
context.out += previous?.parsed ? afterPlaceholder(segment, previous, context) : segment
|
|
143
|
+
}
|
|
144
|
+
for (const part of parts) {
|
|
145
|
+
flush(part.start)
|
|
146
|
+
const start = context.out.length
|
|
147
|
+
if (part.parsed) {
|
|
148
|
+
const extra = part.selector === undefined ? {} : { selector: part.selector, quote_span: part.quote_span }
|
|
149
|
+
context.fillMap.push({ token: part.token, fact_id: part.row.fact_id, start, end: start + part.rendered.length, rendered: part.rendered, ...extra })
|
|
150
|
+
} else if (part.rendered !== "") {
|
|
151
|
+
context.fillMap.push({ token: null, fact_id: part.source.fact_id, replaced: part.key, start, end: start + part.rendered.length, rendered: part.rendered })
|
|
152
|
+
}
|
|
153
|
+
context.out += part.rendered
|
|
154
|
+
cursor = part.end
|
|
155
|
+
previous = part
|
|
156
|
+
}
|
|
157
|
+
flush(text.length)
|
|
158
|
+
return context.out
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} sourceText the report source with {{F..}} / {{Q..}} pointers
|
|
163
|
+
* @param {{ rows: object[] }} ledger
|
|
164
|
+
* @param {{ format?: "html"|"md", replaceFailed?: object, fallback?: "quote"|"omit", view?: boolean }} [options]
|
|
165
|
+
* @returns {{ out: string|null, fillMap: object[], errors: object[], warnings: object[] }}
|
|
166
|
+
*/
|
|
167
|
+
export function fill(sourceText, ledger, options = {}) {
|
|
168
|
+
const format = options.format ?? "html"
|
|
169
|
+
if (!FORMATS.includes(format)) throw new TypeError(`format must be one of ${FORMATS.join(", ")}`)
|
|
170
|
+
const rows = new Map(ledger.rows.map((row) => [row.fact_id, row]))
|
|
171
|
+
const found = findPlaceholders(sourceText)
|
|
172
|
+
const errors = []
|
|
173
|
+
const warnings = []
|
|
174
|
+
const items = []
|
|
175
|
+
for (const pointer of found) {
|
|
176
|
+
const resolved = resolve(pointer, rows, format)
|
|
177
|
+
const line = lineOf(sourceText, pointer.start)
|
|
178
|
+
if (resolved.code) errors.push({ code: resolved.code, token: pointer.token, line, message: resolved.message })
|
|
179
|
+
else {
|
|
180
|
+
const item = { ...pointer, ...resolved }
|
|
181
|
+
if (unitDuplicated(sourceText, item)) {
|
|
182
|
+
errors.push({ code: "unit_duplicated", token: pointer.token, line, message: `${pointer.token} already renders its unit; use {{${item.row.fact_id}:${item.row.label}:value}}` })
|
|
183
|
+
} else items.push(item)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (errors.length > 0) return { out: null, fillMap: [], errors, warnings }
|
|
187
|
+
const replaced = options.replaceFailed ? replacements(sourceText, found, items, { ...options, format }, warnings) : []
|
|
188
|
+
const kept = items.filter((item) => !replaced.some((block) => item.start >= block.start && item.end <= block.end))
|
|
189
|
+
const parts = [...kept, ...replaced].sort((a, b) => a.start - b.start)
|
|
190
|
+
const context = { format, text: sourceText, warnings, fillMap: [], out: "" }
|
|
191
|
+
const out = assemble(sourceText, parts, context)
|
|
192
|
+
if (!options.view) for (const block of replaced) markReplaced(options.replaceFailed, block.key)
|
|
193
|
+
return { out, fillMap: context.fillMap, errors, warnings }
|
|
194
|
+
}
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
// Also the value primitives the schema shares. Pure functions with no IO; node: builtins only.
|
|
6
6
|
|
|
7
7
|
import { isCliEntry } from "./entry-guard.mjs"
|
|
8
|
+
import { canonicalUnit, inputScale, RuleError } from "./unit-algebra.mjs"
|
|
9
|
+
import { metadataErrors } from "./unit-metadata.mjs"
|
|
10
|
+
import { mul } from "./rational.mjs"
|
|
11
|
+
import { placesOf, roundPlaces, toBig } from "./unit-precision.mjs"
|
|
12
|
+
import { OPERATION_RULES } from "./unit-rules.mjs"
|
|
8
13
|
|
|
9
14
|
export const DECIMAL = /^-?\d+(?:\.\d+)?$/
|
|
10
15
|
const INTEGER = /^-?\d+$/
|
|
@@ -16,7 +21,15 @@ export const isString = (value) => typeof value === "string"
|
|
|
16
21
|
export const isNonEmpty = (value) => typeof value === "string" && value.length > 0
|
|
17
22
|
export const isCount = (value) => Number.isInteger(value) && value >= 0
|
|
18
23
|
const isDecimal = (value) => isString(value) && DECIMAL.test(value)
|
|
19
|
-
|
|
24
|
+
export function isRational(exact) {
|
|
25
|
+
if (!isObject(exact) || Object.keys(exact).sort().join(",") !== "den,num" || !isString(exact.num) || !isString(exact.den) || !INTEGER.test(exact.num) || !/^[1-9]\d*$/.test(exact.den)) return false
|
|
26
|
+
let a = BigInt(exact.num)
|
|
27
|
+
let b = BigInt(exact.den)
|
|
28
|
+
if (String(a) !== exact.num) return false
|
|
29
|
+
if (a < 0n) a = -a
|
|
30
|
+
while (b !== 0n) [a, b] = [b, a % b]
|
|
31
|
+
return a === 1n
|
|
32
|
+
}
|
|
20
33
|
|
|
21
34
|
/** The exact rational of a decimal string, reduced. */
|
|
22
35
|
export function exactFromDecimal(value) {
|
|
@@ -72,22 +85,27 @@ export const RULES = Object.freeze({
|
|
|
72
85
|
test: (v, row) => isRational(v) && (!isDecimal(row.value) || (v.num === exactFromDecimal(row.value).num && v.den === exactFromDecimal(row.value).den)),
|
|
73
86
|
},
|
|
74
87
|
rational_or_null: { cell: "rational or null", says: "null or { num: integer string, den: positive integer string }", test: orNull(isRational) },
|
|
75
|
-
by_derive: { cell: "
|
|
88
|
+
by_derive: { cell: "reduced rational", says: "a reduced { num: integer string, den: positive integer string }", test: isRational },
|
|
89
|
+
context_by_derive: { cell: "written by derive (object or null)", says: "null or an object", test: orNull(isObject) },
|
|
90
|
+
derived_operation: { cell: "sum/diff/growth/share/ratio/expr", says: "a derived operation", test: (v) => Object.hasOwn(OPERATION_RULES, v) && OPERATION_RULES[v].kind === "derived" },
|
|
91
|
+
convert_operation: { cell: "`convert`", says: "convert", test: (v) => Object.hasOwn(OPERATION_RULES, v) && OPERATION_RULES[v].kind === "converted" },
|
|
92
|
+
precision: { cell: "`{ places, sigfigs }`", says: "nonnegative places and positive sigfigs", test: (v) => isObject(v) && Object.keys(v).sort().join(",") === "places,sigfigs" && Number.isSafeInteger(v.places) && v.places >= 0 && Number.isSafeInteger(v.sigfigs) && v.sigfigs >= 1 },
|
|
93
|
+
compatibility: { cell: "checked or unchecked (expr)", says: "the operation's compatibility", test: (v, r) => Object.hasOwn(OPERATION_RULES, r.operation) && v === OPERATION_RULES[r.operation].compatibility },
|
|
76
94
|
})
|
|
77
95
|
|
|
78
96
|
/** The grammar's per-kind columns, in table order; `exact` follows `value` because it reads it. */
|
|
79
|
-
export const KIND_COLUMNS = Object.freeze(["fact_id", "value", "source_id", "locator", "snapshot_sha256", "rationale", "exact"])
|
|
97
|
+
export const KIND_COLUMNS = Object.freeze(["fact_id", "value", "source_id", "locator", "snapshot_sha256", "rationale", "exact", "context", "operation", "precision", "compatibility"])
|
|
80
98
|
|
|
81
99
|
const sourced = { source_id: "source", locator: "locator", snapshot_sha256: "sha256" }
|
|
82
100
|
const unsourced = { source_id: "source_or_null", locator: "locator_or_null", snapshot_sha256: "sha256_or_null" }
|
|
83
|
-
const row = (cells) => Object.freeze(Object.fromEntries(KIND_COLUMNS.map((column) => [column, cells[column]])))
|
|
101
|
+
const row = (cells) => Object.freeze(Object.fromEntries(KIND_COLUMNS.map((column) => [column, cells[column] ?? (["precision", "compatibility"].includes(column) && cells.operation !== "null" ? column : "null")])))
|
|
84
102
|
|
|
85
103
|
export const KIND_RULES = Object.freeze({
|
|
86
|
-
measured: row({ fact_id: "f_id", value: "decimal", ...sourced, rationale: "string_or_null", exact: "from_value" }),
|
|
87
|
-
quote: row({ fact_id: "q_id", value: "null", ...sourced, rationale: "string_or_null", exact: "null" }),
|
|
88
|
-
derived: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "string_or_null", exact: "by_derive" }),
|
|
89
|
-
assumed: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "non_empty", exact: "rational_or_null" }),
|
|
90
|
-
converted: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "string_or_null", exact: "by_derive" }),
|
|
104
|
+
measured: row({ fact_id: "f_id", value: "decimal", ...sourced, rationale: "string_or_null", exact: "from_value", context: "null", operation: "null" }),
|
|
105
|
+
quote: row({ fact_id: "q_id", value: "null", ...sourced, rationale: "string_or_null", exact: "null", context: "null", operation: "null" }),
|
|
106
|
+
derived: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "string_or_null", exact: "by_derive", context: "context_by_derive", operation: "derived_operation" }),
|
|
107
|
+
assumed: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "non_empty", exact: "rational_or_null", context: "null", operation: "null" }),
|
|
108
|
+
converted: row({ fact_id: "f_id", value: "decimal", ...unsourced, rationale: "string_or_null", exact: "by_derive", context: "context_by_derive", operation: "convert_operation" }),
|
|
91
109
|
})
|
|
92
110
|
|
|
93
111
|
/** The id prefix nextId assigns for a kind; an unknown kind gets F and fails validation later. */
|
|
@@ -100,15 +118,101 @@ export function exactComesFromValue(kind) {
|
|
|
100
118
|
return KIND_RULES[kind]?.exact === "from_value"
|
|
101
119
|
}
|
|
102
120
|
|
|
121
|
+
/** Columns added after schema version 1 shipped; a ledger written before them still loads (dimension 31). */
|
|
122
|
+
export const LATER_COLUMNS = Object.freeze(["context", "precision", "compatibility"])
|
|
123
|
+
const DERIVED_KINDS = Object.freeze(["derived", "converted"])
|
|
124
|
+
const PRE_REGISTRY_COLUMNS = KIND_COLUMNS.slice(0, KIND_COLUMNS.indexOf("exact") + 1)
|
|
125
|
+
|
|
126
|
+
/** A derived or converted row written before the later columns existed: loadable, never an operand. */
|
|
127
|
+
export const isLegacyDerived = (row) => isObject(row) && DERIVED_KINDS.includes(row.kind) && LATER_COLUMNS.every((column) => !Object.hasOwn(row, column))
|
|
128
|
+
/** A derived or converted row carrying this version's columns: the only rows checked strictly at read time. */
|
|
129
|
+
export const isCurrentDerived = (row) => isObject(row) && DERIVED_KINDS.includes(row.kind) && !isLegacyDerived(row)
|
|
130
|
+
|
|
131
|
+
/** An exact carrying keys beyond num and den: never normalized, never silently stripped, flagged on use. */
|
|
132
|
+
export const hasUnknownExactKeys = (exact) => isObject(exact) && isString(exact.num) && isString(exact.den) && Object.keys(exact).sort().join(",") !== "den,num"
|
|
133
|
+
/** Any integer pair dev's writer accepted, reduced: sign on num, no leading zeros, gcd 1; anything else as given. */
|
|
134
|
+
function readExact(exact) {
|
|
135
|
+
if (!isObject(exact) || hasUnknownExactKeys(exact) || !isString(exact.num) || !isString(exact.den) || !INTEGER.test(exact.num) || !/^\d+$/.test(exact.den) || /^0+$/.test(exact.den)) return exact
|
|
136
|
+
const { num, den } = mul({ num: BigInt(exact.num), den: BigInt(exact.den) }, 1n)
|
|
137
|
+
return { num: String(num), den: String(den) }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const readDecimal = (value) => (isDecimal(value) ? value.replace(/^(-?)0+(?=\d)/, "$1") : value)
|
|
141
|
+
|
|
142
|
+
/** A source unit read through the alias table; unknown, scaled, or unparseable text stays as given. */
|
|
143
|
+
export function sourceUnit(unit) {
|
|
144
|
+
try {
|
|
145
|
+
const canonical = canonicalUnit(unit)
|
|
146
|
+
return isString(canonical) && !inputScale(unit) ? canonical : unit
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof RuleError) return unit
|
|
149
|
+
throw error
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A row as every reader sees it (dimension 31 wins at read time): rows not written by derive in this
|
|
155
|
+
* version get reduced exacts and decimals; source rows also get canonical units and null later columns.
|
|
156
|
+
* Current derived rows stay as stored so tampering is still detected. Idempotent.
|
|
157
|
+
*/
|
|
158
|
+
export function readRow(row) {
|
|
159
|
+
if (!isObject(row) || !Object.hasOwn(KIND_RULES, row.kind) || isCurrentDerived(row)) return row
|
|
160
|
+
const read = { ...row, value: readDecimal(row.value), exact: exactComesFromValue(row.kind) ? row.exact : readExact(row.exact) }
|
|
161
|
+
if (DERIVED_KINDS.includes(row.kind)) return read
|
|
162
|
+
read.unit = sourceUnit(row.unit)
|
|
163
|
+
for (const column of LATER_COLUMNS) if (!Object.hasOwn(read, column)) read[column] = null
|
|
164
|
+
return read
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const carriesDerivation = (row) => row.operation !== null || row.operands?.length || row.derived_from?.length || row.formula !== null || row.factor !== null
|
|
168
|
+
|
|
169
|
+
/** Dimension 32: a stored exact must equal the row's own value at the value's decimal places. */
|
|
170
|
+
function valueExactDisagreement(row) {
|
|
171
|
+
const exact = readExact(row.exact)
|
|
172
|
+
const value = readDecimal(row.value)
|
|
173
|
+
if (!isRational(exact) || !isDecimal(value) || roundPlaces(toBig(exact), placesOf(value)) === value) return null
|
|
174
|
+
return `value ${value} disagrees with exact ${exact.num}/${exact.den} at ${placesOf(value)} decimal places`
|
|
175
|
+
}
|
|
176
|
+
function unitFlaw(unit) {
|
|
177
|
+
try {
|
|
178
|
+
return isString(unit) && (canonicalUnit(unit) !== unit || inputScale(unit)) ? `unit ${unit} has no canonical stored text` : null
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error instanceof RuleError) return `unit ${unit}: ${error.message}`
|
|
181
|
+
throw error
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* What a loadable source row (read through readRow) still breaks under the strict dimensions: dev's
|
|
187
|
+
* writer accepted it, so it loads, but derive and recompute refuse it as an operand with this shape.
|
|
188
|
+
* addRow refuses a new row that has any. Empty for derived and converted rows.
|
|
189
|
+
*/
|
|
190
|
+
export function readFlaws(row) {
|
|
191
|
+
if (!isObject(row) || DERIVED_KINDS.includes(row.kind)) return []
|
|
192
|
+
const flaws = []
|
|
193
|
+
if (carriesDerivation(row)) flaws.push({ shape: "dimension 1", detail: "a source row carries an operation or derivation fields", expected: "null operation and empty derivation fields on measured, quote, and assumed rows" })
|
|
194
|
+
const unit = unitFlaw(row.unit)
|
|
195
|
+
if (unit) flaws.push({ shape: "dimension 6", detail: unit, expected: "a registered or canonical unit without scale" })
|
|
196
|
+
for (const detail of metadataErrors(row)) flaws.push({ shape: "dimension 18", detail, expected: "present, type-valid metadata; explicit null when unknown" })
|
|
197
|
+
const unknownKeys = hasUnknownExactKeys(row.exact)
|
|
198
|
+
const disagreement = unknownKeys ? null : valueExactDisagreement(row)
|
|
199
|
+
if (unknownKeys) flaws.push({ shape: "dimension 7", detail: `exact has unknown keys: ${Object.keys(row.exact).filter((key) => key !== "num" && key !== "den").join(", ")}`, expected: "an exact with exactly num and den (the stored row is kept as written)" })
|
|
200
|
+
if (disagreement) flaws.push({ shape: "dimension 32", detail: disagreement, expected: "an exact equal to value at the value's decimal places" })
|
|
201
|
+
return flaws
|
|
202
|
+
}
|
|
203
|
+
|
|
103
204
|
/** @returns {string[]} every per-kind column error of one row; empty for an unknown kind. */
|
|
104
205
|
export function kindRuleErrors(candidate, at) {
|
|
105
206
|
const rules = KIND_RULES[candidate.kind]
|
|
106
207
|
if (rules === undefined) return []
|
|
107
208
|
const errors = []
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
209
|
+
const legacy = isLegacyDerived(candidate)
|
|
210
|
+
const columns = legacy ? PRE_REGISTRY_COLUMNS : isCurrentDerived(candidate) ? KIND_COLUMNS : KIND_COLUMNS.filter((column) => column !== "operation")
|
|
211
|
+
for (const column of columns) {
|
|
212
|
+
const value = candidate[column] === undefined && LATER_COLUMNS.includes(column) ? null : candidate[column]
|
|
213
|
+
const rule = RULES[legacy && column === "exact" ? "rational_or_null" : rules[column]]
|
|
214
|
+
// An exact with unknown keys loads as written (never stripped); readFlaws refuses it at use and at write.
|
|
215
|
+
if (column === "exact" && hasUnknownExactKeys(value) && !isCurrentDerived(candidate)) continue
|
|
112
216
|
if (rule.test(value, candidate)) continue
|
|
113
217
|
errors.push(`${at}.${column} must be ${rule.says} for a ${candidate.kind} row`)
|
|
114
218
|
if (rule.detail) errors.push(...rule.detail(value, `${at}.${column}`))
|
|
@@ -116,6 +220,16 @@ export function kindRuleErrors(candidate, at) {
|
|
|
116
220
|
return errors
|
|
117
221
|
}
|
|
118
222
|
|
|
223
|
+
/** At-rest rules of current derived rows; source rows answer to readFlaws at use and at write instead. */
|
|
224
|
+
export function storedErrors(row) {
|
|
225
|
+
if (!isCurrentDerived(row)) return []
|
|
226
|
+
const errors = metadataErrors(row)
|
|
227
|
+
try {
|
|
228
|
+
if (canonicalUnit(row.unit) !== row.unit || inputScale(row.unit)) errors.push("unit must be canonical stored text")
|
|
229
|
+
} catch (error) { errors.push(error.message) }
|
|
230
|
+
return errors
|
|
231
|
+
}
|
|
232
|
+
|
|
119
233
|
export const KIND_TABLE_BEGIN = "<!-- kind-rules:begin (rendered from KIND_RULES; do not edit by hand) -->"
|
|
120
234
|
export const KIND_TABLE_END = "<!-- kind-rules:end -->"
|
|
121
235
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// KIND_RULES (fact-ledger-kinds.mjs), never here.
|
|
5
5
|
// Pure functions with no IO; fact-ledger.mjs owns reading, writing, and row changes. node: builtins only.
|
|
6
6
|
|
|
7
|
-
import { DECIMAL, isCount, isNonEmpty, isObject, isString, kindRuleErrors } from "./fact-ledger-kinds.mjs"
|
|
7
|
+
import { DECIMAL, isCount, isCurrentDerived, isNonEmpty, isObject, isString, kindRuleErrors, LATER_COLUMNS, readRow, storedErrors } from "./fact-ledger-kinds.mjs"
|
|
8
|
+
import { malformed } from "./unit-rules.mjs"
|
|
8
9
|
|
|
9
10
|
export { DECIMAL, isNonEmpty, isString } from "./fact-ledger-kinds.mjs"
|
|
10
11
|
|
|
@@ -50,8 +51,13 @@ export const ROW_DEFAULTS = Object.freeze({
|
|
|
50
51
|
formula: null,
|
|
51
52
|
factor: null,
|
|
52
53
|
rationale: null,
|
|
54
|
+
context: null,
|
|
55
|
+
precision: null,
|
|
56
|
+
compatibility: null,
|
|
53
57
|
status: "active",
|
|
54
58
|
})
|
|
59
|
+
/** Fields added after schema version 1 shipped: written on every new row, optional when reading an older ledger. */
|
|
60
|
+
const OPTIONAL_FIELDS = new Set(LATER_COLUMNS)
|
|
55
61
|
const REQUIRED = ["fact_id", "kind", "raw", "quote", "extracted_by", "observed_at", "label"]
|
|
56
62
|
export const ROW_FIELDS = Object.freeze([...REQUIRED, ...Object.keys(ROW_DEFAULTS)])
|
|
57
63
|
|
|
@@ -70,10 +76,16 @@ function nullOr(value, test) {
|
|
|
70
76
|
function validateRow(row, index, errors) {
|
|
71
77
|
const at = `rows[${index}]`
|
|
72
78
|
if (!isObject(row)) return errors.push(`${at} must be an object`)
|
|
73
|
-
for (const key of ROW_FIELDS) check(errors, Object.hasOwn(row, key), `${at}.${key} is required`)
|
|
79
|
+
for (const key of ROW_FIELDS) check(errors, OPTIONAL_FIELDS.has(key) || Object.hasOwn(row, key), `${at}.${key} is required`)
|
|
74
80
|
for (const key of Object.keys(row)) check(errors, ROW_FIELDS.includes(key), `${at}.${key} is not a ledger field`)
|
|
75
81
|
check(errors, FACT_KINDS.includes(row.kind), `${at}.kind must be one of ${FACT_KINDS.join(", ")}`)
|
|
76
|
-
|
|
82
|
+
// Dimension 31 wins at read time: per-kind rules see the row as readRow normalizes it.
|
|
83
|
+
errors.push(...kindRuleErrors(readRow(row), at))
|
|
84
|
+
errors.push(...storedErrors(row).map((e) => `${at}.${e}`))
|
|
85
|
+
if (isCurrentDerived(row)) {
|
|
86
|
+
const problem = malformed(row)
|
|
87
|
+
if (problem) errors.push(`${at}: ${problem}`)
|
|
88
|
+
}
|
|
77
89
|
check(errors, row.unit === undefined || nullOr(row.unit, isNonEmpty), `${at}.unit must be null or a non-empty token`)
|
|
78
90
|
for (const key of ["raw", "quote"]) check(errors, row[key] === undefined || isString(row[key]), `${at}.${key} must be a string`)
|
|
79
91
|
check(errors, row.extracted_by === undefined || isNonEmpty(row.extracted_by), `${at}.extracted_by must be a non-empty string`)
|
|
@@ -100,6 +112,12 @@ function validateRow(row, index, errors) {
|
|
|
100
112
|
check(errors, row.status === undefined || FACT_STATUSES.includes(row.status), `${at}.status must be one of ${FACT_STATUSES.join(", ")}`)
|
|
101
113
|
}
|
|
102
114
|
|
|
115
|
+
export function rowErrors(row) {
|
|
116
|
+
const errors = []
|
|
117
|
+
validateRow(row, 0, errors)
|
|
118
|
+
return errors
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
// The status machine and event rule of references/fact-ledger-grammar.md. validateLedger replays
|
|
104
122
|
// events through transitionError, and every writer in fact-ledger.mjs calls it before appending.
|
|
105
123
|
export const TRANSITIONS = Object.freeze({ active: Object.freeze(["retracted", "superseded"]), retracted: Object.freeze([]), superseded: Object.freeze([]) })
|
|
@@ -163,20 +181,19 @@ export function provenanceErrors(rows) {
|
|
|
163
181
|
}
|
|
164
182
|
})
|
|
165
183
|
const state = new Map()
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
184
|
+
for (const root of refs.keys()) {
|
|
185
|
+
const stack = [{ id: root, finish: false }]
|
|
186
|
+
const path = []
|
|
187
|
+
while (stack.length) {
|
|
188
|
+
const { id, finish } = stack.pop()
|
|
189
|
+
if (state.get(id) === "done") continue
|
|
190
|
+
if (finish) { state.set(id, "done"); path.pop(); continue }
|
|
191
|
+
if (state.get(id) === "open") return [...errors, `${id} derives from itself through cycle ${[...path, id].join(" -> ")}`]
|
|
192
|
+
state.set(id, "open")
|
|
193
|
+
path.push(id)
|
|
194
|
+
stack.push({ id, finish: true })
|
|
195
|
+
for (const next of refs.get(id)) if (next !== id && refs.has(next)) stack.push({ id: next, finish: false })
|
|
173
196
|
}
|
|
174
|
-
state.set(id, "done")
|
|
175
|
-
return null
|
|
176
|
-
}
|
|
177
|
-
for (const id of refs.keys()) {
|
|
178
|
-
const cycle = visit(id, [])
|
|
179
|
-
if (cycle) return [...errors, `${cycle[0]} derives from itself through ${cycle.join(" -> ")}`]
|
|
180
197
|
}
|
|
181
198
|
return errors
|
|
182
199
|
}
|
|
@@ -7,8 +7,9 @@ import { existsSync } from "node:fs"
|
|
|
7
7
|
import { join } from "node:path"
|
|
8
8
|
|
|
9
9
|
import { CliError, readJsonFile, writeJsonAtomic } from "./cli-support.mjs"
|
|
10
|
-
import { DECIMAL, exactComesFromValue, exactFromDecimal, idPrefix, isString } from "./fact-ledger-kinds.mjs"
|
|
10
|
+
import { DECIMAL, exactComesFromValue, exactFromDecimal, idPrefix, isString, readFlaws, readRow } from "./fact-ledger-kinds.mjs"
|
|
11
11
|
import { LEDGER_SCHEMA_VERSION, ROW_DEFAULTS, ROW_FIELDS, transitionError, validateLedger } from "./fact-ledger-schema.mjs"
|
|
12
|
+
import { normalizeInput } from "./unit-algebra.mjs"
|
|
12
13
|
|
|
13
14
|
export { exactFromDecimal } from "./fact-ledger-kinds.mjs"
|
|
14
15
|
export { FACT_KINDS, FACT_STATUSES, LEDGER_SCHEMA_VERSION, OPERATIONS, QUALIFIERS, ROW_FIELDS, TRANSITIONS, transitionError, validateLedger } from "./fact-ledger-schema.mjs"
|
|
@@ -24,11 +25,24 @@ function assertValid(ledger) {
|
|
|
24
25
|
if (errors.length > 0) throw new CliError(`invalid fact ledger: ${errors.join("; ")}`)
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/** The row's fields in ROW_FIELDS order, then any unknown field (which validation reports). */
|
|
29
|
+
function ordered(row) {
|
|
30
|
+
const out = Object.fromEntries(ROW_FIELDS.filter((key) => Object.hasOwn(row, key)).map((key) => [key, row[key]]))
|
|
31
|
+
for (const key of Object.keys(row)) if (!ROW_FIELDS.includes(key)) out[key] = row[key]
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Every row comes back as readRow normalizes it (reduced exacts and decimals; source rows with canonical
|
|
37
|
+
* units and null later columns) before any strict rule runs; the file changes on the next write.
|
|
38
|
+
*/
|
|
27
39
|
export function readLedger(dir) {
|
|
28
40
|
const file = join(dir, LEDGER_FILENAME)
|
|
29
41
|
if (!existsSync(file)) return emptyLedger()
|
|
30
42
|
const ledger = readJsonFile(file)
|
|
31
43
|
assertValid(ledger)
|
|
44
|
+
ledger.rows = ledger.rows.map((row) => ordered(readRow(row)))
|
|
45
|
+
assertValid(ledger)
|
|
32
46
|
return ledger
|
|
33
47
|
}
|
|
34
48
|
|
|
@@ -44,12 +58,12 @@ const ID_NUMBER = Object.freeze({ F: /^F(\d+)$/, Q: /^Q(\d+)$/ })
|
|
|
44
58
|
/** The next id with the kind's prefix from KIND_RULES (F<n> for value rows, Q<n> for quotes). */
|
|
45
59
|
export function nextId(ledger, kind) {
|
|
46
60
|
const prefix = idPrefix(kind)
|
|
47
|
-
let max =
|
|
61
|
+
let max = 0n
|
|
48
62
|
for (const row of ledger.rows) {
|
|
49
63
|
const match = ID_NUMBER[prefix].exec(row.fact_id ?? "")
|
|
50
|
-
if (match) max =
|
|
64
|
+
if (match && BigInt(match[1]) > max) max = BigInt(match[1])
|
|
51
65
|
}
|
|
52
|
-
return `${prefix}${max +
|
|
66
|
+
return `${prefix}${max + 1n}`
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
/**
|
|
@@ -60,14 +74,20 @@ export function nextId(ledger, kind) {
|
|
|
60
74
|
*/
|
|
61
75
|
export function addRow(ledger, row) {
|
|
62
76
|
const filled = { ...structuredClone(ROW_DEFAULTS), ...row, fact_id: row.fact_id ?? nextId(ledger, row.kind) }
|
|
77
|
+
if (!["derived", "converted"].includes(filled.kind)) {
|
|
78
|
+
const normalized = normalizeInput(filled.unit, filled.value)
|
|
79
|
+
if (normalized.value !== filled.value && filled.exact !== null) throw new CliError("scaled input must omit exact; it is computed after normalization")
|
|
80
|
+
Object.assign(filled, readRow({ ...filled, ...normalized }))
|
|
81
|
+
}
|
|
63
82
|
if (exactComesFromValue(filled.kind) && filled.exact === null && isString(filled.value) && DECIMAL.test(filled.value)) {
|
|
64
83
|
filled.exact = exactFromDecimal(filled.value)
|
|
65
84
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
ledger.rows
|
|
70
|
-
|
|
85
|
+
const added = ordered(filled)
|
|
86
|
+
const flaws = readFlaws(added)
|
|
87
|
+
if (flaws.length > 0) throw new CliError(`invalid fact ledger: ${flaws.map((flaw) => `${flaw.shape}: ${flaw.detail}`).join("; ")}`)
|
|
88
|
+
assertValid({ ...ledger, rows: [...ledger.rows, added] })
|
|
89
|
+
ledger.rows.push(added)
|
|
90
|
+
return added
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
/** Build the event from the row's current status and append it only if transitionError allows it. */
|