Repository navigation
route-oracle: measure a plan's stored route against the navmesh - #274
Merged
Merged
Conversation
The audit's Stage 1 needs a fixture per dungeon plan that says how far the plan's stored route and what the server's navmesh plan are apart, leg by leg. The command builds the objective chain (entrance, then per leg its boss, else its to/last step point) and the stored polyline (the plan's own points in the order planexport writes them), plans every leg twice (plain, and on a scratch planner with SetAvoid of the plan's avoid boxes, whose path the metrics use), and reports status, end gap, live and stored length, ratio, stored points' distance to the live path, Z jumps over 8 yd, the breaks touched (exact/direct/gm_cross/check_skip vias within 6 yd of the path, or within 10 yd of a partial path's end), pull groups' corridor membership (radius + 18 yd, dz 8) and redundant approach/via points (within 5 yd), with the SHA-256 of every mmap file used. internal/routeoracle holds the report; its tests plan over a synthetic navfixture mesh, so no real mmaps are needed.
pjerra
marked this pull request as draft
October 20, 2026 18:27
The review's three highs: - The live route and the stored route were two different walks: boss legs planned previous objective to boss position directly, while the stored length counted the via points, the to, every pull group's approach points and centre, and the boss approaches. The oracle now plans the driver's own ordered sequence (via, to, pull approaches and pull centres or holds, step stands, boss approaches, boss, after-boss stands), hop by hop, and scores each stored hop against its own navmesh hop: per-segment rows in the JSON, per-leg sums in the table. - A partial path poisoned the numbers behind it: a hop that does not path complete now fails its leg; the leg's remaining hops are planned on from the point the failed path reached and marked non_comparable, the legs after a failed leg are indeterminate, and every aggregate (ratio, packs, breaks, redundancy) is over the complete legs only. - The report is written under the plan's name, which the command read from the plan JSON: a name like ../escape would write outside --out. The plan's own name is validated with the same strict basename regex as --plan, it must equal the requested plan name, and the resolved destination must stay under --out. Tests: a boss leg with to and pull groups (the walk and its ratio), a partial leg followed by another leg (indeterminate), a traversal name in the plan JSON (refused, nothing written outside --out), a name mismatch, and the end-to-end fixture run; each fails if its check is removed.
… points, no break hops The walk mode (still the default) proves the stored hops are walkable; it cannot say whether a route could be planned from a boss list. The bosses mode plans each leg once, avoid-aware, from the previous objective to the leg's boss (else its to or last point), skipping nothing. - A leg the mesh does not reach is FAILED, adds nothing to any aggregate, and names the stored break it hit: a break hop whose foot the mesh reaches from the start and whose landing reaches the objective, or one within 15 yd of the live end or of the line to the objective. - The leg after a failed one is planned from the last point reached and is labelled non-comparable and excluded; it is also planned from the previous objective (bridged), reported beside the primary numbers, never in them. - Complete legs report packs whose centre the path passes, hand points within 10 yd of it, floor jumps and live length. No stored/live ratio. - Files are <plan>-bosses.txt and .json; an unknown --mode is a usage error. Tests: a fixture mesh with a gap fails the leg and names the break; a failed leg and the leg after it stay out of the totals; a break far from the end is named because it bridges; the mode defaults to walk.
The containment check was lexical: a <plan>.txt or .json that already existed as a symlink to a file outside --out passed it, and os.WriteFile then truncated the target. - --out that is, or has a component that is, a symlink is a usage error; checked before and after the folder is made. - A destination that exists as anything but a regular file (a symlink, even a dangling one, or a folder) is refused, both checked before either file is written. - Each file is written to a fresh O_EXCL temp file through an os.Root on the folder and renamed over the target, so a link planted after the check is replaced, not followed. Tests: symlinked report (walk and bosses, .txt and .json, live and dangling target), folder as report, symlinked --out and --out component, and the rename not following a link.
…ack definition - --objectives plan (default) keeps each leg's own objective; --objectives bosses plans only entrance -> boss 1 -> boss 2 ..., folding a leg without a boss into the next boss, counting every stored to point as a hand point, and not planning legs after the last boss. Files: <plan>-bosses-only.*. Needs --mode bosses. - A pack is on the path by one definition in both modes (GroupOnRoute: a position within radius + 18 yd, 8 yd of height); the bosses mode no longer uses the centroid within the bare radius. - A pull group with no position and no hold is skipped (the walk no longer plans to the NaN centroid of nothing) and counted as empty_groups. - Break hits carry the stored leg; README documents the modes, the symlink refusal of --out and the accepted ancestor-rename limit. Tests: pack boundaries at radius + 18 and 8 yd of height, the same pack verdict in both modes, the boss-only chain (folding, to as hand point, unplanned legs, continuation), empty groups, the flag checks.
…rows always say the stored leg - GroupOnRoute reported the last position's distance while a group was off the path; it now tracks the minimum over all positions. No on/off verdict changes; only near/dz figures of off-path groups. - PullOnPath, HandPoint and BreakTouched carry the stored leg as a pointer, so leg 0 is written in a boss-only row too. Tests: a nearest position that is not the last, and a two-leg folded row whose items name stored leg 0 and 1 (a non-zero index), in the JSON too.
pjerra
marked this pull request as ready for review
October 20, 2026 20:59
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.
Summary
turtle-agent route-oracle --plans dungeons --mmaps DIR --out DIR [--plan NAME]...compares a dungeon plan's stored route with what the server's navmesh plans for the same dungeon, leg by leg, and writes<out>/<plan>.txt(human table) and<out>/<plan>.json(machine report, Stage 1's fixture). Without--planit reports the five mapped plans: wailing_caverns, ragefire_chasm, uldaman, zulfarrak, deadmines.Per plan:
to, else its last step's point); per leg the walk from the previous objective is planned twice — plain, and on aScratch()planner withSetAvoidof the plan's avoid boxes. The avoid-aware path is the one the metrics use, since it is what a run plans with.Per leg: status, end gap, live length (both runs), stored length, ratio, max and mean distance of the stored points to the live path, Z jumps over 8 yd (corners and ground samples), breaks touched (
viawith exact/direct/gm_cross/check_skip within 6 yd of the path, or within 10 yd of a partial path's end), pull groups' corridor membership (group radius + 18 yd, dz ≤ 8) and redundant approach/via points (within 5 yd of the path), and the plan time in ms; the report is written under the plan's name, which is validated with the same strict basename regex as --plan and must equal the requested plan name (the destination stays under --out). The report names the SHA-256 of every.mmap/.mmtilefile the planning used.What changed
internal/routeoracle(report, metrics, thresholds); tests plan over a synthetic mesh (internal/navmesh/navfixture) — no real mmaps needed.cmd/turtle-agent/route_oracle_cmd.go, wired intomain.go(exit 12 when a plan could not be reported).No plan or exporter file is touched.
Tests
~/TortoiseBots-audit/tools/vmtest.sh ~/wt-ta-route-oracle 'make test'→ MAKE-TEST-OK (gofmt check, vet, no-cgo build,go test -race ./...;internal/routeoracleok, cgo tests included on the VM).make build(CGO_ENABLED=1) on the laptop, then one run over the five plans against the server's mmaps; the five summary lines:Every leg of the five plans paths complete on the exported sequence (69/69 legs; the WC serpentis ramp's
direct,check_skiphop is the plan's own exemption and is not measured on the mesh). The plan's route is described in one walk now — the legs' live hops and stored hops pair per row.Live check
not run (the oracle is static: plans and mmaps only).
Bosses mode and output safety (added)
--mode bossesplans each objective once on the live navmesh with no hand point and no break hop skipped;--objectives plan(default) uses each leg's own objective (boss, else its storedto),--objectives bossesplans only the chain entrance -> boss 1 -> boss 2 ... Failed objectives score nothing and name the stored break they hit. Both modes use one pack-on-path definition (a position within radius + 18 yd, 8 yd of height). See the README ("Route oracle") for the flags.--outis refused when it is or contains a symlink (a host where/tmpor/homeis a symlink must name the real folder); a report file that exists as a symlink/folder is refused; files are written to a temp file and renamed.OpenRootcould redirect the writes. This is a local, owner-run tool; the limit is accepted.