protocol: carry arm routing in the descriptor, not in comments #324

Merged
natsukium merged 3 commits from arm-options into main 2026-09-21 19:57:13 +09:00
Owner

The routing row of every oneof arm now rides the protobuf descriptor as a
(felis.v1.arm) field option instead of an // arm: comment. A client generated
from felis.proto reads direction, correlation class, admitting connection modes,
phases and introducing minor from the descriptor its protoc-gen-* already builds,
so nothing about routing has to be transcribed out of Rust or out of prose.

Shape

  • felis.proto gains message ArmRouting (nested Direction, Correlation,
    Mode, Phase enums, plus since) and
    extend google.protobuf.FieldOptions { ArmRouting arm = 50001; }, 50001 sitting
    in protobuf's private extension range. All 86 // arm: comment lines are gone;
    each arm carries the option instead, in the layout buf format canonicalizes
    (one text-format entry per line, closed by }];).
  • The schema header's grammar paragraph is rewritten for the option, and says that
    buf breaking cannot see it.
  • ArmMeta::declaration() emits the option body (entries joined by spaces) instead
    of the comment line, deriving the DIRECTION_ / CORRELATION_ / MODE_ /
    PHASE_ prefixes from the existing tokens so there is no second token table.
    ModeSet::tokens / PhaseSet::tokens keep their lowercase spelling, which the
    driver's phase diagnostics use.
  • the_schema_declares_the_same_arm_table and
    the_schema_spells_no_arm_into_the_preface parse the option block rather than
    the comment. Both were checked against a deliberately broken schema: dropping a
    modes: entry fails the table comparison, and deleting an arm's whole option
    fails with "ConnMsg arm carries no (felis.v1.arm) option".
  • just proto: prost ignores extensions, so the generated diff is the ArmRouting
    message and its nested enums, plus the loss of the arm doc comments the deleted
    // arm: lines used to produce. No message shape, field number, or wire byte
    changes.

Rejected

  • A routing manifest beside the schema. It is the shape the architecture
    overview already turns down for the wire constants: a second committed file to
    keep in step, holding what the descriptor carries anyway.
  • Generating ArmMeta from the schema. One statement of the row leaves the
    driver's table nothing to disagree with, and the comparison of two independent
    statements is the check.
  • Reusing ConnectionMode for modes. That enum is wire vocabulary a major may
    renumber; a routing row is not a message field, so ArmRouting.Mode is its own.

The cost of keeping two statements is the gate: buf breaking compares field
numbers, names and types and never reads options, so the Rust test is the only check
a wrong routing row fails. That is recorded in docs/reference/ipc.md "The arm
table", the buf breaking row of docs/reference/testing.md "Wire compatibility
gates", docs/explanation/architecture/ipc.md (with the rejected alternatives),
docs/explanation/architecture/overview.md "Wire constants",
docs/reference/workspace.md "Cross-language wire contract", and the extend-ipc
skill.

Gates

  • cargo clippy --workspace --all-targets --all-features: clean.
  • cargo nextest run -p felis-protocol -p felis-daemon -p felis-client-core -p felis-cli:
    1611 passed. -p felis-transport (the other tokens() consumer): 154 passed.
  • just proto-compat: wire-compatible with the base.
  • just docs-links: 0 errors.
  • python3 tools/prose_check.py --range origin/main...HEAD: 0 hits.
  • Pre-commit hooks ran in full, buf breaking included, and passed. No SKIP= was
    needed: the option change is invisible to it, which is the point the docs now make.

Closes #303

The routing row of every `oneof` arm now rides the protobuf descriptor as a `(felis.v1.arm)` field option instead of an `// arm:` comment. A client generated from `felis.proto` reads direction, correlation class, admitting connection modes, phases and introducing minor from the descriptor its `protoc-gen-*` already builds, so nothing about routing has to be transcribed out of Rust or out of prose. ## Shape - `felis.proto` gains `message ArmRouting` (nested `Direction`, `Correlation`, `Mode`, `Phase` enums, plus `since`) and `extend google.protobuf.FieldOptions { ArmRouting arm = 50001; }`, 50001 sitting in protobuf's private extension range. All 86 `// arm:` comment lines are gone; each arm carries the option instead, in the layout `buf format` canonicalizes (one text-format entry per line, closed by `}];`). - The schema header's grammar paragraph is rewritten for the option, and says that `buf breaking` cannot see it. - `ArmMeta::declaration()` emits the option body (entries joined by spaces) instead of the comment line, deriving the `DIRECTION_` / `CORRELATION_` / `MODE_` / `PHASE_` prefixes from the existing tokens so there is no second token table. `ModeSet::tokens` / `PhaseSet::tokens` keep their lowercase spelling, which the driver's phase diagnostics use. - `the_schema_declares_the_same_arm_table` and `the_schema_spells_no_arm_into_the_preface` parse the option block rather than the comment. Both were checked against a deliberately broken schema: dropping a `modes:` entry fails the table comparison, and deleting an arm's whole option fails with "ConnMsg arm carries no `(felis.v1.arm)` option". - `just proto`: prost ignores extensions, so the generated diff is the `ArmRouting` message and its nested enums, plus the loss of the arm doc comments the deleted `// arm:` lines used to produce. No message shape, field number, or wire byte changes. ## Rejected - **A routing manifest beside the schema.** It is the shape the architecture overview already turns down for the wire constants: a second committed file to keep in step, holding what the descriptor carries anyway. - **Generating `ArmMeta` from the schema.** One statement of the row leaves the driver's table nothing to disagree with, and the comparison of two independent statements is the check. - **Reusing `ConnectionMode` for `modes`.** That enum is wire vocabulary a major may renumber; a routing row is not a message field, so `ArmRouting.Mode` is its own. The cost of keeping two statements is the gate: `buf breaking` compares field numbers, names and types and never reads options, so the Rust test is the only check a wrong routing row fails. That is recorded in `docs/reference/ipc.md` "The arm table", the `buf breaking` row of `docs/reference/testing.md` "Wire compatibility gates", `docs/explanation/architecture/ipc.md` (with the rejected alternatives), `docs/explanation/architecture/overview.md` "Wire constants", `docs/reference/workspace.md` "Cross-language wire contract", and the `extend-ipc` skill. ## Gates - `cargo clippy --workspace --all-targets --all-features`: clean. - `cargo nextest run -p felis-protocol -p felis-daemon -p felis-client-core -p felis-cli`: 1611 passed. `-p felis-transport` (the other `tokens()` consumer): 154 passed. - `just proto-compat`: wire-compatible with the base. - `just docs-links`: 0 errors. - `python3 tools/prose_check.py --range origin/main...HEAD`: 0 hits. - Pre-commit hooks ran in full, `buf breaking` included, and passed. No `SKIP=` was needed: the option change is invisible to it, which is the point the docs now make. Closes #303
protocol: carry arm routing in the descriptor, not in comments
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Failing after 2m30s
darwin / detect relevant changes (pull_request) Successful in 4s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 52s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m36s
pr / nix flake check (pull_request) Successful in 1m2s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m11s
pr / wire schema is compatible with the base (pull_request) Successful in 9s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m3s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 3s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Successful in 5m31s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
ba9c1eff93
The routing row of every oneof arm was a `// arm:` comment, which no
descriptor carries: a client generated from `felis.proto` could not
learn an arm's direction, correlation class, admitting modes, phases or
introducing minor without transcribing them from Rust or from prose.
That left the cross-language reuse surface short of the contract the
driver actually enforces.

A `(felis.v1.arm)` field option puts the row where a `protoc-gen-*`
already looks. A routing manifest beside the schema was rejected for
the reason the architecture overview rejects a contract manifest at
all: a second committed file to keep in step, holding what the
descriptor carries anyway. Generating `ArmMeta` from the schema was
rejected too, since one statement of the row leaves the driver's table
nothing to disagree with, and the comparison is the check.

prost ignores extensions, so the only shape the codegen gains is the
`ArmRouting` message itself. `buf breaking` never reads options, which
makes `the_schema_declares_the_same_arm_table` the sole gate on a
routing edit; the reference and the testing gate table say so.

Assisted-by: Claude Code
protocol: pin the mode and phase vocabularies to the schema's
Some checks failed
bench / Criterion regression gate (pull_request) Failing after 2m20s
darwin / detect relevant changes (pull_request) Successful in 4s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 53s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m9s
pr / nix flake check (pull_request) Successful in 37s
pr / wire schema is compatible with the base (pull_request) Successful in 9s
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m49s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m12s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 3s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 15s
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
5e55b210c9
A connection mode is spelled in four places: the wire's
`ConnectionMode`, the descriptor's `ArmRouting.Mode`, the `ModeSet`
tokens an arm option is compared through, and the enumeration the
routing tests sweep. Nothing held them together, so a mode could be
routed in Rust while no arm option could name it, and the arm-table
comparison would still pass against options that omitted it. The same
gap sat between `ArmRouting.Phase` and the `PhaseSet` tokens. Both
vocabularies are now compared value by value, walking the generated
enums by number rather than restating them, so an addition to any one
list fails until the others carry it.

The option parser also treated a comment inside an aggregate option as
part of the row, which made a legal annotation next to an entry fail
the sync test. Entries are stripped of their comments while parsed, so
the schema keeps the freedom to explain an entry where the entry is.

The extension number is application-private: 50001 identifies the
option inside a descriptor pool and nowhere else, so a consumer
co-compiling felis with another schema that claims it separates the
pools or renumbers locally. Said next to the extend block and in the
reference, since nothing in the schema can enforce it.

Assisted-by: Claude Code
protocol: take the mode and phase value numbers from the schema
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m54s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m2s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 3s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
windows / frontend smoke (Windows) (pull_request) Successful in 1m52s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Failing after 2m15s
darwin / detect relevant changes (pull_request) Successful in 3s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 52s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m9s
pr / nix flake check (pull_request) Successful in 40s
pr / wire schema is compatible with the base (pull_request) Successful in 11s
windows / cargo nextest (Windows) (pull_request) Successful in 4m39s
9c7eba31a1
The vocabulary comparisons probed enum numbers 0..64, a bound nothing
held: a value numbered outside it would be dropped from both sides of
the comparison, which would then pass while claiming to have compared
every value. The numbers now come from the schema text the test
already reads, so the set being compared is the set the schema
declares, at any numbering, and a generated enum missing one of them
fails by name.

Assisted-by: Claude Code
natsukium deleted branch arm-options 2026-09-21 19:57:13 +09:00
Commenting is not possible because the repository is archived.
No description provided.