[v0.1/P1] Publish CLI and bridge JSON schemas with golden conversations #29
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis#29
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent: #12 (P1.8). Related to #3 and #9.
Why
The protobuf socket and TOML config have machine-readable schemas, but the public non-protobuf extension path exists only as Rust serializers and prose. Consumers cannot validate requests or outputs independently.
Scope
params.Acceptance criteria
skills/felislink to the versioned schema.Triage plan (2026-09-03)
Source-grounded triage against
mainat69076d42, reviewed through seven rounds of an independent reviewer (pisol/luna) until it passed with no findings. The dependency order that supersedes the tracker's is posted on #12.Claim check
Accurate at HEAD.
crates/felis-protocol/proto/felis.proto(REQ-111,docs/reference/spec.md:65); the config iscrates/felis-client-core/felis-config.schema.json, generated by a schemars sync test behind theschemafeature (justfile:143-151,crates/felis-client-core/Cargo.toml:26-41,docs/reference/config.md:59-87). The CLI/bridge objects are serde structs only:cli_output.rs:203-706(envelopes,SessionObject, results,ErrorKind),cli_bridge.rs:334-360(Envelope,Body), request parsing by hand inparse_request/Params(cli_bridge.rs:398-470). No schema file or schema mention exists for them (grep -n "schema" docs/reference/cli.md docs/reference/ipc.mdfinds none).felis-clihas no schemars dependency (crates/felis-cli/Cargo.toml).docs/reference/cli.md:87-130(envelope table, kind list),615-735(bridge shapes, ops, params, bounds),ipc.md:1624("Non-Rust clients: the stdio bridge").crates/felis-cli/tests/cli_bridge.rscovers correlation (276), two concurrent streams each ending once (314), malformed line (488), stdin EOF cancels (531), daemon loss for point (574) and for every outstanding op (663), id reuse (615), cold socket (712), cancel (902), clean terminalcount(949). They are Rust assertions over live output, not committed JSONL. Missing entirely: stdout loss (owned by #21's behavior change), failure before the first item on a bridge stream (the CLI has it attests/cli_sessions.rs:326, the bridge does not), and any reordering-tolerant golden for concurrent replies.parse_requestchecksv,idtype,optype,paramsis object;rows/colsare bounded to0..=65535bridge-side (cli.md:686-694); unknownparamskeys are ignored (theParamsaccessors read by key). So "unknown fields … fail fixtures" is a new rule, not a documented one.Nothing already done; nothing wrong in the issue.
Verdict
accept-with-changes.
schemars::JsonSchemaon thecli_output.rsand bridge types behind aschemafeature infelis-cli, with aUPDATE_SCHEMA=1sync test and ajust schemaextension. A hand-written bundle would drift from the serde types the way prose already does.Params<'_>accessors with per-op#[derive(Deserialize, JsonSchema)] #[serde(deny_unknown_fields)]structs. That yields the request schema for free and makes the bridge refuse unknown fields at runtime asmalformed_request, so the fixture rule and the binary agree. This is the one behavior change; it is pre-freeze and explicit (principle 4) and should be stated in CHANGELOG.Principle check: no capability is added; the schema is a description of an existing surface (principle 1 pass); the strict-params rule removes a silent no-op (principle 4 pass).
Approach
Schema bundle
felis-cli:schema = ["dep:schemars", "felis-protocol/schema"]feature; deriveJsonSchemaonSessionObject,AttachmentObject,LastNotificationObject,SessionRef,TagResult,SwitchResult,RetargetResult,DaemonStatusResult,ResourceObject,ProtocolVersion,ConfigPathResult,CheckResult,DiagnosticObject,EffectiveConfigResult,DoctorResult,CheckObject,CaptureRow,SearchMatch,NotificationObject,MachineError,ErrorKind, and the private envelope structs (ErrorObject,EndTerminal,ErrorTerminal,LagEvent,Versioned) — plus #23'sListResult. Bridge:Envelope/Body(cli_bridge.rs:334-360) and the new typedRequest/per-op params.idaspattern: ^[0-9a-f]{32}$(SessionHex), attachmentidas^[0-9]+$(decimal string,cli_output.rs:492-495),rows/cols0..=65535on the bridge request,vasconst 1,count/droppedasu64. Use#[schemars(regex(...), range(...))].crates/felis-cli/schemas/felis-cli-v1.schema.json(one document per CLI verb class: point result union, point error, stream item union, lag, end, error terminal) andfelis-cli/schemas/felis-bridge-v1.schema.json(request, and the six envelope shapes). Versioned by theSURFACE_VERSIONepoch (cli_output.rs:35); the file name carriesv1so a future epoch adds a file rather than rewriting one.cli_schemainfelis-climirroringconfig_schema(UPDATE_SCHEMA=1writes, otherwise asserts byte equality); extendjust schema(justfile:150-151) to run both; CI's--all-featuresrun (.forgejo/workflows/pr.yml:42) executes it, which is the "deterministic and checked in CI" criterion. Pin schemars output stability through the flake's toolchain (already the case).ErrorKindin the schema is an openstringwith the known tokens as anexamples/x-known-valuesannotation, not anenum: #23 makes the vocabulary additive, and a consumer's validator must not reject a newer kind.Fixture validation
jsonschemacrate, orboon; checkcargo deny checklicenses first). One helperassert_valid(schema_ref, &Value).tests/cli_sessions.rsparse_point/parse_jsonlat271-299,tests/cli_bridge.rsassert_surface_versionat254) calls the validator, so every documented object is exercised against the bundle by the tests that already produce them (acceptance criterion 1).crates/felis-cli/tests/fixtures/schema-invalid/*.json: a 31-hex id, an uppercase id, a numeric attachment id,rows: 65536, an unknown param key, a missingv, av: 2; the test asserts each fails validation and (for requests) that the bridge answersmalformed_requestwhen fed the same line.Golden conversations
crates/felis-cli/tests/fixtures/bridge/<name>.jsonl: each line{"dir":"in"|"out", …object…}; a harness intests/cli_bridge.rsfeeds theinlines, collects stdout until the expected terminal count, masksid-valued session hex and timestamps, groupsoutlines by requestid, and diffs each group's sequence against the fixture; across groups it diffs as a multiset. Scenarios: concurrency (two streams + one point interleaved), cancel racing a terminal (cancelafter the stream already ended →malformed_requestor no-op, whichever #21/#24 settle), daemon loss mid-stream (fixture daemon killed after N items; expects onedaemon_lostterminal per open op and exit 2), stdout loss (Stdio::piped()reader closed early; expects exit code per #21), failure before the first item (sessions.captureon an unknown id → oneerrorterminal, noitem), reordered concurrent replies (two point requests whose replies arrive in either order; multiset check).cli.md"Other verbs" and as harness assertions: exactly one terminal per id, id free after terminal, no object after terminal.Cascade
docs/reference/cli.md: "Machine output" and the bridge bullet link the two schema files with a raw URL likeconfig.md:83; a sentence on the fixtures' role.docs/reference/ipc.md:1624bridge section links the request schema.docs/reference/testing.mdgains the fixture layout and theUPDATE_SCHEMArecipe.docs/explanation/architecture/control-surfaces.md"Machine output" records: generated from the serde types (rejected: hand-written, drifts), openkind(rejected: enum), strict request params (rejected: lenient, silent no-op).skills/felis/SKILL.mdlinks the schema for consumers writing validators.CHANGELOG.md: added schemas; bridge now refuses unknown request fields..claude/skills/add-config-key/extend-ipcskills:extend-ipcmust say "regeneratejust schemaafter anycli_output.rschange" (skills go stale like docs).Dependencies
Must land after every issue that changes a shape it would freeze: #23 (list as point,
short_idremoval,usagekind, exit table), #24 (retarget result fields), #20 (spawn correlated ops, changes bridge spawn failure shape), #21 (bridge bounds and stdout-loss behavior, which one golden conversation encodes), #26 (daemon statusscopesemantics), #50 (whatunsupportedmeans on the bridge). #12's order (step 10, "after the bridge, CLI, and minor-evolution contracts settle") still holds. #35 (post-v0.1 class rule) is not a dependency; the schema's per-class documents make the classification checkable.Risk/effort
L. The schemars derive and sync test are S; typing the bridge params is M (touches every op arm in
cli_bridge.rs); the golden harness with normalization is the bulk and the flake risk. Main risk: brittle fixtures from timing (mitigated by per-id grouping and multiset comparison, and by using the existing quiet fixture daemon attests/cli_bridge.rs:89-120). Secondary: a validator crate that failscargo denyor drags a large tree into dev-deps; check before choosing.Labels
Keep priority/P1, release/v0.1.0. If the schedule tightens, the schema bundle plus fixture validation is the part that must precede the tag (it is what makes epoch 1 checkable); the reordered-reply and stdout-loss conversations could follow as v0.1.x work without reopening the contract, but only if #21's behavior is at least covered by its own tests.
sourceandattachmentstrings the runtime rejects, so schema-valid requests can still fail as malformed #178sourceandattachmentstrings the runtime rejects, so schema-valid requests can still fail as malformed #178