From 54faf8b0212f552ad675b4b6b7e1a5127b40e6bd Mon Sep 17 00:00:00 2001 From: Mehar Khanna Date: Tue, 27 Oct 2026 00:20:26 -0400 Subject: [PATCH] @genclass/runtime 0.3.0-beta.4: the devtools pill counts measured checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the snow day site GenClass fixed invisible text, a stuck scroll lock and a click block on screen, and the pill still read "0 · 0": it counted only interventions and model detections, and checks lived in their tab alone. The pill gets a third counter (scan icon) for the page's measured checks, red when there are any, titled with how many were fixed on screen, pulsing on a new one; with only checks so far the panel opens on the Checks tab; each check card leads with its kind in words, the kind itself beside it. Checks stay "measured", never model findings. Version in package.json, src/version.ts and the lockfile; the CHANGELOG gets 0.3.0-beta.4. No model-visible text changed. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 2 +- packages/runtime/CHANGELOG.md | 11 +++++ packages/runtime/package.json | 2 +- packages/runtime/src/devtools/css.ts | 6 ++- packages/runtime/src/devtools/index.ts | 43 +++++++++++++++---- packages/runtime/src/version.ts | 2 +- packages/runtime/test/devtools-checks.test.ts | 29 +++++++++++++ 7 files changed, 82 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5d0b15a..def635af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16964,7 +16964,7 @@ }, "packages/runtime": { "name": "@genclass/runtime", - "version": "0.3.0-beta.3", + "version": "0.3.0-beta.4", "license": "Apache-2.0", "dependencies": { "onnxruntime-web": "1.30.0" diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index 29ce78a9..d918ed82 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.3.0-beta.4 (2026-10-27) + +- **The devtools pill counts measured checks** (`src/devtools/`, VISUAL.md 9.3). The collapsed pill showed only + interventions and model detections, so a page where GenClass had fixed invisible text, a stuck scroll lock and a + click block on screen still read "0 · 0". The pill now has a third counter (the scan icon) with the page's measured + checks, red when there are any, its title saying how many were fixed on screen, and it pulses when a new one arrives. + With checks and no interventions or detections yet, the panel opens on the Checks tab. Each check card leads with + the kind in words ("Page cannot scroll", "Page stopped taking clicks", "Invisible text", "Ads covering content"), + the kind itself beside it. Checks stay labelled "measured", never model findings. Tests: `devtools-checks.test.ts`. + No model-visible text changed (`situation-v2.3`). + ## 0.3.0-beta.3 (2026-10-26) - **A colour fix stays while its element is out of view** (`src/visual/engine.ts`, VISUAL.md 11.2). The periodic diff --git a/packages/runtime/package.json b/packages/runtime/package.json index a89cc51c..567c202a 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@genclass/runtime", - "version": "0.3.0-beta.3", + "version": "0.3.0-beta.4", "description": "A runtime for web apps: a small local model watches user actions, async work, network, state and errors, and flags, and optionally prevents, stale responses, races, duplicate requests, inconsistent state and failure storms.", "type": "module", "license": "Apache-2.0", diff --git a/packages/runtime/src/devtools/css.ts b/packages/runtime/src/devtools/css.ts index b97a66fc..4a69113e 100644 --- a/packages/runtime/src/devtools/css.ts +++ b/packages/runtime/src/devtools/css.ts @@ -54,6 +54,7 @@ border:1px solid var(--line2);font-size:12.5px;font-weight:500;color:var(--fg);t .pc b{font:500 12px var(--mono)} .pc.on.a{color:var(--go)} .pc.on.d{color:var(--bug)} +.pc.on.c{color:var(--bug)} .pd{width:7px;height:7px;background:var(--fg3);margin-left:1px} .s-ready .pd,.s-ready .dot{background:var(--go-f)} .s-error .pd,.s-error .dot{background:var(--bug-f)} @@ -63,7 +64,8 @@ border:1px solid var(--line2);font-size:12.5px;font-weight:500;color:var(--fg);t .pill::after{content:"";position:absolute;inset:-1px;pointer-events:none;opacity:0;outline:1px solid var(--ring,transparent);outline-offset:0} .pill.pa::after{--ring:var(--go);animation:ring 1.5s cubic-bezier(.2,.7,.3,1)} .pill.pdd::after{--ring:var(--bug);animation:ring 1.5s cubic-bezier(.2,.7,.3,1)} -.pill.pa .pc.a b,.pill.pdd .pc.d b{animation:bump .5s cubic-bezier(.2,.8,.2,1)} +.pill.pck::after{--ring:var(--bug);animation:ring 1.5s cubic-bezier(.2,.7,.3,1)} +.pill.pa .pc.a b,.pill.pdd .pc.d b,.pill.pck .pc.c b{animation:bump .5s cubic-bezier(.2,.8,.2,1)} @keyframes ring{0%{opacity:.9;outline-offset:0}100%{opacity:0;outline-offset:12px}} @keyframes bump{40%{transform:scale(1.25)}} @keyframes blink{50%{opacity:.35}} @@ -142,6 +144,8 @@ background:var(--bg2);border-top:1px solid var(--line);border-bottom:1px solid v .cb{flex:1;min-width:0} .ch{display:flex;align-items:baseline;gap:8px;min-height:20px} .ct{flex:1;min-width:0;font-weight:600;letter-spacing:-.01em;line-height:1.35;padding-top:3px} +.chk .cl{font-weight:600;letter-spacing:-.01em;line-height:1.35;padding-top:3px} +.chk .ct{font:400 11px var(--mono);color:var(--fg3)} .ago{font:400 11px var(--mono);color:var(--fg3);white-space:nowrap} .rep{align-self:center;height:18px;padding:0 5px;font:500 11px/16px var(--mono);color:var(--bug);background:var(--bug-bg);border:1px solid var(--bug-line)} .cx{margin:3px 0 0;font-size:12.5px;line-height:1.5;color:var(--fg2);overflow-wrap:anywhere} diff --git a/packages/runtime/src/devtools/index.ts b/packages/runtime/src/devtools/index.ts index 08995cc2..50dea100 100644 --- a/packages/runtime/src/devtools/index.ts +++ b/packages/runtime/src/devtools/index.ts @@ -131,6 +131,18 @@ const FILTERS: [Group, string][] = [ ["other", "Other"], ]; const QLABEL: Record = { diagnosis: "Diagnosis", action: "Action" }; +// a measured check's kind in words, on its card and in the pill's label (the kind itself stays on the card) +const CHECK_TITLE: Record = { + "text-invisible": "Invisible text", + "text-low-contrast": "Hard-to-read text", + "scroll-lock-stuck": "Page cannot scroll", + "pointer-events-leak": "Page stopped taking clicks", + "click-blocked": "Clicks blocked by an invisible layer", + "dialog-covered": "Dialog covered", + "focus-obscured": "Focused control hidden", + "ad-heavy": "Ads covering content", + "page-blank": "Blank page", +}; const qrank = (q: string): number => (q === "diagnosis" ? 0 : q === "action" ? 1 : 2); const MAX_CARDS = 200; @@ -204,6 +216,7 @@ class Devtools { private readonly pm: HTMLElement; private readonly pcA: HTMLElement; private readonly pcD: HTMLElement; + private readonly pcC: HTMLElement; private readonly nA: HTMLElement; private readonly nD: HTMLElement; private readonly pauseBtn: HTMLElement; @@ -287,11 +300,12 @@ class Devtools { h("span", { class: "vs" }), (this.pcA = h("span", { class: "pc a", title: "Interventions" }, icon("shield"), h("b", { text: "0" }))), (this.pcD = h("span", { class: "pc d", title: "Detections" }, icon("eye"), h("b", { text: "0" }))), + (this.pcC = h("span", { class: "pc c", title: "Measured checks" }, icon("scan"), h("b", { text: "0" }))), h("span", { class: "pd" }), (this.pb = h("span", { class: "pb", hidden: true }, (this.pbi = h("i")))), ); this.pill.addEventListener("animationend", (e) => { - if ((e as AnimationEvent).animationName === "ring") this.pill.classList.remove("pa", "pdd"); + if ((e as AnimationEvent).animationName === "ring") this.pill.classList.remove("pa", "pdd", "pck"); }); // header @@ -460,7 +474,7 @@ class Devtools { on("report", (r) => this.addReport(r)); on("event", (e) => this.addEvent(e)); on("status", () => this.mark(STATUS | MODE)); // setMode() is announced as a status change too - on("check", (f) => this.addCheck(f)); + on("check", (f) => this.addCheck(f, true)); on("report", (r) => { // a fix line (applied, undone, dropped) changes that finding's card const c = r.kind === "check" && r.check ? this.chks.get(r.check.id) : undefined; @@ -501,11 +515,12 @@ class Devtools { safe(() => (this.rt as { vis?: { start(): void } | null }).vis?.start(), undefined); } - private addCheck(f: CheckFinding): void { + private addCheck(f: CheckFinding, live = false): void { if (!f || this.chks.has(f.id)) return; this.chks.set(f.id, { f, dirty: false }); bound(this.chks, MAX_CARDS, (c) => c.el?.remove()); this.mark(LISTS); + if (live) this.notify("c"); } // -------------------------------------------------------------------------------------- data intake @@ -626,12 +641,12 @@ class Devtools { this.mark(EVENTS); } - private notify(kind: "a" | "d"): void { + private notify(kind: "a" | "d" | "c"): void { if (this.paused) this.unseen++; const p = this.pill; - p.classList.remove("pa", "pdd"); + p.classList.remove("pa", "pdd", "pck"); void p.offsetWidth; // restart the animation - p.classList.add(kind === "a" ? "pa" : "pdd"); + p.classList.add(kind === "a" ? "pa" : kind === "d" ? "pdd" : "pck"); } // ------------------------------------------------------------------------------------------ render @@ -684,10 +699,17 @@ class Devtools { this.nA.classList.toggle("on", na > 0); this.nD.classList.toggle("on", nd > 0); this.nD.title = np ? `${nd} likely, ${np} possible` : ""; - this.nC.textContent = String(this.chks.size); - this.nC.classList.toggle("on", this.chks.size > 0); + // measured checks count in the pill too: a page GenClass fixed on screen must not read "0 · 0" + const nc = this.chks.size; + let nf = 0; + for (const c of this.chks.values()) if (c.f.fix?.applied) nf++; + (this.pcC.lastElementChild as HTMLElement).textContent = String(nc); + this.pcC.classList.toggle("on", nc > 0); + this.pcC.title = nc ? `${nc} measured ${nc === 1 ? "check" : "checks"}, ${nf} fixed on screen` : "Measured checks"; + this.nC.textContent = String(nc); + this.nC.classList.toggle("on", nc > 0); const s = safe(() => this.rt.status.state, "off"); - this.pill.setAttribute("aria-label", `Open GenClass devtools: ${na} interventions, ${nd} detections, model ${s}`); + this.pill.setAttribute("aria-label", `Open GenClass devtools: ${na} interventions, ${nd} detections, ${nc} checks (${nf} fixed), model ${s}`); this.pz.hidden = !this.paused; this.pzT.textContent = this.unseen ? `Feed paused · ${this.unseen} new` : "Feed paused"; } @@ -814,6 +836,7 @@ class Devtools { h( "div", { class: "ch" }, + h("span", { class: "cl", text: CHECK_TITLE[f.kind] ?? f.kind }), h("span", { class: "ct", text: f.kind }), h("span", { class: "badge", text: "measured", title: "A deterministic measurement against a fixed threshold, not a model finding" }), h("time", { class: "ago", "data-at": String(f.at), text: ago(this.now() - f.at), title: `runtime clock ${clockT(f.at)}` }), @@ -1372,6 +1395,8 @@ class Devtools { this.pill.setAttribute("aria-expanded", "true"); this.bits |= ALL; this.flush(); + // with only measured checks so far, open on them rather than on an empty Interventions tab + if (this.tab === "interventions" && !this.acts.size && !this.dets.size && this.chks.size) this.tab = "checks"; this.setTab(this.tab, user); this.startTicking(); } diff --git a/packages/runtime/src/version.ts b/packages/runtime/src/version.ts index 3f6d2979..53228b5d 100644 --- a/packages/runtime/src/version.ts +++ b/packages/runtime/src/version.ts @@ -2,4 +2,4 @@ // test/telemetry.test.ts ("RUNTIME_VERSION matches package.json"); bump both together: .github/workflows/release.yml // checks that the v tag equals package.json and that this file holds the same string. -export const RUNTIME_VERSION = "0.3.0-beta.3"; +export const RUNTIME_VERSION = "0.3.0-beta.4"; diff --git a/packages/runtime/test/devtools-checks.test.ts b/packages/runtime/test/devtools-checks.test.ts index b5fdfe36..68eb1874 100644 --- a/packages/runtime/test/devtools-checks.test.ts +++ b/packages/runtime/test/devtools-checks.test.ts @@ -98,4 +98,33 @@ describe("devtools: Checks tab (VISUAL.md 9.3)", () => { $('[data-pane="checks"] [data-act="undo"]')!.click(); expect(undo).toHaveBeenCalledTimes(1); }); + + it("counts measured checks in the collapsed pill, names them in words and opens on them (VISUAL.md 9.3)", async () => { + const rt = new MockRuntime(); + const h = mountDevtools(rt, {}); + handles.push(h); + const sr = (h.element as HTMLElement).shadowRoot as ShadowRoot; + const $ = (s: string) => sr.querySelector(s); + expect($(".pc.c b")?.textContent).toBe("0"); + expect($(".pc.c")?.classList.contains("on")).toBe(false); + const undo = vi.fn(() => true); + rt.emitTo("check", finding({ fix: { id: "f1", applied: true, css: "p.hero__subtitle { color: rgb(115, 115, 115); }", undo } })); + rt.emitTo("check", finding({ id: "c2", kind: "scroll-lock-stuck", suggestion: undefined, message: "[GenClass] Check (measured): the page cannot scroll." })); + await frame(); + // a page GenClass fixed on screen does not read "0 · 0": the pill's third counter has the checks, and it pulses + expect($(".pc.a b")?.textContent).toBe("0"); + expect($(".pc.d b")?.textContent).toBe("0"); + expect($(".pc.c b")?.textContent).toBe("2"); + expect($(".pc.c")?.classList.contains("on")).toBe(true); + expect($(".pc.c")?.title).toBe("2 measured checks, 1 fixed on screen"); + expect($(".pill")?.getAttribute("aria-label")).toContain("2 checks (1 fixed)"); + expect($(".pill")?.classList.contains("pck")).toBe(true); + // with no interventions or detections yet, the panel opens on the checks + $(".pill")!.click(); + await frame(); + expect($('[data-tab="checks"]')?.getAttribute("aria-selected")).toBe("true"); + const cards = [...sr.querySelectorAll('[data-pane="checks"] article.card')]; + expect(cards.map((c) => c.querySelector(".cl")?.textContent)).toEqual(["Page cannot scroll", "Invisible text"]); + expect(cards.map((c) => c.querySelector(".ct")?.textContent)).toEqual(["scroll-lock-stuck", "text-invisible"]); + }); });