Defense-in-depth pair to R3-A.4. Orchestrator now trims the token at
boot so the value handed to the bearer-auth middleware is the same one
the comparison runs against. The fail-loud empty-string guard already
handled the all-whitespace case at the env-var level; this hunk only
normalises non-empty padded values so a " abc " env var boots cleanly
and accepts "Bearer abc" without forcing operators to know about the
auth layer's internal trimming.
Replace `String(err)` in `logger.error` call sites across orchestrator.ts
with the shared `logErrorWithStack` helper from probe-invoker.ts. The
helper attaches `errName`, the message, a 5-line truncated stack at
error level, and emits the full stack in a parallel `<msg>.full-stack`
debug log. Pre-fix, error logs lost the stack entirely — operators
post-mortem-reading the orchestrator log saw only the message and had
no way to correlate errors back to the throwing call site.
Export `logErrorWithStack` from probe-invoker.ts so it can be reused
without introducing module-level duplication. Bus emit payloads and
warn-level logs that carry an `err` field keep `String(err)` (their
contracts and audience are different — bus payloads are stringified
for downstream consumers, warn logs are non-fatal).
Pre-fix the middleware trimmed the presented Bearer token but compared
against the verbatim env value. A copy-pasted OPS_TRIGGER_TOKEN with
surrounding whitespace booted clean but silently 401'd every well-formed
request. Trim BOTH sides at construction so the comparison is symmetric;
an all-whitespace expected token collapses to empty and trips the
existing fail-loud MissingAuthTokenError guard.
The per-service variables-loop catch used to rethrow on
`ctx.abortSignal?.aborted === true` OR a real AbortError from
fetch. The first half of that disjunction over-rethrew unrelated
mid-loop failures: once an external tick-timer fires the
controller, every subsequent iteration sees signal.aborted === true,
so a plain Railway 500 on the next service iteration also escaped
the catch and killed the whole tick — operators saw a discovery
abort with no useful breadcrumb on which service actually 500'd.
Narrow the rethrow to a real AbortError only. We walk up to two
`cause` links because gql() wraps fetch rejections in
DiscoverySourceTransportError; the underlying AbortError lives on
err.cause. Walking the chain catches that case without
re-tripping on unrelated transport errors that happened to fire
while the signal was aborted. AuthError rethrow remains unchanged.
While here, propagate B2 partial-data correlation: gql() now
returns { data, partialData } so downstream Zod-failure logs can
tag `cause: "partial-data"` when the same request already fired
the partial-errors warn. Operators reading the log stream now see
one correlated pair instead of two unrelated keys for the same
root cause. Project-level schema-failure throw includes
"(cause: partial-data)" in the message; the per-service
variables-schema warn carries cause: "partial-data" as a meta
field.
Regression test: 3-service project, abort fires between service-1
and service-2, service-2's variables call returns 500. Asserts the
500 records as env: {} + a single variables-failed warn instead
of escaping the loop, and that signal.aborted === true at the
end (confidence the abort actually fired).
The widened package-name regex in classifyShape() accepts
multi-segment names like `showcase-langgraph-python` without
warning — but it can't distinguish a legitimate multi-segment
package name from a 1-edit-distance typo of `showcase-starter-*`.
`showcase-strater-ag2` (a/r transposition) currently classifies
as package silently, so a typo-renamed Railway service flips
deployment archetype on the next tick without any audit signal.
Add a Levenshtein-distance-1 nudge (with adjacent-transposition
detection — `strater` is distance-2 by pure Levenshtein but
distance-1 with transpositions): when classifying as package, if
the first segment after `showcase-` is exactly one edit away from
`starter`, emit a structured warn
(`discovery.railway-services.classify-typo-suspected`) with the
service name and a suggested correction. Classification is NOT
changed — that would break the documented "anything-not-starter is
package" fallback. The warn is the audit trail; operators decide
whether to act.
Tests cover: (a) the typo case fires the warn with name +
suggested, (b) legitimate multi-segment package names do not
fire, (c) well-formed starter names do not fire.
ENOENT on registry.json is steady-state for non-demos consumers
(image-drift, smoke, aimock-wiring) — they don't mount the registry
volume — so in dev/test it correctly logs at info to avoid pulsing
the alert stream every tick. In production however a missing
registry is genuinely an operational concern: the volume mount may
have failed, or the image was built without the registry payload
copied in. Promote ENOENT back to warn only when
NODE_ENV === "production" so production operators see the missing
registry as a pageable signal while local/CI runs stay quiet.
Other read errors (EACCES, EIO, AbortError) already logged at warn
regardless of environment and continue to do so — those signal an
active fault, not steady-state.
Reorders the out.push({...}) properties to match the
RailwayServiceInfo interface declaration order (name, imageRef,
publicUrl, env, shape, deployedDigest, demos). The previous order
interleaved deployedDigest between imageRef and publicUrl, which
matched neither the interface nor any logical grouping. Pure
ergonomic refactor — no behaviour change. Reviewers can now grep
the interface and the emit site side by side without re-shuffling
fields.
R3-B.1: status-table lastRunTone() returned green for partial-pass runs
(failed=0, passed<total), reintroducing the misread that R2-D.4 fixed in
status-runs-list. Schedule table is the most-viewed surface, so this is
the highest-impact regression. Mirror runs-list semantics: amber tone +
"P/T (S skipped)" result text. Also explicit "P/T pass (F fail)" for
failed runs so the column reads consistently with the tone.
R3-B.2: StatusDetailPanel durations filter used \`typeof d === "number"\`
which admits NaN/Infinity (typeof NaN === "number"). The sparkline then
computed Math.min(...durations) = NaN and emitted "NaN,NaN" coords in
the polyline. Switch to Number.isFinite() so non-finite durations are
dropped.
Tests added (red-green TDD):
- status-table: amber tone + "skipped" text for partial pass; explicit
"pass (N fail)" text for failed; "N/N pass" for fully-passed
- status-detail-panel: NaN/Infinity durations don't appear in sparkline
Adds explicit unit coverage for two paths the suite was previously
silent on:
B5 — deployedDigest extraction. The fixture builder used to discard
latestDeployment entirely, leaving the production code path that
pulls imageDigest from latestDeployment.meta unit-test uncovered.
The fixture now optionally accepts a latestDeployment block; new
tests assert: happy path string digest flows through; null
latestDeployment yields ''; missing meta yields ''; non-string
imageDigest is rejected defensively.
B6 — resolveShape mismatch-throw path. The classifier-vs-explicit
disagreement throw is the load-bearing assertion preventing silent
shape drift at driver boundaries; previously it had zero tests.
Two new tests cover both directions (starter classifier + package
input, package classifier + starter input) and pin the error
message to include both values for operator triage.
R3-C.1: useTriggerProbe.trigger now returns Promise<TriggerResponse | null>.
The supersession path resolves to `null` (typed) instead of the previous
`undefined as unknown as TriggerResponse` cast, which lied about the
return shape and would crash any caller that read fields off the result.
R3-C.2: useProbeDetail clears `error` on id change so the detail panel no
longer renders a stale error message under a freshly-selected probe header.
R3-C.3: useProbes clears `error` on dep change (e.g. baseUrl swap) so a
prior dep tuple's failure does not persist under the new one.
R3-C.4: useTriggerProbe clears `error` at the start of each trigger call
so a successful retry leaves no sticky error from the prior failure.
Bonus: parseJson now attaches the original parse error as `cause` on the
wrapper so debuggers can walk back to the SyntaxError name/stack without
re-parsing the wrapped message.
Tests: red-green tests added for each behavior; full file suite 49/49.
R3-D.2: the toggle-close branch on the Trigger button forgot to reset
pickerOpen and selected, while outside-click and Escape both did. So
toggling the menu closed and reopening it showed ghost checks from a
prior session. Extract a closeMenu() helper used by all three close
paths so the invariant is structural, not duplicated.
Bonus: add aliveRef guard so async rejections from onTrigger that
resolve after unmount don't trigger setState-on-unmounted warnings.
R3-D.1: fetchProbes and fetchProbeDetail are polled every ~10s for live
status data. Without an explicit cache directive the browser and Next.js
App Router fetch wrapper may serve stale responses, defeating the poll.
Add cache: 'no-store' to both GET call sites and cover with tests.
E1: railway-services floor scan now matches both `.yml` and `.yaml`
extensions so a future probe authored as `<name>.yaml` cannot silently
bypass the floor invariant.
E2: every entry in `RAILWAY_PROBES_EXEMPT_FROM_EXCLUDES_FLOOR` must
either carry a `namePrefix` strictly narrower than the broad
`showcase-` floor, or its rationale string must include the grep-able
phrase `all services`. Today qa.yml passes via the narrow-prefix path
(`showcase-langgraph-python`); image-drift.yml passes via the
rationale-phrase path. If qa.yml is later widened to `showcase-`
without dropping the exemption (or restating an explicit
all-services rationale), this guard fires instead of letting infra
services flap red on a stale exemption.
E3: shorten the duplicated cwd-resolution rationale at the top of the
file; the load-bearing explanation now lives once on `configsDir()`.
The watch-callback test for probe-loader was flaking because chokidar's
native FSEvents backend on macOS does not reliably emit events for files
written into a freshly mkdtemp'd directory under Node 22+: the parent path
isn't pre-watched by the kernel, so the first add silently drops.
Add a test-only watcherOptionsOverride field on ProbeLoaderDeps so tests
can opt into chokidar's polling backend (the documented chokidar workaround
for this exact case), and switch the flaky test to use it. Also replace the
fixed setTimeout(500ms) wait with vi.waitFor polling so the assertion no
longer races chokidar's debounce on slow CI.
Production callers leave watcherOptionsOverride undefined, so prod still
gets FSEvents/inotify with no perf change.
Verified stable across 7 consecutive runs of the watch test.
The aimock-fixture-coverage probe scans byoc-json-render.spec.ts and was
flagging two prompts without matching feature-parity.json fixtures:
- "Break down revenue by category as a pie chart"
- "Show me monthly expenses as a bar chart"
Existing pie/bar fixtures used substrings ("revenue distribution by
category", "expenses by category") that don't appear in these prompts,
so the substring matcher returned no hits.
Added two new fixtures with substrings that DO appear in the prompts
("revenue by category as a pie chart", "monthly expenses as a bar
chart") so the coverage probe stays green and aimock returns
deterministic chart data when the BYOC json-render specs run against
the sidecar.
The degraded-state tooltip read "stale — last pass @ <observed_at>", but
observed_at is the most recent producer tick (when the row was last
*seen* in any state) — for a degraded row that is when degradation was
last observed, NOT when it last went green. Operators reading "last
pass @ ..." would mis-triage by treating that timestamp as the last
green tick.
Change copy to "stale — last seen @ <observed_at>" and add a covering
test asserting the new wording (and the absence of "last pass").
R11: The 'How to invoke' example referenced a nonexistent
defaultLaunchBrowser symbol. Reorganized to lead with the CLI wrapper
(scripts/d6-capture-references.ts) which already wires the default
deps; the programmatic example below is annotated as test/internal-
only with a comment pointing at the script for the canonical wiring.
R7 (e2e-parity half): Same per-turn slicing pattern as reference-
capture — runFeatureCapture's inner runConversation always reports
failure_turn === 1. Translated to outer 1-based index in the
'conversation-error' errorDesc so dashboards show which turn actually
failed. Added covering test asserting turn 3 (not turn 1) is reported
when the third turn fails.
R8: createDefaultFleetResolver cached successful reads forever, so
operators editing registry.json needed a process restart for D6
rotation to see the new fleet membership. Added mtime-based
invalidation plus a 60s wall-clock TTL safety rail. Resolver factory
now accepts injected now/stat for deterministic testing. Added two
covering tests: mtime-change invalidation and TTL elapsed even when
mtime is unchanged.
R10: When selectedThisTick === false (rotation skips a slug), the
driver returned aggregate green but emitted no per-feature side rows,
leaving stale red on the dashboard for the entire rotation cycle (up
to a week). Now emits a green per-feature side row with a 'not
selected this tick' note for every requested-known feature so cells
refresh while clearly signalling we didn't actually verify parity.
Updated the existing rotation-skip test to assert one side row per
requested feature and tightened slug expectations.
R9: pickToolCallName checked the bare 'name' field before the
structured 'tool_call.name' / 'tool_use.name' fields. Some transports
include a top-level 'name' that's a step/event label alongside a
structured tool descriptor — that ordering harvested the label and
poisoned the parity-compare engine's tool-call list.
Reordered to: toolCallName -> tool_call.name -> tool_use.name -> name.
Added 3 covering tests asserting structured fields win over a top-level
step-label and the canonical toolCallName still wins over everything.
R7 (reference-capture half): The capture helper runs each turn as
runConversation(page, [turn]) so the SSE interceptor only sees that
turn's request. The inner runConversation always reports
failure_turn === 1 (its slice-local index), which would mislabel
every per-turn capture failure as 'turn 1' regardless of which
outer turn actually failed. Translated to outer 1-based turn index
when constructing the failure reason. Updated the existing covering
test to assert turn 2 (not turn 1) when the second turn fails.
R6: Four sibling files duplicated the assistant-message selector
cascade with subtle drift; some used a bare '[role="article"]'
fallback that picked up user-input bubbles into the transcript and
caused turn-1 user-message substrings to spuriously match the user's
own input.
Hoisted ASSISTANT_MESSAGE_PRIMARY_SELECTOR (canonical testid) and
ASSISTANT_MESSAGE_FALLBACK_SELECTOR ([role="article"] excluding
data-message-role="user") to conversation-runner.ts. Updated
_hitl-shared, _gen-ui-shared, d5-agentic-chat, and d5-shared-state to
import the shared constants and apply them via a string-templated
new Function so closures don't have to cross the page.evaluate
boundary while DOM-lib remains excluded from the helper module.
R5: TIME_PICKER_SLOT_SELECTORS entries themselves embedded the
'[data-testid="time-picker-card"] ...' prefix, and the helper then
composed cardSelector + entry — producing nonsense like
'[data-testid="time-picker-card"] [data-testid="time-picker-card"] button >> nth=0'
that never matched. Simplified the cascade to its bare slot variants
('[data-testid="time-picker-slot"]', 'button >> nth=0') and rely on
the helper's prefixing for card anchoring.
Added _hitl-shared.test.ts with 3 covering tests: no doubled card
prefix anywhere in probed selectors, click composes card + slot, and
fallthrough cascade still anchors under the resolved card selector.
R4: The CLI script's package-root resolution was inlined and not unit
tested. Hoisted to src/probes/helpers/package-root.ts with dependency-
injected exists predicate so it picks up vitest's src/**/*.test.ts
glob. Added 5 covering tests (source-tree, dist-tree, startDir match,
missing throws, deepest stop).
R1: classesEqual was a strict length-equality check that rejected
captured-superset cases. Replaced with capturedClassesIncludeReference
(multiset superset) so a captured DOM with extra classes still passes
while a missing reference class fails the comparison.
R2-R3: When captured.total_chunks === 0 but reference.total_chunks > 0
the timing math degenerates and the stream axis silently passed. Added
an early-return guard with a clear failure reason so the parity verdict
reflects reality.
R2-B.1 — diffCronSchedules now awaits scheduler.unregister and emits
orchestrator.cron-unregister-failed on rejection; orphan rule stays
visible in scheduler.list() (mirrors CR-A2.2 probe-path fix). Watch
callback uses .then/.catch to surface reload failures via the bus.
R2-B.2 — boot wraps serve() in try/catch; on throw, awaits
scheduler.stop() and resets schedulerRunning before rethrowing, so a
late bind failure (e.g. EADDRINUSE) cannot leave cron jobs ticking
without an owner.
Tests: 2 new red-green specs added under orchestrator.test.ts
covering both rejection paths (vi.spyOn(logger.error) + vi.doMock for
the @hono/node-server serve() throw).
R2-B.3: distinguish unset from set-but-empty. An operator who mistypes
`OPS_TRIGGER_TOKEN=` ships an empty value AND sees the silent-skip
log instead of a clear error. Trim and reject whitespace-only too.
- unset → boot succeeds, info log (intentional disable)
- set-empty → boot throws (misconfig — refuse to mount probes router)
- set-value → boot succeeds, router mounted
Adds 4 covering tests under "OPS_TRIGGER_TOKEN empty-string handling".
R2-A.11: two robustness improvements to 1777165230_create_probe_runs.js:
- CREATE INDEX statements now use IF NOT EXISTS so a partial-apply
doesn't trip the next migration run on the index DDL step. The
collection-presence gate above already covers saveCollection
idempotency; this covers the per-index path in case PB ever runs
index DDL separately from the schema commit.
- Document why the up-migration's findCollectionByNameOrId catch must
remain broad: PB JSVM does not expose typed error discrimination
(no ErrCollectionNotFound), so we cannot narrow without a runtime
feature change. The down-migration's catch is already narrowed
because it operates on a resolved-or-skip path.
R2-A.10: ProbeRunTracker.start() spread the existing entry, so a
defensive call on a slug that previously transitioned through complete
or fail carried finishedAt / result / error into the new running entry.
The snapshot then surfaced a "running" service with terminal-state
metadata, which is incoherent.
Replace the spread with an explicit reset to the running shape so a
re-started slug starts fresh.
Tests: assert finishedAt/result/error are undefined after start() on a
previously completed or failed service.
R2-A.7: when pb.getOne returns null (row deleted, lookup race, etc.)
the previous code fell through with NaN duration and called pb.update
on a non-existent id, either throwing on the underlying client or
silently writing junk. Add an explicit null check that logs a warning
and returns early, so finish() is a no-op when the row is gone instead
of producing a stale-row error.
Test: asserts pb.update is not called when getOne returns null.
Five hardening fixes on the /api/probes routes:
R2-A.4: validate filter.slugs is a string[] before forwarding to the
scheduler. The invoker constructs new Set(filterSlugs) on the value, so
a string sneaks through as per-character set membership. Reject malformed
shapes with 400 invalid_filter at the route boundary.
R2-A.5: stamp the rate-limit window AFTER body read + JSON parse +
filter validation, not before. A 400 from validation must not consume
the operator's 5-minute hold. New sequence: auth -> 404 -> body read ->
parse -> filter shape -> rate-limit check -> stamp -> trigger.
R2-A.6: rollbackRateLimit now compare-and-swaps on its own t value
before restoring the prior stamp. Without this, two racing triggers
where A errors after B successfully stamped would have A's rollback
delete B's stamp.
R2-A.8: cap POST body at 16 KiB. Reject oversized payloads with 413
both via Content-Length pre-check and post-read length check (chunked
clients can lie about Content-Length).
R2-A.9: GET /api/probes/:id wraps writer.recent in try/catch so a
transient PB outage degrades to runs:[] + runsError: history_unavailable
with a 200, instead of 500-ing the whole probe-detail page.
Tests: added 13 new tests across five describe blocks covering each
fix, including a concurrent-trigger CAS rollback scenario and chunked
body-size enforcement.
Three correctness fixes in the probe invoker:
R2-A.1: when discovery enumerate fails, the RunSummary previously returned
total=0 with failed=1, violating total === passed + failed. Treat the
discovery itself as the single failed unit so the invariant holds.
Also stop polluting tracker.snapshot().services with a fake entry keyed
by the probe id (cfg.id is not a service slug); the failure is conveyed
structurally via discoveryFailed: true.
R2-A.2: Promise.race against the driver promise left the driver's tail
rejection unhandled when the timeoutPromise won. Under
--unhandled-rejections=throw this can crash. Attach a no-op .catch() to
the driver promise before the race so late rejections are absorbed; the
race outcome is unchanged.
R2-A.3: same fix for the discovery enumerate Promise.race path.
Tests: updated CR-A1.5 invariant assertion plus added two unhandled-
rejection guard tests for driver and enumerate late-reject paths.
Bucket-(b) bonus polish:
- humanizeCron now renders "M H * * *" with a non-zero minute as
"Daily at HH:MM" instead of falling through to the raw expression.
- humanizeCron rejects */0 step values for both the every-Nh and every-Nm
shapes — that's not a valid cron step.
- StatusRunningPanel: rename `completed` to `finished`. The variable
counts services in either completed OR failed states, so "completed"
read as misleadingly success-only. The data-completed attribute name
stays for backwards compatibility with existing tests.
A run with failed=0 but passed<total represents some services that were
skipped/unknown — rendering this as green "completed" misleads operators
into thinking all targets passed (R2-D.4). Now badged amber with a
"partial (N skipped)" label so the partial-coverage case is visible.
Three related fixes to StatusTriggerButton:
- R2-D.2: render onTrigger errors inline via data-testid="status-trigger-error"
instead of discarding the value through a write-only setter.
- R2-D.3: when serviceSlugs prop changes, intersect selected with the new
list so stale slugs cannot be POSTed; also clear selection on
outside-click and Escape close so reopening doesn't show ghost checks.
- R2-D.5: drop unreachable serviceSlugs.length === 0 ternary inside the
picker — the outer guard already requires length > 0.
Replace the no-op handleTrigger stub and empty-list placeholder with real
hooks. useProbes feeds the schedule grid via 10s polling; useTriggerProbe
POSTs /trigger with the operator token. If NEXT_PUBLIC_OPS_TRIGGER_TOKEN
is unset, the trigger throws a clear "token required" error so operators
see fail-loud feedback instead of a silent no-op (R2-D.1).
POST to /trigger is non-idempotent — the server may have already received
the request and queued the run by the time the component unmounts.
Aborting the controller client-side hides the result, but does NOT
unfire the server-side action. Stop aborting on unmount; rely on
aliveRef to gate any setState after teardown.
Back-to-back trigger() calls intentionally abort the prior controller.
The first call's promise was rejecting with AbortError, surfacing as a
user-facing error on the Status tab. Detect AbortError (and the
cancelled-controller case) in the catch block and resolve silently
instead of setError + rethrow. The owning-controller guard on the
finally block prevents a superseded call from flipping pending off
while a newer call is still in flight.
Bonus consistency for useProbes: setLoading(true) on dep change so
consumers can render a "refreshing" indicator across baseUrl swaps.
Tests:
- unmount during in-flight trigger does NOT abort the signal
- post-unmount resolution does not call setState (no act warnings)
- back-to-back triggers: first promise resolves silently, error stays null
ensureOk and parseJson previously caught body-read failures and rewrapped
them in a generic Error, losing the AbortError name. Hooks filter abort
noise via err.name === "AbortError", so the wrap defeated cancellation
filtering and surfaced "AbortError" as a user-facing failure on the
Status tab.
Re-throw AbortError as-is from both helpers; non-Abort body errors keep
the descriptive wrap path. Also tighten ProbeKind to use (string & {})
so known-kind autocomplete is preserved alongside forward-compat strings.
Add covering tests:
- response.text() rejects with AbortError → ensureOk re-throws as-is
- response.json() rejects with AbortError → parseJson re-throws as-is
- non-Abort body errors still wrap with descriptive message
- triggerProbe with already-aborted signal surfaces AbortError unwrapped
Backstop the matrix fixes and address weak/no-op assertions:
- Tighten depth-chip count assertions from '>= N' to exact 'N'
where the fixture pins the count.
- Replace the no-op filter=wired test with a fixture containing a
feature whose every cell is unshipped, then assert that row IS
visible under filter=all and IS HIDDEN under filter=wired.
- Extend the collapse test with a re-expand click that asserts
chip count returns to baseline (regression guard for one-way
toggle state).
- Cover filter=regressions empty-state.
- Cover starter cells (feature === null) in both matrices: the
cell index must skip them without polluting the grid or leaking
a 'null' string into rendered output.
- Pin parity-matrix integration-header ordering across all five
parity tiers (reference, at_parity, partial, minimal, not_wired).
- Cover D2 cap for starter cells in deriveDepth (chat/tools cannot
lift past D2 because D3 is not evaluable without a feature id).
- Replace the locally-declared CatalogCell shape in depth-utils.test
with the source type so tests cannot drift from production.
- Add afterEach(vi.unstubAllGlobals) to both test files so the
localStorage stub does not leak across files in the same worker.
Three related cleanups in cell-matrix.tsx and parity-matrix.tsx:
1. Drop the 'as 0 | 1 | 2 | 3 | 4' typecasts on depth.achieved. The
newly tightened DepthResult.achieved literal-union type makes
them redundant.
2. Skip starter cells (feature === null) when building the
cell-by-key index. The previous code interpolated null into the
key string, producing '<slug>/null' entries that orphaned
starter rows in the index.
3. Render an explicit empty-state in CellMatrix when filter is
'regressions'. The placeholder logic returned false for every
row, silently hiding the entire matrix. Until regression
detection lands (DepthResult.isRegression always false), the
matrix now surfaces a 'Regression detection not yet implemented'
message via data-empty-reason='regressions-not-implemented'.
deriveDepth's return type carried the loose 'number' on achieved, forcing
every consumer to cast to '0 | 1 | 2 | 3 | 4' to satisfy DepthChip's
prop type. Expose AchievedDepth alias and narrow the source so the
casts at use sites (cell-matrix.tsx, parity-matrix.tsx) become
unnecessary.
Per-cell status row corrections + a focused unit-test file covering CP1,
CP2, CP5, CP7, CP8 behaviors:
- CP3: stop consuming `cell.smoke` in CellStatus. resolveCell still
computes the field for backwards-compat with other call-sites, but
per Phase 3 Decision #7 the smoke L1 signal is integration-scoped and
lives in the per-integration strip — there's no per-feature smoke
badge in the cell row. (The CellState type narrowing for the `smoke`
field is owned by the live-status.ts fix in a sibling worktree; this
patch only removes the consumer.)
- CP8: hide the D5/D6 LiveBadges when feature.kind === "testing".
The e2e-deep / e2e-parity producers only emit rows for primary
features per spec; testing features would otherwise render a
perpetual gray "?" that adds noise without information. Mirrors the
docs-row visibility rule (already gated on isTesting).
- CP9: document the missing per-cell drilldown URL convention for D5/D6
with a TODO comment. No drilldown route exists today; once one does,
wire it through keyFor here.
Tests cover red-green for CP1 (mouseleave handler wired), CP2 (first /
error / green_to_red transition formatting), CP5 (opt-out vs absent
tooltip copy), and CP8 (D5/D6 hidden for testing features). The
existing docs-link.test.tsx still passes.
- CP1: wrap Badge in a span that resets `tooltipOpen` on mouseleave/blur
so the lazy-fetch flag can flip back to false. Without this, the flag
stayed true forever after the first hover, defeating the §5.6
optimization and (once the hook upgrades to a subscription) leaking it.
- CP2: discriminate `first` and `error` transitions in the tooltip copy
so operators can tell "first observation (initial: <state>)" apart
from "producer error (error → <state>)" instead of seeing the same
fallback string. Logic moved into a named `formatTransitionLine`
helper for testability.
Four fixes to the per-cell docs row:
- CP4: extract the `unselected` shell-routing fallback into
SHELL_UNSELECTED_PATH so the coupling is documented and easy to find.
- CP5: split the `missing` tooltip into "framework opt-out" vs "no docs
URL declared" so operators can distinguish a framework that declined
docs from a registry entry that never declared one.
- CP6: add `default: never` exhaustiveness guard to DocsLink's switch so
any new DocState variant is a compile error until handled.
- CP7: render `<a>` (not `<span>`) for `state === "error"` when an href
is present so operators can manually verify a flaky probe — `notfound`
stays a span because the URL is positively known broken.
LS6: `mergeRowsToMap` now logs a `console.warn` when the same key is
seen twice across the merged groups. Each dimension is supposed to own
a disjoint slice of the keyspace, so a collision is an upstream bug.
Return semantics are unchanged (last-wins remains correct under
eventual consistency), but the warn surfaces the regression in dev mode.
LS7: `upsertByKey` returns the SAME array reference when the incoming
row is observably identical (key + state + observed_at +
transitioned_at). Producers re-emit unchanged rows on every poll tick;
allocating a fresh array for each one defeats React's reference-equality
short-circuit and re-renders every memoised consumer downstream.
Tests cover no-op identity, mutation paths (state change,
observed_at change), the disjoint-merge happy path, and the collision
warn.
LS1: `allGreen` now requires BOTH `healthRow` and `e2eRow` to be present
and green. Previously a missing e2e row was treated as green-eligible,
which let a brand-new cell read green before any e2e probe had ticked —
the same flavour of stale-green lie as the connection=error case.
LS11: smoke lookup now uses the integration-scoped key (`smoke:<slug>`)
to match the producer. Pre-fix, resolveCell looked up
`smoke:<slug>/<featureId>` which always missed, leaving every smoke badge
gray. Tests update fixtures accordingly and add a regression guard so a
stray feature-keyed smoke row cannot bleed into the resolver.