instar 1.3.43 → 1.3.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/index.html +68 -14
- package/dashboard/process-health.js +204 -53
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +292 -3
- package/dist/commands/server.js.map +1 -1
- package/dist/core/HandoffReceiver.d.ts +56 -0
- package/dist/core/HandoffReceiver.d.ts.map +1 -0
- package/dist/core/HandoffReceiver.js +103 -0
- package/dist/core/HandoffReceiver.js.map +1 -0
- package/dist/core/HandoffWireTransport.d.ts +61 -0
- package/dist/core/HandoffWireTransport.d.ts.map +1 -0
- package/dist/core/HandoffWireTransport.js +114 -0
- package/dist/core/HandoffWireTransport.js.map +1 -0
- package/dist/core/HttpLeaseTransport.d.ts +68 -0
- package/dist/core/HttpLeaseTransport.d.ts.map +1 -0
- package/dist/core/HttpLeaseTransport.js +97 -0
- package/dist/core/HttpLeaseTransport.js.map +1 -0
- package/dist/core/HttpLiveTailTransport.d.ts +88 -0
- package/dist/core/HttpLiveTailTransport.d.ts.map +1 -0
- package/dist/core/HttpLiveTailTransport.js +95 -0
- package/dist/core/HttpLiveTailTransport.js.map +1 -0
- package/dist/core/LeaseCoordinator.d.ts +14 -0
- package/dist/core/LeaseCoordinator.d.ts.map +1 -1
- package/dist/core/LeaseCoordinator.js +38 -0
- package/dist/core/LeaseCoordinator.js.map +1 -1
- package/dist/core/LiveTailSource.d.ts +65 -0
- package/dist/core/LiveTailSource.d.ts.map +1 -0
- package/dist/core/LiveTailSource.js +87 -0
- package/dist/core/LiveTailSource.js.map +1 -0
- package/dist/core/MultiMachineCoordinator.d.ts +14 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +22 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/ReplyMarkerTransport.d.ts +58 -0
- package/dist/core/ReplyMarkerTransport.d.ts.map +1 -0
- package/dist/core/ReplyMarkerTransport.js +66 -0
- package/dist/core/ReplyMarkerTransport.js.map +1 -0
- package/dist/core/handoffReceiverWiring.d.ts +57 -0
- package/dist/core/handoffReceiverWiring.d.ts.map +1 -0
- package/dist/core/handoffReceiverWiring.js +65 -0
- package/dist/core/handoffReceiverWiring.js.map +1 -0
- package/dist/core/handoffSentinelBootWiring.d.ts +53 -0
- package/dist/core/handoffSentinelBootWiring.d.ts.map +1 -0
- package/dist/core/handoffSentinelBootWiring.js +52 -0
- package/dist/core/handoffSentinelBootWiring.js.map +1 -0
- package/dist/core/handoffSentinelWiring.d.ts +60 -0
- package/dist/core/handoffSentinelWiring.d.ts.map +1 -0
- package/dist/core/handoffSentinelWiring.js +59 -0
- package/dist/core/handoffSentinelWiring.js.map +1 -0
- package/dist/core/seamlessnessConfig.d.ts +8 -0
- package/dist/core/seamlessnessConfig.d.ts.map +1 -1
- package/dist/core/seamlessnessConfig.js +1 -0
- package/dist/core/seamlessnessConfig.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +6 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +8 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/ingressDedup.d.ts +55 -0
- package/dist/messaging/ingressDedup.d.ts.map +1 -0
- package/dist/messaging/ingressDedup.js +69 -0
- package/dist/messaging/ingressDedup.js.map +1 -0
- package/dist/messaging/stuckMessageRecovery.d.ts +46 -0
- package/dist/messaging/stuckMessageRecovery.d.ts.map +1 -0
- package/dist/messaging/stuckMessageRecovery.js +52 -0
- package/dist/messaging/stuckMessageRecovery.js.map +1 -0
- package/dist/server/AgentServer.d.ts +64 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +24 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/handoffInitiateRoutes.d.ts +40 -0
- package/dist/server/handoffInitiateRoutes.d.ts.map +1 -0
- package/dist/server/handoffInitiateRoutes.js +57 -0
- package/dist/server/handoffInitiateRoutes.js.map +1 -0
- package/dist/server/machineRoutes.d.ts +47 -0
- package/dist/server/machineRoutes.d.ts.map +1 -1
- package/dist/server/machineRoutes.js +138 -0
- package/dist/server/machineRoutes.js.map +1 -1
- package/dist/server/routes.d.ts +20 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +65 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +49 -49
- package/upgrades/1.3.44.md +72 -0
- package/upgrades/1.3.45.md +28 -0
- package/upgrades/side-effects/cross-machine-seamlessness-consent-acquire.md +52 -0
- package/upgrades/side-effects/cross-machine-seamlessness-exactly-once-ingress.md +73 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-ack-yield.md +82 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-begin.md +48 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-boot-wiring.md +68 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver-wiring.md +52 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver.md +46 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-sentinel-wiring.md +47 -0
- package/upgrades/side-effects/cross-machine-seamlessness-lease-wire.md +46 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-source.md +50 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-streaming.md +50 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-wire.md +65 -0
- package/upgrades/side-effects/cross-machine-seamlessness-noloss-recovery.md +61 -0
- package/upgrades/side-effects/cross-machine-seamlessness-reply-marker.md +65 -0
- package/upgrades/side-effects/process-health-tab-eli16-pass.md +49 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Side-Effects Review — Cross-Machine Seamlessness: no-loss stuck-message recovery (D-noloss)
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3a ("Stuck-`processing` recovery")
|
|
4
|
+
|
|
5
|
+
The complement to the inbound dedup gate. With dedup alone, a crash AFTER claiming an
|
|
6
|
+
inbound event but BEFORE the reply commits would lose the reply (Telegram won't redeliver
|
|
7
|
+
once polled). This adds the spec's explicit "re-run from the stored input by the current
|
|
8
|
+
lease holder" mechanism: a `processing` entry stuck past `maxProcessingMs` is re-routed from
|
|
9
|
+
its `inputSnapshot`, producing the lost reply. Together with the dedup gate this makes the
|
|
10
|
+
flag's guarantee genuinely *exactly-once* (no-loss AND no-duplicate). Still flag-gated dark
|
|
11
|
+
(`multiMachine.exactlyOnceIngress`, default false).
|
|
12
|
+
|
|
13
|
+
## What changed
|
|
14
|
+
- `src/messaging/stuckMessageRecovery.ts` — NEW. `recoverStuckMessages(deps)`: lease-gated
|
|
15
|
+
(a standby never injects); `reclaimStuck(maxProcessingMs)` → for each entry with a stored
|
|
16
|
+
input and `attempts < maxReplayAttempts` (default 3), re-claim under the current epoch and
|
|
17
|
+
call the injected `reinject(topic, dedupeKey, text)`. Pure orchestration over injected deps.
|
|
18
|
+
- `src/commands/server.ts` — after `server.start()`, ONLY when the ledger is wired (flag on)
|
|
19
|
+
AND telegram is present: builds `reinject` (sets the per-topic current-inbound key, then
|
|
20
|
+
routes the stored text via the same `telegram.onTopicMessage` path a fresh forward uses),
|
|
21
|
+
runs recovery at boot, and on a cadence (`max(30s, maxProcessingMs)`), lease-gated.
|
|
22
|
+
|
|
23
|
+
## Over-block / under-block
|
|
24
|
+
- OVER-re-run (the risk = re-answering a message the agent legitimately left unanswered, or a
|
|
25
|
+
persistently-failing turn looping): bounded by (1) only entries stuck past `maxProcessingMs`
|
|
26
|
+
(a genuinely timed-out turn, not one in flight), (2) `attempts < maxReplayAttempts` cap —
|
|
27
|
+
after the cap the entry is left alone, never re-run again, (3) lease-gate (only the awake
|
|
28
|
+
machine), (4) flag-off default. Proven both sides in stuck-message-recovery.test.ts.
|
|
29
|
+
- UNDER-recover: an entry with no `inputSnapshot` can't be replayed (nothing captured) → skipped
|
|
30
|
+
honestly rather than guessed. Inputs ARE captured by the dedup gate's `decideIngress(input:text)`.
|
|
31
|
+
- The known single-duplicate residual (spec §3 / §8 G3a impossibility floor): a crash AFTER the
|
|
32
|
+
reply physically sent but BEFORE `reply_committed` persisted → re-run produces one duplicate.
|
|
33
|
+
This is the documented Two-Generals floor, bounded to one, not a defect.
|
|
34
|
+
|
|
35
|
+
## Signal vs authority
|
|
36
|
+
- recoverStuckMessages decides which entries to re-run; the re-injection authority is the
|
|
37
|
+
existing onTopicMessage routing. The module holds no Telegram/server coupling (injected deps).
|
|
38
|
+
|
|
39
|
+
## Interactions
|
|
40
|
+
- Re-injection sets `currentInboundByTopic[topic]` BEFORE routing so the eventual reply commits
|
|
41
|
+
the correct entry (same coupling the inbound gate uses) — closing the loop: re-run → reply →
|
|
42
|
+
commit → not re-run again.
|
|
43
|
+
- Telegram-only for v1 (the `--no-telegram` sessionManager path is a tracked refinement; the
|
|
44
|
+
block is guarded on `telegram` present).
|
|
45
|
+
- Cadenced re-scan is `unref()`'d (never holds the process open) and lease-gated (a standby's
|
|
46
|
+
timer no-ops).
|
|
47
|
+
|
|
48
|
+
## Rollback cost
|
|
49
|
+
- Low. Flag default-off ⇒ the whole block is skipped (messageLedger undefined). Revert = drop the
|
|
50
|
+
post-start block + the module. No persisted schema beyond the opt-in ledger SQLite.
|
|
51
|
+
|
|
52
|
+
## Tests
|
|
53
|
+
- `tests/unit/stuck-message-recovery.test.ts` (7): re-runs a stuck entry from stored input;
|
|
54
|
+
standby no-op; already-replied not re-run; in-flight (within window) not re-run; attempts-cap
|
|
55
|
+
gives up (no storm); no-input skipped; + boot wiring-integrity (server.ts calls it after
|
|
56
|
+
server.start(), gated on the ledger + coordinator lease).
|
|
57
|
+
|
|
58
|
+
## NOT in this increment (tracked, topic-13481)
|
|
59
|
+
- Behavioral boot-recovery e2e (needs a live Telegram adapter) — exercised by the live flag-flip
|
|
60
|
+
test-as-self, which is the gate before defaulting the flag on. Cross-machine marker (D-xmachine)
|
|
61
|
+
+ CONTINUATION resume (D3) remain.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Side-Effects Review — Cross-Machine Seamlessness: reply-marker propagation (D-xmachine)
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3a ("dual-medium reply_committed marker")
|
|
4
|
+
|
|
5
|
+
The cross-machine half of exactly-once. The same-machine dedup gate + no-loss recovery handle
|
|
6
|
+
one machine; this closes the window where, AFTER a handoff/failover, a provider redelivery of a
|
|
7
|
+
message the OLD holder already answered arrives at the NEW holder — whose ledger wouldn't know,
|
|
8
|
+
so it would re-answer. The holder now propagates a `reply_committed` marker to standby peers on
|
|
9
|
+
each commit; the standby applies it via `applyRemoteReplyMarker`, so the new holder's dedup gate
|
|
10
|
+
drops the redelivery. Still flag-gated dark (`multiMachine.exactlyOnceIngress`, default false).
|
|
11
|
+
|
|
12
|
+
## What changed
|
|
13
|
+
- `src/core/ReplyMarkerTransport.ts` — NEW. `broadcast(marker)` signs + POSTs
|
|
14
|
+
{dedupeKey, platform, replyIdempotencyKey, epoch, topic} to each standby peer's
|
|
15
|
+
`/api/message-marker` over the authenticated machine channel. NO encryption (the marker
|
|
16
|
+
carries no conversation content — authentication only). No peers → reachable no-op. Never
|
|
17
|
+
throws (best-effort; the dedup gate + provider redelivery are the backstop).
|
|
18
|
+
- `src/server/machineRoutes.ts` — `POST /api/message-marker` (machineAuth) + `onReplyMarker`
|
|
19
|
+
ctx callback. Validates the marker shape; 503 when no receiver (exactly-once off).
|
|
20
|
+
- `src/server/AgentServer.ts` — `replyMarkerTransport?` + `onReplyMarker?` options → routeCtx /
|
|
21
|
+
machineRoutes.
|
|
22
|
+
- `src/server/routes.ts` — RouteContext.replyMarkerTransport; the outbound commit at
|
|
23
|
+
`/telegram/reply/:topicId` reads the just-committed `replyIdempotencyKey` from the ledger and
|
|
24
|
+
fires `replyMarkerTransport.broadcast(...)` (fire-and-forget, after the local commit).
|
|
25
|
+
- `src/commands/server.ts` — constructs `ReplyMarkerTransport` (peer resolver = registry
|
|
26
|
+
machines with a lastKnownUrl) ONLY when `exactlyOnceIngress` + coordinator.enabled; wires
|
|
27
|
+
`onReplyMarker` → `messageLedger.applyRemoteReplyMarker`.
|
|
28
|
+
|
|
29
|
+
## Over-block / under-block
|
|
30
|
+
- The marker NEVER over-marks a fresh event: `applyRemoteReplyMarker` only sets reply_committed
|
|
31
|
+
on an entry not already acted-on, and the dedupeKey is the same provider-stable id both
|
|
32
|
+
machines compute — so a marker for event X can only dedup event X. The deterministic
|
|
33
|
+
`replyIdempotencyKey` (hash(dedupeKey+index)) is identical on both machines.
|
|
34
|
+
- UNDER-propagate (a lost marker): best-effort — if the marker doesn't reach the standby, the
|
|
35
|
+
provider's at-least-once redelivery + the standby's own ledger (once it catches up) still
|
|
36
|
+
dedup; the residual is the documented single-duplicate Two-Generals floor, not a new failure.
|
|
37
|
+
- Signed channel: `/api/message-marker` requires machineAuth (unsigned → 401, proven in the e2e);
|
|
38
|
+
the marker carries no secrets so no encryption is needed.
|
|
39
|
+
|
|
40
|
+
## Signal vs authority
|
|
41
|
+
- The transport propagates; the receiving ledger's `applyRemoteReplyMarker` is the authority on
|
|
42
|
+
its own state (only promotes a not-yet-acted entry). The dedup gate remains the single drop authority.
|
|
43
|
+
|
|
44
|
+
## Interactions
|
|
45
|
+
- Fires AFTER the local `commitInboundReply` (the local guarantee never depends on propagation).
|
|
46
|
+
- Skipped for proxy/system sends (same guard as the commit) — a proxy message isn't a user reply.
|
|
47
|
+
- Peer resolver mirrors the live-tail/handoff resolvers (registry machines with lastKnownUrl,
|
|
48
|
+
excludes self/revoked). Solo agent → no peers → no-op.
|
|
49
|
+
|
|
50
|
+
## Rollback cost
|
|
51
|
+
- Low. Flag default-off ⇒ transport not constructed, route 503s, no propagation. Revert = drop
|
|
52
|
+
the transport + route + the broadcast call. No persisted schema.
|
|
53
|
+
|
|
54
|
+
## Tests
|
|
55
|
+
- `tests/unit/reply-marker-transport.test.ts` (3): no-peers no-op; signs + POSTs to each peer;
|
|
56
|
+
resolves false (never throws) when all peers reject.
|
|
57
|
+
- `tests/e2e/reply-marker-cross-machine-e2e.test.ts` (2): a PEER-signed marker over real HTTP +
|
|
58
|
+
machine-auth applies to the receiver's ledger, then a redelivery of that message is DEDUPED
|
|
59
|
+
(already-replied) — the cross-machine guarantee end-to-end; + unsigned marker rejected (401).
|
|
60
|
+
|
|
61
|
+
## Completion status
|
|
62
|
+
- With D-dedup + D-noloss + D-xmachine, the exactly-once guarantee is COMPLETE in all dimensions
|
|
63
|
+
(no-dup same+cross machine, no-loss on crash), all DARK behind the flag. Remaining before the
|
|
64
|
+
flag flips: D3 (CONTINUATION resume verify) + the over-Telegram two-machine test-as-self (the
|
|
65
|
+
live acceptance bar, needs the operator's machines).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Side-Effects Review — Process Health tab ELI16 + structure pass
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `process-health-tab-eli16-pass`
|
|
4
|
+
**Date:** 2026-05-27
|
|
5
|
+
**Author:** echo
|
|
6
|
+
**Spec:** `docs/specs/PROCESS-HEALTH-DASHBOARD-TAB-SPEC.md` (v4, the original feature spec; this PR is a UX-refinement follow-up that does NOT change the spec's behavioral surface)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Eight rounds of user-driven copy + UX iteration on the Process Health dashboard tab (originally shipped in v1.3.27 via PR #435). Pure dashboard-only refinement — no `src/` / `scripts/` / `.husky/` / `skills/` changes — so this PR is outside the instar-dev gate's in-scope set. The reference implementation now backs a durable agent memory (`feedback_dashboard_copy_eli16.md`) that codifies the **Dashboard Standard** for future dashboard features.
|
|
11
|
+
|
|
12
|
+
**Files changed (UI):**
|
|
13
|
+
- `dashboard/process-health.js` (renderers): rewritten `CATEGORY_WORDS`, `ATTRIBUTION_LABELS`, `STAGES`, `INSIGHT_STATUS_WORDS`; new `TYPE_WORDS`, `STATUS_WORDS`, `CAUSE_CONFIDENCE_WORDS`, `statusDotClass`, `statusDot`, `labeledRow`. Restructured `renderCaptured` and `renderPatterns` around the fixed label vocabulary + `<details>` expandability + status color dots. `renderMaturation` reshaped per-stage as expandable `<details>` with plain-English descriptions. Dropped the redundant per-card framing line in v7.
|
|
14
|
+
- `dashboard/index.html`: added page-intro paragraph + per-section subtitles that distinguish each section and cross-reference adjacent ones; new CSS for `.ph-intro`, `.ph-h-sub`, `.ph-item`, `.ph-item-summary`, `.ph-item-text`, `.ph-item-body`, `.ph-item-row`, `.ph-label`, `.ph-value`, `.ph-status-dot` (+ 5 status-color variants), `.ph-stage-summary`, `.ph-stage-body`; chevron disclosure indicator on every `<details>` (opacity 0.8).
|
|
15
|
+
|
|
16
|
+
**Files changed (tests):**
|
|
17
|
+
- `tests/unit/process-health-render.test.ts`: assertions rewritten for new label vocabulary + new status copy + new framing absence + structural assertions on `<details>` + status-dot CSS classes (3 new structural tests added).
|
|
18
|
+
- `tests/integration/process-health-tab.test.ts`: assertions updated for new copy; new framing-absence check.
|
|
19
|
+
- `tests/e2e/process-health-tab-lifecycle.test.ts`: assertions updated for new copy.
|
|
20
|
+
|
|
21
|
+
**Files changed (release):**
|
|
22
|
+
- `upgrades/NEXT.md` (NEW for this PR, `bump: patch`).
|
|
23
|
+
|
|
24
|
+
## Decision-point inventory
|
|
25
|
+
|
|
26
|
+
- **Pure UX refinement vs. functional change?** Pure UX. No new endpoints, no new data fields, no new auth surfaces, no behavioral changes to the polling controller, no changes to the route layer, no changes to migration parity. Only the rendered text + DOM structure changes.
|
|
27
|
+
- **Drop the recommendation field entirely vs. keep + relabel?** Dropped. Justin explicitly directed "avoid language that implies user needs to act unless ABSOLUTELY essential". The recommendation field is upstream prose that is action-implying by content; relabeling chrome around it doesn't change that. Actionable insight delivery is the deferred §10 surface anyway; v1 of the tab is the calm informational surface.
|
|
28
|
+
- **Drop the per-card framing line vs. keep + rephrase?** Dropped in v7. Once "verify before acting" was rephrased to "Same kind of problem has come up more than once" in v5, it became the third echo of the section title + section subtitle. The no-action-authority signal that the framing was originally carrying is now structural (the rendered card has nothing actionable on it), not chrome.
|
|
29
|
+
- **Add status color dots vs. status text only?** Dots. Lets the user scan the collapsed list and read state at a glance without expanding each item. Colors are calm (amber/blue/green/gray) — never alarm reds, per the existing visual rules.
|
|
30
|
+
|
|
31
|
+
## Side-effects analysis
|
|
32
|
+
|
|
33
|
+
- **Behavioral:** None. The polling controller, route layer, and migration paths are unchanged. Only the rendered DOM differs.
|
|
34
|
+
- **Architectural:** None. Same file structure; the renderer module is the same self-contained ESM as before. No new build step, no new runtime dependency.
|
|
35
|
+
- **Security:** The safety contract (`sanitizeForDisplay`, textContent-only DOM writes, element ban, safeUrl) is unchanged. New label spans (`.ph-label`, `.ph-value`) use textContent only. The XSS negative fixture continues to fire zero live elements + no canary. The `detail.full` redaction is unchanged.
|
|
36
|
+
- **Migration parity:** Not applicable — no agent-installed-file changes (`templates.ts` / `PostUpdateMigrator` / shadow markers untouched).
|
|
37
|
+
- **Performance:** Marginal increase in DOM nodes per item (each captured item now has 4–5 `<div>`s of body content instead of 0–4 flat children). Diff-aware rendering still works (signature mechanism is structure-agnostic). No new fetches.
|
|
38
|
+
- **Reversibility:** Fully reversible — revert the commit and the previous render returns.
|
|
39
|
+
|
|
40
|
+
## Standard captured (the durable artifact this PR exists to back)
|
|
41
|
+
|
|
42
|
+
The 8-round iteration converged on a design that's now codified as the **Dashboard Standard** in `feedback_dashboard_copy_eli16.md` (agent memory): 7 rules (ELI16 copy + codename maps + grounding intro + self-contained section titles with subtitles + expandable items + fixed label vocabulary + no action-implying language), 8 default substitutions, 4 calm status-color tokens, and the show-don't-tell iteration pattern. The Process Health tab is now the reference implementation.
|
|
43
|
+
|
|
44
|
+
## Evidence pointers
|
|
45
|
+
|
|
46
|
+
- All 46 tests green: 30 unit + 12 integration + 4 e2e — includes structural assertions for the unified label order, status-dot CSS classes, and that the dropped framing line is genuinely absent.
|
|
47
|
+
- Safety verified end-to-end: XSS negative fixture still fires zero live elements + no canary; `detail.full` redaction unchanged.
|
|
48
|
+
- Typecheck: `tsc --noEmit` clean.
|
|
49
|
+
- Live-rendered both views (collapsed + expanded) to Chrome headless across 8 iterations and sent screenshots to the user for thumbs-up before each commit. Justin approved v8 with "this is good enough, let's go with it; save this as the standard for future dashboard features".
|