Skills に戻る
stark-ai-de/agent-skills実行前に内容を確認

SKILL DETAIL

animated-readme-logo

stark-ai-de/agent-skills/animated-readme-logo

Create, transform, animate, or audit GitHub README logos and their verified SVG/PNG/GIF delivery. Use when a repository needs branding assets, not ordinary README prose or app/site motion.

インストール · 1,563出典を見る

Installation

npx skills add https://github.com/stark-ai-de/agent-skills --skill animated-readme-logo

スキルファイル

SKILL.md

最終同期 · 2026/10/01

agents/openai.yaml›
interface:
  display_name: "Animated README Logo"
  short_description: "Creates validated animated README logo pipelines"
  icon_small: "./assets/openai-icon.png"
  icon_large: "./assets/openai-icon.png"
  default_prompt: "Use Animated README Logo for this README logo task. Show audit, create, transform, and animate; announce and proceed with the intent-bound workflow when outcome and authority are clear, otherwise ask me to choose. Keep provider spend, tool installation, and overwrites separately approved."
policy:
  products:
    - CHAT
    - CODEX
  allow_implicit_invocation: true
references/asset-pipeline.md›
# Asset Pipeline

Use this reference after the canonical SVG and motion specification are ready.

## Required mutating-workflow stack

```text
docs/assets/<slug>-logo.svg              # validated, self-contained source
docs/assets/<slug>-logo-motion.md        # deterministic motion specification
docs/assets/<slug>-logo-animation.mjs     # reviewed deterministic render recipe
docs/assets/<slug>-logo-static.png
docs/assets/<slug>-logo-animated.gif
```

Use the repository's established asset folder instead of `docs/assets/` when one exists.

Successful `create`, `transform`, and `animate` routes produce all five files. Optional WebP, APNG, or web-demo derivatives may be added only after the required stack succeeds and those extras are independently verified. `audit` writes none of them.

## Capability-gated export

1. Detect the available local exporter and animation inspector. Read `local-tooling.md` when a requested capability is missing.
2. For static PNG plus animated GIF delivery, read `export-recipe.md`, review the trusted root-relative recipe, and run the bundled exporter with `--check` before the mutating export. The check prevents exporter-controlled writes; it does not sandbox recipe code.
3. Keep brand-specific rendering in the repository recipe; do not copy rasterization, FFmpeg encoding, output staging, PNG sanitization, or animated inspection into the product repository.
4. Record the exact source, settings, recipe, and command before export.
5. Export only formats the tool actually supports.
6. Inspect every animated raster with the bundled inspector. It rejects non-regular inputs, malformed animation structure, hidden text/comment/application/EXIF/XMP/ICC metadata, and unsupported ancillary chunks.
7. Claim `Animation delivery: completed` only when the required PNG and GIF exist and pass inspection alongside the validated SVG, motion specification, and checked recipe.

When a required exporter or inspector command is missing, present the minimal installation preflight from `local-tooling.md` and ask for explicit approval immediately. Use `Animation delivery: blocked` while approval is pending. If installation is declined, forbidden, or unavailable, keep the validated SVG, motion specification, and checked recipe when available, set `Animation delivery: incomplete`, and provide an exact next step. Do not create placeholder files or claim delivery succeeded.

## Transparency and delivery checks

- Preserve alpha through every export.
- Keep the static first frame useful.
- Test light, dark, and checkerboard backgrounds for matte edges.
- Treat GIF as a conservative compatibility fallback with limited color and alpha quality.
- Include a static reduced-motion source and an `<img>` fallback.
- Use explicit display dimensions and meaningful alt text.
- Keep file-size targets repository-specific unless the repository defines a limit.
references/asset-transformation.md›
# Asset Transformation

Use this reference to create the canonical SVG or faithfully transform an existing mark.

## Authoring rules

- Preserve originals and write derived files with deterministic names.
- Prefer the repository's established asset directory, or `docs/assets/` when none exists.
- Infer only low-risk identity facts from public repository context. State assumptions.
- Author a compact, self-contained SVG directly. Use `drawio-diagrams` only when editable geometric construction materially helps.
- Do not embed raster data, scripts, `foreignObject`, external references, remote fonts, foreign namespaces, comments, editor metadata, or private metadata.
- Keep the canonical SVG at its static first-frame state. Put motion in the separate deterministic specification and in derived web/demo or raster artifacts; the strict validator rejects SMIL, `<style>` blocks, and CSS animation in the canonical master.
- Use allowlisted static SVG elements and attributes, explicit presentation values without CSS variables, and literal same-document `#id` references. Every paint, clip, filter, mask, marker, gradient, pattern, text-path, navigation, and `<use>` fragment must resolve to an appropriate in-document target; empty paint servers are not renderable content.
- Flatten clipping, masks, or filters into directly verifiable geometry before claiming readiness. Effect-bearing geometry is not accepted as the sole proof that a mark visibly renders.
- Use positive concrete `width` and `height`, a positive `viewBox`, meaningful groups or layer identifiers, transparent background by default, and geometry that remains legible at README sizes.
- Do not add a full-canvas background unless the user explicitly requests one.

## Source-specific routes

### No source or intentional redesign

Follow `provider-routing.md`, then turn the accepted concept into a local SVG. Provider output does not replace SVG validation.

### Raster, screenshot, or poster

Treat the input as a reference, not a canonical master. Identify the mark, preserve the original, and recreate it locally on transparency. Avoid simple background removal when glow, noise, or antialiasing would leave matte edges. This faithful route is not Recraft-eligible because it needs reference-media fidelity.

### Existing SVG

Keep a clean SVG as the source route. Simplify excessive paths, name animation layers, remove accidental backgrounds, and remove unsafe or external content without changing the identity. Do not route a clean or faithfully cleaned SVG through Recraft.

## Strict validation

Run:

```bash
python3 scripts/validate_logo_svg.py path/to/logo.svg
```

Treat a non-zero exit as blocking. `Motion readiness: ready` requires this real strict-validator pass in addition to a complete motion specification and checked animation recipe. If Python is unavailable, report `Motion readiness: blocked`; do not substitute visual confidence for validation.

Also inspect the rendered mark on light, dark, and checkerboard backgrounds. Visual inspection supplements, but never replaces, structural validation.

## Mutation boundaries

Ask before overwriting a brand file, installing a tool, spending provider credits, publishing, or changing remote state. Creating the five absent derived files is allowed only when the user requested `create`, `transform`, or `animate`; `audit` is read-only.
references/export-recipe.md›
# Export Recipe Contract

Use the bundled exporter only after the canonical SVG and deterministic motion specification are ready. Persist the reviewed recipe as `<slug>-logo-animation.mjs`; it is one of the five required outputs for every successful mutating workflow.

## Boundary

The motion specification and recipe serve different readers:

- `<slug>-logo-motion.md` is the human-readable, renderer-independent design contract: named layers, keyframes, easing, duration, loop, transparency, and reduced-motion state.
- `<slug>-logo-animation.mjs` is trusted executable repository code that implements that contract for deterministic frame rendering.

Do not replace one with the other, and do not expose export as a public workflow.

- Keep brand geometry, named layers, palette, placement, easing, timing, and repository-specific limits in a root-relative `.mjs` recipe.
- Treat the recipe as trusted executable repository code. Read it before execution; never download or run a remote, dependency-provided, unreviewed generated, or unreviewed pull-request recipe. `--check` is not a sandbox: it prevents exporter-controlled writes, but imported recipe code can still have arbitrary side effects.
- Keep the reusable rasterization, encoding, staging, sanitization, and inspection mechanics in the skill.
- Do not point product scripts at an author-home or sibling-checkout skill path. Use the exporter as an agent tool, or deliberately install and pin the skill project-locally before making it a repository command dependency.

## Recipe

```js
export default {
  schemaVersion: 1,
  source: ".github/assets/example-logo.svg",
  staticOutput: ".github/assets/example-logo-static.png",
  animatedOutput: ".github/assets/example-logo-animated.gif",
  width: 800,
  height: 400,
  fps: 15,
  frameCount: 72,
  maxFileBytes: 5 * 1024 * 1024,
  gifMaxColors: 128,
  preserveTransparency: true,
  renderFrame({ sourceSvg, frameIndex, timeSeconds, width, height, fps, frameCount }) {
    // Return one complete, self-contained SVG with numeric width and height.
    // Extract or transform reviewed geometry from sourceSvg without changing identity.
    return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
  <!-- repository-specific frame ${frameIndex}/${frameCount} at ${timeSeconds}s and ${fps}fps -->
</svg>`;
  },
};
```

Required fields:

- `schemaVersion`: exactly `1`; unsupported recipe versions fail closed.
- `source`, `staticOutput`, and `animatedOutput`: distinct root-relative paths; source ends in `.svg`, outputs end in `.png` and `.gif`.
- `width`, `height`, `fps`, and `frameCount`: positive integers; animation requires at least two frames.
- `renderFrame(context)`: synchronous or asynchronous function returning one complete SVG string.

Optional fields:

- `maxTotalFrameBytes`: cumulative UTF-8 byte limit across all rendered SVG frames, default and maximum 32 MiB. Lower it when the repository needs a stricter render-work ceiling.
- `maxFileBytes`: GIF limit, default 5 MiB and maximum 20 MiB.
- `gifMaxColors`: palette size from 2 through 256, default 128. When `preserveTransparency` is `true`, use at least 3 so FFmpeg can reserve one transparent entry.
- `preserveTransparency`: default `true`; requires an alpha-capable static PNG and reserves GIF transparency. The reviewed recipe must still omit opaque background geometry when transparent pixels are required. Set `false` only when the approved design intentionally supplies an opaque background.

Every rendered frame must be well-formed, self-contained SVG, declare the configured numeric dimensions, and avoid scripts, `foreignObject`, external references, imports, and non-local CSS URLs. Literal resolved same-document fragments such as `<use href="#mark"/>` are allowed; encoded, relative, data, and remote references are not.

The exporter renders two complete validation passes and compares every frame at the same index. A mutating export rasterizes the exact first-pass SVG bytes that passed that comparison; it does not invoke `renderFrame` a third time. Keep `renderFrame` pure and independent of call order even when only `--check` is requested.

## Commands

Inspect help before using optional flags:

```bash
node scripts/export-readme-logo-animation.mjs --help
```

Validate the contract and every rendered frame without exporter-controlled writes or raster tools:

```bash
node scripts/export-readme-logo-animation.mjs \
  --root /path/to/repository \
  --recipe path/to/logo-recipe.mjs \
  --check
```

After tool capability and approval checks, export:

```bash
node scripts/export-readme-logo-animation.mjs \
  --root /path/to/repository \
  --recipe path/to/logo-recipe.mjs
```

Add `--replace` only when the reviewed destinations already exist and regeneration is intended. Without it, already-present outputs fail before tool execution, and each absent destination is claimed atomically without overwriting a path that appears during rendering. Missing output-parent components are created one at a time through a held POSIX directory descriptor; the same held parent and stage descriptors anchor commit and cleanup, so a raced symlink or renamed lexical ancestor cannot redirect a mutation. A mutating export fails closed on platforms without a descriptor-directory view; read-only `--check` remains available. If a later destination collides after an earlier output was linked, the exporter reports `OUTPUT_ROLLBACK_INCOMPLETE` and retains the partial public link plus both stage directories; this conservative recovery state avoids deleting data that another process may have changed through the shared inode.

The exporter uses `rsvg-convert` and FFmpeg and starts its final commit only after strict structure and GIF-timing validation. The commit binds each staged file, public hard link, replacement backup, stage-directory chain, and output-parent chain to content or filesystem identities. A same-inode content change or directory rebind fails closed. When `--replace` actually moves an existing output to backup, a successful export intentionally retains both `.readme-logo-*-stage-*` directories with their validated staged links and `backup-*` files; deleting a verified backup automatically would still race a writer holding an open descriptor. Inspect the public outputs and both retained generations, then remove those directories manually. Fresh exports with no prior outputs still clean their staged links automatically. It preserves existing outputs when generation fails before that commit. It does not install commands. Run the canonical SVG validator before export and keep the motion specification beside the derived assets.

If the exporter reports `OUTPUT_ROLLBACK_INCOMPLETE`, do not rerun it. Inspect the destinations and `.readme-logo-*-stage-*` directories directly inside the declared output parents. Depending on commit mode, these retain partial hard links or `backup-*` files for deliberate manual recovery. Remove the stage directories only after both destinations are verified.
references/github-readme-compatibility.md›
# GitHub README Compatibility

Use this reference when deciding whether logo motion belongs directly in a README or on a web/demo surface.

## Documented baseline

- GitHub documents PNG, JPG, GIF, PSD, and SVG as displayable repository image formats.
- GitHub documents support for the HTML `<picture>` element in Markdown and uses `<source media>` for light/dark variants.
- GitHub states that SVGs do not support inline scripting or animation in its repository image rendering.
- GitHub's documentation does not establish animated WebP, APNG, or `prefers-reduced-motion` selection as a universal README contract. Treat those as preview-gated enhancements.

Use the official [non-code image documentation](https://docs.github.com/en/repositories/working-with-files/using-files/working-with-non-code-files) and [Markdown `<picture>` documentation](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#the-picture-element) as the baseline, then manually preview the actual repository README after commit or push.

## Delivery boundary

- README/profile README: use a documented static `<img>` fallback. GIF is the conservative documented image format to try first for animation, subject to repository preview. Use animated WebP or APNG only after the target GitHub surface renders the committed artifact correctly.
- GitHub Pages/docs/app: live animated SVG, Lottie, dotLottie, CSS, or JavaScript may be appropriate.
- Social preview/static target: use a static raster or validated SVG.

Keep animated SVG and Lottie playback on the web/demo surface. Do not present either as an animated README delivery path.

## `<picture>` contract

Use `<picture>` to add candidate sources while retaining a documented `<img>` fallback with meaningful `alt`, `width`, and `height`.

For an animated README candidate:

1. Put the static reduced-motion candidate first.
2. Add only animated sources that exist and pass local inspection.
3. Keep GIF as the conservative animated candidate drawn from GitHub's documented image formats.
4. Keep a static PNG or validated static SVG in `<img>`.

The `prefers-reduced-motion` source is an accessibility enhancement, not proof that every GitHub surface honors that media query. The static `<img>` fallback must remain useful when source selection differs.

## Fallback variants

Conservative:

1. Static reduced-motion PNG candidate.
2. Inspected animated GIF.
3. Static PNG in `<img>`.

Preview-gated quality-first:

1. Static reduced-motion PNG or WebP candidate.
2. Inspected animated WebP or APNG candidate.
3. Inspected animated GIF.
4. Static PNG in `<img>`.

Static-only:

1. Validated static SVG where repository preview succeeds.
2. Static PNG fallback when SVG rendering is uncertain.

## Compatibility risk flags

Flag source or markup that includes:

- `<script`, `<foreignObject`, `@keyframes`, or `animation:`;
- external `href` or `xlink:href`;
- likely full-canvas background rectangles or fills;
- missing `alt`, `width`, or `height`;
- an animated source without a useful static fallback;
- WebP/APNG or reduced-motion behavior described as guaranteed before target-surface preview.
references/local-tooling.md›
# Local Tooling and Browser Fallbacks

Use this reference when a requested raster export, inspection, or visual preview lacks a working command.

## Keep capabilities separate

- SVG rasterizer: prefer `rsvg-convert` from librsvg for deterministic SVG-to-PNG frames.
- Animation encoder and probe: prefer headless FFmpeg with `ffmpeg` and `ffprobe` for GIF, APNG, and animated WebP.
- Structural inspection: use the bundled strict SVG validator and animated-image inspector; do not replace them with exporter self-reporting.
- Reusable GIF path: use the bundled `export-readme-logo-animation.mjs` after reviewing and checking the repository-owned recipe; the script performs no installation and preserves existing outputs on failure.
- Browser preview: use a configured Chrome or Chromium executable. It is not an exporter or inspector.

Do not report the whole pipeline unavailable because only a screenshot browser is missing.

## Missing-tool approval checkpoint

When a requested output needs a missing command:

1. Identify the exact missing command, requested output, active package manager, and whether the repository has a declarative source of truth.
2. Select the smallest package set. Prefer librsvg plus headless FFmpeg. Use ImageMagick or Inkscape only when a source requires a feature the smaller set lacks.
3. Present the package names, source, exact install command, persistence scope, expected download or disk impact when discoverable, and post-install verification commands.
4. Ask for explicit approval of that exact installation. Keep provider approval separate.
5. Stop before installation with `Animation delivery: blocked` and a clearly labeled local-tool approval pending.
6. After approval, install only the displayed set. Verify executable versions and the requested encoder, muxer, and filter support before exporting.
7. If the user declines, forbids installation, or the install path is unavailable, use `Animation delivery: incomplete`, retain verified intermediates, and create no placeholder.

Never turn an approval for one package manager, version, or package set into a broader update or installation.

## Platform selection

- Nix or NixOS: prefer `librsvg` and `ffmpeg-headless`. Add them to the repository's declarative package source when requested. If activating a dirty configuration would apply unrelated changes, propose a pinned temporary shell or profile install separately; do not switch the whole system implicitly.
- Debian or Ubuntu: discover the active release packages before proposing them; common names are `librsvg2-bin` and `ffmpeg`.
- Homebrew: common formulae are `librsvg` and `ffmpeg`.

Package names and versions are live facts. Inspect the active package manager instead of treating these examples as guarantees.

## Browser-preview fallback ladder

1. Inspect the current browser tool's help and environment for an explicit executable path or managed browser directory.
2. Retry with an existing compatible Chrome or Chromium executable. Do not require Chrome for Testing when a managed browser works.
3. If `agent-browser` already exists, inspect its version and help. When supported, load `agent-browser skills get core`, then launch with the existing executable path and capture the preview.
4. If the CLI is missing or too old, present an approval checkpoint for an exact `agent-browser` install or upgrade. Reuse an existing browser after installation.
5. Only when no compatible local browser exists, offer an approval-gated browser download such as `agent-browser install` or the platform's managed Chromium package.
6. If every approved route fails, report the local screenshot limitation and retain the manual committed-GitHub preview requirement.

Browser installation, browser launch, and provider generation are three different side-effect classes. Never infer approval across them.
references/motion-rubric.md›
# Motion Rubric

Use this reference to define deterministic, accessible logo motion before exporting it.

## Required specification

Record:

- canonical SVG path and named layer or group identifiers;
- canvas/viewBox and intended display size;
- total duration and loop behavior;
- each property, keyframe time/value, and easing;
- static first frame and exact loop point;
- static reduced-motion state;
- transparency requirement;
- intended export formats, frame rate when applicable, and capability status.
- the repository-owned `<slug>-logo-animation.mjs` recipe path and how its frame calculations implement each named keyframe.

Use explicit values rather than descriptions such as “subtle pulse.” The same source and specification should reproduce the same sequence.

## Good motion

- restrained stroke or shape reveal;
- a small highlight pass, accent pulse, orbital dot, or layer parallax tied to existing geometry;
- a tiny character detail when the mark already contains a character;
- an intentional calm loop with a useful static first frame.

## Avoid

- whole-logo wiggle, large pan/zoom/spin/bounce, rapid flash, or high-contrast flicker;
- motion that changes the identity, obscures a wordmark, or depends on a remote runtime;
- nondeterministic particle placement or timing;
- a proprietary-only editable source unless the user accepts the maintenance cost.

## Validation

- Confirm every referenced layer exists in the validated SVG.
- Confirm the last state returns cleanly to the first state.
- Verify the reduced-motion state preserves the same identity and information.
- Check the loop on light, dark, and checkerboard backgrounds.
- Keep the motion specification even when raster export capability is unavailable.
- Run the animation recipe through the bundled exporter with `--check`; a valid prose specification without a checked recipe is not `Motion readiness: ready`.
references/output-contract.md›
# Output Contract

Report these fields for every activated task, including blocked and read-only results.

## Fields

- `Workflow`: `audit`, `create`, `transform`, or `animate`.
- `Source route`: `existing-svg`, `faithful-local-transform`, `recraft_v4_1`, `direct-local-svg`, `drawio-assisted-svg`, or `not-applicable`.
- `Selection`: task evidence and the reason for the selected workflow, or `unresolved` while an ambiguous activation awaits a choice.
- `Write scope and protected originals`: the authorized output boundary and originals that must not be overwritten; use `read-only; no writes` for `audit`.
- `Provider state`: `not-eligible`, `available`, `unavailable`, `indeterminate`, or `used`.
- `Approval state`: `not-required`, `pending`, `approved`, or `declined`.
- `Motion readiness`: `not-evaluated`, `invalid`, `blocked`, or `ready`.
- `Animation delivery`: `not-evaluated`, `incomplete`, `blocked`, or `completed`.

## Rules

- Use `Provider state: not-eligible` and `Approval state: not-required` when routing rules exclude Recraft.
- Use `Provider state: available` only after live capability, exact model, and current batch cost are confirmed.
- Use `Provider state: used` only after an explicitly approved batch actually succeeds.
- Use `Approval state: pending` after showing the live sanitized preflight but before approval. Do not generate or silently take the local fallback while approval is pending.
- Use `Approval state: not-required` for an ineligible, unavailable, or indeterminate provider route.
- Use `Approval state: declined` when the user rejects an available batch, then take the direct local route without repeating the request.
- Use `Motion readiness: ready` only when the canonical SVG passes strict validation, the motion specification is complete, and the repository-owned animation recipe passes the exporter's deterministic `--check`.
- Use `Animation delivery: completed` for a mutating route only when all five required artifacts exist, the PNG is validated, and the GIF passes the bundled inspector. Use `blocked` while an explicitly itemized local-tool installation awaits approval. Use `incomplete` when tooling is declined, forbidden, unavailable, or still cannot produce and verify the required PNG/GIF.
- An `audit` can report any evidence-backed readiness/delivery value, but must not create or repair artifacts. A non-complete pipeline includes concrete remediation.
- `create`, `transform`, and `animate` never report `Animation delivery: completed` without `<slug>-logo.svg`, `<slug>-logo-motion.md`, `<slug>-logo-animation.mjs`, `<slug>-logo-static.png`, and `<slug>-logo-animated.gif`.

After the fields, list:

1. Canonical and derived assets with exact paths and roles.
2. Motion-specification and animation-recipe paths and their distinct roles.
3. README markup or delivery recommendation.
4. Commands run and concise validation evidence.
5. Remaining blockers and the next approval, capability, or manual-preview step.

When a local installation is proposed, also report `Local-tool approval: pending | approved | declined | not-required`. This detail is separate from the public `Approval state`, which continues to describe provider spending.
references/provider-routing.md›
# Provider Routing

Use this reference for `create` tasks and whenever external image generation is proposed.

## Eligibility

Consider Recraft only for a new mark or an intentional redesign that does not require reference-media fidelity.

Do not use Recraft for:

- `audit` work;
- a clean existing SVG;
- faithful transformation or cleanup of an existing mark;
- a request that needs an input image, exact identity preservation, or other reference media;
- animation/export work that already has an acceptable source.

## Live preflight

1. Detect whether a live Higgsfield MCP generation capability is callable in the current session.
2. Query live availability for the exact model identifier `recraft_v4_1`.
3. Obtain the exact current cost for this batch from the live capability. Do not infer availability or cost from documentation, examples, memory, or a prior run.
4. Use `unavailable` when the live surface conclusively lacks the capability or exact model. Use `indeterminate` when capability, model, or exact cost cannot be queried reliably. Take the local route in either state.
5. Sanitize the prompt: include only public product/brand facts needed to design the mark. Remove secrets, private paths, internal hosts, hidden metadata, and unrelated repository content.
6. Present the sanitized brief, exact live cost, and these fixed settings:

```text
model: recraft_v4_1
model_type: utility_vector
resolution: 1k
aspect_ratio: 1:1
count: 1
background_color: null
```

Use `model_type: vector` only when the brief explicitly calls for a more expressive mark; explain that choice in the preflight.

7. Ask for explicit approval of that exact one-output batch. A general request to create a logo, approval from before the live cost check, or approval of a different batch is insufficient.
8. Stop with `Approval state: pending` until the user approves. Make no credit-consuming call.

## Approved generation

- Recheck live availability and cost if the prior preflight may have gone stale. If cost or settings changed, present the new preflight and request approval again.
- Generate only the approved batch.
- Treat the returned image as design input. Inspect it for readability, simple geometry, transparency intent, and brand fit, then author a self-contained SVG and validate that SVG locally.
- Do not imply that a provider result is an editable or validated SVG unless the actual file passes the strict validator.
- If the batch fails, report the failure and do not retry or spend more credits without a new live preflight and approval. Continue with the local route when the user still wants the logo.

## Local fallback

Use direct local SVG authoring when the provider is ineligible, unavailable, indeterminate, declined, or lacks a live cost. Do not treat fallback as degraded completion: it must meet the same SVG and motion contracts.

Use `drawio-diagrams` only when the mark is geometric and an editable diagram-style construction materially helps. Otherwise write a minimal self-contained SVG directly.
references/readme-audit-safety.md›
# README Audit Safety

Use this reference when inspecting local image references in README markup.

## Boundary

Declare a repository root and pass the README as a root-relative path:

```bash
node scripts/audit-readme-logo-assets.mjs --root . --readme README.md
```

Treat the README path and every local `src`, `srcset`, or Markdown image target as untrusted input.

Accept a local reference only when all of these are true:

- it is a relative filesystem path;
- normalization does not traverse above the declared root;
- it is not POSIX absolute, Windows drive-absolute, or UNC/network syntax;
- query and fragment suffixes are removed only for filesystem resolution, not to reinterpret a remote URL as local;
- the resolved file and every followed symlink stay inside the canonical root;
- the target is a regular file when an asset read is attempted.

Reject and report the reference without opening it when any check fails. Do not silently rewrite an unsafe reference into an allowed path.

Root-bounded `..` segments from a nested README are valid only when component-wise and canonical resolution remains inside the declared root. Preserve component order while walking: never collapse `symlink/..` lexically before checking the symlink target. Inline, angle-bracket, balanced-parenthesis, full/collapsed/shortcut reference images, HTML `src`, and candidate-wise `srcset` values receive the same boundary checks. Code examples and HTML comments are not live image references.

For the supported README contract, each `<source>` uses exactly one bare URL in `srcset`; `src` on `<source>` is inert and rejected. Descriptors and multi-candidate `srcset` values are outside this deterministic contract. A `<picture>` contains exactly one final `<img>`, every `<source>` precedes it, and the fallback has a non-empty `src` or single-bare-URL `srcset`, alt text, and positive-integer `width` and `height`. Nested or unclosed `<picture>` markup fails closed. Candidates or ancestors hidden with the HTML `hidden` attribute or obvious inline visibility/opacity styles cannot satisfy readiness.

## Read-only behavior

The audit may read the declared README and root-bounded referenced assets. README and inspected SVG content are capped at 5 MiB, image/reference counts are bounded, and raster animation checks use the bundled structural inspector. It must not create, rewrite, delete, rename, chmod, or stage files. It may report remote HTTP(S) and data URLs as remote or embedded without fetching them.

## Report

For each local or rejected reference, report its source location, normalized root-relative path when safe, and any compatibility risk. Verify the SVG with the bundled strict validator and distinguish static from animated GIF/PNG/APNG/WebP content. A reduced-motion source must be verified static, precede animated candidates, and retain a verified-static final `<img>` fallback.

Summarize rejected absolute, UNC, root-escaping traversal, missing, or symlink-escaping references separately. Never print the canonical root or raw filesystem errors. Exit `0` means clean, `1` means compatibility/readiness findings, and `2` means unsafe input or a path-boundary rejection.
references/readme-snippets.md›
# README Snippets

Use these snippets as starting points. Replace paths, dimensions, and alt text with project-specific values. Use the quality-first variant only after the committed GitHub README renders every candidate source correctly; otherwise use the conservative or static-only variant.

## Quality-first `<picture>`

```html
<p align="center">
  <picture>
    <source
      srcset="docs/assets/example-logo-static.webp"
      type="image/webp"
      media="(prefers-reduced-motion: reduce)"
    />
    <source srcset="docs/assets/example-logo-animated.webp" type="image/webp" />
    <source srcset="docs/assets/example-logo-animated.apng" type="image/apng" />
    <source srcset="docs/assets/example-logo-animated.gif" type="image/gif" />
    <img
      src="docs/assets/example-logo-static.png"
      alt="Example project logo"
      width="360"
      height="360"
    />
  </picture>
</p>
```

## Conservative GIF fallback

```html
<p align="center">
  <picture>
    <source srcset="docs/assets/example-logo-static.png" media="(prefers-reduced-motion: reduce)" />
    <source srcset="docs/assets/example-logo-animated.gif" type="image/gif" />
    <img
      src="docs/assets/example-logo-static.png"
      alt="Example project logo"
      width="360"
      height="360"
    />
  </picture>
</p>
```

## Static SVG or PNG fallback

```html
<p align="center">
  <img
    src="docs/assets/example-logo-static.png"
    alt="Example project logo"
    width="360"
    height="360"
  />
</p>
```

## GitHub Pages demo link

```html
<p align="center">
  <a href="https://OWNER.github.io/REPOSITORY/logo/">View animated logo demo</a>
</p>
```

## Light and dark variants

```html
<picture>
  <source srcset="docs/assets/example-logo-dark.png" media="(prefers-color-scheme: dark)" />
  <source srcset="docs/assets/example-logo-light.png" media="(prefers-color-scheme: light)" />
  <img
    src="docs/assets/example-logo-light.png"
    alt="Example project logo"
    width="360"
    height="360"
  />
</picture>
```

## Alt text rules

- Use meaningful alt text for a logo that carries brand identity, such as `Example project logo`.
- Use empty alt text only when the same name is already adjacent in text and the image is decorative.
- Do not put animation instructions or file-format details in alt text.
- Always include `width` and `height` to keep README layout stable.
scripts/audit-readme-logo-assets.mjs›
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { inspectAnimatedImageFile } from "./lib/animated-image.mjs";

const MAX_README_FILE_BYTES = 5 * 1024 * 1024;
const MAX_SVG_AUDIT_BYTES = 5 * 1024 * 1024;
const MAX_RASTER_AUDIT_BYTES = 5 * 1024 * 1024;
const MAX_VALIDATOR_OUTPUT_BYTES = 1024 * 1024;
const SVG_VALIDATOR_TIMEOUT_MS = 5_000;
const MAX_IMAGE_BLOCKS = 4_096;
const MAX_REFERENCE_DEFINITIONS = 4_096;
const MAX_ASSET_USES_PER_BLOCK = 8_192;
const MAX_MARKDOWN_IMAGE_SYNTAX_BYTES = 64 * 1024;
const MAX_LOCAL_ASSET_REFERENCES = 256;
const HTML_VOID_ELEMENTS = new Set([
  "area",
  "base",
  "br",
  "col",
  "embed",
  "hr",
  "img",
  "input",
  "link",
  "meta",
  "param",
  "source",
  "track",
  "wbr",
]);
const SVG_VALIDATOR = fileURLToPath(new URL("./validate_logo_svg.py", import.meta.url));

function usage() {
  return [
    "Usage: node audit-readme-logo-assets.mjs --root <repo-root> --readme <relative-path>",
    "",
    "Prints a read-only Markdown report for root-bounded README image/logo assets.",
    "",
    "Exit status: 0 clean; 1 compatibility/readiness findings; 2 unsafe input or path.",
  ].join("\n");
}

function auditFailure(message, code = "INVALID_INPUT") {
  const error = new Error(message);
  error.auditSafe = true;
  error.auditCode = code;
  return error;
}

function publicErrorMessage(error, fallback) {
  if (error?.auditSafe) return error.message;
  const code = /^[A-Z0-9_]+$/.test(error?.code || "") ? error.code : "IO_ERROR";
  return `${fallback} [${code}]`;
}

function parseArgs(argv) {
  const args = {};
  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === "--help" || arg === "-h") {
      args.help = true;
      continue;
    }
    if (arg === "--readme") {
      args.readme = argv[i + 1];
      i += 1;
      continue;
    }
    if (arg === "--root") {
      args.root = argv[i + 1];
      i += 1;
      continue;
    }
    throw auditFailure("Unknown command-line option", "UNKNOWN_OPTION");
  }
  return args;
}

function attrsFrom(tag) {
  const attrs = new Map();
  attrs.duplicates = new Set();
  const pattern = /([:\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
  let match;
  while ((match = pattern.exec(tag))) {
    const name = match[1].toLowerCase();
    if (attrs.has(name)) attrs.duplicates.add(name);
    else attrs.set(name, match[2] ?? match[3] ?? match[4] ?? "");
  }
  return attrs;
}

function hasAttributeName(tag, name, attrs = attrsFrom(tag)) {
  if (attrs.has(name)) return true;
  let quote = null;
  let outsideQuotes = "";
  for (const character of tag) {
    if (quote) {
      if (character === quote) quote = null;
      outsideQuotes += " ";
    } else if (character === '"' || character === "'") {
      quote = character;
      outsideQuotes += " ";
    } else {
      outsideQuotes += character;
    }
  }
  return new RegExp("\\s" + name + "(?=\\s|=|/?>)", "i").test(outsideQuotes);
}

function isObviouslyHiddenTag(tag, attrs = attrsFrom(tag)) {
  if (hasAttributeName(tag, "hidden", attrs)) return true;
  const properties = new Map();
  for (const declaration of (attrs.get("style") || "").split(";")) {
    const [rawName, ...rawValue] = declaration.split(":");
    if (rawValue.length === 0) continue;
    properties.set(
      rawName.trim().toLowerCase(),
      rawValue
        .join(":")
        .replace(/\s*!important\s*$/i, "")
        .trim()
        .toLowerCase(),
    );
  }
  if (properties.get("display") === "none") return true;
  if (["hidden", "collapse"].includes(properties.get("visibility"))) return true;
  const opacity = properties.get("opacity");
  if (opacity == null) return false;
  const numeric = Number.parseFloat(opacity);
  return Number.isFinite(numeric) && numeric <= 0;
}

function srcsetUrls(value) {
  const urls = [];
  let index = 0;
  while (index < value.length) {
    while (index < value.length && /[\t\n\f\r ,]/.test(value[index])) index += 1;
    if (index >= value.length) break;

    const start = index;
    while (index < value.length && !/[\t\n\f\r ]/.test(value[index])) index += 1;
    const token = value.slice(start, index);
    const dataMarker = token.search(/(?:^|,)data:/i);
    if (dataMarker !== -1) {
      const dataStart = token[dataMarker] === "," ? dataMarker + 1 : dataMarker;
      urls.push(...token.slice(0, dataMarker).split(",").filter(Boolean));
      urls.push(token.slice(dataStart));
    } else {
      urls.push(...token.split(",").filter(Boolean));
      if (token.endsWith(",")) continue;
    }

    let parentheses = 0;
    while (index < value.length) {
      const character = value[index];
      index += 1;
      if (character === "(") parentheses += 1;
      else if (character === ")" && parentheses > 0) parentheses -= 1;
      else if (character === "," && parentheses === 0) break;
    }
  }
  return urls;
}

function singleBareSrcsetUrl(value) {
  const literal = String(value || "").trim();
  const urls = srcsetUrls(literal);
  return urls.length === 1 && urls[0] === literal ? urls[0] : null;
}

function isPositiveReducedMotionMedia(value) {
  return /^\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)$/i.test(String(value || "").trim());
}

function isLocalAsset(value) {
  if (!value || value.startsWith("#") || value.startsWith("<")) return false;
  if (/^[a-z][a-z0-9+.-]*:/i.test(value) && !/^[a-z]:[\\/]/i.test(value)) return false;
  return true;
}

function hasHtmlCharacterReference(value) {
  return /&#(?:x[0-9a-f]+|[0-9]+);?|&[a-z][a-z0-9]+;/i.test(value);
}

function stripQueryHash(value) {
  const queryless = value.split("?")[0];
  if (hasHtmlCharacterReference(queryless)) return queryless;
  return queryless.split("#")[0];
}

function decodedPath(value, label) {
  let decoded = value;
  for (let iteration = 0; iteration < 4; iteration += 1) {
    let next;
    try {
      next = decodeURIComponent(decoded);
    } catch {
      throw auditFailure(`${label} contains invalid percent encoding`, "INVALID_PERCENT_ENCODING");
    }
    if (next === decoded) break;
    decoded = next;
  }
  if (/%[0-9a-f]{2}/i.test(decoded))
    throw auditFailure(`${label} is over-encoded`, "OVER_ENCODED_PATH");
  if (hasHtmlCharacterReference(decoded)) {
    throw auditFailure(
      `${label} contains an ambiguous HTML character reference`,
      "AMBIGUOUS_CHARACTER_REFERENCE",
    );
  }
  if (
    [...decoded].some((character) => {
      const code = character.codePointAt(0);
      return code < 0x20 || code === 0x7f;
    })
  ) {
    throw auditFailure(`${label} contains control characters`, "CONTROL_CHARACTER");
  }
  if (decoded !== decoded.trim())
    throw auditFailure(`${label} contains URL whitespace`, "URL_WHITESPACE");
  return decoded.replaceAll("\\", "/");
}

function relativeAssetPath(value, label) {
  const decoded = decodedPath(value, label);
  if (
    !decoded ||
    decoded.startsWith("/") ||
    decoded.startsWith("//") ||
    /^[a-z][a-z0-9+.-]*:/i.test(decoded) ||
    path.posix.isAbsolute(decoded) ||
    path.win32.isAbsolute(decoded)
  ) {
    throw auditFailure(`${label} must be a relative repository path`, "ABSOLUTE_OR_REMOTE_PATH");
  }
  return decoded;
}

function isWithin(rootPath, candidatePath) {
  const relative = path.relative(rootPath, candidatePath);
  return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== "..");
}

function resolveWithinRoot(rootRealPath, baseDirectory, value, label) {
  const relative = relativeAssetPath(value, label);
  const components = relative.split("/").filter((component) => component && component !== ".");
  let current = baseDirectory;
  if (!isWithin(rootRealPath, current)) {
    throw auditFailure(label + " base escapes the repository root", "ROOT_ESCAPE");
  }

  for (let index = 0; index < components.length; index += 1) {
    const component = components[index];
    if (component === "..") {
      current = path.dirname(current);
      if (!isWithin(rootRealPath, current)) {
        throw auditFailure(label + " escapes the repository root", "ROOT_ESCAPE");
      }
      continue;
    }

    const next = path.join(current, component);
    const isLast = index === components.length - 1;
    let stat;
    try {
      stat = fs.lstatSync(next);
    } catch (error) {
      if (error.code !== "ENOENT") throw error;
      const unresolved = path.resolve(next, ...components.slice(index + 1));
      if (!isWithin(rootRealPath, unresolved)) {
        throw auditFailure(label + " escapes the repository root", "ROOT_ESCAPE");
      }
      return {
        relative,
        path: unresolved,
        rootRelative: path.relative(rootRealPath, unresolved).replaceAll("\\", "/"),
        exists: false,
      };
    }

    if (stat.isSymbolicLink()) {
      let target;
      try {
        target = fs.realpathSync(next);
      } catch {
        throw auditFailure(`${label} contains a dangling symlink`, "DANGLING_SYMLINK");
      }
      if (!isWithin(rootRealPath, target)) {
        throw auditFailure(
          `${label} escapes the repository root through a symlink`,
          "SYMLINK_ESCAPE",
        );
      }
      const targetStat = fs.statSync(target);
      if (!isLast && !targetStat.isDirectory()) {
        throw auditFailure(
          label + " crosses a non-directory path component",
          "NON_DIRECTORY_COMPONENT",
        );
      }
      if (isLast && !targetStat.isFile()) {
        throw auditFailure(label + " must resolve to a regular file", "NOT_REGULAR_FILE");
      }
      current = target;
      continue;
    }

    if (!isLast && !stat.isDirectory()) {
      throw auditFailure(
        `${label} crosses a non-directory path component`,
        "NON_DIRECTORY_COMPONENT",
      );
    }
    if (isLast && !stat.isFile()) {
      throw auditFailure(`${label} must resolve to a regular file`, "NOT_REGULAR_FILE");
    }
    current = next;
  }
  if (!fs.statSync(current).isFile()) {
    throw auditFailure(`${label} must resolve to a regular file`, "NOT_REGULAR_FILE");
  }
  return {
    relative,
    path: current,
    rootRelative: path.relative(rootRealPath, current).replaceAll("\\", "/"),
    exists: true,
  };
}

function newlineOffsets(text) {
  const offsets = [];
  for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1)) {
    offsets.push(index);
  }
  return offsets;
}

function lineNumberAt(offsets, index) {
  let low = 0;
  let high = offsets.length;
  while (low < high) {
    const middle = Math.floor((low + high) / 2);
    if (offsets[middle] < index) low = middle + 1;
    else high = middle;
  }
  return low + 1;
}

function unique(values) {
  return [...new Set(values)];
}

function visibleControlCharacters(value) {
  return [...String(value)]
    .map((character) => {
      const code = character.codePointAt(0);
      if (character === "\n") return "\\n";
      if (character === "\r") return "\\r";
      if (character === "\t") return "\\t";
      if (character === "\f") return "\\f";
      if (code < 0x20 || code === 0x7f) {
        return `\\u${code.toString(16).padStart(4, "0")}`;
      }
      return character;
    })
    .join("");
}

function markdownCode(value) {
  const visible = visibleControlCharacters(value);
  if (!/[`|&<>]/.test(visible)) return `\`${visible}\``;
  return `<code>${visible
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;")
    .replaceAll("|", "&#124;")
    .replaceAll("`", "&#96;")}</code>`;
}

function normalizeReferenceLabel(value) {
  return String(value)
    .replace(/\\([\\[\]])/g, "$1")
    .trim()
    .replace(/\s+/g, " ")
    .toLowerCase();
}

function isEscapedAt(text, index) {
  let backslashes = 0;
  for (let cursor = index - 1; cursor >= 0 && text[cursor] === "\\"; cursor -= 1) {
    backslashes += 1;
  }
  return backslashes % 2 === 1;
}

function parseBracket(text, openIndex) {
  let depth = 1;
  let cursor = openIndex + 1;
  const limit = Math.min(text.length, openIndex + MAX_MARKDOWN_IMAGE_SYNTAX_BYTES);
  while (cursor < limit) {
    if (text[cursor] === "\\" && cursor + 1 < limit) {
      cursor += 2;
      continue;
    }
    if (text[cursor] === "[") depth += 1;
    else if (text[cursor] === "]") {
      depth -= 1;
      if (depth === 0) {
        return {
          value: text.slice(openIndex + 1, cursor),
          end: cursor + 1,
        };
      }
    }
    cursor += 1;
  }
  return null;
}

function unescapeMarkdownPunctuation(value) {
  return value.replace(/\\([!"#$%&'()*+,\-./:;<=>?@[\]^_`{|}~\\])/g, "$1");
}

function closingParenthesis(text, start, limit) {
  let depth = 1;
  let quote = null;
  for (let cursor = start; cursor < limit; cursor += 1) {
    const character = text[cursor];
    if (character === "\\") {
      cursor += 1;
      continue;
    }
    if (quote) {
      if (character === quote) quote = null;
      continue;
    }
    if (character === '"' || character === "'") {
      quote = character;
      continue;
    }
    if (character === "(") depth += 1;
    else if (character === ")") {
      depth -= 1;
      if (depth === 0) return cursor;
    }
  }
  return -1;
}

function parseInlineImageDestination(text, openIndex) {
  const limit = Math.min(text.length, openIndex + MAX_MARKDOWN_IMAGE_SYNTAX_BYTES);
  let cursor = openIndex + 1;
  while (cursor < limit && /[ \t\n\r]/.test(text[cursor])) cursor += 1;
  if (cursor >= limit) return null;

  let destination;
  let afterDestination;
  if (text[cursor] === "<") {
    const start = cursor + 1;
    cursor = start;
    while (cursor < limit) {
      if (text[cursor] === "\\" && cursor + 1 < limit) {
        cursor += 2;
        continue;
      }
      if (text[cursor] === ">") break;
      if (text[cursor] === "\n" || text[cursor] === "\r") return null;
      cursor += 1;
    }
    if (cursor >= limit || text[cursor] !== ">") return null;
    destination = text.slice(start, cursor);
    afterDestination = cursor + 1;
  } else {
    const start = cursor;
    let nested = 0;
    while (cursor < limit) {
      const character = text[cursor];
      if (character === "\\" && cursor + 1 < limit) {
        cursor += 2;
        continue;
      }
      if (character === "(") nested += 1;
      else if (character === ")") {
        if (nested === 0) {
          destination = text.slice(start, cursor);
          return destination
            ? {
                destination: unescapeMarkdownPunctuation(destination),
                end: cursor + 1,
              }
            : null;
        }
        nested -= 1;
      } else if (/\s/.test(character) && nested === 0) {
        break;
      }
      cursor += 1;
    }
    destination = text.slice(start, cursor);
    afterDestination = cursor;
  }
  if (!destination) return null;
  const close = closingParenthesis(text, afterDestination, limit);
  if (close === -1) return null;
  return {
    destination: unescapeMarkdownPunctuation(destination),
    end: close + 1,
  };
}

function mergeRanges(ranges) {
  const sorted = ranges
    .filter(([start, end]) => end > start)
    .sort((left, right) => left[0] - right[0] || left[1] - right[1]);
  const merged = [];
  for (const range of sorted) {
    const previous = merged.at(-1);
    if (previous && range[0] <= previous[1]) previous[1] = Math.max(previous[1], range[1]);
    else merged.push([...range]);
  }
  return merged;
}

function indexInRanges(index, ranges) {
  let low = 0;
  let high = ranges.length - 1;
  while (low <= high) {
    const middle = Math.floor((low + high) / 2);
    const [start, end] = ranges[middle];
    if (index < start) high = middle - 1;
    else if (index >= end) low = middle + 1;
    else return true;
  }
  return false;
}

function markdownLineRecords(markdown) {
  const records = [];
  let start = 0;
  while (start < markdown.length) {
    let contentEnd = start;
    while (
      contentEnd < markdown.length &&
      markdown[contentEnd] !== "\n" &&
      markdown[contentEnd] !== "\r"
    ) {
      contentEnd += 1;
    }
    let end = contentEnd;
    if (markdown[end] === "\r" && markdown[end + 1] === "\n") end += 2;
    else if (end < markdown.length) end += 1;
    records.push({ start, end, content: markdown.slice(start, contentEnd) });
    start = end;
  }
  return records;
}

function scanHtmlTags(markup, excludedRanges = [], { failOnUnclosed = true } = {}) {
  const tags = [];
  const headPattern = /<(\/)?([a-z][a-z0-9:-]*)(?=[\s/>])/iy;
  for (let cursor = 0; cursor < markup.length;) {
    const start = markup.indexOf("<", cursor);
    if (start === -1) break;
    cursor = start + 1;
    if (indexInRanges(start, excludedRanges)) {
      continue;
    }
    if (markup.startsWith("<!--", start)) {
      const commentEnd = markup.indexOf("-->", start + 4);
      cursor = commentEnd === -1 ? markup.length : commentEnd + 3;
      continue;
    }
    headPattern.lastIndex = start;
    const head = headPattern.exec(markup);
    if (!head) continue;

    const limit = Math.min(markup.length, start + MAX_MARKDOWN_IMAGE_SYNTAX_BYTES);
    let quote = null;
    let closed = false;
    for (let index = start + 1; index < limit; index += 1) {
      const character = markup[index];
      if (quote) {
        if (character === quote) quote = null;
        continue;
      }
      if (character === '"' || character === "'") {
        quote = character;
        continue;
      }
      if (character === ">") {
        tags.push({
          name: head[2].toLowerCase(),
          closing: Boolean(head[1]),
          start,
          end: index + 1,
          markup: markup.slice(start, index + 1),
        });
        cursor = index + 1;
        closed = true;
        break;
      }
    }
    if (!closed && failOnUnclosed) {
      throw auditFailure(
        `HTML <${head[2].toLowerCase()}> tag did not close within the ${MAX_MARKDOWN_IMAGE_SYNTAX_BYTES}-byte syntax limit`,
        "HTML_TAG_LIMIT",
      );
    }
    if (!closed) cursor = limit;
  }
  return tags;
}

function annotateHiddenAncestors(tags) {
  const stack = [];
  let hiddenDepth = 0;
  for (const tag of tags) {
    if (tag.closing) {
      const opening = stack.at(-1);
      if (opening?.name === tag.name) {
        stack.pop();
        if (opening.hidden) hiddenDepth -= 1;
      }
      continue;
    }
    tag.hiddenByAncestor = hiddenDepth > 0;
    const hidden = isObviouslyHiddenTag(tag.markup);
    if (!HTML_VOID_ELEMENTS.has(tag.name) && !/\/\s*>$/.test(tag.markup)) {
      stack.push({ name: tag.name, hidden });
      if (hidden) hiddenDepth += 1;
    }
  }
  return tags;
}

function htmlTagRanges(markdown, excludedRanges) {
  return scanHtmlTags(markdown, excludedRanges, {
    failOnUnclosed: false,
  }).map(({ start, end }) => [start, end]);
}

function markdownNonRenderedRanges(markdown) {
  const ranges = [];
  let fence = null;
  for (const line of markdownLineRecords(markdown)) {
    if (fence) {
      const closing = new RegExp(`^ {0,3}${fence.character}{${fence.length},}[ \\t]*$`);
      if (closing.test(line.content)) {
        ranges.push([fence.start, line.end]);
        fence = null;
      }
      continue;
    }

    const opening = line.content.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
    if (opening && !(opening[1][0] === "`" && opening[2].includes("`"))) {
      fence = {
        start: line.start,
        character: opening[1][0],
        length: opening[1].length,
      };
      continue;
    }
    if (/^(?: {4}|\t)/.test(line.content)) ranges.push([line.start, line.end]);
  }
  if (fence) ranges.push([fence.start, markdown.length]);

  const tentativeBlockRanges = mergeRanges(ranges);
  const tagRanges = htmlTagRanges(markdown, tentativeBlockRanges);
  const blockRanges = tentativeBlockRanges.filter(([start]) => !indexInRanges(start, tagRanges));
  const inlineDelimiterExclusions = mergeRanges([...blockRanges, ...tagRanges]);
  const runs = [];
  for (let index = 0; index < markdown.length;) {
    if (markdown[index] !== "`") {
      index += 1;
      continue;
    }
    const start = index;
    while (markdown[index] === "`") index += 1;
    let backslashes = 0;
    for (let cursor = start - 1; cursor >= 0 && markdown[cursor] === "\\"; cursor -= 1) {
      backslashes += 1;
    }
    if (backslashes % 2 === 0 && !indexInRanges(start, inlineDelimiterExclusions)) {
      runs.push({ start, end: index, length: index - start });
    }
  }
  const nextByLength = new Map();
  const nextSame = Array.from({ length: runs.length });
  for (let index = runs.length - 1; index >= 0; index -= 1) {
    nextSame[index] = nextByLength.get(runs[index].length);
    nextByLength.set(runs[index].length, index);
  }
  for (let index = 0; index < runs.length;) {
    const closingIndex = nextSame[index];
    if (closingIndex == null) {
      index += 1;
      continue;
    }
    ranges.push([runs[index].start, runs[closingIndex].end]);
    index = closingIndex + 1;
  }

  const codeRanges = mergeRanges(ranges);
  const commentsOutside = (excluded) => {
    const comments = [];
    let commentStart = markdown.indexOf("<!--");
    while (commentStart !== -1) {
      if (indexInRanges(commentStart, excluded)) {
        commentStart = markdown.indexOf("<!--", commentStart + 4);
        continue;
      }
      const close = markdown.indexOf("-->", commentStart + 4);
      const end = close === -1 ? markdown.length : close + 3;
      comments.push([commentStart, end]);
      commentStart = markdown.indexOf("<!--", end);
    }
    return mergeRanges(comments);
  };
  const initialComments = commentsOutside(mergeRanges([...codeRanges, ...tagRanges]));
  const filteredCodeRanges = codeRanges.filter(([start]) => !indexInRanges(start, initialComments));
  const commentRanges = commentsOutside(mergeRanges([...filteredCodeRanges, ...tagRanges]));
  return mergeRanges([...filteredCodeRanges, ...commentRanges]);
}

function referenceDefinitions(markdown, excludedRanges) {
  const definitions = new Map();
  let definitionCount = 0;
  const pattern =
    /^[ \t]{0,3}\[((?:\\.|[^\\\]\r\n])+)\]:[ \t]*(?:<([^>\r\n]*)>|(\S+))(?:[ \t]+(?:"[^"\r\n]*"|'[^'\r\n]*'|\([^\r\n)]*\)))?[ \t]*$/gm;
  let match;
  while ((match = pattern.exec(markdown))) {
    if (indexInRanges(match.index, excludedRanges)) continue;
    definitionCount += 1;
    if (definitionCount > MAX_REFERENCE_DEFINITIONS) {
      throw auditFailure(
        `README exceeds the ${MAX_REFERENCE_DEFINITIONS} reference-definition limit`,
        "REFERENCE_DEFINITION_LIMIT",
      );
    }
    const label = normalizeReferenceLabel(match[1]);
    const destination = match[2] ?? match[3];
    if (label && destination != null && !definitions.has(label)) {
      definitions.set(label, destination);
    }
  }
  return definitions;
}

function extractImageBlocks(markdown) {
  const blocks = [];
  const lineOffsets = newlineOffsets(markdown);
  const addBlock = (block) => {
    if (blocks.length >= MAX_IMAGE_BLOCKS) {
      throw auditFailure(
        `README exceeds the ${MAX_IMAGE_BLOCKS} image-block limit`,
        "IMAGE_BLOCK_LIMIT",
      );
    }
    blocks.push(block);
  };
  const pictureRanges = [];
  const excludedRanges = markdownNonRenderedRanges(markdown);
  const htmlTags = annotateHiddenAncestors(scanHtmlTags(markdown, excludedRanges));
  const markdownSyntaxExclusions = mergeRanges([
    ...excludedRanges,
    ...htmlTags.map(({ start, end }) => [start, end]),
  ]);
  const definitions = referenceDefinitions(markdown, markdownSyntaxExclusions);
  const openPictures = [];
  for (const tag of htmlTags) {
    if (tag.name !== "picture") continue;
    if (!tag.closing) {
      if (openPictures.length > 0) {
        throw auditFailure("Nested HTML <picture> elements are unsupported", "NESTED_PICTURE");
      }
      openPictures.push(tag);
      continue;
    }
    const opening = openPictures.pop();
    if (!opening) {
      throw auditFailure(
        "HTML </picture> does not have a matching opening tag",
        "UNMATCHED_PICTURE",
      );
    }
    pictureRanges.push([opening.start, tag.end]);
    addBlock({
      type: "picture",
      line: lineNumberAt(lineOffsets, opening.start),
      markup: markdown.slice(opening.start, tag.end),
      hiddenByAncestor: opening.hiddenByAncestor,
    });
  }
  if (openPictures.length > 0) {
    throw auditFailure("HTML <picture> element is not closed", "UNCLOSED_PICTURE");
  }

  const livePictureRanges = mergeRanges(pictureRanges);
  for (const tag of htmlTags) {
    if (tag.name !== "img" || tag.closing) continue;
    const insidePicture = indexInRanges(tag.start, livePictureRanges);
    addBlock({
      type: insidePicture ? "img in picture" : "img",
      line: lineNumberAt(lineOffsets, tag.start),
      markup: tag.markup,
      hiddenByAncestor: tag.hiddenByAncestor,
    });
  }

  for (let cursor = 0; cursor < markdown.length;) {
    const start = markdown.indexOf("![", cursor);
    if (start === -1) break;
    cursor = start + 2;
    if (isEscapedAt(markdown, start) || indexInRanges(start, markdownSyntaxExclusions)) continue;

    const alt = parseBracket(markdown, start + 1);
    if (!alt) {
      addBlock({
        type: "unparsed markdown image",
        line: lineNumberAt(lineOffsets, start),
        markup: "![",
        parseIssue: "alt text or label did not close within the syntax limit",
      });
      cursor = Math.min(markdown.length, start + MAX_MARKDOWN_IMAGE_SYNTAX_BYTES);
      continue;
    }

    if (markdown[alt.end] === "(") {
      const inline = parseInlineImageDestination(markdown, alt.end);
      if (!inline) {
        addBlock({
          type: "unparsed markdown image",
          line: lineNumberAt(lineOffsets, start),
          markup: markdown.slice(start, alt.end + 1),
          parseIssue: "inline destination could not be parsed within the syntax limit",
        });
        cursor = Math.min(markdown.length, alt.end + MAX_MARKDOWN_IMAGE_SYNTAX_BYTES);
        continue;
      }
      addBlock({
        type: "markdown image",
        line: lineNumberAt(lineOffsets, start),
        markup: markdown.slice(start, inline.end),
        urls: [inline.destination],
      });
      cursor = inline.end;
      continue;
    }

    let label = alt.value;
    let end = alt.end;
    if (markdown[alt.end] === "[") {
      const explicit = parseBracket(markdown, alt.end);
      if (!explicit) {
        addBlock({
          type: "unparsed markdown image reference",
          line: lineNumberAt(lineOffsets, start),
          markup: markdown.slice(start, alt.end + 1),
          parseIssue: "reference label could not be parsed within the syntax limit",
        });
        cursor = Math.min(markdown.length, alt.end + MAX_MARKDOWN_IMAGE_SYNTAX_BYTES);
        continue;
      }
      label = explicit.value || alt.value;
      end = explicit.end;
    }
    const destination = definitions.get(normalizeReferenceLabel(label));
    addBlock({
      type: "markdown image reference",
      line: lineNumberAt(lineOffsets, start),
      markup: markdown.slice(start, end),
      ...(destination
        ? { urls: [destination] }
        : { parseIssue: "reference definition was not resolved" }),
    });
    cursor = end;
  }

  return blocks.sort((a, b) => a.line - b.line);
}

function assetUsesFromBlock(block) {
  const uses = (block.urls || []).map((ref) => ({
    ref,
    role: "image",
    media: "",
    eligible: true,
  }));
  for (const tag of scanHtmlTags(block.markup)) {
    if (tag.closing || !["source", "img"].includes(tag.name)) continue;
    const tagName = tag.name;
    const attrs = attrsFrom(tag.markup);
    const media = attrs.get("media") || "";
    const role =
      tagName === "source" && isPositiveReducedMotionMedia(media)
        ? "reduced-motion"
        : tagName === "source"
          ? "source"
          : "fallback";
    if (attrs.has("src")) {
      uses.push({
        ref: attrs.get("src"),
        role,
        media,
        eligible: tagName === "img",
      });
    }
    if (attrs.has("srcset")) {
      const bareUrl = singleBareSrcsetUrl(attrs.get("srcset"));
      for (const ref of srcsetUrls(attrs.get("srcset"))) {
        uses.push({ ref, role, media, eligible: ref === bareUrl });
      }
    }
    if (uses.length > MAX_ASSET_USES_PER_BLOCK) {
      throw auditFailure(
        `image block exceeds the ${MAX_ASSET_USES_PER_BLOCK} asset-use limit`,
        "ASSET_USE_LIMIT",
      );
    }
  }

  return uses;
}

function urlsFromBlock(block) {
  return unique(
    assetUsesFromBlock(block)
      .map(({ ref }) => ref)
      .filter(isLocalAsset)
      .map(stripQueryHash),
  );
}

function blockFindings(block) {
  const findings = [];
  if (block.hiddenByAncestor) {
    findings.push(
      "line " +
        block.line +
        ": image block has a hidden ancestor and cannot provide a meaningful fallback",
    );
  }
  if (block.parseIssue) {
    findings.push(
      `line ${block.line}: Markdown image readiness is unverified (${block.parseIssue})`,
    );
  }
  const scannedTags = scanHtmlTags(block.markup);
  const tags = scannedTags.filter(
    ({ closing, name }) => !closing && ["source", "img"].includes(name),
  );
  if (block.type === "picture") {
    const openingPicture = scannedTags.find(({ closing, name }) => !closing && name === "picture");
    if (openingPicture && isObviouslyHiddenTag(openingPicture.markup)) {
      findings.push(
        "line " + block.line + ": <picture> is hidden and cannot provide a meaningful fallback",
      );
    }
    const imageIndexes = tags.flatMap((tag, index) => (tag.name === "img" ? [index] : []));
    if (imageIndexes.length === 0) {
      findings.push("line " + block.line + ": picture is missing a final <img> fallback");
    } else if (imageIndexes.length > 1) {
      findings.push("line " + block.line + ": picture must contain exactly one <img> fallback");
    }
    if (imageIndexes.length > 0 && imageIndexes.at(-1) !== tags.length - 1) {
      findings.push(
        "line " + block.line + ": final <img> fallback must follow every <source> candidate",
      );
    }
  }
  for (const tag of tags) {
    const tagName = tag.name;
    const attrs = attrsFrom(tag.markup);
    if (isObviouslyHiddenTag(tag.markup, attrs)) {
      findings.push(
        "line " +
          block.line +
          ": <" +
          tagName +
          "> is hidden and cannot provide a meaningful image candidate",
      );
    }
    const relevant =
      tagName === "source"
        ? new Set(["src", "srcset", "media"])
        : new Set(["src", "srcset", "alt", "width", "height"]);
    for (const duplicate of attrs.duplicates) {
      if (relevant.has(duplicate)) {
        findings.push(
          `line ${block.line}: <${tagName}> contains duplicate ${duplicate} attributes`,
        );
      }
    }
    if (tagName === "img") {
      for (const required of ["alt", "width", "height"]) {
        if (!attrs.has(required)) findings.push(`line ${block.line}: <img> missing ${required}`);
      }
      for (const dimension of ["width", "height"]) {
        if (attrs.has(dimension) && !/^[1-9]\d*$/.test(attrs.get(dimension).trim())) {
          findings.push(
            "line " + block.line + ": <img> " + dimension + " must be a positive integer",
          );
        }
      }
    } else if (
      /prefers-reduced-motion/i.test(attrs.get("media") || "") &&
      !isPositiveReducedMotionMedia(attrs.get("media"))
    ) {
      findings.push(
        `line ${block.line}: <source> media is not an unambiguous positive reduced-motion query`,
      );
    }
    if (tagName === "source" && attrs.has("src")) {
      findings.push(
        "line " + block.line + ": <source> must use srcset; src is ignored by picture rendering",
      );
    }
    const bareSrcset = attrs.has("srcset") ? singleBareSrcsetUrl(attrs.get("srcset")) : null;
    if (attrs.has("srcset") && !bareSrcset) {
      findings.push(
        "line " +
          block.line +
          ": <" +
          tagName +
          "> srcset must contain exactly one bare URL without descriptors",
      );
    }
    const hasCandidate =
      (tagName === "img" && attrs.has("src") && attrs.get("src").trim().length > 0) ||
      Boolean(bareSrcset);
    if (!hasCandidate) {
      findings.push(
        "line " +
          block.line +
          ": <" +
          tagName +
          "> must contain a non-empty src or srcset candidate",
      );
    }
  }

  for (const pattern of [
    ["<script", /<script\b/i],
    ["<foreignObject", /<foreignobject\b/i],
    ["@keyframes", /@keyframes/i],
    ["animation:", /animation\s*:/i],
    ["external href", /\b(?:href|xlink:href)\s*=\s*["'](?:https?:|\/\/|data:)/i],
    ["embedded data image", /\b(?:src|srcset)\s*=\s*["'][^"']*data:/i],
  ]) {
    if (pattern[1].test(block.markup))
      findings.push(`line ${block.line}: markup contains ${pattern[0]}`);
  }

  return findings;
}

function formatBytes(bytes) {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

function readUtf8FileBounded(file, maxBytes, label) {
  const handle = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
  try {
    const stat = fs.fstatSync(handle);
    if (!stat.isFile())
      throw auditFailure(`${label} must resolve to a regular file`, "NOT_REGULAR_FILE");
    if (stat.size > maxBytes) {
      throw auditFailure(`${label} exceeds the ${maxBytes}-byte audit limit`, "FILE_TOO_LARGE");
    }

    const buffer = Buffer.alloc(stat.size);
    let offset = 0;
    while (offset < buffer.length) {
      const bytesRead = fs.readSync(handle, buffer, offset, buffer.length - offset, null);
      if (bytesRead === 0) break;
      offset += bytesRead;
    }
    return buffer.subarray(0, offset).toString("utf8");
  } finally {
    fs.closeSync(handle);
  }
}

function validationCodes(stdout) {
  try {
    const parsed = JSON.parse(stdout);
    return unique(
      (parsed.errors || [])
        .map(({ code }) => String(code || ""))
        .filter((code) => /^[a-z0-9-]+$/.test(code)),
    );
  } catch {
    return [];
  }
}

function inspectSvgReadiness(file) {
  const result = spawnSync("python3", [SVG_VALIDATOR, "--json", file], {
    encoding: "utf8",
    timeout: SVG_VALIDATOR_TIMEOUT_MS,
    maxBuffer: MAX_VALIDATOR_OUTPUT_BYTES,
  });
  if (result.error) {
    const reason =
      result.error.code === "ETIMEDOUT"
        ? "strict SVG validator timed out"
        : result.error.code === "ENOENT"
          ? "strict SVG validator is unavailable"
          : "strict SVG validator could not run";
    return { status: "unverified", reason };
  }
  if (result.status === 0) {
    try {
      const parsed = JSON.parse(result.stdout);
      if (parsed.valid === true) return { status: "static", format: "svg", frameCount: 1 };
    } catch {
      // The validator contract is JSON; malformed output is unverified.
    }
    return {
      status: "unverified",
      reason: "strict SVG validator returned invalid output",
    };
  }
  const codes = validationCodes(result.stdout);
  if (result.status === 1) {
    return {
      status: "invalid",
      reason: codes.length
        ? `strict SVG validation failed: ${codes.join(", ")}`
        : "strict SVG validation failed",
    };
  }
  return {
    status: "unverified",
    reason: codes.length
      ? `strict SVG validation is unverified: ${codes.join(", ")}`
      : "strict SVG validation is unverified",
  };
}

function motionStateForUse(use, assetStates) {
  if (!isLocalAsset(use.ref)) {
    return {
      status: "unverified",
      reason: "remote or embedded candidate was not fetched",
      display: `${use.role} candidate`,
    };
  }
  const ref = stripQueryHash(use.ref);
  return (
    assetStates.get(ref) || {
      status: "unverified",
      reason: "local candidate was not resolved",
      display: `${use.role} candidate`,
    }
  );
}

function motionReadinessFindings(blocks, assetStates) {
  const findings = [];
  for (const block of blocks) {
    if (block.type === "img in picture") continue;
    const evaluated = assetUsesFromBlock(block)
      .filter(({ eligible }) => eligible !== false)
      .map((use, index) => ({
        ...use,
        index,
        state: motionStateForUse(use, assetStates),
      }));
    if (evaluated.length === 0) continue;

    const reported = new Set();
    for (const entry of evaluated) {
      if (!["invalid", "unverified"].includes(entry.state.status)) continue;
      const key = `${entry.role}\u0000${entry.state.display}\u0000${entry.state.reason}`;
      if (reported.has(key)) continue;
      reported.add(key);
      const display = markdownCode(entry.state.display);
      const disposition =
        entry.state.status === "unverified" ? "readiness is unverified" : "is invalid";
      findings.push(
        `line ${block.line}: ${entry.role} ${display} ${disposition} (${entry.state.reason})`,
      );
    }

    const animated = evaluated.filter(
      ({ role, state }) => role !== "reduced-motion" && state.status === "animated",
    );
    if (block.type === "picture") {
      const fallbacks = evaluated.filter(({ role }) => role === "fallback");
      const finalFallback = fallbacks.at(-1);
      if (!finalFallback) {
        findings.push(`line ${block.line}: picture is missing a final <img> fallback`);
      } else {
        if (finalFallback.index !== evaluated.length - 1) {
          findings.push(
            `line ${block.line}: final <img> fallback must follow every <source> candidate`,
          );
        }
        for (const fallback of fallbacks) {
          if (fallback.state.status !== "static") {
            findings.push(
              `line ${block.line}: final <img> fallback ${markdownCode(fallback.state.display)} was not verified static`,
            );
          }
        }
      }
    }
    if (animated.length === 0) continue;
    const reduced = evaluated.filter(({ role }) => role === "reduced-motion");
    if (reduced.length === 0) {
      findings.push(`line ${block.line}: verified animated source without a reduced-motion source`);
      continue;
    }
    const staticReduced = reduced.filter(({ state }) => state.status === "static");
    for (const animatedEntry of animated) {
      if (!staticReduced.some(({ index }) => index < animatedEntry.index)) {
        findings.push(
          `line ${block.line}: verified-static reduced-motion source must precede animated candidate ${markdownCode(animatedEntry.state.display)}`,
        );
      }
    }
    for (const entry of reduced) {
      if (entry.state.status === "animated") {
        findings.push(
          `line ${block.line}: reduced-motion source ${markdownCode(entry.state.display)} is animated`,
        );
      }
    }
    if (staticReduced.length === 0) {
      findings.push(`line ${block.line}: no reduced-motion source was verified static`);
    }
  }
  return findings;
}

function main() {
  let args;
  try {
    args = parseArgs(process.argv.slice(2));
  } catch (error) {
    console.error(publicErrorMessage(error, "invalid command-line input"));
    console.error(usage());
    process.exit(2);
  }

  if (args.help) {
    console.log(usage());
    return;
  }

  if (!args.root || !args.readme) {
    console.error("Missing required options: --root and --readme");
    console.error(usage());
    process.exit(2);
  }

  let rootPath;
  let readmeResolution;
  try {
    rootPath = fs.realpathSync(path.resolve(args.root));
    if (!fs.statSync(rootPath).isDirectory())
      throw auditFailure("--root must resolve to a directory", "INVALID_ROOT");
    readmeResolution = resolveWithinRoot(rootPath, rootPath, args.readme, "README path");
    if (!readmeResolution.exists)
      throw auditFailure("README path does not exist", "MISSING_README");
  } catch (error) {
    console.error(
      `Unsafe or invalid input: ${publicErrorMessage(error, "repository boundary check failed")}`,
    );
    process.exit(2);
  }

  const readmePath = readmeResolution.path;
  let markdown;
  try {
    markdown = readUtf8FileBounded(readmePath, MAX_README_FILE_BYTES, "README");
  } catch (error) {
    console.error(
      `Unable to read declared README: ${publicErrorMessage(error, "README read failed")}`,
    );
    process.exit(error.auditCode === "FILE_TOO_LARGE" ? 2 : 1);
  }

  const readmeDir = path.dirname(readmePath);
  let blocks;
  let blockNotes;
  let assetRefs;
  try {
    blocks = extractImageBlocks(markdown);
    blockNotes = blocks.flatMap(blockFindings);
    assetRefs = unique(blocks.flatMap((block) => urlsFromBlock(block)));
    if (assetRefs.length > MAX_LOCAL_ASSET_REFERENCES) {
      throw auditFailure(
        `README exceeds the ${MAX_LOCAL_ASSET_REFERENCES} local-asset limit`,
        "LOCAL_ASSET_LIMIT",
      );
    }
  } catch (error) {
    console.error(
      `Unable to parse declared README safely: ${publicErrorMessage(error, "README parse limit exceeded")}`,
    );
    process.exit(2);
  }
  const assetRows = [];
  const assetNotes = [];
  const assetStates = new Map();
  const assetDisplayPaths = new Map();
  let unsafeAssetCount = 0;

  for (const ref of assetRefs) {
    let resolved;
    try {
      resolved = resolveWithinRoot(rootPath, readmeDir, ref, "asset reference");
    } catch (error) {
      unsafeAssetCount += 1;
      const displayPath = "rejected asset reference";
      assetDisplayPaths.set(ref, displayPath);
      assetStates.set(ref, {
        status: "unverified",
        reason: "unsafe path was rejected",
        display: displayPath,
      });
      assetRows.push({
        displayPath,
        exists: false,
        size: "-",
        safety: "rejected",
      });
      assetNotes.push(
        `${markdownCode(displayPath)}: rejected unsafe path (${markdownCode(
          publicErrorMessage(error, "path resolution failed"),
        )})`,
      );
      continue;
    }

    const fullPath = resolved.path;
    const exists = resolved.exists;
    const displayPath = resolved.rootRelative || ".";
    assetDisplayPaths.set(ref, displayPath);
    let size = "-";
    if (!exists) {
      assetStates.set(ref, {
        status: "unverified",
        reason: "local asset is missing",
        display: displayPath,
      });
      assetNotes.push(`${markdownCode(displayPath)}: missing local asset`);
      assetRows.push({ displayPath, exists, size, safety: "safe" });
      continue;
    }

    try {
      const stat = fs.statSync(fullPath);
      size = formatBytes(stat.size);
      const lowerPath = resolved.rootRelative.toLowerCase();
      let state;
      if (lowerPath.endsWith(".svg")) {
        state =
          stat.size > MAX_SVG_AUDIT_BYTES
            ? {
                status: "unverified",
                reason: `SVG exceeds the ${MAX_SVG_AUDIT_BYTES}-byte audit limit`,
              }
            : inspectSvgReadiness(fullPath);
      } else if (/\.(?:gif|png|apng|webp)$/.test(lowerPath)) {
        try {
          const inspected = inspectAnimatedImageFile(fullPath, {
            maxFileBytes: MAX_RASTER_AUDIT_BYTES,
          });
          state = {
            status: inspected.animated ? "animated" : "static",
            format: inspected.format,
            frameCount: inspected.frameCount,
          };
        } catch (error) {
          const code = /^[A-Z0-9_]+$/.test(error?.code || "") ? error.code : "INSPECTION_ERROR";
          state = {
            status: ["FILE_LIMIT", "READ_ERROR"].includes(code) ? "unverified" : "invalid",
            reason: `raster inspection failed [${code}]`,
          };
        }
      } else {
        state = {
          status: "unverified",
          reason: "format is not SVG, GIF, PNG/APNG, or WebP",
        };
      }
      assetStates.set(ref, { ...state, display: displayPath });
    } catch (error) {
      assetStates.set(ref, {
        status: "unverified",
        reason: publicErrorMessage(error, "filesystem inspection failed"),
        display: displayPath,
      });
    }
    assetRows.push({ displayPath, exists, size, safety: "safe" });
  }
  const motionNotes = motionReadinessFindings(blocks, assetStates);

  console.log("# README Logo Asset Audit");
  console.log("");
  console.log("- Root boundary: declared repository root");
  console.log(`- README: ${markdownCode(readmeResolution.rootRelative)}`);
  console.log(`- Image blocks found: ${blocks.length}`);
  console.log(`- Local asset references found: ${assetRefs.length}`);
  console.log("");

  console.log("## Image blocks");
  if (blocks.length === 0) {
    console.log("");
    console.log("- None found.");
  } else {
    console.log("");
    for (const block of blocks) {
      const refs = urlsFromBlock(block).map(
        (ref) => assetDisplayPaths.get(ref) || "unresolved asset reference",
      );
      console.log(
        `- Line ${block.line}: ${block.type}${refs.length ? ` (${refs.map(markdownCode).join(", ")})` : ""}`,
      );
    }
  }

  console.log("");
  console.log("## Local assets");
  console.log("");
  if (assetRows.length === 0) {
    console.log("- None found.");
  } else {
    console.log("| Path | Safe | Exists | Size |");
    console.log("| --- | --- | --- | --- |");
    for (const row of assetRows) {
      console.log(
        `| ${markdownCode(row.displayPath)} | ${row.safety === "safe" ? "yes" : "no"} | ${row.exists ? "yes" : "no"} | ${row.size} |`,
      );
    }
  }

  const findings = [...blockNotes, ...assetNotes, ...motionNotes];
  console.log("");
  console.log("## Findings");
  console.log("");
  if (findings.length === 0) {
    console.log("- No compatibility findings.");
  } else {
    for (const finding of findings) console.log(`- ${finding}`);
  }

  if (unsafeAssetCount > 0) process.exitCode = 2;
  else if (findings.length > 0) process.exitCode = 1;
}

main();
scripts/export-readme-logo-animation.mjs›
#!/usr/bin/env node

import { spawnSync } from "node:child_process";
import {
  chmodSync,
  closeSync,
  constants,
  copyFileSync,
  existsSync,
  fstatSync,
  lstatSync,
  mkdirSync,
  mkdtempSync,
  openSync,
  readFileSync,
  readSync,
  realpathSync,
  rmSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { pathToFileURL } from "node:url";
import { TextDecoder } from "node:util";

import { inspectAnimatedImageFile } from "./lib/animated-image.mjs";
import {
  captureOutputSnapshot,
  removeAnchoredStageDirectory,
  replaceOutputsAtomically,
  TransactionalOutputError,
} from "./lib/transactional-output.mjs";

const MAX_DIMENSION = 4096;
const MAX_FRAME_COUNT = 1000;
const MAX_FRAME_BYTES = 8 * 1024 * 1024;
const MAX_TOTAL_FRAME_BYTES = 4 * MAX_FRAME_BYTES;
const MAX_SOURCE_BYTES = 8 * 1024 * 1024;
const MAX_GIF_BYTES = 20 * 1024 * 1024;
const MAX_TOTAL_PIXELS = 250_000_000;
const DEFAULT_GIF_BYTES = 5 * 1024 * 1024;
const DEFAULT_GIF_COLORS = 128;

const deterministicEnvironment = {
  ...process.env,
  LANG: "C",
  LC_ALL: "C",
  SOURCE_DATE_EPOCH: "0",
  TZ: "UTC",
};
delete deterministicEnvironment.FFREPORT;

class ExportError extends Error {
  constructor(code, message, category = "validation") {
    super(message);
    this.name = "ExportError";
    this.code = code;
    this.category = category;
  }
}

function fail(code, message, category) {
  throw new ExportError(code, message, category);
}

function usage() {
  return `Usage: node export-readme-logo-animation.mjs --root <repository> --recipe <relative.mjs> [options]

Export a static PNG and animated GIF from a trusted, repository-owned JavaScript recipe.
The recipe, canonical SVG, and output paths must remain inside the declared repository root.

Options:
  --root PATH       Existing repository root
  --recipe PATH     Root-relative .mjs recipe module
  --check           Validate the recipe and every frame without exporter-controlled writes
  --replace         Replace existing outputs only after both new artifacts validate
  --json            Emit one deterministic JSON object
  -h, --help        Show this help

The exporter runs rsvg-convert and ffmpeg but never installs them. Inspect the skill's
references/local-tooling.md and obtain approval before installing a missing command.

Exit codes: 0=checked/exported; 1=invalid recipe/export failure; 2=usage or I/O failure.`;
}

function requiredValue(argv, index, option) {
  const value = argv[index];
  if (!value || value.startsWith("--")) {
    fail("MISSING_OPTION_VALUE", `${option} requires a value`, "usage");
  }
  return value;
}

function parseArgs(argv) {
  const args = { check: false, json: false, replace: false };
  for (let index = 0; index < argv.length; index += 1) {
    const argument = argv[index];
    if (argument === "--root") {
      args.root = requiredValue(argv, ++index, argument);
    } else if (argument === "--recipe") {
      args.recipe = requiredValue(argv, ++index, argument);
    } else if (argument === "--check") {
      args.check = true;
    } else if (argument === "--json") {
      args.json = true;
    } else if (argument === "--replace") {
      args.replace = true;
    } else if (argument === "--help" || argument === "-h") {
      args.help = true;
    } else {
      fail("UNKNOWN_OPTION", "unknown option", "usage");
    }
  }
  return args;
}

function isWithin(parent, candidate) {
  const rel = relative(parent, candidate);
  return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`));
}

function canonicalDirectory(input, label) {
  let canonical;
  let stats;
  try {
    canonical = realpathSync(resolve(input));
    stats = statSync(canonical);
  } catch {
    fail("INVALID_ROOT", `${label} must name an existing directory`, "io");
  }
  if (!stats.isDirectory()) {
    fail("INVALID_ROOT", `${label} must name an existing directory`, "io");
  }
  return canonical;
}

function safeRelativePath(value, label, expectedExtension) {
  if (typeof value !== "string" || value.length === 0) {
    fail("INVALID_PATH", `${label} must be a non-empty relative path`);
  }
  if (/\0|[\u0001-\u001f\u007f]/u.test(value)) {
    fail("INVALID_PATH", `${label} must not contain control characters`);
  }
  if (
    isAbsolute(value) ||
    /^[a-zA-Z]:[\\/]/.test(value) ||
    /^\\\\/.test(value) ||
    /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value)
  ) {
    fail("INVALID_PATH", `${label} must be a root-relative repository path`);
  }
  if (expectedExtension && extname(value).toLowerCase() !== expectedExtension) {
    fail("INVALID_PATH", `${label} must end in ${expectedExtension}`);
  }
  return value;
}

function lexicalPath(root, value, label) {
  const candidate = resolve(root, safeRelativePath(value, label));
  if (!isWithin(root, candidate)) {
    fail("ROOT_ESCAPE", `${label} escapes the declared repository root`);
  }
  return candidate;
}

function existingRegularFile(root, value, label, expectedExtension) {
  safeRelativePath(value, label, expectedExtension);
  const candidate = lexicalPath(root, value, label);
  let canonical;
  let stats;
  try {
    canonical = realpathSync(candidate);
    stats = statSync(canonical);
  } catch {
    fail("MISSING_INPUT", `${label} must name an existing regular file`, "io");
  }
  if (!isWithin(root, canonical)) {
    fail("ROOT_ESCAPE", `${label} escapes the declared repository root through a symlink`);
  }
  if (!stats.isFile()) {
    fail("INVALID_INPUT", `${label} must name an existing regular file`, "io");
  }
  return canonical;
}

function nearestExistingAncestor(candidate, label) {
  let current = candidate;
  while (true) {
    try {
      lstatSync(current);
      return current;
    } catch (error) {
      if (error?.code !== "ENOENT" && error?.code !== "ENOTDIR") {
        fail("INVALID_OUTPUT", `${label} parent could not be inspected`, "io");
      }
    }
    const parent = dirname(current);
    if (parent === current) return current;
    current = parent;
  }
}

function safeOutputPath(root, value, label, expectedExtension) {
  safeRelativePath(value, label, expectedExtension);
  const candidate = lexicalPath(root, value, label);
  const ancestor = nearestExistingAncestor(dirname(candidate), label);
  let canonicalAncestor;
  let ancestorStats;
  try {
    canonicalAncestor = realpathSync(ancestor);
    ancestorStats = statSync(canonicalAncestor);
  } catch {
    fail("INVALID_OUTPUT", `${label} has no resolvable parent directory`, "io");
  }
  if (!isWithin(root, canonicalAncestor)) {
    fail("ROOT_ESCAPE", `${label} escapes the declared repository root through a symlink`);
  }
  if (!ancestorStats.isDirectory()) {
    fail("INVALID_OUTPUT", `${label} parent must resolve through directories`, "io");
  }
  let candidateStats;
  try {
    candidateStats = lstatSync(candidate);
  } catch (error) {
    if (error?.code !== "ENOENT") {
      fail("INVALID_OUTPUT", `${label} could not be inspected`, "io");
    }
  }
  if (candidateStats) {
    const stats = candidateStats;
    if (stats.isSymbolicLink() || !stats.isFile()) {
      fail("INVALID_OUTPUT", `${label} must be absent or an existing regular file`, "io");
    }
    if (!isWithin(root, realpathSync(candidate))) {
      fail("ROOT_ESCAPE", `${label} escapes the declared repository root through a symlink`);
    }
  }
  return candidate;
}

function outputDirectoryIdentity(root, directory, label) {
  let before;
  let canonical;
  let after;
  try {
    before = lstatSync(directory);
    if (before.isSymbolicLink()) {
      fail("ROOT_ESCAPE", `${label} parent component must not be a symlink`, "io");
    }
    if (!before.isDirectory()) {
      fail("INVALID_OUTPUT", `${label} parent components must be real directories`, "io");
    }
    canonical = realpathSync(directory);
    after = lstatSync(directory);
  } catch (error) {
    if (error instanceof ExportError) throw error;
    fail("INVALID_OUTPUT", `${label} parent component could not be inspected`, "io");
  }
  if (
    before.dev !== after.dev ||
    before.ino !== after.ino ||
    after.isSymbolicLink() ||
    !after.isDirectory()
  ) {
    fail("INVALID_OUTPUT", `${label} parent component changed during inspection`, "io");
  }
  if (!isWithin(root, canonical)) {
    fail("ROOT_ESCAPE", `${label} parent escaped the declared repository root`, "io");
  }
  return { path: directory, canonical, dev: after.dev, ino: after.ino };
}

function descriptorDirectoryRoot() {
  if (process.platform === "linux") return "/proc/self/fd";
  if (process.platform === "win32") return null;
  return "/dev/fd";
}

function directoryOpenFlags() {
  return (
    constants.O_RDONLY |
    (constants.O_DIRECTORY ?? 0) |
    (process.platform === "win32" ? 0 : (constants.O_NOFOLLOW ?? 0))
  );
}

function descriptorPath(descriptor) {
  const root = descriptorDirectoryRoot();
  return root ? `${root}/${descriptor}` : null;
}

function ensureOutputParent(root, outputPath, label) {
  const parent = dirname(outputPath);
  const parentRelative = relative(root, parent);
  if (parentRelative === ".." || parentRelative.startsWith(`..${sep}`)) {
    fail("ROOT_ESCAPE", `${label} parent escapes the declared repository root`, "io");
  }

  const descriptorDirectory = descriptorDirectoryRoot();
  if (!descriptorDirectory) {
    fail(
      "INVALID_OUTPUT",
      `${label} cannot be mutated safely because this platform has no descriptor-directory view`,
      "io",
    );
  }
  const components =
    parentRelative === "" || parentRelative === "."
      ? []
      : parentRelative.split(sep).filter(Boolean);
  let current = root;
  const directoryFlags = directoryOpenFlags();
  let descriptor;
  try {
    descriptor = openSync(root, directoryFlags);
    for (const component of components) {
      const next = join(current, component);
      const anchored = `${descriptorDirectory}/${descriptor}/${component}`;
      let childDescriptor;
      try {
        childDescriptor = openSync(anchored, directoryFlags);
      } catch (error) {
        if (error?.code !== "ENOENT") {
          fail("ROOT_ESCAPE", `${label} parent component could not be opened safely`, "io");
        }
        // POSIX hosts expose the held directory descriptor through /proc or /dev/fd.
        // Creating exactly one child below that descriptor prevents a raced lexical
        // ancestor from redirecting this mutation outside --root. EEXIST is safe only if
        // the no-follow open below
        // subsequently proves that the peer created a real directory.
        try {
          mkdirSync(anchored);
        } catch (mkdirError) {
          if (mkdirError?.code !== "EEXIST") {
            fail("INVALID_OUTPUT", `${label} parent component could not be created`, "io");
          }
        }
        try {
          childDescriptor = openSync(anchored, directoryFlags);
        } catch {
          fail("ROOT_ESCAPE", `${label} parent component became unsafe`, "io");
        }
      }
      const childStats = fstatSync(childDescriptor);
      if (!childStats.isDirectory()) {
        closeSync(childDescriptor);
        fail("INVALID_OUTPUT", `${label} parent component is not a directory`, "io");
      }
      let lexical;
      try {
        lexical = outputDirectoryIdentity(root, next, label);
      } catch (error) {
        closeSync(childDescriptor);
        throw error;
      }
      if (lexical.dev !== childStats.dev || lexical.ino !== childStats.ino) {
        closeSync(childDescriptor);
        fail("INVALID_OUTPUT", `${label} parent component changed during creation`, "io");
      }
      closeSync(descriptor);
      descriptor = childDescriptor;
      current = next;
    }
    const identity = outputParentIdentity(root, outputPath, label);
    const descriptorStats = fstatSync(descriptor);
    if (identity.dev !== descriptorStats.dev || identity.ino !== descriptorStats.ino) {
      fail("INVALID_OUTPUT", `${label} parent changed before it could be bound`, "io");
    }
    return {
      ...identity,
      root,
      descriptor,
      anchorPath: `${descriptorDirectory}/${descriptor}`,
    };
  } catch (error) {
    if (descriptor !== undefined) closeSync(descriptor);
    throw error;
  }
}

function createAnchoredStage(parent, prefix, label) {
  let entryPath;
  let descriptor;
  try {
    entryPath = mkdtempSync(join(parent.anchorPath, prefix));
    const name = basename(entryPath);
    const lexicalPath = join(parent.canonicalParent, name);
    descriptor = openSync(entryPath, directoryOpenFlags());
    const descriptorStats = fstatSync(descriptor);
    const lexical = outputDirectoryIdentity(parent.root, lexicalPath, label);
    if (lexical.dev !== descriptorStats.dev || lexical.ino !== descriptorStats.ino) {
      fail("INVALID_OUTPUT", `${label} changed while it was being bound`, "io");
    }
    return {
      lexicalPath,
      entryPath,
      descriptor,
      anchorPath: descriptorPath(descriptor),
    };
  } catch (error) {
    if (descriptor !== undefined) closeSync(descriptor);
    if (entryPath) {
      try {
        removeAnchoredStageDirectory({ anchorPath: parent.anchorPath, entryPath }, []);
      } catch {
        // The caller will report the primary stage-creation failure. The entry is anchored
        // below a held parent descriptor, so refusing recursive cleanup cannot touch a peer.
      }
    }
    throw error;
  }
}

function outputParentIdentity(root, outputPath, label) {
  let canonicalParent;
  let stats;
  try {
    canonicalParent = realpathSync(dirname(outputPath));
    stats = lstatSync(canonicalParent);
  } catch {
    fail("INVALID_OUTPUT", `${label} parent could not be inspected`, "io");
  }
  if (!stats.isDirectory()) {
    fail("INVALID_OUTPUT", `${label} parent must remain a directory`, "io");
  }
  if (!isWithin(root, canonicalParent)) {
    fail("ROOT_ESCAPE", `${label} parent escaped the declared repository root`, "io");
  }
  return { canonicalParent, dev: stats.dev, ino: stats.ino, label };
}

function assertOutputParentStable(root, outputPath, expected) {
  let current;
  try {
    current = outputParentIdentity(root, outputPath, expected.label);
  } catch {
    throw new TransactionalOutputError(
      "OUTPUT_ROLLBACK_INCOMPLETE",
      "an output parent changed during export; staged files were retained for manual recovery",
      { preserveStageDirectories: true },
    );
  }
  if (
    current.canonicalParent !== expected.canonicalParent ||
    current.dev !== expected.dev ||
    current.ino !== expected.ino
  ) {
    throw new TransactionalOutputError(
      "OUTPUT_ROLLBACK_INCOMPLETE",
      "an output parent changed during export; staged files were retained for manual recovery",
      { preserveStageDirectories: true },
    );
  }
}

function positiveInteger(value, label, maximum) {
  if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
    fail("INVALID_RECIPE", `${label} must be a positive integer no greater than ${maximum}`);
  }
  return value;
}

function recipeObject(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    fail("INVALID_RECIPE", "recipe default export must be an object");
  }
  return value;
}

function rejectUnknownRecipeKeys(recipe) {
  const supported = new Set([
    "schemaVersion",
    "source",
    "staticOutput",
    "animatedOutput",
    "width",
    "height",
    "fps",
    "frameCount",
    "maxTotalFrameBytes",
    "maxFileBytes",
    "gifMaxColors",
    "preserveTransparency",
    "renderFrame",
  ]);
  for (const key of Object.keys(recipe)) {
    if (!supported.has(key)) {
      fail("INVALID_RECIPE", "recipe contains an unsupported field");
    }
  }
}

function validateRecipe(recipeValue, root, recipePath) {
  const recipe = recipeObject(recipeValue);
  rejectUnknownRecipeKeys(recipe);
  if (recipe.schemaVersion !== 1) {
    fail("INVALID_RECIPE", "recipe schemaVersion must be exactly 1");
  }
  const sourcePath = existingRegularFile(root, recipe.source, "recipe source", ".svg");
  const staticOutputPath = safeOutputPath(root, recipe.staticOutput, "static output", ".png");
  const animatedOutputPath = safeOutputPath(root, recipe.animatedOutput, "animated output", ".gif");
  const paths = [sourcePath, recipePath, staticOutputPath, animatedOutputPath];
  if (new Set(paths).size !== paths.length) {
    fail("PATH_COLLISION", "recipe source, recipe module, and outputs must use distinct paths");
  }

  const width = positiveInteger(recipe.width, "width", MAX_DIMENSION);
  const height = positiveInteger(recipe.height, "height", MAX_DIMENSION);
  const fps = positiveInteger(recipe.fps, "fps", 60);
  const frameCount = positiveInteger(recipe.frameCount, "frameCount", MAX_FRAME_COUNT);
  if (frameCount < 2) {
    fail("INVALID_RECIPE", "frameCount must be at least 2 for an animated export");
  }
  if (width * height * frameCount > MAX_TOTAL_PIXELS) {
    fail("INVALID_RECIPE", `recipe exceeds the ${MAX_TOTAL_PIXELS}-pixel export-work limit`);
  }
  const maxTotalFrameBytes = positiveInteger(
    recipe.maxTotalFrameBytes ?? MAX_TOTAL_FRAME_BYTES,
    "maxTotalFrameBytes",
    MAX_TOTAL_FRAME_BYTES,
  );
  const maxFileBytes = positiveInteger(
    recipe.maxFileBytes ?? DEFAULT_GIF_BYTES,
    "maxFileBytes",
    MAX_GIF_BYTES,
  );
  const preserveTransparency = recipe.preserveTransparency ?? true;
  if (typeof preserveTransparency !== "boolean") {
    fail("INVALID_RECIPE", "preserveTransparency must be a boolean when provided");
  }
  const gifMaxColors = positiveInteger(
    recipe.gifMaxColors ?? DEFAULT_GIF_COLORS,
    "gifMaxColors",
    256,
  );
  const minimumGifColors = preserveTransparency ? 3 : 2;
  if (gifMaxColors < minimumGifColors) {
    fail(
      "INVALID_RECIPE",
      `gifMaxColors must be at least ${minimumGifColors} when preserveTransparency is ${preserveTransparency}`,
    );
  }
  if (typeof recipe.renderFrame !== "function") {
    fail("INVALID_RECIPE", "recipe must export a renderFrame function");
  }

  return {
    source: recipe.source,
    sourcePath,
    staticOutput: recipe.staticOutput,
    staticOutputPath,
    animatedOutput: recipe.animatedOutput,
    animatedOutputPath,
    width,
    height,
    fps,
    frameCount,
    maxTotalFrameBytes,
    maxFileBytes,
    gifMaxColors,
    preserveTransparency,
    renderFrame: recipe.renderFrame.bind(recipe),
  };
}

const XML_NAME_RE = /^(?:[A-Za-z_][A-Za-z0-9_.-]*:)?[A-Za-z_][A-Za-z0-9_.-]*/u;
const LOCAL_FRAGMENT_RE = /^#[A-Za-z_][A-Za-z0-9_.:-]*$/u;
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
const XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
const XML_WHITESPACE_RE = /^[\t\n\r ]*$/u;
const CSS_URL_ATTRIBUTES = new Set([
  "clip-path",
  "cursor",
  "fill",
  "filter",
  "marker",
  "marker-end",
  "marker-mid",
  "marker-start",
  "mask",
  "stroke",
  "style",
]);
const UNSUPPORTED_SVG_ELEMENTS = new Set([
  "audio",
  "canvas",
  "embed",
  "foreignObject",
  "iframe",
  "object",
  "script",
  "video",
]);
const DECLARATIVE_ANIMATION_SVG_ELEMENTS = new Set([
  "animate",
  "animatecolor",
  "animatemotion",
  "animatetransform",
  "discard",
  "mpath",
  "set",
]);
const XML_ENTITIES = Object.freeze({
  amp: "&",
  apos: "'",
  gt: ">",
  lt: "<",
  quot: '"',
});

function isXmlCodePoint(codePoint) {
  return (
    codePoint === 0x09 ||
    codePoint === 0x0a ||
    codePoint === 0x0d ||
    (codePoint >= 0x20 && codePoint <= 0xd7ff) ||
    (codePoint >= 0xe000 && codePoint <= 0xfffd) ||
    (codePoint >= 0x10000 && codePoint <= 0x10ffff)
  );
}

function assertXmlCharacters(value, label, code) {
  for (const character of value) {
    if (!isXmlCodePoint(character.codePointAt(0))) {
      fail(code, `${label} contains a character forbidden by XML 1.0`);
    }
  }
}

function decodeXmlEntities(value, label, code) {
  let decoded = "";
  let offset = 0;
  while (offset < value.length) {
    const opening = value.indexOf("&", offset);
    if (opening === -1) {
      decoded += value.slice(offset);
      break;
    }
    decoded += value.slice(offset, opening);
    const end = value.indexOf(";", opening + 1);
    if (end === -1) fail(code, `${label} contains malformed XML text`);
    const entity = value.slice(opening + 1, end);
    if (Object.hasOwn(XML_ENTITIES, entity)) {
      decoded += XML_ENTITIES[entity];
    } else if (/^#\d+$/u.test(entity) || /^#x[\dA-Fa-f]+$/u.test(entity)) {
      const radix = entity.startsWith("#x") ? 16 : 10;
      const digits = entity.slice(radix === 16 ? 2 : 1);
      const codePoint = Number.parseInt(digits, radix);
      if (!Number.isSafeInteger(codePoint) || !isXmlCodePoint(codePoint)) {
        fail(code, `${label} contains an invalid XML character reference`);
      }
      decoded += String.fromCodePoint(codePoint);
    } else {
      fail(code, `${label} contains an unsupported XML entity`);
    }
    offset = end + 1;
  }
  return decoded;
}

function xmlTagEnd(svg, start, label, code) {
  let quote = null;
  for (let index = start; index < svg.length; index += 1) {
    const character = svg[index];
    if (quote) {
      if (character === quote) quote = null;
    } else if (character === '"' || character === "'") {
      quote = character;
    } else if (character === ">") {
      return index;
    } else if (character === "<") {
      fail(code, `${label} contains malformed XML markup`);
    }
  }
  fail(code, `${label} contains unterminated XML markup`);
}

function parseXmlTag(content, label, code) {
  if (content.startsWith("/")) {
    const closing = content.slice(1);
    const match = closing.match(XML_NAME_RE);
    if (!match || !XML_WHITESPACE_RE.test(closing.slice(match[0].length))) {
      fail(code, `${label} contains a malformed closing tag`);
    }
    return { attributes: new Map(), closing: true, name: match[0], selfClosing: false };
  }

  const selfClosing = content.endsWith("/");
  const body = selfClosing ? content.slice(0, -1) : content;
  const nameMatch = body.match(XML_NAME_RE);
  if (!nameMatch) fail(code, `${label} contains a malformed opening tag`);
  const name = nameMatch[0];

  const attributes = new Map();
  const rawAttributes = new Map();
  let offset = name.length;
  while (offset < body.length) {
    if (!/[\t\n\r ]/u.test(body[offset])) {
      fail(code, `${label} contains malformed element attributes`);
    }
    while (offset < body.length && /[\t\n\r ]/u.test(body[offset])) offset += 1;
    if (offset === body.length) break;

    const attributeMatch = body.slice(offset).match(XML_NAME_RE);
    if (!attributeMatch) fail(code, `${label} contains a malformed attribute name`);
    const attribute = attributeMatch[0];
    if (attributes.has(attribute)) fail(code, `${label} contains a duplicate attribute`);
    offset += attribute.length;
    while (offset < body.length && /[\t\n\r ]/u.test(body[offset])) offset += 1;
    if (body[offset] !== "=") fail(code, `${label} contains an unassigned attribute`);
    offset += 1;
    while (offset < body.length && /[\t\n\r ]/u.test(body[offset])) offset += 1;
    const quote = body[offset];
    if (quote !== '"' && quote !== "'") {
      fail(code, `${label} contains an unquoted attribute`);
    }
    const valueStart = ++offset;
    const valueEnd = body.indexOf(quote, valueStart);
    if (valueEnd === -1) fail(code, `${label} contains an unterminated attribute`);
    const value = body.slice(valueStart, valueEnd);
    if (value.includes("<")) fail(code, `${label} contains malformed attribute text`);
    rawAttributes.set(attribute, value);
    attributes.set(attribute, decodeXmlEntities(value, label, code));
    offset = valueEnd + 1;
  }
  return { attributes, closing: false, name, rawAttributes, selfClosing };
}

function resolveElementName(name, namespaces, label, code) {
  const separator = name.indexOf(":");
  const prefix = separator === -1 ? "" : name.slice(0, separator);
  const localName = separator === -1 ? name : name.slice(separator + 1);
  const namespace = namespaces.get(prefix);
  if (namespace === undefined) {
    fail(code, `${label} contains an undeclared XML namespace prefix`);
  }
  return { key: `${namespace}\u0000${localName}`, localName, namespace, rawName: name };
}

function parseSvgDocument(svg, label, code) {
  if (svg.startsWith("\uFEFF")) svg = svg.slice(1);
  assertXmlCharacters(svg, label, code);
  const stack = [];
  const namespaceStack = [];
  const cssValues = [];
  const ids = new Set();
  const references = [];
  let root = null;
  let rootClosed = false;
  let documentNamespace = null;
  let sawXmlDeclaration = false;
  let offset = 0;

  while (offset < svg.length) {
    const opening = svg.indexOf("<", offset);
    const text = opening === -1 ? svg.slice(offset) : svg.slice(offset, opening);
    const decodedText = decodeXmlEntities(text, label, code);
    if (text.includes("]]>") && stack.length > 0) {
      fail(code, `${label} contains a CDATA terminator outside CDATA`);
    }
    if (stack.at(-1)?.localName === "style") stack.at(-1).styleText += decodedText;
    if (stack.length === 0 && !XML_WHITESPACE_RE.test(text)) {
      fail(code, `${label} contains text outside its SVG root`);
    }
    if (opening === -1) break;

    if (svg.startsWith("<!--", opening)) {
      const end = svg.indexOf("-->", opening + 4);
      const content = end === -1 ? "" : svg.slice(opening + 4, end);
      if (end === -1 || content.includes("--") || content.endsWith("-")) {
        fail(code, `${label} contains a malformed XML comment`);
      }
      offset = end + 3;
      continue;
    }
    if (svg.startsWith("<![CDATA[", opening)) {
      if (stack.length === 0) fail(code, `${label} contains CDATA outside its SVG root`);
      const end = svg.indexOf("]]>", opening + 9);
      if (end === -1) fail(code, `${label} contains unterminated CDATA`);
      if (stack.at(-1)?.localName === "style") {
        stack.at(-1).styleText += svg.slice(opening + 9, end);
      }
      offset = end + 3;
      continue;
    }
    if (svg.startsWith("<?", opening)) {
      const end = svg.indexOf("?>", opening + 2);
      const declaration = end === -1 ? "" : svg.slice(opening, end + 2);
      if (
        end === -1 ||
        sawXmlDeclaration ||
        root !== null ||
        opening !== 0 ||
        !/^<\?xml[\t\n\r ]+version=(?:"1\.[01]"|'1\.[01]')(?:[\t\n\r ]+encoding=(?:"[Uu][Tt][Ff]-8"|'[Uu][Tt][Ff]-8'))?(?:[\t\n\r ]+standalone=(?:"(?:yes|no)"|'(?:yes|no)'))?[\t\n\r ]*\?>$/u.test(
          declaration,
        )
      ) {
        fail(code, `${label} contains an unsupported processing instruction`);
      }
      sawXmlDeclaration = true;
      offset = end + 2;
      continue;
    }
    if (svg.startsWith("<!", opening)) {
      fail(code, `${label} contains unsupported XML content`);
    }

    const end = xmlTagEnd(svg, opening + 1, label, code);
    const tag = parseXmlTag(svg.slice(opening + 1, end), label, code);
    if (tag.closing) {
      const closingName = resolveElementName(
        tag.name,
        namespaceStack.at(-1) ??
          new Map([
            ["", ""],
            ["xml", XML_NAMESPACE],
          ]),
        label,
        code,
      );
      if (stack.at(-1)?.key !== closingName.key || stack.at(-1)?.rawName !== closingName.rawName) {
        fail(code, `${label} contains mismatched XML tags`);
      }
      if (stack.at(-1).localName === "style") cssValues.push(stack.at(-1).styleText);
      stack.pop();
      namespaceStack.pop();
      if (stack.length === 0) rootClosed = true;
    } else {
      const namespaces = new Map(
        namespaceStack.at(-1) ?? [
          ["", ""],
          ["xml", XML_NAMESPACE],
        ],
      );
      for (const [attribute, value] of tag.attributes) {
        if (attribute === "xmlns") {
          if (value === XML_NAMESPACE || value === XMLNS_NAMESPACE) {
            fail(code, `${label} contains an invalid default XML namespace declaration`);
          }
          namespaces.set("", value);
          continue;
        }
        if (!attribute.startsWith("xmlns:")) continue;
        const prefix = attribute.slice("xmlns:".length);
        if (
          prefix === "xmlns" ||
          value === "" ||
          value === XMLNS_NAMESPACE ||
          (prefix === "xml" && value !== XML_NAMESPACE) ||
          (prefix !== "xml" && value === XML_NAMESPACE)
        ) {
          fail(code, `${label} contains an invalid XML namespace declaration`);
        }
        namespaces.set(prefix, value);
      }
      const elementName = resolveElementName(tag.name, namespaces, label, code);

      if (stack.length === 0) {
        if (root !== null || rootClosed || elementName.localName !== "svg") {
          fail(code, `${label} must contain exactly one SVG root`);
        }
        if (!["", SVG_NAMESPACE].includes(elementName.namespace)) {
          fail(code, `${label} root has an unsupported namespace`);
        }
        documentNamespace = elementName.namespace;
        root = { ...tag, elementName };
      }
      if (elementName.namespace !== documentNamespace) {
        fail(code, `${label} contains an element outside the root SVG namespace`);
      }
      if (
        UNSUPPORTED_SVG_ELEMENTS.has(elementName.localName) ||
        DECLARATIVE_ANIMATION_SVG_ELEMENTS.has(elementName.localName.toLowerCase())
      ) {
        fail(code, `${label} contains active or unsupported content`);
      }

      const expandedAttributes = new Set();
      for (const [attribute, value] of tag.attributes) {
        if (attribute === "xmlns" || attribute.startsWith("xmlns:")) continue;
        const separator = attribute.indexOf(":");
        const prefix = separator === -1 ? null : attribute.slice(0, separator);
        const localName = separator === -1 ? attribute : attribute.slice(separator + 1);
        const namespace = prefix ? namespaces.get(prefix) : "";
        if (prefix && namespace === undefined) {
          fail(code, `${label} contains an undeclared XML namespace prefix`);
        }
        const expandedName = `${namespace}\u0000${localName}`;
        if (expandedAttributes.has(expandedName)) {
          fail(code, `${label} contains duplicate expanded XML attributes`);
        }
        expandedAttributes.add(expandedName);
        if (localName === "id" && (namespace === "" || namespace === XML_NAMESPACE) && value) {
          if (ids.has(value)) fail(code, `${label} contains a duplicate id`);
          ids.add(value);
        }
        if (namespace === XML_NAMESPACE && localName === "base") {
          fail(code, `${label} contains an external or indirect reference base`);
        }
        if (localName === "href" && (namespace === "" || namespace === XLINK_NAMESPACE)) {
          references.push(tag.rawAttributes.get(attribute));
        }
        if (namespace === "" && CSS_URL_ATTRIBUTES.has(localName.toLowerCase())) {
          cssValues.push(value);
        }
      }

      if (tag.selfClosing) {
        if (stack.length === 0) rootClosed = true;
      } else {
        stack.push({ ...elementName, styleText: elementName.localName === "style" ? "" : null });
        namespaceStack.push(namespaces);
      }
    }
    offset = end + 1;
  }

  if (!root || !rootClosed || stack.length !== 0) {
    fail(code, `${label} must contain one complete, well-formed SVG root`);
  }
  return { cssValues, ids, references, root };
}

function decodeCssEscapes(value) {
  return value
    .replace(/\\([\dA-Fa-f]{1,6})(?:\r\n|[\t\n\f\r ])?/gu, (_match, digits) => {
      const codePoint = Number.parseInt(digits, 16);
      return isXmlCodePoint(codePoint) ? String.fromCodePoint(codePoint) : "\uFFFD";
    })
    .replace(/\\(?:\r\n|[\n\f\r])/gu, "")
    .replace(/\\([\s\S])/gu, "$1");
}

function assertLocalReferences(document, label, code) {
  const localReferences = [...document.references];
  for (const cssValue of document.cssValues) {
    const decodedCss = decodeCssEscapes(cssValue).replace(/\/\*[\s\S]*?\*\//gu, "");
    if (/@import\b/iu.test(decodedCss)) {
      fail(code, `${label} contains an external or indirect reference`);
    }
    for (const match of decodedCss.matchAll(/url\(([^)]*)\)/giu)) {
      let target = match[1].trim();
      if (
        target.length >= 2 &&
        ((target.startsWith('"') && target.endsWith('"')) ||
          (target.startsWith("'") && target.endsWith("'")))
      ) {
        target = target.slice(1, -1).trim();
      }
      localReferences.push(target);
    }
  }
  for (const target of localReferences) {
    if (!LOCAL_FRAGMENT_RE.test(target) || !document.ids.has(target.slice(1))) {
      fail(code, `${label} contains an external, encoded, or unresolved reference`);
    }
  }
}

function assertSelfContainedSvg(svg, width, height, label) {
  if (typeof svg !== "string" || Buffer.byteLength(svg, "utf8") > MAX_FRAME_BYTES) {
    fail(
      "INVALID_FRAME",
      `${label} must return an SVG string no larger than ${MAX_FRAME_BYTES} bytes`,
    );
  }
  if (/\0/u.test(svg) || /<!DOCTYPE\b/i.test(svg)) {
    fail("INVALID_FRAME", `${label} contains unsupported XML content`);
  }
  if (/<(?:script|foreignObject|iframe|object|embed|audio|video|canvas)\b/i.test(svg)) {
    fail("INVALID_FRAME", `${label} contains active or unsupported content`);
  }
  const document = parseSvgDocument(svg, label, "INVALID_FRAME");
  assertLocalReferences(document, label, "INVALID_FRAME");
  const numericAttribute = (name) => {
    const value = document.root.attributes.get(name);
    if (!value || !/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) return null;
    return Number(value);
  };
  if (numericAttribute("width") !== width || numericAttribute("height") !== height) {
    fail("INVALID_FRAME", `${label} must declare numeric ${width} by ${height} dimensions`);
  }
}

function assertCanonicalSource(svg) {
  if (Buffer.byteLength(svg, "utf8") > MAX_SOURCE_BYTES) {
    fail("INVALID_SOURCE", `canonical SVG exceeds ${MAX_SOURCE_BYTES} bytes`);
  }
  if (/\0/u.test(svg) || /<!DOCTYPE\b/i.test(svg)) {
    fail("INVALID_SOURCE", "canonical SVG contains unsupported XML content");
  }
  if (/<(?:script|foreignObject|iframe|object|embed|audio|video|canvas)\b/i.test(svg)) {
    fail("INVALID_SOURCE", "canonical SVG contains active or unsupported content");
  }
  const document = parseSvgDocument(svg, "canonical SVG", "INVALID_SOURCE");
  assertLocalReferences(document, "canonical SVG", "INVALID_SOURCE");
}

function readUtf8FileBounded(file, maximumBytes, label, code) {
  let descriptor;
  try {
    descriptor = openSync(file, "r");
    const stats = fstatSync(descriptor);
    if (!stats.isFile()) fail(code, `${label} must be a regular file`);
    if (stats.size > maximumBytes) fail(code, `${label} exceeds ${maximumBytes} bytes`);

    const buffer = Buffer.allocUnsafe(stats.size + 1);
    let bytesRead = 0;
    while (bytesRead < buffer.length) {
      const count = readSync(descriptor, buffer, bytesRead, buffer.length - bytesRead, null);
      if (count === 0) break;
      bytesRead += count;
    }
    if (bytesRead > stats.size || bytesRead > maximumBytes) {
      fail(code, `${label} changed while reading or exceeds ${maximumBytes} bytes`);
    }
    try {
      return new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytesRead));
    } catch {
      fail(code, `${label} is not valid UTF-8`);
    }
  } catch (error) {
    if (error instanceof ExportError) throw error;
    fail("SOURCE_READ_FAILED", `${label} could not be read`, "io");
  } finally {
    if (descriptor !== undefined) {
      try {
        closeSync(descriptor);
      } catch {
        // The descriptor belongs only to this bounded read; failure is sanitized by later I/O checks.
      }
    }
  }
}

async function renderFrame(config, sourceSvg, frameIndex) {
  let svg;
  try {
    svg = await config.renderFrame(
      Object.freeze({
        sourceSvg,
        frameIndex,
        timeSeconds: frameIndex / config.fps,
        width: config.width,
        height: config.height,
        fps: config.fps,
        frameCount: config.frameCount,
      }),
    );
  } catch (error) {
    fail("RECIPE_FAILED", `renderFrame failed for frame ${frameIndex}`);
  }
  assertSelfContainedSvg(svg, config.width, config.height, `frame ${frameIndex}`);
  return svg;
}

function consumeFrameByteBudget(totalBytes, svg, maximumBytes) {
  const frameBytes = Buffer.byteLength(svg, "utf8");
  if (frameBytes > maximumBytes - totalBytes) {
    fail("INVALID_FRAME", `rendered SVG frames exceed the cumulative ${maximumBytes}-byte limit`);
  }
  return totalBytes + frameBytes;
}

function frameName(index, extension) {
  return `frame-${String(index).padStart(4, "0")}.${extension}`;
}

function runTool(command, arguments_, root) {
  const result = spawnSync(command, arguments_, {
    cwd: root,
    encoding: "utf8",
    env: deterministicEnvironment,
    maxBuffer: 16 * 1024 * 1024,
    shell: false,
  });
  if (result.error) {
    const message = result.error.code === "ENOENT" ? "is unavailable" : `could not start`;
    fail(
      "MISSING_OR_BROKEN_TOOL",
      `${command} ${message}; inspect references/local-tooling.md before proposing an installation`,
      "tool",
    );
  }
  if (result.status !== 0) {
    fail("TOOL_FAILED", `${command} exited with status ${result.status ?? "unknown"}`, "tool");
  }
  return result.stdout.trim();
}

function sanitizeAndValidatePng(file, width, height, preserveTransparency) {
  const png = readFileSync(file);
  if (png.length < 33 || png.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a") {
    fail("INVALID_STATIC_PNG", "rasterized static output is not a valid PNG");
  }
  const signature = png.subarray(0, 8);
  const kept = [signature];
  const allowed = new Set(["IHDR", "PLTE", "tRNS", "cHRM", "gAMA", "sBIT", "sRGB", "IDAT", "IEND"]);
  let offset = 8;
  let chunks = 0;
  let sawHeader = false;
  let sawEnd = false;
  let hasTransparency = false;
  while (offset < png.length) {
    if (offset + 12 > png.length) fail("INVALID_STATIC_PNG", "PNG chunk is truncated");
    const length = png.readUInt32BE(offset);
    const end = offset + 12 + length;
    if (end > png.length) fail("INVALID_STATIC_PNG", "PNG chunk exceeds file bounds");
    const type = png.subarray(offset + 4, offset + 8).toString("ascii");
    chunks += 1;
    if (chunks > 10_000) fail("INVALID_STATIC_PNG", "PNG contains too many chunks");
    if (!sawHeader) {
      if (type !== "IHDR" || length !== 13) {
        fail("INVALID_STATIC_PNG", "PNG must begin with a valid IHDR chunk");
      }
      sawHeader = true;
      if (png.readUInt32BE(offset + 8) !== width || png.readUInt32BE(offset + 12) !== height) {
        fail("INVALID_STATIC_PNG", `PNG dimensions must be ${width} by ${height}`);
      }
      const colorType = png[offset + 17];
      hasTransparency = colorType === 4 || colorType === 6;
    }
    if (type === "tRNS") hasTransparency = true;
    if (allowed.has(type)) {
      kept.push(png.subarray(offset, end));
    } else if (type[0] === type[0].toUpperCase()) {
      fail("INVALID_STATIC_PNG", `PNG contains unknown critical chunk ${type}`);
    }
    offset = end;
    if (type === "IEND") {
      sawEnd = true;
      break;
    }
  }
  if (!sawEnd || offset !== png.length) {
    fail("INVALID_STATIC_PNG", "PNG has a missing IEND chunk or trailing data");
  }
  if (preserveTransparency && !hasTransparency) {
    fail("INVALID_STATIC_PNG", "PNG does not retain an alpha channel or transparency table");
  }
  writeFileSync(file, Buffer.concat(kept));
}

function strictlyInspectStaticPng(file, width, height) {
  let inspected;
  try {
    inspected = inspectAnimatedImageFile(file, {
      maxFileBytes: MAX_GIF_BYTES,
      maxFrames: 1,
      maxDimension: Math.max(width, height),
    });
  } catch {
    fail("INVALID_STATIC_PNG", "sanitized static PNG failed strict structural inspection");
  }
  if (
    inspected.format !== "png" ||
    inspected.width !== width ||
    inspected.height !== height ||
    inspected.frameCount !== 1
  ) {
    fail("INVALID_STATIC_PNG", `static PNG must be ${width} by ${height} and contain one frame`);
  }
}

function expectedGifFrameDelays(frameCount, fps) {
  const delays = [];
  for (let index = 0; index < frameCount - 1; index += 1) {
    delays.push(Math.round(((index + 1) * 100) / fps) - Math.round((index * 100) / fps));
  }
  delays.push(Math.round(100 / fps));
  return delays;
}

function validateAnimatedGif(file, config) {
  let inspected;
  try {
    inspected = inspectAnimatedImageFile(file, {
      maxFileBytes: config.maxFileBytes,
      maxFrames: config.frameCount,
      maxDimension: Math.max(config.width, config.height),
    });
  } catch {
    fail("INVALID_ANIMATED_GIF", "animated GIF failed strict structural inspection");
  }
  if (
    inspected.format !== "gif" ||
    inspected.width !== config.width ||
    inspected.height !== config.height ||
    inspected.frameCount !== config.frameCount ||
    inspected.loopCount !== 0
  ) {
    fail(
      "INVALID_ANIMATED_GIF",
      `GIF must be ${config.width} by ${config.height}, contain ${config.frameCount} frames, and loop infinitely`,
    );
  }
  const expectedDelays = expectedGifFrameDelays(config.frameCount, config.fps);
  const timingMatches =
    inspected.frameDelaysCentiseconds?.length === expectedDelays.length &&
    expectedDelays.every(
      (expectedDelay, index) => inspected.frameDelaysCentiseconds[index] === expectedDelay,
    );
  if (inspected.graphicControlCount !== config.frameCount || !timingMatches) {
    fail(
      "INVALID_ANIMATED_GIF",
      `GIF frame delays must match ${expectedDelays.join(",")} centiseconds at ${config.fps} fps`,
    );
  }
}

async function loadConfiguration(root, recipeArgument) {
  safeRelativePath(recipeArgument, "recipe", ".mjs");
  const recipePath = existingRegularFile(root, recipeArgument, "recipe", ".mjs");
  let version;
  try {
    version = statSync(recipePath).mtimeMs;
  } catch {
    fail("RECIPE_LOAD_FAILED", "recipe module could not be inspected", "io");
  }
  let module;
  try {
    module = await import(`${pathToFileURL(recipePath).href}?v=${version}`);
  } catch (error) {
    fail(
      "RECIPE_LOAD_FAILED",
      "recipe module could not be loaded",
      isNodeIoError(error) ? "io" : undefined,
    );
  }
  return validateRecipe(module.default, root, recipePath);
}

async function renderFramePass(config, sourceSvg) {
  const frames = [];
  let totalFrameBytes = 0;
  for (let frameIndex = 0; frameIndex < config.frameCount; frameIndex += 1) {
    const frame = await renderFrame(config, sourceSvg, frameIndex);
    totalFrameBytes = consumeFrameByteBudget(totalFrameBytes, frame, config.maxTotalFrameBytes);
    frames.push(frame);
  }
  return frames;
}

async function checkRecipe(config, sourceSvg) {
  const verifiedFrames = await renderFramePass(config, sourceSvg);
  const comparisonFrames = await renderFramePass(config, sourceSvg);
  for (let frameIndex = 0; frameIndex < config.frameCount; frameIndex += 1) {
    if (verifiedFrames[frameIndex] !== comparisonFrames[frameIndex]) {
      fail("NONDETERMINISTIC_RECIPE", `frame ${frameIndex} changes between identical passes`);
    }
  }
  return verifiedFrames;
}

function publicToolVersion(command, value) {
  const version = value.match(/\b\d+(?:\.\d+){1,3}(?:[-+][0-9A-Za-z.-]+)?\b/)?.[0];
  return version ? `${command} ${version}` : `${command} available`;
}

function verifyFfmpegCapabilities(root) {
  const filters = runTool("ffmpeg", ["-hide_banner", "-filters"], root);
  const muxers = runTool("ffmpeg", ["-hide_banner", "-muxers"], root);
  const encoders = runTool("ffmpeg", ["-hide_banner", "-encoders"], root);
  if (!/\bpalettegen\b/.test(filters) || !/\bpaletteuse\b/.test(filters)) {
    fail("MISSING_TOOL_CAPABILITY", "ffmpeg lacks the palettegen or paletteuse filter", "tool");
  }
  if (!/^\s*E\s+gif\b/m.test(muxers)) {
    fail("MISSING_TOOL_CAPABILITY", "ffmpeg lacks the GIF muxer", "tool");
  }
  if (!/^\s*V\S*\s+gif\b/m.test(encoders)) {
    fail("MISSING_TOOL_CAPABILITY", "ffmpeg lacks the GIF encoder", "tool");
  }
}

async function exportArtifacts(root, config, verifiedFrames, replace) {
  if (!replace && (existsSync(config.staticOutputPath) || existsSync(config.animatedOutputPath))) {
    fail(
      "OUTPUT_EXISTS",
      "one or more declared outputs already exist; rerun with --replace only after reviewing the recipe and destinations",
    );
  }
  const rsvgVersion = publicToolVersion(
    "rsvg-convert",
    runTool("rsvg-convert", ["--version"], root),
  );
  const ffmpegVersion = publicToolVersion("ffmpeg", runTool("ffmpeg", ["-version"], root));
  verifyFfmpegCapabilities(root);
  const runDirectory = mkdtempSync(join(tmpdir(), "animated-readme-logo-export-"));
  let staticStage;
  let animatedStage;
  let staticOutputParent;
  let animatedOutputParent;
  let preserveStageDirectories = false;
  let result;
  let primaryError;
  try {
    for (let frameIndex = 0; frameIndex < verifiedFrames.length; frameIndex += 1) {
      const svgPath = join(runDirectory, frameName(frameIndex, "svg"));
      const pngPath = join(runDirectory, frameName(frameIndex, "png"));
      writeFileSync(svgPath, verifiedFrames[frameIndex], "utf8");
      runTool(
        "rsvg-convert",
        [
          "--format",
          "png",
          "--width",
          String(config.width),
          "--height",
          String(config.height),
          "--output",
          pngPath,
          svgPath,
        ],
        root,
      );
    }

    staticOutputParent = ensureOutputParent(root, config.staticOutputPath, "static output");
    animatedOutputParent = ensureOutputParent(root, config.animatedOutputPath, "animated output");
    staticStage = createAnchoredStage(
      staticOutputParent,
      ".readme-logo-static-stage-",
      "static stage directory",
    );
    animatedStage = createAnchoredStage(
      animatedOutputParent,
      ".readme-logo-animated-stage-",
      "animated stage directory",
    );
    assertOutputParentStable(root, config.staticOutputPath, staticOutputParent);
    assertOutputParentStable(root, config.animatedOutputPath, animatedOutputParent);
    const staticName = basename(config.staticOutputPath);
    const animatedName = basename(config.animatedOutputPath);
    const stagedStatic = join(staticStage.lexicalPath, staticName);
    const stagedAnimated = join(animatedStage.lexicalPath, animatedName);
    const stagedStaticMutationPath = join(staticStage.anchorPath, staticName);
    const stagedAnimatedMutationPath = join(animatedStage.anchorPath, animatedName);
    const staticDestinationMutationPath = join(staticOutputParent.anchorPath, staticName);
    const animatedDestinationMutationPath = join(animatedOutputParent.anchorPath, animatedName);
    const firstFrame = join(runDirectory, frameName(0, "png"));
    const palette = join(runDirectory, "palette.png");
    const inputPattern = join(runDirectory, "frame-%04d.png");
    const generatedAnimated = join(runDirectory, "generated-animated.gif");

    copyFileSync(firstFrame, stagedStaticMutationPath);
    sanitizeAndValidatePng(
      stagedStaticMutationPath,
      config.width,
      config.height,
      config.preserveTransparency,
    );
    strictlyInspectStaticPng(stagedStaticMutationPath, config.width, config.height);

    runTool(
      "ffmpeg",
      [
        "-nostdin",
        "-hide_banner",
        "-loglevel",
        "error",
        "-y",
        "-filter_threads",
        "1",
        "-threads",
        "1",
        "-framerate",
        String(config.fps),
        "-start_number",
        "0",
        "-i",
        inputPattern,
        "-vf",
        `palettegen=max_colors=${config.gifMaxColors}:reserve_transparent=${config.preserveTransparency ? 1 : 0}:stats_mode=diff`,
        "-frames:v",
        "1",
        "-update",
        "1",
        palette,
      ],
      root,
    );
    runTool(
      "ffmpeg",
      [
        "-nostdin",
        "-hide_banner",
        "-loglevel",
        "error",
        "-y",
        "-filter_complex_threads",
        "1",
        "-threads",
        "1",
        "-framerate",
        String(config.fps),
        "-start_number",
        "0",
        "-i",
        inputPattern,
        "-i",
        palette,
        "-filter_complex",
        "[0:v][1:v]paletteuse=dither=none:diff_mode=none:alpha_threshold=128",
        "-frames:v",
        String(config.frameCount),
        "-an",
        "-map_metadata",
        "-1",
        "-loop",
        "0",
        "-gifflags",
        "0",
        "-bitexact",
        generatedAnimated,
      ],
      root,
    );

    validateAnimatedGif(generatedAnimated, config);
    copyFileSync(generatedAnimated, stagedAnimatedMutationPath);
    chmodSync(stagedStaticMutationPath, 0o644);
    chmodSync(stagedAnimatedMutationPath, 0o644);
    strictlyInspectStaticPng(stagedStaticMutationPath, config.width, config.height);
    validateAnimatedGif(stagedAnimatedMutationPath, config);
    assertOutputParentStable(root, config.staticOutputPath, staticOutputParent);
    assertOutputParentStable(root, config.animatedOutputPath, animatedOutputParent);
    const staticSnapshot = captureOutputSnapshot(stagedStaticMutationPath);
    const animatedSnapshot = captureOutputSnapshot(stagedAnimatedMutationPath);
    let commitResult;
    try {
      commitResult = replaceOutputsAtomically(
        [
          {
            staged: stagedStatic,
            stagedMutationPath: stagedStaticMutationPath,
            destination: config.staticOutputPath,
            destinationMutationPath: staticDestinationMutationPath,
            validatedSnapshot: staticSnapshot,
          },
          {
            staged: stagedAnimated,
            stagedMutationPath: stagedAnimatedMutationPath,
            destination: config.animatedOutputPath,
            destinationMutationPath: animatedDestinationMutationPath,
            validatedSnapshot: animatedSnapshot,
          },
        ],
        { replace },
      );
    } catch (error) {
      preserveStageDirectories = Boolean(error?.preserveStageDirectories);
      throw error;
    }
    preserveStageDirectories ||= Boolean(commitResult?.preserveStageDirectories);
    result = {
      rsvgVersion,
      ffmpegVersion,
      recoveryRetained: Boolean(commitResult?.retainedRecovery),
    };
  } catch (error) {
    preserveStageDirectories ||= Boolean(error?.preserveStageDirectories);
    primaryError = error;
    try {
      if (staticOutputParent) {
        assertOutputParentStable(root, config.staticOutputPath, staticOutputParent);
      }
      if (animatedOutputParent) {
        assertOutputParentStable(root, config.animatedOutputPath, animatedOutputParent);
      }
    } catch (parentError) {
      preserveStageDirectories = true;
      primaryError = parentError;
    }
  }

  const cleanupErrors = [];
  try {
    rmSync(runDirectory, { recursive: true, force: true });
  } catch {
    cleanupErrors.push("run directory");
  }
  if (!preserveStageDirectories && staticStage) {
    try {
      removeAnchoredStageDirectory(staticStage, [basename(config.staticOutputPath)]);
    } catch {
      cleanupErrors.push("static stage directory");
    }
  }
  if (!preserveStageDirectories && animatedStage) {
    try {
      removeAnchoredStageDirectory(animatedStage, [basename(config.animatedOutputPath)]);
    } catch {
      cleanupErrors.push("animated stage directory");
    }
  }
  for (const handle of [staticStage, animatedStage, staticOutputParent, animatedOutputParent]) {
    if (!handle) continue;
    try {
      closeSync(handle.descriptor);
    } catch {
      cleanupErrors.push("directory descriptor");
    }
  }
  if (primaryError) throw primaryError;
  if (cleanupErrors.length > 0) {
    throw new TransactionalOutputError(
      "OUTPUT_CLEANUP_INCOMPLETE",
      "generated outputs are valid, but one or more temporary directories could not be removed",
    );
  }
  return result;
}

function publicConfiguration(config) {
  return {
    schemaVersion: 1,
    source: config.source,
    staticOutput: config.staticOutput,
    animatedOutput: config.animatedOutput,
    width: config.width,
    height: config.height,
    fps: config.fps,
    frameCount: config.frameCount,
    maxTotalFrameBytes: config.maxTotalFrameBytes,
    maxFileBytes: config.maxFileBytes,
    gifMaxColors: config.gifMaxColors,
    preserveTransparency: config.preserveTransparency,
  };
}

function outputSuccess(args, config, tools) {
  const payload = {
    valid: true,
    status: args.check ? "checked" : "completed",
    recipe: args.recipe,
    ...publicConfiguration(config),
    ...(tools
      ? {
          tools: { rsvgVersion: tools.rsvgVersion, ffmpegVersion: tools.ffmpegVersion },
          recoveryRetained: tools.recoveryRetained,
        }
      : {}),
    errors: [],
  };
  if (args.json) {
    console.log(JSON.stringify(payload));
    return;
  }
  if (args.check) {
    console.log(`VALID README LOGO RECIPE: ${args.recipe}`);
    console.log(
      `Frames: ${config.frameCount}; size: ${config.width}x${config.height}; fps: ${config.fps}`,
    );
    console.log("Exporter-controlled files written: none (--check)");
    return;
  }
  console.log(`Generated ${config.staticOutput}`);
  console.log(`Generated ${config.animatedOutput}`);
  console.log(
    `Frames: ${config.frameCount}; size: ${config.width}x${config.height}; fps: ${config.fps}; loop: infinite`,
  );
  console.log(`Tools: ${tools.rsvgVersion}; ${tools.ffmpegVersion}`);
  if (tools.recoveryRetained) {
    console.log("Replacement recovery stage directories retained for manual cleanup");
  }
}

function outputFailure(args, error) {
  const payload = {
    valid: false,
    status: args?.check ? "check-failed" : "export-failed",
    recipe: null,
    errors: [{ code: error.code ?? "UNEXPECTED_ERROR", message: error.message }],
  };
  if (args?.json || process.argv.includes("--json")) {
    console.log(JSON.stringify(payload));
    return;
  }
  console.error(`README LOGO EXPORT FAILED: [${payload.errors[0].code}] ${error.message}`);
}

function isNodeIoError(error) {
  return (
    error instanceof Error &&
    typeof error.code === "string" &&
    (/^E[A-Z0-9_]+$/.test(error.code) || error.code.startsWith("ERR_FS_"))
  );
}

async function main() {
  let args;
  try {
    args = parseArgs(process.argv.slice(2));
    if (args.help) {
      console.log(usage());
      return;
    }
    if (!args.root || !args.recipe) {
      fail("MISSING_ARGUMENT", "--root and --recipe are required", "usage");
    }
    const root = canonicalDirectory(args.root, "--root");
    const config = await loadConfiguration(root, args.recipe);
    const sourceSvg = readUtf8FileBounded(
      config.sourcePath,
      MAX_SOURCE_BYTES,
      "canonical SVG",
      "INVALID_SOURCE",
    );
    assertCanonicalSource(sourceSvg);
    const verifiedFrames = await checkRecipe(config, sourceSvg);
    const tools = args.check
      ? null
      : await exportArtifacts(root, config, verifiedFrames, args.replace);
    outputSuccess(args, config, tools);
  } catch (error) {
    const normalized =
      error instanceof ExportError
        ? error
        : error instanceof TransactionalOutputError
          ? new ExportError(error.code, error.message, error.category)
          : isNodeIoError(error)
            ? new ExportError("IO_FAILED", "filesystem operation failed", "io")
            : new ExportError("UNEXPECTED_ERROR", "unexpected exporter failure");
    outputFailure(args, normalized);
    if (!args?.json && normalized.category === "usage") console.error(usage());
    process.exitCode = ["usage", "io"].includes(normalized.category) ? 2 : 1;
  }
}

await main();
scripts/generate-readme-logo-snippet.mjs›
#!/usr/bin/env node

function usage() {
  return [
    "Usage: node generate-readme-logo-snippet.mjs --fallback <path> --alt <text> --width <px> --height <px> [options]",
    "",
    "Options:",
    "  --static-webp <path>",
    "  --static-png <path>",
    "  --animated-webp <path>",
    "  --animated-apng <path>",
    "  --animated-gif <path>",
    "  --mode quality-first|conservative|static-only",
  ].join("\n");
}

function parseArgs(argv) {
  const args = { mode: "quality-first" };
  const valueOptions = new Set([
    "--static-webp",
    "--static-png",
    "--animated-webp",
    "--animated-apng",
    "--animated-gif",
    "--fallback",
    "--alt",
    "--width",
    "--height",
    "--mode",
  ]);

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === "--help" || arg === "-h") {
      args.help = true;
      continue;
    }
    if (!valueOptions.has(arg)) throw new Error(`Unknown option: ${arg}`);
    const key = arg.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
    const value = argv[i + 1];
    if (!value || value.startsWith("--")) throw new Error(`Missing value for ${arg}`);
    args[key] = value;
    i += 1;
  }

  return args;
}

function escapeAttr(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll('"', "&quot;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
}

function requireOption(args, key) {
  if (!args[key])
    throw new Error(
      `Missing required option: --${key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}`,
    );
}

function hasControlCharacters(value) {
  return [...String(value)].some((character) => {
    const code = character.codePointAt(0);
    return code <= 0x1f || code === 0x7f;
  });
}

function validateAssetPath(value, option) {
  let decoded = value;
  for (let iteration = 0; iteration < 4; iteration += 1) {
    let next;
    try {
      next = decodeURIComponent(decoded);
    } catch {
      throw new Error(`${option} contains invalid percent encoding`);
    }
    if (next === decoded) break;
    decoded = next;
  }
  if (/%[0-9a-f]{2}/i.test(decoded)) throw new Error(`${option} is over-encoded`);
  if (hasControlCharacters(decoded)) {
    throw new Error(`${option} contains control characters`);
  }
  if (decoded !== decoded.trim()) throw new Error(`${option} contains URL whitespace`);
  decoded = decoded.replaceAll("\\", "/");
  if (
    !decoded ||
    decoded.startsWith("/") ||
    decoded.startsWith("//") ||
    /^[a-z][a-z0-9+.-]*:/i.test(decoded) ||
    /^[a-z]:\//i.test(decoded) ||
    decoded.split("/").includes("..")
  ) {
    throw new Error(`${option} must be a safe relative repository path`);
  }
}

function validate(args) {
  for (const key of ["fallback", "alt", "width", "height"]) requireOption(args, key);
  if (!["quality-first", "conservative", "static-only"].includes(args.mode)) {
    throw new Error("--mode must be quality-first, conservative, or static-only");
  }
  for (const key of ["width", "height"]) {
    if (!/^[1-9]\d*$/.test(args[key])) throw new Error(`--${key} must be a positive integer`);
  }
  for (const [key, value] of Object.entries(args)) {
    if (!/(?:fallback|staticWebp|staticPng|animatedWebp|animatedApng|animatedGif)/.test(key))
      continue;
    validateAssetPath(value, `--${key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}`);
  }
  if (hasControlCharacters(args.alt)) {
    throw new Error("--alt must not contain control characters");
  }
  if (
    args.mode === "quality-first" &&
    !args.animatedWebp &&
    !args.animatedApng &&
    !args.animatedGif
  ) {
    throw new Error("quality-first mode requires at least one animated source");
  }
  if (args.mode === "conservative" && !args.animatedGif) {
    throw new Error("conservative mode requires --animated-gif");
  }
}

function sourceLine(srcset, attrs = {}) {
  const renderedAttrs = [
    `srcset="${escapeAttr(srcset)}"`,
    attrs.type ? `type="${escapeAttr(attrs.type)}"` : null,
    attrs.media ? `media="${escapeAttr(attrs.media)}"` : null,
  ].filter(Boolean);
  return `    <source\n      ${renderedAttrs.join("\n      ")}\n    >`;
}

function reducedMotionSource(args) {
  if (args.staticWebp) {
    return sourceLine(args.staticWebp, {
      type: "image/webp",
      media: "(prefers-reduced-motion: reduce)",
    });
  }
  if (args.staticPng) {
    return sourceLine(args.staticPng, {
      media: "(prefers-reduced-motion: reduce)",
    });
  }
  return sourceLine(args.fallback, {
    media: "(prefers-reduced-motion: reduce)",
  });
}

function renderImg(args, indent = "  ") {
  return [
    `${indent}<img`,
    `${indent}  src="${escapeAttr(args.fallback)}"`,
    `${indent}  alt="${escapeAttr(args.alt)}"`,
    `${indent}  width="${escapeAttr(args.width)}"`,
    `${indent}  height="${escapeAttr(args.height)}"`,
    `${indent}>`,
  ].join("\n");
}

function renderSnippet(args) {
  if (args.mode === "static-only") {
    return ['<p align="center">', renderImg(args, "  "), "</p>"].join("\n");
  }

  const sources = [reducedMotionSource(args)];
  if (args.mode === "quality-first") {
    if (args.animatedWebp) sources.push(sourceLine(args.animatedWebp, { type: "image/webp" }));
    if (args.animatedApng) sources.push(sourceLine(args.animatedApng, { type: "image/apng" }));
    if (args.animatedGif) sources.push(sourceLine(args.animatedGif, { type: "image/gif" }));
  }
  if (args.mode === "conservative") {
    sources.push(sourceLine(args.animatedGif, { type: "image/gif" }));
  }

  return [
    '<p align="center">',
    "  <picture>",
    ...sources,
    renderImg(args, "    "),
    "  </picture>",
    "</p>",
  ].join("\n");
}

function main() {
  let args;
  try {
    args = parseArgs(process.argv.slice(2));
    if (args.help) {
      console.log(usage());
      return;
    }
    validate(args);
  } catch (error) {
    console.error(error.message);
    console.error(usage());
    process.exit(1);
  }

  console.log(renderSnippet(args));
}

main();
scripts/inspect-animated-image.mjs›
#!/usr/bin/env node
import {
  AnimatedImageError,
  DEFAULT_ANIMATED_IMAGE_LIMITS,
  inspectAnimatedImageFile,
} from "./lib/animated-image.mjs";

function usage() {
  return `Usage: node inspect-animated-image.mjs [options] <image>

Validate that a local GIF, APNG, or WebP is structurally animated and free of hidden metadata.

Options:
  --json                 Emit one deterministic JSON object
  --max-file-bytes N     Maximum input size (default: ${DEFAULT_ANIMATED_IMAGE_LIMITS.maxFileBytes})
  --max-chunk-bytes N    Maximum logical chunk size (default: ${DEFAULT_ANIMATED_IMAGE_LIMITS.maxChunkBytes})
  --max-frames N         Maximum frame count (default: ${DEFAULT_ANIMATED_IMAGE_LIMITS.maxFrames})
  --max-dimension N      Maximum width or height (default: ${DEFAULT_ANIMATED_IMAGE_LIMITS.maxDimension})
  -h, --help             Show this help

Exit codes: 0=valid animated image, 1=invalid/static/limit failure, 2=usage/I/O.`;
}

function positiveInteger(option, value) {
  if (!/^[1-9]\d*$/.test(value ?? "")) {
    throw new AnimatedImageError(
      "INVALID_OPTION",
      `${option} requires a positive integer`,
      "usage",
    );
  }
  const parsed = Number(value);
  if (!Number.isSafeInteger(parsed)) {
    throw new AnimatedImageError(
      "INVALID_OPTION",
      `${option} exceeds the safe integer range`,
      "usage",
    );
  }
  return parsed;
}

function parseArgs(argv) {
  const args = { limits: {} };
  const limitOptions = new Map([
    ["--max-file-bytes", "maxFileBytes"],
    ["--max-chunk-bytes", "maxChunkBytes"],
    ["--max-frames", "maxFrames"],
    ["--max-dimension", "maxDimension"],
  ]);
  for (let index = 0; index < argv.length; index += 1) {
    const argument = argv[index];
    if (argument === "--help" || argument === "-h") {
      args.help = true;
      continue;
    }
    if (argument === "--json") {
      args.json = true;
      continue;
    }
    if (limitOptions.has(argument)) {
      args.limits[limitOptions.get(argument)] = positiveInteger(argument, argv[index + 1]);
      index += 1;
      continue;
    }
    if (argument.startsWith("-")) {
      throw new AnimatedImageError("UNKNOWN_OPTION", `unknown option: ${argument}`, "usage");
    }
    if (args.file) {
      throw new AnimatedImageError("TOO_MANY_INPUTS", "provide exactly one image path", "usage");
    }
    args.file = argument;
  }
  return args;
}

function outputSuccess(file, inspected, jsonOutput) {
  const payload = { valid: true, path: file, ...inspected, errors: [] };
  if (jsonOutput) {
    console.log(JSON.stringify(payload));
    return;
  }
  console.log(`VALID ANIMATED IMAGE: ${file}`);
  console.log(`Format: ${inspected.format}`);
  console.log(`Dimensions: ${inspected.width} x ${inspected.height}`);
  console.log(`Frames: ${inspected.frameCount}`);
  console.log(`Animated: yes`);
  console.log(`Loop: ${inspected.loop}`);
}

function outputFailure(file, error, jsonOutput) {
  const payload = {
    valid: false,
    path: file ?? null,
    errors: [{ code: error.code ?? "UNEXPECTED_ERROR", message: error.message }],
  };
  if (jsonOutput) {
    console.log(JSON.stringify(payload));
    return;
  }
  console.error(`INVALID ANIMATED IMAGE: ${file ?? "(no input)"}`);
  console.error(`- [${payload.errors[0].code}] ${payload.errors[0].message}`);
}

function main() {
  let args;
  try {
    args = parseArgs(process.argv.slice(2));
  } catch (error) {
    const normalized =
      error instanceof AnimatedImageError
        ? error
        : new AnimatedImageError("INVALID_ARGUMENT", error.message, "usage");
    outputFailure(null, normalized, process.argv.includes("--json"));
    if (!process.argv.includes("--json")) console.error(usage());
    process.exitCode = 2;
    return;
  }
  if (args.help) {
    console.log(usage());
    return;
  }
  if (!args.file) {
    const error = new AnimatedImageError("MISSING_INPUT", "provide one local image path", "usage");
    outputFailure(null, error, args.json);
    if (!args.json) console.error(usage());
    process.exitCode = 2;
    return;
  }

  try {
    const inspected = inspectAnimatedImageFile(args.file, args.limits);
    if (!inspected.animated) {
      throw new AnimatedImageError(
        "STATIC_IMAGE",
        `${inspected.format} contains ${inspected.frameCount} frame and is not animated`,
      );
    }
    outputSuccess(args.file, inspected, args.json);
  } catch (error) {
    const normalized =
      error instanceof AnimatedImageError
        ? error
        : new AnimatedImageError("UNEXPECTED_ERROR", error.message);
    outputFailure(args.file, normalized, args.json);
    process.exitCode = ["usage", "io"].includes(normalized.category) ? 2 : 1;
  }
}

main();
scripts/lib/animated-image.mjs›
import {
  closeSync,
  constants as fsConstants,
  fstatSync,
  lstatSync,
  openSync,
  readSync,
} from "node:fs";

export const DEFAULT_ANIMATED_IMAGE_LIMITS = Object.freeze({
  maxFileBytes: 20 * 1024 * 1024,
  maxChunkBytes: 8 * 1024 * 1024,
  maxFrames: 1_000,
  maxDimension: 16_384,
  maxPixels: 64_000_000,
  maxChunks: 10_000,
});

export class AnimatedImageError extends Error {
  constructor(code, message, category = "validation") {
    super(message);
    this.name = "AnimatedImageError";
    this.code = code;
    this.category = category;
  }
}

function fail(code, message, category) {
  throw new AnimatedImageError(code, message, category);
}

function normalizeLimits(options = {}) {
  const limits = { ...DEFAULT_ANIMATED_IMAGE_LIMITS, ...options };
  for (const [name, value] of Object.entries(limits)) {
    if (!Number.isSafeInteger(value) || value <= 0) {
      fail("INVALID_LIMIT", `${name} must be a positive safe integer`, "usage");
    }
  }
  return limits;
}

function requireBytes(buffer, offset, length, context) {
  if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || length < 0) {
    fail("INTERNAL_BOUNDS_ERROR", `invalid parser bounds for ${context}`);
  }
  if (offset < 0 || offset + length > buffer.length) {
    fail("TRUNCATED_FILE", `${context} is truncated at byte ${offset}`);
  }
}

function assertCanvas(width, height, limits, context) {
  if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) {
    fail("INVALID_DIMENSIONS", `${context} dimensions must be positive integers`);
  }
  if (
    width > limits.maxDimension ||
    height > limits.maxDimension ||
    width * height > limits.maxPixels
  ) {
    fail("DIMENSION_LIMIT", `${context} dimensions exceed configured limits`);
  }
}

function loopLabel(loopCount) {
  if (loopCount === null) return "unspecified";
  return loopCount === 0 ? "infinite" : loopCount;
}

function result(format, width, height, frameCount, loopCount, details = {}) {
  return {
    format,
    width,
    height,
    frameCount,
    animated: frameCount > 1,
    loopCount,
    loop: loopLabel(loopCount),
    ...details,
  };
}

function inspectGif(buffer, limits) {
  requireBytes(buffer, 0, 13, "GIF header");
  const signature = buffer.toString("ascii", 0, 6);
  if (signature !== "GIF87a" && signature !== "GIF89a") {
    fail("INVALID_GIF_SIGNATURE", "GIF signature must be GIF87a or GIF89a");
  }
  const width = buffer.readUInt16LE(6);
  const height = buffer.readUInt16LE(8);
  assertCanvas(width, height, limits, "GIF canvas");
  const logicalPacked = buffer[10];
  const hasGlobalColorTable = Boolean(logicalPacked & 0x80);
  let offset = 13;
  if (hasGlobalColorTable) {
    const colorBytes = 3 * 2 ** ((logicalPacked & 0x07) + 1);
    if (colorBytes > limits.maxChunkBytes) {
      fail("CHUNK_LIMIT", "GIF global color table exceeds the chunk limit");
    }
    requireBytes(buffer, offset, colorBytes, "GIF global color table");
    offset += colorBytes;
  }

  let frameCount = 0;
  let loopCount = null;
  let chunkCount = 0;
  let trailerSeen = false;
  let pendingFrameDelay = null;
  let graphicControlCount = 0;
  const frameDelaysCentiseconds = [];

  const countChunk = (context) => {
    chunkCount += 1;
    if (chunkCount > limits.maxChunks) {
      fail("CHUNK_COUNT_LIMIT", `${context} exceeds the chunk-count limit`);
    }
  };

  const subBlocks = (context, capture = false) => {
    let total = 0;
    const parts = [];
    while (true) {
      requireBytes(buffer, offset, 1, `${context} sub-block length`);
      const length = buffer[offset];
      offset += 1;
      if (length === 0) break;
      countChunk(context);
      total += length;
      if (total > limits.maxChunkBytes) {
        fail("CHUNK_LIMIT", `${context} data exceeds the chunk limit`);
      }
      requireBytes(buffer, offset, length, `${context} sub-block`);
      if (capture) parts.push(buffer.subarray(offset, offset + length));
      offset += length;
    }
    return capture ? Buffer.concat(parts, total) : null;
  };

  while (offset < buffer.length) {
    const marker = buffer[offset];
    offset += 1;
    if (marker === 0x3b) {
      trailerSeen = true;
      break;
    }
    if (marker === 0x2c) {
      if (frameCount >= limits.maxFrames) {
        fail("FRAME_LIMIT", "GIF frame count exceeds the configured limit");
      }
      countChunk("GIF image descriptor");
      requireBytes(buffer, offset, 9, "GIF image descriptor");
      const left = buffer.readUInt16LE(offset);
      const top = buffer.readUInt16LE(offset + 2);
      const frameWidth = buffer.readUInt16LE(offset + 4);
      const frameHeight = buffer.readUInt16LE(offset + 6);
      const packed = buffer[offset + 8];
      offset += 9;
      if (packed & 0x18) {
        fail("INVALID_GIF_DESCRIPTOR", "GIF image descriptor reserved bits must be zero");
      }
      assertCanvas(frameWidth, frameHeight, limits, "GIF frame");
      if (left + frameWidth > width || top + frameHeight > height) {
        fail("FRAME_OUT_OF_BOUNDS", "GIF frame exceeds the logical canvas");
      }
      const hasLocalColorTable = Boolean(packed & 0x80);
      if (hasLocalColorTable) {
        const colorBytes = 3 * 2 ** ((packed & 0x07) + 1);
        if (colorBytes > limits.maxChunkBytes) {
          fail("CHUNK_LIMIT", "GIF local color table exceeds the chunk limit");
        }
        requireBytes(buffer, offset, colorBytes, "GIF local color table");
        offset += colorBytes;
      } else if (!hasGlobalColorTable) {
        fail("MISSING_COLOR_TABLE", "GIF frame has no global or local color table");
      }
      requireBytes(buffer, offset, 1, "GIF LZW minimum code size");
      const minimumCodeSize = buffer[offset];
      offset += 1;
      if (minimumCodeSize < 2 || minimumCodeSize > 8) {
        fail("INVALID_GIF_LZW", "GIF LZW minimum code size must be between 2 and 8");
      }
      subBlocks("GIF image data");
      frameDelaysCentiseconds.push(pendingFrameDelay ?? 0);
      pendingFrameDelay = null;
      frameCount += 1;
      continue;
    }
    if (marker !== 0x21) {
      fail("INVALID_GIF_BLOCK", `unexpected GIF block marker 0x${marker.toString(16)}`);
    }

    requireBytes(buffer, offset, 1, "GIF extension label");
    const label = buffer[offset];
    offset += 1;
    countChunk("GIF extension");
    if (label === 0xf9) {
      requireBytes(buffer, offset, 6, "GIF graphic-control extension");
      if (buffer[offset] !== 4 || buffer[offset + 5] !== 0) {
        fail("INVALID_GIF_EXTENSION", "GIF graphic-control extension has invalid framing");
      }
      const controlPacked = buffer[offset + 1];
      if ((controlPacked & 0xe0) !== 0 || ((controlPacked >>> 2) & 0x07) > 3) {
        fail("INVALID_GIF_EXTENSION", "GIF graphic-control flags are invalid");
      }
      if (4 > limits.maxChunkBytes) {
        fail("CHUNK_LIMIT", "GIF graphic-control extension exceeds the chunk limit");
      }
      if (pendingFrameDelay !== null) {
        fail("CONFLICTING_GIF_CONTROL", "GIF frame has multiple graphic-control extensions");
      }
      pendingFrameDelay = buffer.readUInt16LE(offset + 2);
      graphicControlCount += 1;
      offset += 6;
      continue;
    }
    if (label === 0x01) {
      fail("UNSUPPORTED_GIF_EXTENSION", "GIF plain-text extensions are not allowed");
    }
    if (label === 0xff) {
      requireBytes(buffer, offset, 1, "GIF fixed extension length");
      const fixedLength = buffer[offset];
      offset += 1;
      const expected = 11;
      if (fixedLength !== expected) {
        fail(
          "INVALID_GIF_EXTENSION",
          `GIF extension 0x${label.toString(16)} must be ${expected} bytes`,
        );
      }
      if (fixedLength > limits.maxChunkBytes) {
        fail("CHUNK_LIMIT", "GIF fixed extension data exceeds the chunk limit");
      }
      requireBytes(buffer, offset, fixedLength, "GIF fixed extension data");
      const identifier = buffer.toString("ascii", offset, offset + 11);
      offset += fixedLength;
      if (identifier !== "NETSCAPE2.0" && identifier !== "ANIMEXTS1.0") {
        fail(
          "HIDDEN_METADATA",
          `GIF application extension ${JSON.stringify(identifier)} is not allowed`,
        );
      }
      const extensionData = subBlocks("GIF extension data", true);
      if (identifier === "NETSCAPE2.0" || identifier === "ANIMEXTS1.0") {
        if (extensionData.length !== 3 || extensionData[0] !== 1) {
          fail("INVALID_GIF_LOOP", "GIF loop extension payload is malformed");
        }
        const parsedLoop = extensionData.readUInt16LE(1);
        if (loopCount !== null && loopCount !== parsedLoop) {
          fail("CONFLICTING_LOOP_METADATA", "GIF contains conflicting loop metadata");
        }
        loopCount = parsedLoop;
      }
      continue;
    }
    if (label === 0xfe) {
      fail("HIDDEN_METADATA", "GIF comment extensions are not allowed");
    }
    fail("INVALID_GIF_EXTENSION", `unsupported GIF extension label 0x${label.toString(16)}`);
  }

  if (!trailerSeen) fail("MISSING_GIF_TRAILER", "GIF trailer is missing");
  if (offset !== buffer.length) fail("TRAILING_DATA", "GIF contains data after its trailer");
  if (frameCount === 0) fail("MISSING_FRAMES", "GIF contains no image frames");
  if (pendingFrameDelay !== null) {
    fail("DANGLING_GIF_CONTROL", "GIF has a graphic-control extension without a frame");
  }
  return result("gif", width, height, frameCount, loopCount, {
    graphicControlCount,
    frameDelaysCentiseconds,
    durationCentiseconds: frameDelaysCentiseconds.reduce((total, delay) => total + delay, 0),
  });
}

let crcTable;
function crc32(buffer) {
  if (!crcTable) {
    crcTable = new Uint32Array(256);
    for (let index = 0; index < 256; index += 1) {
      let value = index;
      for (let bit = 0; bit < 8; bit += 1) {
        value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
      }
      crcTable[index] = value >>> 0;
    }
  }
  let crc = 0xffffffff;
  for (const byte of buffer) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
  return (crc ^ 0xffffffff) >>> 0;
}

const PNG_BIT_DEPTHS = new Map([
  [0, new Set([1, 2, 4, 8, 16])],
  [2, new Set([8, 16])],
  [3, new Set([1, 2, 4, 8])],
  [4, new Set([8, 16])],
  [6, new Set([8, 16])],
]);

function inspectPng(buffer, limits) {
  const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
  requireBytes(buffer, 0, signature.length, "PNG signature");
  if (!buffer.subarray(0, signature.length).equals(signature)) {
    fail("INVALID_PNG_SIGNATURE", "PNG signature is invalid");
  }

  let offset = 8;
  let chunkCount = 0;
  let width;
  let height;
  let bitDepth;
  let colorType;
  let seenIhdr = false;
  let seenPlte = false;
  let seenTrns = false;
  let paletteEntries = 0;
  let seenIdat = false;
  let idatClosed = false;
  let seenIend = false;
  let seenActl = false;
  let animationFrameTotal = null;
  let loopCount = null;
  let frameCount = 0;
  let expectedSequence = 0;
  let currentFrameHasData = false;
  let currentFrameDataKind = null;

  while (offset < buffer.length) {
    requireBytes(buffer, offset, 12, "PNG chunk");
    const length = buffer.readUInt32BE(offset);
    const type = buffer.toString("ascii", offset + 4, offset + 8);
    if (!/^[A-Za-z]{4}$/.test(type) || (type.charCodeAt(2) & 0x20) !== 0) {
      fail("INVALID_PNG_CHUNK_TYPE", `PNG chunk type ${JSON.stringify(type)} is invalid`);
    }
    if (length > limits.maxChunkBytes) {
      fail("CHUNK_LIMIT", `PNG ${type} chunk exceeds the configured limit`);
    }
    requireBytes(buffer, offset + 8, length + 4, `PNG ${type} chunk`);
    const data = buffer.subarray(offset + 8, offset + 8 + length);
    const expectedCrc = buffer.readUInt32BE(offset + 8 + length);
    const actualCrc = crc32(buffer.subarray(offset + 4, offset + 8 + length));
    if (actualCrc !== expectedCrc) {
      fail("PNG_CRC_MISMATCH", `PNG ${type} chunk CRC is invalid`);
    }
    offset += 12 + length;
    chunkCount += 1;
    if (chunkCount > limits.maxChunks) {
      fail("CHUNK_COUNT_LIMIT", "PNG chunk count exceeds the configured limit");
    }
    if (!seenIhdr && type !== "IHDR") fail("PNG_ORDER", "IHDR must be the first PNG chunk");
    if (seenIend) fail("TRAILING_DATA", "PNG contains chunks after IEND");
    if (seenIdat && type !== "IDAT") idatClosed = true;

    if (type === "IHDR") {
      if (seenIhdr || length !== 13) fail("INVALID_IHDR", "PNG must contain one 13-byte IHDR");
      seenIhdr = true;
      width = data.readUInt32BE(0);
      height = data.readUInt32BE(4);
      assertCanvas(width, height, limits, "PNG canvas");
      bitDepth = data[8];
      colorType = data[9];
      if (!PNG_BIT_DEPTHS.get(colorType)?.has(bitDepth)) {
        fail("INVALID_IHDR", "PNG bit-depth and color-type combination is invalid");
      }
      if (data[10] !== 0 || data[11] !== 0 || ![0, 1].includes(data[12])) {
        fail("INVALID_IHDR", "PNG compression, filter, or interlace method is invalid");
      }
      continue;
    }
    if (type === "PLTE") {
      if (seenPlte || seenIdat || length === 0 || length % 3 !== 0 || length > 768) {
        fail("INVALID_PLTE", "PNG PLTE must appear once before IDAT with 1 to 256 RGB entries");
      }
      if ([0, 4].includes(colorType)) {
        fail("INVALID_PLTE", "PNG grayscale color types must not contain PLTE");
      }
      if (colorType === 3 && length / 3 > 2 ** bitDepth) {
        fail("INVALID_PLTE", "indexed PNG PLTE has more entries than its bit depth permits");
      }
      paletteEntries = length / 3;
      seenPlte = true;
      continue;
    }
    if (type === "tRNS") {
      if (seenTrns || seenIdat) {
        fail("INVALID_TRNS", "PNG tRNS must appear at most once before IDAT");
      }
      if (colorType === 0 && length !== 2) {
        fail("INVALID_TRNS", "grayscale PNG tRNS must contain one sample");
      }
      if (colorType === 2 && length !== 6) {
        fail("INVALID_TRNS", "truecolor PNG tRNS must contain three samples");
      }
      if (colorType === 3 && (!seenPlte || length === 0 || length > paletteEntries)) {
        fail("INVALID_TRNS", "indexed PNG tRNS must follow PLTE and fit its palette");
      }
      if ([4, 6].includes(colorType)) {
        fail("INVALID_TRNS", "PNG color types with alpha must not contain tRNS");
      }
      seenTrns = true;
      continue;
    }
    if (type === "acTL") {
      if (seenActl || seenIdat || length !== 8) {
        fail("INVALID_ACTL", "APNG acTL must appear once before IDAT and be 8 bytes");
      }
      seenActl = true;
      animationFrameTotal = data.readUInt32BE(0);
      loopCount = data.readUInt32BE(4);
      if (animationFrameTotal === 0) fail("INVALID_ACTL", "APNG frame count must be positive");
      if (animationFrameTotal > limits.maxFrames) {
        fail("FRAME_LIMIT", "APNG declared frame count exceeds the configured limit");
      }
      continue;
    }
    if (type === "fcTL") {
      if (!seenActl || length !== 26) {
        fail("INVALID_FCTL", "APNG fcTL requires acTL and must be 26 bytes");
      }
      if (frameCount > 0 && !currentFrameHasData) {
        fail("MISSING_FRAME_DATA", "APNG frame has no IDAT or fdAT data");
      }
      if (frameCount >= limits.maxFrames) fail("FRAME_LIMIT", "APNG frame count exceeds the limit");
      const sequence = data.readUInt32BE(0);
      if (sequence !== expectedSequence)
        fail("APNG_SEQUENCE", "APNG sequence numbers are not contiguous");
      expectedSequence += 1;
      const frameWidth = data.readUInt32BE(4);
      const frameHeight = data.readUInt32BE(8);
      const x = data.readUInt32BE(12);
      const y = data.readUInt32BE(16);
      assertCanvas(frameWidth, frameHeight, limits, "APNG frame");
      if (
        frameCount === 0 &&
        !seenIdat &&
        (frameWidth !== width || frameHeight !== height || x !== 0 || y !== 0)
      ) {
        fail(
          "INVALID_FCTL",
          "APNG first frame control before IDAT must match the full default-image canvas",
        );
      }
      if (x + frameWidth > width || y + frameHeight > height) {
        fail("FRAME_OUT_OF_BOUNDS", "APNG frame exceeds the PNG canvas");
      }
      if (data[24] > 2 || data[25] > 1) {
        fail("INVALID_FCTL", "APNG dispose or blend operation is invalid");
      }
      frameCount += 1;
      currentFrameHasData = false;
      currentFrameDataKind = seenIdat ? "fdat" : "idat";
      continue;
    }
    if (type === "IDAT") {
      if (length === 0) fail("INVALID_IDAT", "PNG IDAT chunk must not be empty");
      if (idatClosed) fail("PNG_ORDER", "PNG IDAT chunks must be consecutive");
      if (colorType === 3 && !seenPlte) {
        fail("MISSING_PLTE", "indexed PNG requires PLTE before IDAT");
      }
      if (frameCount > 0 && currentFrameDataKind !== "idat") {
        fail("PNG_ORDER", "APNG IDAT data is only valid for the controlled default frame");
      }
      seenIdat = true;
      if (currentFrameDataKind === "idat") currentFrameHasData = true;
      continue;
    }
    if (type === "fdAT") {
      if (
        !seenActl ||
        !seenIdat ||
        frameCount === 0 ||
        currentFrameDataKind !== "fdat" ||
        length < 5
      ) {
        fail("INVALID_FDAT", "APNG fdAT requires prior IDAT data and a subsequent frame control");
      }
      const sequence = data.readUInt32BE(0);
      if (sequence !== expectedSequence)
        fail("APNG_SEQUENCE", "APNG sequence numbers are not contiguous");
      expectedSequence += 1;
      currentFrameHasData = true;
      continue;
    }
    if (type === "IEND") {
      if (length !== 0 || !seenIdat) fail("INVALID_IEND", "PNG IEND is invalid or IDAT is missing");
      if (frameCount > 0 && !currentFrameHasData) {
        fail("MISSING_FRAME_DATA", "APNG final frame has no IDAT or fdAT data");
      }
      seenIend = true;
      if (offset !== buffer.length) fail("TRAILING_DATA", "PNG contains data after IEND");
      break;
    }
    if (type[0] === type[0].toUpperCase()) {
      fail("UNKNOWN_CRITICAL_CHUNK", `unsupported critical PNG chunk ${type}`);
    }
    if (["eXIf", "iCCP", "iTXt", "tEXt", "tIME", "zTXt"].includes(type)) {
      fail("HIDDEN_METADATA", `PNG metadata chunk ${type} is not allowed`);
    }
    if (!["cHRM", "gAMA", "sBIT", "sRGB"].includes(type)) {
      fail("UNSUPPORTED_ANCILLARY_CHUNK", `unsupported PNG ancillary chunk ${type}`);
    }
  }

  if (!seenIhdr || !seenIend) fail("INCOMPLETE_PNG", "PNG is missing IHDR or IEND");
  if (seenActl) {
    if (frameCount === 0 || frameCount !== animationFrameTotal) {
      fail("APNG_FRAME_COUNT", "APNG fcTL count does not match acTL");
    }
    return result("apng", width, height, frameCount, loopCount);
  }
  if (frameCount > 0) fail("APNG_CONTROL_WITHOUT_ACTL", "APNG frame control appears without acTL");
  return result("png", width, height, 1, null);
}

function uint24Le(buffer, offset) {
  return buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16);
}

function parseVp8Dimensions(data, context) {
  if (data.length < 10 || (data[0] & 1) !== 0) {
    fail("INVALID_VP8", `${context} VP8 key-frame header is invalid`);
  }
  if (data[3] !== 0x9d || data[4] !== 0x01 || data[5] !== 0x2a) {
    fail("INVALID_VP8", `${context} VP8 start code is invalid`);
  }
  return {
    width: data.readUInt16LE(6) & 0x3fff,
    height: data.readUInt16LE(8) & 0x3fff,
  };
}

function parseVp8lDimensions(data, context) {
  if (data.length < 5 || data[0] !== 0x2f) {
    fail("INVALID_VP8L", `${context} VP8L header is invalid`);
  }
  const bits = data.readUInt32LE(1);
  if (bits >>> 29 !== 0) fail("INVALID_VP8L", `${context} VP8L version must be zero`);
  return {
    width: (bits & 0x3fff) + 1,
    height: ((bits >>> 14) & 0x3fff) + 1,
  };
}

function parseWebpFramePayload(data, frameWidth, frameHeight, limits, countChunk) {
  let offset = 16;
  let imageDimensions = null;
  let alphaSeen = false;
  while (offset < data.length) {
    requireBytes(data, offset, 8, "WebP ANMF child chunk");
    const type = data.toString("ascii", offset, offset + 4);
    const length = data.readUInt32LE(offset + 4);
    if (length > limits.maxChunkBytes)
      fail("CHUNK_LIMIT", `WebP ${type} child chunk exceeds the limit`);
    const paddedLength = length + (length & 1);
    requireBytes(data, offset + 8, paddedLength, `WebP ${type} child chunk`);
    if (length & 1 && data[offset + 8 + length] !== 0) {
      fail("INVALID_WEBP_PADDING", `WebP ${type} child padding byte must be zero`);
    }
    countChunk("WebP ANMF child chunk");
    const payload = data.subarray(offset + 8, offset + 8 + length);
    if (type === "ALPH") {
      if (alphaSeen || imageDimensions)
        fail("WEBP_FRAME_ORDER", "WebP ALPH must precede one frame image");
      alphaSeen = true;
    } else if (type === "VP8 ") {
      if (imageDimensions)
        fail("MULTIPLE_FRAME_IMAGES", "WebP frame contains multiple image chunks");
      imageDimensions = parseVp8Dimensions(payload, "WebP frame");
    } else if (type === "VP8L") {
      if (imageDimensions || alphaSeen) {
        fail("WEBP_FRAME_ORDER", "WebP lossless frame cannot follow ALPH or another image");
      }
      imageDimensions = parseVp8lDimensions(payload, "WebP frame");
    } else {
      fail(
        "INVALID_WEBP_FRAME_CHUNK",
        `unsupported WebP frame child chunk ${JSON.stringify(type)}`,
      );
    }
    offset += 8 + paddedLength;
  }
  if (!imageDimensions) fail("MISSING_FRAME_IMAGE", "WebP ANMF contains no VP8 or VP8L image");
  assertCanvas(imageDimensions.width, imageDimensions.height, limits, "WebP frame bitstream");
  if (imageDimensions.width !== frameWidth || imageDimensions.height !== frameHeight) {
    fail("FRAME_DIMENSION_MISMATCH", "WebP ANMF and frame bitstream dimensions differ");
  }
}

function inspectWebp(buffer, limits) {
  requireBytes(buffer, 0, 12, "WebP RIFF header");
  if (buffer.toString("ascii", 0, 4) !== "RIFF" || buffer.toString("ascii", 8, 12) !== "WEBP") {
    fail("INVALID_WEBP_SIGNATURE", "WebP RIFF signature is invalid");
  }
  const declaredSize = buffer.readUInt32LE(4) + 8;
  if (declaredSize !== buffer.length)
    fail("WEBP_SIZE_MISMATCH", "WebP RIFF size does not match the file");

  let offset = 12;
  let chunkCount = 0;
  let width;
  let height;
  let extended = false;
  let animationFlag = false;
  let animSeen = false;
  let loopCount = null;
  let frameCount = 0;
  let staticDimensions = null;
  let topLevelAlpha = false;

  const countChunk = (context) => {
    chunkCount += 1;
    if (chunkCount > limits.maxChunks)
      fail("CHUNK_COUNT_LIMIT", `${context} exceeds the chunk limit`);
  };

  while (offset < buffer.length) {
    requireBytes(buffer, offset, 8, "WebP chunk");
    const type = buffer.toString("ascii", offset, offset + 4);
    const length = buffer.readUInt32LE(offset + 4);
    if (!/^[A-Z0-9 ]{4}$/.test(type))
      fail("INVALID_WEBP_CHUNK", `invalid WebP chunk type ${JSON.stringify(type)}`);
    if (length > limits.maxChunkBytes) fail("CHUNK_LIMIT", `WebP ${type} chunk exceeds the limit`);
    const paddedLength = length + (length & 1);
    requireBytes(buffer, offset + 8, paddedLength, `WebP ${type} chunk`);
    if (length & 1 && buffer[offset + 8 + length] !== 0) {
      fail("INVALID_WEBP_PADDING", `WebP ${type} padding byte must be zero`);
    }
    const data = buffer.subarray(offset + 8, offset + 8 + length);
    offset += 8 + paddedLength;
    countChunk("WebP");

    if (type === "VP8X") {
      if (extended || chunkCount !== 1 || length !== 10) {
        fail("INVALID_VP8X", "WebP VP8X must be the first chunk and exactly 10 bytes");
      }
      if ((data[0] & 0xc1) !== 0 || data[1] !== 0 || data[2] !== 0 || data[3] !== 0) {
        fail("INVALID_VP8X", "WebP VP8X reserved bits and bytes must be zero");
      }
      if (data[0] & 0x2c) {
        fail("HIDDEN_METADATA", "WebP ICC, EXIF, and XMP metadata flags are not allowed");
      }
      extended = true;
      animationFlag = Boolean(data[0] & 0x02);
      width = uint24Le(data, 4) + 1;
      height = uint24Le(data, 7) + 1;
      assertCanvas(width, height, limits, "WebP canvas");
      continue;
    }
    if (type === "ANIM") {
      if (!extended || !animationFlag || animSeen || frameCount > 0 || length !== 6) {
        fail("INVALID_ANIM", "WebP ANIM requires one animation VP8X and must precede frames");
      }
      animSeen = true;
      loopCount = data.readUInt16LE(4);
      continue;
    }
    if (type === "ANMF") {
      if (!animSeen || length < 24) fail("INVALID_ANMF", "WebP ANMF requires ANIM and frame data");
      if (frameCount >= limits.maxFrames) fail("FRAME_LIMIT", "WebP frame count exceeds the limit");
      const x = uint24Le(data, 0) * 2;
      const y = uint24Le(data, 3) * 2;
      const frameWidth = uint24Le(data, 6) + 1;
      const frameHeight = uint24Le(data, 9) + 1;
      if (data[15] & 0xfc) fail("INVALID_ANMF", "WebP ANMF reserved flag bits must be zero");
      assertCanvas(frameWidth, frameHeight, limits, "WebP frame");
      if (x + frameWidth > width || y + frameHeight > height) {
        fail("FRAME_OUT_OF_BOUNDS", "WebP frame exceeds its canvas");
      }
      parseWebpFramePayload(data, frameWidth, frameHeight, limits, countChunk);
      frameCount += 1;
      continue;
    }
    if (type === "VP8 " || type === "VP8L") {
      if (staticDimensions || animSeen)
        fail("MULTIPLE_WEBP_IMAGES", "WebP contains unexpected image chunks");
      staticDimensions =
        type === "VP8 " ? parseVp8Dimensions(data, "WebP") : parseVp8lDimensions(data, "WebP");
      if (type === "VP8L" && topLevelAlpha) {
        fail("WEBP_IMAGE_ORDER", "WebP ALPH cannot precede a lossless VP8L image");
      }
      assertCanvas(staticDimensions.width, staticDimensions.height, limits, "WebP image");
      continue;
    }
    if (type === "ALPH") {
      if (!extended || animationFlag || topLevelAlpha || staticDimensions) {
        fail("WEBP_IMAGE_ORDER", "WebP ALPH must precede one extended static VP8 image");
      }
      topLevelAlpha = true;
      continue;
    }
    if (["ICCP", "EXIF", "XMP "].includes(type)) {
      fail("HIDDEN_METADATA", `WebP metadata chunk ${type.trim()} is not allowed`);
    }
    fail("UNKNOWN_WEBP_CHUNK", `unsupported WebP chunk ${JSON.stringify(type)}`);
  }

  if (animationFlag) {
    if (!animSeen || frameCount === 0)
      fail("INCOMPLETE_ANIMATION", "animated WebP lacks ANIM or ANMF chunks");
    if (staticDimensions)
      fail("INVALID_ANIMATED_WEBP", "animated WebP contains a top-level image chunk");
    return result("webp", width, height, frameCount, loopCount);
  }
  if (animSeen || frameCount > 0)
    fail("UNDECLARED_ANIMATION", "WebP animation chunks lack the VP8X flag");
  if (!staticDimensions) fail("MISSING_WEBP_IMAGE", "static WebP contains no VP8 or VP8L image");
  if (extended) {
    if (staticDimensions.width !== width || staticDimensions.height !== height) {
      fail("CANVAS_DIMENSION_MISMATCH", "WebP VP8X and image dimensions differ");
    }
  } else {
    ({ width, height } = staticDimensions);
  }
  return result("webp", width, height, 1, null);
}

export function inspectAnimatedImage(input, options = {}) {
  const limits = normalizeLimits(options);
  const buffer = Buffer.isBuffer(input)
    ? input
    : input instanceof Uint8Array
      ? Buffer.from(input.buffer, input.byteOffset, input.byteLength)
      : null;
  if (!buffer) fail("INVALID_INPUT", "input must be a Buffer or Uint8Array", "usage");
  if (buffer.length === 0) fail("EMPTY_FILE", "image file is empty");
  if (buffer.length > limits.maxFileBytes) {
    fail("FILE_LIMIT", "image file exceeds the configured byte limit");
  }
  if (buffer.length >= 6 && ["GIF87a", "GIF89a"].includes(buffer.toString("ascii", 0, 6))) {
    return inspectGif(buffer, limits);
  }
  if (
    buffer.length >= 8 &&
    buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
  ) {
    return inspectPng(buffer, limits);
  }
  if (
    buffer.length >= 12 &&
    buffer.toString("ascii", 0, 4) === "RIFF" &&
    buffer.toString("ascii", 8, 12) === "WEBP"
  ) {
    return inspectWebp(buffer, limits);
  }
  fail("UNSUPPORTED_FORMAT", "file is not a GIF, PNG/APNG, or WebP image");
}

export function inspectAnimatedImageFile(filePath, options = {}) {
  const limits = normalizeLimits(options);
  let descriptor;
  try {
    const pathStat = lstatSync(filePath);
    if (!pathStat.isFile()) {
      fail("NOT_REGULAR_FILE", "input path is not a regular file", "io");
    }
    const safeReadFlags =
      fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK ?? 0) | (fsConstants.O_NOFOLLOW ?? 0);
    descriptor = openSync(filePath, safeReadFlags);
    const stat = fstatSync(descriptor);
    if (!stat.isFile()) fail("NOT_REGULAR_FILE", "input path is not a regular file", "io");
    if (stat.size > limits.maxFileBytes) {
      fail("FILE_LIMIT", "image file exceeds the configured byte limit");
    }
    const buffer = Buffer.alloc(Math.min(stat.size, limits.maxFileBytes) + 1);
    let total = 0;
    while (total < buffer.length) {
      const read = readSync(descriptor, buffer, total, buffer.length - total, null);
      if (read === 0) break;
      total += read;
    }
    if (total > limits.maxFileBytes)
      fail("FILE_LIMIT", "image file exceeds the configured byte limit");
    return inspectAnimatedImage(buffer.subarray(0, total), limits);
  } catch (error) {
    if (error instanceof AnimatedImageError) throw error;
    fail("READ_ERROR", `unable to read local image: ${error.message}`, "io");
  } finally {
    if (descriptor !== undefined) closeSync(descriptor);
  }
}
scripts/lib/transactional-output.mjs›
import { createHash } from "node:crypto";
import {
  closeSync,
  constants,
  existsSync,
  fstatSync,
  linkSync,
  lstatSync,
  openSync,
  readFileSync,
  readSync,
  renameSync,
  rmdirSync,
  rmSync,
} from "node:fs";
import { basename, dirname, join, parse, relative, resolve, sep } from "node:path";

const MAX_TRANSACTIONAL_OUTPUT_BYTES = 128 * 1024 * 1024;

export class TransactionalOutputError extends Error {
  constructor(code, message, { category = "io", preserveStageDirectories = false } = {}) {
    super(message);
    this.name = "TransactionalOutputError";
    this.code = code;
    this.category = category;
    this.preserveStageDirectories = preserveStageDirectories;
  }
}

function regularFileStats(stats) {
  if (stats.isSymbolicLink?.() || !stats.isFile()) {
    throw new Error("transactional outputs and backups must remain regular files");
  }
  return stats;
}

function statIdentity(stats) {
  return { dev: stats.dev, ino: stats.ino };
}

function sameIdentity(left, right) {
  return left.dev === right.dev && left.ino === right.ino;
}

function digest(bytes) {
  return createHash("sha256").update(bytes).digest("hex");
}

function sameFileState(left, right) {
  return (
    sameIdentity(statIdentity(left), statIdentity(right)) &&
    left.size === right.size &&
    left.mtimeMs === right.mtimeMs &&
    left.ctimeMs === right.ctimeMs
  );
}

function readDescriptorBounded(descriptor) {
  const chunks = [];
  let totalBytes = 0;
  while (true) {
    const remaining = MAX_TRANSACTIONAL_OUTPUT_BYTES + 1 - totalBytes;
    const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, remaining));
    const bytesRead = readSync(descriptor, chunk, 0, chunk.length, null);
    if (bytesRead === 0) return Buffer.concat(chunks, totalBytes);
    totalBytes += bytesRead;
    if (totalBytes > MAX_TRANSACTIONAL_OUTPUT_BYTES) {
      throw new Error("transactional output exceeds the content-snapshot byte limit");
    }
    chunks.push(chunk.subarray(0, bytesRead));
  }
}

function nativeRegularFileSnapshot(file) {
  const pathStats = regularFileStats(lstatSync(file));
  const flags =
    constants.O_RDONLY | (process.platform === "win32" ? 0 : (constants.O_NOFOLLOW ?? 0));
  let descriptor;
  try {
    descriptor = openSync(file, flags);
    const before = regularFileStats(fstatSync(descriptor));
    if (!sameFileState(pathStats, before)) {
      throw new Error("regular file changed while it was being opened");
    }
    const bytes = readDescriptorBounded(descriptor);
    const after = regularFileStats(fstatSync(descriptor));
    const finalPathStats = regularFileStats(lstatSync(file));
    const beforeIdentity = statIdentity(before);
    if (
      !sameFileState(before, after) ||
      !sameFileState(after, finalPathStats) ||
      bytes.length !== after.size
    ) {
      throw new Error("regular file changed while its content was being read");
    }
    return {
      ...beforeIdentity,
      size: after.size,
      mtimeMs: after.mtimeMs,
      ctimeMs: after.ctimeMs,
      digest: digest(bytes),
    };
  } finally {
    if (descriptor !== undefined) closeSync(descriptor);
  }
}

function nativeDirectoryChainSnapshot(directory) {
  const absolute = resolve(directory);
  const root = parse(absolute).root;
  const suffix = relative(root, absolute);
  const paths = [root];
  let current = root;
  for (const component of suffix.split(sep).filter(Boolean)) {
    current = join(current, component);
    paths.push(current);
  }
  return paths.map((path) => {
    const stats = lstatSync(path);
    if (stats.isSymbolicLink() || !stats.isDirectory()) {
      throw new Error("transactional directory chains must not contain symlinks");
    }
    return { path, dev: stats.dev, ino: stats.ino };
  });
}

const defaultOperations = Object.freeze({
  existsSync,
  linkSync,
  lstatSync,
  readFileSync,
  renameSync,
  rmSync,
  snapshotDirectoryChainSync: nativeDirectoryChainSnapshot,
  snapshotRegularFileSync: nativeRegularFileSnapshot,
});

function regularFileSnapshot(operations, file) {
  if (typeof operations.snapshotRegularFileSync === "function") {
    return operations.snapshotRegularFileSync(file);
  }
  const before = regularFileStats(operations.lstatSync(file));
  const bytes = operations.readFileSync(file);
  const after = regularFileStats(operations.lstatSync(file));
  const beforeIdentity = statIdentity(before);
  if (
    !sameIdentity(beforeIdentity, statIdentity(after)) ||
    (typeof after.size === "number" && bytes.length !== after.size)
  ) {
    throw new Error("regular file changed while its content was being read");
  }
  return {
    ...beforeIdentity,
    size: bytes.length,
    digest: digest(bytes),
  };
}

function sameSnapshot(left, right) {
  return sameIdentity(left, right) && left.size === right.size && left.digest === right.digest;
}

function directoryChainSnapshot(operations, directory) {
  return typeof operations.snapshotDirectoryChainSync === "function"
    ? operations.snapshotDirectoryChainSync(directory)
    : null;
}

function sameDirectoryChain(left, right) {
  if (left === null || right === null) return left === right;
  return (
    left.length === right.length &&
    left.every(
      (entry, index) =>
        entry.path === right[index].path &&
        entry.dev === right[index].dev &&
        entry.ino === right[index].ino,
    )
  );
}

function assertDirectoryBindings(operations, state) {
  for (const item of state) {
    const currentStaged = directoryChainSnapshot(operations, dirname(item.staged));
    const currentDestination = directoryChainSnapshot(operations, dirname(item.destination));
    if (
      !sameDirectoryChain(currentStaged, item.stagedDirectoryChain) ||
      !sameDirectoryChain(currentDestination, item.destinationDirectoryChain)
    ) {
      throw new Error("transactional output directory chain changed during commit");
    }
  }
}

export function captureOutputSnapshot(file) {
  return nativeRegularFileSnapshot(file);
}

export function removeAnchoredStageDirectory(
  stage,
  fileNames,
  { operations = { rmSync, rmdirSync } } = {},
) {
  for (const fileName of fileNames) {
    operations.rmSync(join(stage.anchorPath, fileName), { force: true });
  }
  // rmdir removes only the directory entry itself. If a peer replaced that entry with a
  // symlink, it fails without traversing or recursively deleting the foreign target.
  operations.rmdirSync(stage.entryPath);
}

function recoveryRequired(message) {
  return new TransactionalOutputError("OUTPUT_ROLLBACK_INCOMPLETE", message, {
    preserveStageDirectories: true,
  });
}

export function replaceOutputsAtomically(
  replacements,
  { operations = defaultOperations, replace = true } = {},
) {
  const state = replacements.map(
    ({ staged, destination, stagedMutationPath, destinationMutationPath, validatedSnapshot }) => {
      const stagedPath = stagedMutationPath ?? staged;
      const destinationPath = destinationMutationPath ?? destination;
      return {
        staged,
        stagedPath,
        destination,
        destinationPath,
        backup: join(dirname(staged), `backup-${basename(destination)}`),
        backupPath: join(dirname(stagedPath), `backup-${basename(destination)}`),
        backedUp: false,
        backupSnapshot: null,
        installed: false,
        installedByLink: false,
        stagedSnapshot: validatedSnapshot ?? null,
        stagedDirectoryChain: null,
        destinationDirectoryChain: null,
      };
    },
  );

  let collision = false;
  try {
    for (const item of state) {
      item.stagedDirectoryChain = directoryChainSnapshot(operations, dirname(item.staged));
      item.destinationDirectoryChain = directoryChainSnapshot(
        operations,
        dirname(item.destination),
      );
    }
    assertDirectoryBindings(operations, state);

    // Capture every validated source before mutating any destination. Callers may pass
    // the snapshot taken immediately after validation; this closes the validation-to-
    // commit gap as well as protecting the remainder of the transaction.
    for (const item of state) {
      const current = regularFileSnapshot(operations, item.stagedPath);
      if (item.stagedSnapshot && !sameSnapshot(current, item.stagedSnapshot)) {
        throw new Error("staged output changed after validation");
      }
      item.stagedSnapshot = current;
    }

    if (replace) {
      for (const item of state) {
        if (operations.existsSync(item.destinationPath)) {
          assertDirectoryBindings(operations, state);
          const destinationSnapshot = regularFileSnapshot(operations, item.destinationPath);
          operations.renameSync(item.destinationPath, item.backupPath);
          item.backedUp = true;
          assertDirectoryBindings(operations, state);
          item.backupSnapshot = regularFileSnapshot(operations, item.backupPath);
          if (!sameSnapshot(destinationSnapshot, item.backupSnapshot)) {
            throw new Error("destination content changed while it was being moved to recovery");
          }
        }
      }
    }

    for (const item of state) {
      try {
        assertDirectoryBindings(operations, state);
        const stagedSnapshot = regularFileSnapshot(operations, item.stagedPath);
        if (!sameSnapshot(stagedSnapshot, item.stagedSnapshot)) {
          throw new Error("staged output changed before installation");
        }
        operations.linkSync(item.stagedPath, item.destinationPath);
        item.installed = true;
        item.installedByLink = true;
        assertDirectoryBindings(operations, state);
      } catch (error) {
        collision = error?.code === "EEXIST";
        throw error;
      }
      const installedSnapshot = regularFileSnapshot(operations, item.destinationPath);
      if (!sameSnapshot(installedSnapshot, item.stagedSnapshot)) {
        throw new Error("installed output does not match its validated content");
      }
    }

    // Recheck the full installed set after the last link. A writer through an earlier
    // public hard link must not turn unvalidated bytes into a successful transaction.
    for (const item of state) {
      const installedSnapshot = regularFileSnapshot(operations, item.destinationPath);
      const stagedSnapshot = regularFileSnapshot(operations, item.stagedPath);
      if (
        !sameSnapshot(installedSnapshot, item.stagedSnapshot) ||
        !sameSnapshot(stagedSnapshot, item.stagedSnapshot)
      ) {
        throw new Error("output content changed while the output set was being installed");
      }
    }
    assertDirectoryBindings(operations, state);
  } catch {
    if (replace && state.some((item) => item.backedUp || item.installed)) {
      // A path-based check followed by unlink or rename would introduce another TOCTOU
      // window. Once replacement has mutated any destination, retain the complete recovery
      // state rather than touching a path whose ownership and bytes cannot be proved.
      throw recoveryRequired(
        "output replacement was interrupted; current destinations, staged files, and backups were retained for manual recovery",
      );
    }
    if (!replace && state.some((item) => item.installedByLink)) {
      // Once a public hard link exists, another process may have modified that inode in place.
      // Retaining every link is the only rollback policy that cannot discard concurrent data.
      throw recoveryRequired(
        "output commit was interrupted; partial linked outputs and staged files were retained for manual recovery",
      );
    }
    if (collision) {
      throw new TransactionalOutputError(
        "OUTPUT_EXISTS",
        "one or more declared outputs appeared before commit; no existing output was replaced",
        { category: "validation" },
      );
    }
    throw new TransactionalOutputError(
      "OUTPUT_REPLACEMENT_FAILED",
      "validated outputs could not replace their destinations",
    );
  }

  if (replace && state.some((item) => item.backedUp)) {
    // Replacement recovery is intentionally durable. There is no safe verify-then-unlink
    // sequence for a backup whose inode may still be writable through another process's
    // open descriptor, so successful --replace commits retain both staged links and every
    // prior-output backup for deliberate operator cleanup.
    try {
      assertDirectoryBindings(operations, state);
      for (const item of state) {
        if (item.backedUp) {
          const backupSnapshot = regularFileSnapshot(operations, item.backupPath);
          if (!sameSnapshot(backupSnapshot, item.backupSnapshot)) {
            throw new Error("replacement backup changed before recovery retention");
          }
        }
      }
      assertDirectoryBindings(operations, state);
      // Backup reads can execute arbitrary filesystem work and therefore come first. The
      // final operation before return is one complete public+staged digest pass, so a write
      // to an early public hard link during a later backup read cannot escape validation.
      for (const item of state) {
        const installedSnapshot = regularFileSnapshot(operations, item.destinationPath);
        const stagedSnapshot = regularFileSnapshot(operations, item.stagedPath);
        if (
          !sameSnapshot(installedSnapshot, item.stagedSnapshot) ||
          !sameSnapshot(stagedSnapshot, item.stagedSnapshot)
        ) {
          throw new Error("replacement output changed before recovery retention");
        }
      }
    } catch {
      throw recoveryRequired(
        "outputs were installed, but replacement recovery content changed; all remaining recovery files were retained",
      );
    }
    return { preserveStageDirectories: true, retainedRecovery: true };
  }

  // Preflight the complete recovery set before deleting any link. A changed backup or
  // an in-place write through any public hard link must retain every recovery copy.
  try {
    assertDirectoryBindings(operations, state);
    for (const item of state) {
      if (!item.installedByLink) continue;
      const stagedSnapshot = regularFileSnapshot(operations, item.stagedPath);
      const installedSnapshot = regularFileSnapshot(operations, item.destinationPath);
      if (
        !sameSnapshot(stagedSnapshot, item.stagedSnapshot) ||
        !sameSnapshot(installedSnapshot, item.stagedSnapshot)
      ) {
        throw new Error("installed output content changed before staged-link cleanup");
      }
    }
    for (const item of state) {
      if (!item.backedUp) continue;
      const backupSnapshot = regularFileSnapshot(operations, item.backupPath);
      if (!sameSnapshot(backupSnapshot, item.backupSnapshot)) {
        throw new Error("prior-output backup content changed before cleanup");
      }
    }
  } catch {
    throw recoveryRequired(
      "outputs were installed, but recovery content changed before cleanup; all recovery links were retained",
    );
  }

  // Recheck immediately before each removal as well. Any mismatch or filesystem error
  // retains the paths that remain for deliberate manual recovery.
  for (const item of state) {
    if (!item.installedByLink) continue;
    try {
      assertDirectoryBindings(operations, state);
      const stagedSnapshot = regularFileSnapshot(operations, item.stagedPath);
      const installedSnapshot = regularFileSnapshot(operations, item.destinationPath);
      if (
        !sameSnapshot(stagedSnapshot, item.stagedSnapshot) ||
        !sameSnapshot(installedSnapshot, item.stagedSnapshot)
      ) {
        throw new Error("installed output content changed during staged-link cleanup");
      }
      operations.rmSync(item.stagedPath, { force: true });
      assertDirectoryBindings(operations, state);
    } catch {
      throw recoveryRequired(
        "outputs were installed, but staged files could not be safely removed; remaining recovery files were retained",
      );
    }
  }

  // A mutation through a public hard link during cleanup is still a failed commit, even
  // though the inode identity remains unchanged.
  for (const item of state) {
    try {
      assertDirectoryBindings(operations, state);
      const installedSnapshot = regularFileSnapshot(operations, item.destinationPath);
      if (!sameSnapshot(installedSnapshot, item.stagedSnapshot)) {
        throw new Error("installed output content changed during cleanup");
      }
    } catch {
      throw recoveryRequired(
        "outputs were installed, but their validated bytes changed during cleanup; remaining recovery files were retained",
      );
    }
  }
  return { preserveStageDirectories: false, retainedRecovery: false };
}
scripts/validate_logo_svg.py›
#!/usr/bin/env python3
"""Validate a self-contained, repository-safe SVG logo without third-party packages."""

from __future__ import annotations

import argparse
import json
import math
import os
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any
from urllib.parse import unquote


MAX_FILE_BYTES = 5 * 1024 * 1024
MAX_ELEMENTS = 20_000
MAX_DEPTH = 128
MAX_USE_DEPTH = 128
MAX_RENDER_DEPTH = 256
MAX_GRADIENT_REFERENCE_DEPTH = 128
MAX_FINDINGS = 16
MAX_DIMENSION = 32_768.0
MAX_CANVAS_AREA = 64_000_000.0
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
XLINK_NAMESPACE = "http://www.w3.org/1999/xlink"

NUMBER = r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?"
LENGTH_RE = re.compile(rf"^({NUMBER})(?:px)?$", re.IGNORECASE)
URL_RE = re.compile(r"url\s*\(\s*(['\"]?)(.*?)\1\s*\)", re.IGNORECASE)
SAFE_FRAGMENT_RE = re.compile(r"^#[A-Za-z_][A-Za-z0-9_.:-]*$")
DECLARATION_RE = re.compile(r"^\s*<\?xml\s+[^?]*\?>", re.IGNORECASE)
ENCODING_RE = re.compile(r"\bencoding\s*=\s*(['\"])([^'\"]+)\1", re.IGNORECASE)
PATH_TOKEN_RE = re.compile(rf"{NUMBER}|[AaCcHhLlMmQqSsTtVvZz]")
CSS_ESCAPE_RE = re.compile(r"\\(?:[0-9a-fA-F]{1,6}\s?|.)")
CSS_COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)
CSS_VARIABLE_RE = re.compile(r"\bvar\s*\(|(?:^|;)\s*--[a-z0-9_-]+\s*:", re.IGNORECASE)
CSS_LIVE_MOTION_RE = re.compile(
    r"@(?:-[a-z]+-)?keyframes\b|"
    r"(?:^|[;{])\s*(?:-[a-z]+-)?(?:animation|transition)(?:-[a-z-]+)?\s*:",
    re.IGNORECASE,
)
URI_SCHEME_RE = re.compile(r"^[a-z][a-z0-9+.-]*:", re.IGNORECASE)
WINDOWS_ABSOLUTE_RE = re.compile(r"^[a-z]:[\\/]", re.IGNORECASE)

DANGEROUS_ELEMENTS = {
    "script",
    "foreignobject",
    "iframe",
    "object",
    "embed",
    "audio",
    "video",
}
SMIL_ELEMENTS = {
    "animate",
    "animatecolor",
    "animatemotion",
    "animatetransform",
    "discard",
    "mpath",
    "set",
}
REFERENCE_ATTRIBUTES = {"href", "src", "poster"}
RENDERABLE_ELEMENTS = {
    "circle",
    "ellipse",
    "line",
    "path",
    "polygon",
    "polyline",
    "rect",
    "text",
    "use",
}
NON_RENDERING_CONTAINERS = {
    "clippath",
    "defs",
    "desc",
    "filter",
    "lineargradient",
    "marker",
    "mask",
    "metadata",
    "pattern",
    "radialgradient",
    "style",
    "symbol",
    "title",
}
CSS_PRESENTATION_ATTRIBUTES = {
    "animation",
    "animation-delay",
    "animation-direction",
    "animation-duration",
    "animation-fill-mode",
    "animation-iteration-count",
    "animation-name",
    "animation-play-state",
    "animation-timing-function",
    "clip-path",
    "color",
    "cursor",
    "display",
    "fill",
    "fill-opacity",
    "filter",
    "font-family",
    "font-size",
    "font-style",
    "font-weight",
    "marker-end",
    "marker-mid",
    "marker-start",
    "mask",
    "opacity",
    "overflow",
    "pointer-events",
    "shape-rendering",
    "stop-color",
    "stop-opacity",
    "stroke",
    "stroke-dasharray",
    "stroke-dashoffset",
    "stroke-linecap",
    "stroke-linejoin",
    "stroke-opacity",
    "stroke-width",
    "text-anchor",
    "text-rendering",
    "transform-origin",
    "transition",
    "transition-delay",
    "transition-duration",
    "transition-property",
    "transition-timing-function",
    "vector-effect",
    "visibility",
}
STATIC_PRESENTATION_ATTRIBUTES = CSS_PRESENTATION_ATTRIBUTES | {
    "alignment-baseline",
    "baseline-shift",
    "clip-rule",
    "color-interpolation",
    "color-interpolation-filters",
    "color-rendering",
    "direction",
    "dominant-baseline",
    "fill-rule",
    "flood-color",
    "flood-opacity",
    "letter-spacing",
    "lighting-color",
    "paint-order",
    "stroke-miterlimit",
    "text-decoration",
    "unicode-bidi",
    "word-spacing",
    "writing-mode",
}
ALLOWED_STATIC_ELEMENTS = {
    "circle",
    "clippath",
    "defs",
    "desc",
    "ellipse",
    "feblend",
    "fecolormatrix",
    "fecomponenttransfer",
    "fecomposite",
    "feconvolvematrix",
    "fediffuselighting",
    "fedisplacementmap",
    "fedistantlight",
    "fedropshadow",
    "feflood",
    "fefunca",
    "fefuncb",
    "fefuncg",
    "fefuncr",
    "fegaussianblur",
    "feimage",
    "femerge",
    "femergenode",
    "femorphology",
    "feoffset",
    "fepointlight",
    "fespecularlighting",
    "fespotlight",
    "fetile",
    "feturbulence",
    "filter",
    "g",
    "image",
    "line",
    "lineargradient",
    "marker",
    "mask",
    "metadata",
    "path",
    "pattern",
    "polygon",
    "polyline",
    "radialgradient",
    "rect",
    "script",
    "stop",
    "style",
    "svg",
    "symbol",
    "text",
    "textpath",
    "title",
    "tspan",
    "use",
} | DANGEROUS_ELEMENTS | SMIL_ELEMENTS
STATIC_GLOBAL_ATTRIBUTES = STATIC_PRESENTATION_ATTRIBUTES | {
    "aria-describedby",
    "aria-label",
    "aria-labelledby",
    "id",
    "lang",
    "role",
    "space",
    "style",
    "transform",
}
STATIC_GEOMETRY_ATTRIBUTES = {
    "circle": {"cx", "cy", "pathlength", "r"},
    "ellipse": {"cx", "cy", "pathlength", "rx", "ry"},
    "line": {"pathlength", "x1", "x2", "y1", "y2"},
    "path": {"d", "pathlength"},
    "polygon": {"pathlength", "points"},
    "polyline": {"pathlength", "points"},
    "rect": {"height", "pathlength", "rx", "ry", "width", "x", "y"},
    "svg": {
        "baseprofile",
        "height",
        "preserveaspectratio",
        "version",
        "viewbox",
        "width",
        "x",
        "y",
    },
    "symbol": {"height", "preserveaspectratio", "refx", "refy", "viewbox", "width"},
    "text": {"dx", "dy", "lengthadjust", "rotate", "textlength", "x", "y"},
    "textpath": {"href", "lengthadjust", "method", "side", "spacing", "startoffset", "textlength"},
    "tspan": {"dx", "dy", "lengthadjust", "rotate", "textlength", "x", "y"},
    "use": {"height", "href", "width", "x", "y"},
}
STATIC_RESOURCE_ATTRIBUTES = {
    "clippath": {"clippathunits"},
    "filter": {"filterunits", "height", "primitiveunits", "width", "x", "y"},
    "lineargradient": {"gradienttransform", "gradientunits", "href", "spreadmethod", "x1", "x2", "y1", "y2"},
    "marker": {"markerheight", "markerunits", "markerwidth", "orient", "preserveaspectratio", "refx", "refy", "viewbox"},
    "mask": {"height", "maskcontentunits", "maskunits", "width", "x", "y"},
    "pattern": {"height", "href", "patterncontentunits", "patterntransform", "patternunits", "preserveaspectratio", "viewbox", "width", "x", "y"},
    "radialgradient": {"cx", "cy", "fr", "fx", "fy", "gradienttransform", "gradientunits", "href", "r", "spreadmethod"},
    "stop": {"offset"},
}
STATIC_FILTER_ATTRIBUTES = {
    "amplitude",
    "azimuth",
    "basefrequency",
    "bias",
    "diffuseconstant",
    "divisor",
    "dx",
    "dy",
    "edgemode",
    "elevation",
    "exponent",
    "height",
    "in",
    "in2",
    "intercept",
    "kernelmatrix",
    "kernelunitlength",
    "k1",
    "k2",
    "k3",
    "k4",
    "limitingconeangle",
    "mode",
    "numoctaves",
    "operator",
    "order",
    "pointsatx",
    "pointsaty",
    "pointsatz",
    "preservealpha",
    "radius",
    "result",
    "scale",
    "seed",
    "slope",
    "specularconstant",
    "specularexponent",
    "stddeviation",
    "stitchtiles",
    "surfacescale",
    "tablevalues",
    "targetx",
    "targety",
    "type",
    "values",
    "width",
    "x",
    "xchannelselector",
    "y",
    "ychannelselector",
    "z",
}
PAINT_SERVER_TYPES = {"lineargradient", "pattern", "radialgradient"}
REFERENCE_TARGET_TYPES = {
    "clip-path": {"clippath"},
    "fill": PAINT_SERVER_TYPES,
    "filter": {"filter"},
    "marker-end": {"marker"},
    "marker-mid": {"marker"},
    "marker-start": {"marker"},
    "mask": {"mask"},
    "stroke": PAINT_SERVER_TYPES,
    "gradient-href": PAINT_SERVER_TYPES,
    "pattern-href": {"pattern"},
    "textpath-href": {"path"},
}

Bounds = tuple[float, float, float, float]
RenderSummary = tuple[bool, Bounds | None]


class Finding(dict[str, str]):
    def __init__(self, code: str, message: str):
        super().__init__(code=code, message=message)


def local_name(name: str) -> str:
    return name.rsplit("}", 1)[-1].rsplit(":", 1)[-1]


def namespace_name(name: str) -> str | None:
    if name.startswith("{") and "}" in name:
        return name[1:].split("}", 1)[0]
    return None


def static_attribute_is_allowed(element_name: str, attribute_name: str) -> bool:
    if attribute_name in STATIC_GLOBAL_ATTRIBUTES:
        return True
    if attribute_name in STATIC_GEOMETRY_ATTRIBUTES.get(element_name, set()):
        return True
    if attribute_name in STATIC_RESOURCE_ATTRIBUTES.get(element_name, set()):
        return True
    if element_name.startswith("fe") and attribute_name in STATIC_FILTER_ATTRIBUTES:
        return True
    return element_name in {"feimage", "image"} and attribute_name in {
        "height",
        "href",
        "preserveaspectratio",
        "width",
        "x",
        "y",
    }


def is_local_input_path(value: str) -> bool:
    return bool(WINDOWS_ABSOLUTE_RE.match(value)) or not URI_SCHEME_RE.match(value)


def finite_number(value: str) -> float | None:
    try:
        parsed = float(value)
    except ValueError:
        return None
    return parsed if math.isfinite(parsed) else None


def positive_length(value: str | None) -> float | None:
    if value is None:
        return None
    match = LENGTH_RE.fullmatch(value.strip())
    if not match:
        return None
    parsed = finite_number(match.group(1))
    if parsed is None or parsed <= 0:
        return None
    return parsed


def parse_view_box(value: str | None) -> tuple[float, float, float, float] | None:
    if value is None:
        return None
    parts = [part for part in re.split(r"[\s,]+", value.strip()) if part]
    if len(parts) != 4:
        return None
    numbers = [finite_number(part) for part in parts]
    if any(number is None for number in numbers):
        return None
    x, y, width, height = numbers
    if width <= 0 or height <= 0:
        return None
    return (x, y, width, height)


def normalized_reference(value: str) -> str:
    result = value.strip()
    for _ in range(3):
        decoded = unquote(result)
        if decoded == result:
            break
        result = decoded
    return result.strip()


def reference_error(value: str) -> tuple[str, str] | None:
    literal = value.strip()
    normalized = normalized_reference(value)
    lowered = normalized.lower()
    if SAFE_FRAGMENT_RE.fullmatch(literal):
        return None
    if re.match(r"^[a-z][a-z0-9+.-]*:", lowered) or lowered.startswith("//"):
        return (
            "external-reference",
            "external, data, and file references are forbidden",
        )
    if (
        normalized.startswith(("/", "\\", "~"))
        or "\\" in normalized
        or any(part == ".." for part in normalized.split("/"))
    ):
        return ("unsafe-path-reference", "unsafe path-like references are forbidden")
    return (
        "nonlocal-reference",
        "only literal same-document fragment references are allowed",
    )


def color_is_obviously_transparent(value: str) -> bool:
    compact = re.sub(r"\s+", "", value).lower()
    if compact in {"none", "transparent"}:
        return True
    rgba = re.fullmatch(r"rgba\([^,]+,[^,]+,[^,]+,([^\)]+)\)", compact)
    if rgba:
        alpha = finite_number(rgba.group(1).rstrip("%"))
        if alpha is not None:
            if rgba.group(1).endswith("%"):
                return alpha <= 0
            return alpha <= 0
    functional_alpha = re.fullmatch(
        r"(?:rgba|hsla)\(.*,([^,\)]*)\)", compact
    ) or re.fullmatch(r"(?:rgba?|hsla?)\(.*\/([^\)]*)\)", compact)
    if functional_alpha:
        alpha = opacity_fraction(functional_alpha.group(1))
        return alpha is not None and alpha <= 0
    if re.fullmatch(r"#[0-9a-f]{4}", compact):
        return compact[-1] == "0"
    if re.fullmatch(r"#[0-9a-f]{8}", compact):
        return compact[-2:] == "00"
    return False


def opacity_is_zero(value: str | None) -> bool:
    if value is None:
        return False
    stripped = value.strip()
    parsed = finite_number(stripped.rstrip("%"))
    if parsed is None:
        return False
    return parsed <= 0


def rect_covers_canvas(
    element: ET.Element, view_box: tuple[float, float, float, float]
) -> bool:
    min_x, min_y, canvas_width, canvas_height = view_box

    def coordinate(name: str, default: float) -> float | None:
        raw = element.attrib.get(name)
        if raw is None:
            return default
        match = LENGTH_RE.fullmatch(raw.strip())
        return finite_number(match.group(1)) if match else None

    x = coordinate("x", 0.0)
    y = coordinate("y", 0.0)
    width_raw = element.attrib.get("width", "")
    height_raw = element.attrib.get("height", "")
    width = canvas_width if width_raw.strip() == "100%" else positive_length(width_raw)
    height = canvas_height if height_raw.strip() == "100%" else positive_length(height_raw)
    if None in (x, y, width, height):
        return False
    tolerance = max(canvas_width, canvas_height, 1.0) * 1e-7
    return (
        x <= min_x + tolerance
        and y <= min_y + tolerance
        and x + width >= min_x + canvas_width - tolerance
        and y + height >= min_y + canvas_height - tolerance
    )


def add_finding(findings: list[Finding], code: str, message: str) -> None:
    finding = Finding(code, message)
    if finding in findings or any(item["code"] == "too-many-findings" for item in findings):
        return
    if len(findings) >= MAX_FINDINGS - 1:
        findings.append(
            Finding(
                "too-many-findings",
                f"SVG exceeds the {MAX_FINDINGS - 1}-finding reporting limit; remaining findings omitted",
            )
        )
        return
    findings.append(finding)


def decode_svg(data: bytes, findings: list[Finding]) -> str | None:
    if data.startswith((b"\xff\xfe", b"\xfe\xff")) or data.startswith(
        (b"\x00<\x00?", b"<\x00?\x00")
    ):
        add_finding(
            findings,
            "utf16-forbidden",
            "SVG input must be UTF-8 or UTF-8 with a BOM; UTF-16 is forbidden",
        )
        return None
    if b"\x00" in data:
        add_finding(findings, "nul-byte", "SVG input must not contain NUL bytes")
        return None
    try:
        return data.decode("utf-8-sig")
    except UnicodeDecodeError:
        add_finding(findings, "invalid-utf8", "SVG input is not valid UTF-8")
        return None


def declaration_findings(text: str, findings: list[Finding]) -> None:
    declaration = DECLARATION_RE.match(text)
    if declaration:
        encoding = ENCODING_RE.search(declaration.group(0))
        declared_encoding = encoding.group(2).lower().replace("_", "-") if encoding else None
        if declared_encoding not in {None, "utf-8", "utf8"}:
            add_finding(
                findings,
                "encoding-declaration",
                "XML encoding declaration must be UTF-8 when present",
            )
    without_declaration = DECLARATION_RE.sub("", text, count=1)
    if "<?" in without_declaration:
        add_finding(
            findings,
            "processing-instruction-forbidden",
            "XML processing instructions are forbidden",
        )


def hidden_by_presentation(element: ET.Element) -> bool:
    properties: dict[str, str] = {}
    style = element.attrib.get("style", "")
    for declaration in style.split(";"):
        if ":" not in declaration:
            continue
        key, value = declaration.split(":", 1)
        properties[key.strip().lower()] = re.sub(
            r"\s*!important\s*$", "", value.strip().lower()
        )
    for key in ("display", "visibility", "opacity"):
        if key in element.attrib:
            properties[key] = element.attrib[key].strip().lower()
    opacity = properties.get("opacity")
    parsed_opacity = finite_number(opacity.rstrip("%")) if opacity else None
    opacity_is_hidden = parsed_opacity is not None and parsed_opacity <= 0
    return opacity_is_hidden or properties.get("display") == "none" or properties.get(
        "visibility"
    ) in {
        "hidden",
        "collapse",
    }


def paint_state(element: ET.Element, inherited: dict[str, str] | None = None) -> dict[str, str]:
    result = dict(
        inherited
        or {
            "color": "black",
            "fill": "black",
            "fill-opacity": "1",
            "font-size": "16",
            "stroke": "none",
            "stroke-opacity": "1",
            "stroke-width": "1",
        }
    )
    style = element.attrib.get("style", "")
    declarations: dict[str, str] = {}
    for declaration in style.split(";"):
        key, separator, value = declaration.partition(":")
        if separator:
            declarations[key.strip().lower()] = re.sub(
                r"\s*!important\s*$", "", value.strip(), flags=re.IGNORECASE
            )
    for key in (
        "color",
        "fill",
        "fill-opacity",
        "font-size",
        "stroke",
        "stroke-opacity",
        "stroke-width",
    ):
        value = declarations.get(key) or element.attrib.get(key)
        if (
            value
            and value.strip().lower() != "inherit"
            and not (key == "color" and value.strip().lower() == "currentcolor")
        ):
            result[key] = value.strip()
    return result


def presentation_value(element: ET.Element, key: str) -> str | None:
    style = element.attrib.get("style", "")
    declarations: dict[str, str] = {}
    for declaration in style.split(";"):
        property_name, separator, value = declaration.partition(":")
        if separator:
            declarations[property_name.strip().lower()] = re.sub(
                r"\s*!important\s*$", "", value.strip(), flags=re.IGNORECASE
            )
    return declarations.get(key) or element.attrib.get(key)


def computed_color_values(root: ET.Element) -> dict[int, str]:
    colors: dict[int, str] = {}
    stack: list[tuple[ET.Element, str]] = [(root, "black")]
    while stack:
        element, inherited = stack.pop()
        value = (presentation_value(element, "color") or "").strip()
        color = (
            inherited
            if not value or value.lower() in {"inherit", "currentcolor"}
            else value
        )
        colors[id(element)] = color
        stack.extend((child, color) for child in reversed(list(element)))
    return colors


def pattern_has_direct_visible_content(
    pattern: ET.Element,
    identifiers: dict[str, ET.Element],
    computed_colors: dict[int, str],
) -> bool:
    def direct_paint_is_visible(
        element: ET.Element, paint: dict[str, str]
    ) -> bool:
        inherited_color = paint.get("color", "black")
        fill = paint.get("fill", "black")
        stroke = paint.get("stroke", "none")
        if fill.strip().lower() == "currentcolor":
            fill = inherited_color
        if stroke.strip().lower() == "currentcolor":
            stroke = inherited_color
        fill_visible = (
            primitive_fill_has_area(element)
            and not fill.lower().startswith("url(")
            and not color_is_obviously_transparent(fill)
            and not opacity_is_zero(paint.get("fill-opacity"))
        )
        stroke_width = paint.get("stroke-width", "1").strip()
        width_match = re.fullmatch(rf"({NUMBER})(?:[a-zA-Z%]+)?", stroke_width)
        width = finite_number(width_match.group(1)) if width_match else None
        stroke_visible = (
            not stroke.lower().startswith("url(")
            and not color_is_obviously_transparent(stroke)
            and not opacity_is_zero(paint.get("stroke-opacity"))
            and width is not None
            and width > 0
        )
        return fill_visible or stroke_visible

    def subtree(
        element: ET.Element,
        inherited_hidden: bool,
        referenced_root: bool,
        inherited_paint: dict[str, str] | None,
        visiting: set[str],
        depth: int,
    ) -> bool:
        if depth > MAX_RENDER_DEPTH:
            return False
        hidden = inherited_hidden or hidden_by_presentation(element)
        if hidden:
            return False
        if transform_is_obviously_singular(element):
            return False
        if any(
            (presentation_value(element, effect) or "").strip().lower()
            not in {"", "none"}
            for effect in ("clip-path", "filter", "mask")
        ):
            return False
        paint = paint_state(element, inherited_paint)
        name = local_name(element.tag).lower()
        if name in NON_RENDERING_CONTAINERS and not (
            referenced_root and name == "symbol"
        ):
            return False
        if name in RENDERABLE_ELEMENTS - {"use"}:
            return primitive_has_geometry(element, paint) and direct_paint_is_visible(
                element, paint
            )
        if name == "use":
            href = href_value(element)
            literal = href.strip() if href else ""
            if not SAFE_FRAGMENT_RE.fullmatch(literal):
                return False
            fragment = literal[1:]
            if fragment in visiting or len(visiting) >= MAX_USE_DEPTH:
                return False
            target = identifiers.get(fragment)
            if target is None or not use_has_size(element, target):
                return False
            visiting.add(fragment)
            visible = subtree(target, False, True, paint, visiting, depth + 1)
            visiting.remove(fragment)
            return visible
        return any(
            subtree(child, hidden, False, paint, visiting, depth + 1)
            for child in element
        )

    base_paint = paint_state(pattern)
    base_paint["color"] = computed_colors.get(id(pattern), "black")
    return any(
        subtree(child, False, False, base_paint, set(), 0) for child in pattern
    )


def pattern_paint_is_obviously_transparent(
    target: ET.Element,
    identifiers: dict[str, ET.Element],
    computed_colors: dict[int, str],
) -> bool:
    width: str | None = None
    height: str | None = None
    visiting: set[str] = set()
    current = target
    for _ in range(MAX_GRADIENT_REFERENCE_DEPTH):
        identifier = current.attrib.get("id")
        if identifier:
            if identifier in visiting:
                return True
            visiting.add(identifier)
        width = width or current.attrib.get("width")
        height = height or current.attrib.get("height")
        if list(current):
            return (
                positive_length(width) is None
                or positive_length(height) is None
                or not pattern_has_direct_visible_content(
                    current, identifiers, computed_colors
                )
            )
        href = href_value(current)
        literal = href.strip() if href else ""
        if not SAFE_FRAGMENT_RE.fullmatch(literal):
            return True
        inherited = identifiers.get(literal[1:])
        if inherited is None or local_name(inherited.tag).lower() != "pattern":
            return True
        current = inherited
    return True


def paint_server_is_obviously_transparent(
    value: str,
    identifiers: dict[str, ET.Element],
    computed_colors: dict[int, str],
    transparency_cache: dict[str, bool],
) -> bool:
    match = URL_RE.fullmatch(value.strip())
    if not match:
        return False
    reference = match.group(2).strip()
    if not SAFE_FRAGMENT_RE.fullmatch(reference):
        return True

    fragment = reference[1:]
    root_fragment = fragment
    if root_fragment in transparency_cache:
        return transparency_cache[root_fragment]

    def finish(result: bool) -> bool:
        transparency_cache[root_fragment] = result
        return result

    visiting: set[str] = set()
    for _ in range(MAX_GRADIENT_REFERENCE_DEPTH):
        if fragment in visiting:
            return finish(False)
        visiting.add(fragment)
        target = identifiers.get(fragment)
        if target is None:
            return finish(True)
        target_name = local_name(target.tag).lower()
        if target_name == "pattern":
            return finish(
                pattern_paint_is_obviously_transparent(
                    target, identifiers, computed_colors
                )
            )
        if target_name not in {"lineargradient", "radialgradient"}:
            return finish(False)
        stops = [
            child for child in target if local_name(child.tag).lower() == "stop"
        ]
        if stops:
            def stop_is_transparent(stop: ET.Element) -> bool:
                color = presentation_value(stop, "stop-color") or "black"
                if color.strip().lower() == "currentcolor":
                    color = computed_colors.get(id(stop), "black")
                return (
                    color_is_obviously_transparent(color)
                    or opacity_is_zero(presentation_value(stop, "stop-opacity"))
                    or opacity_is_zero(presentation_value(stop, "opacity"))
                )

            return finish(all(stop_is_transparent(stop) for stop in stops))
        href = href_value(target)
        literal = href.strip() if href else ""
        if not SAFE_FRAGMENT_RE.fullmatch(literal):
            return finish(True)
        fragment = literal[1:]
    # An overlong chain is not obviously transparent. The validation pass emits
    # a structured depth finding, while renderability stays conservative.
    return finish(False)


def validate_gradient_reference_chains(
    identifiers: dict[str, ET.Element], findings: list[Finding]
) -> None:
    gradients = {
        identifier: element
        for identifier, element in identifiers.items()
        if local_name(element.tag).lower() in {"lineargradient", "radialgradient"}
    }
    edges: dict[str, str] = {}
    for identifier, element in gradients.items():
        href = href_value(element)
        literal = href.strip() if href else ""
        if SAFE_FRAGMENT_RE.fullmatch(literal) and literal[1:] in gradients:
            edges[identifier] = literal[1:]

    completed: set[str] = set()
    depths: dict[str, int] = {}
    cycle_found = False
    for start in gradients:
        if start in completed:
            continue
        path: list[str] = []
        positions: dict[str, int] = {}
        current = start
        while current not in completed and current in gradients:
            if current in positions:
                cycle_found = True
                break
            positions[current] = len(path)
            path.append(current)
            next_identifier = edges.get(current)
            if next_identifier is None:
                current = ""
                break
            current = next_identifier

        if current in positions:
            for identifier in path:
                completed.add(identifier)
            continue

        depth = depths.get(current, 0)
        for identifier in reversed(path):
            depth += 1
            depths[identifier] = depth
            completed.add(identifier)

    if cycle_found:
        add_finding(
            findings,
            "cyclic-gradient-reference",
            "gradient inheritance must not contain a reference cycle",
        )
    if any(depth > MAX_GRADIENT_REFERENCE_DEPTH for depth in depths.values()):
        add_finding(
            findings,
            "gradient-reference-depth",
            f"gradient inheritance exceeds {MAX_GRADIENT_REFERENCE_DEPTH} targets",
        )


def primitive_fill_has_area(element: ET.Element) -> bool:
    element_name = local_name(element.tag).lower()
    if element_name == "line":
        return False
    if element_name == "polyline":
        return points_have_geometry(element.attrib.get("points"), True)
    if element_name == "path":
        return path_has_fill_area(element.attrib.get("d"))
    return True


def paint_is_visible(
    element: ET.Element,
    paint: dict[str, str],
    identifiers: dict[str, ET.Element],
    computed_colors: dict[int, str],
    transparency_cache: dict[str, bool],
) -> bool:
    inherited_color = paint.get("color", "black")
    fill = paint.get("fill", "black")
    stroke = paint.get("stroke", "none")
    if fill.strip().lower() == "currentcolor":
        fill = inherited_color
    if stroke.strip().lower() == "currentcolor":
        stroke = inherited_color
    fill_visible = (
        primitive_fill_has_area(element)
        and not color_is_obviously_transparent(fill)
        and not paint_server_is_obviously_transparent(
            fill, identifiers, computed_colors, transparency_cache
        )
        and not opacity_is_zero(paint.get("fill-opacity"))
    )
    stroke_width = paint.get("stroke-width", "1").strip()
    width_match = re.fullmatch(rf"({NUMBER})(?:[a-zA-Z%]+)?", stroke_width)
    width = finite_number(width_match.group(1)) if width_match else None
    stroke_visible = (
        not color_is_obviously_transparent(stroke)
        and not opacity_is_zero(paint.get("stroke-opacity"))
        and width is not None
        and width > 0
    )
    return fill_visible or stroke_visible


def coordinate(value: str | None, default: float = 0.0) -> float | None:
    if value is None:
        return default
    match = LENGTH_RE.fullmatch(value.strip())
    return finite_number(match.group(1)) if match else None


def union_bounds(first: Bounds | None, second: Bounds | None) -> Bounds | None:
    if first is None:
        return second
    if second is None:
        return first
    return (
        min(first[0], second[0]),
        min(first[1], second[1]),
        max(first[2], second[2]),
        max(first[3], second[3]),
    )


def bounds_from_points(points: list[tuple[float, float]]) -> Bounds | None:
    if not points:
        return None
    return (
        min(point[0] for point in points),
        min(point[1] for point in points),
        max(point[0] for point in points),
        max(point[1] for point in points),
    )


def translate_bounds(bounds: Bounds, x: float, y: float) -> Bounds:
    return (bounds[0] + x, bounds[1] + y, bounds[2] + x, bounds[3] + y)


def expand_bounds(bounds: Bounds, margin: float) -> Bounds:
    return (
        bounds[0] - margin,
        bounds[1] - margin,
        bounds[2] + margin,
        bounds[3] + margin,
    )


def bounds_may_intersect_view_box(
    bounds: Bounds, view_box: tuple[float, float, float, float]
) -> bool:
    min_x, min_y, width, height = view_box
    max_x = min_x + width
    max_y = min_y + height
    tolerance = max(width, height, 1.0) * 1e-9
    return not (
        bounds[2] < min_x - tolerance
        or bounds[3] < min_y - tolerance
        or bounds[0] > max_x + tolerance
        or bounds[1] > max_y + tolerance
    )


def different(first: tuple[float, float], second: tuple[float, float]) -> bool:
    return first[0] != second[0] or first[1] != second[1]


def path_tokens(path_data: str) -> list[str] | None:
    tokens: list[str] = []
    offset = 0
    for match in PATH_TOKEN_RE.finditer(path_data):
        if path_data[offset : match.start()].strip(" ,\t\r\n"):
            return None
        tokens.append(match.group(0))
        offset = match.end()
    if path_data[offset:].strip(" ,\t\r\n"):
        return None
    return tokens


def points_enclose_area(points: list[tuple[float, float]]) -> bool:
    unique_points = list(dict.fromkeys(points))
    if len(unique_points) < 3:
        return False
    first = unique_points[0]
    second = unique_points[1]
    return any(
        (second[0] - first[0]) * (third[1] - first[1])
        != (second[1] - first[1]) * (third[0] - first[0])
        for third in unique_points[2:]
    )


def path_geometry_summary(path_data: str | None) -> tuple[bool, bool]:
    if not path_data:
        return (False, False)
    tokens = path_tokens(path_data)
    if not tokens:
        return (False, False)

    parameter_counts = {
        "M": 2,
        "L": 2,
        "H": 1,
        "V": 1,
        "C": 6,
        "S": 4,
        "Q": 4,
        "T": 2,
        "A": 7,
    }
    index = 0
    command: str | None = None
    current = (0.0, 0.0)
    subpath_start = current
    subpath_points: list[tuple[float, float]] = []
    has_geometry = False
    has_fill_area = False

    while index < len(tokens):
        token = tokens[index]
        if token.isalpha():
            command = token
            index += 1
            if command.upper() == "Z":
                has_geometry = has_geometry or different(current, subpath_start)
                has_fill_area = has_fill_area or points_enclose_area(subpath_points)
                current = subpath_start
                command = None
                continue
        if command is None:
            return (False, False)

        upper = command.upper()
        count = parameter_counts.get(upper)
        if count is None or index + count > len(tokens):
            return (False, False)
        parameter_tokens = tokens[index : index + count]
        if any(value.isalpha() for value in parameter_tokens):
            return (False, False)
        values = [finite_number(value) for value in parameter_tokens]
        if any(value is None for value in values):
            return (False, False)
        parameters = [float(value) for value in values]
        index += count
        relative = command.islower()
        origin = current

        def point(x: float, y: float) -> tuple[float, float]:
            return (origin[0] + x, origin[1] + y) if relative else (x, y)

        if upper == "M":
            has_fill_area = has_fill_area or points_enclose_area(subpath_points)
            current = point(parameters[0], parameters[1])
            subpath_start = current
            subpath_points = [current]
            command = "l" if relative else "L"
            continue
        if upper in {"L", "T"}:
            endpoint = point(parameters[0], parameters[1])
            has_geometry = has_geometry or different(current, endpoint)
            current = endpoint
            subpath_points.append(endpoint)
            continue
        if upper == "H":
            endpoint = (origin[0] + parameters[0] if relative else parameters[0], origin[1])
            has_geometry = has_geometry or different(current, endpoint)
            current = endpoint
            subpath_points.append(endpoint)
            continue
        if upper == "V":
            endpoint = (origin[0], origin[1] + parameters[0] if relative else parameters[0])
            has_geometry = has_geometry or different(current, endpoint)
            current = endpoint
            subpath_points.append(endpoint)
            continue
        if upper in {"C", "S", "Q"}:
            points = [
                point(parameters[offset], parameters[offset + 1])
                for offset in range(0, count, 2)
            ]
            has_geometry = has_geometry or any(
                different(current, candidate) for candidate in points
            )
            current = points[-1]
            subpath_points.extend(points)
            continue
        if upper == "A":
            endpoint = point(parameters[5], parameters[6])
            valid_flags = parameters[3] in {0.0, 1.0} and parameters[4] in {0.0, 1.0}
            if not valid_flags or parameters[0] < 0 or parameters[1] < 0:
                return (False, False)
            has_geometry = has_geometry or different(current, endpoint)
            if parameters[0] > 0 and parameters[1] > 0 and different(current, endpoint):
                has_fill_area = True
            current = endpoint
            subpath_points.append(endpoint)
            continue
        return (False, False)

    return (has_geometry, has_fill_area or points_enclose_area(subpath_points))


def path_has_geometry(path_data: str | None) -> bool:
    return path_geometry_summary(path_data)[0]


def path_has_fill_area(path_data: str | None) -> bool:
    return path_geometry_summary(path_data)[1]


def path_conservative_bounds(path_data: str | None) -> Bounds | None:
    if not path_data:
        return None
    tokens = path_tokens(path_data)
    if not tokens:
        return None

    parameter_counts = {
        "M": 2,
        "L": 2,
        "H": 1,
        "V": 1,
        "C": 6,
        "S": 4,
        "Q": 4,
        "T": 2,
        "A": 7,
    }
    index = 0
    command: str | None = None
    previous_command: str | None = None
    current = (0.0, 0.0)
    subpath_start = current
    last_cubic_control: tuple[float, float] | None = None
    last_quadratic_control: tuple[float, float] | None = None
    bounds: Bounds | None = None

    def include_point(point: tuple[float, float]) -> None:
        nonlocal bounds
        point_bounds = (point[0], point[1], point[0], point[1])
        bounds = union_bounds(bounds, point_bounds)

    def include_box(box: Bounds) -> None:
        nonlocal bounds
        bounds = union_bounds(bounds, box)

    while index < len(tokens):
        token = tokens[index]
        if token.isalpha():
            command = token
            index += 1
            if command.upper() == "Z":
                include_point(current)
                include_point(subpath_start)
                current = subpath_start
                previous_command = "Z"
                last_cubic_control = None
                last_quadratic_control = None
                command = None
                continue
        if command is None:
            return None

        upper = command.upper()
        count = parameter_counts.get(upper)
        if count is None or index + count > len(tokens):
            return None
        parameter_tokens = tokens[index : index + count]
        if any(value.isalpha() for value in parameter_tokens):
            return None
        parsed_values = [finite_number(value) for value in parameter_tokens]
        if any(value is None for value in parsed_values):
            return None
        parameters = [float(value) for value in parsed_values]
        index += count
        relative = command.islower()
        origin = current

        def point(x: float, y: float) -> tuple[float, float]:
            return (origin[0] + x, origin[1] + y) if relative else (x, y)

        if upper == "M":
            current = point(parameters[0], parameters[1])
            subpath_start = current
            include_point(current)
            command = "l" if relative else "L"
        elif upper in {"L", "T"}:
            endpoint = point(parameters[0], parameters[1])
            include_point(current)
            if upper == "T":
                control = current
                if previous_command in {"Q", "T"} and last_quadratic_control:
                    control = (
                        2 * current[0] - last_quadratic_control[0],
                        2 * current[1] - last_quadratic_control[1],
                    )
                include_point(control)
                last_quadratic_control = control
            include_point(endpoint)
            current = endpoint
        elif upper == "H":
            endpoint = (
                origin[0] + parameters[0] if relative else parameters[0],
                origin[1],
            )
            include_point(current)
            include_point(endpoint)
            current = endpoint
        elif upper == "V":
            endpoint = (
                origin[0],
                origin[1] + parameters[0] if relative else parameters[0],
            )
            include_point(current)
            include_point(endpoint)
            current = endpoint
        elif upper in {"C", "S", "Q"}:
            points = [
                point(parameters[offset], parameters[offset + 1])
                for offset in range(0, count, 2)
            ]
            include_point(current)
            if upper == "S":
                control = current
                if previous_command in {"C", "S"} and last_cubic_control:
                    control = (
                        2 * current[0] - last_cubic_control[0],
                        2 * current[1] - last_cubic_control[1],
                    )
                include_point(control)
                last_cubic_control = points[0]
            elif upper == "C":
                last_cubic_control = points[-2]
            else:
                last_quadratic_control = points[0]
            for candidate in points:
                include_point(candidate)
            current = points[-1]
        elif upper == "A":
            endpoint = point(parameters[5], parameters[6])
            if (
                parameters[3] not in {0.0, 1.0}
                or parameters[4] not in {0.0, 1.0}
                or parameters[0] < 0
                or parameters[1] < 0
            ):
                return None
            radius = 2 * math.hypot(parameters[0], parameters[1])
            include_box(expand_bounds(bounds_from_points([current, endpoint]), radius))
            current = endpoint
        else:
            return None

        if upper not in {"C", "S"}:
            last_cubic_control = None
        if upper not in {"Q", "T"}:
            last_quadratic_control = None
        previous_command = upper

    return bounds


def points_have_geometry(value: str | None, polygon: bool) -> bool:
    if not value:
        return False
    tokens = [part for part in re.split(r"[\s,]+", value.strip()) if part]
    if len(tokens) < (6 if polygon else 4) or len(tokens) % 2:
        return False
    numbers = [finite_number(token) for token in tokens]
    if any(number is None for number in numbers):
        return False
    points = [
        (float(numbers[index]), float(numbers[index + 1]))
        for index in range(0, len(numbers), 2)
    ]
    unique_points = list(dict.fromkeys(points))
    if not polygon:
        return len(unique_points) >= 2
    return points_enclose_area(unique_points)


def text_has_content(
    element: ET.Element, inherited_paint: dict[str, str] | None = None
) -> bool:
    if not "".join(element.itertext()).strip():
        return False
    raw_size = element.attrib.get("font-size")
    style = element.attrib.get("style", "")
    for declaration in style.split(";"):
        if ":" in declaration:
            key, value = declaration.split(":", 1)
            if key.strip().lower() == "font-size":
                raw_size = re.sub(r"\s*!important\s*$", "", value.strip(), flags=re.IGNORECASE)
    if raw_size is None and inherited_paint is not None:
        raw_size = inherited_paint.get("font-size")
    if raw_size is None:
        return True
    numeric_size = re.fullmatch(rf"({NUMBER})(?:[a-zA-Z%]+)?", raw_size)
    if numeric_size:
        parsed_numeric_size = finite_number(numeric_size.group(1))
        return parsed_numeric_size is not None and parsed_numeric_size > 0
    parsed_size = coordinate(raw_size)
    return parsed_size is not None and parsed_size > 0


def primitive_has_geometry(
    element: ET.Element, inherited_paint: dict[str, str] | None = None
) -> bool:
    name = local_name(element.tag).lower()
    if name == "path":
        return path_has_geometry(element.attrib.get("d"))
    if name == "rect":
        return positive_length(element.attrib.get("width")) is not None and positive_length(
            element.attrib.get("height")
        ) is not None
    if name == "circle":
        return positive_length(element.attrib.get("r")) is not None
    if name == "ellipse":
        return positive_length(element.attrib.get("rx")) is not None and positive_length(
            element.attrib.get("ry")
        ) is not None
    if name == "line":
        start = (coordinate(element.attrib.get("x1")), coordinate(element.attrib.get("y1")))
        end = (coordinate(element.attrib.get("x2")), coordinate(element.attrib.get("y2")))
        return None not in (*start, *end) and different(start, end)
    if name in {"polyline", "polygon"}:
        return points_have_geometry(element.attrib.get("points"), name == "polygon")
    if name == "text":
        return text_has_content(element, inherited_paint)
    return False


def points_conservative_bounds(value: str | None) -> Bounds | None:
    if not value:
        return None
    tokens = [part for part in re.split(r"[\s,]+", value.strip()) if part]
    if len(tokens) < 2 or len(tokens) % 2:
        return None
    numbers = [finite_number(token) for token in tokens]
    if any(number is None for number in numbers):
        return None
    return bounds_from_points(
        [
            (float(numbers[index]), float(numbers[index + 1]))
            for index in range(0, len(numbers), 2)
        ]
    )


def stroke_conservative_margin(
    element: ET.Element,
    paint: dict[str, str],
    identifiers: dict[str, ET.Element],
    computed_colors: dict[int, str],
    transparency_cache: dict[str, bool],
) -> float | None:
    stroke = paint.get("stroke", "none")
    if (
        color_is_obviously_transparent(stroke)
        or paint_server_is_obviously_transparent(
            stroke, identifiers, computed_colors, transparency_cache
        )
        or opacity_is_zero(paint.get("stroke-opacity"))
    ):
        return 0.0
    raw_width = paint.get("stroke-width", "1").strip()
    width_match = LENGTH_RE.fullmatch(raw_width)
    width = finite_number(width_match.group(1)) if width_match else None
    if width is None:
        return None
    if width <= 0:
        return 0.0
    vector_effect = presentation_value(element, "vector-effect")
    if vector_effect and vector_effect.strip().lower() != "none":
        return None
    raw_miter_limit = presentation_value(element, "stroke-miterlimit") or "4"
    miter_limit = finite_number(raw_miter_limit.strip())
    if miter_limit is None or miter_limit < 1:
        return None
    return max(width / 2, (width / 2) * miter_limit)


def primitive_conservative_bounds(
    element: ET.Element,
    paint: dict[str, str],
    identifiers: dict[str, ET.Element],
    computed_colors: dict[int, str],
    transparency_cache: dict[str, bool],
) -> Bounds | None:
    name = local_name(element.tag).lower()
    bounds: Bounds | None
    if name == "path":
        bounds = path_conservative_bounds(element.attrib.get("d"))
    elif name == "rect":
        x = coordinate(element.attrib.get("x"))
        y = coordinate(element.attrib.get("y"))
        width = positive_length(element.attrib.get("width"))
        height = positive_length(element.attrib.get("height"))
        if None in (x, y, width, height):
            return None
        bounds = (x, y, x + width, y + height)
    elif name == "circle":
        cx = coordinate(element.attrib.get("cx"))
        cy = coordinate(element.attrib.get("cy"))
        radius = positive_length(element.attrib.get("r"))
        if None in (cx, cy, radius):
            return None
        bounds = (cx - radius, cy - radius, cx + radius, cy + radius)
    elif name == "ellipse":
        cx = coordinate(element.attrib.get("cx"))
        cy = coordinate(element.attrib.get("cy"))
        radius_x = positive_length(element.attrib.get("rx"))
        radius_y = positive_length(element.attrib.get("ry"))
        if None in (cx, cy, radius_x, radius_y):
            return None
        bounds = (cx - radius_x, cy - radius_y, cx + radius_x, cy + radius_y)
    elif name == "line":
        x1 = coordinate(element.attrib.get("x1"))
        y1 = coordinate(element.attrib.get("y1"))
        x2 = coordinate(element.attrib.get("x2"))
        y2 = coordinate(element.attrib.get("y2"))
        if None in (x1, y1, x2, y2):
            return None
        bounds = bounds_from_points([(x1, y1), (x2, y2)])
    elif name in {"polygon", "polyline"}:
        bounds = points_conservative_bounds(element.attrib.get("points"))
    else:
        # Text shaping, font metrics, and future primitives are deliberately
        # treated as unknown so legitimate artwork is not rejected.
        return None

    if bounds is None:
        return None
    margin = stroke_conservative_margin(
        element, paint, identifiers, computed_colors, transparency_cache
    )
    return None if margin is None else expand_bounds(bounds, margin)


def element_makes_bounds_unknown(element: ET.Element) -> bool:
    for property_name in (
        "filter",
        "marker-start",
        "marker-mid",
        "marker-end",
        "transform",
    ):
        value = presentation_value(element, property_name)
        if value and value.strip().lower() != "none":
            return True
    return False


def transform_is_obviously_singular(element: ET.Element) -> bool:
    value = presentation_value(element, "transform")
    if not value or value.strip().lower() == "none":
        return False
    for match in re.finditer(
        r"\b(matrix|scale|scalex|scaley)\s*\(([^\)]*)\)",
        value,
        flags=re.IGNORECASE,
    ):
        name = match.group(1).lower()
        parts = [
            finite_number(part)
            for part in re.split(r"[\s,]+", match.group(2).strip())
            if part
        ]
        if any(part is None for part in parts):
            continue
        numbers = [float(part) for part in parts]
        if name == "scale" and len(numbers) in {1, 2}:
            if numbers[0] == 0 or (len(numbers) == 2 and numbers[1] == 0):
                return True
        elif name in {"scalex", "scaley"} and len(numbers) == 1 and numbers[0] == 0:
            return True
        elif name == "matrix" and len(numbers) == 6:
            if numbers[0] * numbers[3] - numbers[1] * numbers[2] == 0:
                return True
    return False


def href_value(element: ET.Element) -> str | None:
    for raw_name, value in element.attrib.items():
        if local_name(raw_name).lower() == "href":
            return value
    return None


def validate_fragment_target(
    fragment: str,
    attribute_name: str,
    identifiers: dict[str, ET.Element],
    findings: list[Finding],
) -> None:
    target = identifiers.get(fragment)
    if target is None:
        add_finding(
            findings,
            "unresolved-fragment-reference",
            "a local fragment reference does not resolve to an SVG id",
        )
        return
    expected = REFERENCE_TARGET_TYPES.get(attribute_name)
    target_name = local_name(target.tag).lower()
    if expected is not None and target_name not in expected:
        add_finding(
            findings,
            "invalid-fragment-target",
            "a local fragment reference resolves to an incompatible SVG element type",
        )


def validate_local_fragment_references(
    elements: list[ET.Element],
    identifiers: dict[str, ET.Element],
    findings: list[Finding],
) -> None:
    for element in elements:
        element_name = local_name(element.tag).lower()
        for raw_name, raw_value in element.attrib.items():
            attribute_name = local_name(raw_name).lower()
            reference_role = attribute_name
            if attribute_name == "href":
                if element_name in {"lineargradient", "radialgradient"}:
                    reference_role = "gradient-href"
                elif element_name == "pattern":
                    reference_role = "pattern-href"
                elif element_name == "textpath":
                    reference_role = "textpath-href"
            literal = raw_value.strip()
            if attribute_name in REFERENCE_ATTRIBUTES and SAFE_FRAGMENT_RE.fullmatch(literal):
                if element_name != "use":
                    validate_fragment_target(
                        literal[1:], reference_role, identifiers, findings
                    )
            for match in URL_RE.finditer(raw_value):
                reference = match.group(2).strip()
                if SAFE_FRAGMENT_RE.fullmatch(reference):
                    validate_fragment_target(
                        reference[1:], attribute_name, identifiers, findings
                    )

            if attribute_name == "style":
                for declaration in raw_value.split(";"):
                    property_name, separator, property_value = declaration.partition(":")
                    if not separator:
                        continue
                    for match in URL_RE.finditer(property_value):
                        reference = match.group(2).strip()
                        if SAFE_FRAGMENT_RE.fullmatch(reference):
                            validate_fragment_target(
                                reference[1:],
                                property_name.strip().lower(),
                                identifiers,
                                findings,
                            )

        if element_name == "style":
            for match in URL_RE.finditer("".join(element.itertext())):
                reference = match.group(2).strip()
                if SAFE_FRAGMENT_RE.fullmatch(reference):
                    validate_fragment_target(
                        reference[1:], "style", identifiers, findings
                    )


def use_has_size(element: ET.Element, target: ET.Element) -> bool:
    if local_name(target.tag).lower() not in {"svg", "symbol"}:
        return True
    for name in ("width", "height"):
        if name in element.attrib and positive_length(element.attrib[name]) is None:
            return False
    return True


def opacity_fraction(value: str | None) -> float | None:
    if value is None:
        return 1.0
    stripped = value.strip()
    parsed = finite_number(stripped.rstrip("%"))
    if parsed is None:
        return None
    fraction = parsed / 100 if stripped.endswith("%") else parsed
    return max(0.0, min(1.0, fraction))


def validate_explicit_presentation_values(
    element: ET.Element, findings: list[Finding]
) -> None:
    values = {
        local_name(raw_name).lower(): raw_value.strip()
        for raw_name, raw_value in element.attrib.items()
    }
    for declaration in element.attrib.get("style", "").split(";"):
        key, separator, value = declaration.partition(":")
        if separator:
            values[key.strip().lower()] = re.sub(
                r"\s*!important\s*$", "", value.strip(), flags=re.IGNORECASE
            )

    for key in (
        "fill-opacity",
        "flood-opacity",
        "opacity",
        "stop-opacity",
        "stroke-opacity",
    ):
        value = values.get(key)
        if value is None or value.lower() == "inherit":
            continue
        if not re.fullmatch(rf"{NUMBER}%?", value):
            add_finding(
                findings,
                "unsupported-opacity-value",
                "opacity values must be explicit finite numbers or percentages",
            )

    for key in ("font-size", "stroke-width"):
        value = values.get(key)
        if value is None or value.lower() == "inherit":
            continue
        if not re.fullmatch(rf"{NUMBER}(?:[a-zA-Z%]+)?", value):
            add_finding(
                findings,
                "unsupported-length-value",
                "font and stroke lengths must use explicit finite values",
            )

    for key in ("fill", "flood-color", "lighting-color", "stop-color", "stroke"):
        value = values.get(key, "").strip().lower()
        function = re.match(r"([a-z][a-z0-9-]*)\s*\(", value)
        if function and function.group(1) not in {
            "hsl",
            "hsla",
            "rgb",
            "rgba",
            "url",
        }:
            add_finding(
                findings,
                "unsupported-color-function",
                "paint values use an unsupported CSS color function",
            )


def opaque_full_canvas_rect(
    element: ET.Element,
    view_box: tuple[float, float, float, float],
    paint: dict[str, str],
    effective_opacity: float | None,
    identifiers: dict[str, ET.Element],
    computed_colors: dict[int, str],
    transparency_cache: dict[str, bool],
) -> bool:
    if (
        local_name(element.tag).lower() != "rect"
        or not rect_covers_canvas(element, view_box)
    ):
        return False
    if effective_opacity is not None and effective_opacity <= 0:
        return False
    fill = paint.get("fill", "black")
    if fill.strip().lower() == "currentcolor":
        fill = paint.get("color", "black")
    return (
        not color_is_obviously_transparent(fill)
        and not paint_server_is_obviously_transparent(
            fill, identifiers, computed_colors, transparency_cache
        )
        and not opacity_is_zero(paint.get("fill-opacity"))
    )


def has_opaque_full_canvas_background(
    root: ET.Element,
    identifiers: dict[str, ET.Element],
    view_box: tuple[float, float, float, float],
    computed_colors: dict[int, str],
    transparency_cache: dict[str, bool],
) -> bool:
    reference_cache: dict[
        tuple[str, tuple[tuple[str, str], ...], float | None], bool
    ] = {}

    def subtree(
        element: ET.Element,
        inherited_hidden: bool,
        referenced_root: bool,
        inherited_paint: dict[str, str] | None,
        inherited_opacity: float | None,
        visiting: set[str],
        depth: int,
    ) -> bool:
        if depth > MAX_RENDER_DEPTH:
            return False
        hidden = inherited_hidden or hidden_by_presentation(element)
        if hidden:
            return False
        own_opacity = opacity_fraction(presentation_value(element, "opacity"))
        effective_opacity = (
            None
            if inherited_opacity is None or own_opacity is None
            else inherited_opacity * own_opacity
        )
        paint = paint_state(element, inherited_paint)
        name = local_name(element.tag).lower()
        if name in NON_RENDERING_CONTAINERS and not (
            referenced_root and name == "symbol"
        ):
            return False
        if opaque_full_canvas_rect(
            element,
            view_box,
            paint,
            effective_opacity,
            identifiers,
            computed_colors,
            transparency_cache,
        ):
            return True
        if name == "use":
            href = href_value(element)
            literal = href.strip() if href else ""
            if (
                not SAFE_FRAGMENT_RE.fullmatch(literal)
            ):
                return False
            fragment = literal[1:]
            if fragment in visiting or len(visiting) >= MAX_USE_DEPTH:
                return False
            cache_key = (
                fragment,
                tuple(sorted(paint.items())),
                effective_opacity,
            )
            if cache_key in reference_cache:
                return reference_cache[cache_key]
            target = identifiers.get(fragment)
            if (
                target is None
                or not use_has_size(element, target)
                or local_name(target.tag).lower() in {"svg", "symbol"}
            ):
                return False
            visiting.add(fragment)
            covers = subtree(
                target,
                False,
                True,
                paint,
                effective_opacity,
                visiting,
                depth + 1,
            )
            visiting.remove(fragment)
            reference_cache[cache_key] = covers
            return covers
        return any(
            subtree(
                child,
                hidden,
                False,
                paint,
                effective_opacity,
                visiting,
                depth + 1,
            )
            for child in element
        )

    return subtree(root, False, False, None, 1.0, set(), 0)


def has_renderable_graphic(
    root: ET.Element,
    elements: list[ET.Element],
    identifiers: dict[str, ET.Element],
    findings: list[Finding],
    view_box: tuple[float, float, float, float] | None,
    computed_colors: dict[int, str],
    transparency_cache: dict[str, bool],
) -> bool:
    reference_cache: dict[
        tuple[str, tuple[tuple[str, str], ...]], RenderSummary
    ] = {}
    unresolved_reported: set[int] = set()
    empty_reported: set[int] = set()
    cycles_reported: set[str] = set()
    depth_reported: set[str] = set()

    def combine(summaries: list[RenderSummary]) -> RenderSummary:
        usable = False
        unknown_bounds = False
        bounds: Bounds | None = None
        for child_usable, child_bounds in summaries:
            if not child_usable:
                continue
            usable = True
            if child_bounds is None:
                unknown_bounds = True
            elif not unknown_bounds:
                bounds = union_bounds(bounds, child_bounds)
        return (usable, None if unknown_bounds else bounds)

    def apply_coordinate_context(
        element: ET.Element,
        summary: RenderSummary,
        referenced_root: bool,
    ) -> RenderSummary:
        usable, bounds = summary
        if not usable:
            return summary
        name = local_name(element.tag).lower()
        if (
            element_makes_bounds_unknown(element)
            or (name == "svg" and element is not root)
            or (referenced_root and name == "symbol")
        ):
            return (True, None)
        return summary

    def subtree(
        element: ET.Element,
        inherited_hidden: bool,
        referenced_root: bool,
        visiting: set[str],
        resolution_depth: int,
        inherited_paint: dict[str, str] | None,
    ) -> RenderSummary:
        if resolution_depth > MAX_RENDER_DEPTH:
            if "depth" not in depth_reported:
                add_finding(
                    findings,
                    "render-resolution-depth",
                    f"renderability resolution exceeds depth {MAX_RENDER_DEPTH}",
                )
                depth_reported.add("depth")
            return (False, None)
        hidden = inherited_hidden or hidden_by_presentation(element)
        if hidden:
            return (False, None)
        if transform_is_obviously_singular(element):
            return (False, None)
        if any(
            (presentation_value(element, effect) or "").strip().lower()
            not in {"", "none"}
            for effect in ("clip-path", "filter", "mask")
        ):
            return (False, None)
        paint = paint_state(element, inherited_paint)
        name = local_name(element.tag).lower()
        if name in NON_RENDERING_CONTAINERS and not (referenced_root and name == "symbol"):
            return (False, None)
        if name in RENDERABLE_ELEMENTS - {"use"}:
            usable = primitive_has_geometry(element, paint) and paint_is_visible(
                element,
                paint,
                identifiers,
                computed_colors,
                transparency_cache,
            )
            summary = (
                usable,
                primitive_conservative_bounds(
                    element,
                    paint,
                    identifiers,
                    computed_colors,
                    transparency_cache,
                )
                if usable
                else None,
            )
        elif name == "use":
            summary = resolve_use(element, visiting, resolution_depth + 1, paint)
        else:
            summary = combine(
                [
                    subtree(
                        child,
                        hidden,
                        False,
                        visiting,
                        resolution_depth + 1,
                        paint,
                    )
                    for child in element
                ]
            )
        return apply_coordinate_context(element, summary, referenced_root)

    def resolve_fragment(
        fragment: str,
        visiting: set[str],
        resolution_depth: int,
        inherited_paint: dict[str, str],
    ) -> RenderSummary:
        cache_key = (fragment, tuple(sorted(inherited_paint.items())))
        if cache_key in reference_cache:
            return reference_cache[cache_key]
        if fragment in visiting:
            if fragment not in cycles_reported:
                add_finding(
                    findings,
                    "cyclic-use-reference",
                    "<use> references must not contain a cycle",
                )
                cycles_reported.add(fragment)
            return (False, None)
        if len(visiting) >= MAX_USE_DEPTH:
            add_finding(
                findings,
                "use-reference-depth",
                f"<use> reference chain exceeds {MAX_USE_DEPTH} targets",
            )
            return (False, None)
        target = identifiers.get(fragment)
        if target is None:
            return (False, None)
        visiting.add(fragment)
        summary = subtree(
            target, False, True, visiting, resolution_depth + 1, inherited_paint
        )
        visiting.remove(fragment)
        reference_cache[cache_key] = summary
        return summary

    def resolve_use(
        element: ET.Element,
        visiting: set[str],
        resolution_depth: int,
        inherited_paint: dict[str, str],
    ) -> RenderSummary:
        href = href_value(element)
        literal = href.strip() if href else ""
        if not SAFE_FRAGMENT_RE.fullmatch(literal):
            if not href and id(element) not in unresolved_reported:
                add_finding(findings, "unresolved-use", "<use> must reference an existing local ID")
                unresolved_reported.add(id(element))
            return (False, None)
        fragment = literal[1:]
        if fragment not in identifiers:
            if id(element) not in unresolved_reported:
                add_finding(
                    findings,
                    "unresolved-use",
                    "<use> target does not exist",
                )
                unresolved_reported.add(id(element))
            return (False, None)
        target = identifiers[fragment]
        if not use_has_size(element, target):
            if id(element) not in empty_reported:
                add_finding(
                    findings,
                    "zero-use-size",
                    "<use> target has an explicitly non-positive viewport",
                )
                empty_reported.add(id(element))
            return (False, None)
        summary = resolve_fragment(
            fragment, visiting, resolution_depth + 1, inherited_paint
        )
        usable, bounds = summary
        if not usable and fragment not in visiting and id(element) not in empty_reported:
            add_finding(
                findings,
                "empty-use-target",
                "<use> target contains no usable vector geometry",
            )
            empty_reported.add(id(element))
        if not usable or bounds is None:
            return summary
        x = coordinate(element.attrib.get("x"))
        y = coordinate(element.attrib.get("y"))
        if x is None or y is None:
            return (True, None)
        return (True, translate_bounds(bounds, x, y))

    for element in elements:
        if local_name(element.tag).lower() == "use":
            resolve_use(element, set(), 0, paint_state(element))
    usable, bounds = subtree(root, False, False, set(), 0, None)
    return usable and (
        view_box is None
        or bounds is None
        or bounds_may_intersect_view_box(bounds, view_box)
    )


def validate_xml(data: bytes) -> tuple[dict[str, Any], list[Finding]]:
    findings: list[Finding] = []
    metadata: dict[str, Any] = {"width": None, "height": None, "viewBox": None}
    text = decode_svg(data, findings)
    if text is None:
        return metadata, findings

    lowered = text.lower()
    if "<!doctype" in lowered:
        add_finding(findings, "doctype-forbidden", "DOCTYPE declarations are forbidden")
    if "<!entity" in lowered:
        add_finding(findings, "entity-forbidden", "entity declarations are forbidden")
    if "<!--" in text:
        add_finding(
            findings,
            "xml-comment-forbidden",
            "XML comments are forbidden in the canonical SVG master",
        )
    declaration_findings(text, findings)
    if findings:
        return metadata, findings

    try:
        root = ET.fromstring(text)
    except (ET.ParseError, ValueError):
        add_finding(findings, "malformed-xml", "SVG input is malformed XML")
        return metadata, findings

    if (
        local_name(root.tag).lower() != "svg"
        or namespace_name(root.tag) not in {None, SVG_NAMESPACE}
    ):
        add_finding(findings, "root-not-svg", "root element must be <svg>")
        return metadata, findings
    root_namespace = namespace_name(root.tag)

    width = positive_length(root.attrib.get("width"))
    height = positive_length(root.attrib.get("height"))
    view_box = parse_view_box(root.attrib.get("viewBox") or root.attrib.get("viewbox"))
    metadata.update(
        width=width,
        height=height,
        viewBox=list(view_box) if view_box is not None else None,
    )
    if width is None or height is None:
        add_finding(
            findings,
            "invalid-dimensions",
            "root width and height must be positive finite numbers in user units or px",
        )
    elif width > MAX_DIMENSION or height > MAX_DIMENSION or width * height > MAX_CANVAS_AREA:
        add_finding(findings, "canvas-too-large", "root dimensions exceed validator limits")
    if view_box is None:
        add_finding(
            findings,
            "invalid-viewbox",
            "root viewBox must contain four finite numbers with positive width and height",
        )
    elif (
        view_box[2] > MAX_DIMENSION
        or view_box[3] > MAX_DIMENSION
        or view_box[2] * view_box[3] > MAX_CANVAS_AREA
    ):
        add_finding(findings, "viewbox-too-large", "viewBox dimensions exceed validator limits")

    count = 0
    elements: list[ET.Element] = []
    identifiers: dict[str, ET.Element] = {}
    stack: list[tuple[ET.Element, int, bool, bool]] = [(root, 1, False, False)]
    while stack:
        element, depth, inside_non_rendering_container, hidden_by_ancestor = stack.pop()
        count += 1
        if count > MAX_ELEMENTS:
            add_finding(findings, "too-many-elements", f"SVG exceeds {MAX_ELEMENTS} elements")
            break
        if depth > MAX_DEPTH:
            add_finding(findings, "nesting-too-deep", f"SVG exceeds nesting depth {MAX_DEPTH}")
            break

        element_name = local_name(element.tag).lower()
        element_namespace = namespace_name(element.tag)
        hidden = hidden_by_ancestor or hidden_by_presentation(element)
        non_rendering = (
            inside_non_rendering_container or element_name in NON_RENDERING_CONTAINERS
        )
        elements.append(element)
        if element_name not in ALLOWED_STATIC_ELEMENTS:
            add_finding(
                findings,
                "unsupported-svg-element",
                "SVG contains an element outside the static logo allowlist",
            )
        if element_namespace != root_namespace:
            add_finding(
                findings,
                "foreign-namespace-element",
                "an element does not match the root SVG namespace",
            )
        identifier = next(
            (
                value
                for raw_name, value in element.attrib.items()
                if local_name(raw_name).lower() == "id"
            ),
            None,
        )
        if identifier:
            if identifier in identifiers:
                add_finding(findings, "duplicate-id", "SVG IDs must be unique")
            else:
                identifiers[identifier] = element
        if element_name in DANGEROUS_ELEMENTS:
            add_finding(
                findings,
                "active-content",
                "active-content elements are forbidden",
            )
        if element_name in SMIL_ELEMENTS:
            add_finding(
                findings,
                "declarative-animation",
                "declarative animation elements are forbidden",
            )
        if element_name == "image":
            add_finding(
                findings,
                "embedded-raster-image",
                "<image> elements are forbidden; use self-contained vector primitives",
            )
        if element_name == "metadata":
            add_finding(
                findings,
                "hidden-metadata",
                "<metadata> content is forbidden in the canonical SVG master",
            )
        if element_name == "style":
            add_finding(
                findings,
                "stylesheet-forbidden",
                "<style> blocks are forbidden; use explicit SVG presentation attributes",
            )

        for raw_name, raw_value in element.attrib.items():
            attribute_name = local_name(raw_name).lower()
            attribute_namespace = namespace_name(raw_name)
            if not static_attribute_is_allowed(element_name, attribute_name):
                add_finding(
                    findings,
                    "unsupported-svg-attribute",
                    "SVG contains an attribute outside the static logo allowlist",
                )
            if attribute_namespace not in {None, XML_NAMESPACE, XLINK_NAMESPACE}:
                add_finding(
                    findings,
                    "foreign-namespace-attribute",
                    "an attribute uses a non-SVG namespace",
                )
            if attribute_name.startswith("on"):
                add_finding(
                    findings,
                    "event-handler",
                    "event-handler attributes are forbidden",
                )
            if attribute_name in {"base", "xml:base"}:
                add_finding(findings, "base-uri", "base URI attributes are forbidden")
            if (
                attribute_name == "style" or attribute_name in CSS_PRESENTATION_ATTRIBUTES
            ) and CSS_ESCAPE_RE.search(raw_value):
                add_finding(
                    findings,
                    "css-escape-forbidden",
                    "CSS escape syntax is forbidden in presentation attributes",
                )
            if (
                attribute_name == "style" or attribute_name in CSS_PRESENTATION_ATTRIBUTES
            ) and CSS_COMMENT_RE.search(raw_value):
                add_finding(
                    findings,
                    "css-comment-forbidden",
                    "CSS comments are forbidden in presentation attributes",
                )
            if (
                attribute_name == "style" or attribute_name in CSS_PRESENTATION_ATTRIBUTES
            ) and CSS_VARIABLE_RE.search(raw_value):
                add_finding(
                    findings,
                    "css-variable-forbidden",
                    "CSS variables are forbidden in presentation attributes",
                )
            if (
                attribute_name == "style" and CSS_LIVE_MOTION_RE.search(raw_value)
            ) or attribute_name.startswith(("animation", "transition")):
                add_finding(
                    findings,
                    "css-animation-forbidden",
                    "CSS animation and transition syntax is forbidden in presentation attributes",
                )
            if attribute_name in REFERENCE_ATTRIBUTES:
                error = reference_error(raw_value)
                if error:
                    add_finding(findings, *error)
            for match in URL_RE.finditer(raw_value):
                error = reference_error(match.group(2))
                if error:
                    add_finding(findings, *error)
            if "@import" in raw_value.lower():
                add_finding(findings, "css-import", "CSS @import is forbidden")

        validate_explicit_presentation_values(element, findings)

        if element_name == "style":
            style_text = "".join(element.itertext())
            if CSS_ESCAPE_RE.search(style_text):
                add_finding(
                    findings,
                    "css-escape-forbidden",
                    "CSS escape syntax is forbidden in <style> content",
                )
            if CSS_COMMENT_RE.search(style_text):
                add_finding(
                    findings,
                    "css-comment-forbidden",
                    "CSS comments are forbidden in <style> content",
                )
            if CSS_LIVE_MOTION_RE.search(style_text):
                add_finding(
                    findings,
                    "css-animation-forbidden",
                    "CSS keyframes, animations, and transitions are forbidden in <style> content",
                )
            if "@import" in style_text.lower():
                add_finding(findings, "css-import", "CSS @import is forbidden")
            for match in URL_RE.finditer(style_text):
                error = reference_error(match.group(2))
                if error:
                    add_finding(findings, *error)

        children = list(element)
        stack.extend(
            (child, depth + 1, non_rendering, hidden) for child in reversed(children)
        )

    computed_colors = computed_color_values(root)
    transparency_cache: dict[str, bool] = {}
    validate_local_fragment_references(elements, identifiers, findings)
    validate_gradient_reference_chains(identifiers, findings)
    if view_box is not None and has_opaque_full_canvas_background(
        root, identifiers, view_box, computed_colors, transparency_cache
    ):
        add_finding(
            findings,
            "opaque-full-canvas-background",
            "an opaque rect visibly covers the full viewBox; "
            "logo backgrounds must be transparent",
        )
    if not has_renderable_graphic(
        root,
        elements,
        identifiers,
        findings,
        view_box,
        computed_colors,
        transparency_cache,
    ):
        add_finding(
            findings,
            "no-renderable-graphic",
            "SVG must contain a visible graphic whose conservative bounds may intersect the viewBox",
        )

    return metadata, findings


def result_payload(
    display_path: str,
    metadata: dict[str, Any],
    findings: list[Finding],
    *,
    category: str = "validation",
) -> dict[str, Any]:
    return {
        "valid": not findings,
        "path": display_path,
        "width": metadata.get("width"),
        "height": metadata.get("height"),
        "viewBox": metadata.get("viewBox"),
        "category": category,
        "errors": findings,
    }


def print_result(payload: dict[str, Any], json_output: bool) -> None:
    if json_output:
        print(json.dumps(payload, sort_keys=True, separators=(",", ":")))
        return
    status = "VALID" if payload["valid"] else "INVALID"
    print(f"{status} SVG: {payload['path']}")
    if payload["valid"]:
        print(
            "Canvas: "
            f"{payload['width']:g} x {payload['height']:g}; "
            "viewBox: "
            + " ".join(f"{number:g}" for number in payload["viewBox"])
        )
    else:
        for finding in payload["errors"]:
            print(f"- [{finding['code']}] {finding['message']}")


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Validate a self-contained SVG logo. Exit 0=valid, 1=invalid, 2=usage/I/O."
    )
    parser.add_argument("svg", help="path to a local SVG file")
    parser.add_argument("--json", action="store_true", help="emit one deterministic JSON object")
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(sys.argv[1:] if argv is None else argv)
    display_path = os.fspath(args.svg)
    metadata: dict[str, Any] = {"width": None, "height": None, "viewBox": None}
    if not is_local_input_path(display_path):
        findings = [Finding("nonlocal-input", "input must be a local filesystem path")]
        print_result(result_payload(display_path, metadata, findings, category="io"), args.json)
        return 2

    path = Path(display_path)
    try:
        stat = path.stat()
        if not path.is_file():
            raise OSError("path is not a regular file")
        if stat.st_size > MAX_FILE_BYTES:
            findings = [
                Finding("file-too-large", f"SVG exceeds the {MAX_FILE_BYTES}-byte file limit")
            ]
            print_result(result_payload(display_path, metadata, findings), args.json)
            return 1
        with path.open("rb") as handle:
            data = handle.read(MAX_FILE_BYTES + 1)
    except (OSError, ValueError):
        findings = [Finding("read-error", "unable to read local SVG")]
        print_result(result_payload(display_path, metadata, findings, category="io"), args.json)
        return 2

    if len(data) > MAX_FILE_BYTES:
        findings = [Finding("file-too-large", f"SVG exceeds the {MAX_FILE_BYTES}-byte file limit")]
        print_result(result_payload(display_path, metadata, findings), args.json)
        return 1

    metadata, findings = validate_xml(data)
    payload = result_payload(display_path, metadata, findings)
    print_result(payload, args.json)
    return 0 if payload["valid"] else 1


if __name__ == "__main__":
    raise SystemExit(main())
SKILL.md›
---
name: animated-readme-logo
description: Create, transform, animate, or audit GitHub README logos and their verified SVG/PNG/GIF delivery. Use when a repository needs branding assets, not ordinary README prose or app/site motion.
license: Apache-2.0
metadata:
  author: stark-ai-de
  category: engineering-workflows
  version: "0.5.4"
---

# Animated README Logo

## Goal

Audit an existing README logo pipeline or deliver a portable, verified animation from a validated SVG master, deterministic motion specification, and executable repository recipe.

## When to use

- Create, redesign, transform, animate, export, or review a repository or profile-README logo.
- Audit GitHub README logo compatibility, transparency, reduced motion, or local asset references.
- Turn an existing SVG or raster mark into a validated README asset pipeline.

## When not to use

- Ordinary README prose editing with no logo or image concern.
- App/site motion unrelated to repository branding delivery.
- Generic image generation with no README or repository-logo target.

## Workflow selection

Always expose exactly these public workflows:

- `audit`: read-only assessment of existing README logo sources, animation assets, delivery markup, and validation evidence.
- `create`: design a new mark or intentional redesign and deliver the complete verified animation stack.
- `transform`: faithfully recreate or clean up an existing mark and deliver the complete verified animation stack.
- `animate`: use an acceptable existing SVG source and deliver the complete verified animation stack.

There is no `auto` workflow and no public export workflow. Export is an internal rendering stage of every mutating route.

Route from task intent:

- review, compatibility, or quality assessment selects `audit`;
- a new mark or intentional redesign selects `create`;
- faithful work from a raster, screenshot, poster, or unsuitable SVG selects `transform`;
- an acceptable existing SVG that needs motion or raster delivery selects `animate`.

On every activation, show all four workflows, then `Selected`, `Reason`, source evidence, write scope, required outputs, protected originals, and remaining paid/tool/install/overwrite approvals. Proceed after the announcement when intent and mutation authority are unambiguous. A bare invocation, conflicting source evidence, or ambiguity about identity preservation, outcome, scope, or write authority asks the user to choose before substantive inspection. Agent-initiated activation may select `audit`; it may select a mutating workflow only when the existing task explicitly requested that outcome and scope.

Source routing is internal. Provider evaluation is available only within `create`, and selection never authorizes a credit-consuming call or tool installation.

For clear intent, a compact disclosure of `audit | create | transform | animate` and the selected route is sufficient; do not repeat a selection question or details already established in the conversation. Reuse an explicit approval only when it still covers the exact action, target, tool/provider, cost when relevant, and write scope. Separate approval boundaries remain separate: authorization for one never implies another. Ask again only for a changed or missing decision.

## Inputs to inspect

- Repository identity, public brand copy, target surface, and requested task.
- README markup and root-bounded local asset references.
- Existing logo sources, reference-media fidelity needs, transparency, themes, dimensions, accessibility, and export constraints.
- Live provider, authoring, validator, exporter, and inspector capabilities without assuming availability.

## Workflow

1. Resolve and announce the intent-bound workflow. Stop before inspection only when routing or authority is ambiguous.
2. For `audit`, inspect with an explicit repository root, treat local README references as untrusted and root-bounded, run available read-only validation, report remediation, and write nothing. Return immediately after the audit report; do not continue to the mutating workflow steps below.
3. For a mutating workflow, preserve originals and reserve these deterministic names under the established asset folder, or `docs/assets/` when none exists:

   ```text
   <slug>-logo.svg
   <slug>-logo-motion.md
   <slug>-logo-animation.mjs
   <slug>-logo-static.png
   <slug>-logo-animated.gif
   ```

4. Select the internal source route. Read `references/provider-routing.md` for `create`; Recraft is ineligible for `audit`, `transform`, `animate`, clean existing SVG work, and any identity-preserving task.
5. Produce or verify a self-contained SVG at the static first-frame state. An acceptable animation source is a real, self-contained SVG that preserves the intended identity, has stable named layers, and passes the bundled strict validator. A provider result is design input, not readiness proof.
6. Write the human-readable motion specification: the renderer-independent contract for layers, keyframes, easing, duration, loop point, transparency, and reduced-motion state. This explains what must move and is the durable review surface.
7. Write the executable animation recipe: trusted repository-owned `.mjs` code that deterministically turns the SVG and motion contract into frames for the bundled exporter. This explains how the animation is rendered and is not a separate user workflow.
8. Validate the SVG and run the recipe with `--check`. Then use the bundled exporter to create the static PNG and animated GIF, and inspect the GIF. Read `references/asset-transformation.md`, `references/motion-rubric.md`, `references/export-recipe.md`, and `references/asset-pipeline.md`.
9. If a required exporter or inspector command is missing, present the minimal installation preflight and ask for explicit approval. Stop before installation. If installation is declined, forbidden, or unavailable, retain every verified intermediate that can be produced, create no placeholder PNG/GIF, and report incomplete animation delivery.
10. Choose README and optional web-demo delivery from `references/github-readme-compatibility.md`; include static and reduced-motion fallbacks, dimensions, alt text, transparency checks, and a manual committed-GitHub preview.
11. Report the public status fields below, exact files, validation evidence, fallbacks, and remaining blockers.

## Provider boundary

- Detect live Higgsfield MCP capability, exact `recraft_v4_1` availability, and the exact current cost before offering a provider route. Documentation is not availability evidence, and cost must never be hardcoded.
- Present the sanitized brief, live cost, and fixed generation settings before asking for approval.
- Make no credit-consuming call until the user explicitly approves that exact batch after the preflight.
- On unavailable or indeterminate capability, unavailable model/cost data, or explicit refusal, use the direct local SVG fallback.

## Safety rules

- Do not inspect while workflow selection is ambiguous, and do not mutate unless the user's request authorizes the selected mutating outcome and scope.
- Do not install tools, overwrite brand assets, spend credits, publish, or change remote state without the required approval.
- Keep provider approval and local-tool installation approval as separate checkpoints. Approval of one never authorizes the other.
- Do not expose secrets, private paths, internal hostnames, hidden metadata, or customer data in a prompt, asset, snippet, or report.
- Do not send reference media to Recraft or claim it preserves an existing identity.
- Reject absolute, UNC, root-escaping traversal, and symlink-escaping README asset references.
- Do not promise animated SVG, Lottie, or a raster format will work in GitHub README rendering without a compatible fallback and manual GitHub preview.
- Preserve transparency unless the user explicitly requests a background.

## References

Read only what the task needs:

- `references/provider-routing.md`: live Recraft eligibility, cost preflight, approval, fixed settings, and fallback.
- `references/output-contract.md`: exact public fields and status values.
- `references/asset-transformation.md`: direct SVG authoring, transformation, validation, and mutation boundaries.
- `references/asset-pipeline.md`: canonical assets, export capability gates, and transparency checks.
- `references/export-recipe.md`: trusted repository recipe boundary and reusable static-PNG/animated-GIF exporter contract.
- `references/local-tooling.md`: minimal exporter selection, installation approval, verification, and browser-preview fallback routing.
- `references/motion-rubric.md`: deterministic motion specification and reduced-motion requirements.
- `references/github-readme-compatibility.md`: README versus web delivery and renderer fallbacks.
- `references/readme-audit-safety.md`: root-bounded README asset inspection.
- `references/readme-snippets.md`: accessible README and demo markup.

## Scripts

- `scripts/validate_logo_svg.py <svg>` strictly validates the canonical SVG.
- `scripts/inspect-animated-image.mjs <asset>` verifies a generated, metadata-clean GIF, APNG, or animated WebP without modifying it.
- `scripts/export-readme-logo-animation.mjs --root <repo> --recipe <relative.mjs> [--check] [--replace]` validates a trusted repository recipe, then exports a static PNG and animated GIF through approved local tools with validate-before-commit and commit-time no-clobber checks. The exporter modifies only absent declared outputs unless `--replace` is explicit; `--check` performs no exporter-controlled writes, but the trusted recipe still executes.
- `scripts/audit-readme-logo-assets.mjs --root <repo-root> --readme <root-relative-readme>` audits bounded, root-contained README references and fallback roles without modifying files.
- `scripts/generate-readme-logo-snippet.mjs --fallback <path> --alt <text> --width <px> --height <px> [options]` prints markup to stdout.

Run a script with `--help` before relying on optional flags.

## Output format

Always report these fields for an activated task:

```text
Workflow: audit | create | transform | animate
Source route: <route>
Selection: <task evidence and rationale>
Write scope and protected originals: <scope>
Provider state: <state>
Approval state: <state>
Motion readiness: <state>
Animation delivery: <state>
```

Then report the asset stack, motion specification, README delivery, validation evidence, and remaining blockers. Use the exact meanings in `references/output-contract.md`.

## Completion criteria

- All four workflows were exposed and selection was either announced from clear intent or requested for ambiguity.
- `audit` remained strictly read-only.
- Successful `create`, `transform`, and `animate` routes produced and verified the SVG master, motion specification, animation recipe, static PNG, and animated GIF.
- A mutating route with missing tooling retained verified intermediates but reported incomplete delivery rather than success.
- Every paid generation is traceable to a live preflight and explicit approval of that exact batch.
- Every claimed raster export exists and passes the relevant inspector.
- Every README-local path stays within the declared repository root after symlink resolution.
- README animation has a meaningful static fallback, reduced-motion delivery, explicit dimensions and alt text, and a required manual GitHub preview.

## Runtime portability

Keep one host-neutral workflow. Do not branch on Codex, Cursor, Claude, or another agent name and do not emit agent-specific commands. Tailoring has no present benefit because source, approval, validation, motion, and output contracts are shared. Reconsider a split only if a host later requires a materially different tool or output contract.

## Failure modes

- If live provider capability, exact model availability, or current cost cannot be confirmed, report the limitation and author the SVG locally.
- If provider approval is pending, stop before generation. If it is declined, record that once and continue locally.
- If strict SVG or recipe validation fails, correct the source and rerun validation; do not claim motion readiness.
- If a required exporter or inspector runtime is missing and installation has not been declined or forbidden, present the exact minimal tool preflight and ask for approval immediately. Use `Animation delivery: blocked` while approval is pending; install nothing yet.
- If installation is declined, forbidden, or unavailable, keep the validated SVG, motion specification, and checked recipe when possible, report `Animation delivery: incomplete`, and create no placeholder raster.
- If Playwright cannot find its expected browser, do not classify the raster exporter as unavailable. Reuse an existing configured Chrome or Chromium executable, then an existing `agent-browser`; request approval before any CLI or browser download.
- If a README asset reference fails root containment, reject it before reading and report the path class.