# Contract v3.11: changes from v3.10

*W-contract-v311-board-flow-exact, 2026-09-29 01:39 Beirut onward. Brief: `briefs/W-contract-v311-board-flow-exact.md` (Sprint V25 of `SPRINT-CHAIN-2026-09-27.md`, the carried Still open item 1 of `CHANGES-v3.10.md`). The last two USD products in the contract that still ran in Python's default 28-digit context, `flow_from_swaps`'s interval totals and `board.entry_cap_at`, now run under `_EXACT`, and `board.entry_cap_at` gets v3.10's null rule for `assumedSupply`. Neither is reached on a real document today (no flow interval on disk carries a USD product; Board has no real rows), so no page is wrong tonight; both become reachable when B10's writer or Board's read model carries a real USD row. One rule applied twice, three vectors, parity, `legacy-v310/` frozen, no schema change, no new display string, no stamp move. This is not a release.*

## How to check this

```
cd /home/botbox/.openclaw/workspace/projects/caverio/companion-2026-09-26
python3 contract/validate.py                     # exit 0: 192 passed, 0 failed (contract/VALIDATE.out; v3.10: 187)
python3 contract/validate.py --v310-compat       # exit 1 by design: V106 and V107 fail on v3.10 (V107 raises); the pin V108 passes on both
python3 contract/validate.py --v39-compat        # exit 1 by design, byte-identical to v3.10's output (and --v38, --v37, --v36, --v34, --v33, --v32, --v31, --v3)
python3 contract/validate.py --doc build-7/data/real/solana-now.json      # exit 0, document: OK, output byte-identical to v3.10's
python3 contract/validate.py --doc build-7/data/real/bsc-now.json         # exit 0, the same
python3 contract/validate.py --doc build-8/data/real/solana-now.json      # exit 0, the same
python3 contract/validate.py --doc build-8b/data/real/solana-live-now.json  # exit 0, the same (build-8b/CHECKS.md ends Status: DONE)
node contract/js/parity-v311.mjs                 # exit 0: 37 of 37, 12 of 12, 4 of 4, rail 61 of 61 twice, entry cap 11 of 11; writes contract/PARITY-v311.out
node contract/js/parity-v310.mjs                 # exit 0, PARITY-v310.out byte-identical after the rerun
node contract/js/parity-v39.mjs                  # exit 0 (it rewrites PARITY-v39.out in two lines, as at v3.10; restored, see Verification)
node contract/js/parity-v38.mjs                  # exit 0, PARITY-v38.out byte-identical after the rerun
python3 build-8/scripts/defect-exact-product.py && node build-8/scripts/defect-exact-product.mjs
                                                 # output byte-identical to the same scripts on v3.10
(cd contract/legacy-v310 && sha256sum -c SHA256) # 16 of 16 OK
find build-* design-pass* -newer contract/legacy-v310/SHA256 -type f   # no output
```

## Freeze and hashes

"Before" means v3.10 exactly as it stood at 01:40, after the precondition check (`CHANGES-v3.11.md` absent; `pgrep -af 'run-worker[.]sh' | grep -c -E 'build8b|build9|build10'` printed 0) and before the first edit. `legacy-v310/` holds the files `legacy-v39/` holds, in the same naming: `nowat_v310.py`, `clocks_v310.py`, `adapter_v310.py`, `schemas_v310.py`, `projection_v310.py`, `coverage_v310.py`, `board_v310.py`, `validate_v310.py`, `vectors_v310.py`, `companion-v310.schema.json`, `now-v310.schema.json`, `COMPANION-CONTRACT-v310.md`, `state-vectors-v310.json`, `now-v310.json` (the v3.10 `fixtures/now.json`), `VALIDATE-v310.out` (the v3.10 `VALIDATE.out`) and `nowat-v310.js` (the v3.10 `js/nowat.js`). `legacy-v310/SHA256` lists all 16, relative to that folder. Every hash equals the one `CHANGES-v3.10.md` states for the file it landed: nowat `12b750e9…`, JS reader `107e6e7b…`, contract `e57002ce…`, vectors `926fe3b3…`, VALIDATE `c5925901…`, validate `67fbb8f3…`, vectors.py `d12f9507…`; clocks `d56ef6b8…` and board `14420672…` unchanged since v3.4 and v3.5; schemas `aeefae9c…` and `7ebe24dd…` (v3.7's, unchanged).

A v3.11 vector counts only if it passes on v3.11 **and** fails on v3.10, except a pin, which must pass on both. A raise in the version before is a failure, not a crash: `run_now_vector` catches it per case and reports it (`InvalidOperation [<class 'decimal.ConversionSyntax'>]`), as V103 was counted. To run `entry_cap_at` on v3.10, `validate.py` now loads `legacy-v310/board_v310.py` bound to `legacy-v310/nowat_v310.py` and `clocks_v310.py` (`BD310`); earlier legacy runs still refuse the op (`v3.x defines no entry cap callable`), so no older compat output moves. V01 to V105 are byte-equal to v3.10 (`v310-vectors-unchanged`).

## Item 1. `flow_from_swaps` totals under `_EXACT`

| | File and line (v3.11) | Before (v3.10) | After (v3.11) |
|---|---|---|---|
| **Python** | `nowat.py:399-400` | `tot = {side: sum((Decimal(s["quoteAmount"]) * Decimal(s["priceUsd"]) ...), Decimal(0)) ...}` in the default context: each product rounded to 28 significant digits, and each running sum too | the same line inside `with localcontext(_EXACT):` (`nowat.py:82` imports `localcontext`): every product and every sum exact; only `_cents` rounds, once, to the cent (half-up), as §7.7 always said ("exact decimal products, summed, then to the cent") |
| **JavaScript** | none | no flow writer exists in `contract/js/` or in any build (`grep flowFromSwaps` finds nothing; `nowat.js` has `flowNet` and `flowAt`, which read stored cents and multiply nothing) | unchanged. Parity for this function starts when a JavaScript flow writer exists; `parity-v311.mjs` prints V106 from the Python side only and says so |

**Where the contexts differ.** An interval total differs only when an exact product (or sum) has more than 28 significant digits and its 28-digit rounding crosses a half cent. Constructed in V106: a buy of `0.25` SOL at `priceUsd` `479.77999999999999999999999999999992` (a 35-digit price, constructed) is exactly `119.94499999999999999999999999999998` USD, 35 significant digits. Exact, it is 119.94. The default context rounds the product to `119.9450000000000000000000000` and `_cents` half-up gives **119.95**: v3.10 wrote a cent more than the swap was worth.

**No real interval can differ today.** On build-7's `real/solana-now.json` 3,338 of 3,374 flow intervals carry USD and every one is a read zero (`swaps` 0, `buyUsd` and `sellUsd` 0; the other 36 are null with `usd conversion unavailable`); on `real/bsc-now.json` 105 of 1,673 carry USD, all read zeros. build-8's and build-8b's Solana documents carry no USD interval at all (0 of 3,438 and 0 of 3,458). So no product is formed on any real document. The documents carry no swap list, so the widest product formed from real inputs anywhere in the contract is the price-read product: build-7 Solana's widest native amount `0.00022630911706637922` x the pinned quote `119.94072152604973` = `0.0271436787888647784355248007586106`, **33 significant digits** (BSC: `0.0000010067114145475534` x the same quote, also 33); those go through `admit_price_read`, exact since v3.9. A real swap's `quoteAmount` in SOL has at most 9 decimals, so with a 17-digit SOL/USD price a swap of 1,000 SOL or more already makes a 30-digit product: the rounding is reachable the moment B10 writes real USD, even if a half-cent crossing is rare.

## Item 2. `board.entry_cap_at` under `_EXACT`, with the v3.10 null rule

| | File and line (v3.11) | Before (v3.10) | After (v3.11) |
|---|---|---|---|
| **Python, rpc_supply** | `board.py:72` | `float(p * Decimal(s["amount"]) / (Decimal(10) ** s["decimals"]))`, default context: the product rounded to 28 digits, then the division | `float(NW._EXACT.multiply(NW.supply_tokens(s), p))`: the exact token count, then the price, as `market_cap_at` since v3.7/v3.9 |
| **Python, estimate** | `board.py:73-76` | `sup = ...["assumedSupply"]["value"]`; `float(p * Decimal(str(sup)))`: with `sup` null, `Decimal("None")` raises `decimal.InvalidOperation` | `a = row["market"]["marketCap"]["assumedSupply"]`; `a["value"] is None` returns `{value: None, reason: a["reason"], basis: "estimate", entryAt, read}`; else `float(NW._EXACT.multiply(p, Decimal(str(a["value"]))))` |
| **JavaScript** | `js/board.js:39-44` (new) | the builds' `lib/board.js` (`b8dfd484…`, byte-equal in build-1b, 4, 5, 6, 7, 8, 8b and 9, the v3.4 port of `board.py`): `decDiv(decRound(decMul(p, dec(amount)), 28), 10^decimals)` and `decRound(decMul(p, dec(String(sup))), 28)`, the Python context reproduced on purpose; `dec("null")` throws `not a decimal: null` | `decFloat(decMul(supplyTokens(s), p))` and `a.value === null ? {value: null, reason: a.reason, basis: 'estimate', ...} : decFloat(decMul(p, dec(String(a.value))))`, BigInt products, exact |
| **The display** | `board.py` `entry_cap_display` | unchanged | unchanged: a null estimate prints `cap not read` titled `supply not read at <entry minute>` (V107's display in parity: `supply not read at 10:05Z`) |
| **The headers** | `board.py` docstring, `nowat.py` docstring, `js/board.js` first comment | v3.4/v3.5, v3.10 | a v3.11 paragraph in each |

**A JavaScript mirror exists, outside `contract/`.** The brief expected none (v3.9 found none in `contract/js/`); the builds carry one, `lib/board.js`, a line-by-line port of `board.py` that copies the 28-digit rounding and throws on a null assumed supply (`parity-v311.mjs` runs it on V107, read only: `throws: not a decimal: null`). This sprint may not touch `build-*`, so the rule lands in a new `contract/js/board.js`: the builds' file with a v3.11 header, `supplyTokens` imported (and `decDiv`, `decRound` no longer), and the same rule as `board.py`. `check_v311` diffs it against `build-7/lib/board.js`: it differs in its header, the import line and `entryCapAt`'s two return paths only. A build takes it into `lib/` when it next takes the contract; `stagingBoard` and `entryCapField` are unchanged.

The precedence is otherwise unchanged: no entry read (`no tape read at or before the entry`), then no USD at the entry clock (`no USD conversion valid at the entry clock`), then a supply read valid at `entryAt` and available at `t` (`rpc_supply`, the figure), then an assumed supply (`estimate`, the product), then this (`estimate`, null, the document's reason). The JavaScript test is `=== null`, as `marketCapAt`'s. The schema already refuses a null `assumedSupply.value` without a reason string (v3.10, `check_v310`), so no schema byte changes.

## Item 3. Vectors (V106 to V108)

`vectors.py` `neg311` (after `neg310`; constants `_G311`, `_W311`, `_E311`, row `_r108`; the rows reuse v3.10's `_nullrow`), `state-vectors.json` (108 vectors; its `contract` field stays `companion-v3.7`), `validate.py`: `N310`, `BD310`, `NEG311`, `--v310-compat`, `negative-set-v311`, `v310-vectors-unchanged`, `check_v311` (`exact-products-v311`). Each carries `briefId` and `since: v3.11`; V108 also `pin: true`. `NEG31` now excludes `since: v3.11`, so `--v3-compat` still runs V37 to V41 only.

| Id | briefId | What it pins | v3.11 | v3.10 (legacy-v310/) |
|---|---|---|---|---|
| V106 | I1-flow-exact | One interval, 26 Sep 10:00 to 10:05Z, asOf 10:05Z: the item 1 buy (exact 119.94499999999999999999999999999998) and a sell of 1 SOL at the pinned quote `119.94072152604973`: `buyUsd` [119.94], `sellUsd` [119.94], `swaps` [2]; the output schema-valid (`Flow`) | pass | **fails**: `buyUsd [119.95] != [119.94]` |
| V107 | I2-entry-null-assumed | V103's row (the STONK read at `2026-09-26T10:00:18Z`, exact USD, the CoinGecko point held `2026-09-28T17:14:43.362Z`; the supply read valid only 28 Sep 13:27:20Z to 13:27:45.474Z; `assumedSupply` `{value: null, reason: "No supply assumed."}`), entry `2026-09-26T10:05:00Z`, t `2026-09-28T17:20:00Z`: `{value: null, reason: "No supply assumed.", basis: estimate, read.at 10:00:18Z}` | pass | **fails**: `InvalidOperation [<class 'decimal.ConversionSyntax'>]` (a raise counted as a failure) |
| V108 | I2-entry-exact-pin | The V107 row with the STONK supply read valid from T to T + 3 h: `rpc_supply`, 34,341,336.979004525 (cents `34341336.98`, V100's figure); and with `assumedSupply` `1000000000` and no supply valid at the entry: `estimate` 42,166,704.12350223 | pass | pass (pin: the 28-digit rounding changes neither float) |

`--v310-compat` output:

```
v3.10-compat: negative vectors run against legacy-v310/ (v3.10 code and schema, frozen before the first v3.11 edit)
FAILS-ON-V3.10 V106 (I1-flow-exact, ...): case exact-total (): buyUsd [119.95] != [119.94]
FAILS-ON-V3.10 V107 (I2-entry-null-assumed, ...): case no-cap: InvalidOperation [<class 'decimal.ConversionSyntax'>]
PASSES-ON-V3.10 V108 (I2-entry-exact-pin, ...): no difference (a pin: passes on both by design)

2 of 2 negative vectors fail on v3.10; 1 pins pass on both, as they must
```

## Item 4. Real documents

`validate.py --doc` on build-7's `real/solana-now.json` and `real/bsc-now.json`, build-8's `real/solana-now.json` and build-8b's `real/solana-live-now.json` (`build-8b/CHECKS.md` ends `Status: DONE`): exit 0, `document: OK` on each, and each output **byte-identical** to v3.10's validator on v3.10's files (a copy of `contract/` restored from `legacy-v310/` in `/tmp/w311`, the `build-*` folders symlinked read only, compared with `cmp`). `--doc` prints no version label, so there is none to differ. The rail lines, unchanged:

```
build-7  solana-now.json       rail: 61 row-moments (61 per row) through cap_display_at, 0 raised; 0 printed a figure, 61 cap not read (60 titled supply not read at, 1 titled with the price reason)
build-7  bsc-now.json          rail: 61 row-moments (61 per row) through cap_display_at, 0 raised; 0 printed a figure, 61 cap not read (60 titled supply not read at, 1 titled with the price reason)
build-8  solana-now.json       rail: 61 row-moments (61 per row) through cap_display_at, 0 raised; 1 printed a figure, 60 cap not read (60 titled supply not read at, 0 titled with the price reason)
build-8b solana-live-now.json  rail: 61 row-moments (61 per row) through cap_display_at, 0 raised; 1 printed a figure, 60 cap not read (60 titled supply not read at, 0 titled with the price reason)
```

## Item 5. Schemas, version, stamp

No key is added or renamed, so the schemas' `$id` and `meta.contractVersion` stay `companion-v3.7`: `companion.schema.json` `aeefae9c…` and `now.schema.json` `7ebe24dd…` are byte-equal to `legacy-v310/` (checked in `check_v311`), and every v3.10 document is a v3.11 document. The version strings moved to v3.11: the CONTRACT title line and a v3.11 header note, the §7 heading, `validate.py` docstring (a `--v310-compat` entry and check 17) and labels (`v3.11 pass`, `fails on v3.11`, the before-and-after block), `README.md`, `js/README.md`. The contract now says, in §7.6a (**v3.11, every USD product exact**) and in §8 (**v3.11**), that every USD product in the contract is exact and that an entry cap with no supply, read or assumed, is null with the document's reason; §7.7's "Sums" names the context; §11 says v3.11 keeps the stamp. No migration script. `staging/board.json` is v3.4's file and is not rewritten: its SPRING entry cap (1,681,516.314997832) recomputes unchanged on v3.11 and equals v3.10's.

`check_v310` still states v3.10's facts, now about v3.10's frozen files (`legacy-v310/nowat_v310.py` and `nowat-v310.js` against `legacy-v39/`), as v3.10 did for `check_v39`. Its rail walks still run on the working reader, whose `market_cap_at` and `cap_display_at` v3.11 does not touch.

## Item 6. Parity

`contract/js/parity-v311.mjs` and `parity-v311.py`: copies of the v3.10 pair (sections 1 to 4 unchanged), plus a section 5 and a V106 line. `parity-v311.py --entry` prints `board.entry_cap_at` (with `entry_cap_display`) on every V71, V107 and V108 case and `staging_board()`; the JavaScript side runs `js/board.js` on the same. A Board mirror exists, so V107 and V108 run on both sides. `parity-v311.py --flow` prints V106 from Python only, stated in the output.

| Section | Result |
|---|---|
| 1, vector cases | **37 of 37 byte-equal** (as v3.10) |
| 2, `now_at` | **12 of 12** runs byte-equal |
| 3, STONK a and b, exact and 28-digit | **4 of 4 EQUAL** |
| 4a, rail, as on disk | **61 of 61 EQUAL**; 0 figure, 61 `cap not read`, 0 raised |
| 4b, rail, conversion held, `asOf` 18:15:00Z | **61 of 61 EQUAL**; 1 figure, 60 `cap not read`, 0 raised |
| 5, entry cap (`board.py` against `js/board.js`) | **11 of 11 EQUAL**: V71's 7 cases, V107 `no-cap` (null, `No supply assumed.`, `cap not read` titled `supply not read at 10:05Z`), V108 `rpc-supply` (34,341,336.979004525, `$34.34M`) and `estimate`, and the staging Board fixture whole. Reference, not counted: the builds' `lib/board.js` on V107 `throws: not a decimal: null` |
| V106 | Python only: `buyUsd` 119.94, `sellUsd` 119.94, `swaps` 2 |

`PARITY-v311.out` `3aaf73ea…`.

## Verification

- `python3 contract/validate.py`: exit 0, **192 passed, 0 failed** (v3.10: 187). New: `negative:V106`, `V107`, `V108` (pin), `v310-vectors-unchanged`, and `exact-products-v311`: `nowat.py` differs from `legacy-v310/` in its header, the `decimal` import and the flow totals only; `board.py` in its docstring and `entry_cap_at`'s product and null rule only; `js/nowat.js` and both schemas byte-equal to `legacy-v310/`; `js/board.js` against `build-7/lib/board.js` as in item 2; the staging entry caps; the real flow counts of item 1; V106's product in both contexts. `contract/VALIDATE.out` is this run (`1f2e6b25…`).
- `--v310-compat`: exit 1, V106 and V107 fail on v3.10, V108 passes on both. `--v39-compat`, `--v38-compat`, `--v37-compat`, `--v36-compat`, `--v34-compat`, `--v33-compat`, `--v32-compat`, `--v31-compat`, `--v3-compat`: exit 1 each, **each output byte-identical** to v3.10's validator on v3.10's files (`/tmp/w311`, compared with `cmp`), since no older group takes `since: v3.11`.
- `--doc` on the four documents of item 4: exit 0, byte-identical to v3.10's.
- `node contract/js/parity-v311.mjs`: exit 0, every mirrored case EQUAL (item 6). `node contract/js/parity-v310.mjs`: exit 0, `PARITY-v310.out` byte-identical after the rerun (`768e05a3…`). `node contract/js/parity-v38.mjs`: exit 0, `PARITY-v38.out` byte-identical (`0dc87a96…`). `node contract/js/parity-v39.mjs`: exit 0; its rerun rewrote `PARITY-v39.out` in the same two lines v3.10 recorded (section a, exact product at `17:20:00Z`, py and js: v3.9's recorded raise became v3.10's returned `cap not read`); the file was put back to its pre-run bytes (`11a4ce74…`, the hash `CHANGES-v3.9.md` states), as v3.10 did.
- `python3 build-8/scripts/defect-exact-product.py` and `node build-8/scripts/defect-exact-product.mjs`: exit 0, stdout byte-identical to the same scripts on v3.10's readers (the Python one run from `/tmp/w311`, the JavaScript one from a copy pointing at `/tmp/w311/contract/js/nowat.js`): `exact ... admitted`, `prec28 ... conversion_mismatch` in both readers. (stdout compared; Node's `MODULE_TYPELESS_PACKAGE_JSON` warning on stderr comes from the environment, not the readers.)
- **build-3 parity guard** (`/tmp/v311/guard.py`): `build-3/scripts/dump-nowat.py` on the v3.11 `nowat.py` is **9 of 9 byte-equal** to the same script on v3.10's (a copy under `/tmp/w311/x/scripts/`, whose `../../contract` is the restored v3.10 tree).
- **Generators:** `vectors.py` rerun: `state-vectors.json` V01 to V105 byte-equal to `legacy-v310/state-vectors-v310.json`. `make.py` and `board.py --staging` not rerun (no fixture or schema input changed).
- `(cd contract/legacy-v310 && sha256sum -c SHA256)`: 16 of 16 OK.
- No file under any `build-*` or `design-pass*` directory changed: `find build-* design-pass* -newer contract/legacy-v310/SHA256 -type f` returns nothing. Builds' verify harnesses pin `contract/` byte for byte, so a build that resumes re-pins to these bytes (`js/nowat.js` is unchanged at `107e6e7b…`; `js/board.js` `44102214…` is new). No deploy. Nothing sent. No network call. No em dashes in any file written here.
- Hashes: `nowat.py` `430a65b1…`, `board.py` `acbee7e1…`, `js/board.js` `44102214…`, `js/nowat.js` `107e6e7b…` (unchanged), `state-vectors.json` `542e66de…`, `COMPANION-CONTRACT-v3.md` `3940811e…`, `validate.py` `6f06dd78…`, `vectors.py` `50a68d95…`, `js/parity-v311.mjs` `03d0692c…`, `js/parity-v311.py` `7a5fa81c…`, `README.md` `c15c70fa…`, `js/README.md` `61065f0e…`; schemas unchanged (`aeefae9c…`, `7ebe24dd…`).

## Still open

1. From v3.8, v3.9 and v3.10, as far as it still stands: build-8 and build-8b have landed (`Status: DONE`), and their documents pass `--doc` on v3.11 unchanged (item 4), each printing the cap at `asOf` with its clock and the CoinGecko mark and `cap not read` on the other 60 rail moments. What remains is the re-pin: any build whose verify pins `contract/` (build-8b, build-9 and on) re-pins to v3.11's bytes when it next runs; `js/nowat.js` (`107e6e7b…`) and the schema pin (`7ebe24dd…`) are unchanged, so only the Python files, the vectors and the new `js/board.js` move. Everything in CHANGES-v3.7 "Still open" that the builds did not close stands.
2. New, carried from item 2: every build's `lib/board.js` is still the v3.4 port (`b8dfd484…`), which rounds the entry cap to 28 digits and throws `not a decimal: null` on a null assumed supply. It is not reached today (Board has no real rows; the staging fixture's SPRING cap is equal under both rules). The next build that takes the contract copies `contract/js/board.js` into its `lib/` (and its bundle), which closes it.
3. New, carried from item 1: when B10's writer or any JavaScript flow writer appears, its interval totals take the product and sum exactly (a BigInt product, as `decMul`), and parity for `flow_from_swaps` starts there with V106 as its first case.

v3.10's Still open 1 (audit rows 3 and 10: `flow_from_swaps` totals and `board.entry_cap_at` in the default context, and `entry_cap_at`'s null `assumedSupply`) is closed here.

Status: DONE
