From b7b73f8f6678dd3f828940140091ec104964a113 Mon Sep 17 00:00:00 2001 From: miga-heygen Date: Sat, 12 Sep 2026 06:05:24 +0000 Subject: [PATCH 1/5] fix(cli): distinguish a skipped browser check from a genuinely clean one `hyperframes check`'s runtime/layout/motion/contrast JSON sections reported the exact same ok:true/zero-findings shape whether the browser session actually ran and found nothing, or never ran at all (a blocking lint error, a lint-step crash, or an exception thrown before the audit could start all took the same "empty result" fallback). Only the top-level ok field and an undocumented layout.duration === 0 tell separated the two cases apart. Add a skipped boolean to CheckBrowserResult, set at the only two places one is constructed (false in the real audit result, true in the empty-result fallback), and mirror it to a new top-level browserSkipped field on CheckReport. The human-readable report now prints a warning when the browser never ran; the JSON envelope carries the same signal for callers that only check individual sections. Co-Authored-By: Miguel Angel --- packages/cli/src/commands/check.test.ts | 30 ++++++++++++++++++- packages/cli/src/commands/check.ts | 6 ++++ packages/cli/src/utils/checkPipeline.ts | 3 ++ packages/cli/src/utils/checkTypes.ts | 11 +++++++ .../references/lint-validate-inspect.md | 4 +-- 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/check.test.ts b/packages/cli/src/commands/check.test.ts index 914c05af9ed..b3a0bb9d84f 100644 --- a/packages/cli/src/commands/check.test.ts +++ b/packages/cli/src/commands/check.test.ts @@ -949,6 +949,7 @@ function reportWithFindings(overrides: Partial = {}): CheckReport { return { ok: true, strict: false, + browserSkipped: false, lint: { ...emptySection(), filesScanned: 0 }, runtime: emptySection(), layout: { @@ -1047,6 +1048,7 @@ describe("check pipeline", () => { const envelope = JSON.parse(output); expect(envelope).toMatchObject({ ok: true, + browserSkipped: false, lint: { ok: true }, runtime: { ok: true }, layout: { ok: true }, @@ -1057,7 +1059,7 @@ describe("check pipeline", () => { }); }); - it("short-circuits on lint errors without launching a browser", async () => { + it("short-circuits on lint errors without launching a browser, and flags the skipped sections", async () => { const lint = lintWith( "error", "root_missing_composition_id", @@ -1069,6 +1071,32 @@ describe("check pipeline", () => { expect(checkExitCode(report)).toBe(1); expect(report.lint.findings).toHaveLength(1); expect(browser).not.toHaveBeenCalled(); + // PRINFRA-700: runtime/layout/motion/contrast report the same ok:true/ + // zero-findings shape whether the browser ran clean or never launched at + // all — browserSkipped is the only thing that tells the two apart. + expect(report.browserSkipped).toBe(true); + expect(report.runtime).toMatchObject({ ok: true, errorCount: 0, findings: [] }); + expect(report.layout).toMatchObject({ ok: true, errorCount: 0, findings: [], duration: 0 }); + expect(report.motion).toMatchObject({ ok: true, errorCount: 0, findings: [] }); + expect(report.contrast).toMatchObject({ ok: true, errorCount: 0, findings: [] }); + }); + + it("marks browserSkipped false once a browser session actually runs", async () => { + const { report } = await runScenario(fakeDriver()); + expect(report.browserSkipped).toBe(false); + }); + + it("marks browserSkipped true when the browser session throws before producing results", async () => { + const { deps } = dependencies(fakeDriver()); + deps.runBrowserCheck = vi.fn(async () => { + throw new Error("Chrome launch failed"); + }); + const report = await runCheckPipeline(PROJECT, DEFAULT_CHECK_OPTIONS, deps); + + expect(report.ok).toBe(false); + expect(report.browserSkipped).toBe(true); + expect(report.runtime.findings).toHaveLength(1); + expect(report.layout).toMatchObject({ ok: true, errorCount: 0, findings: [] }); }); it("gates AA contrast failures and --no-contrast skips the pass", async () => { diff --git a/packages/cli/src/commands/check.ts b/packages/cli/src/commands/check.ts index f90b494398c..787b221c5de 100644 --- a/packages/cli/src/commands/check.ts +++ b/packages/cli/src/commands/check.ts @@ -405,6 +405,12 @@ function nonNegativeNumber(value: unknown, fallback: number): number { function printHumanReport(report: CheckReport): void { printSection("Lint", report.lint); + if (report.browserSkipped) { + console.log(); + console.log( + ` ${c.warn("⚠")} Browser session never ran — layout, motion, and contrast below are empty placeholders, not a clean pass.`, + ); + } printSection("Runtime", report.runtime); printLayoutSection("Layout", report.layout); printSection("Motion", report.motion); diff --git a/packages/cli/src/utils/checkPipeline.ts b/packages/cli/src/utils/checkPipeline.ts index 1dc0dd40bd0..8e6d8668690 100644 --- a/packages/cli/src/utils/checkPipeline.ts +++ b/packages/cli/src/utils/checkPipeline.ts @@ -1130,6 +1130,7 @@ export async function runAuditGrid( contrastPassed: contrast.passed, screenshots: collected.screenshots, timings: { launchSettleMs: 0, seekLoopMs, contrastMs: collected.contrastMs }, + skipped: false, }; } @@ -1412,6 +1413,7 @@ function buildReport( const report: CheckReport = { ok: errorCount === 0 && (!options.strict || warningCount === 0), strict: options.strict, + browserSkipped: browser.skipped, lint, runtime, layout, @@ -1537,6 +1539,7 @@ function emptyBrowserResult(): CheckBrowserResult { contrastPassed: 0, screenshots: [], timings: { launchSettleMs: 0, seekLoopMs: 0, contrastMs: 0 }, + skipped: true, }; } diff --git a/packages/cli/src/utils/checkTypes.ts b/packages/cli/src/utils/checkTypes.ts index 0b3607b83e6..dc84aaa328e 100644 --- a/packages/cli/src/utils/checkTypes.ts +++ b/packages/cli/src/utils/checkTypes.ts @@ -244,6 +244,13 @@ export interface CheckBrowserResult { contrastPassed: number; screenshots: CheckScreenshot[]; timings: CheckTimings; + /** True when no browser session produced these results — lint blocked the + * run, the lint step itself crashed, or the browser check threw — as opposed + * to a session that ran and simply found nothing. Without this, `layout`/ + * `motion`/`contrast` report the exact same `ok:true`/zero-findings shape + * either way (`runtime` may instead carry a diagnostic finding for the + * crash/throw triggers — it isn't always empty). */ + skipped: boolean; } /** The seek-grid audit loop, injected into checkBrowser so it never imports checkPipeline back. */ @@ -264,6 +271,10 @@ export interface CheckSection { export interface CheckReport { ok: boolean; strict: boolean; + /** Mirrors `CheckBrowserResult.skipped` — true when `runtime`/`layout`/ + * `motion`/`contrast` below reflect no browser session having run, not a + * session that ran and found nothing. */ + browserSkipped: boolean; lint: CheckSection & { filesScanned: number }; runtime: CheckSection; layout: CheckSection & { diff --git a/skills/hyperframes-cli/references/lint-validate-inspect.md b/skills/hyperframes-cli/references/lint-validate-inspect.md index fb814d67f0f..28db0f31ca6 100644 --- a/skills/hyperframes-cli/references/lint-validate-inspect.md +++ b/skills/hyperframes-cli/references/lint-validate-inspect.md @@ -30,7 +30,7 @@ Lints `index.html` and all files in `compositions/`. Reports errors (must fix), ```bash npx hyperframes check # current directory: the full browser gate npx hyperframes check ./my-project # specific project -npx hyperframes check --json # agent-readable envelope {ok, lint, runtime, layout, motion, contrast, hdr, snapshots} +npx hyperframes check --json # agent-readable envelope {ok, browserSkipped, lint, runtime, layout, motion, contrast, hdr, snapshots} npx hyperframes check --snapshots # also write overview frames (annotated) + per-finding crops npx hyperframes check --samples 15 # denser timeline sweep (default 9) npx hyperframes check --at 1.5,4,7.25 # explicit hero-frame timestamps @@ -41,7 +41,7 @@ npx hyperframes check --no-contrast # skip the WCAG audit while iterating npx hyperframes check --strict # exit non-zero on warnings too (default: only errors) ``` -One command, one Chrome boot. `check` runs the linter first and skips the browser entirely when lint reports errors. Then it loads the bundled composition once, wires runtime listeners before navigation, and sweeps one seek grid running every audit per sample: +One command, one Chrome boot. `check` runs the linter first and skips the browser entirely when lint reports errors. When that happens (or the browser session fails to launch), `layout`/`motion`/`contrast` report the same clean `ok:true`/zero-findings shape a genuinely passing session would (`runtime` may too, unless the failure itself left a diagnostic finding there) — check the top-level `browserSkipped` field, not just those sections, before trusting a "clean" result. Otherwise it loads the bundled composition once, wires runtime listeners before navigation, and sweeps one seek grid running every audit per sample: - **Runtime**: JavaScript console errors, unhandled exceptions, failed network requests (media-file `ERR_ABORTED` filtered out), HTTP 4xx/5xx. - **Layout**: text extending outside its container or the canvas, text clipped by its own box, held text overlaps and occlusion (with an approximate covered fraction), children escaping clipping containers. From c30f2e7bcc409990c21865cac3542533a1b69d92 Mon Sep 17 00:00:00 2001 From: miga-heygen Date: Tue, 15 Sep 2026 03:04:23 +0000 Subject: [PATCH 2/5] docs(cli): document browserSkipped in the check --json envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Miguel Ángel --- docs/packages/cli.mdx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index c242d059f81..8555b106df1 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -662,17 +662,22 @@ did, in one command and one browser session. ```bash npx hyperframes check [dir] -npx hyperframes check [dir] --json # {ok, lint, runtime, layout, motion, contrast, snapshots} +npx hyperframes check [dir] --json # {ok, browserSkipped, lint, runtime, layout, motion, contrast, snapshots} npx hyperframes check [dir] --snapshots npx hyperframes check [dir] --at 1.5,4,7.25 npx hyperframes check [dir] --strict # warnings fail too ``` -`check` runs the linter first and skips the browser entirely on a lint error. -Then it loads the bundled composition once and sweeps a single seek grid, -running every audit at every sample: runtime console errors and failed requests, -layout defects (overflow, clipping, held overlaps, occlusion, coordinate-frame -drift), `*.motion.json` assertions, and WCAG AA contrast. +`check` runs the linter first and skips the browser entirely when lint reports +errors. When that happens (or the browser session fails to launch), +`layout`/`motion`/`contrast` report the same clean `ok:true`/zero-findings shape +a genuinely passing session would (`runtime` may too, unless the failure itself +left a diagnostic finding there) — check the top-level `browserSkipped` field, +not just those sections, before trusting a "clean" result. Otherwise it loads +the bundled composition once and sweeps a single seek grid, running every audit +at every sample: runtime console errors and failed requests, layout defects +(overflow, clipping, held overlaps, occlusion, coordinate-frame drift), +`*.motion.json` assertions, and WCAG AA contrast. | Flag | Description | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | From 125c6dbef757cfa504f7637ceff2c1ba3ce97699 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 2 Oct 2026 06:45:57 -0700 Subject: [PATCH 3/5] refactor(cli): trim browserSkipped docs and cover the linter-crash skip path --- docs/packages/cli.mdx | 21 +++++++++---------- packages/cli/src/commands/check.test.ts | 16 +++++++++++--- packages/cli/src/utils/checkTypes.ts | 11 ++-------- .../references/lint-validate-inspect.md | 2 +- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index 8555b106df1..d454219b52b 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -662,22 +662,21 @@ did, in one command and one browser session. ```bash npx hyperframes check [dir] -npx hyperframes check [dir] --json # {ok, browserSkipped, lint, runtime, layout, motion, contrast, snapshots} +npx hyperframes check [dir] --json # {ok, browserSkipped, lint, runtime, layout, motion, contrast, hdr, snapshots} npx hyperframes check [dir] --snapshots npx hyperframes check [dir] --at 1.5,4,7.25 npx hyperframes check [dir] --strict # warnings fail too ``` -`check` runs the linter first and skips the browser entirely when lint reports -errors. When that happens (or the browser session fails to launch), -`layout`/`motion`/`contrast` report the same clean `ok:true`/zero-findings shape -a genuinely passing session would (`runtime` may too, unless the failure itself -left a diagnostic finding there) — check the top-level `browserSkipped` field, -not just those sections, before trusting a "clean" result. Otherwise it loads -the bundled composition once and sweeps a single seek grid, running every audit -at every sample: runtime console errors and failed requests, layout defects -(overflow, clipping, held overlaps, occlusion, coordinate-frame drift), -`*.motion.json` assertions, and WCAG AA contrast. +`check` runs the linter first and skips the browser entirely on a lint error. +Then it loads the bundled composition once and sweeps a single seek grid, +running every audit at every sample: runtime console errors and failed requests, +layout defects (overflow, clipping, held overlaps, occlusion, coordinate-frame +drift), `*.motion.json` assertions, and WCAG AA contrast. + +When the browser never ran (lint errors, a linter crash, or a browser launch +failure), `browserSkipped` is `true` and the `layout`, `motion` and `contrast` +sections are empty, not clean. | Flag | Description | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | diff --git a/packages/cli/src/commands/check.test.ts b/packages/cli/src/commands/check.test.ts index b3a0bb9d84f..00387030065 100644 --- a/packages/cli/src/commands/check.test.ts +++ b/packages/cli/src/commands/check.test.ts @@ -1071,9 +1071,7 @@ describe("check pipeline", () => { expect(checkExitCode(report)).toBe(1); expect(report.lint.findings).toHaveLength(1); expect(browser).not.toHaveBeenCalled(); - // PRINFRA-700: runtime/layout/motion/contrast report the same ok:true/ - // zero-findings shape whether the browser ran clean or never launched at - // all — browserSkipped is the only thing that tells the two apart. + // The browser sections look clean either way; only browserSkipped tells them apart. expect(report.browserSkipped).toBe(true); expect(report.runtime).toMatchObject({ ok: true, errorCount: 0, findings: [] }); expect(report.layout).toMatchObject({ ok: true, errorCount: 0, findings: [], duration: 0 }); @@ -1081,6 +1079,18 @@ describe("check pipeline", () => { expect(report.contrast).toMatchObject({ ok: true, errorCount: 0, findings: [] }); }); + it("marks browserSkipped true when the linter itself crashes", async () => { + const { deps } = dependencies(fakeDriver()); + deps.lintProject = vi.fn(async () => { + throw new Error("unreadable index.html"); + }); + const report = await runCheckPipeline(PROJECT, DEFAULT_CHECK_OPTIONS, deps); + + expect(report.ok).toBe(false); + expect(report.browserSkipped).toBe(true); + expect(report.runtime.findings[0]?.code).toBe("check_lint_failure"); + }); + it("marks browserSkipped false once a browser session actually runs", async () => { const { report } = await runScenario(fakeDriver()); expect(report.browserSkipped).toBe(false); diff --git a/packages/cli/src/utils/checkTypes.ts b/packages/cli/src/utils/checkTypes.ts index dc84aaa328e..50e349c82fe 100644 --- a/packages/cli/src/utils/checkTypes.ts +++ b/packages/cli/src/utils/checkTypes.ts @@ -244,12 +244,7 @@ export interface CheckBrowserResult { contrastPassed: number; screenshots: CheckScreenshot[]; timings: CheckTimings; - /** True when no browser session produced these results — lint blocked the - * run, the lint step itself crashed, or the browser check threw — as opposed - * to a session that ran and simply found nothing. Without this, `layout`/ - * `motion`/`contrast` report the exact same `ok:true`/zero-findings shape - * either way (`runtime` may instead carry a diagnostic finding for the - * crash/throw triggers — it isn't always empty). */ + /** True when no browser session ran (lint blocked or crashed, or the browser check threw). */ skipped: boolean; } @@ -271,9 +266,7 @@ export interface CheckSection { export interface CheckReport { ok: boolean; strict: boolean; - /** Mirrors `CheckBrowserResult.skipped` — true when `runtime`/`layout`/ - * `motion`/`contrast` below reflect no browser session having run, not a - * session that ran and found nothing. */ + /** True when the browser sections below are empty because no session ran, not because it found nothing. */ browserSkipped: boolean; lint: CheckSection & { filesScanned: number }; runtime: CheckSection; diff --git a/skills/hyperframes-cli/references/lint-validate-inspect.md b/skills/hyperframes-cli/references/lint-validate-inspect.md index 28db0f31ca6..f866ae5c30f 100644 --- a/skills/hyperframes-cli/references/lint-validate-inspect.md +++ b/skills/hyperframes-cli/references/lint-validate-inspect.md @@ -41,7 +41,7 @@ npx hyperframes check --no-contrast # skip the WCAG audit while iterating npx hyperframes check --strict # exit non-zero on warnings too (default: only errors) ``` -One command, one Chrome boot. `check` runs the linter first and skips the browser entirely when lint reports errors. When that happens (or the browser session fails to launch), `layout`/`motion`/`contrast` report the same clean `ok:true`/zero-findings shape a genuinely passing session would (`runtime` may too, unless the failure itself left a diagnostic finding there) — check the top-level `browserSkipped` field, not just those sections, before trusting a "clean" result. Otherwise it loads the bundled composition once, wires runtime listeners before navigation, and sweeps one seek grid running every audit per sample: +One command, one Chrome boot. `check` runs the linter first and skips the browser entirely when lint reports errors. When the browser never ran (lint errors, a linter crash, or a browser launch failure), `browserSkipped` is `true` and the `layout`, `motion` and `contrast` sections are empty, not clean. Otherwise it loads the bundled composition once, wires runtime listeners before navigation, and sweeps one seek grid running every audit per sample: - **Runtime**: JavaScript console errors, unhandled exceptions, failed network requests (media-file `ERR_ABORTED` filtered out), HTTP 4xx/5xx. - **Layout**: text extending outside its container or the canvas, text clipped by its own box, held text overlaps and occlusion (with an approximate covered fraction), children escaping clipping containers. From a5ead55c22b0cfbf437ee88508542a2f2eec44f9 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 2 Oct 2026 06:49:56 -0700 Subject: [PATCH 4/5] chore(skills): refresh the hyperframes-cli manifest hash --- skills-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills-manifest.json b/skills-manifest.json index 8f68b27febc..6a89e8540e6 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -30,7 +30,7 @@ "files": 7 }, "hyperframes-cli": { - "hash": "c1b0b3d02ad38edc", + "hash": "0103a548049ffd4e", "files": 11 }, "hyperframes-core": { From 8378f03e0d34d7552ac5405befd2cdaef6983a2e Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 2 Oct 2026 06:54:02 -0700 Subject: [PATCH 5/5] refactor(cli): drop the browserSkipped type comments to keep the comment share flat --- packages/cli/src/utils/checkTypes.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cli/src/utils/checkTypes.ts b/packages/cli/src/utils/checkTypes.ts index 50e349c82fe..bc61a61912a 100644 --- a/packages/cli/src/utils/checkTypes.ts +++ b/packages/cli/src/utils/checkTypes.ts @@ -244,7 +244,6 @@ export interface CheckBrowserResult { contrastPassed: number; screenshots: CheckScreenshot[]; timings: CheckTimings; - /** True when no browser session ran (lint blocked or crashed, or the browser check threw). */ skipped: boolean; } @@ -266,7 +265,6 @@ export interface CheckSection { export interface CheckReport { ok: boolean; strict: boolean; - /** True when the browser sections below are empty because no session ran, not because it found nothing. */ browserSkipped: boolean; lint: CheckSection & { filesScanned: number }; runtime: CheckSection;