Commit Graph

16234 Commits

Author SHA1 Message Date
Guido Vizoso 5361041117 test(showcase): bind the exec withheld-code sweep to its union and to schema branches
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.
2026-09-08 15:39:38 -03:00
Guido Vizoso 4fc1c00372 test(showcase): pin the exec metric vocabulary across all four of its restatements
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.
2026-09-08 15:39:38 -03:00
Guido Vizoso 641128921d fix(showcase): stop the exec block surface and the budget memo dying on reachable inputs
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.
2026-09-08 15:39:38 -03:00
Guido Vizoso 639b171a90 fix(showcase): stop one unrenderable block from 500ing the whole ledger GET
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.
2026-09-08 15:39:37 -03:00
Guido Vizoso 69d63765a9 fix(showcase): treat an unreadable 2xx filing receipt as a filed narrative
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.
2026-09-08 15:39:37 -03:00
Guido Vizoso 9989d2388c fix(showcase): resolve untrusted dashboard and agent ids by own key only
`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.
2026-09-08 15:39:37 -03:00
Guido Vizoso 308c7a25fb fix(showcase): settle the exec skin's chips and keep its receipts operator-facing
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.
2026-09-08 15:39:37 -03:00
Guido Vizoso 65dd8e4f92 chore(showcase): reconcile cross-agent seams from the round-3 fix batch 2026-09-08 15:39:36 -03:00
Guido Vizoso 3f6a532c90 docs(showcase): correct the reskinnable-demo counts, greps and cross-references CR round 3 flagged 2026-09-08 15:39:36 -03:00
Guido Vizoso 830f9f49f7 fix(showcase): make the exec skin's readables say what its screens say
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".
2026-09-08 15:39:36 -03:00
Guido Vizoso 6002737bf5 fix(showcase): disable the exec reset button while a reset is in flight 2026-09-08 15:39:36 -03:00
Guido Vizoso e5cf59ce16 fix(showcase): resolve only known exec roles so every memory bucket is swept 2026-09-08 15:39:36 -03:00
Guido Vizoso 3daf0316c5 fix(showcase): log the backend's rejection body when an exec seed POST fails 2026-09-08 15:39:35 -03:00
Guido Vizoso 4ec50e7a6a fix(showcase): record failed exec memory deletes instead of abandoning the bucket 2026-09-08 15:39:35 -03:00
Guido Vizoso 9182627674 docs(reskinnable-demo): correct two false claims in exec's skin comments
`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.
2026-09-08 15:39:35 -03:00
Guido Vizoso f918d5b9d3 fix(reskinnable-demo): resync exec's OGUI design brief with the shipped theme.css
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.
2026-09-08 15:39:35 -03:00
Guido Vizoso f37edd6c4c fix(reskinnable-demo): window exec's sandbox metric series only on a positive months
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.
2026-09-08 15:39:34 -03:00
Guido Vizoso c4a76082fb test(reskinnable-demo): stop the layout suite timing out under a full run
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.
2026-09-08 15:39:34 -03:00
Guido Vizoso b51c961da7 test(reskinnable-demo): require an identifyUser resolver for every skin agent
`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.
2026-09-08 15:39:34 -03:00
Guido Vizoso 74ab5f70b5 test(reskinnable-demo): make the layout's provider guard replica match the SDK
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.
2026-09-08 15:39:34 -03:00
Guido Vizoso 05b19bc78a fix(reskinnable-demo): resolve skins by own key so /constructor 404s
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.
2026-09-08 15:39:33 -03:00
Guido Vizoso 58743d9367 fix(showcase): give every a2ui surface exactly one home in the shell
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.
2026-09-08 15:39:33 -03:00
Guido Vizoso 0842fc48b4 fix(showcase): colour exec block deltas by breach and fix the variance plan marker
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.
2026-09-08 15:39:33 -03:00
Guido Vizoso ce37e4d090 fix(showcase): refuse breaches and memo driver splits the exec data cannot support
`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.
2026-09-08 15:39:33 -03:00
Guido Vizoso 47a6915516 test(showcase): assert the exec dashboard-block mutation response bodies
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.
2026-09-08 15:39:32 -03:00
Guido Vizoso f24ccb3abd fix(showcase): keep the Intelligence backend address out of the exec reset body
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.
2026-09-08 15:39:32 -03:00
Guido Vizoso bd083434bb fix(showcase): serve the exec ledger no-store
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.
2026-09-08 15:39:32 -03:00
Guido Vizoso f809eefc03 fix(showcase): refuse exec narrative filings against a ledger row that does not exist
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".
2026-09-08 15:39:32 -03:00
Guido Vizoso 0b39bd851c fix(showcase): stop the exec skin reporting a published pack as refused
`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.
2026-09-08 15:39:32 -03:00
Guido Vizoso 3d663f0149 fix(showcase): return exec tool failures as results instead of hanging the tool chip 2026-09-08 15:39:31 -03:00
Guido Vizoso 7f641cf245 fix(showcase): unify the exec block-spec prop refusal code and export its kind-prop map 2026-09-08 15:39:31 -03:00
Guido Vizoso 0416774700 test(showcase): silence the unused destructure in the narratives source-default test 2026-09-08 15:39:31 -03:00
Guido Vizoso 4313de0e71 docs(showcase): retire the stale skin-path defect claim in the nav-target test 2026-09-08 15:39:31 -03:00
Guido Vizoso 2197770b24 docs(showcase): describe the nav-target join as it works, not as a live defect
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.
2026-09-08 15:39:30 -03:00
Guido Vizoso b48bc63e64 fix(showcase): carry the publish refusal's message onto the HITL card path
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.
2026-09-08 15:39:30 -03:00
Guido Vizoso c56cb0fb34 docs(showcase): correct two comments that outlived the code they describe
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.
2026-09-08 15:39:30 -03:00
Guido Vizoso 9bf0eb3f51 fix(showcase): relay the store's refusal message through both publish paths
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.
2026-09-08 15:39:29 -03:00
Guido Vizoso 9d6a42f00f test(showcase): match the exec contrast render-scan to the split card markup 2026-09-08 15:39:29 -03:00
Guido Vizoso ce600f6078 fix(showcase): detect rejected A2UI op lists via the store version counter
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.
2026-09-08 15:39:29 -03:00
Guido Vizoso be03693699 test(showcase): cover exec route success, guard and leak arms 2026-09-08 15:39:29 -03:00
Guido Vizoso 03ee6f4a3d fix(showcase): surface exec chrome failures and guard the two-registry invariant
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.
2026-09-08 15:39:28 -03:00
Guido Vizoso 41ac2ec175 docs(showcase): correct exec-skin falsities in the reskinnable-demo docs and derive the roster fixtures 2026-09-08 15:39:28 -03:00
Guido Vizoso 7c20a8d7d0 fix(showcase): keep the skin layout's CopilotKit provider props stable
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.
2026-09-08 15:39:28 -03:00
Guido Vizoso 242869fb94 fix(showcase): never let an unrenderable a2ui surface hijack the shared canvas
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.
2026-09-08 15:39:28 -03:00
Guido Vizoso 680b78b9c5 fix(showcase): stop the budget memo datelining itself in the future
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.
2026-09-08 15:39:27 -03:00
Guido Vizoso 33763eac6c fix(showcase): stop exec dev-reset calling a half-configured backend an OSS reset 2026-09-08 15:39:27 -03:00
Guido Vizoso 9d5321d675 fix(showcase): verify exec's memory sweep instead of assuming one page and a live row 2026-09-08 15:39:27 -03:00
Guido Vizoso fe109c78e9 fix(showcase): bound exec seed POSTs and make the seeded procedure stepwise 2026-09-08 15:39:27 -03:00
Guido Vizoso ef7c2dee42 fix(showcase): key exec's operator identity off a Map so prototype names cannot resolve 2026-09-08 15:39:26 -03:00
Guido Vizoso dfdbffd0b1 fix(showcase): bring exec's token pairs up to the contrast bar its siblings guard
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.
2026-09-08 15:39:26 -03:00