[v0.1/P1] Give structural session JSON an explicit contract owner #336

Closed
opened 2026-09-21 23:17:30 +09:00 by natsukium · 1 comment
Owner

Problem

felis_grid::transcode::{body_to_json,json_to_body} is described as the shared JSON engine for satellite consumers such as a WebSocket gateway and fcast, but its public shape is the direct serde representation of Rust domain enums.

That creates an unowned second public encoding:

  • Rust variant or field renames silently become downstream JSON breaks;
  • there is no format/version envelope or published schema for the full shape;
  • validation and compatibility are inherited from implementation types rather than an explicit external DTO;
  • protobuf remains the documented wire authority while satellite clients nevertheless depend on another representation.

#193 fixes one incorrect RowPayload shape, but it does not decide who owns or versions the surrounding structural JSON contract.

Decision required

Choose one of two explicit contracts before the first consumers record public data:

  1. Internal transcode helper: stop presenting the serde shape as a shared public format. Each outer protocol/file format owns a versioned envelope and DTO/schema, and this function is not a stability boundary.
  2. Versioned felis JSON format: define a named version such as felis-json.v1, with dedicated DTOs, a version envelope, generated JSON Schema, golden conversations/fixtures, and an explicit compatibility policy.

Do not treat “whatever serde currently emits for the Rust domain model” as a third option.

The CLI bridge's existing v: 1 envelope and published schemas should remain separate unless the decision explicitly makes it an instance of the same format.

Acceptance criteria

  • The owner and compatibility boundary of structural session JSON is explicit.
  • External consumers do not depend accidentally on Rust domain serde names.
  • If public, the format has a version, schema, validation tests, and golden representative frames including RowDelta.
  • If internal, docs stop describing it as the shared public JSON contract and satellite formats own their schemas.
  • #193's structural-row fix is incorporated without blessing the rest of the serde shape implicitly.
  • docs/reference/ipc.md and docs/explanation/architecture/overview.md agree on the resulting boundary.

Evidence

  • crates/felis-grid/src/transcode.rs
  • crates/felis-protocol/src/messages.rs
  • docs/explanation/architecture/overview.md
  • docs/explanation/architecture/ipc.md
  • docs/reference/ipc.md

Related: #29, #193, #326.

## Problem `felis_grid::transcode::{body_to_json,json_to_body}` is described as the shared JSON engine for satellite consumers such as a WebSocket gateway and fcast, but its public shape is the direct serde representation of Rust domain enums. That creates an unowned second public encoding: - Rust variant or field renames silently become downstream JSON breaks; - there is no format/version envelope or published schema for the full shape; - validation and compatibility are inherited from implementation types rather than an explicit external DTO; - protobuf remains the documented wire authority while satellite clients nevertheless depend on another representation. #193 fixes one incorrect `RowPayload` shape, but it does not decide who owns or versions the surrounding structural JSON contract. ## Decision required Choose one of two explicit contracts before the first consumers record public data: 1. **Internal transcode helper:** stop presenting the serde shape as a shared public format. Each outer protocol/file format owns a versioned envelope and DTO/schema, and this function is not a stability boundary. 2. **Versioned felis JSON format:** define a named version such as `felis-json.v1`, with dedicated DTOs, a version envelope, generated JSON Schema, golden conversations/fixtures, and an explicit compatibility policy. Do not treat “whatever serde currently emits for the Rust domain model” as a third option. The CLI bridge's existing `v: 1` envelope and published schemas should remain separate unless the decision explicitly makes it an instance of the same format. ## Acceptance criteria - [ ] The owner and compatibility boundary of structural session JSON is explicit. - [ ] External consumers do not depend accidentally on Rust domain serde names. - [ ] If public, the format has a version, schema, validation tests, and golden representative frames including `RowDelta`. - [ ] If internal, docs stop describing it as the shared public JSON contract and satellite formats own their schemas. - [ ] #193's structural-row fix is incorporated without blessing the rest of the serde shape implicitly. - [ ] `docs/reference/ipc.md` and `docs/explanation/architecture/overview.md` agree on the resulting boundary. ## Evidence - `crates/felis-grid/src/transcode.rs` - `crates/felis-protocol/src/messages.rs` - `docs/explanation/architecture/overview.md` - `docs/explanation/architecture/ipc.md` - `docs/reference/ipc.md` Related: #29, #193, #326.
Author
Owner

Decision: Option 2, a named versioned format with dedicated DTOs and an explicit envelope, owned by felis-grid; P1 (reviewed)

Facts that decide it: both out-of-repo consumers exist and call body_to_json directly. felis-web-gateway forwards Grid/Image/Conn/Session to a separately deployed browser view; felis-fcast persists Grid/Image in .fcast files (its header versions its own envelope and records proto_minor, but the frame payloads are the raw domain-serde shape). Both pin felis-grid by git rev. So the accidental dependency is real and already on disk, and Option 1 would make each satellite invent its own DTO for the same messages, the drift the shared engine exists to prevent. transcode today also accepts every family except Input, wider than any satellite needs, and the in-repo bridge uses it for Region rows.

What lands:

  • felis_grid::json_v1 (behind the json feature): dedicated DTO types for Grid, Image, Conn, Session, plus Input for the replay direction, converted explicitly from and to the domain types, so a domain rename fails to compile the conversion instead of changing the JSON.
  • Explicit envelope {"felis_json": 1, "kind": "<family>", "msg": <DTO>}; the version is in-band.
  • Generated JSON Schema (schemars, like the CLI's published schemas) for exactly those DTOs, checked in with a staleness test, and golden fixtures including a RowDelta with structural rows (#193), round-tripped json → wire → json.
  • Compatibility policy in ipc.md, stricter than REQ-104b because a .fcast file or a deployed browser has no effective-minor gate: within felis_json: 1 the only permitted addition is a new optional object field readers may ignore (schema additionalProperties: true, no deny_unknown_fields); a new kind, variant, enum value, rename, retype, or required semantic is felis_json: 2, writers emit the version their consumer asked for (latest for a recording), readers keep both for a deprecation window. Old-reader/new-writer fixtures: an unknown optional field decodes; an unknown variant is rejected at the version check, not deep inside a mirror.
  • The bridge-only conversions (Ops, Region, Notify, Push, Search) move to felis-cli behind the CLI's own v: 1 contract, whose published schemas already cover them; felis-grid::transcode keeps only the row recode the DTO conversion needs. The bridge envelope stays a separate contract.

Satellite follow-ups (adopt the envelope, drop their kind-name mapping) are filed in their repos after this lands. Doc cascade: ipc.md ("One encoding on the socket, JSON at a bridge"), overview.md, workspace.md, CHANGELOG.md. Independent of the proto batch.

## Decision: Option 2, a named versioned format with dedicated DTOs and an explicit envelope, owned by `felis-grid`; P1 (reviewed) Facts that decide it: both out-of-repo consumers exist and call `body_to_json` directly. `felis-web-gateway` forwards Grid/Image/Conn/Session to a separately deployed browser view; `felis-fcast` persists Grid/Image in `.fcast` files (its header versions its own envelope and records `proto_minor`, but the frame payloads are the raw domain-serde shape). Both pin `felis-grid` by git rev. So the accidental dependency is real and already on disk, and Option 1 would make each satellite invent its own DTO for the same messages, the drift the shared engine exists to prevent. `transcode` today also accepts every family except Input, wider than any satellite needs, and the in-repo bridge uses it for Region rows. What lands: - `felis_grid::json_v1` (behind the `json` feature): **dedicated DTO types** for Grid, Image, Conn, Session, plus Input for the replay direction, converted explicitly from and to the domain types, so a domain rename fails to compile the conversion instead of changing the JSON. - **Explicit envelope** `{"felis_json": 1, "kind": "<family>", "msg": <DTO>}`; the version is in-band. - **Generated JSON Schema** (schemars, like the CLI's published schemas) for exactly those DTOs, checked in with a staleness test, and **golden fixtures** including a `RowDelta` with structural rows (#193), round-tripped `json → wire → json`. - **Compatibility policy** in `ipc.md`, stricter than REQ-104b because a `.fcast` file or a deployed browser has no effective-minor gate: within `felis_json: 1` the only permitted addition is a new optional object field readers may ignore (schema `additionalProperties: true`, no `deny_unknown_fields`); a new kind, variant, enum value, rename, retype, or required semantic is `felis_json: 2`, writers emit the version their consumer asked for (latest for a recording), readers keep both for a deprecation window. Old-reader/new-writer fixtures: an unknown optional field decodes; an unknown variant is rejected at the version check, not deep inside a mirror. - The bridge-only conversions (Ops, Region, Notify, Push, Search) move to `felis-cli` behind the CLI's own `v: 1` contract, whose published schemas already cover them; `felis-grid::transcode` keeps only the row recode the DTO conversion needs. The bridge envelope stays a separate contract. Satellite follow-ups (adopt the envelope, drop their kind-name mapping) are filed in their repos after this lands. Doc cascade: `ipc.md` ("One encoding on the socket, JSON at a bridge"), `overview.md`, `workspace.md`, `CHANGELOG.md`. Independent of the proto batch.
Commenting is not possible because the repository is archived.
No description provided.