# Contract v3.10: changes from v3.9

*W-contract-v310, 2026-09-28 21:19 Beirut onward. Brief: `briefs/W-contract-v310-null-assumed-supply.md` (Sprint V21c of `SPRINT-CHAIN-2026-09-27.md`, chained after V21b, contract v3.9, and before V22, build-8, resumes). Found by the contract v3.9 worker's parity run, 21:09 (`CHANGES-v3.9.md` Still open 1): `market_cap_at`'s estimate path multiplied the price by `market.marketCap.assumedSupply.value`, and both real documents carry `null` there (`No supply assumed.`), so both readers raised at any `t` where the last tape read has USD and no supply read is valid. Before v3.9 no real read had USD, so it never ran. On build-8's re-assembled Solana document (the pinned CoinGecko conversion held from 17:14:43.362Z, a fresh supply read valid only from seconds before `asOf`) the Market rail would have raised on every moment but the last. Ruling (Vesper, lane, 21:20): a moment with a USD price and no supply, read or assumed, has no cap. One line in each reader, three vectors, 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: 187 passed, 0 failed (contract/VALIDATE.out; v3.9: 182)
python3 contract/validate.py --v39-compat        # exit 1 by design: V103 fails on v3.9 (both readers raise); the pins V104, V105 pass on both
python3 contract/validate.py --v38-compat        # exit 1 by design, byte-identical to v3.9's output (and --v37, --v36, --v34, --v33, --v32, --v31, --v3)
python3 contract/validate.py --doc build-7/data/real/solana-now.json   # exit 0, document: OK, with the rail line
python3 contract/validate.py --doc build-7/data/real/bsc-now.json      # exit 0, document: OK, with the rail line
node contract/js/parity-v310.mjs                 # exit 0: 37 of 37 cases, 12 of 12 now_at runs, 4 of 4 STONK sections, 2 rail walks 61 of 61; writes contract/PARITY-v310.out
node contract/js/parity-v39.mjs                  # exit 0 (it rewrites PARITY-v39.out; 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.9
(cd contract/legacy-v39 && sha256sum -c SHA256)  # 16 of 16 OK
find build-* design-pass* -newer contract/legacy-v39/SHA256 -type f   # no output
```

## Freeze and hashes

"Before" means v3.9 exactly as it stood at 21:20, after the precondition check (`CHANGES-v3.10.md` absent; `pgrep -af 'run-worker[.]sh' | grep -c -E 'build7|build8|contract-v39'` printed 0) and before the first edit. `legacy-v39/` holds the files `legacy-v38/` holds, in the same naming: `nowat_v39.py`, `clocks_v39.py`, `adapter_v39.py`, `schemas_v39.py`, `projection_v39.py`, `coverage_v39.py`, `board_v39.py`, `validate_v39.py`, `vectors_v39.py`, `companion-v39.schema.json`, `now-v39.schema.json`, `COMPANION-CONTRACT-v39.md`, `state-vectors-v39.json`, `now-v39.json` (the v3.9 `fixtures/now.json`), `VALIDATE-v39.out` (the v3.9 `VALIDATE.out`) and `nowat-v39.js` (the v3.9 `js/nowat.js`). `legacy-v39/SHA256` lists all 16, relative to that folder. Every hash equals the one `CHANGES-v3.9.md` states for the file it landed: nowat `04596d6d…`, JS reader `998665dd…`, contract `bfbc83fb…`, vectors `d53b10e1…`, VALIDATE `6d9345ed…`, validate `4689eac4…`, vectors.py `2f174f34…`, clocks `d56ef6b8…` (unchanged since v3.4), schemas `aeefae9c…` and `7ebe24dd…` (v3.7's, unchanged).

A v3.10 vector counts only if it passes on v3.10 **and** fails on v3.9, except a pin, which restates a kept rule and 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'>]`). V01 to V102 are byte-equal to v3.9 (`v39-vectors-unchanged`).

## Item 1. The rule (`market_cap_at`, both readers)

| | File and line (v3.10) | Before (v3.9) | After (v3.10) |
|---|---|---|---|
| **Python** | `nowat.py:573-574` | `return {"value": float(p * Decimal(str(mc["assumedSupply"]["value"]))), "reason": None, "basis": "estimate"}`: with `value` null, `Decimal("None")` raises `decimal.InvalidOperation` | `a = mc["assumedSupply"]`; `return {"value": None, "reason": a["reason"], "basis": "estimate"} if a["value"] is None else {"value": float(p * Decimal(str(a["value"]))), "reason": None, "basis": "estimate"}` |
| **JavaScript** | `js/nowat.js:372-373` | `return { value: decFloat(decMul(p, dec(String(mc.assumedSupply.value)))), reason: null, basis: 'estimate' }`: `dec("null")` throws `not a decimal: null` | `const a = mc.assumedSupply;` `return a.value === null ? { value: null, reason: a.reason, basis: 'estimate' } : { value: decFloat(decMul(p, dec(String(a.value)))), reason: null, basis: 'estimate' }` |
| **The display** | `nowat.py:795` (`cap_display_at`), `js/nowat.js` `capDisplayAt` | unchanged | unchanged: with no supply read valid at `t` the title is `cap_title(moment_label(t, exact))`, the v3.5 rule, since the v3.7 price-reason title needs a supply read valid at `t` |
| **The contract sentence** | title note; §7.2 `market.marketCap` **v3.10**; §7.6a **v3.10**; §11 | none | a moment with a USD price and no supply, read or assumed, has no cap; `market_cap_at` returns null with the document's `assumedSupply.reason`, and the surface prints `cap not read` titled `supply not read at <moment>`. Names the finder (the contract v3.9 worker, parity run, 21:09) and why it matters (build-8's rail) |
| **The headers** | `nowat.py` docstring, `js/nowat.js` first comment | v3.9, v3.8 | a v3.10 paragraph in each. `check_v310` diffs both against `legacy-v39/`: each differs in its header and the one line only |

The precedence is otherwise unchanged: not on tape (`not on our tape yet`), then no USD at `t` or the conversion not covering (`no USD price at this moment`, `usd conversion does not cover the read's clock; native quote retained`), then a valid supply read (`rpc_supply`, the figure), then an assumed supply (`estimate`, the product), then this (`estimate`, null, the document's reason). The JavaScript test is `=== null`, not a loose one, so a document missing the key raises in both readers alike (Python `KeyError`, JavaScript `not a decimal: undefined`); the schema refuses such a document anyway.

**The schema already refuses a null `assumedSupply.value` without a string reason.** `assumedSupply` is a `Field`, and `Field` requires `reason` whenever `value` is null (`"if": value null, "then": reason {type: string, minLength: 3}`), so `validate.py --doc` refuses it with no change: `check_v310` sets `assumedSupply` on each real document to `{value: null, reason: null}`, `""` and `7`, and the now schema refuses all 6. No schema byte changes.

Same moment, same words: at a moment where no supply read is valid, the page printed `cap not read` titled `supply not read at <moment>` before the conversion was held (no USD, the v3.5 title), and prints the same words now that it is held. `market_cap_at`'s reason differs (`No supply assumed.` instead of `no USD price at this moment`), but that reason is not the title when no supply read is valid.

## Item 2. Vectors (V103 to V105)

`vectors.py` `neg310` (after `neg39`; helper `_nullrow`, constants `_SV0`, `_SV1`, `_NOSUP`, `_NOTREAD`), `state-vectors.json` (105 vectors; its `contract` field stays `companion-v3.7`), `validate.py`: `N39` (legacy-v39 bound to its own clocks), `NEG310`, `--v39-compat`, `negative-set-v310`, `v39-vectors-unchanged`, `check_v310` (`null-assumed-v310`). Each carries `briefId` and `since: v3.10`; V104 and V105 also `pin: true`. `NEG31` (the v3.1 group, "every negative not otherwise placed") now excludes `since: v3.10`, so `--v3-compat` still runs V37 to V41 only.

The row: V100's STONK read (`2026-09-26T10:00:18Z`, native `0.0003515628686154278` SOL, `usd.value` the exact product `0.042166704123502234168615068024494`, conversion `provider:coingecko` quote `119.94072152604973` at `2026-09-26T10:00:00Z`, `availableAt` `2026-09-28T17:14:43.362Z`), the STONK supply read (raw `814418335339229805`, 9 decimals, block 451332349) observed, available and valid from `2026-09-28T13:27:20Z` to `13:27:45.474Z` as the brief gives it (build-7's own read is valid from `13:27:37.514Z`; either start covers `13:27:42Z` and neither covers `17:20:00Z`, so no vector reads differently), and `assumedSupply` `{value: null, reason: "No supply assumed."}` as both real documents carry it. `moment_label(t)` for a rail minute is `t[11:16] + "Z"`, so `17:20:00Z` labels `17:20Z` and the title is `supply not read at 17:20Z`.

| Id | briefId | What it pins | v3.10 | v3.9 (legacy-v39/) |
|---|---|---|---|---|
| V103 | I1-null-assumed | At `t` = `2026-09-28T17:20:00Z` (USD held, no supply read valid): `market_cap_at` `{value: None, basis: estimate, reason: "No supply assumed."}`; `cap_display_at` `{text: "cap not read", title: "supply not read at 17:20Z", basis: estimate, clock: None, mark: None}`; `usd_fields`: 1 read, USD at `t` the exact product, no gap | pass | **fails**: both cases raise `InvalidOperation` |
| V104 | I1-before-conversion-pin | At `t` = `2026-09-28T13:27:42Z` (supply valid, conversion not held yet): `market_cap_at` null with `no USD price at this moment`; `cap not read` titled `no USD price at this moment` (the v3.7 title rule) | pass | pass (pin; byte-equal) |
| V105 | I1-assumed-pin | The V103 row with `assumedSupply` `{value: "1000000000", reason: null}` at `17:20:00Z`: basis `estimate`, value 42,166,704.12350223 (cents `42166704.12`); `cap_display_at` still `cap not read` titled `supply not read at 17:20Z` (§7.6a: an estimate never prints the figure) | pass | pass (pin) |

`--v39-compat` output:

```
v3.9-compat: negative vectors run against legacy-v39/ (v3.9 code and schema, frozen before the first v3.10 edit)
FAILS-ON-V3.9 V103 (I1-null-assumed, ...): case no-cap: InvalidOperation [<class 'decimal.ConversionSyntax'>]; case usd-at: InvalidOperation [<class 'decimal.ConversionSyntax'>]
PASSES-ON-V3.9 V104 (I1-before-conversion-pin, ...): no difference (a pin: passes on both by design)
PASSES-ON-V3.9 V105 (I1-assumed-pin, ...): no difference (a pin: passes on both by design)

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

## Item 3. Real documents and the rail walk

`validate.py --doc` now walks the 61 rail moments (`rail_moment(asOf, m)` for m 0 to 60, as `build-7/market.html` does; `exact=False`, the rail's `HH:MMZ` label) through `cap_display_at` on every row, prints one `rail:` line, and refuses the document if any moment raises. On both build-7 documents: exit 0, `document: OK`, and the output is byte-identical to v3.9's (the v3.9 validator on a copy of `contract/` restored from `legacy-v39/`, compared with `diff`) apart from the one added line:

```
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)
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)
```

The one titled with the price reason on each is minute 60 (`asOf` itself, `13:27:42Z` and `13:27:28Z`), the only rail moment the supply read covers; no USD is held there, so it is `no USD price at this moment` (the v3.7 title). The other 60 fall before the supply read's `validFrom` and print `supply not read at HH:MMZ`. No figure prints on either document today.

`check_v310` also walks the rail on the Solana document with the conversion held, in memory (the pinned point on the 44 reads it covers with their exact products, `asOf` moved to `2026-09-28T18:15:00Z` and a fresh supply read valid `18:14:55Z` to `18:15:05Z`, the shape build-8 assembles, the clocks illustrative): v3.10 returns on 61 of 61, 1 figure (minute 60) and 60 `cap not read` titled `supply not read at HH:MMZ`; **v3.9 raised on 60 of 61**. On the two documents as on disk v3.9 raised on none and v3.10 equals it on 61 of 61 each.

## Item 4. Schemas, version, stamp

No key is added or renamed, so the schemas' `$id` and `meta.contractVersion` stay `companion-v3.7`, exactly as v3.9 kept them: `companion.schema.json` `aeefae9c…` and `now.schema.json` `7ebe24dd…` are byte-equal to `legacy-v39/` (checked in `check_v310`), and every v3.9 document is a v3.10 document. Contract §11 says so. The version strings moved to v3.10: the CONTRACT title line and a v3.10 header note, the §7 heading, `validate.py` docstring (a `--v39-compat` entry and check 16) and labels (`v3.10 pass`, `fails on v3.10`), `README.md`, `js/README.md`, the `nowat.py` and `js/nowat.js` headers. No migration script.

`check_v39` still states v3.9's facts, now about v3.9's frozen files: `legacy-v39/nowat_v39.py` differs from `legacy-v38/nowat_v38.py` in its header and the one product line, and `legacy-v39/nowat-v39.js` is byte-equal to `legacy-v38/nowat-v38.js`. Its STONK admission counts still run on the working reader (unchanged by v3.10).

## Item 5. Parity

`contract/js/parity-v310.mjs` and `parity-v310.py`: copies of the v3.9 pair with V103 to V105 added to section 1 and a section 4, the rail walk on build-7's `real/solana-now.json` in memory (the document is not written): (a) as on disk; (b) with the conversion held, the same construction as `check_v310` above. Section 3's comment now says a raise is a defect on either side.

| Section | Result |
|---|---|
| 1, vector cases | **37 of 37 byte-equal** (v3.9: 33; V103 `no-cap` and `usd-at`, V104 `no-usd`, V105 `estimate`) |
| 2, `now_at` | **12 of 12** runs byte-equal (build-3's nine, the contract fixture, build-7's two real documents) |
| 3, STONK a and b, exact and 28-digit | **4 of 4 EQUAL**. Section a at `17:20:00Z`, exact product: both sides now return `marketCap {value: null, basis: estimate, reason: "No supply assumed."}` and display `cap not read` titled `supply not read at 17:20:00Z` (this section passes `exact=True`, so the label carries seconds), where v3.9 recorded `{raises: "not a decimal"}` on both |
| 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 |

## Verification

- `python3 contract/validate.py`: exit 0, **187 passed, 0 failed** (v3.9: 182). New: `negative:V103`, `V104` (pin), `V105` (pin), `v39-vectors-unchanged`, and `null-assumed-v310`: `nowat.py` and `js/nowat.js` differ from `legacy-v39/` in their headers and the estimate line; both schemas byte-equal to `legacy-v39`; the now schema refuses a null `assumedSupply.value` with reason null, empty or not a string (6 of 6); the three rail walks above. `contract/VALIDATE.out` is this run (`c5925901…`).
- `--v39-compat`: exit 1, V103 fails on v3.9 (a raise counted as a failure), V104 and V105 pass on both. `--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.9's validator run on v3.9's files (a copy of `contract/` restored from `legacy-v39/` in `/tmp/w310`, compared with `cmp`), since no older compat group takes `since: v3.10`.
- `--doc` on `build-7/data/real/solana-now.json` and `bsc-now.json`: exit 0, `document: OK`, output identical to v3.9's plus the `rail:` line (item 3).
- `node contract/js/parity-v310.mjs`: exit 0, every case EQUAL (item 5). `PARITY-v310.out` `768e05a3…`.
- `node contract/js/parity-v39.mjs`: exit 0 (33 of 33, 12 of 12, 4 of 4). Its rerun rewrote `PARITY-v39.out` in exactly two lines, section a exact product at `17:20:00Z`, py and js: `{raises: "not a decimal"}` became the returned `cap not read` / `No supply assumed.` above. The file was put back to its pre-run bytes (`11a4ce74…`, the hash `CHANGES-v3.9.md` states), as v3.9 did for v3.7's. `node contract/js/parity-v38.mjs`: exit 0, `PARITY-v38.out` byte-identical after the rerun.
- `python3 build-8/scripts/defect-exact-product.py` and `node build-8/scripts/defect-exact-product.mjs`: exit 0, output byte-identical to the same scripts on v3.9's readers (the Python one run from `/tmp/w310`, the JavaScript one from a copy pointing at `/tmp/w310/contract/js/nowat.js`): `exact ... admitted`, `prec28 ... conversion_mismatch` in both readers.
- **build-3 parity guard** (`/tmp/v310/guard.py`): `build-3/scripts/dump-nowat.py` on the v3.10 `nowat.py` is **9 of 9 byte-equal** to the same script on `legacy-v39`'s `nowat.py` (a copy of the script under `/tmp/w310/x/scripts/`, whose `../../contract` is the restored v3.9 tree).
- **Generators:** `vectors.py` rerun: `state-vectors.json` V01 to V102 byte-equal to `legacy-v39/state-vectors-v39.json`; nothing else written. `make.py` not rerun (no fixture or schema input changed).
- `(cd contract/legacy-v39 && sha256sum -c SHA256)`: 16 of 16 OK.
- No file under any `build-*` or `design-pass*` directory changed: `find build-* design-pass* -newer contract/legacy-v39/SHA256 -type f` returns nothing. build-8's verify pins `contract/` byte for byte, so it re-pins to these bytes when it resumes. No deploy. Nothing sent. No network call. No em dashes in any file written here.
- Hashes: `nowat.py` `12b750e9…`, `js/nowat.js` `107e6e7b…`, `state-vectors.json` `926fe3b3…`, `COMPANION-CONTRACT-v3.md` `e57002ce…`, `validate.py` `67fbb8f3…`, `vectors.py` `d12f9507…`, `js/parity-v310.mjs` `e8140b0e…`, `js/parity-v310.py` `d0bb5c2f…`; schemas unchanged (`aeefae9c…`, `7ebe24dd…`).
- Runtime note: the rail walks make `validate.py` slower (about 4 minutes more in total, most of it the 41,823-read BSC document walked 61 times in each of v3.10 and v3.9); `--doc` on the BSC document takes about 40 s more.

## Still open

1. Audit rows 3 and 10 (from v3.9): `flow_from_swaps` totals and `board.entry_cap_at` still multiply in the default context. Neither has a JavaScript mirror today, so the readers cannot split; when B10's writer or Board's read model gets one, take the product under `_EXACT` there too, as `market_cap_at` does. `board.entry_cap_at` also multiplies by `assumedSupply` (`board.py:69`); it is not reached on the real documents today (Board has no real rows), but when it is, it needs the same null rule as v3.10.
2. From v3.8 and v3.9, unchanged apart from the pin: build-8 resumes from `build-8/CHECKS.md` item 2 as written (one SOL/USD reference read stored as a `Conversion` on the STONK read, `usd.value` the exact product, admitted by both readers; `asOf` at or after the fetch clock with a supply read valid there; the cap printed with its clock and the CoinGecko mark through `capDisplayAt`; provenance pinned in SOURCES; BSC unchanged, `cap not read`, `no USD price at this moment`). Its Market rail is now safe: every moment before the fresh supply read's `validFrom` returns `cap not read` titled `supply not read at HH:MMZ` in both readers. Re-pin build-8's verify to v3.10's `contract/` bytes (`js/nowat.js` `107e6e7b…` is the file build-8 copies), schema pin `7ebe24dd…` unchanged. Everything in CHANGES-v3.7 "Still open" that build-7 did not close stands.

v3.9's Still open 1 (the null `assumedSupply` raise) is closed here.

Status: DONE
