Skip to content

refactor(angular): signals-based reasoning collapse - #2

Open
rainerhahnekamp wants to merge 7 commits into
feat/ng-a2ui-expfrom
refactor/angular/reasoning-message-collapse-signals
Open

rainerhahnekamp wants to merge 7 commits into
feat/ng-a2ui-expfrom
refactor/angular/reasoning-message-collapse-signals

Conversation

@rainerhahnekamp

Copy link
Copy Markdown
Collaborator

Summary

Replace effect()-driven collapse/expand behavior in CopilotChatReasoningMessage with derived signals/computed state.

Why

Avoid propagating UI state via effect(); keep toggle behavior deterministic while streaming.

Tests

  • pnpm -C packages/angular test -- -t "renders streaming reasoning messages"

Made with Cursor

msari-ipe-ext-1 and others added 7 commits June 5, 2026 12:40
Adds angular-developer and angular-new-app skills via `npx skills add`
from https://github.com/angular/skills into .agents/skills/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apply the common Angular CLI schematics to code which has been added
by Soverius.
We can do a follow-up for the high amount of effects, but we have to be
careful. We need tests first to do a refactoring for that.
Revert open-generative-ui-tool-renderer and drop empty imports arrays.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@rainerhahnekamp
rainerhahnekamp force-pushed the feat/ng-a2ui-exp branch 2 times, most recently from d0f3161 to 015701f Compare June 10, 2026 20:48
rainerhahnekamp pushed a commit that referenced this pull request Jun 15, 2026
…-side parity (SU7-F1/F2/F3)

SU7-F1 — backend host pattern hardening:
- F1.1  Reject bare trailing ?/# in the backend host pattern
- F1.2  Strip internal tab/CR/LF from the backend host pattern
- F1.3  Warn when ignoring an empty-string local backend override
- F1.4  Reject empty-userinfo @ in the backend host pattern authority
- F1.5  Keep __proto__ keys as data in local-backend maps
- F1.6  Commit the local-backends memo key only after the value computes
- F1.7  Trim local backend overrides before validation and name the real
        rejection
- F1.8  Honest FATAL when the pattern host is a stray scheme fragment
- F1.9  Canonicalize the pattern authority for parity with the override
        path
- F1.10 Acknowledge the staging-to-prod fail-open in the pattern fallback
- F1.11 Harden backend-url/local-backends-env test hygiene

SU7-F2 — runtime-config & client-config edge cases:
- F2.1  Branch POSTHOG_HOST rejection reasons (scheme/degenerate/parse-
        failure) instead of the catch-all mislabel
- F2.2  Reject loopback BASE_URL/DOCS_HOST in production instead of the
        silent http:// prepend
- F2.3  Key the DOCS_HOST fallback once-guard on (mode, shellHost, value)
        and mode-prefix all value-only guard keys
- F2.4  Reject a present-but-empty posthogKey in the client config reader
- F2.5  Drop the trailing slash from SSR_PLACEHOLDER_URL for structural
        parity with server values
- F2.6  Attribute the DOCS_HOST slash-strip to readDocsHost itself
- F2.7  Normalize trailing-dot FQDN spellings in the docs self-host loop
        guard (both compare sides)
- F2.8  Harden console spies to capture all log args; pin the full all-env
        config shape; converge SSR simulation on vi.stubGlobal

SU7-F3 — script-side parity, table classification & test isolation:
- F3 #1  Handle a missing reference integration per the error contract
- F3 #2  Port the runtime backend-host-pattern normalization into the
         generator — scheme/trailing-slash strip, degenerate fallback,
         NEXT_PUBLIC fallback
- F3 #3  Treat non-mapping manifest parses (empty/null/scalar/array YAML)
         as validation errors, not TypeErrors
- F3 #4  Label a missing/unreadable constraints.yaml per the stderr+exit(1)
         error contract
- F3 #5  Align atomic-write tmp naming with the test harness straggler-
         sweep convention; guard main() on direct invocation
- F3 #6  Correct the determineCellStatus unshipped docstring; replace
         stale hardcoded cell counts with formulas
- F3 CopilotKit#7  Isolate the pattern suite on a per-suite tmpdir harness; snapshot
         the generator's full write set
- F3 CopilotKit#8  Classify discarded duplicate wildcards as duplicates — hoist the
         owner check above the destination warns
- F3 CopilotKit#9  Reject a root ("/") EXACT seo-redirect source — homepage-hijack
         twin of the root-wildcard guard
- F3 CopilotKit#10 Reject seo-redirect entries with non-printable-ASCII source/
         destination — close the silent-dead-entry class
- F3 CopilotKit#11 Strip trailing slashes in normalizePosthogHost before the scheme
         test
- F3 CopilotKit#12 Message-filter the empty-slug-set error count; pin the single
         matcher entry
rainerhahnekamp pushed a commit that referenced this pull request Jun 15, 2026
… not only an array nested value; dashboard mirror updated in lockstep (drift test pins byte-identity) (CR G2 #2)
rainerhahnekamp pushed a commit that referenced this pull request Jun 15, 2026
…cer-emitted degraded reads 'degraded', not 'stale' (CF6-G5 #2)
rainerhahnekamp pushed a commit that referenced this pull request Jun 15, 2026
… dereferencing probeKey (CF7 #2)

A null/undefined/primitive enumerator element threw a TypeError out of the
tick body, rejecting the tick promise and violating the 'tick never
rejects' invariant. Non-object elements are now dropped loudly and counted
in the existing invalid-spec accounting.
rainerhahnekamp pushed a commit that referenced this pull request Jun 15, 2026
…gray, never green (CF7-F3 #2)

The D1-D4 gate fires only on d3.exists/d4.exists, so a cell with ONLY
green D5/D6 rows (no e2e/chat/tools rows at all) slipped past it and
rendered a green chip + green d6Effective at achievedDepth=0/
ceilingDepth=0 — a false top-of-ladder claim contradicting the
strictness doctrine (PRESENT-but-null D4 grays; D5-no-data grays the
ladder). A wholly absent D3/D4 family now collapses to the gray
"unverified" chip (same shape as the d4NoData collapse) with red-D5/D6
dominance preserved, and d6Effective stays blocked (null).

cell-model.ts:847-852 (gate) / :905-921 (d6Effective).

One existing fixture (amber pass-through under reclaimed-pending) built
its amber from an absent-D3/D4 map; it now carries green e2e/chat rows
so the chip is genuinely amber through an intact ladder — the test's
never-mask assertion is unchanged.
@rainerhahnekamp
rainerhahnekamp force-pushed the feat/ng-a2ui-exp branch 2 times, most recently from 3064fb9 to cd3a94f Compare June 17, 2026 15:57
wolfmanfx pushed a commit that referenced this pull request Jul 10, 2026
…rwarded inbound headers (CopilotKit#5782)

## Problem

When a self-hosted v2 `CopilotRuntime` is configured with a server-side
agent (an `@ag-ui/client` `HttpAgent` with static `headers` for
service-to-service auth), the runtime forwards inbound
`authorization`/`x-*` request headers onto the agent's outgoing call
**and lets them override the headers the server configured** — silently
breaking service-to-service auth to a secured backend (e.g. a private
Cloud Run agent behind IAM).

`Fixes CopilotKit#5712`

## Root cause


`packages/runtime/src/v2/runtime/handlers/shared/agent-utils.ts:125-128`
merged forwarded inbound headers **last**, so they won on collision:

```ts
agent.headers = {
  ...agent.headers,                      // server-configured
  ...extractForwardableHeaders(request), // inbound — overrode the above
};
```

There are actually **two** failure modes:

1. **Same-case collision** — inbound `authorization` overwrites a server
`authorization` (last-write-wins).
2. **Case-mismatch collision** — `extractForwardableHeaders` lowercases
inbound keys (`authorization`), while the server typically configures
canonical casing (`Authorization`). A plain spread treats those as
*distinct* keys and emits **both** — which undici downstream comma-joins
into a single invalid `"Bearer A, Bearer B"` ("multiple JWTs") value.
Flipping the spread order alone does **not** fix this case.

## Fix

In `agent-utils.ts`, make server-configured `agent.headers`
authoritative on collision, matched **case-insensitively**: drop any
forwarded inbound header whose name (case-insensitively) is already set
on the agent, and let non-colliding inbound headers pass through
unchanged. This preserves the existing forward-for-auth behavior for
headers the server does *not* set, while guaranteeing a server-set token
is never overridden or duplicated.

The merge logic lives in a shared
`mergeForwardableHeaders(serverHeaders, request)` helper in
`packages/runtime/src/v2/runtime/handlers/header-utils.ts` so the
precedence semantics are defined in exactly one place.

### Scope note

This is the conservative precedence + case-insensitive-dedup fix (the
issue's suggested fix #1). I did **not** tighten the default allowlist
to drop hop-by-hop/platform `x-*` headers (`x-serverless-*`,
`x-forwarded-*`, …) or add an opt-out — those alter existing forwarding
behavior and are worth a separate, deliberate change. The precedence fix
alone resolves the reported breakage (the server-set token now wins
regardless of what the platform injects on a colliding header name).

A documented workaround already exists for users on released versions:
pass a custom `fetch` to the `HttpAgent` that builds outgoing headers
from scratch (it runs after `configureAgentForRequest` and survives the
per-request `agent.clone()`).

## Red-green proof (the real fix — `/run` path)

The load-bearing assertion: there must be exactly **one** authorization
header carrying the **server** value.

### RED (fix stashed, against unmodified `agent-utils.ts`)

```
 ❯ src/v2/runtime/__tests__/agent-header-precedence.test.ts (2 tests | 1 failed)
   × configureAgentForRequest — header precedence (CopilotKit#5712) > server-configured agent headers win over a colliding inbound header
AssertionError: expected [ 'Authorization', 'authorization' ] to have a length of 1 but got 2
     81|     expect(authKeys).toHaveLength(1);
 Test Files  1 failed (1)
      Tests  1 failed | 1 passed (2)
```

The pre-existing `agent-utils-header-forwarding.test.ts` also failed,
because it explicitly encoded the buggy behavior
(`expect(...["x-aimock-context"]).toBe("new-context")` — inbound
winning):

```
 FAIL  src/v2/runtime/__tests__/agent-utils-header-forwarding.test.ts > ... > request forwardable headers override matching pre-existing agent headers
AssertionError: expected 'old-context' to be 'new-context'
```

### GREEN (fix applied)

```
 ✓ src/v2/runtime/__tests__/agent-header-precedence.test.ts (2 tests) 2ms
 ✓ src/v2/runtime/__tests__/agent-utils-header-forwarding.test.ts (8 tests) 3ms
 Test Files  2 passed (2)
      Tests  10 passed (10)
```

The colliding test (`agent-utils-header-forwarding.test.ts`) was updated
from asserting the old bug to asserting corrected precedence + a new
case-insensitive-dedup guard. The non-colliding-forward test is retained
unchanged as a regression guard.

## Quality gates

```
NX  Successfully ran target check-types for project @copilotkit/runtime
NX  Successfully ran target test for project @copilotkit/runtime  — Test Files 113 passed (113), Tests 1576 passed (1576)
```

---

## `/connect`-path change — forward-looking plumbing, inert today

The original issue and a prior eval flagged the same forwarding pattern
at `handlers/sse/connect.ts`. To keep the two paths' merge semantics
consistent, the `/connect` path now builds the same server-wins merged
headers (via the shared `mergeForwardableHeaders` helper) and passes
them into `runner.connect()`.

**This is not an active auth fix, and it is not red-green-proven as one
— because there is no live bug to fix on the connect path today.** No
shipped runner consumes the `headers` field of
`AgentRunnerConnectRequest`: the in-memory, intelligence, telemetry, and
sqlite runners all destructure only `threadId` from the connect request
and ignore `headers` entirely. Connect is a thread replay/reconnect, not
a fresh outgoing agent call. So whatever headers we pass into
`runner.connect()` are dropped on the floor by every runner that ships.

What this change actually does:

- Threads the per-request agent clone through `handle-connect.ts →
handleSseConnect` so the connect path *has access to* the
server-configured `agent.headers` (it previously did not).
- Passes `mergeForwardableHeaders(agent?.headers, request)` into
`runner.connect()` — the correct, server-wins argument **shape** for a
future outbound-connecting runner that *would* consume connect-path
headers.
- Rewrites the comments/JSDoc on this path to say this plainly, rather
than implying an active auth fix. It also documents that the
connect-site `cloneAgentForRequest` call is the sole `agentId`-existence
guard (the intelligence branch never re-validates the id), and documents
`cloneAgentForRequest`'s `AbstractAgent | Response` (404) dual-return
contract that both callers depend on.

The real outbound header forwarding — the thing that fixes CopilotKit#5712 — is
the `/run` path's `agent.headers` mutation described above. The connect
change is staged plumbing so that if/when a runner starts honoring
connect-path headers, it inherits the same server-wins precedence
without a second fix.

### Tests on the `/connect` path

The connect tests assert the *merge shape* that reaches
`runner.connect()` (server value wins on collision, exactly one
`authorization` key, non-colliding `x-*` still forwards) and that the
agent-undefined case (no server `agent.headers`) degrades to forwarding
allowlisted inbound headers only and does not crash. These verify the
argument we construct is correctly shaped — not that any shipped runner
consumes it.

## Files

- `packages/runtime/src/v2/runtime/handlers/header-utils.ts` — shared
`mergeForwardableHeaders` helper (case-insensitive, server-wins).
- `packages/runtime/src/v2/runtime/handlers/shared/agent-utils.ts` —
`/run` path uses the helper so server headers win on collision (**the
real fix**).
- `packages/runtime/src/v2/runtime/handlers/sse/connect.ts` — `/connect`
path uses the helper; forward-looking plumbing, inert until a runner
consumes connect-path headers.
- `packages/runtime/src/v2/runtime/handlers/handle-connect.ts` — threads
the per-request agent clone into `handleSseConnect`.
-
`packages/runtime/src/v2/runtime/__tests__/agent-header-precedence.test.ts`
— `/run` regression test exercising the real `configureAgentForRequest`
surface with a real `HttpAgent`.
-
`packages/runtime/src/v2/runtime/__tests__/agent-utils-header-forwarding.test.ts`
— updated the test that encoded the old (buggy) precedence; added a
case-mismatch dedup guard.
-
`packages/runtime/src/v2/runtime/handlers/sse/__tests__/sse-connect-agent-id.test.ts`
— connect-path merge-shape + agent-undefined coverage.

### Notes

- A documented `@ag-ui/client` `HttpAgent` `fetch` workaround already
exists for attaching service-to-service auth the runtime can't override
(see the issue). This change makes the workaround unnecessary for the
`/run` precedence case.
- Conservative scope: this is the **precedence flip on `/run`** plus
forward-looking connect plumbing. Tightening the default allowlist
(dropping hop-by-hop / platform `x-serverless-*`, `x-forwarded-*`,
`x-cloud-trace-context`, …) and an opt-out switch — issue suggestions
#2/#3 — are intentionally left as a follow-up to keep the
security-policy change minimal.
wolfmanfx pushed a commit that referenced this pull request Jul 20, 2026
…d failures (CopilotKit#5956)

## Two silent-failure gaps in the showcase build/deploy/notify pipeline

These are **pre-existing** silent-failure holes surfaced in code review
(not
caused by any recent PR). This PR fixes the two load-bearing ones.

### 1. Green-but-zero-redeploy (silent "we thought we shipped but
didn't")

The `redeploy-staging` job computes the redeploy set as the intersection
of the
build matrix and the build-success set. This job **only runs when
`aggregate-build-results.outputs.any_success == 'true'`** (job-level
`if:`
guard). So if that intersection comes back **EMPTY**, it does NOT mean
"nothing
to deploy" — it means at least one slot built successfully yet none of
those
successes maps back to a matrix `dispatch_name`. That's a
`dispatch_name`↔
`service` contract skew (the aggregator's `service` values and the
matrix's
`dispatch_name` values drifted apart).

The old code emitted `services=` (empty) and exited 0 → the build went
**GREEN
while redeploying NOTHING**, silently.

**Fix:** on an empty intersection in this any_success-guaranteed step,
fail loud
(`::error::` + `exit 1`) with a diagnostic naming both sides of the
skew.
The legitimate "nothing changed / nothing succeeded" no-op paths are
guarded at
the **job level** (`has_changes=='true' && any_success=='true'`), so the
fixed
step never runs there — no false-red.

### 2. Starter build failures had no alert surface (invisible failures)

The `notify` job's `needs` (and its `if: failure()`) omitted
`detect-starter-changes` and `build-starters`, and `build-starters`
wrote no
per-slot build-result artifact. So a **failed starter image build
produced NO
Slack alert and NO PR comment** — it shipped silently.

**Fix:**
- Added `detect-starter-changes` + `build-starters` to `notify.needs` so
`if: failure()` sees a starter build failure → Slack alert + PR comment.
- Gave `build-starters` a per-slot build-result artifact **mirroring the
main
  `build` matrix** (same `{service,status}` shape, `cancelled→skipped`
  normalization, `if: always()`, `if-no-files-found: error`), using a
  **distinct `starter-build-result-*` prefix** so it never matches the
aggregator's `build-result-*` download pattern (starters must not
pollute the
  showcase redeploy set keyed by `dispatch_name`).

### Red / Green

**Finding #1** — extracted the step's shell/jq logic and drove it with
synthetic
inputs:

RED (pre-fix), any_success=true + empty intersection:
```
No services in matrix ∩ success-set — skipping redeploy.
Computed services CSV (matrix ∩ build-success):
EXIT=0        # $GITHUB_OUTPUT: services=   -> silent pass, redeploys NOTHING
```
GREEN (post-fix), same inputs:
```
::error::Build succeeded (any_success=true) but matrix ∩ success-set is EMPTY — dispatch_name/service contract skew; nothing would be redeployed.
Successful build service values: ["shell-RENAMED","mastra-RENAMED"]
Scheduled matrix dispatch_name values: ["shell","mastra"]
EXIT=1        # fails loud
```
No-regression: non-empty intersection → `EXIT=0 ; services=shell`. The
nothing-changed/nothing-succeeded paths are skipped at the job level
(never
reach the step) → no false-red.

**Finding #2** — modeled `if: failure()` (fires iff any `needs` job
result is
`failure`):
```
BEFORE (starters NOT in needs), starter=failure -> notify fires = False  (INVISIBLE, the bug)
AFTER  (starters IN needs),     starter=failure -> notify fires = True   (FIXED)
AFTER no-regression, starters=skipped, all green -> notify fires = False (quiet)
```

### Validation
- `python3 yaml.safe_load` parses OK.
- `actionlint`: only pre-existing findings remain (matrix jq SC2086 +
the known
`depot-ubuntu-24.04-4` runner-label warning); no new errors in edited
regions.
- `yamllint`: only pre-existing line-length/document-start/truthy
warnings.

### Scope
Touches **only** `.github/workflows/showcase_build.yml`, and only these
two
concerns. Does NOT touch the `shell_dashboard` paths-filter region (PR
CopilotKit#5955's
domain), nor the other backlog debt (false-root-cause comment,
double-alert,
check-lockfile guard). Self-contained; not stacked on CopilotKit#5955.
wolfmanfx pushed a commit that referenced this pull request Jul 20, 2026
CopilotKit#6035)

## What

Follow-up nit from [PR
CopilotKit#5969](CopilotKit#5969) review
([OSS-488](https://linear.app/copilotkit/issue/OSS-488)). Fixes the
`getMessages` JSDoc in
`packages/channels-intelligence/src/intelligence-adapter.ts`, which
oversold image support.

The comment cited *"what was in the image"* as a `read_thread` use case,
but image/file parts contribute **no text** in this mapping —
`read_thread` is text-only by design. Image *content* reaches the model
only via `conversationStore`'s seeding of `agent.messages`. The comment
now says so explicitly instead of implying `read_thread` can see image
content.

Comment-only change — no runtime/API behavior change, so no changeset.

## Scope of OSS-488

The ticket listed three nits; this PR addresses the one still
actionable:

- **#1 (double-space when joining content parts)** — ✅ already resolved
on `main` (the `.filter(Boolean).join(" ")` fix + updated test landed
via the OSS-476 CR round). Nothing to do.
- **#2 (JSDoc oversells image support)** — ✅ **this PR**.
- **#3 (duplicate `getHistory` fetch per turn)** — intentionally
**deferred**. A per-turn memo would need turn-scoped caching with a
clear-on-turn-start hook; without one it risks serving stale history — a
correctness regression on something the ticket itself rates "acceptable
for an on-demand tool." Not worth it here.

The **known limitation** (speakers collapse to `user`/`bot` because
upstream `AgentMessage` carries no names) is unchanged and not
actionable at this layer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
wolfmanfx pushed a commit that referenced this pull request Aug 6, 2026
Follows the maintainer's Correction #2 on issue 6363. An exact version in a
docs install command is the same rot as the starlette==0.45.3 pin it replaced:
it goes stale silently and nobody re-resolves prose. The 2.22.0 the docs shipped
was already a version behind current the day it was written.

- docs install lines use pydantic-ai-slim[ag-ui,openai]>=2,<3, which constrains
  the dep the pages actually care about and fails loudly at the v3 boundary
- ag-ui-protocol drops out of the docs lines entirely; no doc snippet imports
  ag_ui, so naming it there was the transitive-dep noise the correction is about
- starlette>=0.46.2 stays, because the v2 snippets import Starlette directly.
  A floor with no ceiling cannot force a downgrade, so it does not recreate the
  silent backtrack
- examples/showcases/pydantic-ai-todos moves to a range in pyproject.toml and
  relocks; the uv.lock is what reproduces
- examples/canvas/pydantic-ai keeps exact pins: it has no lockfile, so
  requirements.txt is its only reproducibility artifact

Smoke-tested the open question from the issue: starlette 1.x works on
pydantic-ai v2. All 8 doc pages pass on 2.23.0 + starlette 1.3.1 and on
2.23.0 + starlette 0.52.1, so Jordan's <1.0 guard can be dropped rather
than raised.
wolfmanfx pushed a commit that referenced this pull request Aug 14, 2026
…wave slots could not reach

Comment-only, no behaviour change. Each of these sat OUTSIDE the boundary of the
slot whose work falsified it, so each described the tree as it was mid-migration.
All verified against the current tree before editing.

src/shell/skin-contract.ts
- `RuntimeProviders`: "airline needs neither, so omits it". Airline omits
  `RuntimeProviders` and DOES supply `useRuntimeProperties` — the two are
  SEPARABLE, and airline is now the worked example: one account holder, no
  switcher, so its hook reads no context and returns a frozen module constant. Do
  not mount an empty provider for symmetry.
- `useData`: "Omit when a skin has no shell-managed data (banking …)". Every skin
  omits it now; documented with the grep, plus WHY the field is kept (the shape is
  legitimate, it just has no worked example left).
- `CanvasSurface`: the "omit if the skin has no report canvas" branch is currently
  unexercised — every shipped skin has one.

src/shell/agent-registry.ts
- The logistics entry said it ships "neither `intelligence/seed-memories.ts` nor
  `intelligence/forget-memories.ts`" and is "identity plumbing only — do NOT read
  it as a durable-memory demo". It ships both (`ls src/skins/*/intelligence/`), and
  its `dev/reset` sweeps and re-seeds through them. Corrected, with the same
  properties-forwarding caveat the other five entries carry, and a note that
  "expensive half built, cheap half skipped" was its state for two releases.

src/skins/keel/data/types.ts
- The `THE REST SUBSTRATE` banner said the two substrates are "deliberately not
  merged yet", that `useKeelData` "holds runs in `useState` and ticks them on a
  900 ms interval", and that "the pages still read it through `useSkinData`". All
  three are false: one substrate, one clock, `useSkinData` returns undefined.
  Rewritten to name the server-settled read (`settle-runs.ts`, called by both
  `GET /ledger` and `GET /runs/[runId]`) and to record WHY the deleted client
  ticker was a defect rather than a design choice — it was a second clock that
  painted progress the server never heard of, which the next re-read after any
  write silently rewound.
- The `KeelData` interface header claimed to be "the interface every page,
  component, and tool codes against". It is not referenced by any code at all
  (`grep -rn KeelData src` returns only comments). Marked HISTORICAL, with a
  do-not-add-a-consumer note. NOT deleted: it is a doc pass, several comments
  across the skin describe the migration in terms of this shape, and removing an
  exported type is a code change for a separate commit. Flagged as a follow-up.

src/skins/keel/data/store.ts
- "it does not advance them on a timer, because the ticker lives in `useKeelData`
  on the client. Whichever slot migrates that hook has to decide where the ticker
  ends up" — decided: the server is the only clock now.

src/skins/keel/data/beat-map.md
- Header: "Keel today is `useKeelData`, an in-memory `useState` store, and it hits
  about one beat." Marked BUILT and reframed as the design record.
- Risk #2 (the two-substrates/ticker question, correctly called "the biggest single
  risk in the migration") marked RESOLVED, with the answer (move the clock, do not
  relocate the ticker) and the generalised lesson.

src/skins/airline/data/beat-map.md
- Header: the tools/prompt/pages/pills were "later slots". All landed.
- § "What this slot did NOT build": every row of the deferral table has shipped.
  Kept as the retrofit record — which is the most useful thing about it — with a
  third column saying where each landed, and the three flagged traps marked
  resolved (including "the reset route says memoryBeats: unarmed on purpose",
  which was removed in exactly the change that added the seed module, as
  instructed).

src/skins/airline/data/fare-waiver-codes.ts
- "⚠️ THE LINT GUARD DOES NOT COVER THIS SKIN YET." It does: both
  `src/skins/airline/tools.tsx` and `agent.ts` are in `withheldGateVocabulary`'s
  `files` glob. Also dropped its "COUNT the selectors" instruction (that count has
  rotted twice) in favour of the resolved-selector table in
  `skins-config.test.ts`, and spelled out that a green lint still leaves the three
  prose channels AND `waiverGround` — which matches no `*_CODES` pattern, so the
  rule cannot see it — as hand-review items.

src/skins/airline/tools.test.ts
- Header said `statusKeyedTerminalRender` "covers logistics only; airline's glob
  entry is a later slot's, so until it lands this file is the whole guard" and that
  `withheldGateVocabulary`'s glob "does not list airline yet either". Both globs
  list airline now. Also fixed "Three defect classes" over a list of five.

src/skins/keel/skin.tsx
- "exactly as it does for the four other REST-backed skins" → every skin; nothing
  sets `useData`.

src/proxy.ts
- "matching how the other three skins behave" → numeral-free. This was one of the
  two known-stale instances named in `skin-roster-docs.test.ts`'s header; that
  header is updated in the app-docs commit, and the remaining one
  (`e2e/inset-layout.spec.ts`'s hardcoded four-skin loop) is deliberately left —
  fixing it means adding assertions against skins the spec has never visited, which
  is a coverage change rather than a prose fix.

docs/teach-mode/README.md
- It correctly refuses to write the roster into prose, but its verified-by-role
  paragraph named only banking/commerce/logistics/people and its "so copy commerce
  or logistics" line named the only two skins with pinned replay behaviour. The
  `offerWorkflowRecording` grep now returns every registered skin, and
  `ls src/skins/*/teach-mode-directives.ts` — added as the mechanical
  discriminator for role #3 — returns four. Also: "logistics and commerce both"
  skip project-scoped rows in `forget-memories.ts` is now every skin but banking,
  replaced with the grep that proves it.

Does this make anything in .claude/skills/reskin/ stale? No — the reverse. The
preceding commit updated the skill for exactly these facts, and these comments were
brought into line with it. Checked: `grep -rn "useKeelData\|use-data\|in-memory"
.claude/skills/reskin/` names no path or symbol that no longer exists.

Verified: pnpm lint, pnpm exec tsc --noEmit, pnpm test:unit (197 files / 2227
tests), pnpm build — all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wolfmanfx pushed a commit that referenced this pull request Aug 19, 2026
…angGraph docs gaps (CopilotKit#6520)

Bundles the CopilotKit-side work from **OSS-769**, **OSS-767**
(partial), and the unshipped remainder of **OSS-609**.

## OSS-769 — `useCoAgent().nodeName` never updates

`useAgentNodeName` tracked the current node in a ref and returned
`nodeNameRef.current`. Mutating a ref schedules no render, so a
component reading `useCoAgent().nodeName` kept showing whichever node
was current at its last render and never updated on its own — it only
appeared to work when something unrelated happened to re-render it.

Backing the value with state fixes it. Five lines, no API change.

## OSS-767 (partial) — silent content-part drops (CopilotKit#1748)

`normalizeMessageContent` handles only `text` and `binary` parts;
anything else — the `{"type": "image", ...}` case from the report — maps
to `""` and is filtered out with no signal, so an agent emitting
structured content sees its output vanish silently.

This makes the drop visible, once per unrecognised part type so
streaming doesn't flood the log. Deliberately **not** the schema change:
carrying structured assistant content needs an `AssistantMessage`
decision upstream in `ag-ui`, which stays open on OSS-767.

## OSS-609 — the five docs gaps that never shipped

Gap #2 shipped in CopilotKit#6403; gaps 1, 3, 4, 5 and 6 did not.

| Gap | Where | Closes |
| --- | --- | --- |
| AWS Lambda self-hosting | `docs/deploy/aws-lambda.mdx` | CopilotKit#1151 |
| Per-user thread authorization | `docs/auth.mdx` (new section) | CopilotKit#2241
|
| Thread persistence without the platform |
`docs/threads-self-managed.mdx` | CopilotKit#6090 |
| DIY guardrails / DLP | `docs/integrations/langgraph/guardrails.mdx` |
CopilotKit#3414 |
| When you need an MCP App | `docs/agentic-protocols/mcp.mdx` (new
section) | CopilotKit#5991 |

The Lambda guide leads with the constraint that actually bites —
streaming is opt-in on every front door, so a chat runtime deployed with
the defaults appears to hang for the whole run and then dumps the reply
at once. It documents the Function URL + `RESPONSE_STREAM` path as the
default, and API Gateway REST + `responseTransferMode: STREAM` for
anyone who needs a REST API in front.

`threads-self-managed` follows the existing shared-snippet pattern with
per-framework wrappers, because two nav contracts require it: every
authored framework must publish the page, and every React destination
must map to an Angular one (`ANGULAR_DOC_REDIRECTS`). All nine wrappers
were confirmed necessary by deleting one and watching the suite fail.

## Review corrections

Two blockers from @MikeRyanDev, both verified against primary sources
before changing anything.

**API Gateway REST APIs can stream**
([`91b3e632d5`](CopilotKit@91b3e632d5)).
The guide was built on the pre-November-2025 limitation and claimed no
API Gateway type supports response streaming, steering readers to a
buffered `serverless-http` setup. REST gained it via
`responseTransferMode: STREAM`, which also lifts the 10 MB cap and
29-second timeout. REST and HTTP are now split; REST is documented as a
streaming front door (payload-format-1.0 event adapter, `AWS_PROXY`
integration on the `2021-11-15/.../response-streaming-invocations` URI,
CLI/CDK/SAM config), and the buffered fallback is scoped to HTTP APIs
and ALB, which still have no streaming path. Added the constraints that
actually matter for chat: the 30-second idle timeout on edge-optimized
endpoints (5 min Regional), and the console Test tab always buffering so
a working config looks broken.

**`identifyUser` is the platform's thread-scoping binding**
([`91b3e632d5`](CopilotKit@91b3e632d5)).
The section told every reader to build an ownership table and enforce it
in `onBeforeHandler`. On the Intelligence path the runtime already
resolves `identifyUser(request)` server-side and carries that id to the
platform; `listThreads` is scoped by user *and* filtered by `agentId`,
so the "every user of one project sees that project's threads" claim was
wrong. `identifyUser` is now documented as the binding, and the DIY
pattern is scoped to SSE runtimes, custom stores, and the local
in-memory runner.

**Follow-up correction — two routes are genuinely unscoped**
([`240672ff56`](CopilotKit@240672ff56)).
My rewrite then over-claimed. `handleGetThreadEvents` and
`handleGetThreadState` resolve the caller and discard it, and the
platform client takes no `userId` on either method
(`client.ts:1113`/`1135`) — unlike `getThreadMessages` at `1063`. Both
hit project-authenticated `_inspect` endpoints, so any caller
`identifyUser` accepts can read the event log and agent state of **any
thread in the project** given its id. The blanket guarantee is replaced
by a per-route table marking those two explicitly unscoped, plus an
`onBeforeHandler` guard narrowed to them.

That is a live gap in shipped runtime code, not a docs error, and it is
tracked as **OSS-851** — a platform-side `_inspect` change plus matching
runtime/client work and tests, out of scope for a docs PR. The interim
callout in `auth.mdx` comes out when OSS-851 lands.

## Not in this PR

**OSS-772** and **OSS-773** are already merged in
`oss-path-to-production` (CopilotKit#237, CopilotKit#238). Both are telemetry-sink changes
with no CopilotKit-side component. OSS-773's remaining half — re-keying
runtime `distinct_id` from email to the Clerk subject — is recorded on
the ticket as an open decision, not a task.

## Testing

**OSS-769.** New `use-agent-nodename.test.tsx`, 5 tests. Against
unmodified `origin/main`, **4 of 5 fail**:

```
× re-renders consumers on every node transition
× reports 'end' when a run errors
× resets to 'start' when a new run begins
✓ unsubscribes on unmount
× carries the agent, thread, and current node
  Tests  4 failed | 1 passed (5)
```

With the fix: `Tests 5 passed (5)`. These assert only re-render
behaviour under normal `act()` flushing — no manufactured intra-batch
window.

**Full react-core suite:** `Tests 7 failed | 1496 passed (1503)`. All 7
failures are **pre-existing** `ResizeObserver is not a constructor`
under jsdom, confined to `CopilotChatView.pinToSend` and
`use-pin-to-send` — neither of which this PR touches.

**Typecheck:** `packages/react-core` → `tsc --noEmit` exit 0, no output.

**OSS-767:** 3 new tests covering the warn, warn-once-per-type, and
no-warn-for-supported-types. `src/graphql/message-conversion/` → `Tests
125 passed (125)`.

**Docs:** `showcase/shell-docs` → `Tests 1 failed | 373 passed (374)`.
The single failure (`channels-docs > publishes the Channels overview
only through provider navigation`) is **pre-existing**; baselining with
all changes stashed reproduces it and nothing else. Re-run unchanged
after both review-correction commits.

**Review corrections.** The AWS rewrite was checked against the AWS
sources rather than written from memory — the REST streaming
announcement, `configuration-response-streaming`,
`response-transfer-mode` (endpoint-type idle timeouts, unsupported
buffered-only features), `response-streaming-lambda-configure`
(CLI/OpenAPI shapes), the CFN `Integration` reference, and the CDK
`ResponseTransferMode` enum. Two details corrected in passing:
`InvokeWithResponseStream` authorizes against plain
`lambda:InvokeFunction` (no new grant, contrary to what the streaming
URI suggests), and ALB still has no Lambda streaming path.

The auth corrections were verified by reading the handlers and the
platform client, not the tests — `resolve-intelligence-user.ts`,
`intelligence/threads.ts` (every `resolveIntelligenceUser` call site),
and `intelligence-platform/client.ts`. The existing tests assert the
`threadId`-only call shape, so they pass under the defect and could not
have surfaced it. Also corrected: there is no `threads/delete` route —
delete is `DELETE` on `threads/update` (`fetch-handler.ts:606`).

Both edited pages MDX-compile clean, and all inbound
`#thread-authorization` anchors still resolve after the h3→h4 demotions.

Two nav tests broke during this work and are fixed rather than papered
over — adding a page to the Rich Threads group violated the
cross-framework ordering contract and the React→Angular parity contract:

```
src/lib/__tests__/docs-render.test.ts
src/lib/__tests__/angular-docs-content.test.ts
  Test Files  2 passed (2)
       Tests  33 passed (33)
```

All 15 internal links in the new pages resolve against the content tree.

Closes CopilotKit#1151, CopilotKit#2241, CopilotKit#3414, CopilotKit#5991, CopilotKit#6090
Refs CopilotKit#1748, OSS-851
wolfmanfx pushed a commit that referenced this pull request Sep 22, 2026
… false about the code (CopilotKit#7146)

## What this is

A consolidation of **CopilotKit#7137** and **CopilotKit#7142** into one reviewable change.
Both are the same defect class — *the dashboard says things about the
code that are not true* — so they belong in one review. Every commit is
cherry-picked as-authored, and every red/green proof from the original
PR bodies is reproduced verbatim below.

## What was broken

Two layers of the showcase dashboard made claims the code does not
support:

**1. The legend lied (CopilotKit#7137).** Four statements in `adaptive-legend.tsx`
/ `chips-explainer.tsx` assert things the code does not do:
- `✗` was described as "not supported". No code path emits `✗` for that
— `🚫` is the sole not-supported glyph. `✗` means *no demo shipped*
(whole cell) or *the probe ran and failed* (per-rung badge). The legend
pointed viewers at exactly the wrong cells.
- The chips-explainer described D6 as LangGraph-Python behavioural
parity on a *weekly rotation*. That describes `e2e-parity.ts`, which is
**unregistered** and cannot even parse its `kind`. Shipped D6 is
`d6-all-pills-e2e.yml`, **hourly**, and does no reference comparison at
all.
- The legend's own D6 entry repeated the same falsehood.
- `▼` was documented as "depth regression" but has **no emitter**
anywhere in the tree — dead legend for a glyph that is never rendered.
`—` was called "supported, no demo yet"; it is actually the
**ladder-gated** mark.

This PR corrects the **text only** — no glyphs, no components, no cell
model, no probes.

**2. The support mapping lied (CopilotKit#7142).** `starterSupport()` collapsed a
genuinely three-way world into two, so `strands-typescript`,
`claude-sdk-python` and `claude-sdk-typescript` — all real, shipping
starters — rendered 🚫 "unsupported framework". The missing distinction
is **unprobed** (we have not measured this cell) vs **unsupported**
(this framework genuinely cannot do this).

## The fix

- Legend text corrected to describe what the renderer actually emits;
the dead `▼` entry removed.
- `starterSupport()` made explicitly three-way: **probed** (12 columns
the fleet probes), **unprobed** (a starter exists in-repo but nothing
probes it — renders the gray `?` no-data chip, making *no* claim about
the framework), **unsupported** (no starter exists — the only state that
may render 🚫; five columns). The three real starters above move from 🚫
to `?`.

🚫 is an outward-facing capability claim about a partner integration; it
was being used to describe a gap in our own plumbing. The only prior
guard was `expect(STARTER_COLUMNS.size).toBe(12)` — a count, which held
at 12 the entire time three real starters were being called unsupported
frameworks.
- A new **non-circular** drift test guards the mapping — it asserts
against the starter inventory rather than against the mapping it is
testing, so the mapping cannot silently drift back. It is
mutation-tested: the exact mutation that reds it is shown verbatim
below.

One question was deliberately left open rather than guessed at; it is
called out in the CopilotKit#7142 section, as is a known limitation.

## Diff equivalence vs. the two source branches

No file is touched by both source PRs — there is **no overlap to
resolve**. Per-file blob hashes on this branch are byte-identical to the
corresponding source branch for all 7 files:

```
CopilotKit#7137   2 files   +5   -10
CopilotKit#7142   5 files   +493 -113
------------------------------
        7 files   +498 -123

this branch:
7 files changed, 498 insertions(+), 123 deletions(-)   ← exact match
```

Blob-hash check, all OK:

```
OK  showcase/shell-dashboard/src/components/adaptive-legend.tsx            (CopilotKit#7137)
OK  showcase/shell-dashboard/src/components/chips-explainer.tsx            (CopilotKit#7137)
OK  showcase/harness/src/probes/helpers/starter-mapping-drift.test.ts      (CopilotKit#7142)
OK  showcase/harness/src/probes/helpers/starter-mapping.ts                 (CopilotKit#7142)
OK  showcase/harness/src/shared/cell-model/live-status.ts                  (CopilotKit#7142)
OK  showcase/shell-dashboard/src/components/feature-grid.tsx               (CopilotKit#7142)
OK  showcase/shell-dashboard/src/lib/live-status.test.ts                   (CopilotKit#7142)
```

---
---

# Carried over verbatim — CopilotKit#7137 · `fix(shell-dashboard): correct legend
statements that are false about the code`

Four dashboard legend / explainer statements assert things the code does
not do. This corrects the **text only** — no glyphs, no components, no
cell model, no probes, no starters.

## The four falsehoods (verified at `origin/main` = `f72d3442`; the
dashboard tree is byte-identical to the recon base `abb0cf636c`)

| # | Where | Said | Actually |
|---|---|---|---|
| 1 | `adaptive-legend.tsx:179-182` | red `✗` = "not supported" | **No
code path emits `✗` for that.** `🚫` is the sole not-supported glyph in
both grids (`feature-grid.tsx:447-454`, `unified-cell.tsx:302-314`,
`depth-chip.tsx:277-291`, all tooltipped *"Not supported by this
framework"*). `✗` means **"no demo shipped"** (whole main-grid cell,
`title="No demo"`, `feature-grid.tsx:455-462`) **or "the probe ran and
failed"** (per-rung badge, `unified-cell.tsx:82-84`). The legend mapped
viewers to exactly the wrong cells. |
| 2 | `chips-explainer.tsx:38-41` | D6 = LangGraph-Python behavioural
parity, *"weekly rotation"* | That describes `e2e-parity.ts`, which is
**unregistered** — no YAML under `harness/config/probes/`, and no
`e2e_parity` member in `DIMENSIONS`
(`harness/src/types/index.ts:48-141`), so its `kind` cannot even parse.
Shipped D6 is `d6-all-pills-e2e.yml`, schedule `"40 * * * *"`
(**hourly**), driven by `d6-all-pills.ts`: the aimock-backed scripted
conversation run across **every** feature type the integration declares,
green only if all pass. It does **no** reference comparison —
`reference` and `langgraph` appear nowhere in the driver. |
| 3 | `adaptive-legend.tsx` D6 entry | "Parity (Reference): full
all-pills run verified against the reference integration" | Same
falsehood as #2 — nothing verifies against a reference. |
| 4 | `adaptive-legend.tsx:83-86`, `:111-114` | `▼` = "depth regression
from previous run"; `—` = "supported, no demo yet" | `▼` has **no
emitter** — `DepthChip`'s `regression` prop only changes colour +
tooltip; the only `▼` in the tree is `cell-drilldown.tsx:448`'s
disclosure chevron. Removed. `—` is the **ladder-gated** mark
(`unified-cell.tsx:57-68`, tooltip *"gated — blocked by a lower rung"*).
Corrected. |

## Red / green proof — rendered page, not DOM measurement

Local `next dev` on this branch's worktree, real catalog +
`docs-status.json` codegen, Playwright screenshots at 1440×900 and
430×932, DPR 2, inspected visually.

### RED (before) — legend, as rendered

`RED-desktop-legend.png` / `RED-mobile430-legend.png` — screenshots
taken and visually inspected; verbatim rendered text:
```
D6  Parity (Reference): full all-pills run verified against the reference integration
▼   depth regression from previous run
—   supported, no demo yet
✗   not supported
```

### GREEN (after) — legend, as rendered

`GREEN-desktop-legend.png` / `GREEN-mobile430-legend.png` — same run,
same viewports; verbatim rendered text:
```
D6  All Pills: every feature type the integration declares is run (D5 runs one representative); green only if all pass
    (▼ entry removed — nothing emits it)
—   gated — a lower rung is failing, so this rung was not run
✗   no demo shipped, or a probe ran and failed
```

### RED → GREEN — chips explainer, as rendered

`RED-chips-explainer.png` / `GREEN-chips-explainer.png`:

```
- D6: does its behaviour match the reference (LangGraph Python), or has it drifted?
      (DOM elements, tool-call sequence, stream cadence, contract shape — informational; weekly rotation)
+ D6: does EVERY demo cell work, not just the one D5 sampled? (the same scripted aimock
      conversations, run across every feature type the integration declares — green only if
      all of them pass; hourly)
```

Screenshots (RED + GREEN, desktop + mobile, both surfaces) are archived
at

`~/.local/share/copilotkit/cr/showcase-starters-2026-09-14/legend-truth/`.

> **Note on the chips-explainer surface:** `ChipsExplainer` is currently
**unreachable** — it is rendered only by `cells-view.tsx`, which nothing
imports; the app's only route is `/` → `DashboardPage` with tabs
`matrix`/`baseline`/`ops`. To get a genuine rendered-page red/green
rather than a DOM assertion, the component was mounted through a
throwaway `app/chips-preview` route for the screenshots; **that route is
not part of this diff** (`next build` route table below confirms it).

## Gates

- `npx tsc --noEmit` — clean
- `npx vitest run` — **71 files, 1407 passed**, 1 skipped
- `npx next build` — green; routes `/`, `/_not-found`,
`/api/ops/[...path]`, `/icon.svg` (no preview route)
- `oxfmt --check`, `oxlint` — clean; lefthook pre-commit + commitlint
passed

## Deliberately NOT changed

These are real defects but belong to the pending **glyph-encoding
redesign**, not to a truth correction, and editing them here would
create churn the redesign has to undo:

- `🚫`, `⚡`, `⟳` and the D-chip colours are undocumented in the legend —
the redesign replaces the marks themselves, so adding entries for the
current glyphs would be written twice.
- The coloured literal `D5` / `D6` chip entries misdescribe how cells
actually render (`1P ✓`, `D6 ✗`) — owned by the redesign.
- `?` "probe has not yet ticked since deploy" is true for starter rows
but false for the main grid, where `Badge` returns `null` for `label ===
"?"`. Un-hiding it is a **behavioural render change** and a product
call, not a text fix.
- **Confirmed and recorded, not fixed:** `⚡`'s "pool unreachable" chip
has *always* painted **yellow**, never indigo. `⚡` is U+26A1 with
`Emoji_Presentation=Yes`, so the colour-emoji font ignores the
`text-indigo-300` class at `depth-chip.tsx:133-146` — contradicting that
component's own doc comment at `:39-46`. `🚫` (U+1F6AB) has the identical
bug against `text-slate-400` / `text-white`. This is a code-point
problem; no CSS colour property can reach a COLR/bitmap emoji glyph.



---

## Commit 4 — the glyph redesign (approved rendered mock)

The three items deferred above are now implemented. `jpr5` signed off on
the rendered mock at `glyph-mock.html`; this is that design.

### The two defects

1. **Three glyphs were Emoji code points, so CSS colour never reached
them.** `🚫` (U+1F6AB) painted **red** on a grey starter chip whose glyph
was explicitly `text-white` — the background said "nothing to test here"
and the mark said "this is bad", simultaneously, on 36 of 84 starter
cells. `⚡` (U+26A1) is the same bug: `depth-chip.tsx`'s own doc comment
promises an indigo "pool unreachable" treatment, and the glyph has
painted **yellow** since the day it landed. `⏱` (U+23F1) is the third.
2. **Absence and failure were the same SHAPE**, so hue was the only cue
— and hue is what fails at 16px, in greyscale, and under colour
blindness. The loudest mark in the entire table was a 16px red `✗` for
**unbuilt work**.

### The encoding

**One filled object per cell** — the thing the cell reports (the depth
chip in a feature cell, the status chip in a starter cell). It goes
**hollow** when nothing was judged. Everything else in a cell is a
**bare mark**: prefix plus a coloured glyph, no box, no fill, no border.
The `UI` / `BE` / `1P` / `D6` prefixes stay.

- Verdicts `✓ ~ ✗` — filled, white glyph, green/amber/red.
- Absences `∅ · ? — ! ⟳` — hollow, tone in the glyph, slate; except the
`!` faults, which are **indigo**, because an infrastructure failure is
not a verdict.
- Colour-free backstop: hollow chips carry a border **style** (solid = a
settled fact · dashed = a slot not filled in · dotted = expected, hasn't
arrived); bare absence marks carry a hairline **dotted underline**.
- `?` is **no longer suppressed** in the main grid. `Badge` used to
return `null` for it, so the grid computed a state it never painted —
which is exactly what made the legend's `?` entry false there.

### RED → GREEN, on the rendered page

Both captures are the same local dev server (`next dev`, port 3002),
same viewports, at DPR 1 and DPR 2, light and dark. No DOM measurements
were trusted for any visual claim — the screenshots were inspected.

**RED (before):**

| state | rendered |
|---|---|
| starter, not supported | grey filled chip with a **RED** `🚫` —
`text-white` ignored |
| starter, no data | grey **filled** chip — indistinguishable from a
verdict |
| main grid, no demo shipped | 16px **red** `✗` |
| depth, pool unreachable | indigo-bordered chip with a **YELLOW** `⚡` —
`text-indigo-300` ignored |
| depth, unshipped | `--` (the same mark as the ref-depth spacer) |
| rung, no data | **nothing rendered** |

**GREEN (after):**

| state | rendered |
|---|---|
| starter, not supported | hollow `∅`, solid slate border |
| starter, no data | hollow `?`, dotted border |
| main grid, no demo shipped | hollow `·`, dashed border |
| depth, pool unreachable | hollow `!`, **indigo** — the colour the
component promised, applying for the first time |
| depth, unshipped | hollow `·`, dashed |
| rung, no data | a visible `?` with a dotted underline |

Verdict chips are unchanged: filled, white glyph. The live dashboard
capture shows the whole starter block leaving the verdict layer, and the
legend regenerating itself from the same constant.

### Anti-rot

`showcase/harness/src/shared/cell-model/glyphs.ts` is **one constant**
that the renderer AND the legend both switch on — `AdaptiveLegend` maps
over it, so a glyph cannot be emitted without a legend row.
`glyphs.contract.test.tsx` fails CI on any drift.

Every new assertion was **mutation-tested**; each was observed failing
before it was trusted:

| mutation | result |
|---|---|
| `depth-chip` unsupported emits a literal `"X"` (a mark outside the
vocabulary) | **RED** — 2 tests |
| `depth-chip` unshipped emits `"--"` | **RED** — 2 tests |
| `AdaptiveLegend` filters out the `gated` row | **RED** — 2 tests |
| add a documented-but-unemitted glyph (`▼`) to `GLYPHS` | **RED** — 1
test |
| set `notSupported.mark` back to `🚫` | **RED** — the emoji ban |

One mutation exposed a defect *in the test itself*: the first draft
scanned `textContent` for marks, and the legend prose legitimately
contains `—` as a separator, so deleting the `gated` legend row left the
suite **green**. The extraction now reads the `data-glyph` attribute
that `StatusChip` / `GlyphMark` stamp, which makes the legend row itself
the evidence. The emoji ban also carries a **positive control**: it
asserts the rule fires on `🚫`, `⚡` and `⏱`, the three glyphs this
dashboard actually shipped.

### Gates

- `next build` — passes (the dashboard's real gate).
- `vitest run` — 72 files, 1425 passed, 1 skipped.
- `tsc --noEmit` — clean (dashboard and harness; the harness `axe-core`
error pre-exists on `origin/main`).
- Harness `src/shared/cell-model` — 214 passed.

### Not touched, deliberately

`chips-explainer.tsx` is unreachable dead UI (nothing imports
`cells-view.tsx`; the app's only route is `/`). It also contains **no**
vocabulary glyph — it describes D0–D6 in prose — so it is already
consistent and needed no edit.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Starter status now distinguishes probed, unprobed, and unsupported
integrations.
* Integrations with repository starters but no live probe display a gray
“?” badge with explanatory text.
* Dashboard status indicators now use a consistent set of accessible
text glyphs and chip styles.

* **Bug Fixes**
* Corrected starter-status indicators, preventing false “Not supported”
messages.
* Improved probe failure coloring to distinguish isolated from repeated
failures.
  * No-data and fault states now remain visible with clearer indicators.

* **Documentation**
* Clarified dashboard legends and chip descriptions for probe outcomes,
skipped checks, and feature coverage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants