Checker-issue round 1 on the Phase 7 plan set:
BLOCKER (07-01) — watchSafeAreaChanges and watchFullscreenEvents were created
and unit-tested but never called in production: main.ts:74 was the only
safe-area site and was not in files_modified, and the fullscreen events were
never registered. Insets never re-resolved at runtime on a real device while
every gate stayed green.
- web/src/main.ts joins 07-01's files_modified; task 4 step 8 wires
watchSafeAreaChanges(getWebApp()) directly after the boot site's
applySafeArea(resolveSafeInsets(getWebApp())) line.
- task 4 step 6 registers watchFullscreenEvents once at the guidance-start
edge in route.ts, with a deliberately no-op callback (events are
observability, never correctness).
- New grep acceptance criteria pin BOTH production call sites; two new
must_haves key_links name them; T-07-06b now names the boot-site call.
WARNING (07-04 / VALIDATION.md) — the golden corpora were attributed to 07-04
t3, which never ran them. Folded into the budget audit: runGoldenCorpusGate()
re-runs `npm run routing:corpus` and `npm run search:corpus` as child
processes (the same shape runCpuLadderGate already uses), so the ONE
budget-audit command enforces SC4.a/SC4.b. VALIDATION.md rows now name 07-04
t2 and the budget-audit command.
WARNING (07-02 t3) — the landscape-leg snippet omitted the required setup.
The action now says to copy the existing corridor guidance leg VERBATIM
(setup stubGuidanceRoute(page, corridorModel) + guideQuery carrying dest +
fixpair + waitMode/expectGuide) and change ONLY the name, the scheme, and the
viewport; two grep criteria pin the setup and the query.
WARNING (07-04) — D-06/SC4 name subrequests but nothing measured beside the
50/request ceiling. Added runSubrequestClassGate(): a per-request-class
inventory derived from the Worker's route table (engine-A /api/route = 0
external subrequests; the cron tick's Telegram send is the only recurring
class; KV ops tracked separately against their own ceiling). Unmeasurable
counts read "not measured", never 0. docs/BUDGETS.md gains a
"## Subrequests per request class" section and the record states that the
inventory is source-derived, not a live Workers Logs metric.
WARNING (07-RESEARCH.md) — "## Open Questions" is now
"## Open Questions (RESOLVED)" and each of the five items carries its adopted
disposition; Q2/Q3 resolve to "answered by measurement at the launch gate per
D-04/D-06".
INFO (07-VALIDATION.md) — nyquist_compliant flipped to true and the sign-off
is approved 2026-10-25 after the map-row correction.
Waves, dependencies, autonomous flags, and the other three plans are unchanged.
28 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-hardening-private-launch | 02 | execute | 1 |
|
true |
|
|
Purpose: a navigation app's empty states ARE the polish surface, and a driver
holds the phone in landscape. D-03 locks the mechanism — landscape fixes are
CSS-only media queries with the Telemetry Rams tokens binding; the map stays
Rams-constant. RESEARCH Pattern 3 (failure classes as named vectors) is the
audit-coverage form: the route-null/off-road behaviours already exist and are
NOT redesigned here — they are pinned by tests.
Output: a landscape media-query block in web/src/shell.css,
web/test/failure-state-matrix.test.mjs (new — the FM1..FM7 named vectors),
and the landscape + failure capture legs in web/scripts/capture.mjs.
<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/07-hardening-private-launch/07-CONTEXT.md @.planning/phases/07-hardening-private-launch/07-RESEARCH.md @.planning/phases/07-hardening-private-launch/07-PATTERNS.md From `web/src/shell.css` (the geometry the landscape block must adapt — the sanctioned machined-geometry literals are: banner min-height 96px, arrow plate 48px + glyph 32px, compact strip 24px, row 48px + glyph 24px, next-row lamp 8px, panel max 320px/480px, bar 4px, 1px rules):.guide-banner {
position: absolute; top: calc(var(--safe-top, 0px) + var(--spacing-space-sm)); left: 0; right: 0;
min-height: 96px; padding: var(--spacing-space-sm) var(--spacing-space-md);
/* … */
}
[data-guidance="active"] .strip {
top: calc(var(--safe-top, 0px) + var(--spacing-space-sm) + 96px + 1px);
min-height: 24px;
}
.guide-list {
pointer-events: auto; position: relative; width: calc(100% - 2 * var(--spacing-margin));
max-width: 320px; max-height: 480px; overflow-y: auto;
margin-top: calc(var(--safe-top, 0px) + var(--spacing-space-sm) + 96px + 1px + 24px + var(--spacing-space-sm));
}
.route-stack[data-register="center"] { top: 50%; transform: translateY(-50%); }
.route-stack[data-register="bottom"] { top: auto; transform: none; bottom: calc(var(--safe-bottom, 0px) + 64px); }
From web/src/route/route.ts (the failure classes that ALREADY render — audit
coverage, not behaviour change):
const result: RouteResult | null = await deps.source
.route({ origin: { lat: fix.lat, lng: fix.lng }, destinationId: destination.id })
.catch(() => null); // T-05-04: an exception renders pending — never a half-drawn line
if (result === null || result.options.length === 0) {
deps.showRouteCard(deps.buildPendingCard(deps.locale, { onSearchAgain: () => walkBack() }), "center");
setState("pending");
console.log("ARAHARAH-ROUTE state=pending");
return;
}
From web/src/guidance/guidance.ts (the off-road state machine — the named
constants the failure matrix pins):
export const OFF_ROUTE_THRESHOLD_M = 50;
export const OFF_ROUTE_CONSECUTIVE_FIXES = 3;
export const OFF_ROUTE_GRACE_MS = 8000;
export const STORM_GUARD_MS = 20000;
export const MISSED_TURN_PAST_MANEUVER_M = 40;
export const MISSED_TURN_GRACE_MS = 3000;
export const ARRIVAL_THRESHOLD_M = 25;
export const GUIDANCE_START_ALONG_M = 30;
export function offRouteThresholdM(accuracyM: number | null): number;
From web/src/guidance/banner.ts (the DOM builders the failure matrix drives
through the minimal document fake — buildGuidanceBanner, buildManeuverList,
buildGuidanceCard, buildConfirmCard, buildTripEndCard, MANEUVER_LIST_ID).
From web/scripts/capture.mjs (the leg shape to extend):
async function captureLeg({ name, query, colorScheme, grantGeo, waitMode, expectState, expectText, expectGuide, setup, interact }) {
const target = join(SHOTS, name);
const context = await browser.newContext({
viewport: { width: 390, height: 844 },
colorScheme,
deviceScaleFactor: 2,
});
// …
}
/* ————— Phase 7 landscape geometry (PX-04/D-03) —————
Landscape is an ADDITIONAL CONSTRAINT, never a separate screen: portrait
remains the default and every rule below only ADAPTS the cockpit/banner/
card geometry. The map cartography stays Rams-constant (D-10) — NO MapLibre
rule appears in this block. Every value is a token reference or a sanctioned
geometry literal; the map's cartography is untouched. */
@media (orientation: landscape) and (max-height: 560px) {
/* the banner band: shorter in landscape, and the horizontal safe insets
are honoured on both edges (max() with the token padding as the floor) */
.guide-banner {
min-height: 72px;
padding-left: max(var(--spacing-space-md), var(--safe-left, 0px));
padding-right: max(var(--spacing-space-md), var(--safe-right, 0px));
}
.guide-banner .guide-plate { width: 40px; height: 40px; }
.guide-banner .guide-plate svg { width: 28px; height: 28px; }
/* the compact strip rides directly under the shorter banner */
[data-guidance="active"] .strip {
top: calc(var(--safe-top, 0px) + var(--spacing-space-sm) + 72px + 1px);
}
/* the maneuver list: a landscape-height-bounded panel that clears the
banner and the strip instead of overlapping them */
.guide-list {
max-height: calc(100dvh - 104px - var(--safe-top, 0px) - var(--safe-bottom, 0px));
margin-top: calc(var(--safe-top, 0px) + var(--spacing-space-sm) + 72px + 1px + 24px + var(--spacing-space-sm));
}
/* the bottom register (the guidance card / the confirm card) sits inside
the landscape height instead of overflowing past it */
.route-stack[data-register="bottom"] {
bottom: calc(var(--safe-bottom, 0px) + var(--spacing-space-sm));
}
.route-stack[data-register="center"] { max-width: 320px; }
}
CONSTRAINTS — every one is binding:
- Tokens only. Every colour/spacing/typography value is a
var(--…)reference. The ONLY literals permitted in this block are the machined geometry numbers already sanctioned by the file's header comment (72px banner min-height, 40px plate, 28px glyph, 104px chrome offset, 1px rule) — and you MUST extend the header comment's sanctioned literal list with exactly:landscape banner min-height 72px, plate 40px + glyph 28px, landscape chrome offset 104px. The comment lists literals so a future reader knows they are deliberate. - Zero radius, zero shadow, zero blur, zero glass. Do NOT write
border-radius,box-shadow,filter: blur,backdrop-filter, or anytransition/animation— the house style forbids them and the theme audit flags them. - No MapLibre rule. No
#mapselector and no MapLibre class may appear in this block (the map is Rams-constant, D-10/PX-02). - No new markup. No
data-*attribute is introduced; the rules hang off the EXISTING.guide-banner/.guide-list/.route-stack/[data-guidance="active"]selectors. 100dvhonly where a viewport-relative height is genuinely needed — the.guide-listmax-height. Do NOT convert the shell'sheight: 100%/position: fixedsizing model (RESEARCH Pitfall 4: keep the current model unless a failing leg proves a change).
After appending, run npm run audit -w theme (the DSGN-01 evidence command)
and confirm it still reports AUDIT CLEAN. If the audit flags a landscape
declaration, that declaration is off-token — replace the literal with the
matching token; never suppress the audit.
<acceptance_criteria>
- grep -c "@media (orientation: landscape)" web/src/shell.css returns exactly 1
- grep -c "max-height: 560px" web/src/shell.css returns exactly 1
- grep -c "border-radius\|box-shadow\|backdrop-filter\|transition:\|animation:" web/src/shell.css returns 0
- grep -c "#map" web/src/shell.css returns the SAME count as before this change (no MapLibre rule was added)
- grep -c "landscape banner min-height 72px" web/src/shell.css returns 1 (the header's sanctioned literal list was extended)
- npm run audit -w theme exits 0 and prints AUDIT CLEAN
</acceptance_criteria>
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm run audit -w theme 2>&1 | tail -8 && npm run web:typecheck 2>&1 | tail -5
The landscape block exists, is CSS-only, uses tokens plus the extended
sanctioned literal list, has zero radius/shadow/blur/transition, touches no
MapLibre rule, and the theme audit stays clean.
The file MUST declare a FAILURE_VECTORS array as DATA (RESEARCH Pattern 3's
data-driven form) with EXACTLY these seven entries and their id/trigger/
expect fields:
const FAILURE_VECTORS = [
{ id: "empty-route", trigger: "route options=[]", expect: "pending-card" },
{ id: "route-error", trigger: "route source throws", expect: "pending-card" },
{ id: "no-geometry", trigger: "polyline decodes to <2 points", expect: "pending-card" },
{ id: "off-road", trigger: "3 fixes beyond the accuracy-aware threshold", expect: "rerouting-or-no-reroute" },
{ id: "location-timeout", trigger: "LocationManager returns null after timeout", expect: "denied-or-awaiting-card" },
{ id: "old-client", trigger: "WebApp version < 8.0 / methods absent", expect: "guidance-usable-without-fullscreen" },
{ id: "route-timeout", trigger: "route source never resolves within the budget", expect: "pending-card" },
];
Then write the named cases (the FM* prefix follows the repo's HS*/GS*/
RS* convention). Every case must be grep-verifiable by name:
- FM1 —
FAILURE_VECTORShas EXACTLY 7 entries and theirids are exactly["empty-route","route-error","no-geometry","off-road","location-timeout","old-client","route-timeout"]. - FM2 — the EMPTY ROUTE renders the designed pending card, never a blank:
build the pending card through the REAL builder
(
buildRoutePendingCardfromweb/src/route/route-card.ts, driven through the minimalmakeEldocument fake copied from guidance-surface.test.mjs), and assert the rendered element carries a non-emptytextContentand that no element in the built card has an empty class or an undefined dataset state. Assert the card's text is NOT empty and NOT a bare "undefined". - FM3 — the ROUTE ERROR and the ROUTE TIMEOUT both land on the same
pending-card surface: assert that
deps.source.routerejecting anddeps.source.routeresolving withoptions: []both produce the pending card. For the timeout vector, use aPromisethat never settles and assert the controller's state staysidle/pending(neverroute) and that NOTHING is drawn — assertsetRouteDrawnwas never called (use the injectedMapLikestub the route-surface suite already uses, or a minimal object with a recordingsetData). The suite MUST have a timeout guard (e.g.Promise.racewith a 200 ms timer) so the case cannot hang the runner. - FM4 — the NO-GEOMETRY route is refused, never half-drawn: assert that a
polyline6 string decoding to fewer than 2 points is rejected. Use
decodePolyline6from@araharah/routing/src/index.tsand assert the decoded point count is< 2for the vector, and that the route controller treats such a geometry as null (the pending card), NOT as a drawable route. - FM5 — the OFF-ROAD class renders calmly: drive the PURE reducer
(
createGuidanceSession+stepGuidancefromweb/src/guidance/guidance.ts) with three fixes beyond the accuracy-aware threshold and assert exactly ONEreroute-requestedeffect fires, that the storm guard holds a second immediate excursion, and that the resulting state isrerouting(notoff_routehanging, not a throw). Assert the banner's state forreroutingrenders the CALM slot — build it withbuildGuidanceBannerand assert the rendered text contains no "REROUTE" blame phrasing and that the card's text is non-empty. - FM6 — the OLD-CLIENT class (SC2.e): a
TelegramWebAppLikefake withversion: "7.0"and NO 8.0 members — assert the guidance chrome still builds (the banner builder produces a non-empty element) and that no capability call is attempted. Do NOT importweb/src/telegram/fullscreen.tshere — plan 07-01 owns that module's suite; this case asserts the CHROME survives an old client, not the fullscreen wrapper. - FM7 — the LOCATION-TIMEOUT class: assert the state's designed outcome
is one of the two designed surfaces (
denied/awaiting-pin), never a blank. Use the location chain's own state vocabulary fromweb/src/location/types.ts(read the file to learn the exact state names) and assert the failure-state set contains "denied" and "awaiting" as members. If the location chain's exported type does not carry those names verbatim, assert instead that the chain's failure branch returns a NON-NULL state object whosekindis one of the two designed failure kinds — read the source and pin the ACTUAL names.
NO case may assert a blank state, and no case may use a real network call, a
real DOM, or a timer longer than 200 ms. Every DOM-driving case must install
its globalThis.document stub and delete it in a finally block (the
homescreen.test.mjs law).
<acceptance_criteria>
- web/test/failure-state-matrix.test.mjs exists and contains the literals FAILURE_VECTORS, FM1, FM2, FM3, FM4, FM5, FM6, FM7
- grep -c "id: \"empty-route\"\|id: \"route-error\"\|id: \"no-geometry\"\|id: \"off-road\"\|id: \"location-timeout\"\|id: \"old-client\"\|id: \"route-timeout\"" web/test/failure-state-matrix.test.mjs returns exactly 7
- grep -c "finally" web/test/failure-state-matrix.test.mjs returns ≥ 1 (the DOM stub is restored)
- npm test -w web exits 0 and the runner's summary line contains 0 failed
</acceptance_criteria>
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm test -w web 2>&1 | tail -25
The failure matrix exists as DATA + named cases, covers empty/error/timeout/
no-geometry/off-road/old-client/location-timeout, asserts designed surfaces
rather than blanks, and the whole web suite is green.
-
captureLegMUST accept ONE new optional parameter:viewport, defaulting to{ width: 390, height: 844 }. Change the signature line to:async function captureLeg({ name, query, colorScheme, grantGeo, waitMode, expectState, expectText, expectGuide, setup, interact, viewport }) {and inside, replace the hard-codedviewport: { width: 390, height: 844 },withviewport: viewport ?? { width: 390, height: 844 },. -
Add ONE new landscape leg after the existing guidance legs, using the
?drive=corridor-followreplay knob (the committed fixture the 06-08 guidance legs already use) and the guidance wait mode.COPY THE EXISTING CORRIDOR GUIDANCE LEG VERBATIM. The new leg is the existing corridor guidance leg (the one that uses
corridorModel+guideQuery— e.g.shell-guidance-active.png) with ONLY THREE things changed: thename, thecolorScheme, and theviewport. Keep EVERYTHING else byte-identical — in particular:- the
setup:—setup: (page) => stubGuidanceRoute(page, corridorModel)— MUST be copied verbatim. WITHOUT this setup the/api/routestub is never installed, the route never resolves, and the leg TIMES OUT at the guide-state race with anARAHARAH-GUIDE state=… race timed outerror. - the
query:—guideQueryMUST be copied verbatim.guideQueryismock=android&dest=${guidePair.destinationId}&fixpair=${guidePair.pairId}&drive=corridor-follow— it carries BOTH thedest(the destination id the route resolves to) AND thefixpair(the fixture pair id the replay stream keys on). Droppingdestgives the leg no destination; droppingfixpairgives it no replay fixes. Both are required. - the
waitMode:/expectGuide:pair — copy the existing corridor leg's pair verbatim (waitMode: 'guide',expectGuide: 'on_route'— READ the file and confirm; if the existing leg useswaitMode: 'settle400'instead, use that).
So the leg is:
/* ————— 07-02 — the landscape leg (PX-04.e) ————— The driving posture: the phone rotated. The cockpit geometry adapts via the @media (orientation: landscape) block (07-02 task 1); the map stays Rams-constant. A 844x390 viewport IS the landscape orientation the media query matches (orientation: landscape + max-height: 560px). The setup + query + waitMode/expectGuide are copied VERBATIM from the existing corridor guidance leg — only the name, the scheme, and the viewport differ. */ await captureLeg({ name: 'shell-guidance-landscape.png', setup: (page) => stubGuidanceRoute(page, corridorModel), query: guideQuery, colorScheme: 'light', grantGeo: true, waitMode: 'guide', expectGuide: 'on_route', viewport: { width: 844, height: 390 }, });The
expectTextarray and theinteract:block are OPTIONAL — copy them from the source leg if you want the same DOM assertions, but the leg MUST at minimum carry thesetup, thequery(withdest+fixpair), and thewaitMode/expectGuidepair. - the
-
Add ONE failure-state leg that photographs the designed pending card (the empty/error route class) so the surface is on film, not only in a Node assertion. Use the existing pending-card leg's query/expectText values verbatim (READ them from the file — the
shell-route-pending.pngleg already exists) and add the landscape variant of it:
/* ————— 07-02 — the landscape failure-state leg (SC2) —————
The pending card in landscape: the designed surface, never a blank map. */
await captureLeg({
name: 'shell-route-pending-landscape.png',
query: '<copy the shell-route-pending.png leg\'s query verbatim>',
colorScheme: 'light',
grantGeo: true,
waitMode: '<copy the shell-route-pending.png leg\'s waitMode verbatim>',
expectState: '<copy the shell-route-pending.png leg\'s expectState verbatim>',
expectText: '<copy the shell-route-pending.png leg\'s expectText verbatim>',
viewport: { width: 844, height: 390 },
});
-
Update the file's header comment block (the "Outputs (web/screenshots/ —" list) with TWO new lines, matching the existing format:
* shell-guidance-landscape.png the cockpit in landscape (driving posture) (07-02, PX-04)* shell-route-pending-landscape.png the designed pending card in landscape (07-02, SC2) -
The capture harness's own MIN_BYTES blank bar already applies to every leg (a landscape shot under 10240 bytes FAILS) — do NOT add a per-leg size exemption. A landscape leg that renders a blank surface must FAIL the capture run.
-
Do NOT change any existing leg's query, expectText, expectState, or screenshot name. This plan ADDS legs; it never edits the existing ones. <acceptance_criteria>
grep -c "viewport" web/scripts/capture.mjsreturns ≥ 3 (the signature, the default, the two legs)grep -c "shell-guidance-landscape.png" web/scripts/capture.mjsreturns ≥ 1grep -c "shell-route-pending-landscape.png" web/scripts/capture.mjsreturns ≥ 1grep -c "width: 844, height: 390" web/scripts/capture.mjsreturns ≥ 2 (both new legs)grep -c "stubGuidanceRoute(page, corridorModel)" web/scripts/capture.mjsreturns ≥ 6 (the five existing corridor legs + the new landscape leg — the landscape leg MUST carry the route stub setup)grep -c "query: guideQuery" web/scripts/capture.mjsreturns ≥ 5 (the four existing corridor legs + the new landscape leg — the landscape leg MUST carry thedest+fixpairquery)npm run capture -w webexits 0 and the report lists BOTH new landscape shots as non-blank </acceptance_criteria> export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm run capture -w web 2>&1 | tail -30 The capture harness accepts a per-leg viewport, and the two new landscape legs (the cockpit + the pending card) render non-blank shots in a real browser — the landscape media query is exercised on film.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Device orientation → layout | The client reports orientation; the layout must stay readable under both. |
| Routing/geometry source → client surface | Route geometry and route-source failures are untrusted inputs; a malformed polyline must not reach the map. |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-07-05 | Denial of Service | the failure classes — a blank screen is a dead app for a driver | mitigate | Every failure class is a NAMED vector in FAILURE_VECTORS and is asserted to render a designed surface (pending card / denied / awaiting) rather than an empty map. Pinned by FM2, FM3, FM5, FM7. |
| T-07-12 | Tampering | web/src/shell.css — a malformed polyline half-drawn on the map |
mitigate | FM4 pins that a polyline6 decoding to < 2 points is refused before the draw path; the pending card renders instead of a half-drawn line. |
| T-07-13 | Information Disclosure | the landscape/failure rendering path | accept | The cockpit renders route geometry and street names already on screen; no new data class is introduced and no coordinate is written to a log or storage surface by this plan. |
| T-07-14 | Tampering | web/src/shell.css — an off-token or blur/shadow declaration smuggled into the landscape block |
mitigate | Every new declaration is a token reference or an extended sanctioned geometry literal; npm run audit -w theme (the DSGN-01 evidence command) must print AUDIT CLEAN — the zero-radius/no-shadow/no-blur house rule is enforced mechanically, not by review. |
| T-07-15 | Denial of Service | the capture harness — a landscape leg that renders blank | mitigate | The existing MIN_BYTES blank bar (10240 bytes) applies unchanged to the new legs; a blank landscape surface FAILS the capture run instead of passing silently. |
| </threat_model> |
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"
cd /home/C-Fu/araharah
npm test -w web
npm run audit -w theme
npm run capture -w web
All three must exit 0. The capture run must report both new landscape shots as
non-blank, and the theme audit must print AUDIT CLEAN.
<success_criteria>
web/src/shell.csscarries exactly one@media (orientation: landscape) and (max-height: 560px)block that adapts only the cockpit/banner/list/bottom-register geometry, uses tokens plus the extended sanctioned literal list, and contains zero radius/shadow/blur/transition declarations and zero MapLibre rules.web/test/failure-state-matrix.test.mjscarriesFAILURE_VECTORS(exactly 7 named entries) and the FM1–FM7 cases; every failure class asserts a designed surface, never a blank.web/scripts/capture.mjsaccepts a per-legviewportand adds the two landscape legs (shell-guidance-landscape.png,shell-route-pending-landscape.png).npm test -w web,npm run audit -w themeandnpm run capture -w weball exit 0. </success_criteria>