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,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
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// The sole per-operation registry: guards, context, arithmetic, precision, audit and compatibility.
|
|
2
|
+
import { evaluate, leaves } from "./fact-expr.mjs"
|
|
3
|
+
import { add, div, mul, sub, toDecimal } from "./rational.mjs"
|
|
4
|
+
import { canonicalUnit, inputScale, isAffine, isCurrencyRate, isPercentish, refuse, RuleError, unitOver } from "./unit-algebra.mjs"
|
|
5
|
+
import { checkMetadata, nonEmpty, object, propagatedMetadata } from "./unit-metadata.mjs"
|
|
6
|
+
import { equalUnits, inferExpr, listed, minusUnit, notAffine, show } from "./unit-pairs.mjs"
|
|
7
|
+
import { display, terminates, toBig } from "./unit-precision.mjs"
|
|
8
|
+
import { convertShape, exprShape, noContext, repeatedOperand, rowShape, shapes, sumShape } from "./unit-rules-shapes.mjs"
|
|
9
|
+
import { convertFactor, dimensionOf } from "./units.mjs"
|
|
10
|
+
export { canonicalUnit, inferExpr, isAffine, RuleError }
|
|
11
|
+
|
|
12
|
+
const EXPR_HINT = "currencies never convert by table: add a dated rate row and use --op expr"
|
|
13
|
+
const compare = (a, b) => a.num * b.den - b.num * a.den
|
|
14
|
+
const SNAPSHOT = ["population", "snapshot_window"]
|
|
15
|
+
const ADDITIVE = [...SNAPSHOT, "accounting_basis"]
|
|
16
|
+
const pair = (a, b) => (spec) => [spec[a], spec[b]]
|
|
17
|
+
const listOf = (spec) => spec.operands
|
|
18
|
+
const generated = (exact, formula, factor = null) => ({ exact, formula, factor })
|
|
19
|
+
const idsOf = (rows) => rows.map((r) => r.fact_id)
|
|
20
|
+
const valuesOf = (rows) => rows.map((r) => toBig(r.exact))
|
|
21
|
+
const ordinary = {
|
|
22
|
+
kind: "derived", compatibility: "checked", context: () => null, shape: noContext,
|
|
23
|
+
precision: () => "sigfigs", render: (shown) => shown, metadataResult: propagatedMetadata,
|
|
24
|
+
}
|
|
25
|
+
const binary = { ...ordinary, arity: (n) => n === 2, arityText: "two operands" }
|
|
26
|
+
|
|
27
|
+
function sharedReference(rows, reference) {
|
|
28
|
+
if (nonEmpty(reference)) return
|
|
29
|
+
if (object(reference)) {
|
|
30
|
+
const named = rows.map((r) => reference[r.fact_id])
|
|
31
|
+
if (Object.keys(reference).length === rows.length && named.every(nonEmpty) && named.every((v) => v === named[0])) return
|
|
32
|
+
refuse("% operands name different references or missing/extra ids", undefined, "reference", "one denominator for exactly the operands")
|
|
33
|
+
}
|
|
34
|
+
refuse("summing % operands needs one shared reference (denominator): pass reference")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Registry rows own every operation-specific choice; consumers only dispatch through these rows. */
|
|
38
|
+
export const OPERATION_RULES = Object.freeze({
|
|
39
|
+
sum: Object.freeze({
|
|
40
|
+
...ordinary, arity: (n) => n >= 2, arityText: "two or more operands", operandIds: listOf,
|
|
41
|
+
inputFields: ["operands", "disjoint", "reference"],
|
|
42
|
+
context: (s) => ({ disjoint: Object.hasOwn(s, "disjoint") ? s.disjoint : false, reference: Object.hasOwn(s, "reference") ? s.reference : null }),
|
|
43
|
+
metadata: ADDITIVE, precision: () => "places", shape: sumShape,
|
|
44
|
+
resultUnit(rows, units, context) {
|
|
45
|
+
notAffine("sum", rows, units)
|
|
46
|
+
const unit = equalUnits(rows, units)
|
|
47
|
+
if (unit === "%") {
|
|
48
|
+
if (context.disjoint !== true) refuse("summing % operands needs disjoint shares: pass disjoint")
|
|
49
|
+
sharedReference(rows, context.reference)
|
|
50
|
+
if (!nonEmpty(context.rationale)) refuse("summing % operands needs a rationale for the disjoint shares")
|
|
51
|
+
const values = valuesOf(rows)
|
|
52
|
+
if (values.some((r) => r.num < 0n || r.num > 100n * r.den) || compare(values.reduce(add), { num: 100n, den: 1n }) > 0n) refuse("disjoint shares must each be in [0, 100] and total <= 100", undefined, "share_total", "valid disjoint shares totaling at most 100%")
|
|
53
|
+
}
|
|
54
|
+
return unit
|
|
55
|
+
},
|
|
56
|
+
calculate: (rows) => generated(valuesOf(rows).reduce(add), idsOf(rows).join(" + ")),
|
|
57
|
+
malformed: shapes("no context", "% without disjoint", "% without reference", "% without rationale", "unequal units", "affine units", "closed context", "share total"),
|
|
58
|
+
}),
|
|
59
|
+
diff: Object.freeze({
|
|
60
|
+
...binary, operandIds: listOf, metadata: ADDITIVE, precision: () => "places",
|
|
61
|
+
inputFields: ["operands"],
|
|
62
|
+
resultUnit: (rows, units) => minusUnit(units[0], units[1], `diff ${listed(rows, units)}`),
|
|
63
|
+
calculate: (rows) => generated(sub(...valuesOf(rows)), idsOf(rows).join(" - ")),
|
|
64
|
+
malformed: shapes("%p - % pair", "unit - other unit pair", "affine units", "stray context"),
|
|
65
|
+
}),
|
|
66
|
+
growth: Object.freeze({
|
|
67
|
+
...binary, arityText: "two operands (from, to)", operandIds: pair("from", "to"), metadata: ["population", "accounting_basis"],
|
|
68
|
+
inputFields: ["from", "to"],
|
|
69
|
+
resultUnit(rows, units) {
|
|
70
|
+
const [from, to] = rows
|
|
71
|
+
notAffine("growth", rows, units)
|
|
72
|
+
equalUnits(rows, units)
|
|
73
|
+
if (!nonEmpty(from.entity) || from.entity !== to.entity) refuse(`growth needs one declared entity: ${from.entity} vs ${to.entity}`)
|
|
74
|
+
if (!nonEmpty(from.valid_at) || !nonEmpty(to.valid_at) || from.valid_at === to.valid_at) refuse(`growth needs two different valid_at periods: ${from.valid_at} vs ${to.valid_at}`)
|
|
75
|
+
if (toBig(from.exact).num <= 0n) refuse(`growth needs a positive starting value in ${from.fact_id}`)
|
|
76
|
+
return "%"
|
|
77
|
+
},
|
|
78
|
+
calculate(rows) { const [a, b] = valuesOf(rows); const [x, y] = idsOf(rows); return generated(mul(div(sub(b, a), a), 100n), `(${y} - ${x}) / ${x} * 100`) },
|
|
79
|
+
malformed: shapes("affine units", "unequal units", "entity mismatch", "one period", "from <= 0", "stray context"),
|
|
80
|
+
}),
|
|
81
|
+
share: Object.freeze({
|
|
82
|
+
...binary, arityText: "two operands (part, whole)", operandIds: pair("part", "whole"), metadata: SNAPSHOT,
|
|
83
|
+
inputFields: ["part", "whole"],
|
|
84
|
+
resultUnit(rows, units) {
|
|
85
|
+
notAffine("share", rows, units)
|
|
86
|
+
equalUnits(rows, units)
|
|
87
|
+
const [part, whole] = valuesOf(rows)
|
|
88
|
+
if (whole.num <= 0n) refuse(`share needs a positive whole in ${rows[1].fact_id}`)
|
|
89
|
+
if (part.num < 0n || compare(part, whole) > 0n) refuse(`share needs 0 <= part <= whole: ${rows[0].fact_id} is outside [0, ${rows[1].fact_id}]`)
|
|
90
|
+
return "%"
|
|
91
|
+
},
|
|
92
|
+
calculate: (rows) => generated(mul(div(...valuesOf(rows)), 100n), `${idsOf(rows).join(" / ")} * 100`),
|
|
93
|
+
malformed: shapes("affine units", "unequal units", "whole <= 0", "part < 0", "part above whole", "stray context"),
|
|
94
|
+
}),
|
|
95
|
+
ratio: Object.freeze({
|
|
96
|
+
...binary, arityText: "two operands (num, den)", operandIds: pair("num", "den"), metadata: SNAPSHOT,
|
|
97
|
+
inputFields: ["num", "den"],
|
|
98
|
+
resultUnit(rows, units) {
|
|
99
|
+
notAffine("ratio", rows, units)
|
|
100
|
+
if (toBig(rows[1].exact).num === 0n) refuse(`ratio denominator ${rows[1].fact_id} is zero`)
|
|
101
|
+
if (units[0] === units[1]) return null
|
|
102
|
+
if (units.some(isPercentish)) refuse(`a ratio over % or %p needs equal units: ${listed(rows, units)}; use expr`)
|
|
103
|
+
return unitOver(units[0], units[1])
|
|
104
|
+
},
|
|
105
|
+
calculate: (rows) => generated(div(...valuesOf(rows)), idsOf(rows).join(" / ")),
|
|
106
|
+
malformed: shapes("affine units", "divides by zero", "stray context"),
|
|
107
|
+
}),
|
|
108
|
+
convert: Object.freeze({
|
|
109
|
+
...ordinary, kind: "converted", arity: (n) => n === 1, arityText: "one operand", operandIds: listOf, metadata: [],
|
|
110
|
+
inputFields: ["operands", "to"],
|
|
111
|
+
context: (s) => ({ target: canonicalUnit(s.to) }), shape: convertShape,
|
|
112
|
+
defaultUnit: (s) => s.to,
|
|
113
|
+
resultUnit(rows, units, context) {
|
|
114
|
+
if (!nonEmpty(context.target)) refuse("convert needs a target unit")
|
|
115
|
+
const [from] = units
|
|
116
|
+
const target = context.target
|
|
117
|
+
if (from === target) refuse(`${rows[0].fact_id} is already in ${target}`)
|
|
118
|
+
if ([from, target].some((u) => dimensionOf(u)?.startsWith("currency:"))) refuse(`${rows[0].fact_id} in ${show(from)} has no table conversion to ${target} (a currency)`, EXPR_HINT)
|
|
119
|
+
if (!convertFactor(from, target)) refuse(`${rows[0].fact_id} in ${show(from)} has no table conversion to ${target}`)
|
|
120
|
+
return target
|
|
121
|
+
},
|
|
122
|
+
calculate(rows, context) {
|
|
123
|
+
const row = rows[0]
|
|
124
|
+
const f = convertFactor(row.unit, context.target)
|
|
125
|
+
const scale = { num: f.num, den: f.den }
|
|
126
|
+
const text = terminates(scale) ? toDecimal(scale) : `${scale.num}/${scale.den}`
|
|
127
|
+
return generated(add(mul(toBig(row.exact), scale), f.offset ?? 0n), `${row.fact_id} * ${text}${f.offset ? ` + ${f.offset.num}/${f.offset.den}` : ""}`, terminates(scale) ? text : null)
|
|
128
|
+
},
|
|
129
|
+
render: (shown, rows) => `${shown} (${display(rows[0].value, rows[0].unit)})`,
|
|
130
|
+
malformed: shapes("no context", "empty target", "alias target", "currency target", "no factor to target", "closed context"),
|
|
131
|
+
}),
|
|
132
|
+
expr: Object.freeze({
|
|
133
|
+
...ordinary, arity: (n) => n >= 1, arityText: "one or more operands", compatibility: "unchecked",
|
|
134
|
+
expression: true, operandIds: (s, ast) => Object.hasOwn(s, "operands") ? s.operands : leaves(ast).ids,
|
|
135
|
+
inputFields: ["formula", "operands"],
|
|
136
|
+
metadata: [], precision: (ast) => ast?.op === "+" || ast?.op === "-" ? "places" : "sigfigs",
|
|
137
|
+
resultUnit(rows, units, _context, ast) {
|
|
138
|
+
for (const r of rows) if (isCurrencyRate(r.unit) && !nonEmpty(r.valid_at)) refuse(`FX rate ${r.fact_id} needs a dated valid_at; date compatibility remains unchecked`, EXPR_HINT, "undated_rate", "a dated FX rate row")
|
|
139
|
+
const inferred = inferExpr(ast, new Map(rows.map((r, i) => [r.fact_id, units[i]])), new Map(rows.map((r) => [r.fact_id, r]))).unit
|
|
140
|
+
notAffine("expr", rows, units)
|
|
141
|
+
return inferred
|
|
142
|
+
},
|
|
143
|
+
calculate(rows, _context, ast, formula) {
|
|
144
|
+
const { units } = inferExpr(ast, new Map(rows.map((r) => [r.fact_id, r.unit])))
|
|
145
|
+
return generated(evaluate(ast, new Map(rows.map((r) => [r.fact_id, toBig(r.exact)])), units), formula.trim())
|
|
146
|
+
},
|
|
147
|
+
shape: (row) => noContext(row) ?? exprShape(row),
|
|
148
|
+
malformed: shapes("formula id mismatch", "parse error", "divides by zero", "uninferable unit", "affine sum", "stray context", "undated rate"),
|
|
149
|
+
}),
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
export function ruleOf(op) {
|
|
153
|
+
if (!Object.hasOwn(OPERATION_RULES, op)) refuse(`unknown operation ${String(op)}; use one of ${Object.keys(OPERATION_RULES).join(", ")}`, undefined, "operation", Object.keys(OPERATION_RULES))
|
|
154
|
+
return OPERATION_RULES[op]
|
|
155
|
+
}
|
|
156
|
+
export function operandUnit(row) {
|
|
157
|
+
if (canonicalUnit(row.unit) !== row.unit || inputScale(row.unit)) refuse(`${row.fact_id} unit must be canonical stored text`, undefined, "noncanonical_operand", { unit: canonicalUnit(row.unit) })
|
|
158
|
+
return row.unit
|
|
159
|
+
}
|
|
160
|
+
export function expectedUnit(op, rows, { ast = null, context = null } = {}) {
|
|
161
|
+
const rule = ruleOf(op)
|
|
162
|
+
const repeated = repeatedOperand(idsOf(rows))
|
|
163
|
+
if (repeated) refuse(repeated, undefined, "repeated_operand", "distinct operand ids")
|
|
164
|
+
if (!rule.arity(rows.length)) refuse(`${op} takes ${rule.arityText}, got ${rows.length}`, undefined, "arity", rule.arityText)
|
|
165
|
+
const units = rows.map(operandUnit)
|
|
166
|
+
checkMetadata(rows, rule.metadata)
|
|
167
|
+
return rule.resultUnit(rows, units, context ?? {}, ast)
|
|
168
|
+
}
|
|
169
|
+
export function renderPrecision(op, unit, ast = null) {
|
|
170
|
+
return unit === "%p" ? "places" : ruleOf(op).precision(ast)
|
|
171
|
+
}
|
|
172
|
+
export const unitMismatch = (op, rows, unit, expected) => `unit ${show(unit)} does not follow from ${op} over ${rows.map((r) => `${r.fact_id} ${r.unit ?? "null"}`).join(", ")}: expected unit ${show(expected)}`
|
|
173
|
+
export const malformed = (row) => rowShape(row, OPERATION_RULES)
|
|
@@ -10,10 +10,10 @@ import { div, fromDecimal, sub } from "./rational.mjs"
|
|
|
10
10
|
|
|
11
11
|
/** [token, dimension, factor, reading, aliases, offset?]; the Korean reading is noted per row. */
|
|
12
12
|
const TABLE = [
|
|
13
|
-
["mm", "length", "0.001", "open", ["millimeter", "millimeters", "\uBC00\uB9AC\uBBF8\uD130"]], // millimiteo
|
|
14
|
-
["cm", "length", "0.01", "open", ["centimeter", "centimeters", "\uC13C\uD2F0\uBBF8\uD130"]], // sentimiteo
|
|
13
|
+
["mm", "length", "0.001", "open", ["millimeter", "millimeters", "millimetre", "millimetres", "\uBC00\uB9AC\uBBF8\uD130"]], // millimiteo
|
|
14
|
+
["cm", "length", "0.01", "open", ["centimeter", "centimeters", "centimetre", "centimetres", "\uC13C\uD2F0\uBBF8\uD130"]], // sentimiteo
|
|
15
15
|
["m", "length", "1", "open", ["meter", "meters", "metre", "metres", "\uBBF8\uD130"]], // miteo
|
|
16
|
-
["km", "length", "1000", "open", ["kilometer", "kilometers", "\uD0AC\uB85C\uBBF8\uD130"]], // killomiteo
|
|
16
|
+
["km", "length", "1000", "open", ["kilometer", "kilometers", "kilometre", "kilometres", "\uD0AC\uB85C\uBBF8\uD130"]], // killomiteo
|
|
17
17
|
["g", "mass", "1", "closed", ["gram", "grams", "\uADF8\uB7A8"]], // geuraem
|
|
18
18
|
["kg", "mass", "1000", "closed", ["kilogram", "kilograms", "\uD0AC\uB85C\uADF8\uB7A8"]], // killogeuraem
|
|
19
19
|
["t", "mass", "1000000", "closed", ["ton", "tons", "tonne", "tonnes", "\uD1A4"]], // ton
|
|
@@ -65,7 +65,7 @@ for (const [token, dimension, factor, reading, aliases, offset] of TABLE) {
|
|
|
65
65
|
for (const alias of [token, ...aliases]) {
|
|
66
66
|
const key = squash(alias)
|
|
67
67
|
EXACT.set(key, entry)
|
|
68
|
-
if (key.length >
|
|
68
|
+
if (alias !== token && key.length > 2 && /^[a-z]+$/.test(key) && !WORD.has(key.toLowerCase())) WORD.set(key.toLowerCase(), entry)
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|