# Aperta image contract

> **Generated file — do not edit.** Written by `npm run skill:export` (`scripts/export-agent-skill.ts` → `src/lib/agent/skillExport.ts`) from `fabrication.config.ts` (`resolveFab().minHole`, `FAB.minLetterBridgeMm`), `src/lib/agent/config.ts` and `capabilitiesOf()`. A test fails when this file and the code disagree.
> This is a snapshot at contract version `2026-09` (API `v1`). The live values are `GET /api/agent/v1/capabilities` — fetch it first; it wins over this file.

The PNG you submit is a **measurement, not an illustration**: Aperta traces it, scales it to the job's
millimetres and cuts exactly that shape from a flat 1.5 mm sheet of C260 brass, annealed (O60), which is then rolled.
Black is metal, white is air.

## 1. The file

| Rule | Value |
| --- | --- |
| Format | **PNG only** — a raw body with `Content-Type: image/png`, or `multipart/form-data` with a file field named `image`. JPEG and WebP are refused (`415 UNSUPPORTED_FORMAT`): lossy ringing corrupts the black/white edge the tracer measures. |
| Size | at most **6 MB** (6,000,000 bytes) — `413 FILE_TOO_LARGE` |
| Short side | at least **256 px** — `400 IMAGE_TOO_SMALL` |
| Long side | at most **8192 px** — `400 IMAGE_TOO_LARGE` |
| Pixels | at most **24 MP** (24,000,000 pixels) — `400 IMAGE_TOO_LARGE` |
| PNG header | a valid signature and IHDR; bit depth 1/2/4/8/16, colour type 0/2/3/4/6 — otherwise `400 INVALID_FILE` |
| Recommended | **1536 × 1024**, landscape, 8-bit greyscale or RGB, no alpha channel, no colour profile — about 200 KB |

Aperta never decodes your image in its edge worker: it checks the signature and the IHDR header, then hands the
bytes to the tracer. Nothing else about the file is inspected before tracing.

## 2. The drawing

1. **Flat black on pure white.** Ink `#000000`, background `#FFFFFF`. No grey, gradients,
   shadows, texture, anti-aliased halos or transparency. When 5% or more of the ink pixels are
   mid-grey you get the warning `NOT_TWO_TONE`.
2. **One connected piece.** Only the largest black region is traced; everything else vanishes. When the other
   regions hold 0.5% or more of the ink you get the warning `DISCONNECTED_REGIONS`. Metal that
   sits inside an opening (an island) is bridged to the body or removed by the framing step
   (`ISLAND_BRIDGED` / `ISLAND_REMOVED` warnings), or refused (`TRAPPED_ISLAND`) when neither is possible.
3. **Landscape.** Draw the piece horizontally — its long axis left to right — on a landscape canvas.
4. **Margin.** Leave at least **5%** white on every side. Pixels on the border are read as
   background; when 50% or more of a border is covered you get the warning `DESIGN_TOUCHES_EDGE`.
5. **Proportion.** The drawn ratio — the black piece's own bounding box, length ÷ width — must be the job's
   `lengthMm / widthMm` (`job.drawing.ratio`). The piece is scaled to the job's length and declared width:
   - within ±5% of the job's ratio: nothing to report;
   - further off: the pattern is stretched along the length to keep the declared width, with the warning
     `PROPORTION_MISMATCH` (see `geometry.stretch`);
   - a stretch of ×1.5 or more (or ×1/1.5 or less): the error `PROPORTION_MISMATCH` —
     the design would be distorted, so it is refused.
6. **Crop aspect.** Independently of the job, the piece itself (not the canvas) must lie between **1:2.28**
   (at most 2.28 times taller than wide) and **46:1** (at most 46.15 times longer than
   high). Outside that range the tracer cannot sample it, and the submission fails with `PROPORTION_MISMATCH`.

Worked examples (computed by the same sizing code as `POST /jobs`):

| Job | Blank | Draw at ratio | Smallest opening area |
| --- | --- | --- | --- |
| bracelet, `wristMm` 165, `fit` comfort, `widthMm` 18 | 160.4 × 18 mm | 8.91 | 1.155 mm² |
| ring, `usRingSize` 7, `widthMm` 6 | 55.95 × 6 mm | 9.33 | 0.134 mm² |

## 3. Manufacturing floors

These are the two enforced floors, read from `fabrication.config.ts` when this file was exported — the same
values the validator applies (V5 and V4):

| Floor | Value | Refused as |
| --- | --- | --- |
| Minimum opening — every cutout, at its narrowest point | **0.2 mm** | `OPENING_TOO_SMALL` (with `locations`) |
| Minimum metal — every strip of metal, including bridges that hold islands | **0.6 mm** | `NECK_TOO_THIN` (with `locations`) |

Thinner necks may be widened by the framing step instead (`NECK_THICKENED` warning); openings the tracer finds
narrower than 0.2 mm are filled before tracing (`OPENING_REMOVED` warning) or refused
(`FEATURES_TOO_FINE`) when too many would go.

The floors as a share of the piece's height in the image (that height is the piece's width in millimetres):

| Piece width | Minimum opening | Minimum metal |
| --- | --- | --- |
| 6 mm | 3.3% | 10% |
| 8 mm | 2.5% | 7.5% |
| 12 mm | 1.7% | 5% |
| 18 mm | 1.1% | 3.3% |
| 25 mm | 0.8% | 2.4% |
| 40 mm | 0.5% | 1.5% |

### The area floor

Before tracing, the tracer fills every opening whose area is smaller than **0.04% of the piece's area**
(`0.0004 × lengthMm × widthMm` mm²; per job: `job.drawing.minOpeningAreaMm2`). On a
160 × 18 mm bracelet that is **1.15 mm²** — a round hole smaller than about 1.2 mm across
disappears, while a 0.2 × 10 mm slot (2 mm²) survives. On a 56 × 6 mm ring it is
0.134 mm². Filled openings are reported as the warning `OPENING_REMOVED`.

So every opening must be **at least 0.2 mm wide and larger than 0.04% of L × W**.

### Pixels

What limits an image model is pixels, not millimetres: a feature narrower than about **3 px** in
your PNG does not survive drawing and tracing reliably. On the recommended 1536 px canvas with 5%
margins, a 160 mm bracelet spans about 8.6 px/mm — so 0.2 mm is about
**1.7 px**, below that floor, and 3 px is about 0.35 mm. A 56 mm ring
spans about 25 px/mm (3 px ≈ 0.12 mm), where the 0.2 mm floor is
the one that binds. On long pieces, draw openings and metal comfortably wider than the floors.

## 4. The rules exactly as `GET /capabilities` returns them (`image.rules`)

1. Flat black ink (#000000) on a pure white background (#FFFFFF); no grey, gradients, shadows, texture or transparency.
2. Exactly one piece: every black region must connect to the main body. Detached regions are dropped.
3. Black is metal; white inside the piece is a cutout. The piece's outline is its silhouette.
4. Draw the piece horizontally, longer than wide, at the job's length-to-width ratio, with at least 5% white margin on every side.
5. Every cutout must be at least 0.2 mm wide at its narrowest point and larger than 0.04% of the piece's area.
6. Every strip of metal, including bridges that hold islands, must be at least 0.6 mm wide.
7. No text rendered by the image model, no gemstones, no colour, no closed rings.
8. The piece is cut flat and then rolled; the two short ends stay open. Ends may be rounded, pointed or shaped.

## 5. Self-check before you submit

- [ ] PNG, 1536 × 1024 (or within the limits above), under 6 MB.
- [ ] Only pure black and pure white; no shadow, gradient, glow, frame, caption or watermark.
- [ ] One black piece; nothing floating; nothing touching the border; ≥ 5% margin.
- [ ] Horizontal, and its bounding box has the job's ratio (`job.drawing.ratio`) within 5%.
- [ ] No opening narrower than 0.2 mm or smaller than `job.drawing.minOpeningAreaMm2`; no metal thinner than 0.6 mm.
- [ ] No text, gemstones, colour or closed ring; both short ends open.
