Canonical-store erasure leg — measurement and verdict (2026-08-12)
Question: does the canonical side expose a subject-erasure surface the gateway's compliance runner could call today — or does Cell 33's erasure cascade already cover connector-landed canonical data?
Verdict: NEITHER — branch (c). No code changed in this landing. The
canonical_store_erasure leg stays honestly pending_in_build and keeps
customers_redact jobs OPEN (rule 01: fail-closed needs somewhere to close
onto; wiring a pretend-eraser would be worse than the open job). The minimal
canonical-service change is proposed below for that service's own lane.
Everything below is measured (file:line, this tree at 4f5a0d4bc);
nothing is live-verified.
1. No erasure surface exists on the canonical service — branch (a) is out
services/service-canonical-ingestion/holds exactly one file,main.py(209 lines). Its complete route table:GET /healthz+GET /health(main.py:87-94),GET /readyz(main.py:97-117),POST /api/v1/ingest(main.py:147-187),GET /api/v1/events/point-in-time(main.py:190-203). No DELETE route, no subject/erasure/redact surface of any kind.grep -rn -i "erasure|redact|subject" services/service-canonical-ingestion/→ zero matches (re-measured 2026-08-12; independently confirms the round-3 record anddocs/roadmap/P1_BUILD_PLAN.md:97-102).- Same grep over
contracts/mizoki_contracts/→ zero matches. Deeper: the sharedStore(contracts/mizoki_contracts/store.py:41-233) exposesput/transact_write/get/query/query_events/list_allplus the append-only audit chain — no delete primitive exists at the store layer at all, and the outbox (contracts/mizoki_contracts/ outbox.py) only moves rows pending→published→dead. An erasure surface is not merely unrouted; its substrate operation does not exist yet.
2. Cell 33's cascade does NOT cover the canonical store — branch (b) is out
DELETE /v1/identity/{identity_id}(src/cells/cell33/ingest_cell/ main.py:278-317) →erase_identity(src/cells/cell33/ingest_cell/ orchestrator.py:483-492) → eraser overIDENTITY_LINKED_TABLES(src/cells/cell33/ingest_cell/storage.py:493-500):intent_signals,intent_outcomes,intent_scores,intent_transitions,intent_training_examples,intent_holdouts— plus the Cell 35 graph leg (storage.pyGraphEraser; erasure-mode config at src/cells/cell33/ingest_cell/config.py:55-76).- Deliberate non-targets:
ERASURE_EXCLUDED_TABLES(storage.py:504-517) =intent_consent_denials,intent_taxonomy_dim. - No leg names the canonical stream's stores: not the Firestore
eventscollection (the envelope landing — contracts/mizoki_contracts/outbox.py:34-88, written by services/service-canonical-ingestion/main.py:168-173), notevents_outbox/events_dlq, not anymizoki_unified_data.*table.grep -rln unified_revenue src/cells/cell33 src/cells/cell34 src/cells/cell35 src/cells/cell36→ zero files. - This scope is by design, not omission: "Cell 33 owns the erasure path because it owns the consented write door" (storage.py:488-492) — the cascade erases the intent family. Connector-landed canonical envelopes never pass through Cell 33's stores, so the cascade cannot reach them.
- Adjacent precedent pointing the same way: net-yield's canonical-adjacent tables get erasure only as manual operator SQL (docs/net-yield/RUNBOOK.md:161-173).
3. Current gateway posture (correct today, unchanged by this landing)
POST /api/v1/shopify/compliance/run
(services/service-marketing-connectors/main.py:1137-1174) invokes
process_compliance_job without a canonical_eraser
(main.py:1152-1159), so the third customers_redact leg reports
pending_in_build and the job stays OPEN and visible
(services/service-marketing-connectors/shopify_install.py:949-1011; leg
order is LAW per the §6 comment at shopify_install.py:966-969; the
pending_in_build branch at :986-990). Test-asserted in both suites:
tests/connectors/test_shopify_oauth_gateway.py:1078 and
services/service-marketing-connectors/test_shopify_install.py:655.
The same injectable pattern already covers the sibling gaps
(shop_eraser for tenant-wide erasure, exporter for
customers/data_request — both also None today).
4. Proposed minimal canonical-service change (its own lane — NOT built here)
No side door: the gateway must never write BigQuery/Firestore deletes itself. The erasure leg needs the canonical service to grow a governed subject-erasure surface, roughly:
- A store deletion/redaction primitive in
mizoki_contracts.store(none exists — §1), tenant-scoped, audit-chained (an erasure is itself an auditable action; the append-only audit chain rows reference event_ids, not payloads, so the chain survives). POST /api/v1/subjects/eraseonservice-canonical-ingestion(IAM-locked viaverify_caller, tenant viaresolve_tenant), taking the compliance job's identifiers (subject_key,order_ids, entity ids). Envelope matching is feasible today: gateway-landed records carry customer/order ids inentity_ids(services/service-marketing-connectors/main.py:411-444 sync, :1091-1093 webhook) which persist on the envelope (contracts/mizoki_contracts/envelope.py:58).- All three copies: the
eventscollection,events_outbox, andevents_dlqrows carry the full envelope/envelope_json(outbox.py:73-88) — an eraser that misses the outbox leaves the PII in a replayable row. Any future BigQuery landing joins this list. - Redact-vs-delete is a design decision for that lane: envelopes are
content-addressed (
event_id = stable_hash(source, payload_hash, tenant), envelope.py:112-114) and the platform promises deterministic replay, so in-place redaction breaks hash integrity while deletion breaks replay completeness — the canonical lane must choose (e.g. a bitemporal redaction event + payload tombstone) rather than have the gateway improvise it. - Per-leg receipts, fail-closed: 2xx only when every matched row is confirmed gone/redacted; anything unconfirmable is a non-2xx so the gateway job stays open (the Cell 33/35 receipt pattern).
Once that surface exists, the gateway side is the small, already-designed
branch (a): an OIDC canonical_eraser client following the existing
outbound pattern (outbound_headers, cf. _cell33_cascade
main.py:1114-1130), injected in the job-runner wiring behind
CANONICAL_ERASER_URL (unset ⇒ leg stays pending_in_build, never
fake-confirms; only a 2xx confirming receipt marks the leg confirmed),
with both-direction tests.
Coordinator decision requested: register the §4 build on the canonical
service's lane; the gateway leg stays pending_in_build until then.