feat(06-06): the guidance pump — start gate, effects dispatch, cockpit chrome, silent reroute (GREEN)

- route.ts grows the pump beside the Phase 5 graph: the injected watch arms
  with the route draw; the first fix opens the 06-02 session; the start
  gate (>= GUIDANCE_START_ALONG_M along) activates the cockpit — PX-03
  close protection, [data-guidance=active], banner + card + list, camera
  eased to the SNAPPED line, state=on_route token
- effects dispatch: haptics via fireHaptic, reroute-requested through the
  injected RoutingSource (fail-honest catch; success = setData snap-in with
  NO camera jump + list re-render; failure = calm NO REROUTE + card state
  line); arrival -> the D-14 teardown (watch stop, protection release,
  chrome clear, trip-end card center)
- END TRIP/BackButton -> the ONE confirm card; YES ABANDON = same teardown;
  CONTINUE dismisses with guidance alive; guidancePhase exposes
  none/driving/confirm/ended for the PX-01 resolver; walkBack refuses
  while the confirm owns the exit and vacates the route stack on the walk
- guidanceView (guidance.ts): banner/card/list view data composed beside
  the machine's own maneuver marks — route/ imports and calls it, never
  re-derives (the RS6 re-pin's boundary; zero banned tokens in route/)
- G1-G8 green; RS6/RS8 re-pins green; 137 passed, typecheck clean
This commit is contained in:
araharah-executor
2026-10-24 15:16:56 +08:00
parent 0316be485b
commit 14e8dfcdd9
3 changed files with 578 additions and 46 deletions
+116
View File
@@ -46,6 +46,7 @@ import type { RoutePoint, RouteStep } from "@araharah/routing/src/types.ts";
import { haversineM } from "@araharah/routing/src/geo.ts";
import { decodePolyline6 } from "@araharah/routing/src/polyline.ts";
import type { Fix } from "../location/types.ts";
import type { GlyphKind } from "./glyphs.ts";
/* ————— THE constants table (06-RESEARCH Pattern 3; replay-tuned, never inline) ————— */
@@ -436,3 +437,118 @@ function processEdges(next: GuidanceSnapshot, fix: Fix, effects: GuidanceEffect[
next.approachArmed = false;
}
}
/* ————— the chrome's view data (06-06) ————— */
/**
* guidanceView composes EVERYTHING the cockpit chrome renders from a
* snapshot — banner slots, card view fields, and the maneuver list rows —
* so the route controller never re-derives maneuver/progress data (the
* RS6 re-pin's law: guidance IMPLEMENTATION lives in web/src/guidance/;
* route/ imports and calls, never re-computes). Pure: snapshot in, plain
* data out — structurally the view types banner.ts's builders consume.
*
* The banner's distance slot is the model-derived distance TO THE NEXT
* MANEUVER MARK (D-07: never a raw fix-to-destination distance) — null
* (the honest dash) while REROUTING or in the NO REROUTE degradation; the
* RESUMING register legally shows the projection numbers. The street slot
* is the next maneuver's own step name (the road the maneuver puts you
* on); the banner builder swaps in the calm-state copy for the wait
* states regardless.
*/
export interface GuidanceListRow {
kind: GlyphKind;
street: string;
distanceM: number;
rowState: "next" | "future" | "past";
}
export interface GuidanceBannerData {
state: GuidanceState;
glyph: GlyphKind;
distanceM: number | null;
street: string | null;
listOpen: boolean;
}
export interface GuidanceCardData {
destinationName: string;
progressPct: number;
showNoReroute: boolean;
}
export interface GuidanceViewData {
banner: GuidanceBannerData;
card: GuidanceCardData;
rows: GuidanceListRow[];
}
export function guidanceView(snap: GuidanceSnapshot, listOpen: boolean): GuidanceViewData {
const { model } = snap;
const marks = maneuverMarksOf(model);
const next = marks[snap.nextManeuver] ?? null;
// The road a mark names: a TURN mark names the road the turn puts you on
// (its owning step); the ARRIVE mark names the road being arrived on —
// the last NAMED step of the route (an arrive step itself carries no
// name; a blank slot would render the unnamed fallback and lie).
const arrivalRoad = (() => {
let named: string | null = null;
for (const step of model.steps) {
if (step.name !== undefined && step.name.length > 0) named = step.name;
}
return named;
})();
// the next maneuver's owning step: the step whose turn-kind AND prefix
// match the mark (a step runs FROM its maneuver TO the next boundary)
let street: string | null = null;
let along = 0;
if (next !== null) {
for (const step of model.steps) {
if (step.kind === next.kind && along === next.alongM) {
street = step.kind === "arrive" ? arrivalRoad : (step.name ?? null);
break;
}
along += step.distanceM ?? 0;
}
}
const toManeuverM = next === null ? null : Math.max(0, Math.round(next.alongM - snap.alongM));
const calm = snap.state === "rerouting" || snap.state === "no_reroute";
const totalM = model.cumulative[model.cumulative.length - 1] ?? 0;
const progressPct = totalM > 0 ? Math.min(100, Math.max(0, (snap.alongM / totalM) * 100)) : 0;
const rows: GuidanceListRow[] = marks.map((mark, i) => {
let rowStreet = "";
let cursor = 0;
for (const step of model.steps) {
if (step.kind === mark.kind && cursor === mark.alongM) {
rowStreet = mark.kind === "arrive" ? (arrivalRoad ?? "") : (step.name ?? "");
break;
}
cursor += step.distanceM ?? 0;
}
return {
kind: mark.kind ?? "straight",
street: rowStreet,
distanceM: Math.max(0, Math.round(mark.alongM - snap.alongM)),
rowState: i < snap.nextManeuver ? "past" : i === snap.nextManeuver ? "next" : "future",
};
});
return {
banner: {
state: snap.state,
glyph: (next?.kind ?? "straight") as GlyphKind,
distanceM: calm ? null : toManeuverM,
street,
listOpen,
},
card: {
destinationName: model.destinationName,
progressPct,
showNoReroute: snap.state === "no_reroute",
},
rows,
};
}