# ADR 066: Batch Recipe Import Staging

- HTML version: https://robbiepalmer.me/projects/recipe-site/adrs/066-batch-recipe-import-staging
- Project: Recipe Site (https://robbiepalmer.me/projects/recipe-site.md)
- Status: Proposed
- Date: 2026-08-08

# Summary

Add a persistent batch-import and review layer over the existing one-recipe ingestion jobs.

A `recipe_import_batch` groups independently executable `recipe_import_job` records in source
order. Each successful job creates an immutable generated artifact and a mutable import draft.
Users review drafts in one queue-based workspace, autosave edits, and independently accept, skip,
retry, or reject each item. Accepting a draft creates a canonical recipe; generating a draft does
not.

URL and local-file batches use deterministic boundaries: one URL or file is one recipe. Photo
batches first produce a user-confirmed grouping manifest in which each recipe group owns one or
more ordered images. Automated grouping may be offered as a suggestion but is not authoritative.

Collection archives are expanded into ordered batch items that follow the same review contract as
every other source. No import creates a canonical recipe until the user reviews and explicitly
accepts that item. Accepted recipes retain batch provenance so a collection import can still be
identified and undone safely.

# Context

The site currently supports four ways to add one recipe:

* manual Cooklang authoring;
* importing one schema.org Recipe URL;
* importing one Cooklang or JSON-LD file;
* importing up to six photographs that together represent one recipe.

Every method feeds the same editor and live preview before an explicit save. Photo ingestion is
already durable: [ADR 049](/projects/recipe-site/adrs/049-cloudflare-workflows-recipe-ingestion)
assigns one `recipe_import_job` to the production extraction workflow, keeps operational state in
Postgres, stores immutable source and stage artifacts in R2, and returns an editable generated
draft.

Batch use cases introduce concerns that do not belong inside a single job:

* one user action creates many independent work units;
* aggregate progress must survive navigation and browser restarts;
* one failed or duplicate source must not fail the other sources;
* editors need stable per-item navigation without losing unsaved corrections;
* shared defaults such as visibility and duplicate policy apply across a batch;
* collection migrations need provenance and a safe whole-batch undo;
* photo batches need an explicit mapping from many unordered images to several recipes;
* expensive work must remain independently retryable and idempotent.

The existing final draft artifact is immutable evidence of what the model generated. It should not
also become mutable editor state. Conversely, canonical recipes should not be created merely so an
unfinished import can survive navigation. A staging model is therefore required between extraction
and publication.

# Decision

## Preserve One Recipe per Execution Job

Keep `recipe_import_job` as the independently executable and billable work unit selected by
[ADR 049](/projects/recipe-site/adrs/049-cloudflare-workflows-recipe-ingestion). Do not make one
Workflow instance parse a whole batch sequentially.

Each job:

* owns exactly one recipe source boundary;
* may reference one URL, one local recipe file, or an ordered group of recipe images;
* has independent execution status, attempts, artifacts, usage, retry, and failure diagnostics;
* can finish even when sibling jobs fail or await review.

Generalize the existing image-oriented job metadata with a source type and immutable source
manifest reference. `imageCount` becomes nullable or zero for non-image sources; image object keys
remain in the source manifest rather than widening the job row for every input type.

This preserves the current artifact and observability model and makes bounded parallelism possible.
The batch coordinator may limit how many child workflows are active for one user or provider, but
does not replace their execution state.

## Add a Batch Parent

Add a `recipe_import_batch` record containing:

* owner and batch mode;
* aggregate status and counts;
* shared visibility, collection/label, and duplicate-policy defaults;
* source count and deterministic source-order metadata;
* creation, update, completion, and optional undo timestamps;
* an idempotency key for the batch submission.

Add a nullable batch relationship and stable position to `recipe_import_job`. Existing single-photo
jobs remain valid without a batch. Aggregate progress is derived from child states and may be
denormalized on the batch for efficient list reads, provided child job state remains authoritative.

Batch status describes the overall user journey, not execution of one parsing chain. Initial
states are:

* `preparing` — sources are being validated, deduplicated, or grouped;
* `processing` — at least one child job is queued or running;
* `awaiting_review` — processing is terminal and at least one item needs a decision;
* `completed` — every item is accepted, skipped, rejected, or otherwise terminal;
* `undoing` and `undone` — a migration rollback is active or complete;
* `failed` — batch preparation failed before useful child work could be created.

A failed child job does not normally make the batch itself `failed`.

Child-state transitions and any denormalized batch counters or status projection update in the
same database transaction. Child records remain authoritative, so a periodic reconciliation can
rebuild the projection and alert on drift rather than letting an inconsistent aggregate become a
second source of truth.

## Separate Execution, Review, and Recipe State

Execution and editorial review are independent state machines.

Each batch item has a review state such as:

* `waiting`;
* `ready`;
* `needs_attention`;
* `possible_duplicate`;
* `accepted`;
* `skipped`;
* `rejected`.

A failed execution remains retryable and has no ready draft. A successful execution normally
creates:

1. an immutable generated draft artifact in the existing artifact graph; and
2. a mutable `recipe_import_draft` record used by the editor.

The mutable draft contains the normalized recipe payload or Cooklang source, editor metadata,
validation state, optimistic-concurrency version, and update timestamp. Autosave updates this
record. It does not rewrite the immutable model artifact.

Autosave is a conditional update against the version last read by the client. A stale write is
rejected without overwriting the newer draft, and the workspace surfaces a conflict that lets the
user reload or compare changes. The same version check guards acceptance so an older browser tab
cannot publish over newer edits.

Accepting a draft runs one authenticated transaction that:

1. verifies ownership, current review state, and draft version;
2. applies the chosen visibility and other overrides;
3. creates the canonical recipe;
4. records the accepted recipe ID and correction/provenance metadata;
5. marks the item accepted.

The operation uses an item-level idempotency key so retries cannot create duplicate recipes. The
key, requested draft version, and resulting recipe ID are stored durably with a uniqueness
constraint scoped to the owner, item, and accept action. Replaying a completed request returns the
recorded result; a second key still cannot accept an already accepted item.

Review transitions are validated server-side and recorded in an audit log. Processing success
moves a waiting item to `ready`, `needs_attention`, or `possible_duplicate`; acceptance requires a
current draft version and resolution of blocking validation or duplicate warnings; retry applies
only to failed execution; and accepted items cannot return to an editable state through an ordinary
review action. Accepted, skipped, and rejected are terminal for batch accounting; reopening a
skipped or rejected item is an explicit, audited transition rather than an incidental edit.

## Use One Review Workspace

The product surface is a persistent review workspace:

* a queue or filterable item rail shows processing and review states;
* the existing recipe editor and live preview render the selected draft;
* edits autosave before navigating between items;
* **Save and next** accepts the current item and selects the next item needing review;
* skip, retry, and duplicate-resolution actions affect only the selected item;
* each item has a stable route under the batch so reload, deep linking, and browser navigation work;
* every item requires explicit review and acceptance, with additional resolution steps for
  duplicates and validation warnings.

The browser may leave while jobs run. The batch list and item routes are reconstructed entirely from
server state rather than depending on an open tab.

The coordinator applies explicit per-owner and per-provider active-job caps. Cap checks, quota
reservation, and child-job admission occur atomically with job creation; excess work remains queued
rather than starting unbounded workflows. Queue depth and throttled state are visible to the
workspace so backpressure is not presented as stalled work.

## Treat Collection Archives as Batch Sources

Supported structured archives from known recipe applications use the same batch and review model
as URL, file, and photo imports.

The archive adapter validates and expands the uploaded archive during batch preparation. Each
logical recipe entry becomes its own ordered batch item and job, retaining the archive checksum and
entry identifier as source provenance.

Archive preparation enforces compressed size, expanded size, entry-count, compression-ratio, and
normalized-path limits before creating child work. Expansion is streamed with bounded memory;
each entry is parsed with bounded memory and execution time; and an over-budget or invalid entry
receives item-level diagnostics while valid siblings can continue to review.

Every structurally valid entry produces a mutable review draft. The user must open, review, and
explicitly accept each draft before it creates a canonical recipe. Shared batch defaults can reduce
repetitive editing, but cannot replace item-level acceptance. Every accepted recipe records its
source batch and source item.

Whole-batch undo deletes only accepted recipes created by that batch and only when they have not
subsequently been transferred, forked, or otherwise made unsafe to delete. Unsafe items are
reported and left intact for manual resolution.

Undo is an explicit authenticated background operation with per-item results; it is not a broad
delete by label or timestamp. Batch labels remain useful for discovery but are not the integrity
boundary.

Before execution, undo produces a dry-run summary from immutable batch provenance. Each deletion
then locks the canonical recipe and its provenance record, rechecks dependent transfers, forks,
logs, and other protected references inside one transaction, and deletes only if those guards still
pass. Concurrent changes therefore preserve the recipe and produce a per-item diagnostic rather
than racing the safety check.

## Require a Photo Grouping Manifest

Photo batches cannot assume one image equals one recipe because a recipe may span several pages.
Before starting child workflows, persist a grouping manifest containing:

* every uploaded image exactly once unless the user intentionally excludes it;
* ordered recipe groups;
* ordered image IDs within each group;
* optional user titles or source labels;
* manifest version and confirmation timestamp.

The UI offers **Each image is a separate recipe** as a fast path. Otherwise, users arrange
thumbnails into groups and can create, merge, split, reorder, or remove groups. Suggested boundaries
may use filenames, capture time, page headings, numbering, or model output, but extraction begins
only from the confirmed manifest.

Source images remain immutable R2 objects. Regrouping before processing creates a new manifest
version rather than moving or rewriting image objects.

Confirmed manifest versions are immutable and checksummed. Every child job binds to the exact
confirmed version it executes, and an unconfirmed or superseded version cannot start new work.
The job's immutable artifact metadata records that manifest version and checksum so the binding is
verified again before acceptance. Prior confirmed versions are retained with the source artifacts
for replay and audit.

## API Shape

The exact route names may evolve, but the resource model should support:

* create and preflight a batch;
* add URL, file, archive, or uploaded-image sources;
* confirm a photo grouping manifest;
* start or resume processing;
* list a user's batches and aggregate counts;
* list/filter items in a batch;
* read and autosave one mutable draft;
* retry, skip, reject, accept, or resolve a duplicate for one item;
* begin migration undo and read its per-item outcome.

Large lists use cursor pagination. Queue filters are server-addressable so the next item needing
review can be selected without loading the entire batch.

# Alternatives Considered

## Open a Separate Editor Page or Browser Tab per Recipe

Rejected as the primary experience. It reuses existing routes cheaply but loses batch context,
makes progress and failures difficult to scan, and creates fragile unsaved state across many tabs.
Stable item routes are still valuable, but they render inside one batch workspace.

## Create Canonical Private Recipes as Soon as Parsing Finishes

Rejected for every import source, including structured collection archives. It pollutes the recipe
box with unreviewed or malformed content, conflates privacy with editorial readiness, and makes
cleanup dependent on broad deletion. Provenance and undo improve recovery, but do not replace
explicit review before publication.

## Use One Workflow Instance for the Entire Batch

Rejected. A large batch would become one oversized failure and retry domain, complicate per-item
quota reconciliation, and serialize work that can safely run with bounded concurrency. A parent
coordinator may enqueue child jobs, but the existing one-recipe workflow remains the unit of
execution.

## Store Edits by Mutating the Generated Artifact

Rejected. Generated artifacts are evidence needed for debugging, evaluation, and measuring user
corrections. Mutable editor state belongs in a separate draft record whose accepted version can be
compared with the original artifact.

## Let AI Infer Photo Boundaries Without Confirmation

Rejected. A wrong boundary can merge unrelated recipes or omit necessary ingredient and instruction
pages while still producing plausible output. Boundary suggestions are useful assistance, but the
grouping manifest is a user-confirmed input contract.

# Consequences

## Positive

* Batch progress and corrections survive browser navigation and restarts.
* Existing one-recipe workflows, artifacts, quotas, and observability remain reusable.
* One bad source does not block successful siblings.
* Generated evidence remains immutable while editor drafts are safely autosaved.
* Review is efficient without sacrificing independent acceptance.
* URL, file, archive, and photo batches share one review and publication model.
* Migration provenance supports precise recovery rather than best-effort cleanup.

## Negative

* The database gains parent, draft, review, provenance, and undo state.
* Aggregate batch status can drift if transition code is scattered or not transactional.
* Autosave needs optimistic concurrency and conflict handling across multiple browser tabs.
* Large batches require workflow concurrency controls and careful quota reservation.
* Migration undo needs explicit safety checks and per-item diagnostics.
* Photo grouping adds an interaction step before users see extracted recipes.

# Implementation Sequence

1. Add URL-list and multi-file batch capture, using one existing job per source and a persisted
   review queue.
2. Add collection archive adapters, reviewed drafts, batch provenance, and safe whole-batch undo.
3. Add the photo thumbnail grouping manifest and reuse the existing one-to-six-image workflow for
   each confirmed group.

# When To Revisit

* Batch sizes or provider limits require a dedicated queue-based fan-out coordinator.
* Review drafts need collaborative editing rather than single-owner optimistic concurrency.
* Migration adapters need resumable streaming ingestion for archives too large to preflight in one
  request.
* Boundary suggestions become reliable enough to reduce, but not remove, the photo confirmation
  step.

---

Markdown index of this site: https://robbiepalmer.me/llms.txt
