Both sweeps hand-copied the four VAR-* codes, so a fifth code added to
NarrativeCode would simply never be looked for. Both now derive the list through
`as const satisfies Record<NarrativeCode, true>`, which fails the typecheck
instead.
The schema walker also read only enum values and describe() text: a
`z.literal("VAR-TIMING")` (member on _def.value) or a union of literals (branches
on _def.options) published the whole catalogue into a tool's JSON schema and the
sweep stayed green — proven by mutation. It now walks literals, union and tuple
branches, record key/value types and intersection halves, with a synthetic
fixture pinning that coverage so the arms cannot regress silently.
MetricId and Department are restated by hand in four files. Only two carried
`as const satisfies Record<MetricId, true>`; catalog/definitions.ts and agent.ts
kept plain z.enum literal lists, so a metric added to the union stayed
unbindable and unaskable with every typecheck green. Both now derive their enum
from a compiler-checked record, and a new parity suite reads the four SHIPPED
schemas (plus the narratives route, through its handler) and the seed, asserting
they are set-equal — which satisfies alone cannot do, since it checks each copy
against the union and never against the ledger that has to hold rows for it.
Three crash paths in the exec skin, each reachable from an ordinary value:
- `Heading`/`Text` read `props.text.path` for a non-string label, but the
A2UI binder resolves dynamic props before render and passes `undefined`
for an unresolved binding — it never leaves a standing `{ path }` object.
Reading into it threw a TypeError out of the renderer, taking the whole
surface down instead of the one label that failed to bind. The guard now
reports whatever shape arrives, and the comment that described the old
standing-ref theory is corrected.
- `DEPARTMENT_LABEL[dept].toLowerCase()` threw for a department key outside
the four seeded ones — a key the agent can send, and one that lands on the
failure path by construction. The label now falls back to the raw key, so
the path that exists to REPORT a bad query no longer throws on one.
- The budget memo's fixed 0.62 driver split does not survive whole dollars:
overruns of $2 and $4 round to a 1/1 and a 2/2 TIE, which the builder's
`DriverSplitError` refuses and the route turned into a bare HTTP 500.
The timing share is now ceiled off the rounded overrun, which keeps it
strictly the larger driver for every overrun, and the route maps the
builder's own refusals to their codes (NOT_AN_OVERRUN 404, DRIVER_SPLIT
409) rather than reporting a ledger fact as a server fault.
Ops are rebuilt from the stored spec on every read, and buildBlockOps throws for a spec it cannot render (seeds construct blocks directly, bypassing assertValidBlockSpec). That throw escaped the handler and 500'd the entire snapshot, which the provider's first-load gate turns into an error panel above the chat and the grid - taking out the very grid needed to unpin the bad block. The failure is now scoped to its own block: empty ops plus an opsError, block kept so its card chrome still offers unpin, logged loudly.
The narratives route appends the narrative before composing its response, so a 201 whose body will not parse is a lost receipt, not a failed filing. A bare res.json() rejected with a SyntaxError, skipping refresh() and printing a parser error on beat 6's filing form over a narrative that IS in the ledger. Mirrors publishPack's existing lost-receipt arm: reconstruct the row from what was sent (the fields the form and the teach chain read), refresh either way.
`state.dashboards` and `agentRegistry` are plain objects indexed with
caller-supplied ids — a URL path segment for the exec block routes and for
`publishPack`, and a URL-derived `agentId` for the shared CopilotKit route.
A plain-object index walks the prototype chain, so `constructor`,
`toString`, `valueOf`, `hasOwnProperty`, `__proto__` … each resolve to a
truthy INHERITED member.
In the exec store that sailed past the `!dashboard` guards and threw the
raw `undefined.blocks` TypeError those guards exist to prevent: a 500 with
no body where `store-errors.ts`' mapped 404 belongs. Worse in
`publishPack`, which THREW instead of returning, breaking the returned-not-
thrown refusal contract the packs route (no try/catch) relies on. Both now
go through one `lookupDashboard` helper using `Object.hasOwn`.
`agentRegistry` is latent today — the route's `?.` swallows the inherited
member — but it is the same class, one registry over from the `getSkin`
fix. It is now built on a null-prototype object rather than behind an
accessor, so the guarantee covers the index sites that already exist
(including the agent-map builder) instead of only callers that remember a
helper. Enumeration is unaffected, so `agentIds` is unchanged.
Both suites pin the behaviour per inherited key rather than pinning which
own-key primitive implements it.
Four defects in Vantage's tool surface, all of them things the room reads or
waits on:
- navigateTo's `period` carried a schema-level `.regex(...)` and `top` an
`.int().min(0)`. The AI SDK rejects a bad VALUE at the parameter boundary and
emits a `tool-error`, which @copilotkit/runtime has no arm for (verified
against ai@6.0.259) — no TOOL_CALL_RESULT is emitted, so an off-format period
left the chip spinning "Navigating…" forever. Same defect class agent.ts
already documents for `months`. Both are relaxed to shape-only and checked in
`navLeverRefusal` inside the handler, answered as a correctable result;
`period` is validated through the Metrics Explorer's own
`normalizePeriodLever`, so the tool cannot refuse a lever that page would
honour. No other frontend schema carries a value constraint.
- get_metrics and list_exceptions had no exact renderer, so the shell's
wildcard chip — which keys "done" off status alone — ticked "Reading metrics
✓" over a read that returned an error. Both now render through `ReadSettle`:
a refusal reads as a refusal, a success keeps the settled label and never
prints the series payload.
- A failed pin printed "Could not pin that block: ALREADY_PINNED: …". The strip
removed only the ledger's prefix and leaked the store enum into the
transcript; `pinFailedLine` strips the coded prefix too and keeps the store's
remedy.
- `refusalLine` preferred the producer's `message`, and agent.ts writes those
FOR THE MODEL — the room saw "Call render_metric_block again…" and "do not
retry the same call". The message is now used only when it is already
operator-safe, with a per-code operator phrase table behind it. The model's
copy of the message is unchanged.
The Metrics Explorer's readable published every row: the unlevered page is
528 seeded points, so ~100KB of JSON went to the agent on every turn. It now
publishes a bounded prefix and says so (showingCount, rowsTruncated), so a
cap can never be mistaken for the whole view.
Alongside it, the readables and screens that disagreed:
- ceo-dashboard publishes varianceDisplay beside the raw fraction, and nulls
a non-finite variancePct deliberately rather than letting JSON do it while
the card reads "n/a"; visibleExceptions is memoized.
- The exception cards drill in with the metric they name, via a new metric
lever on filterMetricRows and its own control on the explorer.
- board-packs emits each timestamp's display string beside the ISO instant,
unifies the two divergent dashboard-title fallbacks, makes its newest-first
comparators total, logs the submit it is actually performing, and hands
FiledNarrativesList the list and label map the page already derived.
- cfo-dashboard's readable page id is "finance", the nav segment navigateTo
takes; board-packs' is "packs" for the same reason.
- normalizeDepartmentLever trims, and a def-less metric is now reported once
by id instead of dropped in silence.
- Explorer smalls: the unknown-unit diagnostic fires once per unit rather
than twice per row per render, the breaches-only toggle carries
aria-pressed, lever writes replace instead of flooding history, and the
top select no longer claims an off-vocabulary value came "from chat".
`execToolLabels` was described as covering the "report" beats; exec
ships no a2ui report tool at all, which is the very reason the skin
supplies no `CanvasSurface`. And the `CanvasSurface` note cited the
shared canvas's "no renderer for this kind" branch as how exec is
handled, while report-canvas.tsx and skin-contract.ts both say that
branch is defensive, never runs for exec (its surfaces are all
`block:`-prefixed and skipped before the region is claimed) and must
not be cited as covered.
In the test, the comment calling catalog/designSkill OPTIONAL on the
`Skin` contract was wrong — both are required — so it misnamed what
these cases guard. They guard the EMPTY value the type cannot see, which
the catalog case now actually asserts rather than only checking the
field is defined.
The brief inlines every colour as a literal because the sandbox iframe
ships a bare CSS reset and `var(--brand)` resolves to nothing in there —
but the copy was never resynced after the contrast fix. It shipped the
PRE-fix brand (43 55% 45%, the 2.71:1 pair), positive (152 50% 38%),
dark negative (0 65% 60%, 4.31:1) and muted ink (220 9% 44%), so
generated UI rendered the unreadable palette the app itself had already
replaced. Three of the four are the exact ratios theme.test.ts records
as the failures it exists to catch.
Every literal is now updated to the current token value and PINNED:
theme.test.ts parses the `hsl()` values out of the brief and asserts
each equals the theme.css token it names, failing on any literal it has
no mapping for, so the two files cannot drift apart again.
Also records why the dark guidance keys on `prefers-color-scheme` while
the app toggles a `.dark` class: the class cannot reach the iframe (the
renderer builds its document from the model's HTML plus a reset), so the
media query is the only dark signal the generated markup can observe.
Two ride-alongs in the same file: the per-pair site scan is now
per-SITE, because asserting the flattened match list was non-empty let a
pair citing five files pass on one survivor (gutting ceo-dashboard.tsx
kept all 46 green; it now fails 6). And the note claiming tools.tsx
"is being given" a `dark:text-brand-violet` counterpart is replaced by
listing tools.tsx among that pair's sites, so the claim is grepped.
The OGUI sandbox getter still used a plain `if (months)`, the shape
`store.ts`'s `periodWindow` was written to replace: 0 is falsy and
silently returned the full 24-month history, and a negative reached
`slice(-(-3))` === `slice(3)` — the OLDEST periods, an inverted window
answering a request for the newest three. `months` arrives from
LLM-authored JS inside the iframe, which is the least constrained caller
in the app, so both are reachable rather than hypothetical.
Reimplements the same normalization locally (the store's helper is
private) and names the contract in the comment. Also fails loud on an
unknown metricId/department, which previously resolved to an empty
series indistinguishable from a metric with no rows yet, and derives the
zod enums from `satisfies Record<MetricId, true>` records so the schema
cannot drift from the unions in `data/types.ts` — the pattern
`api/exec/v1/narratives/route.ts` already uses.
The fixture is unsorted so the getter's own sort is actually exercised;
pre-sorted, deleting the sort left every assertion green.
Mounting the layout waits on vite transforming the whole shell tree behind
`layout.tsx` — about a second when the file runs alone, but over vitest's 5s
default whenever it competes for the transform pool with the other 60-odd
suites. `pnpm vitest run src/shell src/app` failed here on most runs while the
same file passed in isolation, and the failure named a timeout rather than
anything the tests assert. Pre-existing: the suite fails the same way at the
previous revision.
Load the module once at file scope instead of per mount, and give the describe
block a 30s budget.
`identifyUser` is optional by type but mandatory in practice — drop one and the
skin still renders, still chats, and silently loses its durable-memory scope,
with nothing in tsc, lint or the existing suite to catch it (verified by
mutation: removing exec's resolver kept all four tests green).
Also replaces the `agentIds` assertion, which compared the export against
`Object.keys(agentRegistry)` — its own definition, so it held for every possible
implementation. It now checks `agentIds` against the client SkinRegistry keys,
which stays meaningful if the export is ever rewritten as a hand-kept literal.
The `useStableArrayProp` stand-in omitted the SDK's `prop ?? empty`
substitution and its `isMeaningfulChange` comparator, so it over-reported: a
nullish prop or a benign renderToolCalls refactor would have failed here and
passed in the real provider. Transcribe both from the installed dist bundle and
correct the "faithful replica"/"unconditionally" claims that described neither.
Also: complete the `@/shell/registry` mock with `allSkins` (selector-card calls
it), pin the provider render count at exactly 3 instead of `> 1`, and add the
`a2ui`/`openGenerativeUI` identity assertions — deleting either useMemo in
layout.tsx left every existing test green, because the array guard never looks
at those objects. The `?? []` red-evidence claim is now documented as what it
is: a compound mutant that needs the memo gone too.
getSkin indexed the plain-object SkinRegistry with the untrusted [skin] URL
segment, so /constructor, /toString, /__proto__ and friends returned truthy
Object.prototype members, passed the layout's `if (!skin) notFound()`, and
crashed on skin.identity.favicon — a 500 where a 404 belongs. Guard with
Object.hasOwn, matching the Map-based untrusted-key lookups the skins'
intelligence/user-id.ts modules already use, and pin the behaviour in a new
registry.test.ts.
Also drops the unconsumed `export { defaultSkinId }` re-export: every consumer
imports it from ./skins-config, and the duplicate path offered an import that
would drag all eight client skin modules into an RSC.
The chat's block reader and the canvas's classifier each walked the same
operation list their own way, and both walked it wrong.
- Both took `createSurface ?? updateComponents ?? updateDataModel`, which
stops at the first container that merely EXISTS: an op carrying an empty
`createSurface` beside a real `updateComponents` read as "no surface
here". Both now search all three containers per op, the way
`build-block-ops.ts` already documents on the write side.
- The canvas scanned every op while the chat took the first op that carried
any surfaceId, so a list holding a block surface AND a report surface
rendered inline AND claimed the whole content region — the disagreement
`[skin]/layout.tsx` states can never happen. `decideA2uiSurface` is now
the single reader-side decision and `blockSurfaceIdFrom` delegates to it,
which also drops the third copy of `BLOCK_SURFACE_PREFIX` and the second
spelling of the operations key.
- A non-string surfaceId reached `.startsWith` and threw mid-render, with
no error boundary between the card and the transcript; it is now
typeof-guarded like the canvas side always was.
- An `open-generative-ui` activity claimed the region with no content
check, and `OguiCanvas` then rendered null — a blank page behind a bare
"← Back". It must now carry content `useOguiSurface` can hand over, and
an earlier OGUI cannot stand in for a blank latest one.
- The envelope-drift warning latched on a module-level flag, so the first
drifted SSR render in a process silenced it for every request after;
it is now a per-provider effect, dev-only as the comment always claimed.
Tests: minted exec ops now run through the canvas classifier as well as
the chat reader, so either copy of the prefix drifting fails a test; the
malformed-content loop became labelled cases; and the card's
createSurface-strip branch gained the growing-snapshot case that actually
exercises it.
MetricTile, TrendLine and VarianceBar coloured their delta by SIGN, so the
seeded CFO dashboard showed distribution opex +9% and burn rate +10% in
green — one of them beside the tile's own "Breach" chip. They now tone off
`isBreach`, the rule ExceptionList and the CEO strip already follow.
The VarianceBar plan marker reused the bar's 2% minimum WIDTH as a position:
the largest-plan department's 1px rule landed at left:100% and was clipped
away by the track, and a zero plan drew a false line at 2%. The marker now
takes its true share of the scale, offsets itself by its own width, and is
not drawn at all without a plan. The missing-def arm also stops claiming the
metric "has no per-department series" when the rows are present and the def
is what is missing.
`isBreach` now screens a non-finite variance out of the breach decision. A
point planned at zero divides by zero in `variancePct`, and
`Math.abs(Infinity) > thresholdPct` is true for every threshold — so such a
point breached permanently: an exception no narrative could retire, blocking
the publish gate while `JSON.stringify` wrote its `Infinity` as `null` and
left a blank where the variance should be. Its `NaN` twin answered the
opposite unfounded thing (never a breach). Both are now one explicit
no-variance-data path, mirroring the screen `metric-rows.ts`'s ranking
comparator and the catalog's `Delta` already apply.
`buildBudgetMemoPdf` now enforces the two driver invariants its header only
claimed: the printed drivers must account for the whole overrun, and timing
must be strictly the larger of the two (the cue the reader infers VAR-TIMING
from). Same belt-and-suspenders as the existing sign guard, under its own
`DriverSplitError` so a broken split is not reported as a sign fault.
Tests, each proven by mutating the code they cover:
- a narrative clears only its OWN period's breach (dropping the period half
of `explained` left all 2964 tests green)
- a pack records only narratives for the period it reports on (dropping the
stale-period half of `publishPack`'s filter left 468 exec tests green)
- no metric breaches in more than one department at the latest period — the
seed-side invariant `explained`'s narrow `(metricId, period)` key rests on
- the two driver-split refusals, the zero-plan breach refusals, the
department filter composing with the period window, and the 24-period
claim now checked for every company-wide metric rather than `revenue`
Also: a decision comment on `exceptions()` recording why `Narrative` stays
department-free and pointing at the guard test; a `metricDefs()` accessor
that skips `snapshot()`'s `exceptions()` recomputation; and three stale
comments corrected — `metricSeries` no longer asserts `sandbox-functions.ts`
is "fixed", `snapshot()` no longer claims to be the only read, `seed.ts`'s
pinned-threshold list no longer omits `burnRate`, and `store.test.ts`'s
`emptyDashboard` no longer describes an iteration hazard that cannot occur.
POST/PATCH/DELETE had their statuses and store effects asserted but never their bodies, and the grid redraws from exactly those bodies — a handler that mutated correctly and answered with the pre-mutation list, or the other dashboard's, left a stale grid on screen with every store assertion still green.
redactSecrets(apiUrl) is by construction the constant "<intelligence-backend>" — INTELLIGENCE_API_URL is itself the needle — so the field carried zero information while inviting the leak it was scrubbing. Keel's dev/reset states the rule: the log is the only place the address appears. The half-configured arm now logs raw and serializes redacted, matching redact-secrets' contract and the failure arm below it. Tests: the pinned INTELLIGENCE_USER_ID fan-out (never entered before, every case stubbed it empty) and a DATA-half assertion that is no longer vacuous.
Every on-stage mutation is followed by a re-read of this route, and a cached copy answers with the pre-mutation snapshot — which reads as "the pin didn't work". The client already asks with cache: no-store, but that governs only the browser's own cache; the response now says so too, as the sibling budget-memo route does. Also pins why the narrative code stays in this body: board-packs' filed-narratives list renders it, and the withheld-vocabulary boundary is agent channels, not the browser.
The beat-6 filing form POSTs to this route with unconstrained metric and period selects, so a shape-valid (metricId, period) the store holds no point for filed cleanly: 201 "filed", a narrative matching nothing, and the breach still unexplained behind the publish gate. Mirror the agent tool's NO_LEDGER_POINT guard word for word (pinned by a test that runs both layers) and answer 422. Also bound code and body: a whitespace-only code trimmed to "" and came back as the nameless "\"\" is not a code this ledger files under".
`publishPack` answered a 2xx whose body would not parse with a
failure-shaped result, and the countersign card keyed success on
`"pack" in outcome` — so a pack that WAS written rendered as "Publish
refused: publish pack succeeded but returned an unreadable body", and
the agent, reading the same settle, published a duplicate. The result
now discriminates on `published`, carries a `note` when the receipt was
lost rather than the write, and the card's whole decision moves into
`publishSettleFor` so it is assertable without a live interrupt.
Alongside it, in the same publish/settle path:
- the countersign card never cleared `busy` on the SUCCESS path, leaving
four disabled digits under a permanent "Publishing…";
- the dashboard radios' `onClick` bypassed `selectDashboard`, so a mouse
pick moved the selection without moving focus and the next Tab escaped
the roving-tabindex group;
- `fileNarrative` gated on `res.status !== 201` where every sibling
mutation gates on `!res.ok`;
- `blockId` went into the DELETE/PATCH URL unencoded, so an id carrying
a `/` or `#` addressed a different route;
- the backend's "filed, but this clears nothing" note was never shown to
the operator — the receipt now surfaces it.
Tests: removeBlock/moveBlock (including the encoded path) and the 2xx
unreadable-body arm now covered in the provider suite; the countersign
card's in-flight and focus behaviour covered in the settle suite; the
narrative receipt's console.error assertion scoped to its own message
and the `refreshRef` indirection exercised with a changed identity.
The header claimed /exec/?query was still emitted and that the fix belonged in
src/shell/skin-path.ts. Neither holds: tools.tsx's navigateTo splits the query
off with splitTargetQuery and gives skinHref only the path half, so the index
segment composes /exec?department=… (or /?department=… under a lock). The
comment now describes that mechanism and cites the two nav-target tests that
actually pin this module's side of it.
The packs route answers a refusal as { error, message?, breaches? } and the
receipt's refusalLine already prefers message, but the two hops between them
dropped it: publishPack parsed only error and breaches off the body, and the
countersign card's respond() forwarded only those two. EMPTY_DASHBOARD is the
code that proves it — REFUSAL_PHRASES has no wording of its own for it, so the
store's sentence is its whole explanation and the room read the enum spelled
as words instead.
The card's payload build moves into publishRefusalPayload so the forwarding is
assertable without driving the interrupt. Both extras stay conditionally
spread: BAD_COUNTERSIGN still answers { error } and nothing else.
The blocks route no longer relies on its zod enum to stop a TypeError:
addBlockToDashboard goes through requireDashboard, which throws a coded
NOT_FOUND that storeErrorResponse maps to 404. The enum's job is now the 400
that names the allowed values.
ReportCanvas's null branch is NOT exercised by bookstore and exec omitting
CanvasSurface: bookstore emits no a2ui-surface at all and exec's are all
block:-prefixed, which useLatestCanvasSurface skips. Both comments now match
CLAUDE.md's wording.
publishPack's EMPTY_DASHBOARD (422) and NOT_FOUND (404) arms carry a human
message; the packs route and publish_board_pack spread only code and breaches,
so the receipt fell back to spelling the enum as words. EMPTY_DASHBOARD has no
phrasing of its own in REFUSAL_PHRASES, so that message is its whole
explanation. BAD_COUNTERSIGN still answers { error } and nothing else.
A2UIProvider.processMessages never throws: it catches the processor error,
console.warns it, records the message in its error state and returns void,
bumping the store version counter only when the op list applied. Both block
surfaces wrapped the call in try/catch and latched the ops hash in the
success path, so the catch was dead code and the hash latched on REJECTED
lists too - the next snapshot carrying that list was skipped as a duplicate
and the card stayed blank forever.
Both call sites now record the version before calling and latch on a later
render only if it advanced. The exec grid additionally moves its A2UIProvider
from the grid down to each BlockCard: a shared provider has one error slot
(any sibling's success cleared the failing block's banner) and one version
counter (a sibling's success would read as this block's rejection having
applied). The failure line now names the block from the grid's own context,
since only 2 of the processor's 6 rejection messages name a surface.
askCopilot swallowed a rejected runAgent to the console, leaving the user turn it had just posted unanswered with no on-screen signal; it now also raises a dismissible error line in the layout. The presenter reset never logged and reported the route's 403 FORBIDDEN gate as a generic 'Reset failed', which reads as a broken reset rather than a disabled flag; it now logs and names the refusal, tolerantly matched on the body's error or the status in the message so a contract change degrades to the generic arm. The OGUI design brief told the model to read the host's --surface/--canvas custom properties, which never resolve inside the sandbox iframe (bare CSS reset, no app stylesheet); it now inlines literal hsl values, as commerce's and bookstore's briefs do. Adds a shell test pinning every id in the client SkinRegistry to a matching entry in the server agentRegistry and back, derived from both exported structures - a miss there 404s every run of a skin that otherwise renders perfectly, with nothing in tsc, lint or the neighbouring config suite to catch it. Pins the previously unguarded catalog, sandboxFunctions and toolLabels ride-alongs, and clears the shared module spies between cases.
CopilotKitProvider compares its array props by reference against the first
render and console.errors on any change, ungated by NODE_ENV. The layout
passed sandboxFunctions: skin.sandboxFunctions ?? [] — a fresh literal on
every render — so airline and bookstore (the skins that contribute none)
logged that error on every render after the first. The empty array is now a
module-level constant, and the a2ui / openGenerativeUI config objects are
memoized so the provider stops rebuilding its activity-renderer list each
render.
The report handoff pill also stopped claiming rendered on the canvas for
content the canvas will not claim; that arm now says a surface was generated
without promising where. The block: dispatch contract is unchanged.
The canvas claimed any a2ui-surface activity whose content blockSurfaceIdFrom
could not read — malformed content, a stringified envelope, a missing ops
array — which blanked the page behind a bare "Back" for a skin with no
CanvasSurface: the exact failure the old comment claimed to prevent.
Classification is now explicit and biased against the canvas: scan every op
for a surfaceId, parse a stringified envelope only far enough to recognise a
block: tile, and treat anything unreadable as skip rather than report.
Id-less surfaces get a position-derived fallback id instead of a shared
constant, so dismissing one no longer suppresses every later one, and an
id-less OGUI activity claims the canvas instead of discarding an earlier
surface. agent.messages is narrowed rather than cast, warning once on drift.
The dateline was period close + 5 days unconditionally, so on the 1st-4th
of every month it printed a date that had not happened yet (probed on
2026-09-04: "5 September 2026"). Clamp it to today, keeping the calendar-day
arithmetic that survives a DST fallback.
Also unifies the memo on one locale (en-US dates alongside its en-US
currency), makes the exported builder refuse a non-overrun instead of
Math.abs-ing the sign away, fixes a TZ teardown that wrote the literal string
"undefined" into the environment for every later test, and covers the two
untested route arms.
Exec ships two sets of token values (it is dark-capable) and had no contrast
guard, so six text pairs sat under 4.5:1: light --brand on --brand-soft at
2.71:1, light --positive at 3.55:1 on its soft and 3.90:1 on the card, dark
--negative at 4.31:1 on the card and 3.82:1 on its banner, and dark
--brand-violet at 3.91:1 on --brand-soft.
The brand pair could not be nudged: under a deep --brand-foreground the two
pairs --brand anchors pull in opposite directions and 4.5:1 on both is
arithmetically impossible. The light block now takes the shape every sibling
uses -- a deep brass fill under a white label -- and the dark block re-values
both sides.
Adds src/skins/exec/theme.test.ts, ported from the commerce guard: it parses
the shipped declarations, composites alpha tints before measuring, and derives
each pair's render sites by grepping for the class pair, so a citation cannot
rot and a pair that stops rendering fails instead of looking like coverage.
Also corrects the dark block's comment, which claimed only --brand and
--brand-soft were re-valued while the block already moved --brand-indigo and
--brand-violet too; the claim is now asserted rather than written.