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,157 @@
|
|
|
1
|
+
# Unit rules for derived facts
|
|
2
|
+
|
|
3
|
+
The one definition of which unit a derived or converted fact row carries, and the reviewer's checklist for
|
|
4
|
+
`scripts/unit-rules.mjs`. ONE registry (`OPERATION_RULES`) owns, per operation: arity, operand-unit guards, context
|
|
5
|
+
fields, result-unit inference, and malformed shapes. `derive()` (`fact-derive.mjs`) and `recompute()`
|
|
6
|
+
(`fact-recompute.mjs`) both call it; neither has op-specific unit code. `row.unit` is NEVER used to choose arithmetic;
|
|
7
|
+
numeric guards may read `exact` values, unit selection may not. The property tests in
|
|
8
|
+
`scripts/fact-unit-rules.test.ts` iterate the registry, so a new operation needs a table row here and cases there.
|
|
9
|
+
|
|
10
|
+
## Units
|
|
11
|
+
- Canonical unit text is stored and compared exactly (case-sensitive symbols). Registered aliases
|
|
12
|
+
(`pp` -> `%p`, `kilometre` -> `km`, ...) are canonicalized ONLY at input (derive/fact add); a relabel to an alias
|
|
13
|
+
at rest is a relabel and is detected. Unit text with no alias is stored as given: whitespace is trimmed only around
|
|
14
|
+
`*`, `/`, `^`, and parentheses, so `million people`, `metric ton`, and `USD per person` keep their inner spaces.
|
|
15
|
+
- A scale word (`million`, `M`, `bn`, `billion`, `thousand`, `e6`, ...; the `scale` rows of `units.mjs`) is a
|
|
16
|
+
multiplier, not a unit, and denomination scaling is out of scope, so it never becomes a unit atom: `million`,
|
|
17
|
+
`USD/M`, and `million*person` are refused at input (derive, and every operand read). Either store the scaled numeral
|
|
18
|
+
in the counted unit (5 million people as `5000000 person`) or keep the whole phrase as plain unit text:
|
|
19
|
+
`million people` has no table alias as a whole, so it is stored as given and sums only with `million people`, never
|
|
20
|
+
with `person`.
|
|
21
|
+
- `null` = dimensionless. Omitted unit on a derived spec is an error, not null (distinguish omission from declared null).
|
|
22
|
+
- Percent `%` stores percentage numerals (20% stores 20). Delta `%p` (percentage points). 1 basis point = 0.01 %p
|
|
23
|
+
(input alias `bp` -> `%p` with /100 scaling).
|
|
24
|
+
- Composite units are canonical algebraic strings built by the registry (`USD/kg`, `person`), never by concatenating
|
|
25
|
+
operand strings: equal factors cancel (`USD/(USD/person)` -> `person`).
|
|
26
|
+
- Affine scales (temperature: `C`, `F`) are flagged in the unit table; sum/diff/growth/share/ratio and every expr
|
|
27
|
+
node refuse them (temperatures are not summed or differenced as absolute values); only convert takes them.
|
|
28
|
+
Affine atoms in composites are refused before cancellation. Exponents are exact signed bigint integers, including zero.
|
|
29
|
+
|
|
30
|
+
## Operation table
|
|
31
|
+
|
|
32
|
+
| op | arity | operand guards | context fields (persisted on the row) | result unit | malformed -> recompute ok:false |
|
|
33
|
+
|---|---|---|---|---|---|
|
|
34
|
+
| sum | n>=2 | non-affine units, all equal after canonicalization; if `%`: `disjoint: true` AND `reference` (one denominator id/text) equal on every operand AND rationale | disjoint, reference, rationale | the common unit (`%` stays `%`) | <2 operands; missing id; no context; unequal/affine units; `%` without disjoint, reference, or rationale |
|
|
35
|
+
| diff | 2 | non-affine units; typed pairs only: U-U -> U (same non-% unit); %-% -> `%p`; %-%p -> `%`; %p-%p -> `%p`; %p-% REFUSED | none | per the pair rule | arity; invalid pair (`%p - %`, `m - person`); affine units; stray context |
|
|
36
|
+
| growth | 2 (from,to) | equal non-affine units; same entity; two different valid_at; from.exact > 0 | none | `%` (= 100(to-from)/from) | arity; unequal/affine units; entity/valid_at; from<=0; stray context |
|
|
37
|
+
| share | 2 (part,whole) | equal non-affine units; whole.exact > 0; 0 <= part <= whole | none | `%` (= 100 part/whole) | arity; units; whole<=0; part outside [0,whole]; stray context |
|
|
38
|
+
| ratio | 2 (num,den) | non-affine units; den.exact != 0; unequal `%`/`%p` REFUSED | none | registry composite: equal units -> null; else canonical `U(num)/U(den)` with cancellation | arity; den=0; stray context |
|
|
39
|
+
| convert | 1 | U(a) has a registered factor to `target`; currencies REFUSED (use expr with a dated rate row) | target (persisted as canonical text; the ONLY accepted declared unit; `row.unit` is compared to it exactly, never canonicalized at rest) | the persisted target | arity; no context; target missing; target not canonical (an alias at rest); currency target; no factor; declared unit != persisted target |
|
|
40
|
+
| expr | n>=1 | formula ids == operand set; node rules: `+`/`-` follow the sum/diff typed-pair rules, affine refusal included (so `20% - 10%` is `%p`, never `%`, and `25 C + 30 C` is refused); `+` also takes `%` + `%p` in either order -> `%` (as diff's `%-%p`), while `%` + `%` is refused (use sum); `*` and `/` compose canonical composites, `%`/`%p` operands normalize by /100 to dimensionless first; result must be fully inferred: NO rationale escape | none | the inferred unit; an explicit null->`%` conversion is a separate `convert`-like node (`pct()`) | formula/operand id mismatch; parse error; /0; uninferable unit; affine operands; stray context |
|
|
41
|
+
|
|
42
|
+
Every row's malformed column also holds the two operand-identity shapes below (`repeated operand`, `self reference`).
|
|
43
|
+
|
|
44
|
+
## Operand identity (applies to every row)
|
|
45
|
+
- Operand ids are DISTINCT within one derived row. sum of F1, F1 is not a sum of two shares, and diff, ratio, share,
|
|
46
|
+
growth, or convert of an operand with itself is meaningless and would dodge every pair guard. A repeated id is
|
|
47
|
+
refused by `derive()` (`operand F1 is repeated`) and is `ok:false` at `recompute()` (malformed shape
|
|
48
|
+
`repeated operand`). The check sits in the registry (`expectedUnit` and `malformed`), so no operation can skip it.
|
|
49
|
+
- expr may reference the same id in several leaves (`F1 * F1` is a legitimate square). Its stored operand list is the
|
|
50
|
+
formula's id SET, which must equal the formula ids (`formula id mismatch`); an id listed twice in that list is the
|
|
51
|
+
`repeated operand` shape too.
|
|
52
|
+
- A derived row may not cite itself, directly (`self reference`: its own id among its operands) or through nested
|
|
53
|
+
derived operands (a cycle). `recompute()` refuses both. `derive()` recomputes every derived operand first and
|
|
54
|
+
refuses one that does not recompute (tampered, cyclic, or self-citing), so nothing is built on such a row.
|
|
55
|
+
|
|
56
|
+
## Invariants (property tests iterate the table)
|
|
57
|
+
1. For each op: generate valid typed operands (rational boundaries, aliases at input, composites, %/%p, affine,
|
|
58
|
+
metadata, conversion targets, repeated expr refs), derive, assert `recompute(row).ok === true`.
|
|
59
|
+
2. Relabel: for each op, set `unit` to every different canonical string/null (incl. aliases and other valid targets),
|
|
60
|
+
regenerate render, keep exact: `recompute(row).ok === false` naming the expected unit; and `derive()` with that
|
|
61
|
+
declared unit is refused.
|
|
62
|
+
3. Malformed: per op, every shape in the table's malformed column has one case, named in the registry's
|
|
63
|
+
`OPERATION_RULES[op].malformed` list (the test asserts its case names equal that list): `recompute` returns
|
|
64
|
+
`ok:false` for that shape's own reason, never throws.
|
|
65
|
+
4. Nested derived rows are verified recursively (operands may be derived rows); cycles and self references are
|
|
66
|
+
refused, by `recompute()` and by `derive()` before it builds on a derived operand.
|
|
67
|
+
5. Render precision (`renderPrecision` in `unit-rules.mjs`): fewest decimal places for sum, diff, an expr whose root
|
|
68
|
+
node is `+` or `-`, and every `%p` result; fewest significant figures otherwise (an expr `*` or `/` root with a
|
|
69
|
+
non-`%p` result included). Operand values and expr literals both count.
|
|
70
|
+
|
|
71
|
+
## Decisions
|
|
72
|
+
- Q1: the canonical composite goes in `unit` (consumers compare `unit` text); no side field.
|
|
73
|
+
- Q2: expr with an unresolved unit is REFUSED; a known-unit `a*b/c` is inferable, so no rationale escape exists.
|
|
74
|
+
- Q3: canonical `%p`, input alias `pp` (matches units.mjs).
|
|
75
|
+
- Q4: strict null metadata: null matches only null; required metadata must be present.
|
|
76
|
+
|
|
77
|
+
## Out of scope (follow-ups)
|
|
78
|
+
Per-capita identity, named index/base and index-point differences, weighted means, dated FX / nominal-real bases,
|
|
79
|
+
same-currency denomination scaling (M/B), summing ratios over a common base. Each needs its own table row + tests.
|
|
80
|
+
|
|
81
|
+
## Dimensions
|
|
82
|
+
|
|
83
|
+
1. Enforced: kind and operation agree; a changed discriminator cannot disable recursive verification.
|
|
84
|
+
2. Enforced: expression and direct arithmetic check population, snapshot, and accounting metadata; common metadata propagates.
|
|
85
|
+
3. Enforced: stored `precision: { places, sigfigs }` carries operand precision independently of display trailing zeros.
|
|
86
|
+
4. Enforced: scaled aliases normalize at every composite factor and exponent, scaling the input numeral exactly.
|
|
87
|
+
5. Enforced: derive and recompute return classified refusals with `shape`, `expected`, and `detail`; derive never appends on refusal.
|
|
88
|
+
6. Enforced: aliases canonicalize at input; noncanonical stored results and conversion targets are refused, and a source unit that needs rescaling (a scaled alias) is refused as an operand.
|
|
89
|
+
7. Enforced: stored rationals are reduced integer strings with positive denominators and canonical zero.
|
|
90
|
+
8. Enforced: operation contexts have closed keys and types, including non-percent sums and context-free operations.
|
|
91
|
+
9. Enforced: formula, factor, derived_from, raw, render, metadata, precision, and compatibility are compared on replay.
|
|
92
|
+
10. Enforced: metadata object equality ignores key insertion order; missing and invalid metadata never equal explicit null.
|
|
93
|
+
11. Enforced: new derivations require active transitive dependencies; historical arithmetic remains reproducible after retraction.
|
|
94
|
+
12. Enforced: graph and expression traversal use explicit stacks; shared nodes are evaluated once per verification.
|
|
95
|
+
13. Enforced: factor exponents use bigint, preserve grouping and quotient direction, and canonicalize idempotently.
|
|
96
|
+
14. Enforced: affine atoms cannot hide in composites or cancellation; scale words are never unit atoms.
|
|
97
|
+
15. Enforced: disjoint percentage shares are individually in [0,100], total at most 100, and use exactly one reference.
|
|
98
|
+
16. Enforced: currency table conversion is refused; FX expressions require a dated rate and persist compatibility `unchecked`.
|
|
99
|
+
17. Enforced: symbols are case-sensitive; only registered spelling aliases normalize; opaque Unicode and phrases stay distinct.
|
|
100
|
+
18. Enforced: missing, undefined, and empty required fields do not substitute for explicit null; zero remains valid.
|
|
101
|
+
19. Enforced: identifiers allocate exactly beyond 2^53 using bigint; operand lists remain distinct.
|
|
102
|
+
20. Enforced: a supplied expression operand list must equal the formula's unique leaf set.
|
|
103
|
+
21. Enforced: additive expression roots and percentage-point results use decimal places; other roots count literal significant figures.
|
|
104
|
+
22. Enforced: cloned snapshots with the same spec and timestamp produce identical rows; replay appends a new identity.
|
|
105
|
+
23. Documented limitation: coherent rewrites of trusted operands, context, and all dependent audit fields require external provenance to detect.
|
|
106
|
+
24. Enforced: arity belongs to each table row; sums are permutation-invariant while ordered operations keep positional meaning.
|
|
107
|
+
25. Enforced: denominators, positive growth starts, and bounded shares obey numeric domains; ordinary negative arithmetic remains valid.
|
|
108
|
+
26. Enforced: percent, percentage-point, and fraction semantics follow typed pairs, multiplicative normalization, and explicit pct nodes.
|
|
109
|
+
27. Enforced: table factors and affine offsets compose exactly; unknown and cross-dimension conversions are refused.
|
|
110
|
+
28. Enforced: only decimal literals, fact identifiers, binary arithmetic, parentheses, and pct parse; unsupported syntax is refused.
|
|
111
|
+
29. Enforced: rounding is half away from zero; values are plain decimals and display uses canonical units and grouping.
|
|
112
|
+
30. Enforced: seeded factor trees and conversion compositions have expected values computed independently of OPERATION_RULES.
|
|
113
|
+
31. Enforced: a ledger written before `context`, `precision`, and `compatibility` existed still loads and accepts new derivations. On measured, quote, and assumed rows a missing column reads as null, and a registered alias unit is canonicalized on read (unknown or scaled text stays as given); the file changes only on the next write. Derived and converted rows without those columns load, but `recompute` reports shape `legacy derived row` and `derive` refuses them as operands with the same shape until they are regenerated.
|
|
114
|
+
32. Enforced: value/exact agreement. When a row stores both, `exact` must equal `value` at the value's decimal places; a legacy row that disagrees loads and is flagged `dimension 32`, and `addRow` refuses a new one.
|
|
115
|
+
|
|
116
|
+
## Precedence: dimension 31 wins at read time
|
|
117
|
+
|
|
118
|
+
Anything dev's shipped writer could write loads and validates. Every row is first read through `readRow`: exacts that
|
|
119
|
+
are integer pairs are reduced (sign on the numerator, no leading zeros, positive denominator), decimal values lose
|
|
120
|
+
leading zeros, and source rows get canonical units and null later columns. Measured exacts stay as stored because they
|
|
121
|
+
are always the reduced exact of `value`. Only then do the strict dimensions run. They apply in full to derived and
|
|
122
|
+
converted rows that carry this version's columns, and to every row this version writes. A source row that loads but
|
|
123
|
+
still breaks a strict rule (a scale word as unit, empty metadata, an operation or derivation fields) is flagged when it
|
|
124
|
+
is used: `derive` and `recompute` refuse it with shape `dimension 6`, `dimension 18`, or `dimension 1`, and `addRow`
|
|
125
|
+
refuses to write a new one. Normalization never strips user data: an `exact` carrying keys beyond `num` and `den` is left exactly as written and flagged `dimension 7` on use, never silently reduced to `{ num, den }`. Reading a normalized file again changes nothing. The legacy test fixture is generated by
|
|
126
|
+
dev's own `addRow` (`scripts/tests/fixtures/fidelity/gen-legacy-dev.mjs`, which pins the dev commit).
|
|
127
|
+
|
|
128
|
+
## Trust boundary
|
|
129
|
+
|
|
130
|
+
Recomputation verifies arithmetic against the ledger's trusted operands and operation context, not against an external
|
|
131
|
+
immutable source. A zero-metre conversion rewritten from km to cm together with its target, formula, factor, precision,
|
|
132
|
+
and display is indistinguishable from an honestly generated cm conversion. Single-field relabels are detected; a coherent
|
|
133
|
+
rewrite needs an immutable ledger baseline or source provenance outside this arithmetic verifier.
|
|
134
|
+
|
|
135
|
+
Rows carry no per-row schema marker, so a current derived row with `context`, `precision`, and `compatibility` all
|
|
136
|
+
deleted reads as a legacy row: `validateLedger` skips its at-rest unit and row-shape checks, and a relabeled unit on
|
|
137
|
+
it passes validation. It never becomes trusted again: `recompute` and `derive` refuse it as `legacy derived row`.
|
|
138
|
+
|
|
139
|
+
## Implementation notes
|
|
140
|
+
|
|
141
|
+
- Context fields live in the row's `context` column (null or an object on derived and converted rows, always null on
|
|
142
|
+
measured, quote, and assumed rows: a `KIND_RULES` column):
|
|
143
|
+
sum stores `{ disjoint, reference }`, convert stores `{ target }`. The sum rationale stays in `rationale`.
|
|
144
|
+
Schema and recomputation both require the operation's context. Context-free rows carry explicit null.
|
|
145
|
+
- `schemaVersion` remains 1. A derived or converted row from an older writer (no `context`, `precision`, or
|
|
146
|
+
`compatibility`) is kept as written and never used as an operand: regenerate it with `derive` to carry precision,
|
|
147
|
+
compatibility, and complete audit fields. Older source rows need nothing: they are read with null later columns.
|
|
148
|
+
- `reference` is either one text (the caller asserts one shared denominator) or a map from each operand id to its
|
|
149
|
+
denominator; the map passes only when every operand names the same non-empty denominator.
|
|
150
|
+
- `addRow` canonicalizes source units and scales alias numerals, including `100 bp/person` to `1 %p/person`.
|
|
151
|
+
At rest, source-row aliases are canonicalized on read (dimension 31); current derived rows with alias units are refused. A declared result unit with a nontrivial alias scale is refused.
|
|
152
|
+
Scale exponents with absolute value above 10000 are explicitly refused instead of allocating an unbounded numeral.
|
|
153
|
+
- A ratio over `%` or `%p` operands with unequal units is refused (use expr, where `%` enters `*` and `/` as a fraction).
|
|
154
|
+
- convert reads its target from `to`; a declared `unit` is optional and must equal that target.
|
|
155
|
+
- expr persists compatibility `unchecked` on the row as well as returning it. Population, snapshot, and accounting
|
|
156
|
+
predicates apply; a currency-rate operand needs a nonempty `valid_at`. This is arithmetic and unit verification,
|
|
157
|
+
not a guarantee of rate freshness, matching dates, population identity, or financial comparability.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// claim-kinds.mjs - the claim kinds a report sentence can carry, as one table, CLAIM_KINDS.
|
|
2
|
+
//
|
|
3
|
+
// A claim kind is structural: the writer marks it in the placeholder ({{V:F12:label}}), fill
|
|
4
|
+
// emits it as the data-claim-kind attribute on the element it renders, and the fidelity gate
|
|
5
|
+
// (fidelity-claims.mjs) checks that every marked element cites a row the kind accepts. An
|
|
6
|
+
// unmarked element is a plain fact or quote and is checked only by the value and quote checks.
|
|
7
|
+
// The one lexical piece is `unmarked`: a few first-person run patterns that raise a minor warning
|
|
8
|
+
// when a sentence reads like the kind but carries no mark; they never raise a major. The table in
|
|
9
|
+
// references/fidelity-claim-kinds.md is rendered from CLAIM_KINDS (`node claim-kinds.mjs --render-doc`).
|
|
10
|
+
// Pure; node: builtins only. Korean phrases are \u escapes.
|
|
11
|
+
|
|
12
|
+
import { isCliEntry } from "./entry-guard.mjs"
|
|
13
|
+
|
|
14
|
+
export const CLAIM_ATTRIBUTE = "data-claim-kind"
|
|
15
|
+
|
|
16
|
+
const kind = (entry) => Object.freeze({ ...entry, rowKinds: Object.freeze(entry.rowKinds), sourceKinds: entry.sourceKinds && Object.freeze(entry.sourceKinds), unmarked: entry.unmarked && Object.freeze({ ...entry.unmarked, patterns: Object.freeze(entry.unmarked.patterns) }) })
|
|
17
|
+
|
|
18
|
+
// Korean pieces as \u escapes: WE_SUBJECT = uri|jeohui as a standalone subject word (+ ga|neun, or " tim-i"),
|
|
19
|
+
// WE_RUN = silhaeng-haet/hayeo/hae | dol-lyeot/lyeo, DIRECT = jikjeop, RUN = silhaeng | dol-li/ryeo/ryeot/rin/ril,
|
|
20
|
+
// SUBJECT = a word ending in the particle ga|i|eun|neun|kkeseo (a third party ran it, not us).
|
|
21
|
+
const KO_WE_SUBJECT = "(?:^|[\\s,.;!?(])(?:\uC6B0\uB9AC|\uC800\uD76C)(?:[\uAC00\uB294]|\\s+\uD300\uC774)\\s+"
|
|
22
|
+
const KO_WE_RUN = "(?:\uC2E4\uD589(?:\uD588|\uD558\uC5EC|\uD574)|\uB3CC[\uB838\uB824])"
|
|
23
|
+
const KO_RUN = "(?:\uC2E4\uD589|\uB3CC[\uB9AC\uB824\uB838\uB9B0\uB9B4])"
|
|
24
|
+
const KO_SUBJECT = "[^\\s,.;!?]*(?:[\uAC00\uC774\uC740\uB294]|\uAED8\uC11C)(?=\\s)"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* kind -> { prefix: placeholder prefix (null = the default form), placeholder: an example,
|
|
28
|
+
* rowKinds: ledger row kinds it accepts, sourceKinds: meta.json kinds the snapshot holding the
|
|
29
|
+
* cited row's own locator artifact must have (null = any), defect: code when a marked fact cites a
|
|
30
|
+
* row it refuses, note: the rule's limits, unmarked: { code, rule, patterns } for an unmarked
|
|
31
|
+
* sentence that reads like the kind (a minor warning only), or null }.
|
|
32
|
+
*/
|
|
33
|
+
export const CLAIM_KINDS = Object.freeze({
|
|
34
|
+
fact: kind({
|
|
35
|
+
prefix: null,
|
|
36
|
+
placeholder: "{{F12:label}}",
|
|
37
|
+
rowKinds: ["measured", "converted", "derived"],
|
|
38
|
+
sourceKinds: null,
|
|
39
|
+
defect: "claim_kind_mismatch",
|
|
40
|
+
note: "Accepts exactly what an unmarked numeral accepts, so marking fact is never stricter than not marking. Neither accepts an `assumed` row: an unmarked assumption reads as a fact, so it needs its `{{A:F12:label}}` mark (`claim_kind_mismatch` otherwise).",
|
|
41
|
+
unmarked: null,
|
|
42
|
+
}),
|
|
43
|
+
quote: kind({ prefix: null, placeholder: "{{Q7:label}}", rowKinds: ["quote"], sourceKinds: null, defect: "claim_kind_mismatch", note: "", unmarked: null }),
|
|
44
|
+
verified: kind({
|
|
45
|
+
prefix: "V",
|
|
46
|
+
placeholder: "{{V:F12:label}}",
|
|
47
|
+
rowKinds: ["measured", "quote"],
|
|
48
|
+
sourceKinds: ["command"],
|
|
49
|
+
defect: "unsupported_verification",
|
|
50
|
+
note: "The cited row itself must have a locator artifact inside `sources/<source_id>/` whose `meta.json` kind is `command`; another command row in the sentence never counts. Whether it is the right command for the claim is the meaning verifier's call (minor), not this gate's.",
|
|
51
|
+
unmarked: {
|
|
52
|
+
code: "verification_claim_unmarked",
|
|
53
|
+
rule: "a first-person run: we/I (have) ran, run, executed, verified by (not ran out of); by running after we/our in the sentence; \\uC6B0\\uB9AC or \\uC800\\uD76C as the subject word (+ \\uAC00, \\uB294, or \\uD300\\uC774), then \\uC2E4\\uD589\\uD588, \\uC2E4\\uD589\\uD558\\uC5EC, \\uC2E4\\uD589\\uD574, \\uB3CC\\uB838, or \\uB3CC\\uB824 in the clause with no third-party subject between; \\uC9C1\\uC811 with no third-party subject, then \\uC2E4\\uD589 or \\uB3CC\\uB9AC in the clause",
|
|
54
|
+
patterns: [
|
|
55
|
+
/\b(?:we|I)(?:'ve)?\s+(?:(?:have|had|also|then|first|just|again)\s+)*(?:ran|run|re-?ran|re-?run|executed|verified\s+by)\b(?!\s+out\s+of\b)/i,
|
|
56
|
+
/\b(?:we|our)\b.*?\bby\s+running\b/i,
|
|
57
|
+
new RegExp(`${KO_WE_SUBJECT}(?:(?!${KO_SUBJECT})[^\\s,.;!?]+\\s+)*?${KO_WE_RUN}`),
|
|
58
|
+
new RegExp(`(?:^|[,.;!?])\\s*(?:(?!${KO_SUBJECT})[^\\s,.;!?]+\\s+)*\uC9C1\uC811\\s*(?:[^\\s,.;!?]+\\s*)??${KO_RUN}`),
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
derived: kind({ prefix: "D", placeholder: "{{D:F12:label}}", rowKinds: ["derived", "converted"], sourceKinds: null, defect: "claim_kind_mismatch", note: "", unmarked: null }),
|
|
63
|
+
assumed: kind({
|
|
64
|
+
prefix: "A",
|
|
65
|
+
placeholder: "{{A:F12:label}}",
|
|
66
|
+
rowKinds: ["assumed"],
|
|
67
|
+
sourceKinds: null,
|
|
68
|
+
defect: "claim_kind_mismatch",
|
|
69
|
+
note: "Numeric assumptions only (an assumed ledger row carries a decimal value); a qualitative assumption stays prose.",
|
|
70
|
+
unmarked: null,
|
|
71
|
+
}),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/** Whether a row satisfies a claim kind, given the meta.json kind of the snapshot holding its own locator artifact. */
|
|
75
|
+
export function claimSatisfied(name, row, sourceKind) {
|
|
76
|
+
const entry = CLAIM_KINDS[name]
|
|
77
|
+
return Boolean(entry && row && entry.rowKinds.includes(row.kind) && (entry.sourceKinds === null || entry.sourceKinds.includes(sourceKind)))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The kind an unmarked element citing `row` must be marked with (an assumed row needs `assumed`), or null when unmarked is fine. */
|
|
81
|
+
export function unmarkedRequires(row) {
|
|
82
|
+
if (!row || ["fact", "quote"].some((name) => CLAIM_KINDS[name].rowKinds.includes(row.kind))) return null
|
|
83
|
+
return Object.keys(CLAIM_KINDS).find((name) => CLAIM_KINDS[name].rowKinds.includes(row.kind)) ?? null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The kinds whose unmarked patterns match the sentence. */
|
|
87
|
+
export function unmarkedKinds(text) {
|
|
88
|
+
return Object.entries(CLAIM_KINDS)
|
|
89
|
+
.filter(([, entry]) => entry.unmarked?.patterns.some((re) => re.test(text)))
|
|
90
|
+
.map(([name]) => name)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const CLAIM_TABLE_BEGIN = "<!-- claim-kinds:begin (rendered from CLAIM_KINDS; do not edit by hand) -->"
|
|
94
|
+
export const CLAIM_TABLE_END = "<!-- claim-kinds:end -->"
|
|
95
|
+
|
|
96
|
+
const code = (text) => `\`${text}\``
|
|
97
|
+
|
|
98
|
+
/** The Markdown table of references/fidelity-claim-kinds.md. */
|
|
99
|
+
export function renderClaimKindTable() {
|
|
100
|
+
const head = ["| Kind | Placeholder | Attribute fill emits | Accepted row kinds | Required source kind | Defect when unsatisfied | Unmarked warning | Notes |", "|---|---|---|---|---|---|---|---|"]
|
|
101
|
+
const lines = Object.entries(CLAIM_KINDS).map(([name, entry]) => {
|
|
102
|
+
const attribute = entry.prefix === null ? `none (or ${code(`${CLAIM_ATTRIBUTE}="${name}"`)})` : code(`${CLAIM_ATTRIBUTE}="${name}"`)
|
|
103
|
+
const source = entry.sourceKinds === null ? "any" : entry.sourceKinds.map(code).join(", ")
|
|
104
|
+
const warn = entry.unmarked ? `${code(entry.unmarked.code)} on ${entry.unmarked.rule}` : "none"
|
|
105
|
+
return `| ${code(name)} | ${code(entry.placeholder)} | ${attribute} | ${entry.rowKinds.map(code).join(", ")} | ${source} | ${code(entry.defect)} | ${warn} | ${entry.note || "-"} |`
|
|
106
|
+
})
|
|
107
|
+
return `${[...head, ...lines].join("\n")}\n`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (isCliEntry(import.meta.url)) {
|
|
111
|
+
if (process.argv[2] === "--render-doc" && process.argv.length === 3) process.stdout.write(renderClaimKindTable())
|
|
112
|
+
else {
|
|
113
|
+
process.stderr.write("usage: node claim-kinds.mjs --render-doc\n")
|
|
114
|
+
process.exitCode = 2
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// cli-support.mjs - argument parsing, typed usage errors, exit-code mapping, and JSON file IO
|
|
2
2
|
// shared by every report-tools subcommand. node: builtins only.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { createHash } from "node:crypto"
|
|
5
|
+
import { readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
5
6
|
import { basename, dirname, join } from "node:path"
|
|
6
7
|
|
|
7
8
|
/** A usage or input error: always exit code 2. */
|
|
@@ -87,11 +88,24 @@ export function readJsonFile(path) {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
/**
|
|
91
|
-
export
|
|
91
|
+
/** The sha256 hex digest of a string or bytes: the one hash every record and receipt carries. */
|
|
92
|
+
export const sha256Of = (content) => createHash("sha256").update(content).digest("hex")
|
|
93
|
+
|
|
94
|
+
/** Write text through a sibling temp file + rename; a failed write removes the temp file. */
|
|
95
|
+
export function writeTextAtomic(path, text) {
|
|
92
96
|
const temp = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.tmp`)
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
try {
|
|
98
|
+
writeFileSync(temp, text)
|
|
99
|
+
renameSync(temp, path)
|
|
100
|
+
} catch (error) {
|
|
101
|
+
rmSync(temp, { force: true })
|
|
102
|
+
throw error
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Write JSON through writeTextAtomic, so a crash never leaves a half-written file. */
|
|
107
|
+
export function writeJsonAtomic(path, value) {
|
|
108
|
+
writeTextAtomic(path, `${JSON.stringify(value, null, 2)}\n`)
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
/** Print a result: JSON on stdout, one summary line on stderr. */
|
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
export const SCHEMA_VERSION = 1
|
|
8
8
|
|
|
9
|
+
/** The severity vocabulary in rank order; every DEFECT_CODES entry uses one of these. */
|
|
10
|
+
export const SEVERITIES = Object.freeze(["blocker", "major", "minor"])
|
|
11
|
+
|
|
12
|
+
/** Defect counts per severity, then total. */
|
|
13
|
+
export function countDefects(defects) {
|
|
14
|
+
const counts = Object.fromEntries(SEVERITIES.map((severity) => [severity, 0]))
|
|
15
|
+
for (const defect of defects) counts[defect.severity] += 1
|
|
16
|
+
return { ...counts, total: defects.length }
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
function deepFreeze(value) {
|
|
10
20
|
if (value === null || typeof value !== "object") return value
|
|
11
21
|
for (const key of Object.keys(value)) deepFreeze(value[key])
|
|
@@ -48,7 +58,7 @@ export const DEFECT_CODES = deepFreeze({
|
|
|
48
58
|
pointer_label_mismatch: { severity: "blocker", integrity: true, hint: "The placeholder's label is not the row's label; check the id, then the label." },
|
|
49
59
|
quote_anchor_unresolved: { severity: "blocker", integrity: true, hint: "A from or to anchor is missing or occurs more than once in the quote; pick unique anchor words." },
|
|
50
60
|
unit_duplicated: { severity: "blocker", integrity: true, hint: "The unit after the placeholder repeats the row's unit; use :value or remove the unit." },
|
|
51
|
-
polish_diff_fail: { severity: "blocker", integrity: true, hint: "The polish pass changed numbers, tickers, or
|
|
61
|
+
polish_diff_fail: { severity: "blocker", integrity: true, hint: "The polish pass changed numbers, tickers, table rows, claim ids, markup, or hidden content; revert to the pre-polish artifact and apply the language fixes only." },
|
|
52
62
|
fidelity_interview_incomplete: { severity: "blocker", integrity: true, hint: "A fidelity field is pending or missing in brief.md; answer the open question or record its default, then rerun." },
|
|
53
63
|
derived_not_allowed: { severity: "blocker", integrity: true, hint: "The brief allows no derived or assumed numbers; cite a source row directly or change the brief with the requester." },
|
|
54
64
|
source_outside_floor: { severity: "blocker", integrity: true, hint: "The row's source is outside the source floor the brief records; use a source the brief allows or remove the fact." },
|
|
@@ -57,7 +67,8 @@ export const DEFECT_CODES = deepFreeze({
|
|
|
57
67
|
quote_unbound: { severity: "blocker", integrity: true, hint: "A direct quotation has no ledger quote row; register it with fact quote and insert it through its placeholder." },
|
|
58
68
|
edition_unverified: { severity: "blocker", integrity: true, hint: "The delivered file has no matching fact verify record; run fact verify on that edition and record it with --verified-by." },
|
|
59
69
|
untraceable_number: { severity: "major", integrity: false, hint: "Replace the typed number with a {{F<n>}} placeholder backed by a ledger row, or register it as assumed with a rationale." },
|
|
60
|
-
unsupported_verification: { severity: "major", integrity: false, hint: "
|
|
70
|
+
unsupported_verification: { severity: "major", integrity: false, hint: "A sentence marked verified (data-claim-kind=\"verified\") cites a row that is not recorded command output; cite the recorded output row or drop the verified mark." },
|
|
71
|
+
claim_kind_mismatch: { severity: "major", integrity: false, hint: "An element marked with a claim kind (data-claim-kind) cites a row that kind does not accept, or the kind is unknown; cite a row of that kind or fix the mark (references/fidelity-claim-kinds.md)." },
|
|
61
72
|
conflicting_readings: { severity: "major", integrity: false, hint: "Two ledger rows with different periods or qualifiers are cited as one fact; split the sentence or retire one row." },
|
|
62
73
|
hedge_dropped: { severity: "major", integrity: false, hint: "Restore the qualifier the source uses (estimated, about, not yet)." },
|
|
63
74
|
direction_flipped: { severity: "major", integrity: false, hint: "The source states the opposite direction; rewrite from the quote." },
|
|
@@ -65,7 +76,8 @@ export const DEFECT_CODES = deepFreeze({
|
|
|
65
76
|
qualifier_dropped: { severity: "major", integrity: false, hint: "The source bounds this value (up to, at least, about); restore the qualifier or use :q." },
|
|
66
77
|
unbound_sentence: { severity: "major", integrity: false, hint: "Bind the sentence to a fact, a quote, a citation, or a claim-graph node with data-claim." },
|
|
67
78
|
meaning_drift_confirmed: { severity: "major", integrity: false, hint: "Two independent readers found the same drift; rewrite from the quote or dispute with a reason." },
|
|
68
|
-
|
|
79
|
+
meaning_unverified: { severity: "major", integrity: false, hint: "No meaning reader judged this pair, or no strong reader judged a force_strong pair; run the missing reader on pairs.json and rerun fact meaning-check." },
|
|
80
|
+
polish_edit_rejected: { severity: "major", integrity: false, hint: "The edit changed a number, a hedge, a qualifier, a link, or a claim id, or contained <, > or &; re-propose the language fix only, narrowed to the words around any entity or tag." },
|
|
69
81
|
polish_hedging_decreased: { severity: "major", integrity: false, hint: "The polish pass removed qualifiers; restore them." },
|
|
70
82
|
polish_text_churn: { severity: "major", integrity: false, hint: "More than half the text changed during polish; the pass composed instead of proofreading." },
|
|
71
83
|
snapshot_mismatch: { severity: "major", integrity: false, hint: "The operands come from different collection windows; use rows from one window or state the windows separately." },
|
|
@@ -73,6 +85,7 @@ export const DEFECT_CODES = deepFreeze({
|
|
|
73
85
|
quote_stale_version: { severity: "major", integrity: false, hint: "The quoted source revision differs from the revision the report targets; re-snapshot the target revision or date the quote." },
|
|
74
86
|
quote_fuzzy_match: { severity: "minor", integrity: false, hint: "The source text is OCR-noisy; confirm the snapshot and prefer the raw document." },
|
|
75
87
|
name_outside_quote: { severity: "minor", integrity: false, hint: "A name in the sentence is not in the cited quote; add the source or remove the name." },
|
|
88
|
+
verification_claim_unmarked: { severity: "minor", integrity: false, hint: "The sentence reads as a check we ran but carries no verified mark; mark it {{V:F<n>:label}} on a recorded command-output row, or reword it." },
|
|
76
89
|
scope_widened_warn: { severity: "minor", integrity: false, hint: "The sentence uses a broader scope word than the quote; confirm the scope or narrow it." },
|
|
77
90
|
particle_unverified: { severity: "minor", integrity: false, hint: "The particle after this value could not be checked; read it aloud and fix it by hand if wrong." },
|
|
78
91
|
fallback_without_entity: { severity: "minor", integrity: false, hint: "The replacement quote has no entity for attribution; set one on the row." },
|
|
@@ -87,10 +100,10 @@ export const FIDELITY_CODES = Object.freeze([
|
|
|
87
100
|
"unresolved_placeholder", "value_mismatch", "unit_mismatch", "quote_not_verbatim", "claim_status_invalid",
|
|
88
101
|
"incompatible_operands", "pointer_label_mismatch", "quote_anchor_unresolved", "unit_duplicated", "polish_diff_fail",
|
|
89
102
|
"fidelity_interview_incomplete", "derived_not_allowed", "source_outside_floor", "snapshot_hash_mismatch",
|
|
90
|
-
"chart_value_untraceable", "quote_unbound", "edition_unverified", "untraceable_number", "unsupported_verification",
|
|
103
|
+
"chart_value_untraceable", "quote_unbound", "edition_unverified", "untraceable_number", "unsupported_verification", "claim_kind_mismatch",
|
|
91
104
|
"conflicting_readings", "hedge_dropped", "direction_flipped", "causality_added", "qualifier_dropped", "unbound_sentence",
|
|
92
|
-
"meaning_drift_confirmed", "polish_edit_rejected", "polish_hedging_decreased", "polish_text_churn", "snapshot_mismatch",
|
|
93
|
-
"join_integrity_fail", "quote_stale_version", "quote_fuzzy_match", "name_outside_quote", "scope_widened_warn",
|
|
105
|
+
"meaning_drift_confirmed", "meaning_unverified", "polish_edit_rejected", "polish_hedging_decreased", "polish_text_churn", "snapshot_mismatch",
|
|
106
|
+
"join_integrity_fail", "quote_stale_version", "quote_fuzzy_match", "name_outside_quote", "verification_claim_unmarked", "scope_widened_warn",
|
|
94
107
|
"particle_unverified", "fallback_without_entity", "meaning_drift_warn", "polish_edit_unlocated", "polish_link_changed",
|
|
95
108
|
"polish_name_changed",
|
|
96
109
|
])
|
|
@@ -184,6 +197,14 @@ export function validateOutcomeManifest(manifest) {
|
|
|
184
197
|
if (isObject(manifest.gates)) {
|
|
185
198
|
for (const gate of GATE_NAMES) check(errors, GATE_STATUSES.includes(manifest.gates[gate]), `gates.${gate} must be one of ${GATE_STATUSES.join(", ")}`)
|
|
186
199
|
} else errors.push("gates must be an object")
|
|
200
|
+
if (manifest.gateHistory !== undefined) {
|
|
201
|
+
if (!Array.isArray(manifest.gateHistory)) errors.push("gateHistory must be an array")
|
|
202
|
+
else for (const entry of manifest.gateHistory) {
|
|
203
|
+
check(errors, isObject(entry) && GATE_NAMES.includes(entry.gate) && GATE_STATUSES.includes(entry.status)
|
|
204
|
+
&& typeof entry.at === "string" && ISO_DATE.test(entry.at)
|
|
205
|
+
&& (entry.reason === undefined || typeof entry.reason === "string"), "gateHistory entries must have a known gate, status, timestamp, and optional reason")
|
|
206
|
+
}
|
|
207
|
+
}
|
|
187
208
|
if (Array.isArray(manifest.residualDefects)) manifest.residualDefects.forEach((defect, index) => validateDefect(defect, `residualDefects[${index}]`, errors))
|
|
188
209
|
else errors.push("residualDefects must be an array")
|
|
189
210
|
check(errors, manifest.repair === null || isObject(manifest.repair), "repair must be null or an object")
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// fact-commands-meaning.mjs - `fact pairs` and `fact meaning-check`, the script half of the
|
|
2
|
+
// separate-session meaning verifier. fact pairs writes the (source, report sentence) pairs a
|
|
3
|
+
// reader lane gets and nothing else; it honours the brief's `meaning check` field and writes
|
|
4
|
+
// `{ skipped: true }` when it is off. fact meaning-check ingests the readers' verdict files
|
|
5
|
+
// (meaning-check.mjs), writes fidelity-meaning-defects.json (a fidelity record bound to the pairs'
|
|
6
|
+
// artifact_sha256 and report, through writeFidelityRecord), and records the run in
|
|
7
|
+
// fidelity-calibration.jsonl: a header with the promotion criterion on first write, then per run
|
|
8
|
+
// one run line plus one line per flagged pair. The run id hashes the inputs only, so a rerun or a
|
|
9
|
+
// --dispute rerun of the same inputs replaces that run's lines in place (disputes carry forward)
|
|
10
|
+
// instead of counting a second run. Neither command calls a model or the network.
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
13
|
+
import { dirname, join, resolve } from "node:path"
|
|
14
|
+
|
|
15
|
+
import { CliError, parseArgs, readJsonFile, sha256Of, writeJsonAtomic, writeTextAtomic } from "./cli-support.mjs"
|
|
16
|
+
import { countDefects } from "./contracts.mjs"
|
|
17
|
+
import { markdownAsHtml, readEdition } from "./fact-commands-verify.mjs"
|
|
18
|
+
import { readLedger } from "./fact-ledger.mjs"
|
|
19
|
+
import { validateLedger } from "./fact-ledger-schema.mjs"
|
|
20
|
+
import { extractPairs, PAIR_KINDS } from "./fact-pairs.mjs"
|
|
21
|
+
import { FIDELITY_INCOMPLETE, meaningCheckEnabled, requireLockedBrief } from "./fidelity-brief.mjs"
|
|
22
|
+
import { MEANING_RECORD, writeFidelityRecord } from "./fidelity-records.mjs"
|
|
23
|
+
import { blocksDelivery } from "./gates-fidelity.mjs"
|
|
24
|
+
import { ingestVerdicts, unverifiedDefects, VERDICT_SCHEMA } from "./meaning-check.mjs"
|
|
25
|
+
|
|
26
|
+
export const PAIRS_FILE = "pairs.json"
|
|
27
|
+
export const CALIBRATION_FILE = "fidelity-calibration.jsonl"
|
|
28
|
+
/** Starting values, recorded in the calibration log header; changed only with evidence. */
|
|
29
|
+
export const PROMOTION = Object.freeze({ code: "meaning_drift_confirmed", to: "integrity", min_runs: 20, max_disputed_to_confirmed: 0.1, false_block_window: 20, max_report_false_blocks_in_window: 2 })
|
|
30
|
+
|
|
31
|
+
/** The command status: fail when any defect blocks delivery under the registry policy. */
|
|
32
|
+
export const meaningStatus = (defects) => (defects.some(blocksDelivery) ? "fail" : "pass")
|
|
33
|
+
|
|
34
|
+
const sessionOf = (flags) => {
|
|
35
|
+
const dir = flags["session-dir"] ?? process.env.SESSION_DIR
|
|
36
|
+
return dir && dir !== true ? resolve(dir) : null
|
|
37
|
+
}
|
|
38
|
+
const inSession = (session, name, flag) => {
|
|
39
|
+
if (session === null) throw new CliError(`${flag} is required without --session-dir (or SESSION_DIR)`)
|
|
40
|
+
return join(session, name)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readLedgerFrom(flags, session) {
|
|
44
|
+
if (flags.ledger === undefined) {
|
|
45
|
+
if (session === null) throw new CliError("--ledger is required without --session-dir (or SESSION_DIR)")
|
|
46
|
+
return readLedger(session)
|
|
47
|
+
}
|
|
48
|
+
const ledger = readJsonFile(flags.ledger)
|
|
49
|
+
const errors = validateLedger(ledger)
|
|
50
|
+
if (errors.length > 0) throw new CliError(`${flags.ledger} is not a valid fact ledger: ${errors[0]}`)
|
|
51
|
+
return ledger
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function briefState(flags, session) {
|
|
55
|
+
const path = flags.brief ?? inSession(session, "brief.md", "--brief")
|
|
56
|
+
const lock = requireLockedBrief(path)
|
|
57
|
+
if (lock.exitCode === 2) throw new CliError(`${path}: malformed ## Fidelity block: ${lock.errors.join("; ")}`)
|
|
58
|
+
return { path, locked: lock.locked, off: !meaningCheckEnabled(lock.parsed) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runPairs(args) {
|
|
62
|
+
const [report] = args.positionals
|
|
63
|
+
const { flags } = args
|
|
64
|
+
if (!report) throw new CliError("usage: fact pairs <report> [--format html|md] [--ledger F] [--claims claims.json] [--changed-since <fill-map.json>] [--brief brief.md] [--out pairs.json]")
|
|
65
|
+
const session = sessionOf(flags)
|
|
66
|
+
const out = resolve(flags.out ?? inSession(session, PAIRS_FILE, "--out"))
|
|
67
|
+
const brief = briefState(flags, session)
|
|
68
|
+
if (!brief.locked) {
|
|
69
|
+
const json = { command: "fact pairs", status: "fail", exitCode: 1, error: "fidelity_interview_incomplete", message: `${brief.path} has no locked ## Fidelity block` }
|
|
70
|
+
return { json, summary: `fact pairs: ${json.message}`, exitCode: 1 }
|
|
71
|
+
}
|
|
72
|
+
if (brief.off) {
|
|
73
|
+
const record = { schemaVersion: 1, skipped: true, reason: "meaning check: off", pairs: [] }
|
|
74
|
+
writeJsonAtomic(out, record)
|
|
75
|
+
return { json: { command: "fact pairs", status: "skipped", exitCode: 0, data: { out, ...record } }, summary: "fact pairs: skipped (meaning check: off)", exitCode: 0 }
|
|
76
|
+
}
|
|
77
|
+
const edition = readEdition(report, flags)
|
|
78
|
+
const claimsPath = flags.claims ?? (session === null ? null : join(session, "claims.json"))
|
|
79
|
+
const claims = claimsPath !== null && (flags.claims !== undefined || existsSync(claimsPath)) ? readJsonFile(claimsPath) : null
|
|
80
|
+
const changed = flags["changed-since"] === undefined ? undefined : readJsonFile(flags["changed-since"]).entries
|
|
81
|
+
const ledger = readLedgerFrom(flags, session)
|
|
82
|
+
const html = edition.format === "md" ? markdownAsHtml(edition.text, edition.entries, ledger).html : edition.text
|
|
83
|
+
const pairs = extractPairs(html, ledger, claims, changed)
|
|
84
|
+
const counts = { total: pairs.length, force_strong: pairs.filter((pair) => pair.force_strong).length }
|
|
85
|
+
for (const kind of PAIR_KINDS) counts[kind] = pairs.filter((pair) => pair.kind === kind).length
|
|
86
|
+
const record = { schemaVersion: 1, skipped: false, report: resolve(report), format: edition.format, artifact_sha256: sha256Of(edition.text), pairs_sha256: sha256Of(JSON.stringify(pairs)), counts, verdict_schema: VERDICT_SCHEMA, pairs }
|
|
87
|
+
writeJsonAtomic(out, record)
|
|
88
|
+
const summary = `fact pairs: ${counts.total} pair(s), ${counts.force_strong} routed to the strong reader`
|
|
89
|
+
return { json: { command: "fact pairs", status: "pass", exitCode: 0, data: { out, ...record, pairs: undefined } }, summary, exitCode: 0 }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Every line of the log after the header, in file order; a corrupt line is a usage error naming path:line. */
|
|
93
|
+
function calibrationRows(path) {
|
|
94
|
+
if (!existsSync(path)) return []
|
|
95
|
+
const rows = []
|
|
96
|
+
for (const [index, line] of readFileSync(path, "utf8").split("\n").entries()) {
|
|
97
|
+
if (line.trim() === "") continue
|
|
98
|
+
try {
|
|
99
|
+
rows.push(JSON.parse(line))
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new CliError(`${path}:${index + 1}: corrupt calibration log line (${error.message}); repair or move the file`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return rows.filter((row) => row.header !== true)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The promotion criterion evaluated over the log: runs, disputed-to-confirmed, report-level false blocks. */
|
|
108
|
+
export function promotionEligibility(rows) {
|
|
109
|
+
const runs = [...new Set(rows.map((row) => row.run_id))]
|
|
110
|
+
const confirmed = rows.filter((row) => row.run !== true && row.confirmed)
|
|
111
|
+
const disputed = confirmed.filter((row) => row.disputed).length
|
|
112
|
+
const ratio = confirmed.length === 0 ? null : disputed / confirmed.length
|
|
113
|
+
const falseBlocks = runs.slice(-PROMOTION.false_block_window).filter((run) => {
|
|
114
|
+
const blocking = confirmed.filter((row) => row.run_id === run)
|
|
115
|
+
return blocking.length > 0 && blocking.every((row) => row.disputed)
|
|
116
|
+
}).length
|
|
117
|
+
const eligible = runs.length >= PROMOTION.min_runs && ratio !== null && ratio < PROMOTION.max_disputed_to_confirmed && falseBlocks <= PROMOTION.max_report_false_blocks_in_window
|
|
118
|
+
return { runs: runs.length, confirmed: confirmed.length, disputed_confirmed: disputed, disputed_to_confirmed: ratio, report_false_blocks_in_window: falseBlocks, eligible }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Writes this run's lines: one run line (so a run with no flagged pair still counts) and one line per
|
|
123
|
+
* flagged pair. A run id already in the log is replaced in place, keeping its position in the false
|
|
124
|
+
* block window and any earlier dispute on a pair the new lines do not dispute.
|
|
125
|
+
*/
|
|
126
|
+
function recordCalibration(path, lines, runId, pairsSha) {
|
|
127
|
+
const before = calibrationRows(path)
|
|
128
|
+
const previous = before.filter((row) => row.run_id === runId)
|
|
129
|
+
const disputes = new Map(previous.filter((row) => row.disputed).map((row) => [row.pair_id, row.reason]))
|
|
130
|
+
const pairRows = lines.map((line) => {
|
|
131
|
+
const row = { ...JSON.parse(line), run_id: runId, pairs_sha256: pairsSha }
|
|
132
|
+
return !row.disputed && disputes.has(row.pair_id) ? { ...row, disputed: true, reason: disputes.get(row.pair_id) } : row
|
|
133
|
+
})
|
|
134
|
+
const rows = [{ run: true, run_id: runId, pairs_sha256: pairsSha, flagged: pairRows.length }, ...pairRows]
|
|
135
|
+
const at = previous.length === 0 ? before.length : before.indexOf(previous[0])
|
|
136
|
+
const after = [...before.slice(0, at).filter((row) => row.run_id !== runId), ...rows, ...before.slice(at).filter((row) => row.run_id !== runId)]
|
|
137
|
+
const header = { header: true, schemaVersion: 1, promotion: PROMOTION, note: "starting values; change them only with evidence" }
|
|
138
|
+
writeTextAtomic(path, `${[header, ...after].map((row) => JSON.stringify(row)).join("\n")}\n`)
|
|
139
|
+
return { written: pairRows.length, replaced: previous.length > 0, rows: after }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A `{ skipped: true }` pairs file is accepted only when the locked brief turns the meaning check off. */
|
|
143
|
+
function skippedRefusal(flags, session) {
|
|
144
|
+
const brief = briefState(flags, session)
|
|
145
|
+
if (!brief.locked) return { error: FIDELITY_INCOMPLETE, message: `${brief.path} has no locked ## Fidelity block, so a skipped meaning check cannot be accepted` }
|
|
146
|
+
if (!brief.off) return { error: "meaning_check_required", message: `${flags.pairs} says skipped but ${brief.path} has meaning check on; rerun fact pairs` }
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function runMeaningCheck(args) {
|
|
151
|
+
const { flags } = args
|
|
152
|
+
const usage = "usage: fact meaning-check --pairs pairs.json --verdicts cheap.json [--verdicts strong.json] [--dispute P<n> --reason TEXT] [--run-id ID] [--calibration F] [--out F]"
|
|
153
|
+
if (flags.pairs === undefined || !Array.isArray(flags.verdicts)) throw new CliError(usage)
|
|
154
|
+
const session = sessionOf(flags)
|
|
155
|
+
const pairsText = readFileSync(flags.pairs, "utf8")
|
|
156
|
+
const pairsDoc = readJsonFile(flags.pairs)
|
|
157
|
+
if (pairsDoc?.skipped === true) {
|
|
158
|
+
const refusal = skippedRefusal(flags, session)
|
|
159
|
+
if (refusal !== null) return { json: { command: "fact meaning-check", status: "fail", exitCode: 1, ...refusal }, summary: `fact meaning-check: ${refusal.message}`, exitCode: 1 }
|
|
160
|
+
return { json: { command: "fact meaning-check", status: "skipped", exitCode: 0, defects: [] }, summary: JSON.stringify({ command: "fact meaning-check", status: "skipped" }), exitCode: 0 }
|
|
161
|
+
}
|
|
162
|
+
if (typeof pairsDoc?.artifact_sha256 !== "string" || typeof pairsDoc.report !== "string") throw new CliError(`${flags.pairs} names no artifact_sha256 and report, so the meaning record cannot be bound to an edition; rerun fact pairs`)
|
|
163
|
+
for (const file of flags.verdicts) readJsonFile(file)
|
|
164
|
+
const verdictTexts = flags.verdicts.map((file) => readFileSync(file, "utf8"))
|
|
165
|
+
const dispute = flags.dispute === undefined ? undefined : { pair_id: flags.dispute, reason: args.required("reason") }
|
|
166
|
+
let result
|
|
167
|
+
try {
|
|
168
|
+
result = ingestVerdicts(pairsDoc, flags.verdicts, dispute ? { dispute } : {})
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new CliError(`invalid verdict input: ${error.message}`)
|
|
171
|
+
}
|
|
172
|
+
const out = resolve(flags.out ?? inSession(session, MEANING_RECORD, "--out"))
|
|
173
|
+
const calibration = resolve(flags.calibration ?? join(session ?? dirname(out), CALIBRATION_FILE))
|
|
174
|
+
const runId = flags["run-id"] ?? sha256Of([pairsText, ...verdictTexts].join("\0")).slice(0, 16)
|
|
175
|
+
const pairsSha = pairsDoc.pairs_sha256 ?? sha256Of(pairsText)
|
|
176
|
+
const log = recordCalibration(calibration, result.calibrationLines, runId, pairsSha)
|
|
177
|
+
const defects = [...result.defects, ...unverifiedDefects(result.unverified)]
|
|
178
|
+
const counts = countDefects(defects)
|
|
179
|
+
const status = meaningStatus(defects)
|
|
180
|
+
const exitCode = status === "fail" ? 1 : 0
|
|
181
|
+
const record = writeFidelityRecord(out, "meaning", { command: "fact meaning-check", status, exitCode, artifact_sha256: pairsDoc.artifact_sha256, report: pairsDoc.report, run_id: runId, pairs_sha256: pairsSha, defects, counts, dropped: result.dropped, unverified: result.unverified, calibration_path: calibration, calibration_written: log.written, calibration_replaced: log.replaced, promotion_eligibility: promotionEligibility(log.rows) })
|
|
182
|
+
const confirmed = result.defects.filter((defect) => defect.code === "meaning_drift_confirmed").length
|
|
183
|
+
const summary = JSON.stringify({ command: "fact meaning-check", status, defects: defects.length, confirmed, warn: result.defects.length - confirmed, dropped: result.dropped.length, unverified: result.unverified.length })
|
|
184
|
+
return { json: { ...record, out }, summary, exitCode }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @param {string[]} argv the arguments after `fact pairs` or `fact meaning-check` */
|
|
188
|
+
export function runFactMeaning(sub, argv) {
|
|
189
|
+
const args = parseArgs(argv, { repeatable: ["verdicts"] })
|
|
190
|
+
return sub === "pairs" ? runPairs(args) : runMeaningCheck(args)
|
|
191
|
+
}
|