The `hyperframes feedback` convention only prompted for a free-text
`--comment` "with the failing composition pattern and what you tried".
Agents dutifully filed vague reports (blank CJK text, mid-run exit, 4K
timeout) with no error string, no failure-mode, and — critically — no
published composition, so none could be reproduced or root-caused.
Two additions to the CLI skill:
- Lead bug reports with `--file-issue` (+ `--dir`), which publishes a
minimal repro of the project to a public URL. A comment alone almost
never lets a maintainer reproduce; the composition is what does.
- Give the `--comment` a concrete bug checklist: exact error string
verbatim + whether output was produced / fell back / hard-exited; the
isolated trigger; exact command + HF_*/PRODUCER_* env; frame/timestamp +
visual defect. Drop the "repeat env" ask (the CLI already attaches it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolveVideoCaptureBeyondViewport gated Chrome's beyond-viewport screenshot
path to hardware-GPU captures, to skip the full-surface software
re-rasterization tax. But without beyond-viewport, the viewport-bound
capture clips the bottom edge of any frame containing a native video
surface (the same #1094 tall-portrait guard the alpha capture paths already
hardcode) — leaving ~87 bottom rows black.
This hit two cohorts: software-GPU macOS/Linux hosts, and — worse — EVERY
distributed chunk render, which hardcodes browserGpuMode "software", so the
whole distributed fleet shipped video renders with a black bottom band.
Reporter confirmed forcing resolveVideoCaptureBeyondViewport=true fixes it.
Correct output wins over the software perf optimization: enable
beyond-viewport for any render with a native video surface, regardless of
GPU mode. Drops the now-vestigial browserGpuMode parameter (and its type)
and updates both call sites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Stack
1. **#2298 — DE router stall watchdog** ← you are here
2. #2300 — video bottom-edge clip
3. #2301 — feedback bug-report guidance
## Problem
The DE parallel router auto-enables the interleaved parallel-**streaming** capture for the ≥24 GB macOS trial cohort. If a worker wedges mid-capture (a hung seek/screenshot at an early frame), the render makes **zero frame progress** yet sits until the per-frame CDP `protocolTimeout` (~5 min) fires before the pinned self-verify fallback can run — a silent multi-minute hang shipped to real users.
Reported: stuck at frame 2/2031 for 6+ min, no fallback, until the user manually set `HF_DE_PARALLEL_ROUTER=false` (71 s clean).
## Fix
Add a no-frame-progress watchdog to the parallel branch of `runCaptureStreamingStage`:
- Ticks off `executeParallelCapture`'s progress callback. If no **new** frame lands within `HF_DE_PARALLEL_STALL_MS` (default **60 s** — well under the 5-min protocol timeout, ≫ the 15–32 ms/frame budget), it fires.
- On trip: aborts the **reorder buffer** (so peer workers parked in `waitForFrame` reject instead of deadlocking the `Promise.all` pool) and aborts the pool via a **separate** `AbortController` linked to the parent abort.
- The parent `abortSignal` stays un-aborted, so the orchestrator reads the failure as a generic `capture_error` (not a cancellation) and re-renders on the pinned screenshot path — the same fallback a verify failure already uses.
## Test
- Watchdog trips on no progress → rethrows a stall error (routes to fallback).
- A genuine parent-abort is **not** relabeled as a stall (stays a cancellation).
The DE parallel router auto-enables the interleaved parallel-streaming
capture for the >=24GB macOS trial cohort. If a worker wedges mid-capture
(a hung seek/screenshot at an early frame) the render made no frame
progress yet sat until the per-frame CDP protocolTimeout (~5 min) fired
before the pinned self-verify fallback could run — a silent multi-minute
hang shipped to real users (report: stuck at frame 2/2031 for 6+ min,
no fallback, until HF_DE_PARALLEL_ROUTER=false).
Add a no-frame-progress watchdog to the parallel branch of
runCaptureStreamingStage. It ticks off executeParallelCapture's progress
callback; if no NEW frame lands within HF_DE_PARALLEL_STALL_MS (default
60s, well under protocolTimeout and >> the 15-32ms/frame budget), it
aborts the reorder buffer (unsticking peer workers parked in waitForFrame
so the pool doesn't deadlock) and aborts the pool via a SEPARATE
controller linked to the parent abort. Because the parent abortSignal
stays un-aborted, the orchestrator reads the failure as a generic
capture_error (not a cancellation) and re-renders on the pinned screenshot
path — the same fallback a verify failure already uses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`hyperframes lambda deploy` runs `sam deploy --resolve-s3`, and SAM's
managed artifacts bucket (aws-sam-cli-managed-default) is created with
default SSE encryption. Setting that requires s3:PutEncryptionConfiguration,
which the generated deploy policy did not grant, so a first deploy by a
user provisioned exactly per `lambda policies user` 403s on the bucket and
the managed stack rolls back.
Add s3:GetEncryptionConfiguration and s3:PutEncryptionConfiguration to the
s3Bucket action set (Get pairs with Put for CloudFormation update/drift
reads, matching the existing Get/Put pairs in the list). Also add a hint to
the sam-deploy failure path pointing at the ROLLBACK_COMPLETE recovery step,
since first-time users hit the stuck-rollback error on their retry.
Fixes#2137
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(studio): timeline collision and placement model
What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.
Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.
How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.
Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).
* feat(studio): timeline magnetic snapping
What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.
Why: the magnet math for clip drags/trims, reviewable standalone.
How: new files only; type-only playerStore imports; consumers land with the
drag engine.
Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.
* feat(studio): multi-clip drag preview math
What: new pure module timelineMultiDragPreview — group-drag passenger
offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds,
multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests.
Why: the group-drag math, standalone and DOM-free.
How: new files only; consumed later by TimelineLanes.
Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit;
fallow audit clean.
* feat(studio): timeline z-stacking sync model
What: new pure module timelineStackingSync — lane order ↔ z-index
reconciliation (laneIsAbove, computeStackingPatches) with tests.
Why: the single source of truth for how timeline lane order maps to canvas
stacking; the ordering rules and tie-breaks live here.
How: new files only; consumed later by timelineZones and the stacking-sync
hook.
Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit;
fallow audit clean.
* feat(studio): timeline lane-zone model
What: new pure module timelineZones — visual/audio track-zone
classification (classifyZone) and normalizeToZones, which re-packs lanes
into zone-consistent rows; tests cover the stacking/zones interaction.
Why: completes the z-model started in the stacking-sync PR.
How: new files; consumes isAudioTimelineElement (leaf-helpers PR) and
computeStackingPatches (stacking-sync PR); type-only playerStore imports.
Test plan: bunx vitest run timelineZones.test.ts; tsc --noEmit; fallow
audit clean.
* feat(studio): asset click policy and canvas nudge gate
What: two small pure modules with tests — assetClickBehavior (click vs
double-click policy for sidebar assets) and canvasNudgeGate (debounce gate
for arrow-key canvas nudges).
Why: policy dependencies of the upcoming asset card and nudge hook,
reviewable as plain decision tables.
How: new files only.
Test plan: bunx vitest run on both test files; tsc --noEmit; fallow audit
clean.
* test(studio): characterization suites for resize commit and razor history
What: two test-only suites pinning CURRENT behavior before the NLE swap:
anchoredResizeReleaseShift.test.ts (manual-offset resize release commits)
and useRazorSplit.history.test.tsx (razor split undo/redo history).
Why: regression tripwires — the later glue-swap PRs must keep these green.
How: test files only; they import existing main modules unchanged and pass
against them as-is.
Test plan: bunx vitest run on both suites; fallow audit clean.
* feat(studio): canvas context menu and z-order actions (unwired)
What: CanvasContextMenu (right-click menu for canvas selections) and
canvasContextMenuZOrder (tie-aware bring-forward/send-backward z-order patch
computation) with its test suite. Shipped unwired.
Why: the z-order rules are the substance; mounting is one line in the later
overlay swap.
How: new files, compiled against current main. Nothing mounts the menu yet,
so .fallowrc.jsonc gains TEMP(studio-dnd) entries (entry registration +
ignoreExports) — removed by the app-shell swap PR that wires everything.
Test plan: bunx vitest run canvasContextMenuZOrder.test.ts; tsc --noEmit;
fallow audit clean.
---------
Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
* feat(studio): timeline collision and placement model
What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.
Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.
How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.
Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).
* feat(studio): timeline magnetic snapping
What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.
Why: the magnet math for clip drags/trims, reviewable standalone.
How: new files only; type-only playerStore imports; consumers land with the
drag engine.
Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.
* feat(studio): multi-clip drag preview math
What: new pure module timelineMultiDragPreview — group-drag passenger
offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds,
multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests.
Why: the group-drag math, standalone and DOM-free.
How: new files only; consumed later by TimelineLanes.
Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit;
fallow audit clean.
* feat(studio): timeline z-stacking sync model
What: new pure module timelineStackingSync — lane order ↔ z-index
reconciliation (laneIsAbove, computeStackingPatches) with tests.
Why: the single source of truth for how timeline lane order maps to canvas
stacking; the ordering rules and tie-breaks live here.
How: new files only; consumed later by timelineZones and the stacking-sync
hook.
Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit;
fallow audit clean.
* feat(studio): timeline lane-zone model
What: new pure module timelineZones — visual/audio track-zone
classification (classifyZone) and normalizeToZones, which re-packs lanes
into zone-consistent rows; tests cover the stacking/zones interaction.
Why: completes the z-model started in the stacking-sync PR.
How: new files; consumes isAudioTimelineElement (leaf-helpers PR) and
computeStackingPatches (stacking-sync PR); type-only playerStore imports.
Test plan: bunx vitest run timelineZones.test.ts; tsc --noEmit; fallow
audit clean.
* feat(studio): asset click policy and canvas nudge gate
What: two small pure modules with tests — assetClickBehavior (click vs
double-click policy for sidebar assets) and canvasNudgeGate (debounce gate
for arrow-key canvas nudges).
Why: policy dependencies of the upcoming asset card and nudge hook,
reviewable as plain decision tables.
How: new files only.
Test plan: bunx vitest run on both test files; tsc --noEmit; fallow audit
clean.
* test(studio): characterization suites for resize commit and razor history
What: two test-only suites pinning CURRENT behavior before the NLE swap:
anchoredResizeReleaseShift.test.ts (manual-offset resize release commits)
and useRazorSplit.history.test.tsx (razor split undo/redo history).
Why: regression tripwires — the later glue-swap PRs must keep these green.
How: test files only; they import existing main modules unchanged and pass
against them as-is.
Test plan: bunx vitest run on both suites; fallow audit clean.
---------
Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
* feat(studio): timeline collision and placement model
What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.
Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.
How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.
Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).
* feat(studio): timeline magnetic snapping
What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.
Why: the magnet math for clip drags/trims, reviewable standalone.
How: new files only; type-only playerStore imports; consumers land with the
drag engine.
Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.
* feat(studio): multi-clip drag preview math
What: new pure module timelineMultiDragPreview — group-drag passenger
offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds,
multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests.
Why: the group-drag math, standalone and DOM-free.
How: new files only; consumed later by TimelineLanes.
Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit;
fallow audit clean.
* feat(studio): timeline z-stacking sync model
What: new pure module timelineStackingSync — lane order ↔ z-index
reconciliation (laneIsAbove, computeStackingPatches) with tests.
Why: the single source of truth for how timeline lane order maps to canvas
stacking; the ordering rules and tie-breaks live here.
How: new files only; consumed later by timelineZones and the stacking-sync
hook.
Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit;
fallow audit clean.
* feat(studio): timeline lane-zone model
What: new pure module timelineZones — visual/audio track-zone
classification (classifyZone) and normalizeToZones, which re-packs lanes
into zone-consistent rows; tests cover the stacking/zones interaction.
Why: completes the z-model started in the stacking-sync PR.
How: new files; consumes isAudioTimelineElement (leaf-helpers PR) and
computeStackingPatches (stacking-sync PR); type-only playerStore imports.
Test plan: bunx vitest run timelineZones.test.ts; tsc --noEmit; fallow
audit clean.
* feat(studio): asset click policy and canvas nudge gate
What: two small pure modules with tests — assetClickBehavior (click vs
double-click policy for sidebar assets) and canvasNudgeGate (debounce gate
for arrow-key canvas nudges).
Why: policy dependencies of the upcoming asset card and nudge hook,
reviewable as plain decision tables.
How: new files only.
Test plan: bunx vitest run on both test files; tsc --noEmit; fallow audit
clean.
---------
Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
* feat(studio): timeline collision and placement model
What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.
Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.
How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.
Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).
* feat(studio): timeline magnetic snapping
What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.
Why: the magnet math for clip drags/trims, reviewable standalone.
How: new files only; type-only playerStore imports; consumers land with the
drag engine.
Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.
* feat(studio): multi-clip drag preview math
What: new pure module timelineMultiDragPreview — group-drag passenger
offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds,
multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests.
Why: the group-drag math, standalone and DOM-free.
How: new files only; consumed later by TimelineLanes.
Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit;
fallow audit clean.
* feat(studio): timeline z-stacking sync model
What: new pure module timelineStackingSync — lane order ↔ z-index
reconciliation (laneIsAbove, computeStackingPatches) with tests.
Why: the single source of truth for how timeline lane order maps to canvas
stacking; the ordering rules and tie-breaks live here.
How: new files only; consumed later by timelineZones and the stacking-sync
hook.
Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit;
fallow audit clean.
* feat(studio): timeline lane-zone model
What: new pure module timelineZones — visual/audio track-zone
classification (classifyZone) and normalizeToZones, which re-packs lanes
into zone-consistent rows; tests cover the stacking/zones interaction.
Why: completes the z-model started in the stacking-sync PR.
How: new files; consumes isAudioTimelineElement (leaf-helpers PR) and
computeStackingPatches (stacking-sync PR); type-only playerStore imports.
Test plan: bunx vitest run timelineZones.test.ts; tsc --noEmit; fallow
audit clean.
---------
Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
* feat(studio): timeline collision and placement model
What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.
Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.
How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.
Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).
* feat(studio): timeline magnetic snapping
What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.
Why: the magnet math for clip drags/trims, reviewable standalone.
How: new files only; type-only playerStore imports; consumers land with the
drag engine.
Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.
* feat(studio): multi-clip drag preview math
What: new pure module timelineMultiDragPreview — group-drag passenger
offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds,
multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests.
Why: the group-drag math, standalone and DOM-free.
How: new files only; consumed later by TimelineLanes.
Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit;
fallow audit clean.
* feat(studio): timeline z-stacking sync model
What: new pure module timelineStackingSync — lane order ↔ z-index
reconciliation (laneIsAbove, computeStackingPatches) with tests.
Why: the single source of truth for how timeline lane order maps to canvas
stacking; the ordering rules and tie-breaks live here.
How: new files only; consumed later by timelineZones and the stacking-sync
hook.
Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit;
fallow audit clean.
---------
Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
* feat(studio): timeline collision and placement model
What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.
Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.
How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.
Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).
* feat(studio): timeline magnetic snapping
What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.
Why: the magnet math for clip drags/trims, reviewable standalone.
How: new files only; type-only playerStore imports; consumers land with the
drag engine.
Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.
---------
Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.
Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.
How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.
Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).
What: the studio-server files route at its final NLE-stack form, with its
test suite (25 tests).
Why: standalone package seam — the server-side dependency of the studio
asset workflow, reviewable in isolation.
How: additive route behavior; existing route consumers unchanged.
Test plan: bunx vitest run src/routes/files.test.ts in packages/studio-server;
tsc --noEmit in packages/studio-server; fallow audit clean.
* feat(studio): timeline leaf helpers — audio inspector, zoom math, UI prefs
What: extends three leaf modules to their final NLE-stack form, tests in the
same change: timelineInspector (isAudioTimelineElement, resolveBeatSourceTrack),
timelineZoom (zoom/pps math incl. computePinnedZoomPercent), and
studioUiPreferences (persisted editor prefs).
Why: leaf dependencies of the NLE timeline stack; landing them first keeps
the later glue PRs to wiring.
How: additive from the consumer side — every export main already uses is
unchanged (typecheck against main's consumers passes untouched); every new
export is exercised by a test in this PR.
Test plan: bunx vitest run on the three test files; tsc --noEmit in
packages/studio; fallow audit --base origin/main clean.
* feat(studio): seek-restore contract for player reloads
What: useTimelineSyncCallbacks gains resolveReloadSeekTime and revealIframe —
the pure contract for where the playhead lands after a preview reload
(pending seek > deep-link seek > store playhead, clamped) and for undoing
refreshPlayer's iframe hide. Test suite included.
Why: the NLE editor reloads the preview on every committed edit; this
contract is the difference between "playhead restores" and "jumps to 0".
How: additive exports on an existing hook file; main's consumers unchanged.
Test plan: bunx vitest run useTimelineSyncCallbacks.test.ts; tsc --noEmit in
packages/studio; fallow audit clean.
What: extends three leaf modules to their final NLE-stack form, tests in the
same change: timelineInspector (isAudioTimelineElement, resolveBeatSourceTrack),
timelineZoom (zoom/pps math incl. computePinnedZoomPercent), and
studioUiPreferences (persisted editor prefs).
Why: leaf dependencies of the NLE timeline stack; landing them first keeps
the later glue PRs to wiring.
How: additive from the consumer side — every export main already uses is
unchanged (typecheck against main's consumers passes untouched); every new
export is exercised by a test in this PR.
Test plan: bunx vitest run on the three test files; tsc --noEmit in
packages/studio; fallow audit --base origin/main clean.
Addresses review on #2266: the gradient-text exclusion was too broad — any
background-clip:text skipped the invisible-text check, so a broken/missing
gradient (clip:text with no image and a transparent background, which paints
nothing) went unreported. Now exclude only when a real background fills the
glyphs (background-image != none, or an opaque background-color). Expands the
test suite to the reviewer's full case set: direct transparent fill, inherited
transparent fill over an opaque child color, color:transparent fallback, opaque
baseline, gradient-over-real-background exclusion, broken-gradient still flagged,
and empty-text no-op.
The layout-audit test mock returns computed styles as plain camelCase
properties (no getPropertyValue), so the invisible-text detector threw
'cs.getPropertyValue is not a function' and broke the whole audit in CI. Read
webkitTextFillColor/webkitBackgroundClip/backgroundClip by property to match
the rest of the script (works in a real browser too). Adds tests: flags
transparent -webkit-text-fill-color, ignores opaque color, ignores gradient
text (background-clip:text).
Wild report (5th in cluster, CLI 0.7.53): snapshots omitted all text while
check passed — text painting with a transparent -webkit-text-fill-color (which
overrides `color` for the glyph fill AND inherits, so a parent's transparent
fill silently blanks descendant text that has its own opaque `color`) renders
invisible, but every geometry/occlusion/contrast audit missed it. Contrast in
particular reads `color`, not the fill that actually paints, so white-`color`
+ transparent-fill text scored as high-contrast and passed.
Add an invisible-text detector to the layout audit: flag any text element whose
effective fill (computed -webkit-text-fill-color, which already resolves to
`color` when unset) is transparent. Gradient/clipped text (background-clip:text)
legitimately uses a transparent fill and is excluded. Verified: check now fails
on an inherited-transparent-fill fixture (text_not_painted) while gradient text,
body-inherited color, sub-composition color, and real registry examples stay
clean.
Addresses review on #2264: the localization helper had one broad catch around
both dynamic producer resolution and injector execution, so it couldn't tell a
benign 'producer not in this environment' from a real injector/fetch failure,
and emitted no diagnostic. Split into loadFontInjector() (returns null when the
module is absent — silent fail-open) and localizeWithProducer() (warns ONCE per
distinct message when the injector itself throws, then fails open). Per-family
resolution failures remain the injector's own responsibility (producer's
warnUnresolvedFonts). The localizer seam is injectable; tests now cover success,
producer-unavailable, injector-throw, warn dedup, and call-site integration.
The CLI test job builds with --filter '!@hyperframes/producer', and render.ts
imports producer only as a type — so a static import("@hyperframes/producer")
in the font-localization helper failed Vitest's transform-time module
resolution ("Failed to resolve entry for package"), breaking checkBrowser
tests and the helper's own test. Keep the specifier out of the static module
graph (@vite-ignore + variable specifier) so it resolves at runtime only:
production/installed CLI has producer in node_modules and localizes fonts;
the test env fail-opens to the plain bundle. Localizer is now injectable so
the helper's unit tests cover it without needing producer resolvable.