grid: give structural session JSON a named format and an owner #341

Merged
natsukium merged 2 commits from issue-336 into main 2026-09-22 04:49:19 +09:00
Owner

Closes #336.

Structural session JSON now has an owner and a version: felis_grid::json_v1 is a named format ({"felis_json": 1, "kind", "msg"}) with dedicated DTOs for Grid, Image, Conn, Session and Input, explicit conversions to and from the domain types (a domain rename fails to compile rather than changing the JSON), structural row cells with their own DTO tree, a generated JSON Schema (crates/felis-grid/schemas/felis-json-v1.schema.json, staleness-tested, integer bounds and the OSC 66 fraction invariant encoded, goldens validated against it), golden fixtures including a RowDelta with structural rows, and a compatibility policy stricter than the wire's because a .fcast file or a deployed browser has no effective-minor gate. The bridge-only families (Ops, Region, Notify, Push, Search) move into felis-cli behind the CLI's own v: 1 contract, byte-identical; felis-grid::transcode and the row JSON helpers are removed so the crate has one JSON spelling of a row.

Decision: the last comment on #336 (six rounds of design review; Option 2 because felis-web-gateway and felis-fcast both consume the shape today and fcast persists it). Code review: pi luna (timestamp normalization) then pi sol, five rounds (row cells DTO, checked bitmaps, reader/schema parity, schema soundness, feature gating): PASS.

Satellite follow-ups (their repos): adopt the envelope via json_v1::{encode, decode}, drop their own kind-name mapping, read rows as RowCellsJson, emit lowercase session ids, and take the daemon's build from identity.

Closes #336. Structural session JSON now has an owner and a version: `felis_grid::json_v1` is a named format (`{"felis_json": 1, "kind", "msg"}`) with dedicated DTOs for Grid, Image, Conn, Session and Input, explicit conversions to and from the domain types (a domain rename fails to compile rather than changing the JSON), structural row cells with their own DTO tree, a generated JSON Schema (`crates/felis-grid/schemas/felis-json-v1.schema.json`, staleness-tested, integer bounds and the OSC 66 fraction invariant encoded, goldens validated against it), golden fixtures including a `RowDelta` with structural rows, and a compatibility policy stricter than the wire's because a `.fcast` file or a deployed browser has no effective-minor gate. The bridge-only families (Ops, Region, Notify, Push, Search) move into `felis-cli` behind the CLI's own `v: 1` contract, byte-identical; `felis-grid::transcode` and the row JSON helpers are removed so the crate has one JSON spelling of a row. Decision: the last comment on #336 (six rounds of design review; Option 2 because `felis-web-gateway` and `felis-fcast` both consume the shape today and fcast persists it). Code review: pi luna (timestamp normalization) then pi sol, five rounds (row cells DTO, checked bitmaps, reader/schema parity, schema soundness, feature gating): PASS. Satellite follow-ups (their repos): adopt the envelope via `json_v1::{encode, decode}`, drop their own kind-name mapping, read rows as `RowCellsJson`, emit lowercase session ids, and take the daemon's build from `identity`.
`felis bridge` reached into `felis-grid`'s shared transcode for the one
conversion it needs, a `Region::Row` turned into a capture row, and that
transcode also carried Ops, Notify, Push and Search: families no
consumer outside the bridge ever asks for. Keeping them there made the
shared engine look like the owner of shapes that belong to the CLI's own
`v: 1` contract, which already publishes their schemas.

The rendered JSON is unchanged. These families still cross through their
serde form, only from a module inside the crate whose contract they are.

Assisted-by: Claude Code
grid: give structural session JSON a named format and an owner
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 6s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m34s
bench / Criterion regression gate (pull_request) Failing after 2m36s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 2m8s
pr / nix flake check (pull_request) Successful in 1m50s
pr / cargo build / clippy / test / deny (pull_request) Successful in 3m28s
pr / wire schema is compatible with the base (pull_request) Successful in 12s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 3m59s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 5s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 24s
windows / cargo nextest (Windows) (pull_request) Successful in 14m41s
windows / frontend smoke (Windows) (pull_request) Successful in 2m26s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
1e873aa41e
`felis-grid::transcode` was described as the shared JSON engine for the
consumers that re-expose or record a daemon session, but what it
published was the direct serde rendering of felis's own Rust enums.
Two out-of-repo consumers already depend on it by git rev, so a variant
or field rename there is a silent break in a deployed browser view or in
`.fcast` files nobody can re-record, and there was no version, no
schema, and no validation that was not inherited from implementation
types.

`felis_grid::json_v1` replaces it with a named format. Dedicated DTOs
for the five families a session mirror needs mean a domain rename fails
the conversion to compile; an in-band `{"felis_json": 1, "kind", "msg"}`
envelope lets a reader refuse a version it does not know before it looks
at the payload; a generated schema and golden frames pin what a writer
emits, the `RowDelta` rows included, which is where #193's structural
row lands without blessing the rest of the serde shape.

The DTOs reach down to a dirty row's cells, the format's largest and
most-read payload, so the row codec now owns the packed bytes alone and
this format owns their JSON spelling; the crate had two spellings of a
row while `wire`'s JSON view remained, which is the drift the whole
change is against.

Admission is one rule, stated twice. A conversion refuses every input
the row codec would refuse on the way back in, so a replay cannot get a
frame past the envelope and then kill the consuming client's connection
with it: reserved bitmap bits, non-printable ASCII, a sized cell outside
the row, a zero handle. The schema states the same bounds wherever a
validator can carry them, integer widths included, since `format` is an
annotation a validator may ignore; the three structural checks it cannot
carry are named in `ipc.md` rather than left for a consumer to discover
as a decode failure it was told could not happen.

The compatibility rule is stricter than the wire's additive minor
(REQ-104b) because a recording on disk and a separately deployed browser
have no effective-minor handshake: inside a version only an optional
field readers ignore may be added, and everything wider bumps the
version. The alternative, leaving the conversion an internal helper so
each satellite owns its own DTOs, was rejected in the issue: it is the
drift a shared engine exists to prevent.

Closes #336.

Assisted-by: Claude Code
natsukium deleted branch issue-336 2026-09-22 04:49:19 +09:00
Commenting is not possible because the repository is archived.
No description provided.