# Contract v3.4: changes from v3.3

*W-contract-v34, 2026-09-28 09:49 to about 11:00 Beirut. Brief: `briefs/W-contract-v34.md` (Sprint V8 of `SPRINT-CHAIN-2026-09-27.md`). Triggers: the curator's `inbox/rook/2026-09-28-0555-curator-two-contract-versions-...` and `2026-09-28-0910-the-validator-was-widened-...` (sections 1 and 2). Ruling: Vesper, 10:00 Beirut, **the v3.3 vocabulary wins** (`now.history {since, kind}`, `marketCap.basis: estimate`, anchor `previous_material`, PriceRead `native`/`usd`); the v3.2 names are dead; Bolo's B4 store migrates, and the contract does not alias. This is not a release. Nothing ships until Bolo re-probes v3.4. No page changed: every `design-pass*` file is untouched, and the build pages' `innerText` is byte-equal before and after (below).*

## How to check this

```
cd /home/botbox/.openclaw/workspace/projects/caverio/companion-2026-09-26
python3 contract/validate.py                    # exit 0: 140 passed, 0 failed (contract/VALIDATE.out)
python3 contract/validate.py --v33-compat       # exit 1 by design: 6 of 6 v3.4 vectors fail on v3.3
python3 contract/validate.py --v32-compat       # exit 1 by design: 7 of 7 v3.3 negative vectors fail on v3.2 (unchanged)
python3 contract/validate.py --v31-compat       # exit 1 by design: 17 of 17 v3.2 negative vectors fail on v3.1 (unchanged)
python3 contract/validate.py --v3-compat        # exit 1 by design: 5 of 5 v3.1 negative vectors fail on v3 (unchanged)
python3 contract/migrate_v32_to_v33.py --check  # exit 0: the one-time migration over the two frozen v3.2 files
grep -n downshift contract/*.py                 # no output
```

"Before" means v3.3 exactly as it stood at 09:49, before the first edit. `legacy-v33/` holds `nowat_v33.py`, `clocks_v33.py`, `adapter_v33.py`, `schemas_v33.py`, `projection_v33.py`, `coverage_v33.py`, `companion-v33.schema.json`, `now-v33.schema.json`, `COMPANION-CONTRACT-v33.md`, `state-vectors-v33.json`, `now-v33.json` (the v3.3 `fixtures/now.json`), `board-v33.json`, `vectors_v33.py`, `validate_v33.py` and `VALIDATE-v33.out`, with `legacy-v33/SHA256` (`sha256sum -c` passes). Key hashes: nowat `227bbd66…`, clocks `d56ef6b8…`, companion schema `c41ca140…`, now schema `615b8dad…`, contract `d359c40a…`, vectors `9fff257b…`, now fixture `54b952ba…`. Two files were added after the freeze, as `SHA256.notes` says: `pageminute_v33.py`, v3.3's §7.1 page-minute text transcribed so that V70 has a before (v3.3 had no callable for it, the same situation as `legacy-v32/pricemove_v32.py`), and `neg33-v32-inputs.json` (item 1). A v3.4 vector counts only if it passes on v3.4 **and** fails on v3.3.

**Vector ids.** V66 to V71, 71 in total. Each carries `briefId`: I2-historyFrom, I2-estimate, I2-anchor, I2-price (item 2), I4-page-minute (item 4), I5-entry-cap (item 5). V01 to V65 are byte-equal to v3.3 (`v33-vectors-unchanged`). The before-and-after texts of V37 to V65 in `VALIDATE.out` are identical to v3.3's `VALIDATE.out` (diffed), and so are the `--v32-compat`, `--v31-compat` and `--v3-compat` result lines.

**Bolo's own probes.** `reprobe.py` and `strengthen.py` were copied unmodified to `/tmp/v34probe/`, and both exit 0. His B6 folder's listing and timestamps are unchanged (compared before and after). `validatorExit 0`, `priorProbeExit 0`, `sourceStableAfter true`; the 2099 event is still refused, and so are the four price strings, with `price is not a finite positive decimal; not admitted`. His `strengthen.py` adds a `price` key holding NaN: that still gets the price-boundary reason, because the price string check runs before the dead-name check (V69 control case). The baseline is 87 before and after the window slide. The results are in `VALIDATE.out`, after the compat runs.

## Item 1. `downshift()` removed; one-time migration

| | File and line (v3.4) | Before (v3.3) | After (v3.4) |
|---|---|---|---|
| **The run-time map is gone** | `validate.py`: `downshift` deleted. `grep -n downshift contract/*.py` returns nothing. | `validate.py:92-119` `downshift`: native/usd to `price`, Fields to bare clocks, `history` to `historyFrom`, `estimate` to `assumed_supply`, `previous_material` to `previous_same_kind` and one threshold sentence to the other. It was applied at run time to every legacy `now_rule` run and to the V42 to V58 equality check. | No map at run time. |
| **The migration** | `migrate_v32_to_v33.py` (`migrate` `:75`, `check` `:123`). It is the exact inverse of `downshift` and changes nothing else, apart from one stamp a migrated document needs to validate: `meta.contractVersion` `companion-v3.2` becomes `companion-v3.3`. Two v3.3 facts are not in a v3.2 document, so the caller supplies them, and the defaults are the honest ones. `--stable-quote ASSET` declares that the pool quotes a USD stable, so `native = {asset, amount: price}`; without it, native is absent with `native quote not in the v3.2 document`. `--history-kind prospective` sets the kind; the default is `persisted`, which is what v3.2's `historyFrom` meant. The script renames and does not re-derive. It stops with exit 2, and writes nothing, on a v3.2 read without a price, a null row `surfacedAt`, or a half-migrated read. | | |
| **Its check** (`--check`) | Migrates `legacy-v32/state-vectors-v32.json` with `--stable-quote USDC@rh` (vectors.py's `_rd` pools) and `legacy-v32/now-v32.json` with the defaults. The migrated Now validates against the v3.4 `companion.schema.json` and `now.schema.json` with 0 errors, `now_at` at `asOf` admits all six rows with no refusals, every read is admitted, and no v3.2 name is left. | | vectors: 33 renames, output **`legacy-v32/state-vectors-v32-migrated.json` sha256 `ca73b290ae406764b5e5f2b0a9ac8c5c6968428b39dd09d1df5d6bd48e47431f`** (committed). now-v32: 131 renames, output `/tmp/now-v32-migrated.json` sha256 **`168b3f868e7f75f6c002affcf685946e0675aa4c5a72fc843f023e8888e34acc`** (not committed: it is v3.2's derivation under v3.3 names; its 17:27Z price move keeps v3.2's 16:57Z anchor clock under the new kind name, whereas the v3.3 derivation anchors it on 17:08:05Z) |
| **V42 to V58 equality** | `validate.py:792` `v32-vectors-migrated`: V01 to V58 are compared directly with `legacy-v32/state-vectors-v32-migrated.json`, apart from descriptions. | V01 to V58 went through `downshift` and were compared with `legacy-v32/state-vectors-v32.json`. | Equal. The migrated file equals the v3.3 vectors exactly (58 of 58), which is the proof that the migration is the inverse of the map. |
| **Legacy runs read files, not a map** | `validate.py:754` `LEGACY_INPUTS`: V42 to V58 on v3.1 run from `legacy-v32/state-vectors-v32.json` (what v3.2 ran), and V59 to V65 on v3.2 run from `legacy-v33/neg33-v32-inputs.json`. That file was generated once, by applying the frozen `downshift` in `legacy-v33/validate_v33.py` (sha256 `eb86f772…`) to V59 to V65 of `legacy-v33/state-vectors-v33.json`, and it is data. | Generated on every run by `downshift`. | Same inputs, same results (the compat outputs are line-identical to v3.3's). |

**For Bolo's B4 store.** His one diagnostic snapshot (`b4/history.sqlite`, seq 1, payload sha256 `38fb7494…`, kind `b2-diagnostic-only/1`) holds **no v3.2 name**. The springNowRow is the B2 envelope, `nowRow` is null, and its `history` is B2's own `{since, scope, ...}`. I extracted it read-only and ran the script over it. The result is 0 renames and a document equal to the input. So the migration is a re-pin plus one script run, as the brief expected:

```
# one script run over the snapshot payload (extract it however the store's own reader does; this is what I ran, read-only):
python3 contract/migrate_v32_to_v33.py <payload.json> <payload.v33.json> --history-kind prospective    # expect "0 renames"
# the re-pin: the store's binding (meta.binding.schemaSha256, versions.schema_sha256, the payload contractPin) moves from
#   v3.2 now.schema.json 1432bc360d621912104cae8496f2223d107141fe488278e6774cd0dac4dcd48f
#   to v3.4 now.schema.json 7b89c378e565b2d101e94b974d0fb8e135473fa5824e5175697d511745c28df3  ($id .../contract/v3.4/now.schema.json)
```

The owner-schema pin is his to move, and I did not touch his store. `--history-kind prospective` matters only when a payload still carries `historyFrom`. His later publications (B7) should be written in v3.3 names from the start.

## Item 2. The dead names refused, naming the key

| Dead name | File and line (v3.4) | Vector | Before (v3.3): callable / schema | After (v3.4): callable / schema |
|---|---|---|---|---|
| key **`historyFrom`** on a Now document | `nowat.py:128` `dead_names`; `:485` `now_at` refuses the document with the first offending path | **V66** (I2-historyFrom): only `historyFrom`, and both `history` and `historyFrom` | **raised `KeyError 'history'`** / refused (`additionalProperties`, missing `history`); with both keys: **admitted**, `historyFrom` silently ignored / refused | `key historyFrom is a v3.2 name, not in the contract (history.since); not admitted` for both / refused |
| value **`assumed_supply`** under `basis` | `nowat.py:496` `now_at`: a row carrying a v3.2 name goes under `refusals[]` with its path | **V67** (I2-estimate): a row with `market.marketCap.basis: assumed_supply` | **admitted**: the row on Now with a recomputed `estimate` cap / refused (enum) | not on Now; `refusals: [DEAD, market.marketCap.basis, "value assumed_supply is a v3.2 name, not in the contract (estimate); not admitted"]` / refused |
| value **`previous_same_kind`** as the anchor kind | `nowat.py:221` `admit_material_event` | **V68** (I2-anchor) case `v32-event` | refused, but with `material event names no anchor; not admitted` (names nothing) / refused (enum) | `value previous_same_kind is a v3.2 name, not in the contract (previous_material); not admitted` / refused |
| the **v3.2 threshold sentence** | `nowat.py:223`; `schemas.py:547` `threshold` gains `not: {const: ">= 25% against the previous price move"}` | **V68** case `v32-threshold-v33-anchor` (control `v33-event-control` admitted by both) | **admitted** / **valid** (free text) | `threshold '>= 25% against the previous price move' is v3.2 wording, not in the contract ('>= 25% against the price at the previous material event'); not admitted` / refused |
| key **`price`** on a PriceRead | `nowat.py:182` `admit_price_read`, after the price-string check | **V69** (I2-price): a v3.3 read plus `price`, and a pure v3.2 read; control: `price` holding NaN | refused, but with `price read has fields outside the contract; not admitted` (names nothing), and for the pure v3.2 read `price is not a finite positive decimal; not admitted` (wrong: the price was fine; the native quote was missing) / refused (`additionalProperties`) | `key price is a v3.2 name, not in the contract (native.amount, usd.value); not admitted` for both; the NaN control keeps `price is not a finite positive decimal; not admitted` (V61 and Bolo's `strengthen.py` unchanged) / refused |

Every case in V66 to V69 carries `v33: {callable, schemaValid}`, the record of what v3.3 did. `validate.py:831` `v33_record` re-runs each case against `legacy-v33/` (`nowat_v33.py` bound to `clocks_v33.py`, and `companion-v33.schema.json`) and fails if the record is wrong (`v33-record`). It also fails if a v3.4 reason names no key. Where v3.3 already refused a name through `additionalProperties` or an enum, the vector records that, and the change is the message. The v3.2 names appear nowhere in the five fixtures or in V01 to V65 (`no-v32-names`). The five reasons are in contract §10, under "The v3.2 names, refused (v3.4)". Contract §0.19 states the rule.

## Item 3. The widening rule, and the fork it closes

- `README.md`, "The widening rule (v3.4)": a validator change that lets two shapes both pass does not merge unless the same change names, in CHANGES, which shape survives and files the migration. That is one paragraph, with the v3.3 case as its reason.
- **The fork.** v3.2 and v3.3 came from two duplicate Vesper order files, which filed the same three definitions to Bolo. **v3.2** (`CHANGES-v3.2.md`, 00:01) followed **`inbox/bolo/2026-09-27-2220-vesper-three-definitions-closed-since-seen-baseline-tilde-by-validity-history-start-boundary.md`**: `now.historyFrom`, `assumed_supply`, and the previous-price-move anchor. Its own items 5 and 6 flagged that the 23:35 wording differed. **v3.3** (`CHANGES-v3.3.md`, 00:40) followed **`inbox/bolo/2026-09-27-2335-vesper-ack-accepted-three-definitions-closed-since-seen-baseline-tilde-validity-history-start.md`** (the 23:35 pins): `history.since`, `estimate`, and the previous-material-event anchor. It then kept v3.2's shapes passing through `downshift`. The 22:20 file was marked superseded only at 23:39 (curator 05:55). **v3.4 closes the fork**: one vocabulary (v3.3's), the old names refused, the map removed, and the migration filed as `migrate_v32_to_v33.py`. The curator's first ask (a bd issue naming the winner) is Vesper's and Rook's ledger, not this worker's; I filed no issue and sent nothing.

## Item 4. The page minute, settled

| | File and line | Vector | Before (v3.3) | After (v3.4) |
|---|---|---|---|---|
| Contract §7.1 | `COMPANION-CONTRACT-v3.md:426` "v3.4, the page minute"; §12.2 item 17 (line 697) | **V70** (I4-page-minute) | "A page minute `HH:MM` is the moment `HH:MM:00Z`". The validator (`HH:MM:30Z`) and build-1 (`asOf - (60 - m) min`) did not follow it (build-1 `CHECKS.md` §0). | Minute `m` of the rail is `asOf - (60 - m)` minutes, with the seconds (and any fraction) inherited from `asOf`. A rail minute is labelled `HH:MMZ`, and an exact `?t=` clock is labelled `HH:MM:SSZ`. |
| Callables | `nowat.py:514` `rail_moment(as_of, m)`, `:522` `moment_label(t, exact)`, `:508` `iso_exact` (keeps a staged fraction such as `20:49:02.572628Z`) | V70: minute 30 of 17:27:30Z is 16:57:30Z, where the price is `$0.00168` (the pass 7 string); minute 15 gives `$0.00106`; minutes 0 and 60; a staged fraction; the exact label `16:57:05Z` | `pageminute_v33.py` (transcription): 16:57:00Z, where the 16:57 read (available 16:57:06) is absent and the price is `$0.00106`; label `16:57Z` | as stated |
| The validator's own clocks | `validate.py:877` `page-minute`: the pass 7 clocks 16:32:30, 16:42:30 and 16:57:30Z are rail minutes 5, 15 and 30 | | the clocks were hard-coded with no rule behind them | hard-coded, and now checked against the rule |

No string on any page changed. The pass 7 string check (`now-v33`) is unchanged and passes.

## Item 5. Board entry cap, defined

| | File and line | Vector | Before (v3.3) | After (v3.4) |
|---|---|---|---|---|
| Field | `schemas.py:635` `EntryCap {value: PriceField, basis: rpc_supply \| estimate, entryAt, read: {at, availableAt} \| null, note?}`; `:653` `BoardRow.entryCap`, **optional** (BoardRow's `required` is now explicit and unchanged). Contract §8 (line 488), §7.2 `clock.gaps[]` row, §10 "Board entry cap (v3.4)". | | §7.2 named "Board's H1 entry cap" among the USD-derived fields; no field, no callable | as stated |
| Callable | `board.py:47` `entry_cap_at(row, entry_at, t)`. The entry read is the last admitted read with `at <= entryAt` and `availableAt <= t`. Its USD counts when a conversion observation valid at the entry clock exists at `t` (`tape_usd`, or `native_x_provider` with `conversion.at <= entryAt` and `conversion.availableAt <= t`). The cap is USD x supply: `rpc_supply` when a supply read valid at `entryAt` is available at `t`, else `estimate` with the assumed supply. Absent states: `no tape read at or before the entry` and `no USD conversion valid at the entry clock`. I put it in a separate `board.py` so that `nowat.py`, which build-1's `lib/nowat.js` ports line by line, gains no Board function. | **V71** (I5-entry-cap): present (2,000,000 estimate); absent: no read, USD `unavailable`, conversion observed after the entry; **future availability**: null at 10:10 and 4,500,000 from 10:20, when the conversion becomes available; a read available after `t` | v3.3 has no callable (`LookupError`) | as listed |
| Fixture | `staging/board.json` (sha256 `44f56c22…`), written by `python3 contract/board.py --staging`: `fixtures/board.json` plus `entryCap` on SPRING (entry 17:11:01Z; entry read 16:57:00Z, available 16:57:06Z; `1681516.31`, `estimate`, so `~$1.68M`; `t` = the Now document's `asOf`, 17:27:30Z). Its `note` and `meta.fixture` say "admitted for board.json when Board gets its v3.3 read model". The other board rows have no Now row with reads, so they carry no `entryCap`. `validate.py:864` `board-entry-cap`: the staging file is schema-valid and recomputes, and `fixtures/board.json` has no `entryCap` and equals `legacy-v33/board-v33.json` byte for byte. | | | **no public document changed** |

## Item 6. Harness note for Bolo (his files untouched)

Two probes in `reprobe.py` ask about fields that do not exist. They are harness labels, not contract state, and they still print the same as at v3.3 (`VALIDATE.out`):
- `historyPresent` reads `doc.get('historyFrom')` and `row.get('history')`. Both print null. The field is **`now.history`** on the Now document: `{since: 2026-09-14T16:27:00Z, kind: persisted}` on the fixture. `historyFrom` is now refused by name wherever it appears (V66).
- `missingField:history` deletes a row-level `history` that never existed (`wasPresent: false`, `schemaErrors: []`). The meaningful probe deletes `now.history` from the document, which the `Now` schema refuses (`'history' is a required property`) and `now_at` cannot answer.

On the re-probe, point both at `doc['history']` (`since`, `kind`). If a probe needs the v3.2 key, expect the v3.4 refusal text.

## Item 7. v3.5 candidates (reserved, nothing added)

Things B7 (Solana live tape, `inbox/bolo/2026-09-28-0950-vesper-b7-...`) may ask for under its "wants from the read model". None is a field in v3.4.
1. **pump.fun decode as an absence state.** B7 order 5: hb_signal carries no `liveDecodedCurves`, so anything that depends on curve decode must print an absence state. v3.3 has `BondingCurve` Fields that can be null with a reason, but no catalogue reason for "decode not measured", and no row-level statement that a count excludes undecoded curves. Candidate: a §10 reason (such as `curve decode not measured`) and a rule for which Now fields inherit it.
2. **`surfacedAt` from the new writer.** B7 order 6: a pool surfaced by the new writer after `history.since` has a real surface clock. v3.3 carries the clock (`surfacedAt` Field) but not its provenance. Candidate: a basis or source on `surfacedAt` (writer id, store admission clock), so a real surface clock can be told from one inferred from first-seen-in-tape, which B7 forbids.
3. **Native SOL quote.** `native.asset` already admits `SOL@sol`, and `native_x_provider` covers the conversion. Open: the SOL/USD conversion source and its clocks (`conversion.at` from a provider that gives one), and whether a Solana pool quoting USDC is `tape_usd` (`USDC@sol`, already in `USD_STABLES`). Candidate: a named provider and a statement of how old a conversion may be relative to the read.

## Choices you may want to overrule

1. **`contractVersion` stays `companion-v3.3`.** v3.4 changes no required document field and no document on disk. The schema files' `$id` moves to `.../contract/v3.4/...`, so their hashes change. Bumping the document version would have changed every fixture and every build-1 data hash for no difference in shape.
2. **The migration's two inputs.** `--stable-quote` and `--history-kind` are caller-supplied because a v3.2 document does not carry them. The defaults (native absent with a reason; `persisted`) invent nothing.
3. **Dead-name check order in `admit_price_read`.** The price string check runs first, so a `price` key holding NaN keeps the price-boundary reason (Bolo's `strengthen.py`). A well-formed `price` key gets the dead-name reason.
4. **A row with a v3.2 name is refused, not repaired.** `now_at` lists it under `refusals[]` with the first offending path, and the other rows still render. A document-level v3.2 name (`historyFrom`) refuses the whole document.
5. **Entry cap supply validity is judged at the entry clock**, not at `t`. The cap describes the moment of entry, and it uses a supply read only once that read is available at `t`.

## Verification

- `python3 contract/validate.py`: exit 0, **140 passed, 0 failed** (was 129). New checks: `v32-vectors-migrated`, `v33-vectors-unchanged`, `no-v32-names`, `negative:V66` to `V71`, `v33-record`, `board-entry-cap`, `page-minute`.
- `--v33-compat`: exit 1, 6 of 6 fail on v3.3. `--v32-compat`, `--v31-compat` and `--v3-compat`: exit 1, 7 of 7, 17 of 17 and 5 of 5, with result lines identical to v3.3's.
- `migrate_v32_to_v33.py --check`: exit 0. `grep -n downshift contract/*.py`: nothing.
- `make.py` rerun: the five fixtures are byte-identical to v3.3 (`sha256sum -c`). Only the six schema files changed, gaining `EntryCap`, `BoardRow.entryCap`, the `threshold` `not`, and the `$id`/title. Hashes: `companion.schema.json` `33715de9…`, `now.schema.json` `7b89c378e565b2d1…`, `state-vectors.json` `f2563286…`.
- **build-0**: `python3 scripts/build0-fixtures.py` copied all five fixtures byte for byte, then `NODE_PATH=/home/botbox/node_modules node scripts/build0-shots.cjs` exited 0 with **ALL OK**.
- **build-1**: `NODE_PATH=/home/botbox/node_modules node build-1/scripts/verify.cjs` exit 0, **ALL PASS**: the same 106 `PASS` lines as `build-1/scripts/verify-output.txt` apart from JPEG sizes, including rule parity (`contract/nowat.py` against `lib/nowat.js`, canonical JSON at 16 fixture clocks and 3 staged clocks, byte-equal) and the frozen strings against pass 7. It ran headless and needed nothing manual. It regenerates its shots, and two JPEGs differ in bytes (`shots/now-staged-rewound.jpg`, `shots/situation-spring-1280-live.jpg`: re-encoded renders). Every other file under `build-0/` and `build-1/`, shots aside, is byte-identical before and after (sha256 listing diffed).
- **innerText**: build-0 `now`, `board`, `situation?f=spring` and `index`, build-1 `now`, `situation` and `index`, and every html page under `design-pass7`, `design-pass8`, `design-pass9` and `design-pass10`, read over http before the reruns and after (sha256 of `document.body.innerText`): **identical on all 19 pages**
- No file under any `design-pass*` directory was touched. There was no deploy and nothing was sent. Bolo's B4 store and B6 evidence were read only.

## Still open (not in this brief)

- **Bolo re-probes v3.4**, then re-pins B4 to the v3.4 `now.schema.json` (above).
- **A bd issue naming the winner** (curator 09:10 ask 1). The ruling exists (Vesper 10:00), but I did not file the ledger entry; that belongs to the seat that owns the ledger.
- **Provenance gate** (curator 09:10 section 3: `illustrative` required by `validate.py`, banned by the website's `verify.cjs`). Not in this brief and untouched.
- Everything in CHANGES-v3.3 "Still open" stands: the G2 page strings for the v3.3 nulls, build-0's Situation `.value` cells, circulating supply, B4 persistence before any live rewind claim, and the generic event `values`.
- `entryCap` enters `fixtures/board.json` when Board gets its v3.3 read model.

Status: DONE
