Skip to content

Approval gate glyph: a state machine for the gate, zero-dep (#544) - #545

Merged
richard-devbot merged 3 commits into
mainfrom
claude/gate-glyph-499
Aug 2, 2026
Merged

Approval gate glyph: a state machine for the gate, zero-dep (#544)#545
richard-devbot merged 3 commits into
mainfrom
claude/gate-glyph-499

Conversation

@richard-devbot

@richard-devbot richard-devbot commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Closes #544. Last child of epic #499the epic completes at 8/8 with this.

Taken on Richardson's call: "if it brings advantages for our ui/ux then lets develop it."

Built zero-dep, and why that is not a substitution

The advantage being asked for is the glyph — a gate whose states are a machine, so the approval lifecycle reads as a mechanism rather than a colour change. Every objection I raised against the Rive child on #499 attaches to the runtime, not to that visual:

objection attaches to
unreviewable .riv binary the asset pipeline
a state machine holding its own copy of governance state the runtime
a canvas nothing in our suite can assert against the runtime

Authoring the same state machine in SVG + CSS keeps the advantage and loses all three. If Rive-the-tool is wanted for its own sake, the Studio is where those objections don't bite — that stays open as a separate call.

What it does

The shackle carries the meaning:

state shackle mark
pending shut
approved opens check draws
consumed shuts again check → spent bar
rejected shut cross draws

The consumed row is why this was worth building. A spent one-shot override is back to blocking, and nothing on the card said so as a mechanism — the copy explains it in a sentence, and after #541 the colour merely faded. A shackle that visibly re-closes states it in the way a reader absorbs without reading.

Correctness properties, not styling choices

Every part exists in every state, undrawn or transparent rather than absent. A part rendered only for its own state would be a new node at the flip, and a new node cannot transition — so the change would never animate on the one occasion it means something. That is the #541 Proof Rail lesson applied.

transition, never @keyframes. A card that arrives approved is still; only a card that flips animates. Motion never claims an event nobody watched happen. Both directions are asserted.

Transform, opacity and stroke-dash only — a browser test measures that the card neither resizes nor moves when the gate opens, because this list repaints and a shift would move the page under the reader's cursor. A unit test enumerates the transitioned properties and rejects anything outside that set.

aria-hidden — the pill beside it already states the status in words. A second announcement would be noise, not access.

Verification

  • 1973/1973 core, 59/59 browser (52 → 59), lint 0 errors, typecheck clean, validate 196 agents, security baseline OK, git diff --check clean.
  • Two unit tests carry vacuity guards: both iterate parts and continue when a rule is missing, so without the guards they would pass on an unstyled glyph. Caught before implementing — I ran the suite and saw them pass green against nothing.
  • Browser: per-state geometry read on fresh nodes (re-classing one node and reading immediately samples mid-transition — the Motion: Proof Rail transitions, approval gate lifecycle, scroll reveal (#499 remaining zero-dep children) #541 lesson), the flip asserted on a provably surviving node, arrival asserted still, reduced motion, card-geometry stability, axe-clean, zero page errors.
  • Screenshots taken and read, per the rule this wave keeps re-learning.

Not built

The Rive runtime and any .riv asset. Empty-state illustrations — declined on #499: our empty states are deliberately informative ("No telemetry yet", "No runs in this scope") and an illustration adds warmth but no information.

Summary by CodeRabbit

  • New Features

    • Added visual approval gate glyphs to approval cards.
    • Glyphs clearly indicate pending, approved, rejected, and consumed states.
    • State changes include smooth visual transitions and reduced-motion support.
  • Accessibility

    • Approval glyphs are decorative and do not interfere with assistive technologies.
    • Visual updates maintain stable card layouts without disruptive movement.

Last child of epic #499, built zero-dep instead of on the Rive runtime.

The advantage asked for is the GLYPH — a gate whose states are a machine, so
the approval lifecycle reads as a mechanism. The objections raised against
the Rive child all attach to the RUNTIME rather than that visual: an
unreviewable .riv binary, a state machine holding its own copy of governance
state, and a canvas nothing in the suite can assert against. Authoring the
same machine in SVG + CSS keeps the advantage and loses all three.

The shackle carries the meaning:

  pending   shut          the gate is closed, awaiting a human
  approved  OPENS + check passage granted
  consumed  SHUTS AGAIN   the one attempt was spent; blocking resumed
  rejected  shut + cross  refused

The consumed row is why this was worth building. A spent one-shot override
is back to blocking, and nothing on the card said so as a mechanism — the
copy explained it in a sentence and the colour just faded. A shackle that
visibly re-closes states it in the way a reader absorbs without reading.

Every part is present in every state, undrawn or transparent rather than
absent: a part rendered only for its own state would be a new node at the
flip, and a new node cannot transition (the #541 Proof Rail lesson). And
transition rather than animation means a card that ARRIVES approved is
still, while only a real flip animates — motion never claims an event that
did not happen.

Transform, opacity and stroke-dash only; a browser test asserts the card
neither resizes nor moves when the gate opens. aria-hidden, because the pill
beside it already states the status in words.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@strix-security

strix-security Bot commented Aug 1, 2026

Copy link
Copy Markdown

Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@richard-devbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e3e52e1-13b2-4f1a-b4c8-d4bf8a4752b4

📥 Commits

Reviewing files that changed from the base of the PR and between bf6d1e4 and 3f61431.

📒 Files selected for processing (1)
  • tests/browser/helpers/harness.js
📝 Walkthrough

Walkthrough

Approval cards now include an accessibility-hidden SVG gate glyph. CSS maps pending, approved, rejected, and consumed states to distinct gate visuals and transitions. Unit-style and browser tests validate rendering, state changes, motion behavior, geometry, accessibility, and live-update test control.

Changes

Approval gate glyph

Layer / File(s) Summary
Glyph rendering and state styling
src/observability/dashboard/ui/pages/approvals.js, src/observability/dashboard/ui/styles.js
Approval cards render persistent SVG gate parts. CSS styles pending, approved, rejected, and consumed states with shackle, mark, color, opacity, and transform changes.
Glyph behavior validation
tests/hub-gate-glyph-544.test.js, tests/browser/dashboard-544-gate-glyph.test.js
Tests cover state rendering, transitions, reduced motion, geometry, accessibility, and card integration.
Live update test control
tests/browser/helpers/harness.js, tests/browser/dashboard-516-micro.test.js, tests/browser/dashboard-541-motion.test.js
The browser harness adds holdLiveTicks(page). Existing tests use it instead of local snapshot-handler overrides.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ApprovalPage
  participant ApprovalCard
  participant CSSStateRules
  ApprovalPage->>ApprovalCard: render approval status and gate glyph
  ApprovalCard->>CSSStateRules: apply approval state class
  CSSStateRules-->>ApprovalCard: update shackle, marks, colors, and spent bar
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: richardsongunde

Poem

A rabbit sees the gate glow bright,
The shackle opens, marks take flight.
A spent gate closes once again,
Crosses guard the blocked domain.
SVG hops through every state.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: a zero-dependency approval gate glyph implemented as a state machine.
Linked Issues check ✅ Passed The changes implement the linked issue requirements for SVG/CSS states, persistent parts, transitions, reduced motion, accessibility, and browser coverage.
Out of Scope Changes check ✅ Passed The test harness and browser test updates directly support reliable verification of the approval gate glyph and remain within scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/gate-glyph-499

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Approvals: add SVG gate glyph state machine for approval lifecycle

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add zero-dependency SVG+CSS “gate” glyph that animates approval state transitions.
• Render glyph beside the existing status pill to better convey consumed/one-shot overrides.
• Add unit + real-browser tests to enforce transition-only, no-reflow, and a11y invariants.
Diagram

graph TD
  A["Approvals page (approvals.js)"] --> B["approvalHtml()"] --> C["gateGlyphHtml(status)"] --> D["Inline SVG parts (shackle/check/cross/bar)"] --> E["CSS state rules (styles.js)"] --> F["Tests (unit + browser)"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use Rive runtime/asset pipeline
  • ➕ Designer-friendly authoring environment and richer motion tooling
  • ➕ Potential reuse of existing Rive animation workflows
  • ➖ Binary assets reduce reviewability and diff quality
  • ➖ Runtime introduces a second state machine that can drift from app governance state
  • ➖ Canvas-based rendering is harder to assert in the existing test suite
2. Static icon set per status (no transitions)
  • ➕ Simplest implementation and lowest CSS complexity
  • ➕ Minimal risk of unexpected motion
  • ➖ Loses the key UX benefit: the lifecycle reads as a mechanism (especially consumed re-closing)
  • ➖ Harder to communicate ‘spent override’ without motion semantics
3. SMIL or @keyframes-based SVG animation
  • ➕ Can express complex drawings compactly
  • ➕ Potentially less per-state CSS
  • ➖ Violates the PR’s contract: arrivals would animate and imply unseen events
  • ➖ More brittle across browsers and harder to constrain to non-layout properties

Recommendation: The chosen SVG+CSS transition approach is the best fit for the stated constraints (reviewability, zero-dep runtime, and assertable behavior). The tests meaningfully lock in the intended invariants (node persistence, transition-only motion, no layout shift, decorative a11y), making this preferable to Rive or keyframe-driven animation for this surface.

Files changed (4) +394 / -1

Enhancement (2) +72 / -1
approvals.jsRender approval state glyph next to the status pill +27/-1

Render approval state glyph next to the status pill

• Introduces gateGlyphHtml(status) that emits a persistent-part SVG glyph (shackle/body/check/cross/spent bar) and marks it aria-hidden. Updates approval card rendering to include the glyph beside the existing textual pill within a new .approval-state wrapper.

src/observability/dashboard/ui/pages/approvals.js

styles.jsAdd CSS-driven gate glyph state machine and transitions +45/-0

Add CSS-driven gate glyph state machine and transitions

• Adds styling for the glyph container and SVG parts, using only transitions (no keyframes) and relying on stroke-dashoffset/opacity/transform changes. Defines per-status rules so approved opens the shackle and draws the check, rejected draws the cross, and consumed re-closes the shackle and swaps the check for a spent bar without affecting layout geometry.

src/observability/dashboard/ui/styles.js

Tests (2) +322 / -0
dashboard-544-gate-glyph.test.jsBrowser tests for glyph behavior, motion rules, and geometry stability +179/-0

Browser tests for glyph behavior, motion rules, and geometry stability

• Adds real-browser coverage to verify the shackle open/close semantics across states, mark visibility, and that transitions occur on surviving nodes (flip) but do not replay on arrival. Also asserts reduced-motion behavior, zero geometry shift on approval, and runs axe to ensure no serious/critical a11y regressions (when axe-core is available).

tests/browser/dashboard-544-gate-glyph.test.js

hub-gate-glyph-544.test.jsUnit tests for glyph DOM invariants and allowed transitioned properties +143/-0

Unit tests for glyph DOM invariants and allowed transitioned properties

• Adds unit tests that validate every SVG part exists in every state, the glyph is decorative (aria-hidden), and that the card renders glyph + pill together. Parses styles to assert the shackle opens on approved and re-closes on consumed, each state reveals only its own mark, no @keyframes are used, and only a constrained set of properties are transitioned to avoid reflow risk.

tests/hub-gate-glyph-544.test.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/browser/dashboard-544-gate-glyph.test.js`:
- Around line 106-116: Update the setup around the flip evaluation in
dashboard-544-gate-glyph.test.js to stop overriding window.handleGlobalSnapshot,
since the dashboard uses a module-local handler. Use an interception mechanism
shared with the dashboard client, or invoke the client’s render path directly
during setup, so live snapshot rendering is reliably paused before mutating the
approval card.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 814d3e42-123b-42d5-88fd-b4b43f93a1cb

📥 Commits

Reviewing files that changed from the base of the PR and between 905e758 and e98423c.

📒 Files selected for processing (4)
  • src/observability/dashboard/ui/pages/approvals.js
  • src/observability/dashboard/ui/styles.js
  • tests/browser/dashboard-544-gate-glyph.test.js
  • tests/hub-gate-glyph-544.test.js

Comment thread tests/browser/dashboard-544-gate-glyph.test.js
@qodo-code-review

qodo-code-review Bot commented Aug 1, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (2)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 17 invoked
  code-review-pr
  claude-api
  documentation-writing
  pptx
  docx
  performance-monitoring
  security-compliance
  cso
  plan-eng-review
  design-review
  prompt-engineering
  mcp-builder
  qa-testing
  code-patterns
  xlsx
  security-owasp
  testing-qa

Grey Divider


Remediation recommended

1. Unguarded DOM dereferences ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The new Playwright suite dereferences querySelector() results inside page.evaluate() (e.g.,
cloneNode/getComputedStyle) without checking for null, so selector/fixture regressions will fail as
TypeErrors instead of actionable assertions. This makes CI failures harder to diagnose and slower to
fix.
Code

tests/browser/dashboard-544-gate-glyph.test.js[R52-60]

+  const card = document.querySelector('#page-approvals .approval-card');
+  const out = {};
+  for (const state of wanted) {
+    const clone = card.cloneNode(true);
+    clone.className = 'approval-card ' + state;
+    card.parentNode.appendChild(clone);
+    const shackle = clone.querySelector('.gate-shackle');
+    const style = getComputedStyle(shackle);
+    out[state] = {
Relevance

●●● Strong

Null-guard/assertion suggestions in page.evaluate were definitely accepted to avoid TypeErrors in
browser tests.

PR-#534
PR-#528

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new test code calls methods on the result of document.querySelector(...) without checking it
exists, so any DOM/fixture mismatch becomes an uninformative runtime exception inside the browser
context. This matches a previously accepted bug pattern in the same test area where explicit
assertions were recommended to avoid TypeErrors.

tests/browser/dashboard-544-gate-glyph.test.js[51-70]
tests/browser/dashboard-544-gate-glyph.test.js[103-116]
PR-#534

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new browser tests in `dashboard-544-gate-glyph.test.js` directly dereference DOM nodes returned by `querySelector()` inside `page.evaluate()`. If the fixture changes or the DOM structure regresses, the test will throw an unhelpful `TypeError` (e.g., calling `cloneNode` on null) rather than failing with a clear assertion about which element is missing.

### Issue Context
This repo has had prior review guidance to harden browser tests against null DOM lookups so failures are actionable (avoid TypeErrors inside `page.evaluate`).

### Fix Focus Areas
- tests/browser/dashboard-544-gate-glyph.test.js[51-70]
- tests/browser/dashboard-544-gate-glyph.test.js[103-116]
- tests/browser/dashboard-544-gate-glyph.test.js[152-164]

### Suggested change
Inside each `page.evaluate()`:
- Assert required nodes exist before dereferencing (e.g., `if (!card) throw new Error('...')`).
- Optionally add a small helper like `must(sel)` that throws a descriptive error when a selector is missing, and use it for `.approval-card`, `.gate-check`, `.gate-shackle`, `.gate-glyph`, etc.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. gateGlyphHtml not verb-noun 📜 Skill insight ⚙ Maintainability
Description
The newly added function gateGlyphHtml does not follow a verb-noun naming pattern as required.
This reduces consistency and readability across the JS codebase.
Code

src/observability/dashboard/ui/pages/approvals.js[R67-76]

+function gateGlyphHtml(status) {
+  return '<span class="gate-glyph ' + esc(status || 'pending') + '" aria-hidden="true">' +
+    '<svg viewBox="0 0 32 32" focusable="false">' +
+      '<path class="gate-shackle" d="M10 14v-3a6 6 0 0 1 12 0v3" />' +
+      '<rect class="gate-body" x="7" y="14" width="18" height="14" rx="3" />' +
+      '<path class="gate-check" pathLength="1" d="M11.5 21.2l3 3 6-6" />' +
+      '<path class="gate-cross" pathLength="1" d="M12 17.5l8 8M20 17.5l-8 8" />' +
+      '<path class="gate-bar" d="M11.5 21h9" />' +
+    '</svg></span>';
+}
Relevance

● Weak

Verb-noun rename suggestions were definitely rejected (helper renames), showing naming pattern isn’t
enforced.

PR-#534

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1399694 requires function names to follow a verb-noun naming pattern. The added
function is named gateGlyphHtml, which begins with a noun instead of a verb.

src/observability/dashboard/ui/pages/approvals.js[67-76]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new function name `gateGlyphHtml` does not start with a verb (verb-noun pattern required).

## Issue Context
This function generates HTML markup for the approval gate glyph; a verb-led name like `renderGateGlyphHtml` or `getGateGlyphHtml` would satisfy the naming convention.

## Fix Focus Areas
- src/observability/dashboard/ui/pages/approvals.js[67-76]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Gate glyph tests lack screenshots 📘 Rule violation ▣ Testability
Description
The new browser UI tests verify geometry and computed styles but do not perform any screenshot-based
visual comparison, so visual regressions (misalignment, clipping, stroke rendering differences)
could slip through. This violates the requirement that UI tests include screenshot-based visual
verification for layout/visual correctness.
Code

tests/browser/dashboard-544-gate-glyph.test.js[R152-167]

+glyphTest('the glyph never changes the card geometry (#544)', async ({ page, server }) => {
+  await openApprovals(page, server);
+  const box = await page.evaluate(() => {
+    const card = document.querySelector('#page-approvals .approval-card');
+    const before = card.getBoundingClientRect();
+    card.className = 'approval-card approved';
+    const after = card.getBoundingClientRect();
+    const glyph = card.querySelector('.gate-glyph').getBoundingClientRect();
+    return {
+      shifted: Math.abs(after.height - before.height) + Math.abs(after.top - before.top),
+      glyphW: Math.round(glyph.width), glyphH: Math.round(glyph.height),
+    };
+  });
+  assert.equal(box.shifted, 0, 'the card does not resize or move when the gate opens');
+  assert.ok(box.glyphW >= 20 && box.glyphH >= 20, `the glyph is large enough to read — ${box.glyphW}x${box.glyphH}`);
+});
Relevance

● Weak

Screenshot-based visual verification requests for UI/browser motion tests were definitely rejected.

PR-#542

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2448668 requires screenshot-based visual verification for UI/layout correctness
tests. The added test the glyph never changes the card geometry (#544) performs bounding-box
assertions but contains no screenshot capture or comparison step.

Rule 2448668: UI tests must include screenshot-based visual verification
tests/browser/dashboard-544-gate-glyph.test.js[152-167]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New Playwright browser tests for the approvals gate glyph assert layout/visual properties without any screenshot-based comparison, which violates the requirement for screenshot-based visual verification in UI tests.

## Issue Context
`tests/browser/dashboard-544-gate-glyph.test.js` includes layout/visual checks (e.g., bounding boxes) but never captures a screenshot and compares it to a committed baseline.

## Fix Focus Areas
- tests/browser/dashboard-544-gate-glyph.test.js[152-167]

## Implementation notes
- Capture a deterministic screenshot of the relevant region (e.g., the first `.approval-card` or `#page-approvals`) after the page is settled.
- Add an automated comparison against a committed baseline (e.g., by hashing the screenshot buffer with SHA-256 and asserting it equals an `EXPECTED_*_SCREENSHOT_HASH`, or by introducing a lightweight pixel-diff/snapshot mechanism used by the repo).
- Ensure the screenshot is taken in a stable state (fonts loaded, fixed viewport) to reduce flakiness.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Glyph transitions non-allowed properties 📜 Skill insight ➹ Performance
Description
The new gate glyph transitions stroke, fill, and stroke-dashoffset, but the compliance rule
allows animations/transitions only for transform and opacity. This can create performance and
rendering risks beyond the allowed animation surface.
Code

src/observability/dashboard/ui/styles.js[R1176-1188]

+  transition: transform var(--motion-base) var(--motion-ease-emphatic), stroke var(--motion-base) var(--motion-ease);
+}
+.gate-body { fill: var(--panel); stroke: var(--muted); stroke-width: 2.2; transition: stroke var(--motion-base) var(--motion-ease), fill var(--motion-base) var(--motion-ease); }
+.gate-check {
+  fill: none; stroke: var(--green); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round;
+  stroke-dasharray: 1; stroke-dashoffset: 1; opacity: 0;
+  transition: stroke-dashoffset var(--motion-slow) var(--motion-ease-emphatic), opacity var(--motion-fast) var(--motion-ease);
+}
+.gate-cross {
+  fill: none; stroke: var(--red); stroke-width: 2.6; stroke-linecap: round;
+  stroke-dasharray: 1; stroke-dashoffset: 1; opacity: 0;
+  transition: stroke-dashoffset var(--motion-base) var(--motion-ease-emphatic), opacity var(--motion-fast) var(--motion-ease);
+}
Relevance

● Weak

“Only transform/opacity transitions” suggestions were definitely rejected; team keeps
stroke/fill/dash transitions.

PR-#542
PR-#528
PR-#539

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1399699 restricts animated/transitioned properties to transform and opacity.
The new glyph CSS explicitly transitions stroke, fill, and stroke-dashoffset in
src/observability/dashboard/ui/styles.js.

src/observability/dashboard/ui/styles.js[1173-1188]
Skill: design-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The gate glyph CSS transitions properties other than `transform` and `opacity` (notably `stroke`, `fill`, and `stroke-dashoffset`), violating the rule that only `transform` and `opacity` may be animated.

## Issue Context
Current rules animate the check/cross drawing via `stroke-dashoffset` and also transition `stroke`/`fill` colors. Compliance requires restricting the animated surface to `transform` and `opacity` only.

## Fix Focus Areas
- src/observability/dashboard/ui/styles.js[1173-1188]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread tests/browser/dashboard-544-gate-glyph.test.js Outdated
CodeRabbit asked whether `window.handleGlobalSnapshot = ...` actually holds
live ticks. Verified in a real browser rather than reasoned about: the client
bundle is a classic <script>, so its top-level function declarations become
window properties, and assigning over one rebinds the identifier the WS
handler calls. Probe returned {onWindow: true, rebound: true} — the override
works and the comment was accurate.

But three suites depended on that non-obvious fact silently. A switch to
<script type="module"> would scope the declaration and turn every one of
those overrides into a no-op, with the tests still passing for the wrong
reason. Extracted `holdLiveTicks(page)` into the shared harness, which
performs the override AND asserts the rebind took effect, and routed all
three suites through it — the #515 lesson that copies drift, applied to a
line whose correctness nobody could see.

Qodo: unguarded querySelector dereferences inside page.evaluate turned any
selector or fixture regression into a TypeError instead of a readable
failure. Named lookups now throw with the selector and the likely cause.

1973/1973 core, 59/59 browser, lint 0 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@richard-devbot

Copy link
Copy Markdown
Owner Author

Both findings addressed — and CodeRabbit's turned out to be worth more than its severity suggested

CodeRabbit: does window.handleGlobalSnapshot = ... actually hold live ticks?

Yes — verified in a real browser rather than reasoned about. The client bundle is served as a classic <script>, so its top-level function declarations become window properties, and assigning over one rebinds the identifier the WS handler calls:

{ onWindow: true, rebound: true }

So the override works and the comment was accurate.

But the question exposed something worth fixing anyway. Three suites (#516, #541, #544) each carried that bare line, depending on a non-obvious fact nobody could see from the call site. If the bundle ever became <script type="module">, the declaration would be module-scoped, every one of those overrides would silently become a no-op — and the tests would keep passing, for the wrong reason, while live ticks quietly overwrote the state they injected.

Extracted holdLiveTicks(page) into the shared harness: it performs the override and asserts the rebind took effect, with an error message that explains the failure. All three suites now route through it. That is the #515 lesson — copies drift — applied to a line whose correctness was invisible.

Good catch. The finding was a question, and answering it properly was more valuable than the "minor" label implied.

Qodo: unguarded DOM dereferences

Correct. A selector or fixture regression surfaced as TypeError: Cannot read properties of null rather than something a reader could act on. Named lookups now throw with the selector and the likely cause:

expected .gate-shackle to exist — the glyph markup or the fixture changed

Applied across the suite's page.evaluate blocks.

Verification

1973/1973 core, 59/59 browser, lint 0 errors, typecheck clean, security baseline OK, whitespace clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/browser/helpers/harness.js`:
- Around line 190-193: Update the handler check in the surrounding harness
helper to store the replacement function in a local variable, assign that same
function to window.handleGlobalSnapshot, and compare both
window.handleGlobalSnapshot and the bare handleGlobalSnapshot result from new
Function against the stored replacement. This must verify that rebinding
actually takes effect rather than accepting an unchanged handler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b05e553-1cc9-4650-a255-acb96e505224

📥 Commits

Reviewing files that changed from the base of the PR and between e98423c and bf6d1e4.

📒 Files selected for processing (4)
  • tests/browser/dashboard-516-micro.test.js
  • tests/browser/dashboard-541-motion.test.js
  • tests/browser/dashboard-544-gate-glyph.test.js
  • tests/browser/helpers/harness.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/browser/dashboard-544-gate-glyph.test.js

Comment thread tests/browser/helpers/harness.js Outdated
#544)

CodeRabbit, and it was right at Major. My check read window.handleGlobalSnapshot
and the bare identifier and compared them TO EACH OTHER. If the assignment
silently failed — a non-writable or getter-only property — both reads return
the original, match, and the check passes while the override does nothing.

Both reads are now compared against the replacement function itself, and the
failure message names which side did not take.

Proved rather than asserted. On a global defined non-writable:

  old check => true   (would have passed, blind)
  new check => correctly failed

A guard that cannot fail is not a guard, and this one is meant to protect
three suites from silently asserting on state a live tick already replaced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@richard-devbot

Copy link
Copy Markdown
Owner Author

Right, and at the correct severity — fixed

My check read window.handleGlobalSnapshot and the bare identifier and compared them to each other. If the assignment silently failed, both reads return the original, they match, and the guard passes while the override does nothing. A guard that cannot fail is not a guard — and this one exists to stop three suites from asserting on state a live tick has already replaced.

Both reads now compare against the replacement function itself, and the failure message names which side did not take.

Proved rather than asserted. Against a global defined non-writable — the exact hole you described:

old check => true    (would have passed, blind)
new check => correctly failed

Worth noting for anyone reading later: the property on the real page is a top-level function declaration, so it is non-configurable and cannot be corrupted in place — I had to reproduce the condition on a blank page to demonstrate it. That is also why the hole was invisible: it cannot occur today, only after a packaging change, which is precisely when a silent no-op would do the most damage.

59/59 browser, lint 0 errors.

@richard-devbot
richard-devbot merged commit 4872ce5 into main Aug 2, 2026
10 checks passed
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.

Approval gate glyph: a state machine for the gate, zero-dep (#499 last child)

2 participants