Repository navigation
fix(spec): economics refuses, never throws, on a number the canonical form cannot write (D5, #359) - #569
Merged
Conversation
… form cannot write (D5) Under the evidence profile's D5 (#359), canonicalize refuses a number that is not finite or whose magnitude is 2^53 or more, so hashing a body that holds one throws. Three economics paths hashed such input before checking it, and threw out where they used to refuse: - compileEconomics hashed each options schedule inside the options check, before rule 5. With thresholdCents 2^53, scale 1e16 or 2^53, decayPerSecond 1e21, or scale Infinity it threw NonCanonicalValueError. - clausesFromCompositionManifest hashed the manifest before any check. With groupBps 2^53 or an unknown field of 1e21 it threw. - splitsFromTrainingManifest: the schema leaves dataPointCount unbounded, so 2^53 threw in the hash. The fix holds with or without D5, so it merges in either order with #359: - rule 5: scale and decayPerSecond must also have magnitude at most 2^53-1, D5's number domain. A larger real is an integer outside the safe range, which has no canonical form. - compile: the schedule hash is guarded. A body with no canonical form is a SCHEMA_INVALID options refusal. - adapters: numberWithoutCanonicalForm refuses such a manifest before it is hashed (MANIFEST_INVALID), and the hash itself is guarded. - tests: - round 3b's sealed() uses a stand-in hash when the body has none; - the toString test keeps only reals that both forms write; - new tests make the hash throw as D5's does, so the guards are exercised in either order. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ID for numbers with no canonical form (D5) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, hashes and pays that copy (astra EC5) Each finding below was reproduced at 2832a1c first, with the new tests in "astra EC5" (economics-d5.test.ts) and a trace. - H1. The composition adapter hashed manifest.entries and then read it again to pay. An entries getter that returned Alice's entries on the first read and Bob's on the second gave ok: true with a clause paying bob under Alice's hash. The training adapter had the same flaw: a datasets getter paid d2 under ds:1's hash. - M1. Two throw paths sat outside the guard: - a manifestHash of null threw a TypeError at .toLowerCase(); - a throwing datasets getter escaped TrainingManifestSchema.safeParse. - M2. The number check stopped silently at depth 64, so behavior differed with and without D5. 65 nested objects ending in 2^53 converted before D5 and were refused under it. The fix: - snapshotJson takes { canonical: true }. It copies only what the canonical form writes: an undefined object member is dropped, as canonicalize drops it, and a number that is not finite or above 2^53-1 is refused. The compiler's default is unchanged. numberWithoutCanonicalForm is removed. - All three adapters copy their whole input once. An accessor (never called), a cycle, nesting past 64, a throwing proxy trap or such a number is INPUT_INVALID (GRAPH_INVALID for a graph), the same with or without D5. - Composition checks the copy against CompositionManifestSchema plus the input fields, so manifestHash null is MANIFEST_INVALID. It hashes the copy as written and pays only the parsed copy. - Training checks each lineage level's shape before use, then schema-checks, hashes and pays the copy. - The graph adapter also reads usedComponents once, through Set.prototype.values, and refuses anything that is not a Set (INPUT_INVALID). Each adapter refuses an idPrefix that is not a string. - The L7 test now expects MANIFEST_INVALID for the "__proto__" address, because the schema refuses it before any lookup. A real address missing from the table is still UNKNOWN_CONTRIBUTOR. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…en they refuse, and that they do not throw (astra EC5) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…ll CI run (steward #6340) A plain merge: its tree equals git merge-tree of 725635b and master. No economics file is edited; economics' own #569 fixed D5 there (#6244). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Economics refuses a number the canonical form cannot write, and never throws on one. This answers the semantic conflict between #359 (evidence's D5) and the merged #360. It works in either order with #359 and #566, and #359 merges after it (board v3.461, row N15).
The problem
Under D5 (#359, evidence profile v1 §1),
canonicalizerefuses two kinds of number: one that is not finite, and one whose magnitude is 2^53 or more. Hashing a body that holds one throws. Three economics paths hashed their input before checking it.#359's CI. #359's red build-and-test (run 37157738930, job 111304576433) fails only
economics-review.test.ts, in three "clean-room round 3b" tests. #566 fixes R8 only. Locally, mastercd9d8770+ #359725635b3+ #566f1a0d6ffstill fails those three.Product. These were traced on master + #359 + #566 and compared with master.
compileEconomicsthrewNonCanonicalValueErrorout of the compiler. Each schedule inoptions.schedulesis hashed inside the options check (compile.ts:89), before rule 5 runs. Each input below was a refusal or compiled before D5:thresholdCents2^53scale1e16scale2^53decayPerSecond1e21scaleInfinityclausesFromCompositionManifestthrew because it hashes the manifest before any check:groupBps2^53 was GROUP_WEIGHTS_INVALID before D5;splitsFromTrainingManifestthrew because the schema leavesdataPointCountunbounded: 2^53 was accepted before D5. These adapters are called from contracts'licensing-engine.tsandpayouts.ts.Not affected. The agreement, license and kit hashes are safe: every economics field is required and safe-bounded, with no optional members.
bind.tsparses server facts before it canonicalizes them. The gateway's publish route runs rule 5 and then a guarded hash, so it answers 400. The dashboard's publish page hashes inside try blocks.The fix
Every change holds with or without D5 in
canonicalize.assertScheduleIsWellFormed):scaleanddecayPerSecondmust be finite and have magnitude at most 2^53−1, which is D5's number domain. A larger real is an integer outside the safe range, so it has no canonical form. The registry's publish route uses the same check.SCHEMA_INVALIDoptions refusal.numberWithoutCanonicalForm(economics/input.ts) refuses such a manifest before it is hashed (MANIFEST_INVALID), and the hash itself is guarded.Tests
sealed()uses a stand-in hash when the body has none, because the compiler must refuse whatever hash it carries.economics-d5.test.ts, mocks each hash to throw the way D5's does, so the guards are exercised in either order. It also covers the adapters' number check and the helper.🤖 Generated with Claude Code