@intentius/behold 0.10.1 → 0.12.0

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.
@@ -0,0 +1,158 @@
1
+ // #234 joins 3 + 1: the pure half of web/operator.js, tested the way
2
+ // web/run-playhead.js (#284) is — no DOM, no jsdom, no browser. Everything the
3
+ // strip DECIDES (which loops get a row, how a lease reads, what the approve
4
+ // button says it does) is a function from data to data; the DOM half is
5
+ // smoke/ui-smoke.mjs's job.
6
+ import { describe, it, expect } from "vitest";
7
+ import {
8
+ hasOperator,
9
+ operatorRefusal,
10
+ operatorRows,
11
+ leaseText,
12
+ operatorHeadline,
13
+ convergeGates,
14
+ APPROVE_SEMANTICS,
15
+ APPROVED_SEMANTICS,
16
+ CONVERGE_LOOP_LINE,
17
+ ONE_TICK_CAVEAT,
18
+ LEASE_LABEL,
19
+ } from "./operator.js";
20
+
21
+ /** src/operator.ts's OperatorState, as the server broadcasts it. Values trace to
22
+ * chant's own operator tests — see src/operator.test.ts's provenance note. */
23
+ const declared = [{ name: "staging-converge", env: "staging", dial: "apply" }];
24
+ const stripRow = {
25
+ op: "staging-converge",
26
+ env: "staging",
27
+ log: "converge(staging): drifted=1 remediated=0 reported=0 skipped-budget=0 skipped-flap=0 gated=1 unobserved=0 adopted=0",
28
+ at: "2026-01-01T00:00:00.000Z",
29
+ lease: "held",
30
+ leaseHolder: "op-a",
31
+ leaseExpiresAt: "2026-01-01T00:05:00.000Z",
32
+ pendingGates: 1,
33
+ };
34
+ const gate = {
35
+ loop: "converge",
36
+ rule: "drift-apply",
37
+ op: "fountain-apply",
38
+ gate: "rollout-gate",
39
+ convergeOp: "staging-converge",
40
+ env: "staging",
41
+ since: "2026-01-01T00:00:00.000Z",
42
+ semantics: APPROVE_SEMANTICS,
43
+ approve: {
44
+ method: "POST",
45
+ path: "/api/operator/approve/fountain-apply/rollout-gate",
46
+ command: "chant approve fountain-apply rollout-gate",
47
+ },
48
+ };
49
+ const state = {
50
+ declared,
51
+ strip: { rows: [stripRow], pendingGates: 1 },
52
+ gates: [gate],
53
+ note: null,
54
+ code: null,
55
+ readAt: "2026-01-01T00:02:00.000Z",
56
+ };
57
+ const NOW = Date.parse("2026-01-01T00:02:00.000Z");
58
+
59
+ describe("hasOperator", () => {
60
+ it("gates the strip on a DECLARED loop, never on a status read having answered", () => {
61
+ expect(hasOperator(null)).toBe(false);
62
+ expect(hasOperator({ declared: [] })).toBe(false);
63
+ // Declared, never read: the strip still appears and says so.
64
+ expect(hasOperator({ declared, strip: null })).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe("operatorRefusal", () => {
69
+ it("is null on a clean read", () => {
70
+ expect(operatorRefusal(state)).toBeNull();
71
+ });
72
+
73
+ it("states a refusal plainly for each of the three ways a read can fail", () => {
74
+ for (const code of ["no-operator", "no-operator-cli", "operator-status"]) {
75
+ const r = operatorRefusal({ ...state, strip: null, gates: [], note: `something about ${code}`, code });
76
+ expect(r).toEqual({ code, text: `something about ${code}` });
77
+ }
78
+ });
79
+ });
80
+
81
+ describe("operatorRows", () => {
82
+ it("prints chant's own tick line verbatim, with the instant and a coarse age", () => {
83
+ const [row] = operatorRows(state, NOW);
84
+ expect(row.log).toBe(stripRow.log);
85
+ expect(row.at).toBe("2026-01-01T00:00:00.000Z");
86
+ expect(row.ago).toBe("2m");
87
+ });
88
+
89
+ it("keeps a declared loop the last read didn't mention, rather than undercounting", () => {
90
+ const rows = operatorRows({ ...state, declared: [...declared, { name: "prod-observe", env: "prod" }] }, NOW);
91
+ expect(rows.map((r) => r.op)).toEqual(["staging-converge", "prod-observe"]);
92
+ expect(rows[1]).toMatchObject({ log: null, at: null, leaseText: "not in the last status read" });
93
+ });
94
+
95
+ it("says a declared loop has not been read yet before the first ask", () => {
96
+ const [row] = operatorRows({ ...state, strip: null, gates: [] }, NOW);
97
+ expect(row.leaseText).toBe("not read yet");
98
+ });
99
+ });
100
+
101
+ describe("leaseText", () => {
102
+ it("always names the holder — 'held' with nobody named is unactionable", () => {
103
+ expect(leaseText(stripRow)).toBe("lease held by op-a until 2026-01-01T00:05:00.000Z");
104
+ });
105
+
106
+ it("says an expired lease will be reclaimed, not that it is free", () => {
107
+ expect(leaseText({ ...stripRow, lease: "expired" })).toMatch(/expired \(op-a until .+\) — the next round reclaims it/);
108
+ });
109
+
110
+ it("reads a lease-less row as free", () => {
111
+ expect(leaseText({ ...stripRow, lease: "free", leaseHolder: null })).toBe(LEASE_LABEL.free);
112
+ });
113
+ });
114
+
115
+ describe("operatorHeadline", () => {
116
+ it("counts the loops, and the gates when a human is owed one", () => {
117
+ expect(operatorHeadline(state)).toBe("⟳ operating loop — 1 ConvergeOp · 1 gate pending");
118
+ expect(operatorHeadline({ ...state, gates: [] })).toBe("⟳ operating loop — 1 ConvergeOp");
119
+ });
120
+ });
121
+
122
+ describe("the one-tick caveat", () => {
123
+ it("says the strip is not a timeline, and why", () => {
124
+ expect(ONE_TICK_CAVEAT).toMatch(/only the last one/);
125
+ expect(ONE_TICK_CAVEAT).toMatch(/chant#2029/);
126
+ expect(ONE_TICK_CAVEAT).toMatch(/strip, not a timeline/);
127
+ });
128
+ });
129
+
130
+ describe("the converge gate card's words", () => {
131
+ it("says approving records a fact and does not unblock the dispatch", () => {
132
+ expect(APPROVE_SEMANTICS).toMatch(/records a fact/);
133
+ expect(APPROVE_SEMANTICS).toMatch(/does not unblock the dispatch/);
134
+ expect(APPROVE_SEMANTICS).toMatch(/next tick reads it/);
135
+ expect(APPROVED_SEMANTICS).toBe("recorded; the next tick acts on it");
136
+ });
137
+
138
+ it("names its loop, so it can't be read as the Temporal run gate", () => {
139
+ expect(CONVERGE_LOOP_LINE).toMatch(/converge loop/);
140
+ expect(CONVERGE_LOOP_LINE).toMatch(/chant approve/);
141
+ expect(CONVERGE_LOOP_LINE).toMatch(/not the Temporal run gate/);
142
+ });
143
+
144
+ it("carries the delegated write it performs, and no URL (chant#2028)", () => {
145
+ const [g] = convergeGates(state);
146
+ expect(g.approve).toEqual({
147
+ method: "POST",
148
+ path: "/api/operator/approve/fountain-apply/rollout-gate",
149
+ command: "chant approve fountain-apply rollout-gate",
150
+ });
151
+ expect(JSON.stringify(g)).not.toMatch(/https?:/);
152
+ });
153
+
154
+ it("has no cards when nothing is pending", () => {
155
+ expect(convergeGates({ ...state, gates: [] })).toEqual([]);
156
+ expect(convergeGates(null)).toEqual([]);
157
+ });
158
+ });
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The run playhead's panel (#284 item 2) — the SPA half of src/run-playhead.ts.
3
+ *
4
+ * Same discipline as web/carve-steps.js, web/json-view.js and
5
+ * web/layout-store.js: everything this panel DECIDES is a pure function that
6
+ * takes data and returns data, unit-tested next door in run-playhead.test.js;
7
+ * the DOM half at the bottom is smoke/ui-smoke.mjs's job. The fetches, the SSE
8
+ * subscription and the graph re-pull all stay in app.js.
9
+ *
10
+ * What it renders, from the `runState` the server broadcasts:
11
+ * - one honest verdict line — and there is deliberately no wording for
12
+ * "probably finished". A stream that died says so.
13
+ * - the pending gate as a first-class card (#234): who is waiting, since when,
14
+ * what it holds back, and an Approve button that POSTs the EXISTING
15
+ * op-signal route. behold never signals on its own initiative.
16
+ * - the settled steps, in the order they settled.
17
+ */
18
+
19
+ /** Settled-step tones, in the same token vocabulary the apply progress panel
20
+ * uses (`APPLY_STATUS_COLOR` in app.js) so "green means it worked" reads the
21
+ * same everywhere. */
22
+ export const RUN_STEP_COLOR = {
23
+ ok: "var(--managed)",
24
+ fail: "var(--degraded)",
25
+ skipped: "var(--muted)",
26
+ };
27
+
28
+ /** How the run as a whole reads. `lost` is its own tone, not a shade of
29
+ * failure: behold does not know what happened, which is a different statement
30
+ * from "it failed". */
31
+ export const RUN_STATUS_COLOR = {
32
+ idle: "var(--muted)",
33
+ running: "var(--pending)",
34
+ ok: "var(--managed)",
35
+ failed: "var(--degraded)",
36
+ lost: "var(--foreign)",
37
+ };
38
+
39
+ /** `1062` → `1.1s`. */
40
+ export function took(ms) {
41
+ return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
42
+ }
43
+
44
+ /**
45
+ * How long a gate has been waiting, from its ISO `since`. Coarse on purpose —
46
+ * an approval that has been pending "3h" is the fact; the second it started
47
+ * is on the card as the raw instant.
48
+ */
49
+ export function waitingFor(since, now = Date.now()) {
50
+ const t = Date.parse(since);
51
+ if (!Number.isFinite(t)) return null;
52
+ const s = Math.max(0, Math.round((now - t) / 1000));
53
+ if (s < 60) return `${s}s`;
54
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
55
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
56
+ return `${Math.floor(s / 86400)}d`;
57
+ }
58
+
59
+ /**
60
+ * The one-line verdict, or null when there is nothing to say at all.
61
+ *
62
+ * Every branch states what behold KNOWS. `lost` never borrows the word
63
+ * "finished", and an adopted run (the gate query answered for a run this
64
+ * session didn't start) doesn't claim zero settled steps as if it had watched.
65
+ */
66
+ export function runVerdict(run) {
67
+ if (!run || !run.op) return null;
68
+ const n = (run.records || []).length;
69
+ const settled = `${n} step${n === 1 ? "" : "s"} settled`;
70
+ if (run.status === "idle") {
71
+ return { text: `${run.op}: no per-step records — this session didn't start the run`, tone: "idle" };
72
+ }
73
+ if (run.status === "running") {
74
+ return {
75
+ text:
76
+ run.mode === "local"
77
+ ? `${run.op}: running on the local executor — steps arrive together when it ends`
78
+ : `${run.op}: running — ${settled}`,
79
+ tone: "running",
80
+ };
81
+ }
82
+ if (run.status === "ok") return { text: `${run.op}: exited cleanly — ${settled}`, tone: "ok" };
83
+ if (run.status === "failed") return { text: `${run.op}: a step failed — ${settled}`, tone: "failed" };
84
+ return { text: `${run.op}: stream lost — frozen at ${settled}`, tone: "lost" };
85
+ }
86
+
87
+ /** The settled steps, in arrival order, as rows. */
88
+ export function stepRows(run) {
89
+ return (run?.records || []).map((r) => ({
90
+ label: `${r.phase} · ${r.fn}`,
91
+ status: r.status,
92
+ took: r.status === "skipped" ? "skipped" : took(r.durationMs),
93
+ error: r.error || null,
94
+ }));
95
+ }
96
+
97
+ /**
98
+ * The pending gate card (#234), from the run state alone.
99
+ *
100
+ * `guards` and `node` come from the graph's own answer (`meta.gate`, built by
101
+ * src/run-playhead.ts's `pendingGateCard`) when the ops lens has been loaded;
102
+ * this is the fallback so the card still appears on the Deploy tab of a session
103
+ * that never opened the lens. `approve` is stated, not implied — it is the same
104
+ * delegated write the existing gate button performs.
105
+ */
106
+ export function gateCardFrom(run) {
107
+ if (!run || !run.op || !run.gate) return null;
108
+ return {
109
+ op: run.op,
110
+ signalName: run.gate.signalName,
111
+ description: run.gate.description || undefined,
112
+ since: run.gate.since,
113
+ approve: {
114
+ method: "POST",
115
+ path: `/api/ops/${encodeURIComponent(run.op)}/signal/${encodeURIComponent(run.gate.signalName)}`,
116
+ },
117
+ };
118
+ }
119
+
120
+ /** Does the panel have anything to show at all? */
121
+ export function hasPlayhead(run) {
122
+ return Boolean(run && (run.op || (run.records && run.records.length)));
123
+ }
124
+
125
+ // ── the DOM half ─────────────────────────────────────────────────────────────
126
+
127
+ function el(tag, cls, text) {
128
+ const n = document.createElement(tag);
129
+ if (cls) n.className = cls;
130
+ if (text !== undefined) n.textContent = text;
131
+ return n;
132
+ }
133
+
134
+ /**
135
+ * Render the playhead panel into `host` (cleared first — the whole subtree is
136
+ * rebuilt per event, exactly as renderDial() does).
137
+ *
138
+ * `card` is the graph's richer pending-gate answer when one has been loaded,
139
+ * else null. `actions.approve(op, signalName)` performs the delegated signal.
140
+ */
141
+ export function renderPlayhead(host, run, card, actions = {}) {
142
+ host.textContent = "";
143
+ const verdict = runVerdict(run);
144
+ if (!verdict) return host;
145
+
146
+ const line = el("div", "run-verdict", verdict.text);
147
+ line.style.color = RUN_STATUS_COLOR[verdict.tone] || "var(--muted)";
148
+ host.appendChild(line);
149
+
150
+ if (run.lost) host.appendChild(el("div", "run-lost", run.lost));
151
+ if (run.gateNote) host.appendChild(el("div", "run-lost", `Gate state unavailable — ${run.gateNote}`));
152
+
153
+ const gate = card || gateCardFrom(run);
154
+ if (gate) host.appendChild(renderGateCard(gate, actions));
155
+
156
+ const rows = stepRows(run);
157
+ if (rows.length) {
158
+ const list = el("div", "run-steps");
159
+ for (const r of rows) {
160
+ const row = el("div", "run-step");
161
+ const dot = el("span", "run-dot");
162
+ dot.style.background = RUN_STEP_COLOR[r.status] || "var(--muted)";
163
+ row.appendChild(dot);
164
+ row.appendChild(el("span", "grow", r.label));
165
+ row.appendChild(el("span", "run-took", r.took));
166
+ if (r.error) row.title = r.error;
167
+ list.appendChild(row);
168
+ }
169
+ host.appendChild(list);
170
+ }
171
+ return host;
172
+ }
173
+
174
+ /**
175
+ * The pending-approval card. Its button is the delegated write, and the card
176
+ * says so out loud: behold triggers Ops and signals gates a human presses; it
177
+ * never approves anything on its own initiative.
178
+ */
179
+ export function renderGateCard(gate, actions = {}) {
180
+ const box = el("div", "run-gate");
181
+ box.appendChild(el("div", "run-gate-title", `⏸ waiting for approval — ${gate.signalName}`));
182
+ if (gate.description) box.appendChild(el("div", "run-gate-desc", gate.description));
183
+ const waited = waitingFor(gate.since);
184
+ box.appendChild(
185
+ el("div", "run-gate-meta", `${gate.op} · pending ${waited ? `${waited} ` : ""}since ${gate.since}`),
186
+ );
187
+ if (gate.guards) box.appendChild(el("div", "run-gate-meta", `holds back: ${gate.guards}`));
188
+
189
+ const approve = el("button", "approve", `Approve ${gate.signalName}`);
190
+ approve.title = `${gate.approve.method} ${gate.approve.path} — the same delegated signal as chant run signal ${gate.op} ${gate.signalName}. behold never approves on its own.`;
191
+ approve.addEventListener("click", () => actions.approve?.(gate.op, gate.signalName));
192
+ box.appendChild(approve);
193
+ return box;
194
+ }
@@ -0,0 +1,134 @@
1
+ // #284 item 2: the pure half of web/run-playhead.js, tested the way
2
+ // web/carve-steps.js (#254), web/json-view.js (#259) and web/layout-store.js
3
+ // (#245) are — no DOM, no jsdom, no browser. Everything the panel DECIDES
4
+ // (what the run's verdict is, how long a gate has been waiting, what the
5
+ // approve button POSTs) is a function from data to data; the DOM half is
6
+ // smoke/ui-smoke.mjs's job.
7
+ import { describe, it, expect } from "vitest";
8
+ import { runVerdict, stepRows, gateCardFrom, hasPlayhead, took, waitingFor } from "./run-playhead.js";
9
+
10
+ const base = {
11
+ op: "playhead-probe",
12
+ mode: "temporal",
13
+ status: "running",
14
+ records: [],
15
+ gate: null,
16
+ gateNote: null,
17
+ startedAt: "2026-08-29T18:00:00.000Z",
18
+ endedAt: null,
19
+ lost: null,
20
+ };
21
+ const withRecords = (n) => ({
22
+ ...base,
23
+ records: Array.from({ length: n }, (_, i) => ({ phase: "P", fn: `f${i}`, status: "ok", durationMs: 1000 })),
24
+ });
25
+
26
+ describe("took", () => {
27
+ it("reads sub-second in ms and the rest in seconds", () => {
28
+ expect(took(5)).toBe("5ms");
29
+ expect(took(1062)).toBe("1.1s");
30
+ });
31
+ });
32
+
33
+ describe("waitingFor", () => {
34
+ const now = Date.parse("2026-08-29T18:30:00.000Z");
35
+ it("coarsens the wait, keeping the raw instant for the card", () => {
36
+ expect(waitingFor("2026-08-29T18:29:30.000Z", now)).toBe("30s");
37
+ expect(waitingFor("2026-08-29T18:00:00.000Z", now)).toBe("30m");
38
+ expect(waitingFor("2026-08-29T15:00:00.000Z", now)).toBe("3h");
39
+ expect(waitingFor("2026-08-26T18:00:00.000Z", now)).toBe("3d");
40
+ });
41
+
42
+ it("answers nothing rather than a wrong duration for an unreadable instant", () => {
43
+ expect(waitingFor("soon", now)).toBeNull();
44
+ expect(waitingFor("", now)).toBeNull();
45
+ });
46
+ });
47
+
48
+ describe("runVerdict", () => {
49
+ it("has nothing to say before anything has run", () => {
50
+ expect(runVerdict(null)).toBeNull();
51
+ expect(runVerdict({ op: null, status: "idle", records: [] })).toBeNull();
52
+ });
53
+
54
+ it("never calls a lost stream finished", () => {
55
+ const v = runVerdict({ ...withRecords(3), status: "lost", lost: "the stream ended" });
56
+ expect(v.tone).toBe("lost");
57
+ expect(v.text).toContain("frozen at 3 steps settled");
58
+ expect(v.text).not.toMatch(/complete|finished|done/i);
59
+ });
60
+
61
+ it("distinguishes a clean exit from a failed step", () => {
62
+ expect(runVerdict({ ...withRecords(2), status: "ok" })).toMatchObject({ tone: "ok" });
63
+ expect(runVerdict({ ...withRecords(2), status: "failed" }).text).toContain("a step failed");
64
+ });
65
+
66
+ it("explains why a local run shows nothing until it ends", () => {
67
+ expect(runVerdict({ ...base, mode: "local" }).text).toContain("steps arrive together when it ends");
68
+ });
69
+
70
+ it("doesn't claim to have watched a run it didn't start", () => {
71
+ const v = runVerdict({ ...base, status: "idle" });
72
+ expect(v.text).toContain("this session didn't start the run");
73
+ expect(v.text).not.toContain("0 steps");
74
+ });
75
+ });
76
+
77
+ describe("stepRows", () => {
78
+ it("keeps arrival order and labels a skip as a skip, not a duration", () => {
79
+ const rows = stepRows({
80
+ records: [
81
+ { phase: "Build", fn: "chantBuild", status: "ok", durationMs: 1062 },
82
+ { phase: "Verify", fn: "httpCheck", status: "fail", durationMs: 15164, error: "fetch failed" },
83
+ { phase: "Never", fn: "httpCheck", status: "skipped", durationMs: 0 },
84
+ ],
85
+ });
86
+ expect(rows.map((r) => `${r.label} ${r.took}`)).toEqual([
87
+ "Build · chantBuild 1.1s",
88
+ "Verify · httpCheck 15.2s",
89
+ "Never · httpCheck skipped",
90
+ ]);
91
+ expect(rows[1].error).toBe("fetch failed");
92
+ expect(rows[0].error).toBeNull();
93
+ });
94
+
95
+ it("survives a state with no records at all", () => {
96
+ expect(stepRows(null)).toEqual([]);
97
+ expect(stepRows({})).toEqual([]);
98
+ });
99
+ });
100
+
101
+ describe("gateCardFrom", () => {
102
+ it("is null unless a gate was actually NAMED — a stalled track is not a gate", () => {
103
+ expect(gateCardFrom(base)).toBeNull();
104
+ expect(gateCardFrom({ ...base, gateNote: "no run" })).toBeNull();
105
+ });
106
+
107
+ it("points approve at the existing delegated op-signal route", () => {
108
+ const card = gateCardFrom({
109
+ ...base,
110
+ op: "prod-promote",
111
+ gate: { signalName: "approve-promote", description: "Approve the prod promotion", since: "2026-08-29T18:00:12.000Z" },
112
+ });
113
+ expect(card).toEqual({
114
+ op: "prod-promote",
115
+ signalName: "approve-promote",
116
+ description: "Approve the prod promotion",
117
+ since: "2026-08-29T18:00:12.000Z",
118
+ approve: { method: "POST", path: "/api/ops/prod-promote/signal/approve-promote" },
119
+ });
120
+ });
121
+
122
+ it("escapes names into the signal path", () => {
123
+ const card = gateCardFrom({ ...base, op: "a b", gate: { signalName: "c/d", since: "x" } });
124
+ expect(card.approve.path).toBe("/api/ops/a%20b/signal/c%2Fd");
125
+ });
126
+ });
127
+
128
+ describe("hasPlayhead", () => {
129
+ it("is false on a fresh session and true once an Op is being watched", () => {
130
+ expect(hasPlayhead(null)).toBe(false);
131
+ expect(hasPlayhead({ op: null, records: [] })).toBe(false);
132
+ expect(hasPlayhead(base)).toBe(true);
133
+ });
134
+ });
@@ -0,0 +1,35 @@
1
+ // #240: a few pathological palettes (Darkermatrix and friends) leave --muted
2
+ // illegible on --panel because the quieted fg/panel mix lands well under a
3
+ // readable ratio. tokensFor() pushes --muted's OKLCH L away from --panel's
4
+ // until a real WCAG contrast() clears the floor (#229's guaranteed-visible
5
+ // trick for --active, generalised to an actual ratio). smoke/ui-smoke.mjs
6
+ // sweeps all 552 palettes for the same assertion; this pins the exact
7
+ // palette the issue was filed against, with no browser involved.
8
+ import { describe, it, expect } from "vitest";
9
+ import { contrast, desaturate, mix, tokensFor } from "./theme.js";
10
+ import { THEMES } from "./themes.js";
11
+
12
+ describe("tokensFor — the --muted contrast floor (#240)", () => {
13
+ it("Darkermatrix's naive fg/panel mix reads at ~1.25:1, the issue's reported ratio — the floor push has to move it", () => {
14
+ const th = THEMES["Darkermatrix"];
15
+ const panel = tokensFor(th).panel;
16
+ // What tokensFor() derived --muted from before #240's floor push:
17
+ // fg mixed to 0.5, one chroma step down.
18
+ const naiveMuted = desaturate(mix(th.bg, th.fg, 0.5), 0.75);
19
+ const ratio = contrast(naiveMuted, panel);
20
+ expect(ratio).toBeGreaterThan(1.2);
21
+ expect(ratio).toBeLessThan(1.3);
22
+ });
23
+
24
+ it("clears the 3:1 floor on Darkermatrix after the push", () => {
25
+ const t = tokensFor(THEMES["Darkermatrix"]);
26
+ expect(contrast(t.muted, t.panel)).toBeGreaterThanOrEqual(3);
27
+ });
28
+
29
+ it("holds the floor across every shipped palette, not just the one reported", () => {
30
+ for (const th of Object.values(THEMES)) {
31
+ const t = tokensFor(th);
32
+ expect(contrast(t.muted, t.panel)).toBeGreaterThanOrEqual(3);
33
+ }
34
+ });
35
+ });