protocol: bind the gated-field list to the call sites it claims #233

Merged
natsukium merged 1 commit from issue-175 into main 2026-09-08 21:56:04 +09:00
Owner

GATED_FIELDS recorded every post-baseline field twice — once in a hand-written list, once at the Requires::when call site inside the owning requires() arm — with nothing binding the two. Deleting a call site left the field listed and claimed by the prose ledger while the send gate quietly stopped authorizing it, and the ledger-coverage test could not see it because it walked the list rather than the gate.

Each gated field is now one table entry pairing its requirement with the predicate that decides whether a value carries it (GatedField<T>), and a requires impl folds its owner's table instead of naming fields one by one. The list gated_fields() reports is derived from those entries, so a field cannot be listed without a call site. A new per-field test (every_gated_field_is_authorized_where_it_is_carried) closes the remaining freedom: for each field it routes a message that omits it and a message that carries it through the owning impl, requiring the carrier to answer exactly the field's declared requirement. Removing a table fold from any requires impl fails it, naming the field.

just check passes (fmt, clippy, 3428 nextest tests, deny, proto-compat, release self-test).

Fixes #175

`GATED_FIELDS` recorded every post-baseline field twice — once in a hand-written list, once at the `Requires::when` call site inside the owning `requires()` arm — with nothing binding the two. Deleting a call site left the field listed and claimed by the prose ledger while the send gate quietly stopped authorizing it, and the ledger-coverage test could not see it because it walked the list rather than the gate. Each gated field is now one table entry pairing its requirement with the predicate that decides whether a value carries it (`GatedField<T>`), and a `requires` impl folds its owner's table instead of naming fields one by one. The list `gated_fields()` reports is derived from those entries, so a field cannot be listed without a call site. A new per-field test (`every_gated_field_is_authorized_where_it_is_carried`) closes the remaining freedom: for each field it routes a message that omits it and a message that carries it through the owning impl, requiring the carrier to answer exactly the field's declared requirement. Removing a table fold from any `requires` impl fails it, naming the field. `just check` passes (fmt, clippy, 3428 nextest tests, deny, proto-compat, release self-test). Fixes #175
protocol: give a gated field one home in the minor ledger
All checks were successful
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) Successful in 2m9s
darwin / detect relevant changes (pull_request) Successful in 3s
darwin / darwin docs-only no-op (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m10s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m46s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 54s
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
darwin / build felis (aarch64-darwin) (pull_request) Successful in 50s
pr / nix flake check (pull_request) Successful in 29s
pr / wire schema is compatible with the base (pull_request) Successful in 8s
windows / cargo nextest (Windows) (pull_request) Successful in 4m38s
windows / frontend smoke (Windows) (pull_request) Successful in 1m55s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
d9c2efa85b
The gated-field list and the `Requires::when` calls the send gate reads
were written separately, so deleting a call site left the field listed,
claimed by the prose ledger, and no longer authorized by anything --- a
gap the ledger-coverage test could not see because it walked the list,
not the gate.

Each field is now a table entry pairing its requirement with the test
that decides whether a value carries it; a `requires` impl folds its
owner's table instead of naming fields one by one, so the list is
derived from the same entries the gate reads. A per-field sample pair
pins the remaining freedom: a message that carries the field must answer
exactly the field's requirement, which fails the moment an impl stops
folding its table.

Assisted-by: Claude Code
natsukium deleted branch issue-175 2026-09-08 21:56:04 +09:00
Sign in to join this conversation.
No description provided.