[object Object]

← back to Wallco Ai

docs: TIF publish-gate review — rule unenforced + contradicts reality (3632/4936 published rows have NULL tif_path; download path uses upscale not tif). Options A-D + recommendation (soft-flag trigger + correct CLAUDE.md), deferring hard gate.

0f9c57af4b53e39390f2c26fcc4385ac5ea67252 · 2026-06-02 08:12:32 -0700 · Steve Abrams

Files touched

Diff

commit 0f9c57af4b53e39390f2c26fcc4385ac5ea67252
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 2 08:12:32 2026 -0700

    docs: TIF publish-gate review — rule unenforced + contradicts reality (3632/4936 published rows have NULL tif_path; download path uses upscale not tif). Options A-D + recommendation (soft-flag trigger + correct CLAUDE.md), deferring hard gate.
---
 docs/tif-publish-gate-review.md | 104 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 104 insertions(+)

diff --git a/docs/tif-publish-gate-review.md b/docs/tif-publish-gate-review.md
new file mode 100644
index 0000000..1c1df7f
--- /dev/null
+++ b/docs/tif-publish-gate-review.md
@@ -0,0 +1,104 @@
+# TIF Publish-Gate — Review & Proposal
+
+**Status:** FOR REVIEW — nothing applied. No schema change made.
+**Date:** 2026-06-02
+**Trigger for this doc:** 17 dog patterns found published with `tif_path = NULL`; asked "how did they slip through." The answer turned out to be a catalog-wide pattern, not an isolated bug.
+
+---
+
+## 1. The documented rule
+
+CLAUDE.md, "Hard rules baked into this project":
+
+> **TIFs are full-size archives.** Every design has a `tif_path` (150 dpi, up to 36000 × 19800 px, ~2 GB). **The publish gate refuses NULL `tif_path`.** Thumbs online, real file must exist.
+
+Also `server.js:1816` (comment): *"TIF archive still required before any is_published=TRUE."*
+
+## 2. Reality — the rule is not enforced and contradicts actual practice
+
+### 2a. There is no enforcement anywhere
+- **No DB trigger / CHECK constraint** requires `tif_path` on publish. The *settlement* gate IS a real trigger (`settlement_publish_check BEFORE UPDATE OF is_published ON all_designs`) — the TIF half was never given the same treatment.
+- **No publish code path checks it.** Every `is_published=TRUE` write is `UPDATE spoon_all_designs SET is_published=TRUE ...`.
+- **The publish path structurally cannot check it.** `spoon_all_designs` is a **VIEW** over the real `all_designs` table, and the view **does not expose `tif_path`**. So even a deliberate `WHERE tif_path IS NOT NULL` is impossible through the path the app actually uses.
+
+### 2b. The rule contradicts the catalog at scale
+Query against local `dw_unified.all_designs`:
+
+| metric | count |
+|---|---|
+| published designs | **4,936** |
+| published **with NULL `tif_path`** | **3,632 (73%)** |
+| of those, with a vector-SVG fallback | **0** |
+
+The 17 dogs were not special — they are 17 of **3,632**. Top NULL-tif published groups: drunk-animals (735), stoned-animals (610), chinoiserie (497), muybridge-plate (306), monterey-mural (285)… i.e. the core of the live catalog.
+
+### 2c. TIF is decoupled from customer fulfillment
+The digital fulfillment path does **not** read `tif_path`:
+- `/api/design/:id/license/checkout` (server.js:4350) — Stripe checkout; never references tif.
+- `/api/design/:id/download` (server.js:4425) — delivers `design_licenses.upscaled_path` (an on-demand **Real-ESRGAN x4** upscale of the SDXL native), falling back to the raw `local_path` native PNG. **The 2 GB TIF is never delivered here.**
+
+So a NULL `tif_path` does **not** make a design "un-orderable" — customers can still buy and download via the upscale path. The TIF archive exists for a *different* purpose (large-format / physical-roll / Spoonflower-grade print masters), which is not gated by `is_published`.
+
+> ⚠️ **Correction to the dogs action:** the earlier framing that the 17 dogs were "un-orderable" was wrong — they were deliverable via the upscale path like the other 3,632 NULL-tif published rows. Unpublishing 17 of 3,632 is inconsistent. See §5 for the recommended reconciliation (re-publish or define a real policy first).
+
+## 3. Why it ended up this way (not negligence)
+- TIFs are ~2 GB each; Mac2 disk runs hot (`scripts/export-etsy-digital.js:33`: *"NO TIF gen by default — Mac2 disk runs hot"*). TIF generation is intentionally **opt-in / lazy**.
+- The curate-then-publish workflow (hot-or-not "HOT → is_published=TRUE") publishes immediately on the curator's pick; TIFs are built later (or never) by `build-tif.py` / `archive-svg-5400.js`.
+- So "publish" came to mean "visible in catalog," while "has a print master" became a separate, lazily-satisfied property. The docs never caught up.
+
+## 4. Options
+
+### Option A — Literal hard gate (block NULL-tif publish)
+Add tif enforcement to the publish trigger. **NOT RECOMMENDED.**
+- Would halt re-publish of any of the 3,632 existing NULL-tif rows (unpublish→edit→republish is a normal curation move).
+- Would block the entire curate-then-publish workflow unless every operator sets the override flag on every publish — which defeats the gate.
+- Enforces a guarantee that does not gate the actual purchase/download flow.
+
+```sql
+-- Option A DDL (for reference only — do NOT apply without resolving the 3,632 backlog)
+-- Extend settlement_publish_check (or a sibling) after the override short-circuit:
+IF NEW.tif_path IS NULL THEN
+  RAISE EXCEPTION
+    'TIF gate: refusing to publish design id=% with NULL tif_path. Build the TIF first '
+    '(scripts/build-tif.py %), or override: SET LOCAL settlement.allow_override = ''true'';',
+    NEW.id, NEW.id;
+END IF;
+```
+
+### Option B — Move the hard requirement to the real fulfillment point
+Leave publish alone; gate **delivery of a print-grade master** where it actually matters (physical-roll / Spoonflower push, if/when that path exists). The digital `/download` route already degrades gracefully (upscale → native), so no customer-facing break. **Recommended if a physical-print fulfillment path is in scope.**
+
+### Option C — Soft trigger: flag, don't block (RECOMMENDED for now)
+On the false→true flip, if `tif_path IS NULL`, **stamp a note + set a visibility flag** instead of raising. Gives curation visibility ("this is live without a print master") without breaking the workflow. Pair with an admin "needs-TIF" queue ordered by recency.
+
+```sql
+-- Option C DDL — soft flag, non-blocking. Add a nullable column first:
+--   ALTER TABLE all_designs ADD COLUMN IF NOT EXISTS tif_missing_at timestamptz;
+-- Then, inside the publish trigger AFTER the override short-circuit and BEFORE settlement checks:
+IF NEW.is_published IS TRUE AND OLD.is_published IS DISTINCT FROM NEW.is_published
+   AND NEW.tif_path IS NULL THEN
+  NEW.tif_missing_at := now();
+  NEW.notes := COALESCE(NEW.notes || E'\n','') ||
+    'tif-gate: published without a print master (tif_path NULL) at ' || now()::text;
+END IF;
+-- Admin queue: SELECT id, category, tif_missing_at FROM all_designs
+--   WHERE is_published AND tif_path IS NULL ORDER BY tif_missing_at DESC NULLS LAST;
+```
+
+### Option D — Retire the rule; correct the docs
+Decide the TIF archive is an **archival/print-master nicety, not a publish prerequisite**, and update CLAUDE.md to say so (publish = catalog-visible; print master is built lazily and gated only at physical-print fulfillment). Cheapest; removes a false invariant that has misled at least one investigation (this one).
+
+## 5. Recommendation
+1. **Adopt Option C now** (soft flag) + an admin "needs-TIF" queue — restores honesty to the gate without breaking the 73%.
+2. **Adopt Option D in parallel** — fix CLAUDE.md so the documented rule matches reality (publish ≠ has-TIF).
+3. **Defer Option A/B** until there's a real physical-print fulfillment path that genuinely needs a TIF master; gate it *there*, not at catalog publish.
+4. **Reconcile the 17 dogs** — either re-publish them (they're no more broken than the other 3,632) or, if we adopt a real "no-tif → not-live" policy, apply it to all 3,632 consistently, not just the dogs. Audit log: `data/unpublish-no-tif-dogs-20260602T150224Z.jsonl`.
+
+## 6. Rollout / rollback notes (when something IS chosen)
+- **Prod is source of truth** for schema (Kamatera `dw_admin` owns the table). Apply via `sudo -u postgres psql -c "..."` then `pm2 restart wallco-ai-9878` to refresh the in-memory cache.
+- **⚠️ Kamatera is the box with the active, unremediated root-backdoor compromise** (see `project_kamatera_pakchoi_root_backdoor` memory). Do **not** apply any prod-schema change there until IR + secret rotation is done.
+- Rollback for a trigger: `DROP TRIGGER`/restore the prior `CREATE OR REPLACE FUNCTION settlement_publish_check` body (current body archived in this repo's git history / the settlement skill `binding/`).
+- Soft-flag column (Option C) is additive and reversible: `ALTER TABLE all_designs DROP COLUMN tif_missing_at;`.
+
+---
+*Prepared by Claude. Investigation only — DB unaffected except the 17-dog unpublish already performed and logged.*

← 4ef783f TIF resolution: PG-independent disk lookup by design_<id>.ti  ·  back to Wallco Ai  ·  TIF lookup: match both <id>.tif and <category>/design_<id>.t c9f7813 →