# Proposed implementation plan — first instrumented slice

Nonfinal checkpoint 0.5.2 · **Proposal, not approved.** Prose in this file is maintained by hand; Mermaid blocks between `<!-- diagram:… -->` markers are synced from `docs/diagrams/*-ex_*.mmd` when you run `node tools/generate-docs.js`. The reviewed architecture baseline remains checkpoint 0.4.0. Explainer figures are authored in `assets/fixtures-explainers.js`, rendered in the workbench, and exported to those `.mmd` sources.

**Illustrated HTML.** The workbench screen `implementation-plan.html` is not in this package. The same proposals are drawn on the guide’s [implementation roadmap](../roadmap.html). Mermaid copies for this file are under `diagrams-mmd/`. The workbench path `docs/diagrams` is not in this package.

## Decision status

- **Confirmed:** a maximum of **USD 50/month** for recurring services. This is Jonathan’s only confirmed decision.
- **Proposed (PROP), established-in-design but unbuilt (EST), or OPEN:** everything else in this plan. That includes the stack, deployment arrangement, launch scope, publication policy, reduced recovery-spike scope, new spikes and thresholds. These labels match the 0.4.0 design documents.
- This plan reuses the 0.4.0 [first slice](architecture/first-slice.md), [decisions](architecture/decisions-for-jonathan.md), [technology and spikes](architecture/technology-ports.md), [budget controls](budget-controls.md), [cost model](cost-model.md) and [deployment matrix](deployment-matrix.md). It does not deploy infrastructure, incur charges or implement the application.

## Overview

1. Build the deterministic Pantry & Pour flow locally, at no recurring cost: intake, evidence, normalization, identity, acceptance, transactional outbox and product projection.
2. Run the platform-semantics spikes. This is the first point at which billing may begin, and it is gated on pricing.
3. Add **one** authorized real source in shadow mode and measure it.
4. Release routine updates within an explicitly authorized publication scope.
5. Revise the cost estimate from the measurements and decide whether a second scraper fits the budget.

## 1. Provisional stack and deployment (PROP)

**Figures:** [Local first slice](../roadmap.html#local) (`ex_deploy_local`, guide figure `fig-deploy-local`) · [Hosted arrangement](../roadmap.html#hosted) (`ex_deploy_hosted`, guide figure `fig-deploy-hosted`) · Mermaid sources [local](diagrams-mmd/22-ex_deploy_local.mmd) · [hosted](diagrams-mmd/23-ex_deploy_hosted.mmd). The workbench screens `decisions.html` and `implementation-plan.html` are not in this package.

<!-- diagram:ex_deploy_local -->

```mermaid
flowchart TB
  alt("Browser host [OPTIONAL]<br/><small>not used locally</small>"):::alt
  coll["Fixture feed<br/><small>Pantry & Pour JSON</small>"]:::service
  api["Resolver<br/><small>Hono on Node</small>"]:::service
  prod["Product service<br/><small>Hono on Node</small>"]:::service
  dom["Domain packages + ports [STABLE]<br/><small></small>"]:::boundary
  p1("ArtifactStore<br/><small>port</small>"):::state
  p2("JobTransport<br/><small>port</small>"):::state
  p3("UnitOfWork<br/><small>port + repositories</small>"):::state
  p4("ModelProvider<br/><small>port</small>"):::state
  a1[("Filesystem [ADAPTER]<br/><small>by checksum</small>")]:::storage
  a2[/"PgJobTransport [ADAPTER]<br/><small>jobs in PostgreSQL</small>"/]:::queue
  a3[("Local PostgreSQL [ADAPTER]<br/><small>one instance</small>")]:::db
  a4("Fake provider [ADAPTER]<br/><small>no paid calls</small>"):::alt
  rs[("resolver schema<br/><small>resolver_app writes; product_app denied</small>")]:::db
  ps[("product schema<br/><small>product_app writes projections</small>")]:::db
  alt -.-> coll
  coll -->|capture| api
  api ==>|events| prod
  p1 -->|implemented by| a1
  p2 --> a2
  p3 --> a3
  p4 --> a4
  a3 -->|resolver_app| rs
  a3 -->|product_app| ps
  classDef db stroke-width:2px
  classDef queue stroke-dasharray:6 3
  classDef storage fill:#eef1f4
  classDef llm stroke:#b7862b,stroke-width:2px
  classDef evidence stroke-dasharray:2 2
  classDef external stroke-dasharray:2 3
  classDef alt stroke-dasharray:4 4
  classDef gate stroke-width:2px
  classDef decision stroke-width:1.5px
  classDef ok stroke-width:1.5px
  classDef hold stroke-dasharray:4 3
  classDef stop stroke-width:2px
  classDef unknown stroke-dasharray:2 2
  classDef boundary stroke-dasharray:8 4,stroke-width:1.5px
```

<!-- /diagram:ex_deploy_local -->

<!-- diagram:ex_deploy_hosted -->

```mermaid
flowchart TB
  alt("Browser host [OPTIONAL]<br/><small>browser or regional; / only if a source needs it</small>"):::alt
  coll["Workers + Cron<br/><small>feed / API collector</small>"]:::service
  api["Resolver [CHANGES]<br/><small>Hono on Workers</small>"]:::service
  prod["Product service [CHANGES]<br/><small>Hono on Workers</small>"]:::service
  dom["Domain packages + ports [STABLE]<br/><small></small>"]:::boundary
  p1("ArtifactStore<br/><small>port</small>"):::state
  p2("JobTransport<br/><small>port</small>"):::state
  p3("UnitOfWork<br/><small>port + repositories</small>"):::state
  p4("ModelProvider<br/><small>port</small>"):::state
  a1[("Private R2 [ADAPTER]<br/><small>S3-compatible</small>")]:::storage
  a2[/"Queues [ADAPTER]<br/><small>Node: PgJobTransport</small>"/]:::queue
  a3[("Hyperdrive + PG [ADAPTER]<br/><small>Neon or other</small>")]:::db
  a4("Model API [ADAPTER]<br/><small>metered; optional</small>"):::external
  rs[("resolver schema<br/><small>resolver_app writes; product_app denied</small>")]:::db
  ps[("product schema<br/><small>product_app writes projections</small>")]:::db
  alt -.-> coll
  coll -->|capture| api
  api ==>|events| prod
  p1 -->|implemented by| a1
  p2 --> a2
  p3 --> a3
  p4 --> a4
  a3 -->|resolver_app| rs
  a3 -->|product_app| ps
  classDef db stroke-width:2px
  classDef queue stroke-dasharray:6 3
  classDef storage fill:#eef1f4
  classDef llm stroke:#b7862b,stroke-width:2px
  classDef evidence stroke-dasharray:2 2
  classDef external stroke-dasharray:2 3
  classDef alt stroke-dasharray:4 4
  classDef gate stroke-width:2px
  classDef decision stroke-width:1.5px
  classDef ok stroke-width:1.5px
  classDef hold stroke-dasharray:4 3
  classDef stop stroke-width:2px
  classDef unknown stroke-dasharray:2 2
  classDef boundary stroke-dasharray:8 4,stroke-width:1.5px
```

<!-- /diagram:ex_deploy_hosted -->

| Layer | Provisional choice | Status | Fallback if a spike fails |
|---|---|---|---|
| Language and structure | TypeScript. Domain packages depend only on ports; provider code stays in adapters (ADR-007) | PROP | — |
| Local stage (M0–M1) | Hono on Node, local PostgreSQL, filesystem `ArtifactStore`, `PgJobTransport`, in-memory or fake paid providers | PROP | — |
| Hosted runtime (M2 onward) | Hono on Workers, Hyperdrive to managed PostgreSQL (DJ-07) | PROP | Node container near the database. Only the adapters change; the domain contracts stay the same |
| Database | One PostgreSQL instance with `resolver` and `product` schemas and service-specific roles. The cost model assumes Neon at 0.25 CU for 16 h/day (DJ-01, ADR-002) | PROP | Separate instances if role or failure isolation requires it |
| Accepted state | Relational tables plus a transactional outbox (DJ-02, ADR-003) | PROP | — |
| Evidence | Private R2 bucket, artifacts stored by checksum | PROP | Any S3-compatible store |
| Job transport | At-least-once delivery, references only, acknowledge after commit, pending-job sweeper (ADR-009) | PROP | `PgJobTransport` |
| Product projection | `product.item_view` using JSONB + GIN in the shared instance (ADR-010) | OPEN | Generated columns or search, only if SPK-04 requires them |
| First real collector | Workers + Cron for a feed or API. Browser Run or a regional host only if the source requires it | PROP | Local Playwright |
| Operator UI | Static assets on an access-controlled admin origin | PROP | — |

**Arrangement (PROP):** one Cloudflare account and one database project. Production is the only hosted environment; development and CI stay local. No hosted surface accepts unbounded public traffic.

## 2. Proposed revisions to the 0.4.0 design

These are proposals and need Jonathan’s agreement.

- **DJ-11 is narrowed.** 0.4.0 proposes launching with two scrapers. This plan launches one real source, measures it, then decides on the second (M5).
- **SPK-03 is recast.** It currently targets `src_halden_us`/`src_lumen_web` against Browser Run limits. It should target the source selected in DJ-06. If that source is a feed with stable identifiers, the browser, LLM-fallback and most review paths will not be exercised. Those cost rows stay as assumptions marked **unknown**, never zero.
- **Queue redelivery is separated from paid attempts (§5).** The 0.4.0 simulation `redeliver(id)` in `assets/budget.js` counts deliveries on the reservation. The implementation keeps transport deliveries and provider attempts in separate records.
- **SPK-05 scope becomes a decision (§7).** ADR-007 requires SPK-01..05 to pass before the slice counts as done. A plan sized to USD 50 would run a reduced SPK-05 first.

## 3. Milestones

**Figures:** [Milestones and gates](../roadmap.html#milestones) (`ex_milestones`, guide figure `fig-milestones`) · [Mermaid source](diagrams-mmd/21-ex_milestones.mmd). The workbench screens `decisions.html` and `implementation-plan.html` are not in this package.

<!-- diagram:ex_milestones -->

```mermaid
flowchart TB
  lz["Local · no recurring service cost<br/><small></small>"]:::boundary
  hz["Hosted · must fit USD 50 ceiling<br/><small></small>"]:::boundary
  g1[["Gate 1 · decisions<br/><small>DJ-02, DJ-09, DJ-10 / SPK-05 scope</small>"]]:::gate
  m0["M0 · Skeleton [NO COST]<br/><small>ports, schemas, ledger</small>"]:::service
  m1["M1 · Fixture slice [NO COST]<br/><small>Pantry & Pour end to end / policy + ledger checks</small>"]:::service
  g2[["Gate 2 · pricing<br/><small>P1–P4 priced / DJ-01, DJ-07 decided</small>"]]:::gate
  m2["M2 · Platform spikes [MAY BILL]<br/><small>SPK-01, SPK-02, SPK-05</small>"]:::service
  g3[["Gate 3 · real source<br/><small>DJ-06 + written access / spikes passed</small>"]]:::gate
  m3["M3 · Shadow source [PRIVATE]<br/><small>14 daily cycles / no public release</small>"]:::service
  g4[["Gate 4 · publication<br/><small>policy v1 approved</small>"]]:::gate
  m4["M4 · Scoped release [PUBLISHES]<br/><small>routine updates in scope</small>"]:::service
  g5[["Gate 5 · budget fit<br/><small>SPK-09 fit rule</small>"]]:::gate
  m5["M5 · Second source? [DECISION]<br/><small>add · reduce · defer</small>"]:::service
  f1(["Hold the build<br/><small>decide or narrow scope</small>"]):::hold
  f2(["Stay local<br/><small>no paid service until / priced within USD 50</small>"]):::stop
  f2b(["Swap the adapter<br/><small>Node container (DJ-07) / domain code unchanged</small>"]):::hold
  f3(["Keep the fixture<br/><small>choose another source</small>"]):::stop
  f3b(["Re-price or reject<br/><small>source fails measurement</small>"]):::hold
  f4(["Stay private<br/><small>accepted data unpublished</small>"]):::stop
  f5(["Defer or reduce<br/><small>one source; reserve kept</small>"]):::hold
  g1 -->|pass| m0
  m0 --> m1
  m1 --> g2
  g2 -->|pass| m2
  m2 --> g3
  g3 -->|pass| m3
  m3 --> g4
  g4 -->|pass| m4
  m4 --> g5
  g5 -->|pass| m5
  g1 -->|fails| f1
  g2 -->|fails| f2
  m2 -->|spike fails| f2b
  g3 -->|fails| f3
  m3 -->|poor data| f3b
  g4 -->|no approval| f4
  g5 -->|no fit| f5
  classDef db stroke-width:2px
  classDef queue stroke-dasharray:6 3
  classDef storage fill:#eef1f4
  classDef llm stroke:#b7862b,stroke-width:2px
  classDef evidence stroke-dasharray:2 2
  classDef external stroke-dasharray:2 3
  classDef alt stroke-dasharray:4 4
  classDef gate stroke-width:2px
  classDef decision stroke-width:1.5px
  classDef ok stroke-width:1.5px
  classDef hold stroke-dasharray:4 3
  classDef stop stroke-width:2px
  classDef unknown stroke-dasharray:2 2
  classDef boundary stroke-dasharray:8 4,stroke-width:1.5px
```

<!-- /diagram:ex_milestones -->

| # | Milestone | Concrete deliverables | Acceptance checks |
|---|---|---|---|
| **M0** | Skeleton and contracts (no cost) | Repository with domain, ports and adapters packages. Migrations for the `resolver` and `product` schemas and roles. Contract fixtures from `contracts/` as test vectors. Measurement record schema (§4). Budget-ledger tables with separate delivery and attempt records (§5) | Contract vectors pass. `product_app` cannot read or write the `resolver` schema (grant test). Migrations apply cleanly to an empty database |
| **M1** | Deterministic fixture slice, local (no cost) | Pantry & Pour end to end: `cap_pp_feed_1011` → AW-02 intake → `pa_shopfeed_json`/`np_coffee` → identity rules → AW-01 with outbox → AW-03 relay → AW-04 projection → release. Publication-policy enforcement (§6). Instrumentation on every step. Ledger with fake paid providers | All ten [first-slice](architecture/first-slice.md) checks pass. Examples: one receipt for a duplicate capture; the sweeper recovers when the process is killed between commit and enqueue; `lst_pp_ridge_2x250` never maps to `itm_ridgeline_500g`; private records never reach public responses. The policy checks (§6) and the redelivery/attempt checks (§5) also pass |
| **M2** | Platform-semantics spikes | SPK-01, SPK-02 and SPK-05 (reduced or full) against the hosted services | Exit criteria in §4. **First milestone that may bill.** Gated on pricing tasks P1–P4 (§8). Capped at the priced base plus a short database trial |
| **M3** | One real source in shadow mode | DJ-06 source with access terms confirmed in writing. Collector deployed. Captures accepted privately with **no public release** for the measurement window (proposed: 14 daily cycles) | Every metric in §4 is either measured, with its sample size, or `unknown (unexercised)` with a reason. Ledger exposure never exceeded the USD 40 operating envelope |
| **M4** | First authorized publication scope | Policy v1 for (tenant, catalog, source, field allowlist), approved by a `publications:write` holder. Routine updates release automatically within that scope | Public API serves only applied releases. Out-of-scope fields stay private. Identity changes and uncertain claims open review cases (§6) |
| **M5** | Revised cost and second-scraper decision | Cost model rerun with measured inputs replacing assumptions (SPK-09). Decision memo on the second source | Second scraper admitted only if the fit rule (SPK-09) holds. Otherwise deferred, with the gap stated |

## 4. Spikes; measurements are their outputs

**Figures:** [Measurements driving the second-source decision](../roadmap.html#measures) (`ex_measurements`, guide figure `fig-measures`) · [Mermaid source](diagrams-mmd/24-ex_measurements.mmd). The workbench screens `decisions.html` and `implementation-plan.html` are not in this package.

<!-- diagram:ex_measurements -->

```mermaid
flowchart TB
  m1("Source reliability [SPK-03]<br/><small>fetch success, bytes per / capture, pages per run</small>"):::evidence
  m2("Database activity [SPK-07]<br/><small>active hours per day / storage growth</small>"):::evidence
  m3("Parser failures [SPK-08]<br/><small>failure rate by source</small>"):::evidence
  m4("Model usage [SPK-08]<br/><small>attempts and tokens / including retries</small>"):::evidence
  m5("Human review effort [SPK-08]<br/><small>cases and minutes</small>"):::evidence
  unk("Unexercised path [UNKNOWN]<br/><small>conservative assumption / marked unknown, never 0</small>"):::unknown
  fc("Revised forecast<br/><small>SPK-09 · two sources</small>"):::state
  q{"Fits the budget?<br/><small>forecast ≤ 35, exposure / ≤ 40, reserve 10 kept</small>"}:::decision
  add(["Add second source<br/><small>within the envelope</small>"]):::ok
  red(["Reduce scope<br/><small>fewer listings, less / browser use</small>"]):::hold
  def(["Defer second source<br/><small>state the gap</small>"]):::stop
  m1 -.-> fc
  m2 -.-> fc
  m3 -.-> fc
  m4 -.-> fc
  m5 -.-> fc
  unk -.->|not zero| fc
  fc -->|compare| q
  q -->|fits| add
  q -->|smaller| red
  q -->|no fit| def
  classDef db stroke-width:2px
  classDef queue stroke-dasharray:6 3
  classDef storage fill:#eef1f4
  classDef llm stroke:#b7862b,stroke-width:2px
  classDef evidence stroke-dasharray:2 2
  classDef external stroke-dasharray:2 3
  classDef alt stroke-dasharray:4 4
  classDef gate stroke-width:2px
  classDef decision stroke-width:1.5px
  classDef ok stroke-width:1.5px
  classDef hold stroke-dasharray:4 3
  classDef stop stroke-width:2px
  classDef unknown stroke-dasharray:2 2
  classDef boundary stroke-dasharray:8 4,stroke-width:1.5px
```

<!-- /diagram:ex_measurements -->

| Spike | Question | Required inputs | Acceptance | Result that changes the approach |
|---|---|---|---|---|
| **SPK-01** (existing) | Does Hyperdrive plus the chosen driver keep AW-01 atomic, including row locks and constraint errors? | Hosted database, Hyperdrive binding, concurrent-decision harness, process kill during the transaction | No partial commits. A stale revision returns a conflict. No outbox row without its facts. Records transaction duration and connection wait | Any partial commit or lost lock → Node-container fallback (DJ-07). Domain code unchanged |
| **SPK-02** (existing) | Does the product consumer stay correct under duplicate, reordered and dependency-blocked events? | Prototype scenario-8 cases, product-ingestion queue | Projection equals the resolver export after every run. Records parked-event count and time | Duplicate handling turns out costly → job outbox for all jobs (ADR-009 alternative) |
| **SPK-05** (reduced, PROP) | Does a rebuild from snapshot plus retained tail equal the live projection, and does pruning respect an active rebuild lease? | Slice data, one forced pruning race | Rebuild equals live. No missed committed event. Records rebuild duration and retained bytes | Divergence → recovery redesign before M3. Rebuild time sets the DJ-03 retention window |
| **SPK-03** (recast) | Can the selected source be acquired reliably, and how large are the artifacts? | DJ-06 source and access terms, collector, about 200 listings, daily cadence | Measured **fetch success**, **extra-fetch rate**, **bytes per capture** (HTML, JSON, screenshots), **pages per run**. Browser seconds measured, or unknown if unused | Success below the 0.4.0 assumption, or unpriced browser or egress needs → re-price before M4, or reject the source |
| **SPK-07** (new) | How much database active time and storage does the real cadence use? | Provider usage export for compute hours and storage; ledger | Measured active hours per day against the assumed 16. GB growth per week | Close to always-on (the USD 40.84 sensitivity case) → batch collection into a window, or revisit topology/provider (DJ-01) |
| **SPK-08** (new) | What are the parser failure rate, model usage and human review effort? | Parser outcome counters; model-adapter token counts including retries; review-case timestamps | **Parser failure %**, **LLM attempts and tokens** (unknown if never needed), **review cases and minutes** | About 20% parser failure (USD 38.89, 15.68 h case) → fix the parser before adding sources, or reject the source. Review time above Jonathan’s availability → narrow the publication scope |
| **SPK-09** (new) | Does a second scraper fit the budget? | SPK-03/07/08 outputs, priced service inventory (§8) | Revised forecast, with each unknown priced at its conservative 0.4.0 assumption, never zero | **Fit rule:** add the second source only if the two-source forecast is ≤ USD 35 (discretionary-deferral threshold) and operating exposure ≤ USD 40, with USD 10 held in reserve. Otherwise reduce listings, rely less on the browser, or defer |

**Measurement record.** Each metric stores its value, sample size, window, source of truth (provider usage export or application counter) and status: `measured`, `estimated` or `unknown (unexercised)`. Unexercised paths are never recorded as zero cost.

## 5. Queue redelivery is not a paid attempt

**Figures:** [Queue redelivery versus paid attempts](../operations.html#redelivery) (`ex_redelivery`, guide figure `fig-redelivery`) · [Mermaid source](diagrams-mmd/20-ex_redelivery.mmd). The workbench screens `cost.html` and `implementation-plan.html` are not in this package. The three-delivery proposal and the five-attempt proposal stay unresolved.

<!-- diagram:ex_redelivery -->

```mermaid
flowchart TB
  job[/"Job delivered [DELIVERY +1]<br/><small>max 3 deliveries per job</small>"/]:::queue
  q{"Attempt on record?<br/><small>idempotent attempt id</small>"}:::decision
  reuse(["Reuse result<br/><small>settled attempt / no new provider call</small>"]):::ok
  adm[["Fresh admission<br/><small>quota + new reservation / must fit the envelope</small>"]]:::gate
  keep(["Keep liability<br/><small>uncertain timeout: / reservation stays open</small>"]):::hold
  def(["Defer<br/><small>no provider call / job parked</small>"]):::stop
  n_call("Paid provider call [PAID ATTEMPT +1]<br/><small>fetch, browser or LLM</small>"):::external
  set[("Settle actual charge<br/><small>replaces the reservation / exactly once</small>")]:::db
  job -->|look up| q
  q -->|settled| reuse
  q -->|none| adm
  q -->|uncertain| keep
  keep -->|retry| adm
  adm -->|admitted| n_call
  adm -->|rejected| def
  n_call -->|result| set
  n_call -->|timeout| keep
  classDef db stroke-width:2px
  classDef queue stroke-dasharray:6 3
  classDef storage fill:#eef1f4
  classDef llm stroke:#b7862b,stroke-width:2px
  classDef evidence stroke-dasharray:2 2
  classDef external stroke-dasharray:2 3
  classDef alt stroke-dasharray:4 4
  classDef gate stroke-width:2px
  classDef decision stroke-width:1.5px
  classDef ok stroke-width:1.5px
  classDef hold stroke-dasharray:4 3
  classDef stop stroke-width:2px
  classDef unknown stroke-dasharray:2 2
  classDef boundary stroke-dasharray:8 4,stroke-width:1.5px
```

<!-- /diagram:ex_redelivery -->

- **Transport deliveries** are counted per job id: at most three, acknowledged after commit, dead-lettered after that (ADR-009). A redelivery creates **no reservation and no permission to spend**.
- **Provider attempts** (fetch, browser session, LLM call) each need their own admission, reservation and quota increment, recorded in the ledger under an idempotent attempt id. A redelivered job first looks up its prior attempt:
  - if that attempt is **settled**, the job reuses its result;
  - if it is **uncertain** (timed out, no response), its reservation and liability **remain open until reconciled**, and any new attempt is a separate admission.
- **M1 checks:**
  - redelivery after a timed-out fetch makes zero new provider calls without a new admission;
  - the uncertain reservation still counts in exposure;
  - duplicate settlement cannot spend twice;
  - quotas count failed attempts.

## 6. Publication policy (proposal; refines DJ-10 and DJ-05)

**Figures:** [Publication decision flow](../follow-one-product.html#states) (`ex_publication`, guide figure `fig-states`) · [Mermaid source](diagrams-mmd/15-ex_publication.mmd). The workbench screens `data.html` and `implementation-plan.html` are not in this package.

<!-- diagram:ex_publication -->

```mermaid
flowchart TB
  pol("Policy v1 [PROPOSED]<br/><small>tenant · catalog · source / field allowlist · approver</small>"):::state
  in("Validated update<br/><small>accepted privately</small>"):::state
  q1{"In policy scope?<br/><small>tenant · catalog · source</small>"}:::decision
  q2{"Field allowlisted?<br/><small>e.g. price, availability</small>"}:::decision
  q3{"Identity change?<br/><small>remap, split or merge</small>"}:::decision
  q4{"Deterministic, agreed?<br/><small>no LLM claim, no conflict</small>"}:::decision
  s1(["Stay private<br/><small>outside approved scope</small>"]):::stop
  s2(["Stay private<br/><small>new field needs policy v2</small>"]):::stop
  h3(["Human review<br/><small>identity decision</small>"]):::hold
  h4(["Human review<br/><small>LLM-derived, low- / confidence or conflicting</small>"]):::hold
  ok(["Automatic release [ROUTINE]<br/><small>release applies, then the / public API can serve it</small>"]):::ok
  pol -->|defines scope| q1
  in --> q1
  q1 -->|no| s1
  q1 -->|yes| q2
  q2 -->|no| s2
  q2 -->|yes| q3
  q3 ==>|yes| h3
  q3 -->|no| q4
  q4 ==>|no| h4
  q4 -->|yes| ok
  classDef db stroke-width:2px
  classDef queue stroke-dasharray:6 3
  classDef storage fill:#eef1f4
  classDef llm stroke:#b7862b,stroke-width:2px
  classDef evidence stroke-dasharray:2 2
  classDef external stroke-dasharray:2 3
  classDef alt stroke-dasharray:4 4
  classDef gate stroke-width:2px
  classDef decision stroke-width:1.5px
  classDef ok stroke-width:1.5px
  classDef hold stroke-dasharray:4 3
  classDef stop stroke-width:2px
  classDef unknown stroke-dasharray:2 2
  classDef boundary stroke-dasharray:8 4,stroke-width:1.5px
```

<!-- /diagram:ex_publication -->

This policy builds on `publications:write`, release allowlists and `release_sequence`.

- **Policy record.** Versioned, with a `policy_id`. Its scope is (tenant, catalog, source, field allowlist; for example `price`, `availability`, `observed_at`). It records the approver and the approval time.
- **Automatic release only when all of these hold:**
  - the claim came from a deterministic parser and passed validation;
  - the field is on the allowlist;
  - the listing→item mapping is unchanged;
  - item identity is unchanged.
- **Human review required for:**
  - a listing remap, or any split or merge (the `rc_0004` class);
  - any claim derived from the LLM or parser fallback;
  - low-confidence or conflicting claims;
  - activation of identity attributes.
- **Never implied:**
  - a new field, source or catalog requires a new policy version;
  - approving v1 does not cover fields added later, even if they are parsed and accepted privately;
  - accepted but out-of-scope data stays private.
- **Checks:**
  - an in-scope price change releases automatically;
  - a new field `roast_date` stays private;
  - a 2×250 g → 500 g remap opens a review case;
  - a source added after v1 releases nothing.

## 7. Decisions before M1

1. **DJ-02:** relational state with a transactional outbox, which the M1 schema assumes.
2. **DJ-09:** tenant-private catalogs, which fix the key shape.
3. **DJ-10 / §6:** adopt the scoped-policy model, and decide who holds `publications:write`.
4. **SPK-05 scope:** reduced (rebuild equality plus one pruning race; restore/epoch cases deferred until DJ-04 sets recovery targets) or full.

**Decided later, at their gates:**
- before M2: DJ-01 and DJ-07;
- before M3: DJ-06 plus written access terms, and DJ-05;
- after M3: SPK-04 latency targets and DJ-03 retention.

## 8. Pricing and measurement tasks before paid deployment

- **P1 Plan requirements.** Confirm from current provider pages which services (Queues, Hyperdrive, Browser Run) require Workers Paid, plus their allowances and billing increments. Recheck rather than reuse the 0.4.0 assumptions.
- **P2 Database.** Quote the database plan for 0.25 CU, scale-to-zero behaviour, storage, and the backup/PITR level DJ-04 needs.
- **P3 Shared allowances.** Confirm the account’s included usage is not already used by other apps. The no-allowance case (USD 44.18) exceeds the envelope.
- **P4 Other required services.** Itemize authentication, logs, monitoring, domain, tax and FX. The USD 2 provision is unverified, and any unpriced required service blocks admission.
- **P5 Usage reporting.** Confirm each provider’s usage-reporting delay. The ledger stops new paid work when reports are more than 24 hours old.
- **P6 Billing alerts.** Configure provider billing alerts at USD 35 and USD 40. There is no verified hard cap.
- **P7 Reconciliation.** Daily ledger reconciliation, tested against fake provider usage in M1.

## 9. Deferred capabilities

| Deferred | Revisit when |
|---|---|
| Second scraper | SPK-09 fit rule passes |
| Browser Run or regional host | The selected source measurably needs rendering or country-specific egress |
| LLM extraction and review | Measured parser failures are material and the extra cost fits the proposed USD 40 operating envelope while preserving the USD 10 uncertainty reserve |
| Search index, D1, separate projection store | SPK-04 misses the targets Jonathan sets |
| Full SPK-05 (restore and epochs) | DJ-04 sets recovery targets (RPO/RTO) |
| Shared catalogs | A real collaboration need exists (DJ-09) |
| Full event sourcing | Reconstruction needs exceed evidence, decisions and revisioned state |
| Separate database instances | Role or failure isolation fails, or load requires it |
| Applied replay beyond deterministic dry runs | A parser change requires it, within quota |
