Skip to content

feat(metrics): a forced run can override more than one stop - #171

Merged
thedavidmeister merged 1 commit into
masterfrom
render-forced-array-shape
Aug 9, 2026
Merged

thedavidmeister merged 1 commit into
masterfrom
render-forced-array-shape

Conversation

@thedavidmeister

Copy link
Copy Markdown
Contributor

Refs rainlanguage/issue-pr-cron#245

Follows #169 (merged), and pairs with rainlanguage/issue-pr-cron#248, which changes the field this page reads.

Why the shape changed

issue-pr-cron#245 was re-ruled by the user after the first observation run printed SKIP: DISABLED flag present and did nothing:

"force needs to force."

--force now overrides policy stops — the usage-gate PAUSE and the DISABLED kill switch — while correctness stops (the flock, a gate config refusal) still never yield. So a single forced run can walk past two stops, and forced: "usage-gate" cannot say that. Both fields are now parallel lists: index i of forced is a stop's kind and index i of forceReason is that stop's own line, verbatim.

What changed here

  • The tooltip names every stop it overrode, each with its own line, instead of one.
  • The caption and the aria description stop naming the usage gate. The gate is no longer the only stop a force can walk past, so naming it would mislabel a run that overrode the other one.
  • "forced": [] gets its own words. That is the ordinary case — a human started a run to watch it and nothing was in the way — and the row still rings and still counts. "The schedule did not start this" is the fact the marker carries; the stops are detail on top of it. Rendering that run as an ordinary tick is the corruption the whole field exists to prevent.
  • forcedKinds / forcedReasons coerce the shape once, so a bare string (the row shape feat(metrics): draw a forced run apart from a paced tick #169 shipped against), a non-list, a null element, or a non-string kind all render as something a reader can act on rather than as undefined. The emitter can also grow a third stop kind without this page learning its name.

The reasons are deliberately not zipped into the kind line by index. The emitter refuses a mismatched pair, so zipping here would be a second place believing the two lists are the same length — and the failure of that belief would silently drop a reason rather than showing it.

Screenshots

nix run .#screenshot (pinned chromium), metrics.html, 1300×1500, each read before posting. nix run .#roh-scan was NOT run — pure rendering change. Posted as a comment below.

The live file still carries zero forced rows — no forced run has ever been recorded — so the live state is again no visible change, demonstrated rather than asserted: master and this branch render byte-identical against it (cmp, 111381 bytes each). The fixture is the live file plus two rows, chosen as the two cases the new shape exists for: one mid-series run forced past both stops (and errored, so the ring is seen composing with the red dot), and one appended run with "forced": [] — forced, nothing overridden.

QA

  • Discriminating tests: metrics forced: the tooltip names EVERY stop, each with its own line (renamed from …carries the gate's line verbatim, now looping both lists), metrics forced: a run that overrode NOTHING still says a human started it (new — "forced": [] is forced, is ringed, is counted, and says so without claiming it walked past anything), metrics forced: a legacy single-value row still renders (new — the row shape feat(metrics): draw a forced run apart from a paced tick #169 shipped against, coerced rather than refused), metrics forced: a stop kind that is not a string still names itself (widened to [true, null]), metrics forced caption: counts them, and says they still count (now also asserts the caption does not claim one particular stop), plus the unchanged the gate field marks a forced run and nothing else, a forced run is a run — it charts, and it counts, the chart rings the forced run, and only it, the ring is ADDED to an errored run, a forced run with no plotted value gets no ring, the end label clears the ring on a forced last run, a file with no forced runs renders exactly as before, and both hostile input tests. Fails on base: with master's site/metrics.html restored under this branch's tests, the suite reports 0 passed | 1 failed — it cannot even load, because bind("metrics.html", "forcedKinds", …) is a top-level binding and master's page has no such function. That is the structural form of "fails on base": every assertion below it is unreachable against the old renderer. 261 pass on this branch.
  • Mutations applied: 8 over the changed lines, zero survivors, each run as a real mutation of the shipped page:
    • forcedKinds wraps the list instead of spreading it → metrics forced: a run that overrode NOTHING still says a human started it
    • forcedKinds/forcedReasons drop the non-list arm → metrics forced: a legacy single-value row still renders, both hostile input forced tests
    • the empty-override branch claims it walked past something → a run that overrode NOTHING…
    • the empty-override branch says nothing at all → a run that overrode NOTHING…
    • the null/empty element filter dropped → metrics forced: a stop kind that is not a string still names itself
    • only the FIRST reason rendered → metrics forced: the tooltip names EVERY stop, each with its own line
    • the caption names the usage gate again → metrics forced caption: counts them, and says they still count
    • a reason rendered through innerHTML instead of a text node → dashboard pages contain no markup sink at all, both hostile input forced tests, a legacy single-value row still renders, the tooltip names EVERY stop
  • Oracle: the user's ruling as quoted, plus the emitter's contract read off fix(runners): --force overrides policy stops, not correctness stops issue-pr-cron#248 final_record and ForceStamp — parallel arrays, absent (not null) on scheduled rows, empty as a real state, never beside skipped. The legacy-string case is read off what feat(metrics): draw a forced run apart from a paced tick #169 actually shipped, not guessed. Visual claims are checked against actual screenshots, read, not assumed.
  • Category check: the change asks that (a) every overridden stop be reachable, (b) the empty case still read as forced, (c) rows from the previous shape not break, (d) rows with no fields render as before. Covered a–d. Refs rather than Closes: the issue is in another repo and fix(runners): --force overrides policy stops, not correctness stops issue-pr-cron#248 is the PR that carries it.

issue-pr-cron#245 was re-ruled: `--force` now overrides POLICY stops — the
usage-gate PAUSE and the DISABLED kill switch — while correctness stops (the
flock, a gate config refusal) still never yield. One forced run can therefore
walk past two stops, so `forced`/`forceReason` became parallel LISTS: index i
is a stop's kind and that stop's own line, verbatim.

The tooltip names every stop it overrode, each with its line, instead of one.
The caption and the aria description stop naming the usage gate, because the
gate is no longer the only stop a force can walk past and naming one would
mislabel a run that overrode the other.

`"forced": []` is the ordinary case and is rendered as its own words: a human
started the run and nothing was in the way. That row still rings and still
counts — "the schedule did not start this" is the fact the marker carries,
and the stops are detail on top of it.

`forcedKinds`/`forcedReasons` coerce the shape once, so a bare string (the
row shape #169 shipped against), a non-list, a null element or a non-string
kind all render as something a reader can act on rather than as `undefined`.

Rows without the fields render exactly as before: byte-identical PNGs against
the live file, which still carries no forced row at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

Next review available in: 2 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9f4a6b3e-ce8c-45e4-80c7-167d6cf5c466

📥 Commits

Reviewing files that changed from the base of the PR and between 5606d79 and ef3a813.

📒 Files selected for processing (2)
  • site/metrics.html
  • test/dashboard.test.js

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.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Screenshots

nix run .#screenshot (pinned chromium), metrics.html, 1300×1500. Each one read before posting. nix run .#roh-scan was NOT run — pure rendering change.

Live data: still no visible change

No forced run has ever been recorded, so the live metrics/runs.jsonl carries zero rows with the field — the same honest state as #169. Master and this branch render byte-identical against it: cmp reports no difference, 111381 bytes each.

live data, master

live data, this branch — byte-identical

Fixture: the two cases the new shape exists for

The live file plus two rows, and nothing else changed:

  1. a mid-series run forced past both policy stops — "forced": ["disabled","usage-gate"] with each stop's own line — on a run that also errored, so the ring is seen composing with the red dot rather than replacing it;
  2. an appended run with "forced": [] — the ordinary forced run: a human started it to watch it and nothing was in the way.

What the shot shows:

  • both are ringed, including the empty-override one — being forced is what earns the ring, not having overridden something
  • runs recorded 90 → 91: a forced run counts, which is the whole distinction from a skipped tick
  • the caption now reads "2 runs were started by hand with --force — ringed points; hover one for which stops it overrode, each with that stop's own line." It no longer names the usage gate, because the gate is no longer the only stop a force can walk past
  • 30.6m still sits clear of the last ring (the label-clearance fix from feat(metrics): draw a forced run apart from a paced tick #169 still holds under the new shape)

fixture: two forced runs — both stops, and none

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Reviewed ef3a813: pass

Verified on the head blob.

Backward compatibility is by construction, not by branch. const list = Array.isArray(raw) ? raw : raw == null ? [] : [raw]; wraps a bare string from the pre-array emitter into a single-element list, so a legacy row renders identically. That is not hypothetical: rainlanguage/issue-pr-cron#246 and rainlanguage/issue-pr-cron#248 merged 44 minutes apart, so any row written between them carries the string form. Pinned by a legacy single-value row still renders.

forced: [] still counts as forced. isForced tests != null and an empty array is non-null, so a run that overrode nothing keeps its ring and reads "forced — nothing to override". That is precisely the case 248's --forced-run flag exists for — a human-started run that met no stop would otherwise be drawn as a paced tick. Pinned by a run that overrode NOTHING still says a human started it.

Every field stays untrusted: kinds and reasons are filtered, String()-coerced and appended as text nodes through the same path skipReason takes.

Rulings-conformance: checked against the rulings governing this work.

  • "we need the ability to force producer and vetter runs manually as a once off for observation" and its requirement that the dashboard distinguish a forced run from a paced one (thedavidmeister, 2026-08-09, Force a one-off manual producer/vetter run past the usage gate, for observation issue-pr-cron#245). OBEYED: this is the half that makes the multi-stop marker legible.
  • "force needs to force" (thedavidmeister, 2026-08-09) — the ruling that made a force able to walk more than one stop, and so made the marker an array. OBEYED: the tooltip names EVERY stop with that stop's own line, rather than collapsing to one.
  • "never invent scope" (standing). OBEYED: the diff carries the shape change and nothing adjacent; the ring, the legend and the statistics treatment from feat(metrics): draw a forced run apart from a paced tick #169 are untouched.
  • "data gaps are data entry — the honest empty state is correct" (standing). OBEYED: live data has no multi-stop row yet, so the live shots are byte-identical to master and that was reported as the evidence rather than dressed up; the fix is shown on a fixture that differs by exactly the added rows.
  • "screenshot every visual PR, and READ it" (standing). OBEYED.
  • Untrusted-data rendering and CURRENT-behaviour-only comments (CLAUDE.md). OBEYED.

The prose correctly moves off "forced past the usage gate" to "started by hand", which is now the accurate claim since a marker can name a kill switch, a gate, both, or neither.

CI: all checks pass. Merging with --merge --admin per the standing no-squash rule.

@thedavidmeister
thedavidmeister merged commit 8573648 into master Aug 9, 2026
9 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.

2 participants