Collapse the zone vocabulary/binding split: zones are floor-level coordinates, unbound goes - #117
Merged
Merged
Conversation
A zone is a coordinate in the floor's frame — a fact about the building. The kitchen does not move. A map revision is an *estimate* of the same layout registered into that frame, so re-mapping the floor changes how well the robot knows where it is and nothing about where the kitchen is; where the two disagree it is the map that gets aligned. Zones therefore belong to the floor, in one `floors/<floor>/zones.yaml`, and neither a map revision nor one robot owns them. zone/v0's split imported a premise — "names are shared, coordinates are not, maps are never shared" — that describes a heterogeneous fleet with one SLAM frame per platform. Mote's has neither: M4 distributes one canonical revision to every robot on a floor. The split bought a state (`unbound`), a concept (local extensions), a stamp read as meaning (`platform_id`), a binding-replacement step on map install, and a promotion rule about which half a candidate carries. What changes: - `vocabulary.yaml` + `binding.yaml` collapse back into `zones.yaml`. `bundle._read_split_pair` is the only reader of the pair; the first write rewrites the floor as one file and keeps each document as `<name>.premigration`. - `spec/zone.py` keeps `vocabulary()` and `binding()` as *views* built at the wire from the single record. `platform_id`/`frame_id`/`map_revision` are filled in at serialisation, never stored. `split()`/`merge()` are gone. - `unbound` goes. A name with no coordinate is not a zone on this floor — it is a name nobody has placed, which is what `unknown_name` says. `zones.load_floor` becomes the one `load_zones`, and `mission/v0`'s `unresolved_zone` carries `unknown_name` and `ambiguous` only. - `anchor` goes. It existed to answer "does this coordinate survive a re-map?", and under this model every coordinate does. `source` (`save-zone` | `segment-map` | `editor`) replaces it — what made the zone, and nothing about what the coordinate is worth. zone/v0 still requires an `anchor.method` on a binding entry, so `zone.bound` fills one in from `source`; `_ANCHOR_METHOD` is the one place that mapping lives. - Distribution is unchanged in mechanism: a revision carries a copy of the floor's zones, promotion publishes an edit, and `sites._adopt_zones` installs the revision's copy over the floor's, keeping `zones.<old-rev>.yaml`. That function now replaces the whole *layout* rather than one file — copying a `binding.yaml` in beside a `zones.yaml` that stayed would have installed geometry `read_floor` then ignored, since it prefers the single file. - `bundle_store`'s three zone reads become three views of one floor read. `/v1/maps/.../zones.json` keeps its published-map gate (it is served beside a basemap); `/v1/zones` stays ungated and still serves the names-only view, built from `VOCABULARY_KEYS` rather than stripped — the safety property the split had that is worth keeping. - Docs: CLAUDE.md's two zone sections become one, plus the Sites paragraph, `fleet-api.md`, `docs/fleet/README.md`, `docs/robot/sites.md`, `mapping-pipeline.md`, `control-plane.md` and `mote_tasks/README.md`. The feedback owed to the spec — a shared-map fleet is the common case, and a binding is per-frame rather than per-platform — is recorded in CLAUDE.md, the spec repo not being in this checkout. Verified: `pixi run test` equivalent (736 passed, 17 skipped) and `pixi run -e dev test-fleet` green; `pixi run lint` clean; `pixi run fleet-ui-check` 47/47 with the review pane's zones unchanged; every committed `zones.yaml` in the tree round-trips through write/read with identical geometry and no leak into the names-only view. `test_spec_conformance.py` runs against the real `augereai-spec` checkout — one strict `xfail` records that its vocabulary schema still requires `kind`, which mote #609 retired, a divergence that predates this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AAT8LjmhkfuJGA6LZDV3MJ
… that shape The split landed in code and migration only ever happened on write, so no floor anywhere holds the pair — not this box, not mote-01, not the sim bundles, not the fleet server's store. The reader was guarding a case that has never occurred, so it goes rather than being carried. - `bundle.BINDING_YAML`, `VOCABULARY_YAML`, `_read_split_pair` and their `ALLOWED` entries are gone; `read_floor` reads `zones.yaml` and nothing else. `write_floor` no longer sets the pair aside as `.premigration`, and drops the `site`/`floor` parameters it never read. - `zone.source_from_anchor` and `_ANCHOR_SOURCE` had one caller, the reader. - `sites.has_zones`/`_adopt_zones`, `mapsync.publish` and `bundle_store._zones_file` look for one file. `site info` has no layout note to print. - The stored counter is `revision`, not `vocabulary_revision`: it counts a floor's zones, and there is no vocabulary document at floor level to number. zone/v0's binding *view* keeps the spec's own `vocabulary_revision` field. - `test_zone_transition.py` and the old-layout install test go with the code they covered. Every assertion in them that was about something still here — the names-only view carrying no coordinates, `source` surviving a read — was already covered in `test_zones.py` and `test_bundle.py`. - Docs and docstrings describe the one layout that exists. CLAUDE.md keeps the two live disagreements with zone/v0 (#629 for the premise, #616 for the vocabulary's still-required `kind`) and drops the account of what the split had been. mote-01's floor was rewritten through the new reader/writer at the same time: `vocabulary_revision: 1` became `revision: 1` and the fields #609 retired (`kind`, `display_name`, `aliases`, `tags`) went, 2195 bytes to 1302. All seven rooms, their outlines and their `navigable` flags came through byte-identical, asserted rather than eyeballed. The original is kept on the robot as `zones.yaml.pre-collapse`. Verified: 729 passed / 17 skipped; `pixi run lint` clean; `fleet-ui-check` 47/47; `test_spec_conformance.py` against the real augereai-spec checkout 10 passed / 1 xfailed; every committed `zones.yaml` still round-trips with identical geometry and no leak into the names-only view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AAT8LjmhkfuJGA6LZDV3MJ
Every comment and docstring this branch touched now states what the code does. The narration of what a field used to be, what a taxonomy replaced, and which document the reader stopped needing is gone from the source and lives in the pull request instead. - `spec/zone.py`: the module docstring drops the argument about zone/v0's premise and says plainly that the spec's two documents are views over the one record. `CONSTRAINT_KINDS`, `ZONE_NAME_RE`, `LEGACY_KEYS`, `SOURCES`, `_ANCHOR_METHOD` and `REASONS` describe what they hold and what reads them. `term`, `_navigable` and `ambiguities` state the rule rather than the field it came from. - `bundle.py`: `LOCAL_SITE` says which floors get the placeholder; `parse_zones` says why the editor shares the reader. - `mote_tasks/zones.py`: `append_zone`'s docstring described a transition reader this branch had already deleted — a stale claim, now the one sentence about what `path` takes. The module docstring and `Zone.label` state the accepted fields and the property's purpose. - `fleet_server.py`: the `/v1/zones` paragraph says what the route serves, without the account of how the question was answered before it existed. - `test_spec_conformance.py`: the `xfail` reason names the disagreement — the schema requires `kind`, Mote's vocabulary is name/note/navigable — and the task tracking it. - `CLAUDE.md`: the two paragraphs shaped as a change log are gone. The design record keeps the model, the rules and the live divergences from zone/v0. Verified: 729 passed / 17 skipped, unchanged from the reviewed revision; `pixi run lint` clean; `test_spec_conformance.py` against the real augereai-spec checkout 10 passed / 1 xfailed. No behaviour changed — the diff is comments, docstrings and two paragraphs of CLAUDE.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MJohnson459
added a commit
that referenced
this pull request
Sep 11, 2026
Main brought #115 (GET /v1/robots/<id> with retained state), #117 (the zone split collapsed) and #94 (arm teleop). #115 is the one that met the gate. - fleet_server.py: main still dispatched through the if/elif chain and gave `_robot` its own operator check and path parsing. The route table keeps dispatching; main's `_roster` and `_robot` replace this branch's thin ones, with `_robot` taking `robot_id` from the table and its auth from the gate. - fleetctl.py: main's `robots [<id>]` kept, the roster read now sends the token. - fleet-api.md, CLAUDE.md, README.md: main's "the roster is anonymous until M7" prose rewritten, since the roster is behind the gate here. - Tests: main's two 401 tests and two e2e roster reads pass `token=""` / the operator token, since the harness now sends one by default. Verified: mote_fleet/test 338 passed with a real mosquitto on PATH, none skipped; pre-commit clean; ui_check.py 49/49. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UcGpBYdT7QUHoy8hRH5EzZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A zone is a coordinate in the floor's frame — a fact about the building. The
kitchen does not move. A map revision is an estimate of the same layout
registered into that frame, so re-mapping a floor changes how well the robot
knows where it is and nothing about where the kitchen is; where the two
disagree it is the map that gets aligned. Zones therefore belong to the floor,
in one
floors/<floor>/zones.yaml, and neither a map revision nor one robotowns them.
zone/v0's split imported a premise — "names are shared, coordinates are not,
maps are never shared" — that describes a heterogeneous fleet with one SLAM
frame per platform. Mote's has neither: M4 distributes one canonical revision
to every robot on a floor.
Two commits: the collapse, then the removal of the transition code it had
carried, once a survey showed nothing had ever been written in the split shape.
What changed
vocabulary.yaml+binding.yamlcollapse intozones.yaml(
name,note,navigable,x/y/yaw, optionalradius/polygon,source). The stored counter isrevision, notvocabulary_revision: itcounts a floor's zones, and there is no vocabulary document at floor level to
number.
spec/zone.pykeepsvocabulary()andbinding()as views built at the wire from the single record;platform_id/frame_id/map_revisionare filled in there, never stored.split()andmerge()are gone.unboundgoes. A name with no coordinate is not a zone on this floor —it is a name nobody has placed, which is what
unknown_namesays.zones.load_floorbecomes the oneload_zones;mission/v0'sunresolved_zonecarriesunknown_nameandambiguousonly. Localextensions go with it: every zone on the floor is the floor's.
anchorgoes (the addendum). It existed to answer "does this coordinatesurvive a re-map?", and under this model every coordinate does.
source(
save-zone|segment-map|editor) replaces it — what made the zone,and nothing about what the coordinate is worth. zone/v0 still requires an
anchor.methodon a binding entry, sozone.boundfills one in fromsourceand_ANCHOR_METHODis the one place that mapping lives.happened on write, so no floor anywhere holds the pair — not this box, not
mote-01, not the sim bundles, not the fleet server's store. The reader was
guarding a case that has never occurred, so
BINDING_YAML,VOCABULARY_YAML,_read_split_pair,zone.source_from_anchorand theirtests are gone rather than carried. Every docstring and doc now describes the
one layout that exists.
floor's zones, promotion publishes an edit, and
sites._adopt_zonesinstallsthe revision's copy over the floor's, keeping
zones.<old-rev>.yaml./v1/maps/<s>/<f>/zones.jsonkeeps its published-map gate (it is servedbeside a basemap);
/v1/zonesstays ungated and serves the names-only view,built from
VOCABULARY_KEYSrather than stripped — the safety propertyworth keeping from the split.
mote-01
Its floor was rewritten through the new reader/writer:
vocabulary_revision: 1becamerevision: 1, and the fields #609 retired(
kind,display_name,aliases,tags) went — 2195 bytes to 1302. Allseven rooms, their outlines and their
navigableflags came throughbyte-identical, asserted rather than eyeballed. The original is kept on the
robot as
zones.yaml.pre-collapse.Those seven rooms carried human names in the retired
display_name— LivingRoom, Bedroom, Bathroom, Kitchen, Guest Room, Office, Hallway — and #609 drops
that field on read rather than promoting it to
name, so since that commit therobot had answered only to
room_01..room_07andgoto kitchenfailed on it.Each name is now the zone's name, which is what the record means under
place-names. Done as a one-off migration of one robot's file rather than as a
rule in the reader: a reader that renamed zones could collide two of them
silently.
Verified against the pre-migration file: same seven places, same pose, same
outline (4/4/6/6/5/4/8 corners), same
navigable, no retired field left, andmote_tasks.zones.load_zonesresolves every name case-insensitively —goto kitchenworks. The original is on the robot aszones.yaml.pre-collapse.One consequence worth knowing: promotion is what publishes a floor's zones, so
promoting an older candidate would install its copy over these names. The dev
box's fleet store still holds one such candidate (
20260802T203339, carryingroom_01..room_07). Its bytes back an announced digest and must not berewritten; the robot's next
publish-mapsupersedes it.Verification
mote_bringup,mote_tasks,mote_fleet,run against the worktree sources through the main checkout's env.
pixi run lint: clean.pixi run fleet-ui-check: 47/47, review pane's zones unchanged.zones.yaml(3 sim worlds, 3 sim site bundles, the packageddefault) round-trips through write/read with identical geometry and no leak
into the names-only view.
test_spec_conformance.pyagainst the realaugereai-speccheckout:10 passed, 1 xfailed.
The xfail
New on
test_a_vocabulary_conforms: zone/v0's vocabulary schema still requireskind, which #609 retired, so Mote's/v1/zonespayload does not validateagainst it. That divergence predates this work — the test had never
actually run here, because the pixi dev env has no
jsonschemaand the speccheckout is not a sibling of a worktree, so it always skipped. Two latent
failures were in it; the other (a stale
kind=kwarg that wouldTypeError)is fixed, and the geometry-leak assertion was split into its own test so it
still runs. #616 landing the successor revision will fail the marker loudly.
Follow-ups: #629 — the spec should hear why the split was collapsed (a
shared-map fleet is the common case; a binding is per-frame, not per-platform).
#503's lines 25–32 still need rewriting against this model rather than #620's.