Skip to content

Collapse the zone vocabulary/binding split: zones are floor-level coordinates, unbound goes - #117

Merged
MJohnson459 merged 3 commits into
mainfrom
zone-floor-coordinates
Sep 11, 2026
Merged

Collapse the zone vocabulary/binding split: zones are floor-level coordinates, unbound goes#117
MJohnson459 merged 3 commits into
mainfrom
zone-floor-coordinates

Conversation

@MJohnson459

Copy link
Copy Markdown
Contributor

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 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.

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

  • Storage. vocabulary.yaml + binding.yaml collapse into zones.yaml
    (name, note, navigable, x/y/yaw, optional radius/polygon,
    source). 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.
  • Spec shapes become serialisers. 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 there, never stored.
    split() and 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; mission/v0's
    unresolved_zone carries unknown_name and ambiguous only. Local
    extensions go with it: every zone on the floor is the floor's.
  • anchor goes (the addendum). 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 and _ANCHOR_METHOD is the one place that mapping lives.
  • No transition code. 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 BINDING_YAML,
    VOCABULARY_YAML, _read_split_pair, zone.source_from_anchor and their
    tests are gone rather than carried. Every docstring and doc now describes the
    one layout that exists.
  • 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.
  • The fleet server's three zone reads are three views of one floor read.
    /v1/maps/<s>/<f>/zones.json keeps its published-map gate (it is served
    beside a basemap); /v1/zones stays ungated and serves the names-only view,
    built from VOCABULARY_KEYS rather than stripped — the safety property
    worth keeping from the split.

mote-01

Its floor was rewritten through the new reader/writer:
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.

Those seven rooms carried human names in the retired display_name — Living
Room, Bedroom, Bathroom, Kitchen, Guest Room, Office, Hallway — and #609 drops
that field on read rather than promoting it to name, so since that commit the
robot had answered only to room_01..room_07 and goto kitchen failed 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, and
mote_tasks.zones.load_zones resolves every name case-insensitively —
goto kitchen works. The original is on the robot as zones.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, carrying
room_01..room_07). Its bytes back an announced digest and must not be
rewritten; the robot's next publish-map supersedes it.

Verification

  • 729 passed, 17 skipped across 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.
  • Every committed zones.yaml (3 sim worlds, 3 sim site bundles, the packaged
    default) round-trips through write/read with identical geometry and no leak
    into the names-only view.
  • test_spec_conformance.py against the real augereai-spec checkout:
    10 passed, 1 xfailed.

The xfail

New on test_a_vocabulary_conforms: zone/v0's vocabulary schema still requires
kind, which #609 retired, so Mote's /v1/zones payload does not validate
against it. That divergence predates this work — the test had never
actually run here, because the pixi dev env has no jsonschema and the spec
checkout is not a sibling of a worktree, so it always skipped. Two latent
failures were in it; the other (a stale kind= kwarg that would TypeError)
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.

MJohnson459 and others added 3 commits September 2, 2026 15:43
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
MJohnson459 merged commit f440e46 into main Sep 11, 2026
6 checks passed
@MJohnson459
MJohnson459 deleted the zone-floor-coordinates branch September 11, 2026 12:15
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant