Closing the Map Update Loop with Automated Repair

Everything up to this point produces evidence. This stage is where evidence becomes an edit to a map vehicles drive on, and it is the only place in change detection and map maintenance where being wrong has a physical consequence.

The design that makes automation acceptable is a single asymmetry: automation may narrow what the map permits and may never widen it. Everything else here — the re-gating, the probation window, the rollback — follows from taking that rule seriously.

The asymmetry, and what each direction costs when the automation is wrong:

Why Automation May Narrow and May Not Widen Two panels comparing the failure cost of a wrong narrowing edit and a wrong widening edit, with the routing decision each implies. narrowing close a lane · remove a connection shrink a drivable area if the automation is wrong: a detour — recoverable, and visible in routing applied automatically, after re-gating with a probation window and an armed rollback widening open a lane · add a connection grow a drivable area if the automation is wrong: a vehicle drives somewhere nobody checked queued for a surveyor, at any confidence fleet agreement does not substitute for a survey the rule is about the direction of the edit, not about how confident the evidence is

Prerequisites #

  • Python 3.10+, plus the map build and gate toolchain.
  • Input: promoted candidates from scoring and triaging map change candidates, and write access to a branch of the map.
  • Upstream stage: triage; nothing reaches here that has not cleared the confidence threshold.
  • Output: a repaired tile in a signed release, or a surveyor task, plus an armed rollback.

Step-by-Step #

1. Classify the edit by direction #

python
NARROWING = {"remove_lane", "remove_connection", "narrow_lane",
             "close_lane", "remove_turn_permission"}
WIDENING = {"add_lane", "add_connection", "widen_lane",
            "open_lane", "add_turn_permission"}

def route(candidate) -> str:
    kind = candidate["kind"]
    if kind in NARROWING:
        return "automate"
    if kind in WIDENING:
        return "surveyor"
    raise KeyError(f"{kind!r} is unclassified — decide its direction before shipping")

Raising on an unclassified kind is deliberate. A new candidate type appearing in the pipeline is a decision about safety, and a default of either kind is wrong: defaulting to automate ships an unreviewed widening, defaulting to surveyor silently disables automation for a whole class.

2. Apply narrowing edits on a branch #

python
def apply_narrowing(tile, candidate):
    """Apply the edit to a branch of the map, never to the released tile."""
    branch = tile.branch(f"auto/{candidate['id']}")
    if candidate["kind"] in {"close_lane", "remove_lane"}:
        branch.set_lane_access(candidate["feature_id"], access="none",
                               reason=candidate["id"])
    elif candidate["kind"] == "remove_connection":
        branch.remove_connection(candidate["feature_id"], reason=candidate["id"])
    else:
        branch.narrow_lane(candidate["feature_id"], candidate["offset_m"])
    return branch

Recording the candidate identifier as the edit's reason is what makes the repair reversible and auditable — the provenance record in producing ISO 26262 traceability for map artefacts then names an automated repair and the evidence behind it, rather than an anonymous edit.

3. Re-gate the whole tile #

python
def gate_branch(branch, criteria) -> list:
    """Full release gate — not a subset chosen because the edit looked local."""
    reports = build_and_validate(branch)
    return run_gate(reports, criteria)

A closed lane can orphan a neighbour, break a route across a junction, or leave a dangling connection three lanes away — none of which is visible from the edit. Running the full gate makes a repaired tile indistinguishable from a surveyed one as far as the release process is concerned, which is the property that lets the two share a pipeline.

4. Arm a rollback on the same signal #

python
from dataclasses import dataclass

@dataclass(frozen=True)
class Probation:
    candidate_id: str
    tile: str
    window_days: int = 7
    clear_threshold: float = 0.2      # residual rate relative to pre-repair

def evaluate_probation(p: Probation, signal_before, signal_after) -> str:
    if signal_after / max(signal_before, 1e-6) <= p.clear_threshold:
        return "confirm"                      # the residual cleared — repair was right
    if signal_after >= signal_before:
        return "revert"                       # nothing improved, or it got worse
    return "extend"

The fleet verifies its own repair: a correct edit makes the residual that triggered it disappear, and an incorrect one leaves it — or produces a new one where the map now disagrees with the road. Nothing else is available at this cadence, and it happens to be a strong test.

The three probation outcomes on a week of automated repairs:

Probation Outcomes for One Week of Automated Repairs Three groups of repairs plotted by their post-repair residual rate relative to before, with the confirm, extend and revert thresholds marked. 00.20.5 0.81.0+ residual rate after repair, relative to before 27 confirmed 4 extended 3 reverted the three reverted candidates return to the surveyor queue rather than being deleted — they described something

Verification & Acceptance Criteria #

python
def assert_repair_loop(applied, gates, probations) -> None:
    for a in applied:
        assert a["kind"] in NARROWING, f"{a['id']}: automation widened the map"
        assert gates[a["id"]] == [], f"{a['id']}: shipped with gate findings"
        assert a["reason"] == a["id"], f"{a['id']}: edit has no provenance"

    for p in probations:
        assert p.window_days >= 7, "probation shorter than a weekly traffic cycle"
        assert p.rollback_armed, f"{p.candidate_id}: no rollback armed"

Acceptance gate: every automated edit in the narrowing set; zero gate findings on any shipped repair; every edit carrying its candidate identifier as provenance; and a rollback armed with a window of at least a week, because traffic patterns are weekly and a shorter window measures the wrong thing.

Which checks a repaired tile has to pass, and why the list is the full release gate rather than a subset:

Why a Repaired Tile Runs the Full Gate Four rows pairing a release check with whether a narrowing edit can break it and why it is run anyway. one closed lane, four checks connectivity can a neighbour be orphaned? yes — it may have been the only successor must re-run junction reachability can a route be broken? yes — three lanes away, invisibly must re-run accuracy can geometry move? no — but the run is seconds run it anyway coverage do lane-metres change? yes — by construction must re-compute running only the checks that seem relevant is how an automated repair ships a topologically broken tile

Common Errors & Fixes #

An automated repair opened a lane. A candidate kind was classified as narrowing when it widens. The classification is a safety decision — review it in the same way as a release criterion.

A repaired tile broke routing three lanes away. Only a subset of the gate was run. Run all of it; one tile is seconds.

A reverted repair was deleted along with its candidate. The candidate described something real even if the repair was wrong. Return it to the surveyor queue.

Probation always confirms. The signal being watched is not the one that triggered the repair — usually a fleet-wide average rather than the tile's own residual rate. Scope the signal to the repaired region.

Automated repairs pile up on one tile. Each is applying to a fresh branch of the released tile, so they do not compose. Apply sequentially to the same branch, re-gating after each, or batch them into one edit.

FAQ #

What is the narrowing-only rule? #

Automation may only reduce what the map permits — close a lane, remove a connection, narrow a drivable area — and never increase it. The asymmetry comes from what happens when the automation is wrong: a wrongly closed lane costs a detour, while a wrongly opened one asserts that a vehicle may drive somewhere nobody has checked. That is the claim a survey exists to make, and no amount of fleet agreement substitutes for it.

Does a repaired tile need the whole gate re-run? #

Yes. A narrowing edit is local in intent and not local in effect: closing one lane can orphan another, break a route across a junction, or leave a dangling connection three lanes away. Running only the checks that seem relevant is how an automated repair ships a topologically broken tile. The full gate on one tile is seconds, which is a small price for the property that a repaired tile is indistinguishable from a surveyed one as far as the release process is concerned.

How does an automatic rollback know it should fire? #

By watching the same fleet signal that triggered the repair. If the change was real, the residual that flagged it disappears once the map is corrected; if it was not, the residual persists or a new one appears where the map now disagrees with the road. Giving the repair a probation window and reverting when the signal does not clear turns the fleet into the verifier of its own repair, which is the only verification available at this cadence.

Up one level: Change Detection & Map Maintenance — the maintenance loop this stage closes.