ADR-NY-001 — Net Yield backend scaffold
Status: accepted (scaffold) · Date: 2026-08-07 · claim_label: built, pre-benchmark
Deploy state: deployed via human dispatch (amended 2026-08-19: first
green dispatch run 31221912948 on 2026-08-07, rev net-yield-00005-cgc;
registry row deployed-dispatch). Deploys ONLY via human dispatch: there is no
push-triggered workflow for services/net-yield/**, so bot merges deploy
nothing; the paths to a serving revision are the dispatch-only
.github/workflows/deploy-net-yield.yml (added 2026-08-07 on owner
instruction; DDL + build/deploy + fail-closed smoke) and the operator RUNBOOK
(docs/net-yield/RUNBOOK.md), which stays authoritative for IAM grants,
extender feed, scheduler, and writeback enablement.
Context
The Signal v2.1 program (master checklist / drop/claude-code-prompt-signal-net-yield-build-v2.md,
Phase 4) requires the "Net contribution, not top line" roadmap card to be
backed by real code — order economics from real cost inputs, nightly net
contribution per order and cohort, a serving API, and value-writeback stubs —
while the customer-facing label stays "Preview · in development" until a
pilot writes verified numbers (positioning doc claim ledger).
Decisions
- New bounded service
services/net-yield/, not an in-place extension of a deployed service. The Deploy Router redeploys any service whosedeploy-*.ymlpush-paths match a merged diff. Touchingservice-marketing-connectors/**orsrc/shared/virtuoso_models/**(Boss + coding-MOA images) would redeploy live services on merge — violating this build's "nothing live" rule. A new directory matches no workflow; the spec's "new service or extension of the Financial cell" option made this the honest choice. (The positioning doc's "extension of Cell 35 + Financial cell" reads as capability lineage; runtime-wise the fleet has no standalone financial cell to extend without a live redeploy.) - The live Shopify intake is
intent-shopify-extender; it gains a default-off forward. That service is the platform's real Shopify webhook receiver (HMAC-authed, fail-closed) and has no push-path deploy workflow (intent-family deploys are dispatch-only), so an additive change is deploy-inert. WithNET_YIELD_INGEST_URLunset (the default), behavior is byte-for-byte unchanged; when the operator sets it, order/refund payloads are ALSO copied to net-yield over OIDC, fire-and-forget — a forward failure can never fail the webhook or perturb the Cell 33 path.service-marketing-connectorsremains the strategic gateway; when its Shopify order sync goes live, it should feed the same/v1/ingest/shopifycontract and the extender forward retires (migration note below). - One envelope, no schema fork. Order economics ride in
CanonicalEventEnvelope.payload["order_economics"](the constitution's shape registry, imported and validated at ingest) — no second envelope, no change to the shared JourneyEvent schema (whosesrc/shared/**home would redeploy Boss/MOA on merge). Follow-up, to batch with the next shared- package change: addshopifyto the JourneyEventevent_sourceenum and a typed OrderEconomics block tomizoki_contracts(a migration per GOVERNANCE Art. 5.4 — additive optional field, dual-readable). - Dataset reality:
mizoki_unified_data, notunified. The spec namedunified.order_economics; the platform's revenue dataset ismizoki_unified_data(seeunified_revenue.sql,unified-revenue-ingestion). Reality wins:mizoki_unified_data.order_economics,.net_contribution,.net_contribution_cohort, partitioned/clustered per the existing conventions. /health, not/healthz. The spec asked for/healthz; on run.app the GFE reserves/healthz, so the fleet serves/health(OPERATING_SYSTEM 2.3.3). The service follows the fleet.- No invented economics — structural, not aspirational. Costs come only
from
config/net_yield_costs.yaml(strict schema, unknown keys rejected). A missing cost is NAMED inmissing_costs, flipseconomics_complete=false, keepsnet_contributionNULL, and excludes the row from cohort aggregates (incomplete_excludedmakes the exclusion visible). Writeback builders refuse incomplete rows outright. - Expected returns are earned, not assumed. Per-SKU return rates enter
the formula only after a SKU shows ≥
return_cycle_daysof history (tenant-configured; 365-day OFF-window when unconfigured). Before that, contribution is actual-only and labeledreturn_basis=actual_only. The formula is documented incompute.pyand mirrored in the nightly SQL. - Cohorts, v1:
tenant|channel|YYYY-MM, channel from landing-site UTM →source_name→unattributed. Deliberately coarse; the pilot defines the next granularity (e.g. joins to attribution edges). Never guesses a channel not present in the payload. - PII minimization + consent stance. Economics rows carry the
deterministic
shp_<id>/ hashed anonymous key only — never email, phone, name, or address. Order economics is first-party transactional bookkeeping (same class asunified_revenue), not behavioral intent modeling, so it is not gated on marketing opt-in; the intent path's consent gate is untouched. Probabilistic keys stay out of causal math (platform §8.4). Erasure cascades reach these tables via the RUNBOOK's GDPR section. - Writeback is hard-flagged OFF and L1 recommend-only.
NET_YIELD_WRITEBACK=falseis baked into the image env and cloudbuild;/healthreports it; a unit test pins the default;send_*raisesWritebackDisabledwhile off. No actuator is registered with the action-runner, so even an enabled flag cannot fire autonomously (two-key rule) — enablement conditions live in the RUNBOOK §8 and flip the positioning claim ledger with owner approval.
Consequences
- Merging this scaffold to
mainchanges no running service and serves no new surface: the site deploys only via the humanAPPROVEDdispatch, the net-yield service has no workflow, and the extender change is inert until its env var is set (its intent-family deploys are dispatch-only anyway). - Tests: 59 service tests + 13 extender tests, offline, fakes only;
business-logic coverage ≥83% per module (compute 100%).
bq.py's GCP branch and the real BigQuery/Scheduler/IAM path are exercised only at operator deploy time — per the platform honesty note, nothing here is claimed live until verified against a serving revision. - Go-live still owes the connector production gates (service registry entry,
runtime SA, OIDC audience checks, DLQ/reconciliation, tenant-scoped
observability, backfill watermarks —
.claude/memory/active/marketing-commerce-connectors.md). The scaffold's status at acceptance wasimplemented; superseded by the dispatch runs recorded in the header — the registry row isdeployed-dispatchon that run evidence (amended 2026-08-19).
Cross-links
docs/net-yield/RUNBOOK.md (operator commands) ·
# MIZ OKI 3.5/docs/marketing/mizoki-shopify-net-yield-positioning.md
(claim ledger — the Preview label flips only after a verified pilot) ·
# MIZ OKI 3.5/docs/marketing/signal-story-bank.md v1.1 (Story 7) ·
# MIZ OKI 3.5/scripts/content_qa.py (net-yield preview framing enforced in CI).