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,140 @@
|
|
|
1
|
+
// source-commands.mjs - the `source snapshot` and `claims index` subcommands of report-tools.
|
|
2
|
+
// A source dir is immutable: the same id with identical text is a no-op, different text fails
|
|
3
|
+
// (exit 1, snapshot_exists), because ledger rows pin the old hash. `--url` fetches with plain
|
|
4
|
+
// fetch (30 s timeout, no browser) and records extractor tool:source-snapshot@1; `--html` and
|
|
5
|
+
// `--text` import a browser-lane capture and record extractor model (lower trust), or the named
|
|
6
|
+
// extractor for `--text --extractor NAME@VERSION`; `--command` runs an argv without a shell and
|
|
7
|
+
// snapshots its stdout, with the argv, cwd, exit status, and output hashes in command.json.
|
|
8
|
+
// node: builtins and sibling imports only.
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process"
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
12
|
+
import { join, resolve } from "node:path"
|
|
13
|
+
|
|
14
|
+
import { indexClaims } from "./claims-index.mjs"
|
|
15
|
+
import { CliError, parseArgs, sha256Of, writeJsonAtomic } from "./cli-support.mjs"
|
|
16
|
+
import { parseFidelityBlock } from "./fidelity-brief.mjs"
|
|
17
|
+
import { writeSnapshot } from "./source-snapshot.mjs"
|
|
18
|
+
|
|
19
|
+
const FETCH_TIMEOUT_MS = 30_000
|
|
20
|
+
const TOOL_EXTRACTOR = "tool:source-snapshot@1"
|
|
21
|
+
const SNAPSHOT_USAGE =
|
|
22
|
+
"usage: source snapshot --id S<n> (--url <u> | --html <file> | --text <file> [--extractor NAME@VERSION] | --command '<argv json>' --cwd DIR) [--kind page|command|document] [--provenance primary|secondary] [--revision R] --session-dir D"
|
|
23
|
+
|
|
24
|
+
function readInput(path) {
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(path, "utf8")
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new CliError(`cannot read ${path}: ${error.code ?? error.message}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The session dir from --session-dir or SESSION_DIR; a usage error when neither is set. */
|
|
33
|
+
export function sessionDirOf(flags) {
|
|
34
|
+
const dir = flags["session-dir"] ?? process.env.SESSION_DIR
|
|
35
|
+
if (!dir || dir === true) throw new CliError("--session-dir is required (or set SESSION_DIR)")
|
|
36
|
+
return resolve(dir)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The snapshot of one source: meta.json, text.txt, and source.html when the source was a page. */
|
|
40
|
+
export function snapshotOf(sessionDir, id) {
|
|
41
|
+
const dir = join(sessionDir, "sources", String(id))
|
|
42
|
+
if (!existsSync(join(dir, "meta.json"))) throw new CliError(`source ${id} has no snapshot; run source snapshot --id ${id} first`)
|
|
43
|
+
const html = join(dir, "source.html")
|
|
44
|
+
return { meta: JSON.parse(readInput(join(dir, "meta.json"))), text: readInput(join(dir, "text.txt")), html: existsSync(html) ? readInput(html) : null }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whether the brief's source floor lists this source as pointed at (`pointed only: a, b`). */
|
|
48
|
+
export function pointedFlag(sessionDir, url) {
|
|
49
|
+
const brief = join(sessionDir, "brief.md")
|
|
50
|
+
const floor = existsSync(brief) ? (parseFidelityBlock(readInput(brief)).fields?.["source floor"] ?? "") : ""
|
|
51
|
+
const match = /^pointed only:\s*(.+)$/i.exec(floor.trim())
|
|
52
|
+
return match ? match[1].split(",").map((entry) => entry.trim()).includes(url) : false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function fetchSource(url) {
|
|
56
|
+
if (!URL.canParse(url)) throw new CliError(`--url is not a URL: ${url}`)
|
|
57
|
+
let response
|
|
58
|
+
try {
|
|
59
|
+
response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new CliError(`fetch failed for ${url}: ${error.message}`)
|
|
62
|
+
}
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
await response.body?.cancel()
|
|
65
|
+
throw new CliError(`fetch failed: HTTP ${response.status} for ${url}`)
|
|
66
|
+
}
|
|
67
|
+
const type = response.headers.get("content-type") ?? ""
|
|
68
|
+
if (/pdf/i.test(type)) {
|
|
69
|
+
await response.body?.cancel()
|
|
70
|
+
throw new CliError(`pdf response: ${url}; extract its text and use --text <file> --extractor NAME@VERSION`)
|
|
71
|
+
}
|
|
72
|
+
const body = await response.text()
|
|
73
|
+
return /html|xml/i.test(type) || /^\s*</.test(body) ? { url, html: body } : { url, text: body }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function runCommand(argvJson, cwd) {
|
|
77
|
+
let argv
|
|
78
|
+
try {
|
|
79
|
+
argv = JSON.parse(argvJson)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new CliError(`--command must be a JSON array of strings: ${error.message}`)
|
|
82
|
+
}
|
|
83
|
+
if (!Array.isArray(argv) || argv.length === 0 || !argv.every((part) => typeof part === "string" && part.length > 0)) {
|
|
84
|
+
throw new CliError("--command must be a non-empty JSON array of non-empty strings")
|
|
85
|
+
}
|
|
86
|
+
if (!cwd || cwd === true) throw new CliError("--command needs --cwd DIR")
|
|
87
|
+
const proc = spawnSync(argv[0], argv.slice(1), { cwd: resolve(cwd), encoding: "utf8", shell: false, maxBuffer: 64 * 1024 * 1024 })
|
|
88
|
+
if (proc.error) throw new CliError(`--command could not start: ${proc.error.message}`)
|
|
89
|
+
const receipt = { argv, cwd: resolve(cwd), exit_status: proc.status, stdout_sha256: sha256Of(proc.stdout), stderr_sha256: sha256Of(proc.stderr) }
|
|
90
|
+
return { path: `command:${argv.join(" ")}`, text: proc.stdout, kind: "command", receipt }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function acquire(flags) {
|
|
94
|
+
const given = ["url", "html", "text", "command"].filter((name) => flags[name] !== undefined)
|
|
95
|
+
if (given.length !== 1) throw new CliError(SNAPSHOT_USAGE)
|
|
96
|
+
if (flags.extractor !== undefined && flags.text === undefined) throw new CliError("--extractor goes with --text only")
|
|
97
|
+
if (flags.url !== undefined) return { ...(await fetchSource(flags.url)), extractor: TOOL_EXTRACTOR }
|
|
98
|
+
if (flags.html !== undefined) return { path: resolve(flags.html), html: readInput(flags.html), extractor: "model" }
|
|
99
|
+
if (flags.text !== undefined) return { path: resolve(flags.text), text: readInput(flags.text), extractor: flags.extractor === undefined ? "model" : `extractor:${flags.extractor}` }
|
|
100
|
+
return { ...runCommand(flags.command, flags.cwd), extractor: TOOL_EXTRACTOR }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function runSource(argv) {
|
|
104
|
+
const [sub, ...rest] = argv
|
|
105
|
+
if (sub !== "snapshot") throw new CliError(SNAPSHOT_USAGE)
|
|
106
|
+
const args = parseArgs(rest)
|
|
107
|
+
const id = args.required("id")
|
|
108
|
+
const sourcesDir = join(sessionDirOf(args.flags), "sources")
|
|
109
|
+
const { receipt, ...input } = await acquire(args.flags)
|
|
110
|
+
if (args.flags.kind !== undefined) input.kind = args.flags.kind
|
|
111
|
+
if (args.flags.provenance !== undefined) input.provenance = args.flags.provenance
|
|
112
|
+
if (args.flags.revision !== undefined) input.revision = args.flags.revision
|
|
113
|
+
let written
|
|
114
|
+
try {
|
|
115
|
+
written = writeSnapshot(sourcesDir, id, input)
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (String(error.message).startsWith("snapshot_exists")) {
|
|
118
|
+
return { json: { command: "source snapshot", status: "fail", exitCode: 1, error: "snapshot_exists", message: error.message }, summary: `source snapshot: ${error.message}`, exitCode: 1 }
|
|
119
|
+
}
|
|
120
|
+
if (error instanceof TypeError) throw new CliError(error.message)
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
123
|
+
if (receipt) writeFileSync(join(written.dir, "command.json"), `${JSON.stringify(receipt, null, 2)}\n`)
|
|
124
|
+
const meta = JSON.parse(readFileSync(join(written.dir, "meta.json"), "utf8"))
|
|
125
|
+
const data = { source_id: id, directory: written.dir, snapshot_sha256: written.sha256, extractor: meta.extractor, kind: meta.kind, provenance: meta.provenance }
|
|
126
|
+
return { json: { command: "source snapshot", status: "pass", exitCode: 0, data }, summary: `source snapshot: ${id} ${written.sha256.slice(0, 12)} (${meta.extractor})`, exitCode: 0 }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function runClaims(argv) {
|
|
130
|
+
const [sub, ...rest] = argv
|
|
131
|
+
const args = parseArgs(rest)
|
|
132
|
+
const [graph] = args.positionals
|
|
133
|
+
if (sub !== "index" || !graph) throw new CliError("usage: claims index <claim-graph.md> --out claims.json")
|
|
134
|
+
const out = args.required("out")
|
|
135
|
+
const index = indexClaims(readInput(graph))
|
|
136
|
+
if (index.errors.length > 0) throw new CliError(`claims index ${graph}: ${index.errors.join("; ")}`)
|
|
137
|
+
writeJsonAtomic(out, { schemaVersion: 1, graph: resolve(graph), graph_sha256: sha256Of(readFileSync(graph)), ...index })
|
|
138
|
+
const verified = index.claims.filter((claim) => claim.in_verified).length
|
|
139
|
+
return { json: { command: "claims index", out: resolve(out), claims: index.claims.length, verified }, summary: `claims index: ${index.claims.length} claim(s), ${verified} verified`, exitCode: 0 }
|
|
140
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// source-snapshot.mjs - the per-source snapshot that fact locators read.
|
|
2
|
+
//
|
|
3
|
+
// snapshotFromHtml turns a page into text.txt form: one line per block (prose blocks via
|
|
4
|
+
// proseText, one table cell per line; head, scripts, styles and code are dropped). writeSnapshot
|
|
5
|
+
// writes <sourcesDir>/<S-id>/ with text.txt (pages joined by a form feed), source.html when html
|
|
6
|
+
// was given, meta.json, sentences.jsonl (sid S<n>:s<k>) and acronyms.json. The sha256 covers
|
|
7
|
+
// text.txt only. Nothing here touches the network: the caller supplies the html or text.
|
|
8
|
+
// Plain Node ESM, node: builtins and sibling imports only.
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
11
|
+
import { join } from "node:path"
|
|
12
|
+
|
|
13
|
+
import { sha256Of } from "./cli-support.mjs"
|
|
14
|
+
import { parseHtml, PROSE_TAGS, proseText } from "./html-lite.mjs"
|
|
15
|
+
import { splitSentences } from "./sentence-split.mjs"
|
|
16
|
+
|
|
17
|
+
const SKIP = new Set(["head", "script", "style", "svg", "code", "pre", "kbd", "samp", "template", "noscript"])
|
|
18
|
+
const INLINE = new Set([
|
|
19
|
+
"a", "abbr", "b", "bdi", "bdo", "cite", "data", "del", "dfn", "em", "font", "i", "ins", "label", "mark", "q", "s",
|
|
20
|
+
"small", "span", "strong", "sub", "sup", "time", "u", "var",
|
|
21
|
+
])
|
|
22
|
+
const SOURCE_ID = /^S[1-9]\d*$/
|
|
23
|
+
const EXTRACTOR = /^(?:tool:source-snapshot@1|model|extractor:[\w.@+-]+)$/
|
|
24
|
+
const KINDS = new Set(["page", "command", "document"])
|
|
25
|
+
const PROVENANCE = new Set(["primary", "secondary", "unknown"])
|
|
26
|
+
const ACRONYM_AFTER = /\(([A-Z]{2,6})\)/g
|
|
27
|
+
const ACRONYM_BEFORE = /(?<![\p{L}\p{N}])([A-Z]{2,6})\s?\(([A-Z][^()\n]{2,79})\)/gu
|
|
28
|
+
const NAME_WORD = /^[A-Z][\p{L}\p{N}-]*$/u
|
|
29
|
+
const CONNECTOR = new Set(["of", "and", "for", "the", "in", "on", "to", "&"])
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Block lines of a page: { root, text, blocks: [{ node, start, end }] } where text joins the block
|
|
33
|
+
* texts with "\n" and text.slice(start, end) is one block. fact-locate reads cells through `node`.
|
|
34
|
+
*/
|
|
35
|
+
export function snapshotBlocks(html) {
|
|
36
|
+
const root = parseHtml(html)
|
|
37
|
+
const lines = []
|
|
38
|
+
let buffer = ""
|
|
39
|
+
const flush = (node) => {
|
|
40
|
+
const line = buffer.replace(/\s+/g, " ").trim()
|
|
41
|
+
buffer = ""
|
|
42
|
+
if (line) lines.push({ node, text: line })
|
|
43
|
+
}
|
|
44
|
+
const walk = (node) => {
|
|
45
|
+
for (const child of node.children) {
|
|
46
|
+
if (child.tag === "#text") buffer += child.text
|
|
47
|
+
else if (SKIP.has(child.tag)) continue
|
|
48
|
+
else if (child.tag === "br") flush(node)
|
|
49
|
+
else if (INLINE.has(child.tag)) walk(child)
|
|
50
|
+
else if (PROSE_TAGS.has(child.tag)) {
|
|
51
|
+
flush(node)
|
|
52
|
+
const line = proseText(child)
|
|
53
|
+
if (line) lines.push({ node: child, text: line })
|
|
54
|
+
} else {
|
|
55
|
+
flush(node)
|
|
56
|
+
walk(child)
|
|
57
|
+
flush(child)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
walk(root)
|
|
62
|
+
flush(root)
|
|
63
|
+
const blocks = []
|
|
64
|
+
let at = 0
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
blocks.push({ node: line.node, start: at, end: at + line.text.length })
|
|
67
|
+
at += line.text.length + 1
|
|
68
|
+
}
|
|
69
|
+
return { root, text: lines.map((line) => line.text).join("\n"), blocks }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** text.txt form of an html page. */
|
|
73
|
+
export function snapshotFromHtml(html) {
|
|
74
|
+
return snapshotBlocks(html).text
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Sentence index rows { sid: "S<n>:s<k>", start, end, text }; a form feed (page break) always ends a sentence. */
|
|
78
|
+
export function indexSentences(text, sourceId) {
|
|
79
|
+
return splitSentences(text.replace(/\f/g, "\n")).map((row) => ({
|
|
80
|
+
sid: `${sourceId}:${row.sid}`,
|
|
81
|
+
start: row.start,
|
|
82
|
+
end: row.end,
|
|
83
|
+
text: text.slice(row.start, row.end),
|
|
84
|
+
}))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sentenceIdAt = (sentences, offset) => sentences.find((row) => row.start <= offset && offset < row.end)?.sid ?? null
|
|
88
|
+
|
|
89
|
+
/** "High Bandwidth Memory (HBM)": the capitalized words before the parenthesis, at most one per letter. */
|
|
90
|
+
function nameBefore(text, at, letters) {
|
|
91
|
+
const prefix = text.slice(Math.max(0, at - 160), at)
|
|
92
|
+
if (!/\S[ \t]?$/.test(prefix)) return null
|
|
93
|
+
const words = [...prefix.matchAll(/\S+/g)]
|
|
94
|
+
const picked = []
|
|
95
|
+
let content = 0
|
|
96
|
+
for (let k = words.length - 1; k >= 0 && content < letters; k--) {
|
|
97
|
+
const word = words[k][0]
|
|
98
|
+
if (NAME_WORD.test(word)) content++
|
|
99
|
+
else if (!CONNECTOR.has(word) || picked.length === 0) break
|
|
100
|
+
picked.unshift(words[k])
|
|
101
|
+
}
|
|
102
|
+
while (picked.length > 0 && CONNECTOR.has(picked[0][0])) picked.shift()
|
|
103
|
+
if (picked.length === 0) return null
|
|
104
|
+
const start = at - prefix.length + picked[0].index
|
|
105
|
+
const end = at - prefix.length + picked[picked.length - 1].index + picked[picked.length - 1][0].length
|
|
106
|
+
return { start, expansion: text.slice(start, end) }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Every `Full Name (ACR)` and `ACR (Full Name)` in the text, ordered by position. */
|
|
110
|
+
export function findAcronyms(text, sentences) {
|
|
111
|
+
const entries = []
|
|
112
|
+
for (const match of text.matchAll(ACRONYM_AFTER)) {
|
|
113
|
+
const name = nameBefore(text, match.index, match[1].length)
|
|
114
|
+
if (!name || name.expansion === match[1]) continue
|
|
115
|
+
entries.push({ acronym: match[1], expansion: name.expansion, span: [name.start, match.index + match[0].length] })
|
|
116
|
+
}
|
|
117
|
+
for (const match of text.matchAll(ACRONYM_BEFORE)) {
|
|
118
|
+
const expansion = match[2].trim()
|
|
119
|
+
if (!/\s/.test(expansion) || expansion === expansion.toUpperCase()) continue
|
|
120
|
+
entries.push({ acronym: match[1], expansion, span: [match.index, match.index + match[0].length] })
|
|
121
|
+
}
|
|
122
|
+
return entries
|
|
123
|
+
.sort((a, b) => a.span[0] - b.span[0])
|
|
124
|
+
.map((entry) => ({ acronym: entry.acronym, expansion: entry.expansion, sentence_id: sentenceIdAt(sentences, entry.span[0]), span: entry.span }))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function snapshotText(input) {
|
|
128
|
+
if (Array.isArray(input.pages)) return input.pages.join("\f")
|
|
129
|
+
if (typeof input.text === "string") return input.text
|
|
130
|
+
if (typeof input.html === "string") return snapshotFromHtml(input.html)
|
|
131
|
+
throw new TypeError("writeSnapshot needs text, pages, or html")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function checkInput(sourceId, input) {
|
|
135
|
+
if (!SOURCE_ID.test(String(sourceId))) throw new TypeError(`source id must be S<n>, got ${sourceId}`)
|
|
136
|
+
if (!EXTRACTOR.test(String(input.extractor))) throw new TypeError(`extractor must be tool:source-snapshot@1, model, or extractor:<name>, got ${input.extractor}`)
|
|
137
|
+
if (typeof (input.url ?? input.path) !== "string") throw new TypeError("writeSnapshot needs a url or a path")
|
|
138
|
+
if (input.kind !== undefined && !KINDS.has(input.kind)) throw new TypeError(`kind must be page, command, or document, got ${input.kind}`)
|
|
139
|
+
if (input.provenance !== undefined && !PROVENANCE.has(input.provenance)) throw new TypeError(`provenance must be primary, secondary, or unknown, got ${input.provenance}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Write <sourcesDir>/<sourceId>/ for one source; returns { dir, sha256 } with sha256 over text.txt.
|
|
144
|
+
* input: { url | path, text | pages | html, extractor, observed_at?, revision?, kind?, provenance? }.
|
|
145
|
+
* Re-writing the same text is a no-op change; a different text over an existing snapshot throws
|
|
146
|
+
* snapshot_exists, because ledger rows pin the old hash.
|
|
147
|
+
*/
|
|
148
|
+
export function writeSnapshot(sourcesDir, sourceId, input) {
|
|
149
|
+
checkInput(sourceId, input)
|
|
150
|
+
const text = snapshotText(input)
|
|
151
|
+
const sha256 = sha256Of(Buffer.from(text, "utf8"))
|
|
152
|
+
const dir = join(sourcesDir, sourceId)
|
|
153
|
+
const textPath = join(dir, "text.txt")
|
|
154
|
+
if (existsSync(textPath) && sha256Of(readFileSync(textPath)) !== sha256) {
|
|
155
|
+
throw new Error(`snapshot_exists: ${sourceId} already holds a different text.txt`)
|
|
156
|
+
}
|
|
157
|
+
mkdirSync(dir, { recursive: true })
|
|
158
|
+
writeFileSync(textPath, text)
|
|
159
|
+
if (typeof input.html === "string") writeFileSync(join(dir, "source.html"), input.html)
|
|
160
|
+
const meta = {
|
|
161
|
+
source_id: sourceId,
|
|
162
|
+
url: input.url ?? input.path,
|
|
163
|
+
observed_at: input.observed_at ?? new Date().toISOString(),
|
|
164
|
+
sha256,
|
|
165
|
+
extractor: input.extractor,
|
|
166
|
+
revision: input.revision ?? null,
|
|
167
|
+
kind: input.kind ?? (input.url !== undefined || typeof input.html === "string" ? "page" : "document"),
|
|
168
|
+
provenance: input.provenance ?? "unknown",
|
|
169
|
+
}
|
|
170
|
+
writeFileSync(join(dir, "meta.json"), `${JSON.stringify(meta, null, 2)}\n`)
|
|
171
|
+
const sentences = indexSentences(text, sourceId)
|
|
172
|
+
writeFileSync(join(dir, "sentences.jsonl"), sentences.map((row) => `${JSON.stringify(row)}\n`).join(""))
|
|
173
|
+
const acronyms = { schemaVersion: 1, source_id: sourceId, snapshot_sha256: sha256, entries: findAcronyms(text, sentences) }
|
|
174
|
+
writeFileSync(join(dir, "acronyms.json"), `${JSON.stringify(acronyms, null, 2)}\n`)
|
|
175
|
+
return { dir, sha256 }
|
|
176
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Canonical factor algebra. Exponents are bigint; aliases enter only through normalizeInput.
|
|
2
|
+
import { fromDecimal, mul, sigFigsOf, toDecimal } from "./rational.mjs"
|
|
3
|
+
import { dimensionOf, normalizeUnit } from "./units.mjs"
|
|
4
|
+
import { refuse } from "./unit-errors.mjs"
|
|
5
|
+
export { refuse, RuleError } from "./unit-errors.mjs"
|
|
6
|
+
|
|
7
|
+
const SCALED = new Set(["bp", "basispoint", "basispoints"])
|
|
8
|
+
export const isPercentish = (unit) => unit === "%" || unit === "%p"
|
|
9
|
+
const scaled = (text) => SCALED.has(text.replace(/\s+/g, "").toLowerCase())
|
|
10
|
+
|
|
11
|
+
function atomOf(text) {
|
|
12
|
+
if (text === "1") return null
|
|
13
|
+
const atom = scaled(text) ? "%p" : normalizeUnit(text) ?? text
|
|
14
|
+
if (dimensionOf(atom) === "scale") refuse(`"${text}" is a scale word (${atom}), not a unit`, undefined, "scale_atom", "a counted unit with scale in the numeral")
|
|
15
|
+
return atom
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const combine = (a, b, sign) => {
|
|
19
|
+
for (const [atom, power] of b) a.set(atom, (a.get(atom) ?? 0n) + sign * power)
|
|
20
|
+
return a
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Iterative unit parser: factor maps plus the input's decimal scaling exponent. */
|
|
24
|
+
function parse(raw) {
|
|
25
|
+
if (raw === null) return { factors: new Map(), shift: 0n }
|
|
26
|
+
if (typeof raw !== "string" || !raw.trim()) refuse("unit must be text or explicit null", undefined, "unit_text", "canonical unit text or null")
|
|
27
|
+
const tokens = raw.match(/[*/()^]|[^*/()^]+/g).map((x) => x.trim()).filter(Boolean)
|
|
28
|
+
const frames = [{ factors: new Map(), shift: 0n, sign: 1n, need: true }]
|
|
29
|
+
let affine = false
|
|
30
|
+
const apply = (value, i) => {
|
|
31
|
+
if (tokens[i] === "^") {
|
|
32
|
+
const power = tokens[i + 1]
|
|
33
|
+
if (!/^[+-]?\d+$/.test(power ?? "")) refuse(`unit "${raw}" has a bad exponent`, undefined, "exponent", "an integer exponent")
|
|
34
|
+
const n = BigInt(power)
|
|
35
|
+
value = { factors: new Map([...value.factors].map(([a, p]) => [a, p * n])), shift: value.shift * n }
|
|
36
|
+
i += 2
|
|
37
|
+
}
|
|
38
|
+
const frame = frames.at(-1)
|
|
39
|
+
combine(frame.factors, value.factors, frame.sign)
|
|
40
|
+
frame.shift += frame.sign * value.shift
|
|
41
|
+
frame.need = false
|
|
42
|
+
return i
|
|
43
|
+
}
|
|
44
|
+
for (let i = 0; i < tokens.length;) {
|
|
45
|
+
const frame = frames.at(-1)
|
|
46
|
+
const token = tokens[i]
|
|
47
|
+
if (frame.need) {
|
|
48
|
+
if (token === "(") {
|
|
49
|
+
frames.push({ factors: new Map(), shift: 0n, sign: 1n, need: true })
|
|
50
|
+
i++
|
|
51
|
+
} else {
|
|
52
|
+
if (/^[*/()^]$/.test(token)) refuse(`unexpected "${token}" in unit "${raw}"`)
|
|
53
|
+
const atom = atomOf(token)
|
|
54
|
+
affine ||= dimensionOf(atom) === "temperature"
|
|
55
|
+
i = apply({ factors: new Map(atom === null ? [] : [[atom, 1n]]), shift: scaled(token) ? -2n : 0n }, i + 1)
|
|
56
|
+
}
|
|
57
|
+
} else if (token === ")" && frames.length > 1) {
|
|
58
|
+
const value = frames.pop()
|
|
59
|
+
i = apply(value, i + 1)
|
|
60
|
+
} else if (token === "*" || token === "/") {
|
|
61
|
+
frame.sign = token === "*" ? 1n : -1n
|
|
62
|
+
frame.need = true
|
|
63
|
+
i++
|
|
64
|
+
} else refuse(`unexpected "${token}" in unit "${raw}"`)
|
|
65
|
+
}
|
|
66
|
+
if (frames.length !== 1 || frames[0].need) refuse(`unit "${raw}" ends early or is missing a closing parenthesis`)
|
|
67
|
+
// Refuse affine composites before cancellation loses the atom that made them unsafe.
|
|
68
|
+
if (affine && (tokens.length !== 1 || frames[0].shift !== 0n)) refuse("affine (temperature) units cannot occur in composites", undefined, "affine_units", "a non-affine composite or bare temperature for convert")
|
|
69
|
+
return frames[0]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function format(factors) {
|
|
73
|
+
const side = (sign) => [...factors].filter(([, p]) => sign * p > 0n).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
74
|
+
.map(([a, p]) => { const n = sign * p; return n === 1n ? a : `${a}^${n}` })
|
|
75
|
+
const top = side(1n)
|
|
76
|
+
const bottom = side(-1n)
|
|
77
|
+
if (!top.length && !bottom.length) return null
|
|
78
|
+
return `${top.join("*") || "1"}${bottom.length ? `/${bottom.length === 1 ? bottom[0] : `(${bottom.join("*")})`}` : ""}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const canonicalUnit = (raw) => format(parse(raw).factors)
|
|
82
|
+
export function inputScale(raw) {
|
|
83
|
+
const { shift } = parse(raw)
|
|
84
|
+
if (shift === 0n) return null
|
|
85
|
+
if (shift > 10000n || shift < -10000n) refuse("scaled alias exponent exceeds exact input capacity", undefined, "scale_range", "an absolute decimal scale exponent <= 10000")
|
|
86
|
+
return shift > 0n ? `1${"0".repeat(Number(shift))}` : `0.${"0".repeat(Number(-shift - 1n))}1`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function normalizeInput(unit, value) {
|
|
90
|
+
const canonical = canonicalUnit(unit)
|
|
91
|
+
const factor = inputScale(unit)
|
|
92
|
+
return { unit: canonical, value: factor ? toDecimal(mul(fromDecimal(value), fromDecimal(factor)), sigFigsOf(value)) : value }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const unitTimes = (a, b) => format(combine(parse(a).factors, parse(b).factors, 1n))
|
|
96
|
+
export const unitOver = (a, b) => format(combine(parse(a).factors, parse(b).factors, -1n))
|
|
97
|
+
export const isAffine = (unit) => typeof unit === "string" && dimensionOf(unit) === "temperature"
|
|
98
|
+
export function isCurrencyRate(unit) {
|
|
99
|
+
const currencies = [...parse(unit).factors].filter(([a, n]) => n !== 0n && dimensionOf(a)?.startsWith("currency:"))
|
|
100
|
+
return currencies.some(([, n]) => n > 0n) && currencies.some(([, n]) => n < 0n)
|
|
101
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Classified refusals shared by public derivation boundaries and the registry.
|
|
2
|
+
export class RuleError extends Error {
|
|
3
|
+
constructor(detail, hint, shape = "invalid_input", expected = "a valid ledger and derivation specification") {
|
|
4
|
+
super(detail)
|
|
5
|
+
this.detail = detail
|
|
6
|
+
this.shape = shape
|
|
7
|
+
this.expected = expected
|
|
8
|
+
if (hint) this.hint = hint
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const refuse = (detail, hint, shape, expected) => {
|
|
13
|
+
throw new RuleError(detail, hint, shape, expected)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function refusal(error) {
|
|
17
|
+
return {
|
|
18
|
+
shape: error instanceof RuleError ? error.shape : "invalid_input",
|
|
19
|
+
expected: error instanceof RuleError ? error.expected : "valid JSON fields matching the fact ledger grammar",
|
|
20
|
+
detail: error instanceof Error ? error.message : "invalid input",
|
|
21
|
+
...(error instanceof RuleError && error.hint ? { hint: error.hint } : {}),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Metadata contracts and order-independent JSON equality, shared by schema and algebra.
|
|
2
|
+
import { isDeepStrictEqual } from "node:util"
|
|
3
|
+
import { refuse } from "./unit-errors.mjs"
|
|
4
|
+
|
|
5
|
+
export const METADATA = ["population", "accounting_basis", "snapshot_window", "entity", "valid_at"]
|
|
6
|
+
export const sameDeclared = isDeepStrictEqual
|
|
7
|
+
export const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0
|
|
8
|
+
export const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
9
|
+
|
|
10
|
+
export function metadataErrors(row) {
|
|
11
|
+
return METADATA.filter((field) => {
|
|
12
|
+
const value = row[field]
|
|
13
|
+
return value !== null && !(nonEmpty(value) || (["population", "snapshot_window"].includes(field) && object(value)))
|
|
14
|
+
}).map((field) => `${field} must be explicitly null or valid non-empty metadata`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function checkMetadata(rows, fields) {
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
const errors = metadataErrors(row)
|
|
20
|
+
if (errors.length) refuse(`${row.fact_id}: ${errors.join("; ")}`, undefined, "metadata", "present, type-valid metadata; explicit null when unknown")
|
|
21
|
+
}
|
|
22
|
+
for (const field of fields) {
|
|
23
|
+
if (rows.some((row) => !sameDeclared(row[field], rows[0][field]))) {
|
|
24
|
+
refuse(`${field} differs across ${rows.map((row) => row.fact_id).join(", ")}`, undefined, "metadata", `equal ${field}`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function propagatedMetadata(rows) {
|
|
30
|
+
return Object.fromEntries(METADATA.map((field) => [field, rows.every((row) => sameDeclared(row[field], rows[0][field])) ? structuredClone(rows[0][field]) : null]))
|
|
31
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// unit-pairs.mjs - the typed operand-unit rules shared by the operation registry and expr nodes (references/unit-rules.md).
|
|
2
|
+
// equalUnits and notAffine guard whole operations; minusUnit and plusUnit are the typed-pair rules that diff and sum
|
|
3
|
+
// use and that expr's `-` and `+` nodes follow; inferExpr assigns a canonical unit to every node of an expr tree.
|
|
4
|
+
|
|
5
|
+
import { isAffine, isPercentish, refuse, unitOver, unitTimes } from "./unit-algebra.mjs"
|
|
6
|
+
import { postorder } from "./fact-expr.mjs"
|
|
7
|
+
import { checkMetadata, propagatedMetadata } from "./unit-metadata.mjs"
|
|
8
|
+
|
|
9
|
+
export const show = (unit) => JSON.stringify(unit ?? null)
|
|
10
|
+
export const listed = (rows, units) => rows.map((row, index) => `${row.fact_id} ${units[index] ?? "null"}`).join(", ")
|
|
11
|
+
|
|
12
|
+
export function equalUnits(rows, units) {
|
|
13
|
+
if (units.some((unit) => unit !== units[0])) refuse(`unit differs across ${listed(rows, units)}`)
|
|
14
|
+
return units[0]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function notAffine(op, rows, units) {
|
|
18
|
+
if (units.some(isAffine)) refuse(`${op} refuses affine (temperature) units: ${listed(rows, units)}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The typed-pair rule shared by diff and expr `-`: U-U is U; %-% is %p; %-%p is %; %p-%p is %p; %p-% is refused. */
|
|
22
|
+
export function minusUnit(a, b, where) {
|
|
23
|
+
if (isAffine(a) || isAffine(b)) refuse(`${where}: subtracting affine (temperature) values as absolute values has no meaning`)
|
|
24
|
+
if (a === b) return a === "%" ? "%p" : a
|
|
25
|
+
if (a === "%" && b === "%p") return "%"
|
|
26
|
+
refuse(`${where}: ${show(a)} - ${show(b)} is not a typed pair (U-U, %-%, %-%p, %p-%p)`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function plusUnit(a, b, where) {
|
|
30
|
+
if (isAffine(a) || isAffine(b)) refuse(`${where}: adding affine (temperature) values has no meaning`)
|
|
31
|
+
if (a === b && a === "%") refuse(`${where}: adding % values needs disjoint shares of one reference: use --op sum`)
|
|
32
|
+
if (a === b) return a
|
|
33
|
+
if ((a === "%" && b === "%p") || (a === "%p" && b === "%")) return "%"
|
|
34
|
+
refuse(`${where}: ${show(a)} + ${show(b)} has no common unit`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Unit of every node of an expr tree; refuses when a node's unit cannot be inferred. */
|
|
38
|
+
export function inferExpr(ast, unitsById, rowsById = null) {
|
|
39
|
+
const units = new Map()
|
|
40
|
+
const metadata = new Map()
|
|
41
|
+
for (const node of postorder(ast)) {
|
|
42
|
+
let unit
|
|
43
|
+
if (node.id) unit = unitsById.get(node.id)
|
|
44
|
+
else if (node.literal) unit = null
|
|
45
|
+
else if (node.fn === "pct") {
|
|
46
|
+
unit = units.get(node.arg)
|
|
47
|
+
if (unit !== null) refuse(`pct() takes a dimensionless value, got ${show(unit)}`)
|
|
48
|
+
unit = "%"
|
|
49
|
+
} else {
|
|
50
|
+
const left = units.get(node.left)
|
|
51
|
+
const right = units.get(node.right)
|
|
52
|
+
if (node.op === "+") unit = plusUnit(left, right, "expr")
|
|
53
|
+
else if (node.op === "-") unit = minusUnit(left, right, "expr")
|
|
54
|
+
else {
|
|
55
|
+
if (isAffine(left) || isAffine(right)) refuse(`expr: ${node.op} on affine (temperature) units has no meaning`)
|
|
56
|
+
const fraction = (value) => (isPercentish(value) ? null : value)
|
|
57
|
+
unit = (node.op === "*" ? unitTimes : unitOver)(fraction(left), fraction(right))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
units.set(node, unit)
|
|
61
|
+
if (rowsById) {
|
|
62
|
+
const children = node.id ? [rowsById.get(node.id)] : node.literal ? [] : node.fn ? [metadata.get(node.arg)] : [metadata.get(node.left), metadata.get(node.right)]
|
|
63
|
+
const rows = children.filter(Boolean)
|
|
64
|
+
if (rows.length) {
|
|
65
|
+
const fields = node.op === "+" || node.op === "-" ? ["population", "snapshot_window", "accounting_basis"] : node.op === "/" ? ["population", "snapshot_window"] : []
|
|
66
|
+
checkMetadata(rows, fields)
|
|
67
|
+
metadata.set(node, { fact_id: "expression", ...propagatedMetadata(rows) })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { unit: units.get(ast), units, metadata: metadata.get(ast) }
|
|
72
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Precision is stored independently from display strings; generated fields are rederived on replay.
|
|
2
|
+
import { sigFigsOf, toDecimal } from "./rational.mjs"
|
|
3
|
+
import { refuse } from "./unit-errors.mjs"
|
|
4
|
+
|
|
5
|
+
export const toBig = (r) => ({ num: BigInt(r.num), den: BigInt(r.den) })
|
|
6
|
+
export const toText = (r) => ({ num: String(r.num), den: String(r.den) })
|
|
7
|
+
export const placesOf = (decimal) => (String(decimal).split(".")[1] ?? "").length
|
|
8
|
+
export function terminates(r) {
|
|
9
|
+
let den = r.den
|
|
10
|
+
for (const prime of [2n, 5n]) while (den % prime === 0n) den /= prime
|
|
11
|
+
return den === 1n
|
|
12
|
+
}
|
|
13
|
+
export function precisionOf(row) {
|
|
14
|
+
if (row.precision !== null && row.precision !== undefined) {
|
|
15
|
+
const p = row.precision
|
|
16
|
+
if (!p || typeof p !== "object" || Object.keys(p).sort().join(",") !== "places,sigfigs" || !Number.isSafeInteger(p.places) || p.places < 0 || !Number.isSafeInteger(p.sigfigs) || p.sigfigs < 1) refuse("invalid stored precision", undefined, "precision_field", "{ places: nonnegative integer, sigfigs: positive integer }")
|
|
17
|
+
return p
|
|
18
|
+
}
|
|
19
|
+
return { places: placesOf(row.value), sigfigs: sigFigsOf(row.value) }
|
|
20
|
+
}
|
|
21
|
+
export function resultPrecision(rows, literals, exact, mode) {
|
|
22
|
+
const inputs = [...rows.map(precisionOf), ...literals.map((value) => precisionOf({ value }))]
|
|
23
|
+
const places = Math.min(...inputs.map((p) => p.places))
|
|
24
|
+
const sigfigs = Math.min(...inputs.map((p) => p.sigfigs))
|
|
25
|
+
if (mode === "places") return { places, sigfigs: sigFigsOf(roundPlaces(exact, places)) }
|
|
26
|
+
const rendered = toDecimal(exact, sigfigs)
|
|
27
|
+
return { places: placesOf(rendered), sigfigs }
|
|
28
|
+
}
|
|
29
|
+
export function roundPlaces(r, places) {
|
|
30
|
+
const digits = ((r.num < 0n ? -r.num : r.num) * 10n ** BigInt(places) * 2n + r.den) / (r.den * 2n)
|
|
31
|
+
const text = digits.toString().padStart(places + 1, "0")
|
|
32
|
+
return (r.num < 0n && digits !== 0n ? "-" : "") + (places ? `${text.slice(0, -places)}.${text.slice(-places)}` : text)
|
|
33
|
+
}
|
|
34
|
+
export function renderValue(exact, precision, mode) {
|
|
35
|
+
const plain = terminates(exact) ? toDecimal(exact) : null
|
|
36
|
+
const n = precision[mode]
|
|
37
|
+
if (plain !== null && (mode === "places" ? placesOf(plain) <= n : sigFigsOf(plain) < n)) return plain
|
|
38
|
+
return mode === "places" ? roundPlaces(exact, n) : toDecimal(exact, n)
|
|
39
|
+
}
|
|
40
|
+
export function display(value, unit) {
|
|
41
|
+
const [whole, fraction] = value.split(".")
|
|
42
|
+
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (fraction === undefined ? "" : `.${fraction}`)
|
|
43
|
+
return unit === null ? grouped : unit === "%" ? `${grouped}%` : `${grouped} ${unit}`
|
|
44
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Structural rules used by the operation table, shared by schema and recomputation.
|
|
2
|
+
import { leaves, parseExpr } from "./fact-expr.mjs"
|
|
3
|
+
import { isRational } from "./fact-ledger-kinds.mjs"
|
|
4
|
+
import { canonicalUnit, RuleError } from "./unit-algebra.mjs"
|
|
5
|
+
import { nonEmpty, object } from "./unit-metadata.mjs"
|
|
6
|
+
import { show } from "./unit-pairs.mjs"
|
|
7
|
+
|
|
8
|
+
export const COMMON = ["operands null", "operands empty", "wrong arity", "missing operand id", "invalid rational", "no operation", "repeated operand", "self reference",
|
|
9
|
+
"kind operation", "audit fields", "precision field", "compatibility field", "metadata fields", "noncanonical operand"]
|
|
10
|
+
export const shapes = (...own) => Object.freeze([...COMMON, ...own])
|
|
11
|
+
export const needsContext = (row) => object(row.context) ? null : `${row.operation} rows carry a context object, got ${show(row.context)}`
|
|
12
|
+
export const noContext = (row) => row.context === null ? null : `${row.operation} rows carry no context, got ${show(row.context)}`
|
|
13
|
+
export const repeatedOperand = (ids) => new Set(ids).size !== ids.length ? `operand ${ids.find((id, i) => ids.indexOf(id) !== i)} is repeated: the operands of one row are distinct facts` : null
|
|
14
|
+
|
|
15
|
+
export function sumShape(row) {
|
|
16
|
+
const missing = needsContext(row)
|
|
17
|
+
if (missing) return missing
|
|
18
|
+
const c = row.context
|
|
19
|
+
if (Object.keys(c).sort().join(",") !== "disjoint,reference" || typeof c.disjoint !== "boolean" || !(c.reference === null || nonEmpty(c.reference) || object(c.reference))) return "sum context needs exactly boolean disjoint and null, text, or map reference"
|
|
20
|
+
if (object(c.reference) && (Object.keys(c.reference).sort().join(",") !== [...row.operands].sort().join(",") || !Object.values(c.reference).every(nonEmpty))) return "reference map must name exactly the supplied operands"
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function convertShape(row) {
|
|
25
|
+
const target = object(row.context) ? row.context.target : undefined
|
|
26
|
+
if (!nonEmpty(target)) return needsContext(row) ?? "a convert row carries its target in context.target"
|
|
27
|
+
if (Object.keys(row.context).join(",") !== "target") return "convert context has exactly target"
|
|
28
|
+
try {
|
|
29
|
+
const canonical = canonicalUnit(target)
|
|
30
|
+
return canonical === target ? null : `context.target ${show(target)} is not canonical unit text (canonical: ${show(canonical)})`
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof RuleError) return `context.target ${show(target)} is not unit text: ${error.detail}`
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function exprShape(row) {
|
|
38
|
+
try {
|
|
39
|
+
const ids = leaves(parseExpr(row.formula)).ids
|
|
40
|
+
if (ids.length !== row.operands.length || ids.some((id) => !row.operands.includes(id))) return `formula ids ${ids.join(", ")} do not match operands ${row.operands.join(", ")}`
|
|
41
|
+
return null
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof RuleError) return error.detail
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function rowShape(row, rules) {
|
|
49
|
+
if (!object(row)) return "the row is not an object"
|
|
50
|
+
if (!Object.hasOwn(rules, row.operation)) return `operation ${show(row.operation)} is not one of ${Object.keys(rules).join(", ")}`
|
|
51
|
+
const rule = rules[row.operation]
|
|
52
|
+
const { operands, exact } = row
|
|
53
|
+
if (!Array.isArray(operands) || !operands.length || !operands.every(nonEmpty)) return `operands must be a non-empty array of fact ids, got ${show(operands)}`
|
|
54
|
+
const repeated = repeatedOperand(operands)
|
|
55
|
+
if (repeated) return repeated
|
|
56
|
+
if (operands.includes(row.fact_id)) return `${row.fact_id} cites itself as an operand`
|
|
57
|
+
if (!rule.arity(operands.length)) return `${row.operation} takes ${rule.arityText}, got ${operands.length}`
|
|
58
|
+
if (!isRational(exact)) return `exact must be { num, den } reduced integer strings, got ${show(exact)}`
|
|
59
|
+
if (row.kind !== rule.kind) return `kind ${row.kind} must be ${rule.kind} for ${row.operation}`
|
|
60
|
+
const own = rule.shape(row)
|
|
61
|
+
if (own) return own
|
|
62
|
+
if (!nonEmpty(row.formula) || !Array.isArray(row.derived_from) || row.derived_from.length !== operands.length) return "formula and derived_from are required audit fields"
|
|
63
|
+
return null
|
|
64
|
+
}
|