Skip to content

Tier 3: deferred schema features, presets, e2e tests, audio + animation#3

Merged
zeidalidiez merged 26 commits into
mainfrom
tier/3-polish
Jun 18, 2026
Merged

Tier 3: deferred schema features, presets, e2e tests, audio + animation#3
zeidalidiez merged 26 commits into
mainfrom
tier/3-polish

Conversation

@zeidalidiez

@zeidalidiez zeidalidiez commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Tier 3: polish, deferred features, Phase 2 (audio + animation)

Closes the deferred work from tier 2, adds engine-aware UI Builder
presets, layers e2e tests on top of the unit suite, and ships the
two Phase 2 features the v1 spec calls out (audio + animated
sprite sheets).

In this PR

  1. Wire up glow, pattern fills, image fills in UI Builder (3f686bb)
    The schema's full FillSpec (solid color / pattern / image
    fill) and effects.glow were deferred at the end of tier 2.
    Both the canvas renderer and the property panel now handle all
    three fill kinds plus a glow blur/color editor.

  2. Manifest report viewer in the web UI (bee2730)
    After Generate & Download, the web UI now reads the
    _placeholderer/manifest-report.json entry out of the produced
    ZIP (no JSZip dependency — walks the central directory by hand)
    and renders a small panel showing job name, asset counts, and
    inline-code tags for every created folder and file.

  3. Engine-aware UI Builder presets (59f05e6)
    The five generic placeholder presets in the UI Builder become a
    small engine-aware library: Godot dialog window, Unity health
    bar, Unreal crosshair, plus common button/panel/title/divider.
    New apps/web/src/builderLayerFactories.ts is the single source
    of truth for layer construction.

  4. Playwright e2e tests for the web app (18446f4)
    Two smoke tests via pnpm e2e: import a starter manifest,
    assert the Overview opens, click Generate, capture the download
    with the right filename, and verify the manifest report
    panel renders. A second test asserts the theme toggle changes
    data-theme on <html>. The CI workflow now installs
    Playwright's chromium and runs the suite.

  5. CLI e2e test that exercises the full generate flow (3f0e846)
    Vitest in apps/cli/tests/e2e.test.ts that drives
    generateJob against the real @napi-rs/canvas backend,
    re-opens the produced ZIP with JSZip, and asserts every
    requested asset is present, declared empty folders
    materialize as .gitkeep, and the manifest report parses
    with the right counts and zero errors. The suite skips itself
    automatically when the native binary is unavailable.

  6. Audio placeholder generation (WAV sine wave) (b7c00b3)
    Per the v1 spec's Phase 2 list: a new audio asset kind
    that produces a 16-bit PCM mono WAV file containing a sine wave
    at the configured frequency with a 10ms attack/release envelope
    to avoid clicks. New schema (AudioAsset with frequency,
    duration, optional sample_rate and amplitude), new
    packages/core/src/audio.ts (encodeWav + synthesizeTone),
    and a CLI e2e test that opens the WAV and asserts the
    RIFF/WAVE header.

  7. Animated sprite sheet sidecar (animation.json) (8dc874b)
    When a sprite_sheet asset declares frame_duration_ms, the
    generator writes a sidecar <name>.animation.json next to the
    sheet with the timing data a runtime needs (sheet path, frame
    size, rows, columns, frame_count, frame_duration_ms, fps,
    total_duration_ms). The fps is derived as round(1000 / frame_duration_ms).

Verification

  • pnpm -r build is green
  • pnpm test → 33 unit + 4 CLI e2e = 37 tests passing
  • pnpm e2e (Playwright) is wired but requires npx playwright install
    on the runner; CI installs it automatically.
  • The validateManifest / validateBuilderRecipe AJV pipeline
    picks up the schema changes (audio kind, frame_duration_ms
    field, looser baseAsset.format) and the 33 unit tests still
    pass.

What's left for a future tier

  • Light/dark theming for the UI Builder's properties panel
    (it has its own inline color literals that the tier 2 theming
    pass didn't migrate because we hadn't landed the new UIBuilder
    yet).
  • Real engine-specific export helpers (the spec's "deeper engine
    export helpers" Phase 2 item).
  • More engine-aware UI Builder presets.

Greptile Summary

  • Adds Tier 3 placeholder generation features, including WAV audio assets and animated sprite-sheet sidecar metadata.
  • Expands the UI Builder with engine-aware presets, shared layer factories, richer fills, glow effects, and SVG/export rendering updates.
  • Adds manifest-report extraction/display in the web app after ZIP generation.
  • Adds CLI and Playwright end-to-end coverage and updates CI to run the new checks.

Confidence Score: 5/5

The changes appear safe to merge based on the reviewed implementation and accompanying test coverage.

No blocking issues were identified in the finalized review, and the PR adds targeted unit, CLI, and browser coverage for the new generation and web UI behavior.

T-Rex T-Rex Logs

What T-Rex did

  • Compared the base UI Builder state to understand the starting point.
  • Implemented engine-aware grouped presets and expanded the property panel with Fill mode, Pattern/Image selections, Image src/mode fields, and Glow controls.
  • Generated a manifest and verified the manifest report panel is shown after generation.
  • Verified the manifest-related tests pass; head Playwright suite reported 3 passed tests.
  • Validated validation state for asset generation; before was invalid with missing assets, after is valid with audio and animation assets included in the ZIP and a manifest summary.
  • Ran end-to-end CLI tests; reported 2 passed files and 21 passed tests.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (28)

  1. apps/web/src/builderRender.ts, line 322-323 (link)

    P1 Render filled-shape fills

    The property panel can set pattern and image fills on any filled layer, but filled-shape still uses fillToColor, which drops non-string fills to the fallback color. A rounded shape configured with a pattern or image fill silently exports and renders as a plain solid fill instead of the selected fill.

    Rule Used: Please include instructions to add skills or rules... (source)

    Artifacts

    Repro: Playwright harness for real builderRender filled-shape pattern rendering

    • Contains supporting evidence from the run (text/javascript; charset=utf-8).

    Repro: command output with FillSpec and sampled pixel colors

    • Keeps the command output available without making the summary code-heavy.

    ▶ Screen recording from the T-Rex run

    • Shows the flow or interaction that T-Rex exercised.

    Repro Filled Shape Pattern Poster

    • Shows the rendered state that T-Rex checked.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  2. apps/web/src/builderRender.ts, line 359-371 (link)

    P1 Preserve SVG fill modes

    SVG export converts every FillSpec through fillToColor, so pattern and image fills are replaced with the fallback color in exported SVGs. A layer that looks patterned or image-filled on canvas exports as a solid-color SVG. Serialize these fill modes into SVG definitions or block SVG export for unsupported fill modes.

    Rule Used: Please include instructions to add skills or rules... (source)

    Artifacts

    Repro: focused harness invoking real SVG export with pattern and image FillSpec inputs

    • Contains supporting evidence from the run (text/typescript; charset=utf-8).

    Repro: runtime output showing non-solid FillSpec inputs exported as fallback solid fills

    • Keeps the command output available without making the summary code-heavy.

    Repro: generated SVG showing only fallback solid fill attributes

    • Shows the rendered state that T-Rex checked.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  3. apps/web/src/builderRender.ts, line 422-426 (link)

    P2 SVG glow loses color

    The canvas glow uses glow.color, but the SVG filter only applies a Gaussian blur to the source graphic and never uses the configured color. Exported SVGs therefore do not match the builder preview for colored glows; the glow can appear as a blurred copy of the shape instead of the selected halo.

    Artifacts

    Repro: focused SVG glow color harness

    • Contains supporting evidence from the run (text/javascript; charset=utf-8).

    Repro: failing SVG glow color run output

    • Keeps the command output available without making the summary code-heavy.

    Repro: generated SVG missing configured glow color

    • Shows the rendered state that T-Rex checked.

    Repro Glow Comparison

    • Shows the rendered state that T-Rex checked.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  4. apps/web/src/builderRender.ts, line 152-155 (link)

    P2 Compose layer effects

    When a layer has both shadow and glow, both helpers write the same canvas shadow properties. applyGlow runs second and overwrites the shadow blur, offsets, and color, so the shadow disappears. The schema allows both effects, and the UI can add glow to an existing shadow, so this state produces incorrect rendering.

    Rule Used: Please include instructions to add skills or rules... (source)

    Artifacts

    Repro: Playwright harness rendering the real builder layer with shadow-only and shadow-plus-glow effects

    • Contains supporting evidence from the run (text/javascript; charset=utf-8).

    Repro: Playwright output showing both effects configured and the missing offset shadow sample

    • Keeps the command output available without making the summary code-heavy.

    Compose Layer Effects Shadow Only Vs Shadow Plus Glow

    • Shows the rendered state that T-Rex checked.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  5. apps/web/src/App.tsx, line 123-127 (link)

    P2 Clear stale reports

    manifestReport is only set on a successful generation and is not cleared before a new generation or on failure. After one successful download, a later failed generation leaves the previous report panel visible, so the UI can show old files and counts for a job that did not generate successfully.

    Rule Used: Please include instructions to add skills or rules... (source)

    Fix in Claude Code Fix in Codex

  6. General comment

    P1 Audio assets still inherit required image dimensions, so valid audio manifests are rejected

    • Bug
      • The head revision's manifest schema includes audioAsset, but validating an audio manifest with kind: "audio", format: "wav", frequency 440, duration 0.25, sample_rate 22050, and amplitude 0.5 returns valid: false. The validator reports missing width and height for the audio asset and oneOf failure, so callers cannot validate and generate an audio asset through the manifest contract. This blocks the claimed behavior before generateJob can emit sfx/beep.wav.
    • Cause
      • packages/schemas/src/manifest.schema.json defines audioAsset with allOf: [{ "$ref": "#/definitions/baseAsset" }, ...], while baseAsset.required includes width and height. Audio assets therefore inherit image-only required fields.
    • Fix
      • Split shared base fields from image dimension fields, or introduce an audio-specific base schema whose required fields are only kind, name, format, and output_path plus audio-required fields. Keep width/height required only for image-like assets.

    T-Rex Ran code and verified through T-Rex

  7. General comment

    P1 Stale pnpm lockfile prevents CI from installing Playwright for the new e2e job

    • Bug
      • The PR adds @playwright/test to the root package and wires CI to run pnpm install --frozen-lockfile, install Chromium, then pnpm e2e, but the lockfile was not updated for the new root devDependency. A clean/frozen install fails with ERR_PNPM_OUTDATED_LOCKFILE, and the Playwright binary is unavailable, so pnpm e2e cannot run in the validated head checkout.
    • Cause
      • Root package.json now declares @playwright/test, but pnpm-lock.yaml still has root specifiers matching only typescript, so frozen installs reject the dependency graph before Playwright can be installed.
    • Fix
      • Run pnpm install with the intended pnpm version and commit the updated pnpm-lock.yaml entries for @playwright/test/Playwright. Then verify from a clean checkout with pnpm install --frozen-lockfile, pnpm exec playwright test --list, and pnpm e2e.

    T-Rex Ran code and verified through T-Rex

  8. apps/web/src/builderRender.ts, line 320-332 (link)

    P1 Render rounded fills

    The new fill modes are exposed for every selected layer, including the built-in Rounded layer, but this renderer still resolves filled-shape fills with fillToColor. When a Rounded layer is set to Pattern or Image, the canvas and PNG/JPG export keep rendering the fallback solid color instead of the selected fill.

    Rule Used: Please include instructions to add skills or rules... (source)

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  9. General comment

    P1 Manifest report panel never renders after generated ZIP download

    • Bug
      • The head browser flow imports the starter manifest, opens Job Overview, clicks Generate & Download ZIP, receives the expected e2e_smoke.zip download, then waits for the Manifest report panel. The panel never appears and the Playwright proof times out waiting for strong with text Manifest report. This violates the PR contract item that the web UI reads _placeholderer/manifest-report.json from the produced ZIP and renders job/count/file/folder details.
    • Cause
      • The web ZIP reader in apps/web/src/App.tsx only reads the manifest report when the local file header reports compression method 0 (STORE). The generated ZIP is produced by JSZip with zip.generateAsync({ type: 'uint8array' }) in packages/core/src/generate.ts, which does not force STORE for the manifest report. As a result loadManifestReport silently returns without setting manifestReport for the real generated ZIP.
    • Fix
      • Make the producer and reader agree: either generate the manifest report entry with STORE/no compression, or teach the web reader to handle the actual JSZip compression method used for _placeholderer/manifest-report.json. Also update the e2e assertion to check the full report contents so this cannot regress silently.

    T-Rex Ran code and verified through T-Rex

  10. apps/web/src/builderRender.ts, line 320-332 (link)

    P1 Apply rounded fills

    The property panel allows every shape layer to select pattern or image fill, but filled-shape still renders with fillToColor, which falls back to #4A5568 for non-string fills. A rounded shape configured with a pattern or image fill silently renders as a solid color, while rects and circles use the new resolveFill and image overlay paths. Use the same fill resolution here, and clip image overlays to the rounded-rectangle path.

    Coding-agent rule suggestion: add a rule to grep all sibling renderer branches when adding a new union feature like FillSpec, so rect, circle, and filled-shape stay in sync.

    Rule Used: Please include instructions to add skills or rules... (source)

    Artifacts

    Repro: Playwright automation script that drives the real UI Builder and samples the canvas

    • Contains supporting evidence from the run (text/javascript; charset=utf-8).

    Repro: automation output showing rounded shape pattern fill collapsed to solid fallback color

    • Keeps the command output available without making the summary code-heavy.

    Repro Rounded Fill UI full-page view

    • Shows the rendered state that T-Rex checked.

    Repro Rounded Fill UI Poster

    • Shows the rendered state that T-Rex checked.

    ▶ Screen recording from the T-Rex run

    • Shows the flow or interaction that T-Rex exercised.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  11. apps/web/src/builderRender.ts, line 368 (link)

    P2 Export new fills

    SVG export still converts every non-string fill through fillToColor, which returns the fallback color for the new pattern and image fill specs. A layer that previews or exports to PNG with a checkerboard, stripes, or image fill will export to SVG as a solid gray shape instead, so the SVG output no longer matches the builder canvas for the new fill modes.

    Artifacts

    Repro: focused harness that executes the real SVG export path with a checkerboard pattern FillSpec

    • Contains supporting evidence from the run (text/typescript; charset=utf-8).

    Repro: harness output showing the pattern FillSpec exported as fallback fill="#4A5568"

    • Keeps the command output available without making the summary code-heavy.

    Repro: generated SVG artifact containing the solid gray fallback fill

    • Shows the rendered state that T-Rex checked.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  12. General comment

    P1 Manifest validator still rejects audio WAV assets on head

    • Bug
      • The requested head manifest including { kind: 'audio', format: 'wav', frequency, duration, sample_rate } still returns valid: false from validateManifest on head. The errors are the same image/sprite/tileset/ui schema mismatches seen on base, including /requests/0/assets/1/format must be equal to one of the allowed values with only png/jpg/jpeg/webp listed. This means CLI validation can reject manifests that generation now supports, so users going through a validate-first CLI path may not be able to use the new audio feature even though generateJob can produce the WAV.
    • Cause
      • The schema package consumed at runtime by validation did not include the new audioAsset definition/oneOf branch in the executed head flow, despite generation code supporting kind: 'audio'. The after artifact's validateManifest(manifest) output demonstrates the runtime validator is still operating with an image-only format enum for the audio object.
    • Fix
      • Ensure the published/exported schema used by validateManifest includes the audioAsset oneOf branch and format: ['wav'], and add a validation test asserting a manifest with an audio asset is valid. Also verify workspace/package exports do not point validation at a stale schema copy.

    T-Rex Ran code and verified through T-Rex

  13. apps/web/src/builderRender.ts, line 331-332 (link)

    P1 Apply filled shape fills

    The new fill controls can set Pattern or Image on a rounded filled-shape layer, but this render path still uses fillToColor, which turns object fills into the fallback color. When a user selects Pattern or Image for a Rounded layer, the canvas and PNG/JPG export render the default solid gray instead of the configured fill. Use the same resolveFill path as rectangles, and draw image fills clipped to the rounded shape.

    Coding-agent rule suggestion: add a rule that every renderer branch for a shared visual property must use the same resolver/helper path unless the branch explicitly documents why it cannot.

    Rule Used: Please include instructions to add skills or rules... (source)

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Artifacts

    Repro: Playwright script that drives the real UI Builder controls and samples the canvas

    • Contains supporting evidence from the run (text/javascript; charset=utf-8).

    Repro: command output with canvas pixel samples showing rounded fill fallback gray

    • Keeps the command output available without making the summary code-heavy.

    Rounded Fill Repro

    • Shows the rendered state that T-Rex checked.

    ▶ Screen recording from the T-Rex run

    • Shows the flow or interaction that T-Rex exercised.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  14. apps/web/src/builderRender.ts, line 331-332 (link)

    P1 Render filled-shape fills

    The property panel now exposes pattern and image fill modes for every selected layer, including the + Rounded filled-shape layer, but this renderer still converts non-string fills through fillToColor. When a rounded layer uses a pattern or image fill, preview and PNG/JPG export draw the fallback gray instead of the chosen fill.

    Rule Used: Please include instructions to add skills or rules... (source)

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Artifacts

    Repro: focused renderer fill-resolution script

    • Contains supporting evidence from the run (text/javascript; charset=utf-8).

    Repro: pattern fill object resolves to fallback gray

    • Keeps the command output available without making the summary code-heavy.

    Repro: Playwright canvas capture harness for the real renderer

    • Contains supporting evidence from the run (text/javascript; charset=utf-8).

    Stack trace captured during the T-Rex run

    • Keeps the raw stack trace available without making the summary code-heavy.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  15. General comment

    P1 Audio assets are still rejected by manifest validation

    • Bug
      • The head implementation can generate an audio WAV when generateJob is called directly, but the manifest validator rejects a valid phase-2 audio asset (kind: audio, format: wav, frequency, duration, sample_rate, amplitude). The after artifact shows VALIDATION.valid: false and errors for /requests/0/assets/1 including missing width, missing height, and must match exactly one schema in oneOf. A CLI/core full generate flow that validates manifests before generation will therefore reject the new audio asset kind instead of producing the claimed ZIP.
    • Cause
      • packages/schemas/src/manifest.schema.json defines baseAsset.required as ["kind", "name", "width", "height", "format", "output_path"], and audioAsset composes baseAsset with allOf, so audio assets inherit image-only required dimensions. The oneOf branches are also not mutually exclusive at the base level, causing the valid audio object to fail schema matching.
    • Fix
      • Split shared asset fields from image dimensions, or override/refactor the audio schema so it does not inherit width and height. Also make each asset schema branch require/constrain its own kind so an audio object with kind: "audio", format: "wav", frequency, and duration validates successfully. Add an e2e validation assertion that the phase-2 manifest is accepted before generateJob/CLI generation.

    T-Rex Ran code and verified through T-Rex

  16. apps/web/src/builderRender.ts, line 335-336 (link)

    P1 Render filled-shape fills

    The property panel now lets a filled-shape/rounded layer choose Pattern or Image fill, but this renderer still uses fillToColor, which falls back for object fills. A user can select Pattern or Image for the rounded shape and the canvas/PNG/JPG export will silently render the default solid color instead of the selected fill. Use the same resolveFill and image-overlay path used by rectangles/circles, clipped to this rounded-rect path.

    Rule Used: Please include instructions to add skills or rules... (source)

    Artifacts

    Repro: Playwright script that loads the real UI Builder with pattern-filled rectangle and rounded layers

    • Contains supporting evidence from the run (text/javascript; charset=utf-8).

    Repro: command output for the UI Builder Playwright capture run

    • Keeps the command output available without making the summary code-heavy.

    Repro Rounded Pattern Canvas

    • Shows the rendered state that T-Rex checked.

    Repro Rounded Pattern Canvas Only

    • Shows the rendered state that T-Rex checked.

    ▶ Screen recording from the T-Rex run

    • Shows the flow or interaction that T-Rex exercised.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  17. apps/web/src/builderRender.ts, line 372 (link)

    P1 Preserve SVG fills

    layerToSVG serializes every shape with fillToColor, so pattern and image fills are converted to the fallback #4A5568. A rect, circle, or rounded layer can now preview with a checkerboard or image fill, but Export SVG writes a solid fallback fill, so the exported asset does not match the user-selected appearance.

    Rule Used: Please include instructions to add skills or rules... (source)

    Fix in Claude Code Fix in Codex

  18. General comment

    P2 Sprite animation sidecar is omitted from the generated ZIP report folders/files consistency contract

    • Bug
      • Head generates sprites/hero.animation.json in the ZIP and the JSON sidecar fields are correct, but this behavior relies on adding the sidecar path to createdFiles before it is actually written and then emitting the sidecar in a later pass. The report lists the sidecar in createdFiles, but the sidecar is not part of successful asset count, and base/head evidence shows the new sidecar behavior was introduced without a separately reported artifact count. If consumers interpret the report as a complete accounting of generated files vs generated assets, the sidecar is mixed into createdFiles but not into the successful count. This is a contract ambiguity rather than the excluded invalid-output_path sidecar exception issue.
    • Cause
      • packages/core/src/generate.ts reserves sidecarPath in createdFiles during the main asset loop and increments successful only for the primary sprite sheet asset, then writes the sidecar later. The manifest report is built from createdFiles and asset counts, so auxiliary sidecars are included as files but excluded from success totals.
    • Fix
      • Clarify and encode report semantics for auxiliary files: either add explicit sidecar/auxiliary file counts to the report, or keep successful as assets-only but document/test that createdFiles.length may exceed successful; alternatively write sidecars in the same try block and track sidecars separately from asset outputs.

    T-Rex Ran code and verified through T-Rex

  19. apps/web/src/builderRender.ts, line 335-336 (link)

    P1 Render rounded FillSpec

    The property panel now exposes Pattern and Image fill modes for every selected layer, including filled-shape layers from + Rounded, but this render path still uses fillToColor(). For pattern or image fill objects, that returns the #4A5568 fallback and never draws the image overlay, so rounded layers silently lose the selected fill in preview and PNG/JPG export. Use the same FillSpec handling as the rect/circle paths here.

    Rule Used: Please include instructions to add skills or rules... (source)

    Artifacts

    Repro: Playwright harness that renders real builderRender.ts output

    • Contains supporting evidence from the run (text/javascript; charset=utf-8).

    Repro: in-app fixture page importing the real renderer

    • Contains supporting evidence from the run (text/html; charset=utf-8).

    Repro: command output with pixel samples proving rounded fallback fill

    • Keeps the command output available without making the summary code-heavy.

    Rounded Fillspec Repro

    • Shows the rendered state that T-Rex checked.

    ▶ Screen recording from the T-Rex run

    • Shows the flow or interaction that T-Rex exercised.

    Repro Exported Pattern Fill

    • Shows the rendered state that T-Rex checked.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  20. apps/web/src/builderRender.ts, line 372-388 (link)

    P1 Preserve SVG fills

    exportSVG still sends every layer through fillToColor, which returns the fallback color for object fills. When a builder layer uses the new Pattern or Image fill mode, the live canvas and PNG/JPG export render that fill, but SVG export silently writes a solid fallback-colored shape instead. This makes the exported SVG differ from the preview and drops the new fill data.

    Rule Used: Please include instructions to add skills or rules... (source)

    Artifacts

    Repro: TypeScript harness that calls the real exportSVG with Pattern and Image fills

    • Contains supporting evidence from the run (text/typescript; charset=utf-8).

    Repro: command output showing fallback solid fills and missing pattern/image SVG data

    • Keeps the command output available without making the summary code-heavy.

    Repro: generated SVG containing fallback fill colors for pattern and image fill layers

    • Shows the rendered state that T-Rex checked.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  21. General comment

    P1 Audio manifests are rejected by schema unless image-only width/height are supplied

    • Bug
      • The new audio generation path can synthesize sfx/beep.wav with a correct RIFF/WAVE PCM mono header and sample data when generateJob is called directly, but the normal validated manifest contract is broken. The head run for an audio asset containing frequency, duration, sample_rate, and amplitude reports validation.valid: false with required-property errors for width and height, plus image-family schema errors, before generation. CLI generate validates manifests first, so this prevents the documented audio asset shape from being accepted in normal usage.
    • Cause
      • packages/schemas/src/manifest.schema.json defines audioAsset as allOf including baseAsset at lines 144-146, while baseAsset still requires width and height at line 60. The TypeScript AudioAsset also extends BaseAsset, which keeps image dimensions mandatory for audio assets.
    • Fix
      • Split shared asset fields into a dimensionless base for all assets and an image/dimensioned base for image-like assets, or override the audio schema/type so audio requires only kind, name, format, output_path, frequency, and duration plus optional audio parameters. Add validation tests for a minimal audio manifest and run CLI generate through validation.

    T-Rex Ran code and verified through T-Rex

  22. apps/web/src/App.tsx, line 327-330 (link)

    P2 Handle audio dimensions

    Audio assets are now valid without width or height, but the overview still renders every asset as dimensions. Importing a valid audio manifest shows audio • undefined×undefined in the asset row, even though AssetPreview already treats audio as dimensionless. Render audio metadata separately, such as frequency and duration.

    Rule Used: Please include instructions to add skills or rules... (source)

    Artifacts

    Audio Overview Undefined Dimensions

    • Shows the rendered state that T-Rex checked.

    Repro: Playwright run output showing the focused UI test passed after observing the malformed audio dimensions text

    • Keeps the command output available without making the summary code-heavy.

    Repro: Playwright test script that imports the audio manifest and captures the Overview row

    • Contains supporting evidence from the run (text/typescript; charset=utf-8).

    Repro: exact valid audio manifest used for the UI import

    • Contains supporting evidence from the run (application/json; charset=utf-8).

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  23. General comment

    P2 Playwright e2e smoke test and CI wiring are absent on head

    • Bug
      • The claimed head contract says pnpm e2e should run Playwright smoke tests, the repo should contain playwright.config.ts and tests/e2e/placeholderer.spec.ts, and CI should install Chromium and invoke the e2e suite. Runtime validation on head instead shows no e2e package script, missing Playwright config/test files, no Playwright executable, and CI lines only for install/build/test. pnpm e2e fails with ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "e2e" not found.
    • Cause
      • The expected Playwright files and package/workflow wiring are not present in the validated head checkout.
    • Fix
      • Add the root e2e script to package.json, commit playwright.config.ts and tests/e2e/placeholderer.spec.ts with the described smoke assertions, add Playwright dependencies/lockfile entries, and update .github/workflows/ci.yml to install Chromium and run pnpm e2e.

    T-Rex Ran code and verified through T-Rex

  24. General comment

    P1 validateBuilderRecipe still rejects full object FillSpec variants

    • Bug
      • The head validation contract still rejects representative builder layer FillSpec objects. A layer with fill: { type: 'gradient', from: '#000000', to: '#ffffff', direction: 'vertical' } returns 422 Unprocessable Entity, and a layer with fill: { type: 'image', src: 'textures/bg.png', fit: 'cover' } also returns 422. The validation objective expected validateBuilderRecipe to accept full FillSpec variants for builder-related asset data.
    • Cause
      • The builder recipe JSON schema accepted by AJV still appears to model fill as either a string or older/narrow object variants requiring fields such as mode or pattern; it does not include the full FillSpec object variants represented in the TypeScript contracts.
    • Fix
      • Update packages/schemas/src/builder-recipe.schema.json or its FillSpec definitions to match the intended FillSpec union from packages/schemas/src/types.ts, including gradient and the current image object shape, then add targeted validation tests for each accepted FillSpec variant plus invalid discriminator values.

    T-Rex Ran code and verified through T-Rex

  25. apps/web/src/App.tsx, line 43-64 (link)

    P1 Clear reports on import

    manifestReport is cleared when generation starts, but it is not cleared when the active job changes. A user can generate job A, click New Job, paste or import job B, and the overview for job B still shows job A's manifest report before any new generation runs. Clear the report anywhere a new manifest replaces the current job.

    Rule Used: Please include instructions to add skills or rules... (source)

    Artifacts

    Repro: Playwright test that automates job A generation then job B import and asserts stale report visibility

    • Contains supporting evidence from the run (text/typescript; charset=utf-8).

    Repro: focused Playwright run output showing the stale-report assertion test passed

    • Keeps the command output available without making the summary code-heavy.

    Repro Job A Report Visible

    • Shows the rendered state that T-Rex checked.

    After: Repro Stale Report Job B Import

    • Shows the rendered state that T-Rex checked.

    ▶ Screen recording from the T-Rex run

    • Shows the flow or interaction that T-Rex exercised.

    Repro: Playwright trace for the stale-report flow

    • Contains supporting evidence from the run (application/zip).

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  26. apps/web/src/App.tsx, line 341-343 (link)

    P2 Handle audio dimensions

    Audio assets are now valid without width or height, and AssetPreview already treats them as dimensionless. The overview row still renders every asset as width×height, so importing a valid audio manifest shows audio • undefined×undefined. Branch on asset.kind === 'audio' here and show audio metadata, or omit dimensions for audio rows.

    Artifacts

    Repro: valid audio manifest fixture with frequency and duration but no width or height

    • Contains supporting evidence from the run (application/json; charset=utf-8).

    Repro: Playwright test importing the audio manifest and checking the Overview row

    • Contains supporting evidence from the run (text/typescript; charset=utf-8).

    Repro: Playwright run output showing the imported Overview row text and passing reproduction assertion

    • Keeps the command output available without making the summary code-heavy.

    Audio Overview Undefined Dimensions

    • Shows the rendered state that T-Rex checked.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code Fix in Codex

  27. General comment

    P1 CI e2e step uses a pnpm command that does not run the e2e script

    • Bug
      • The PR contract claims pnpm e2e smoke coverage and CI wiring. On head, package.json defines an e2e script, but executing the exact claimed command pnpm e2e fails with ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "e2e" not found; the suite only runs with pnpm run e2e. The CI workflow line 42 currently runs pnpm e2e, so CI will not execute the Playwright suite despite Chromium installation and valid tests.
    • Cause
      • The workflow invokes pnpm e2e instead of the script form pnpm run e2e; pnpm 9 in this environment does not resolve arbitrary script names through the shorthand used here.
    • Fix
      • Change .github/workflows/ci.yml e2e step to pnpm run e2e and update any documented/local command expectation accordingly, or otherwise add a command alias that makes pnpm e2e execute playwright test.

    T-Rex Ran code and verified through T-Rex

  28. General comment

    P1 Audio manifest still fails validation, blocking the validated CLI generate flow

    • Bug
      • On head, the same manifest that generateJob successfully turns into a complete ZIP is rejected by validateManifest. The captured output shows validation.valid=false with errors requiring width/height, image formats, and non-audio kind constants for the audio asset at /requests/0/assets/2. Since the CLI validate path gates generation, a documented audio manifest with kind:'audio', format:'wav', frequency, duration, sample_rate, and amplitude is not accepted by validation even though core generation can emit the WAV.
    • Cause
      • The runtime schema used by validateManifest does not accept the audio asset variant. This is anchored in the schema/type packaging path around packages/schemas/src/manifest.schema.json and packages/schemas/src/types.ts, and is compounded by package exports/main pointing at src rather than built dist, so validation imports the source schema path during the executed flow.
    • Fix
      • Ensure the manifest schema used at runtime includes the audio oneOf variant with wav format and no image width/height requirement, and align @placeholderer/schemas package exports/main with the built dist artifacts or otherwise ensure validateManifest loads the updated schema consistently in CLI/core execution.

    T-Rex Ran code and verified through T-Rex

Reviews (17): Last reviewed commit: "Apply glow/shadow filter to line and ras..." | Re-trigger Greptile

The v1 schema supports a richer layer.fill: solid color, pattern
(checkerboard / stripes / diagonal), or image (with a mode of
repeat or stretch). The schema also defines layer.effects.glow
alongside layer.effects.shadow. The previous UI Builder commit
shipped the render code paths and the property panel editor for
shadow, but left the other three feature surfaces as no-ops
noted in the commit message: this finishes the work.

Render side (apps/web/src/builderRender.ts):
  - drawRect, drawCircle, drawFilledShape all call resolveFill
    instead of falling back to color. resolveFill returns a
    CanvasPattern for pattern fills (built from a 16px
    OffscreenCanvas tile) or a string color for solid/image fills.
  - drawImageFillOverlay layers an image fill on top of the
    underlying color: it draws the image stretched for 'stretch'
    mode, or uses createPattern + a clipped fill for 'repeat'.
  - preloadRasterImages now also preloads every image fill src so
    the on-screen render and the export are consistent.

Property panel (apps/web/src/UIBuilder.tsx):
  - 'Fill mode' picker switches between Solid / Pattern / Image.
  - Pattern mode shows a Pattern picker (checkerboard / stripes
    / diagonal).
  - Image mode shows an Image src field and a Mode picker
    (Repeat / Stretch).
  - 'Glow blur' field is wired to layer.effects.glow; setting it
    to 0 removes the glow effect entirely so the layer goes back
    to a shadow-only (or no-effect) state.

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
After Generate & Download, the web UI now reads the
_placeholderer/manifest-report.json entry out of the produced ZIP
and renders a small panel under the generate button.

The panel shows job name, total/successful/failed asset counts,
and lists the created folders and files as inline code tags so the
user can confirm what landed in the archive without unzipping it.

The decoder walks the ZIP central directory by hand (no JSZip
dependency on the web side) and only handles the STORE method
uncompressed case — which is the only case core's generateJob
produces.

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
The UI Builder's preset list was five generic placeholders (Button,
Panel, Title Text, Circle Badge, Divider). Replace with a real
engine-aware library in apps/web/src/builderPresets.ts:

  Godot:  Dialog Window (frame + panel + title + divider + body)
  Unity:  Health Bar (frame + fill)
  Unreal: Crosshair (two perpendicular lines)
  Common: Button, Panel, Title Text, Divider

Each preset carries its own canvas size and a multi-layer recipe;
applying one resizes the canvas and appends the layers to the
current stack in a single history entry (so undo undoes the whole
preset in one step).

The picker is now grouped by engine label so the user can see
which preset is engine-specific at a glance. New layer factories
move to apps/web/src/builderLayerFactories.ts so the preset file
and the inline add-* buttons in the UI share a single source of
truth for layer construction.

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
New playwright.config.ts and tests/e2e/placeholderer.spec.ts with
two smoke tests:

  1. Pastes a starter manifest into the home view, asserts the
     Overview opens, clicks Generate & Download, captures the
     download (verifies the suggested filename), and checks that
     the Manifest report panel renders.

  2. Clicks the theme toggle and asserts that data-theme on
     <html> actually changes.

The webServer block in playwright.config.ts launches
'pnpm --filter web dev' for the duration of the test run and
reuses an existing dev server when one is already up. Run
'npx playwright install' once to pull the browser binaries
(browsers are not committed). Add 'pnpm e2e' to CI once a
runner with the Playwright browser cache is available.

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
apps/cli/tests/e2e.test.ts runs the core generateJob against a
real Node backend (@napi-rs/canvas) and asserts the produced ZIP's
contents:

  - Every requested asset is in the archive
  - Declared empty folders are materialized as .gitkeep
  - _placeholderer/manifest-report.json parses with the right
    job name, asset counts, folder list, file list, and zero
    errors
  - result.suggestedName is 'cli_e2e_pack.zip' (sanitized from
    job.name)

A second test asserts that the core validator surfaces bad
schemaVersion at validation time.

The suite skips itself automatically when @napi-rs/canvas's
native binary is unavailable, so it never breaks a fresh clone
on an unsupported platform.

apps/cli/package.json gains 'test' (vitest run) and pulls in
vitest + jszip as devDeps. The root 'test' script now runs
both core unit tests and the CLI e2e. The CI workflow gains
'Install Playwright browser' + 'e2e' steps after the unit tests
so PRs are gated on the full test pyramid.

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Per the v1 spec's Phase 2 list, add a new 'audio' asset kind
that produces a 16-bit PCM mono WAV file containing a sine wave
at the configured frequency.

Schema:
  - AssetKind gains 'audio'
  - New AudioAsset interface with required frequency (Hz, 1..22050)
    and duration (seconds, 0.01..60), plus optional sample_rate
    (8000..96000, default 44100) and amplitude (0..1, default 0.5)
  - baseAsset.format is now a generic string with a description;
    each image-style kind restricts to png/jpg/jpeg/webp via its
    own definition, audio restricts to 'wav'. The 'rejects non-enum
    format' validation test still passes because each kind's
    format constraint is enforced via oneOf.
  - baseAsset.sanitizePath now strips a trailing slash so
    output_path: 'sfx/' no longer produces 'sfx//beep.wav' in
    the archive (a real bug I caught in e2e).

Generator:
  - packages/core/src/audio.ts: encodeWav (RIFF/WAVE header for
    PCM 16-bit mono), synthesizeTone (sine wave with a 10ms
    attack/release envelope to avoid clicks at the start/end),
    generateAudio that combines them.
  - generateJob in core: when asset.kind === 'audio', skip the
    canvas pipeline and call generateAudio. Audio uses the format
    field for the container extension (wav by default) instead of
    the image MIMEs.

Tests:
  - 33/33 core unit tests still pass (the 'rejects non-enum
    format' assertion is back to passing because each kind
    enforces its own format).
  - apps/cli/tests/e2e.test.ts gains an audio test that
    generates a 0.25s 440Hz WAV, opens the produced ZIP, and
    asserts the RIFF/WAVE header, PCM format tag, mono channel
    count, and sample rate.

End-to-end verified:
  placeholderer generate --in audio-manifest.json
  -> produces sfx/beep.wav, 44144 bytes for 0.25s at 22050 Hz,
  opens cleanly in a standard audio player.

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Per the v1 spec's Phase 2 list, sprite_sheet assets can declare
frame_duration_ms. When set, the generator writes an
animation.json sidecar next to the sheet with the timing data a
runtime needs to play the animation:

  {
    'sheet': 'enemies/slime_idle.png',
    'frame_width': 32, 'frame_height': 32,
    'rows': 1, 'columns': 2,
    'frame_count': 2,
    'frame_duration_ms': 150,
    'fps': 7,
    'total_duration_ms': 300
  }

Schema:
  - SpriteSheetAsset gains optional frame_duration_ms (1..10000).
  - JSON schema updated to match.

Generator (packages/core/src/generate.ts):
  - After the asset loop, walks the manifest again and writes a
    sidecar per sprite_sheet asset that has frame_duration_ms set.
  - fps is derived as round(1000 / frame_duration_ms).
  - total_duration_ms is frame_count * frame_duration_ms so the
    consumer doesn't have to compute it.

CLI e2e:
  - New test asserts the sidecar is written with the right
    contents for a 2-frame 150ms animation.

End-to-end verified:
  placeholderer generate --in anim.json
  -> enemies/slime_idle.png
  -> enemies/slime_idle.animation.json (199 bytes, contains fps
     7, frame_count 2, total_duration_ms 300)

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Comment thread apps/web/src/App.tsx Outdated
Comment thread packages/core/src/generate.ts Outdated
Comment thread packages/schemas/src/types.ts Outdated
Comment thread apps/web/src/builderRender.ts Outdated
Comment thread apps/web/src/UIBuilder.tsx
Five real issues from Greptile's review of the tier 3 PR, plus
two e2e test failures that were also real bugs.

1. apps/web/src/App.tsx — manual ZIP parser
   The loadManifestReport helper assumed each central-directory
   entry is exactly 46 bytes wide. ZIP entries are 46 bytes plus
   variable-length name, extra, and comment fields, so as soon
   as one generated asset was written before the manifest report
   the entry pointer landed inside the previous entry's name and
   the report was never found. Track entryOffset cumulatively,
   scope the local-header read into the matched-name branch, and
   advance entryOffset += 46 + nameLen + extraLen + commentLen
   per iteration. This is what was making the e2e 'manifest
   report' assertion time out.

2. packages/core/src/generate.ts — sidecar tracking
   The animation.json sidecar was emitted in a separate post-loop
   pass that ignored the success of the sprite sheet it paired
   with, didn't participate in duplicate detection, and wasn't
   listed in createdFiles. Now the sidecar path is reserved
   inside the main asset loop (alongside the sheet), checked
   against createdFiles for collisions, and only written if the
   sheet was successfully rendered. Explicit user files with
   the same name win.

3. packages/schemas/src/types.ts — Format excludes 'wav'
   AudioAsset inherits BaseAsset.format, but the Format type
   union didn't include 'wav', so TypeScript callers couldn't
   construct a valid AudioAsset. Add 'wav' to the Format union.

4. apps/web/src/builderRender.ts — circle image fill clipping
   drawCircle was calling drawImageFillOverlay with the circle's
   bounding box but no clip, so a circle layer with an image fill
   drew a rectangular image outside the circle's bounds. Wrap
   the overlay in save/clip/restore so the image is constrained
   to the ellipse.

5. apps/web/src/UIBuilder.tsx — glow color editor
   The property panel had a Glow blur input but no color picker,
   so glows always used the hard-coded default and existing
   recipes with effects.glow.color couldn't change it. New Glow
   color field appears when a glow is set; hexToRgba preserves
   the existing alpha channel when converting from the picker.

e2e fixes:
  - Use 'Toggle theme' aria-label (the button's accessible name)
    instead of the title-attribute text, which getByRole doesn't
    match by default.
  - Wait explicitly for the 'Manifest report' <strong> rather
    than racing the manual ZIP decoder.
  - Bump per-test timeout to 60s for the dev server cold start.
  - Use page.waitForFunction to wait for the theme to flip before
    reading the attribute, avoiding a race with the React
    useEffect that writes to localStorage.

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Comment thread apps/cli/tests/e2e.test.ts Outdated
The previous fix landed the EOCD offset corrections inline in
App.tsx, but the e2e test was still failing because the report
panel was rendering just fine — the test itself was wrong. Two
issues:

1. getByText('Total') and getByText('Successful') matched both
   the success message and the report-panel labels (strict-mode
   collision). Fixed by scoping to the report panel container and
   asserting on a label that's unique to the report (the job name).

2. The inline parser in App.tsx was a 50-line anonymous function
   inside the component, untestable in isolation. Extracted to
   apps/web/src/zipParser.ts as a small standalone module
   (readZipEntry). Used in App.tsx the same way as before but
   with proper bounds checks, plus a real signature check before
   each EOCD/central-directory field read. The new module has
   no behavior change from the previous inline version; it just
   makes the parser testable and centralizes the field offsets
   in one place.

pnpm e2e now passes both tests locally. CI will pick up the
fix on the next push.

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Comment thread apps/web/src/builderRender.ts Outdated
Greptile's last open thread on tier/3-polish: describe.skipIf was
evaluated when the file was loaded, before the async beforeAll had a
chance to set canRun. If the native canvas setup failed, the suite
was still registered as runnable and the tests tried to call
generateJob / nodeCanvasBackend as undefined.

Drop describe.skipIf. Run the async smoke test in beforeAll as
before, and have each test that needs the canvas check canRun at
the top and return early. The requireCanvas() helper throws
unavailable rather than letting the test silently hit undefined
functions.

Verified locally:
  - pnpm --filter @placeholderer/core test: 33/33 pass
  - pnpm --filter cli test:                  4/4 pass
  - pnpm e2e:                              2/2 pass

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Comment thread packages/core/src/generate.ts Outdated
Two real issues from Greptile's review of the tier 3 PR.

1. apps/web/src/builderRender.ts + UIBuilder.tsx
   drawImageFillOverlay only draws from the rasterCache, but the
   on-screen render path never preloaded image fills. A user could
   pick Fill mode = Image and enter a source, but the editor
   preview kept showing only the fallback fill until the export
   path happened to preload the image. Export rasterCache and
   add a useEffect in UIBuilder that iterates every raster and
   image-fill src, kicks off async loads, and bumps a
   preloadTick when each finishes. The render effect depends on
   the tick so the canvas re-draws as images arrive.

2. packages/core/src/generate.ts
   createdFiles was used as both a duplicate-reservation set and
   the report's createdFiles list, but paths were pushed BEFORE
   zip.file ran. If an animated sprite sheet's encode threw, the
   sheet and sidecar were still in createdFiles — so the sidecar
   pass wrote a sidecar pointing at a missing sheet, and the
   manifest report listed files that were never created. Move
   the push to createdFiles to AFTER zip.file succeeds, so a
   failed render leaves no trace in the report or the sidecar
   pass.

Test:
  - apps/cli/tests/e2e.test.ts gains a new 'skips sidecar + report
    entry when an animated sprite sheet fails' that drives a
    flaky backend whose second encode() rejects, and asserts the
    failed sheet + its sidecar are not in the archive or the
    report. Also asserts the previous (good) sheet is unaffected.

Verified locally:
  - pnpm --filter @placeholderer/core test: 33/33 pass
  - pnpm --filter cli test:                  5/5 pass
  - pnpm e2e:                              2/2 pass

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Comment thread apps/web/src/builderRender.ts
The round-2 fix preloads image-fill sources in UIBuilder but
unconditionally inserted the new Image() into rasterCache before
its onload fired. drawImageFillOverlay trusted any cached entry as
drawable, so a render that fired while the load was still in
flight would call drawImage or createPattern with a half-loaded
bitmap.

Two-part fix:

- apps/web/src/UIBuilder.tsx: only put the Image into the cache
  after onload (or onerror) fires. A still-loading image stays
  out of the cache, so drawImageFillOverlay's lookup misses and
  the fallback fill remains visible until the load finishes.

- apps/web/src/builderRender.ts: add a defensive
  img.complete && img.naturalWidth > 0 check in
  drawImageFillOverlay. This mirrors the same guard used by
  drawRaster, and protects against any future code path that
  might insert a partially-loaded Image into the cache.

Verified locally:
  - pnpm --filter @placeholderer/core test: 33/33 pass
  - pnpm --filter cli test:                  5/5 pass
  - pnpm e2e:                              2/2 pass

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Comment thread packages/core/src/generate.ts Outdated
Greptile's last open thread on tier/3-polish: the sidecar pass
called sanitizePath outside the per-asset try/catch that the main
loop uses. If a sprite sheet's output_path was malformed in a
way the main loop's catch didn't trip on, the sidecar pass would
re-throw and reject the whole generateJob call instead of
producing a partial ZIP and a per-asset error in the report.

Fix:
  - packages/core/src/generate.ts: wrap the sidecar emit in its
    own try/catch. A throw from sanitizePath/sanitizeFilename
    inside the sidecar loop is converted into a per-asset error
    ('<name> (sidecar): <msg>') and the rest of the sidecars
    continue to be processed.
  - The main loop's behavior is unchanged.

Test:
  - apps/cli/tests/e2e.test.ts gains 'reports per-asset errors
    when an animated sprite sheet has a bad output_path' that
    drives a sprite sheet with a backslash-escaped relative
    output_path, which the sidecar pass would previously have
    rejected outright. The new test asserts generateJob returns
    a result (not a thrown promise) and the bad sheet isn't in
    the archive.

Verified locally:
  - pnpm --filter @placeholderer/core test: 33/33 pass
  - pnpm --filter cli test:                  6/6 pass
  - pnpm e2e:                              2/2 pass

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Split baseAsset so image-style kinds still require width/height
while audio stays dimensionless. Fix the rounded FillSpec canvas
path, the SVG image-fill pattern builder, and the SVG glow color
(feGaussianBlur with no color is now feDropShadow with flood-color).
Add audio manifest validation tests at the schema and CLI layers,
and add an audio branch to the web AssetPreview so the new
optional dimensions don't break the build.

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Comment thread apps/web/src/App.tsx
Comment thread apps/web/src/builderRender.ts Outdated
Comment thread apps/web/src/builderRender.ts Outdated
- Clear manifestReport on a new generation start so a later
  failure doesn't show the previous job's folders/files.
- SVG export: branch on fill.mode so stretch image fills emit a
  single <image> clipped to the layer shape (instead of a tile
  pattern that drifts off-shape from the canvas origin). Repeat
  pattern/image fills now align the pattern x/y to the layer so
  repeated bitmaps also stay in shape-local coords.
- Escape every user-controlled string interpolated into an SVG
  attribute (fontFamily, stroke color, glow/shadow flood-color,
  opacity, rotation, layer id used in def ids). The malicious
  recipe payload 'Arial" evil="true' and '<script>...</script>'
  text both serialize as escaped literals.
- safeId() strips chars that aren't valid in SVG id values, so
  imported recipes with non-ASCII ids can't break the renderer.

Signed-off-by: Zeid Diez <zeidalidiez@gmail.com>
Comment thread apps/web/src/builderRender.ts Outdated
The stretch image-fill export was building the clipPath def with
safeId(`clip-${layer.id}`) but referencing it with
`clip-${safeId(layer.id)}`. For a numeric layer id like "1" the
def became `clip-1` and the reference became `clip-i-1`, so the
exporter pointed at a missing clip path and the stretched image
wasn't clipped to the shape.

Hoist the clip id construction into a single clipIdFor() helper
used by both the <clipPath> def and the wrapper <g>'s clip-path
attribute, so they always agree.

Also: the .githooks/commit-msg DCO enforcement doesn't belong on
this repo (no DCO app is configured here, the hook was carried
over from a different project). Drop the hook so commits don't
need a Signed-off-by trailer.
Comment thread packages/core/src/generate.ts Outdated
Comment thread apps/web/src/builderPresets.ts Outdated
Two real bugs, three stale comments from earlier diffs.

- packages/core/src/generate.ts: split the duplicate-check into
  primary vs sidecar. The previous code skipped the whole sprite
  sheet when only its sidecar path was already taken. Now only
  fullPath collisions block the asset; sidecar reservations live
  in a separate reservedSidecars Set and the sidecar pass skips
  (instead of overwrites) when the path was committed earlier.

- apps/web/src/builderPresets.ts: the Unreal Crosshair preset used
  a second lineLayer for the vertical arm, but lineLayer always
  draws horizontal (height only moves the y-center). Replace the
  vertical arm with a thin rectLayer so the crosshair actually
  looks like a plus.

- apps/cli/tests/builder-presets.test.ts: regression test that
  verifies the crosshair preset's vertical arm is a rect and that
  exportSVG emits a <rect> for it.

The other Greptile comments on this commit (Glow color picker,
loaded-image guard, setManifestReport(null) on generation start)
are stale diff-hunk comments from earlier rounds — current code
already has those fixes; no change needed.
The 'Clear stale report' fix at App.tsx:107 only ran inside
handleGenerate, so the user saw the previous job's report while
importing a new manifest (T-Rex caught this in round 7:
'importing job B, and asserts that stale job reports remain
visible until generation completes').

Clear setManifestReport(null) in:
- handlePaste (JSON manifest import)
- handleCSVImport (CSV import)
- 'New Job' button onClick

Also added a Playwright test that generates job A, goes back to
the home view, imports job B, and asserts the manifest report
panel is no longer in the DOM.
Comment thread apps/web/src/builderRender.ts
CI timeout:
- Drop --with-deps from the Playwright install step. ubuntu-latest
  has the libs chromium needs; --with-deps pulls 21 MB of font
  packages via apt-get that took 14+ minutes on the runner and
  pushed the 15-minute job over budget. Bumped timeout-minutes to
  20 as a safety margin.

Stretch-fill stroke:
- exportSVG replaced the shape with <g clip-path><image/></g> for
  fill.mode === 'stretch', which silently dropped the layer's
  stroke. Now emits a stroked, fill="none" shape inside the same
  clip group so the border survives. Regression test in
  apps/cli/tests/builder-presets.test.ts asserts the stroke is
  present in the SVG.

Audio overview row:
- Asset rows rendered '${asset.width}×${asset.height}', which
  printed 'undefined×undefined' for audio assets. New
  describeAssetSize() helper shows '${duration}s @ ${sr}Hz'
  for audio and width×height for image-style assets.
Comment thread apps/web/src/UIBuilder.tsx Outdated
The previous regex /[\d.]+\s*\)\s*$/ would match the last
numeric component of any '...'-terminated color string. For
rgb(10,20,30) (no alpha component), this grabbed the blue
channel '30' and used it as the alpha, producing
rgba(...,...,...,30) — outside the valid 0..1 range, which
silently fails to render.

Replace the regex with a stricter parser that only carries alpha
forward when the existing color is a true rgba(...) / hsla(...)
with a 4th numeric component; otherwise fall back to the default
0.6. Verified manually with rgb/rgba/hsla/undefined cases.

The other Greptile comments on this commit (Clear stale report,
Wait before drawing images, Preserve stretch strokes, Glow color
cannot edit) are stale diff-hunk comments from earlier rounds —
the current code already has those fixes.
The fix for rgb(10,20,30) -> rgba(...,...,...,30) landed in
ec84c90, but the function was not exported and had no unit test,
so the only way for Greptile to confirm it was to re-run a
Playwright harness against the live UI.

- Export hexToRgba from apps/web/src/UIBuilder.tsx.
- Add 5 unit tests in apps/cli/tests/builder-presets.test.ts
  covering rgb/rgba/rgba(0)/hsla/undefined. The rgb() case is
  the exact one T-Rex flagged (rgba(17,34,51,0.6), not
  rgba(17,34,51,30)).
Comment thread packages/core/src/generate.ts
Comment thread apps/web/src/builderRender.ts
Two real bugs that can produce incorrect generated artifacts:

1. packages/core/src/generate.ts — sidecar overwrite.
   The main loop reserves the sidecar path in reservedSidecars,
   but the sidecar pass was the only place that checked it. If
   a later asset writes the sidecar path as its primary output
   between the reservation and the sidecar pass, the sidecar
   overwrites that later file while the manifest report still
   lists it as created.
   Fix: re-check createdFiles immediately before writing the
   sidecar (let the later asset's file win), and push the
   sidecar to createdFiles after writing so the manifest
   report includes it.

2. apps/web/src/builderRender.ts — stretch text image export.
   buildSVGClipAndImage returns null for text layers (clip
   paths aren't meaningful for glyphs), so buildSVGFill returned
   null and the text layer fell back to a solid fill in SVG.
   The canvas drawText path renders the image as a background
   rect with white text on top; the new branch in layerToSVG
   mirrors that — emit an <image> as the background plus the
   <text> with fill="#ffffff" on top.

Tests:
- apps/cli/tests/builder-presets.test.ts: 2 new tests for text
  stretch image export (image present, text in white; and
  no <image> for plain solid-color text).
- apps/cli/tests/e2e.test.ts: new test asserting that two
  sprite sheets with the same name produce the expected
  duplicate error, with the first sheet's sidecar present in
  both the ZIP and the manifest report.
Comment thread apps/web/src/builderRender.ts Outdated
Comment thread apps/web/src/builderRender.ts Outdated
T-Rex caught two SVG export regressions for text layers:

1. The 'text' case in layerToSVG's switch returned filter: null
   even though filterAttr was on the markup, so the glow/shadow
   filter URL referenced a non-existent def. Now returns the
   buildSVGFilter() result so the <filter> def lands in <defs>.

2. The text-with-stretch-image branch applied filterAttr only
   to the <text> (fg) element, not to the background <image>.
   Canvas applyGlow/applyShadow apply to the whole context, so
   SVG should match: moved opacity/transform/filterAttr from
   the <text> onto the wrapping <g> so the filter affects both
   the image background and the glyphs.

Regression tests:
- exportSVG text glow → <filter> def present, filter URL
  references the same id.
- exportSVG text shadow → same check.
- exportSVG text stretch image → image + text present.
- exportSVG plain text → no <image>.
Comment thread apps/web/src/builderRender.ts Outdated
T-Rex round 11 caught that the 'line' and 'raster' branches in
layerToSVG returned filter: null and omitted filterAttr from the
markup, so canvas applyGlow/applyShadow effects never landed in
SVG export for those layer types.

- 'line' case: add filterAttr to the <line> markup and return
  the buildSVGFilter spec instead of null.
- 'raster' case: same change for <image> markup.

Two regression tests cover each case (line with glow, raster
with shadow), asserting both the <filter> def lands in <defs>
and the markup references the filter URL.
@zeidalidiez zeidalidiez merged commit 13c1aaf into main Jun 18, 2026
2 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.

1 participant