oh-my-opencode 5.1.45 → 5.1.47
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/.agents/command/publish.md +12 -13
- package/.agents/skills/publish/SKILL.md +12 -13
- package/.opencode/command/publish.md +12 -13
- 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/agents/atlas/agent.d.ts +3 -2
- package/dist/agents/sisyphus/index.d.ts +1 -0
- package/dist/agents/sisyphus/kimi-k3-1.d.ts +9 -0
- package/dist/agents/sisyphus-agent-factory.d.ts +1 -1
- package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
- package/dist/agents/sisyphus-junior/index.d.ts +1 -0
- package/dist/agents/sisyphus-junior/kimi-k3-1.d.ts +6 -0
- package/dist/agents/types.d.ts +2 -2
- package/dist/cli/index.js +91 -163
- package/dist/cli/service-auth/keystore.d.ts +8 -3
- package/dist/cli-node/index.js +91 -163
- package/dist/config-migration/index.d.ts +2 -2
- package/dist/config-migration/migration-plans.d.ts +0 -10
- package/dist/index.js +1784 -1204
- package/dist/skills/ast-grep/AGENTS.md +1 -1
- package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
- package/dist/skills/frontend/ATTRIBUTION.md +2 -2
- package/dist/skills/ultimate-browsing/engine/AGENTS.md +1 -1
- package/dist/skills/ulw-research/AGENTS.md +14 -2
- package/dist/skills/ulw-research/SKILL.md +37 -12
- package/dist/skills/ulw-research/references/deliverable-phase.md +81 -14
- package/dist/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
- package/dist/skills/ulw-research/references/fact-ledger.md +95 -0
- package/dist/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
- package/dist/skills/ulw-research/references/report-gates.md +21 -4
- package/dist/skills/ulw-research/references/report-visualization.md +2 -0
- package/dist/skills/ulw-research/references/unit-rules.md +157 -0
- package/dist/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
- package/dist/skills/ulw-research/scripts/claim-kinds.test.ts +193 -0
- package/dist/skills/ulw-research/scripts/cli-support.mjs +19 -5
- package/dist/skills/ulw-research/scripts/cli-support.test.ts +13 -2
- package/dist/skills/ulw-research/scripts/contracts.mjs +27 -6
- package/dist/skills/ulw-research/scripts/contracts.test.ts +3 -0
- package/dist/skills/ulw-research/scripts/copy-returns-polish.test.ts +209 -0
- package/dist/skills/ulw-research/scripts/copy-returns.test.ts +183 -0
- package/dist/skills/ulw-research/scripts/delivery-barrier.test.ts +245 -0
- package/dist/skills/ulw-research/scripts/fact-commands-meaning.mjs +176 -0
- package/dist/skills/ulw-research/scripts/fact-commands-polish.mjs +127 -0
- package/dist/skills/ulw-research/scripts/fact-commands-verify.mjs +183 -0
- package/dist/skills/ulw-research/scripts/fact-commands.mjs +241 -0
- package/dist/skills/ulw-research/scripts/fact-derive.mjs +66 -0
- package/dist/skills/ulw-research/scripts/fact-derive.test.ts +375 -0
- package/dist/skills/ulw-research/scripts/fact-expr.mjs +108 -0
- package/dist/skills/ulw-research/scripts/fact-fill.mjs +194 -0
- package/dist/skills/ulw-research/scripts/fact-fill.test.ts +221 -0
- package/dist/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +9 -2
- package/dist/skills/ulw-research/scripts/fact-ledger-kinds.mjs +127 -13
- package/dist/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +18 -3
- package/dist/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +160 -0
- package/dist/skills/ulw-research/scripts/fact-ledger-schema.mjs +33 -16
- package/dist/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
- package/dist/skills/ulw-research/scripts/fact-ledger.test.ts +2 -0
- package/dist/skills/ulw-research/scripts/fact-locate.mjs +231 -0
- package/dist/skills/ulw-research/scripts/fact-locate.test.ts +118 -0
- package/dist/skills/ulw-research/scripts/fact-pairs.mjs +185 -0
- package/dist/skills/ulw-research/scripts/fact-pairs.test.ts +151 -0
- package/dist/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
- package/dist/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +409 -0
- package/dist/skills/ulw-research/scripts/fact-unit-rules.test.ts +388 -0
- package/dist/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
- package/dist/skills/ulw-research/scripts/fidelity-binding.test.ts +81 -0
- package/dist/skills/ulw-research/scripts/fidelity-brief.mjs +5 -0
- package/dist/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
- package/dist/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
- package/dist/skills/ulw-research/scripts/fidelity-lexicon.test.ts +117 -0
- package/dist/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
- package/dist/skills/ulw-research/scripts/fidelity-state.mjs +2 -1
- package/dist/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
- package/dist/skills/ulw-research/scripts/fidelity-trace.test.ts +58 -0
- package/dist/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
- package/dist/skills/ulw-research/scripts/fidelity-values.test.ts +270 -0
- package/dist/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
- package/dist/skills/ulw-research/scripts/gates-fidelity.test.ts +73 -0
- package/dist/skills/ulw-research/scripts/gates-static.mjs +28 -8
- package/dist/skills/ulw-research/scripts/html-lite.mjs +2 -2
- package/dist/skills/ulw-research/scripts/html-markup.mjs +182 -0
- package/dist/skills/ulw-research/scripts/md-lite.mjs +193 -0
- package/dist/skills/ulw-research/scripts/md-lite.test.ts +271 -0
- package/dist/skills/ulw-research/scripts/meaning-check.mjs +196 -0
- package/dist/skills/ulw-research/scripts/meaning-check.test.ts +228 -0
- package/dist/skills/ulw-research/scripts/outcome.mjs +6 -5
- package/dist/skills/ulw-research/scripts/placeholder.mjs +184 -0
- package/dist/skills/ulw-research/scripts/placeholder.test.ts +77 -0
- package/dist/skills/ulw-research/scripts/polish-apply.mjs +115 -0
- package/dist/skills/ulw-research/scripts/polish-apply.test.ts +363 -0
- package/dist/skills/ulw-research/scripts/polish-diff.mjs +31 -0
- package/dist/skills/ulw-research/scripts/polish-diff.test.ts +168 -0
- package/dist/skills/ulw-research/scripts/polish-markup.test.ts +187 -0
- package/dist/skills/ulw-research/scripts/polish-project.mjs +125 -0
- package/dist/skills/ulw-research/scripts/polish-rules.mjs +170 -0
- package/dist/skills/ulw-research/scripts/references-fences.test.ts +25 -0
- package/dist/skills/ulw-research/scripts/report-tools-commands.mjs +19 -8
- package/dist/skills/ulw-research/scripts/report-tools.mjs +15 -0
- package/dist/skills/ulw-research/scripts/report-tools.test.ts +545 -2
- package/dist/skills/ulw-research/scripts/source-commands.mjs +140 -0
- package/dist/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
- package/dist/skills/ulw-research/scripts/source-snapshot.test.ts +113 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/assets/share.csv +3 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/gen-legacy-dev.mjs +89 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-basic.json +223 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-legacy-dev.json +859 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-verify.json +420 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/polish-edits.json +12 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/report-bad.html +19 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/report-good.html +39 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/acronyms.json +3 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/meta.json +9 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/sentences.jsonl +7 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/text.txt +7 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4.html +19 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/acronyms.json +1 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/meta.json +9 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/sentences.jsonl +1 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/text.txt +1 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S9.txt +1 -0
- package/dist/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
- package/dist/skills/ulw-research/scripts/unit-errors.mjs +23 -0
- package/dist/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
- package/dist/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
- package/dist/skills/ulw-research/scripts/unit-precision.mjs +44 -0
- package/dist/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
- package/dist/skills/ulw-research/scripts/unit-rules.mjs +173 -0
- package/dist/skills/ulw-research/scripts/units.mjs +4 -4
- package/dist/testing/module-mock-lifecycle.d.ts +1 -1
- package/dist/tui.js +32 -34
- package/package.json +13 -13
- package/packages/lsp-core/src/mcp-protocol-pin.test.ts +29 -0
- package/packages/lsp-core/src/mcp.ts +5 -2
- 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 +23 -14
- 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 +81 -14
- 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 +176 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-polish.mjs +127 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +183 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +241 -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 +185 -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 +28 -19
- 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 +81 -14
- 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-polish.test.ts +209 -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 +176 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-polish.mjs +127 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +183 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +241 -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 +185 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.test.ts +151 -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/references-fences.test.ts +25 -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 +545 -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/polish-edits.json +12 -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
- package/dist/config-migration/dori-section-rename.d.ts +0 -3
|
@@ -80,12 +80,16 @@ export function runLayoutProbe(argv) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function runRepairDecide(argv) {
|
|
83
|
-
const args = parseArgs(argv)
|
|
83
|
+
const args = parseArgs(argv, { repeatable: ["defects"] })
|
|
84
84
|
const statePath = args.required("state")
|
|
85
85
|
const now = nowFrom(args.flags)
|
|
86
|
-
|
|
87
|
-
const defects =
|
|
88
|
-
|
|
86
|
+
// Every --defects file (check result, fidelity record, or bare array) joins one defect list.
|
|
87
|
+
const defects = args.required("defects").flatMap((path) => {
|
|
88
|
+
const input = readJsonFile(path)
|
|
89
|
+
const list = Array.isArray(input) ? input : input.defects
|
|
90
|
+
if (!Array.isArray(list)) throw new CliError(`--defects ${path} must be a JSON array or a check result with a defects array`)
|
|
91
|
+
return list
|
|
92
|
+
})
|
|
89
93
|
const state = existsSync(statePath)
|
|
90
94
|
? deserializeState(readText(statePath))
|
|
91
95
|
: createTracker({
|
|
@@ -122,7 +126,7 @@ function manifestPath(flags) {
|
|
|
122
126
|
return join(sessionDirFrom(flags), outcome.MANIFEST_FILENAME)
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
export function runOutcome(argv) {
|
|
129
|
+
export async function runOutcome(argv) {
|
|
126
130
|
const [sub, ...rest] = argv
|
|
127
131
|
const args = parseArgs(rest, { booleans: ["json"] })
|
|
128
132
|
const file = sub === "state" ? null : manifestPath(args.flags)
|
|
@@ -135,7 +139,7 @@ export function runOutcome(argv) {
|
|
|
135
139
|
}
|
|
136
140
|
case "set": {
|
|
137
141
|
const [format, status] = args.positionals
|
|
138
|
-
if (!format || !status) throw new CliError("usage: outcome set <format> <status> [--path p] [--reason r] [--pages n]")
|
|
142
|
+
if (!format || !status) throw new CliError("usage: outcome set <format> <status> [--path p] [--reason r] [--pages n] [--verified-by fidelity-defects.json] [--rendered-from source]")
|
|
139
143
|
const manifest = outcome.readManifest(file)
|
|
140
144
|
const detail = {}
|
|
141
145
|
if (args.flags.path !== undefined) {
|
|
@@ -144,13 +148,14 @@ export function runOutcome(argv) {
|
|
|
144
148
|
}
|
|
145
149
|
if (args.flags.reason !== undefined) detail.reason = args.flags.reason
|
|
146
150
|
if (args.flags.pages !== undefined) detail.pages = args.int("pages")
|
|
151
|
+
for (const [flag, key] of [["verified-by", "verifiedBy"], ["rendered-from", "renderedFrom"]]) if (args.flags[flag] !== undefined) detail[key] = resolve(args.flags[flag])
|
|
147
152
|
outcome.writeManifest(file, outcome.setDeliverable(manifest, format, status, { ...detail, now: nowFrom(args.flags) }))
|
|
148
153
|
return { json: { command: "outcome set", format, status }, exitCode: 0 }
|
|
149
154
|
}
|
|
150
155
|
case "gate": {
|
|
151
156
|
const [gate, status] = args.positionals
|
|
152
157
|
if (!gate || !status) throw new CliError(`usage: outcome gate <${GATE_NAMES.join("|")}> <${GATE_STATUSES.join("|")}>`)
|
|
153
|
-
outcome.writeManifest(file, outcome.setGate(outcome.readManifest(file), gate, status))
|
|
158
|
+
outcome.writeManifest(file, outcome.setGate(outcome.readManifest(file), gate, status, { reason: args.flags.reason, now: nowFrom(args.flags) }))
|
|
154
159
|
return { json: { command: "outcome gate", gate, status }, exitCode: 0 }
|
|
155
160
|
}
|
|
156
161
|
case "render": {
|
|
@@ -169,11 +174,17 @@ export function runOutcome(argv) {
|
|
|
169
174
|
return { json: { command: "outcome state", state }, summary: `state: ${state}`, exitCode: 0 }
|
|
170
175
|
}
|
|
171
176
|
case "verify": {
|
|
172
|
-
const
|
|
177
|
+
const manifest = outcome.readManifest(file)
|
|
178
|
+
const base = outcome.verifyManifest(manifest)
|
|
179
|
+
const { fidelityProblems } = await import("./fidelity-records.mjs")
|
|
180
|
+
const problems = [...base.problems, ...fidelityProblems(sessionDirFrom(args.flags), manifest)]
|
|
181
|
+
const result = { ok: problems.length === 0, problems }
|
|
173
182
|
return { json: { command: "outcome verify", ...result }, summary: `verify: ${result.ok ? "OK" : `${result.problems.length} problem(s)`}`, exitCode: result.ok ? 0 : 1 }
|
|
174
183
|
}
|
|
175
184
|
case "finish": {
|
|
176
185
|
const manifest = outcome.readManifest(file)
|
|
186
|
+
const refused = (await import("./fidelity-records.mjs")).fidelityProblems(sessionDirFrom(args.flags), manifest)
|
|
187
|
+
if (refused.length > 0) return { json: { command: "outcome finish", refused: true, problems: refused }, summary: `finish: refused (${refused.map((p) => p.code).join(", ")})`, exitCode: 1 }
|
|
177
188
|
const ledgerPath = args.flags.ledger ?? join(sessionDirFrom(args.flags), "sources-ledger.md")
|
|
178
189
|
const now = nowFrom(args.flags)
|
|
179
190
|
const facts = outcome.briefingFacts(manifest, existsSync(ledgerPath) ? readText(ledgerPath) : "", now)
|
|
@@ -20,6 +20,15 @@ export const COMMANDS = Object.freeze([
|
|
|
20
20
|
"outcome finish",
|
|
21
21
|
"outcome briefing",
|
|
22
22
|
"format-extract",
|
|
23
|
+
"fact add",
|
|
24
|
+
"fact quote",
|
|
25
|
+
"fact retract",
|
|
26
|
+
"fact export",
|
|
27
|
+
"fact fill",
|
|
28
|
+
"fact verify",
|
|
29
|
+
"fact derive",
|
|
30
|
+
"source snapshot",
|
|
31
|
+
"claims index",
|
|
23
32
|
])
|
|
24
33
|
|
|
25
34
|
export async function run(argv) {
|
|
@@ -36,6 +45,12 @@ export async function run(argv) {
|
|
|
36
45
|
return runOutcome(rest)
|
|
37
46
|
case "format-extract":
|
|
38
47
|
return runFormatExtract(rest)
|
|
48
|
+
case "fact":
|
|
49
|
+
return (await import("./fact-commands.mjs")).runFact(rest)
|
|
50
|
+
case "source":
|
|
51
|
+
return (await import("./source-commands.mjs")).runSource(rest)
|
|
52
|
+
case "claims":
|
|
53
|
+
return (await import("./source-commands.mjs")).runClaims(rest)
|
|
39
54
|
case "--help":
|
|
40
55
|
case "help":
|
|
41
56
|
if (rest.includes("--json")) return { json: { commands: COMMANDS }, exitCode: 0 }
|
|
@@ -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
|
+
}
|