Recipes

This document defines how recipe QIP components are discovered from disk.

Root #

Given MIME type/subtype, recipe directory is:

WARC Recipes #

application/warc recipes run at the whole-site layer instead of one page at a time. You can use them for site-wide transforms, such as adding trailing-slash redirects, verifying there are no broken links, or using the path to modify body content.

Debugging broken links

warc-check-broken-links.wasm traps when an internal HTML link does not resolve. To inspect the failures, run the same archive through warc-extract-broken-links.wasm:

qip router warc ./site --view-source \
  | qip run components/application/warc/warc-extract-broken-links.wasm

The result is another application/warc archive. It keeps only response pages containing broken links and reduces each HTML body to the exact opening tags with broken href, src, action, data, or srcset values. An archive with no broken links produces an empty WARC.

Rendering referenced content sizes

recipes/application/warc/25-add-content-size.wasm fills <qip-content-size> elements from the response body stored at an absolute site path:

<qip-content-size src="/components/example.wasm"></qip-content-size>

Bodies below 1,000 bytes render as bytes. Larger bodies render as decimal kilobytes with two fractional digits. The recipe updates the enclosing HTTP and WARC content lengths. During single-route development, the router adds direct static src dependencies to the subset WARC. An unresolved size path is an error instead of producing a plausible size.

Loading custom elements selectively

components/application/warc/warc-add-custom-element-scripts.wasm connects element routes to the pages that use them. It discovers top-level /elements/<tag-name>.js responses in the archive, detects matching custom-element tags in each HTML response, and inserts one external module script per used element:

<script type="module" src="/elements/qip-edit.js"></script>

The recipe ignores tag-shaped text in comments, script, style, textarea, and title content. Existing scripts are not inserted again. Nested routes such as /elements/lib/shared.js remain available to imports but are not treated as entrypoints.

Run this recipe late in the WARC chain so it sees elements introduced by earlier transforms. This repository links it as 99-add-custom-element-scripts.wasm. During single-route development, the router includes transformed top-level element modules in the subset WARC so discovery has the same inputs as a whole-site export.

Execution Context #

WARC recipes can run in two useful scopes. Pick the scope based on the question you are answering.

Use subset scope while developing recipe logic. Use whole-site scope before publishing so final archive semantics are still exercised.

# Fast single-path iteration:
qip router get ./site /docs/router

# Final whole-site run:
qip router warc ./site

Planning And Dry Runs #

qip dry run resolves and validates the same ordered component pipeline as qip run, but does not read input, call render, or write output:

qip dry run \
  components/text/markdown/commonmark.0.31.2.wasm \
  components/text/html/html-page-wrap.wasm

The report is intended to be useful in CI logs without another formatting step:

Pipeline compatible: 2 step(s)
1. components/text/markdown/commonmark.0.31.2.wasm — Content
   Input:  encoding=UTF-8, type=text/markdown, capacity=2.0 MiB (2097152 bytes)
   Output: encoding=UTF-8, type=text/html, capacity=2.0 MiB (2097152 bytes)
   Buffers: 4.0 MiB (4194304 bytes)
2. components/text/html/html-page-wrap.wasm — Content
   Input:  encoding=UTF-8, type=text/html, capacity=256.0 KiB (262144 bytes)
   Output: encoding=UTF-8, type=text/html, capacity=512.0 KiB (524288 bytes)
   Buffers: 768.0 KiB (786432 bytes)
   Note: step 2 (components/text/html/html-page-wrap.wasm): previous output capacity 2.0 MiB (2097152 bytes) exceeds this input capacity 256.0 KiB (262144 bytes); qip run remains valid when the actual intermediate output fits
Total declared buffer capacity: 4.8 MiB (4980736 bytes)
Warnings: 1

A compatible plan exits successfully. Invalid component contracts, uniforms, encoding or MIME composition, and module-policy violations return a non-zero exit status.

Use --capacities-must-fit to turn capacity warnings into errors:

qip dry run --capacities-must-fit \
  components/text/markdown/commonmark.0.31.2.wasm \
  components/text/html/html-page-wrap.wasm

The check requires each Content component's declared maximum output capacity to fit the next Content component's input capacity. This is useful in CI and when refining component contracts: without the flag, the pipeline remains valid when its actual intermediate values fit. Tile capacities are per-tile working buffers rather than whole-image Content capacities, so the Tile contract validates those separately.

The host first extracts a plain description for each recipe step: component kind, input and output encoding, optional MIME types, declared buffer capacities, and Tile halo or Interactive frame dimensions. A pure planner then validates those values and returns the ordered plan used by both commands. The dry-run output prints every step and the sum of its declared input/output buffer capacities. An in-place Tile buffer appears as both input and output but is counted once.

Composition is directional and based only on the ordered step descriptions. The planner does not inspect example input bytes or use browser/runtime heuristics, so the same component artifacts and uniforms produce the same plan or the same error:

There is no generic bytes-to-pixels rule. Image tiling is available only through that explicit bridge, which keeps text, opaque binary data, and pixel buffers from being guessed into one another.

The encoding relationship is small:

Content encodings

raw bytes
└── valid UTF-8

Allowed widening:  UTF-8 ──> raw bytes
Rejected narrowing: raw bytes -X-> UTF-8

Explicit Tile bridge (not subtyping)

image/bmp raw bytes
        │ host decodes
        v
RGBA32Float tiles (width × height × 4 channels, in-place)
        │ host encodes
        v
image/bmp raw bytes

RGBA32Float pixels are physically held in linear memory, but they are not an opaque Content bytes value. Their dimensions, channels, coordinates, tile size, and halo are part of the Tile contract. Only the host's explicit image bridge may cross that boundary.

For example, this plan decodes SVG Content to BMP, applies an in-place Tile filter, then passes BMP Content to the ICO encoder:

qip dry run \
  components/image/svg+xml/svg-rasterize.wasm \
  components/rgba/brightness.wasm '?brightness=0.1' \
  components/image/bmp/bmp-to-ico.wasm

The middle step reports RGBA32Float tile for its input and output encoding; the adjacent Content steps report image/bmp raw bytes at the bridge.

Capacity maxima do not make two steps incompatible by themselves: an upstream component may declare a larger output buffer while producing an actual value that fits the next input buffer. Dry run reports this as a warning because only execution can determine the intermediate byte count.

Host And URLs #

qip router warc controls canonical route host via --host <host>. We prefer setting this explicitly for production builds so recipe logic that reads target URLs sees stable, deploy-intended origins.

Example:

qip router warc ./site --host https://qip.dev

Adding Routes #

WARC recipes can synthesize or rewrite archive records, which means they can add output routes (for example /sitemap.xml) when they emit additional WARC records.

Ordering #

Filename format:

Disabled filename format:

Examples:

Tie-Breaking #

Validation #

Host should reject recipe entries if:

Host should ignore non-.wasm files in the recipes tree.

Scope #