OPT-GOV caller wiring — growth-scheduler built DARK + ONNX exporter

Branch: claude/opt-gov-phase5-caller-wiring (+ infra/growth-scheduler-deploy for the protected-path workflow) · Session: 7pjhmg · Date: 2026-08-22 Authority: owner directive "I want all the lanes live" + in-session ruling "Build it all, dark" (tenant scope: mycocoons first). Standing approved:commit/merge/deploy tokens. Every lane flag remains false; nothing here arms anything.

What "live" required, and what this build supplies

The four OPT-GOV library lanes shipped with injected seams and no production caller — by spec. This build supplies the caller while preserving every posture: services/growth-scheduler, an IAM-locked FastAPI service (verify_caller + tenant resolution on every route) that is dark by construction — observable and side-effect-free until the owner arms values:

Route Lane Dark behavior (all pinned by tests)
POST /api/v1/f4/schedule geo-SCM scheduler Config from config/f4_geo_candidates.yaml (mycocoons nulls ⇒ the engine's own refused/not_configured); observations from the declared F4_OBSERVATIONS_BQ_TABLE (unset ⇒ named 422, never a default); cell36 holdout registration through the real client (CELL36_URL; the library's holdout-before-submit order holds); DCP submission via DCP_URL. F4_CALIBRATION dark ⇒ the library's flag_dark record — no holdout registered, nothing submitted (zero-side-effect pinned).
POST /api/v1/f4/mmm-propose MMM priors Caller declares the starting prior; posterior built only from stored CLEAN cycles; zero clean cycles ⇒ honest 409 unearned_posterior. MMM_PRIOR_WRITEBACK dark ⇒ shadow, payload withheld.
POST /api/v1/f2/blend LTV blend Curves from the declared F2_ORDER_ECONOMICS_BQ_TABLE (unset ⇒ named 422); candidate regimes stated by the caller (business hypotheses are PLAN inputs — never invented); data_insufficient passes through unsoftened; LTV_BID_WEIGHTING dark ⇒ shadow record, blended_bid_value withheld.
POST /api/v1/edge/distill edge distillation Parent scoring batch supplied by the caller (no scoring source fabricated); caller-declared fidelity floor; refusals persist as evidence without the tree; provenance vs the caller-declared current registry parent; EDGE_INFERENCE_SERVE dark ⇒ shadow offer, registration request withheld.

Durable persistence: DurableCalibrationStore implements the F4 store seam over the governed mizoki_contracts.STORE, with the seam's own person-data-free guard imported (one guard, two homes impossible). New collections f4_mmm_proposals, f2_bid_weight_records, edge_distillation_records are posture-registered (ledger-retention, guard-enforced).

Cell36 seam, measured contract: the client maps to the shipped POST /v1/causal/holdouts:assign {tenant_id, units:[{unit_kind: "geo", unit_id}]} shape and reads assignments[]; unset URL / non-200 / empty assignments return registered: False with the reason named — surfacing as the scheduler's holdout_not_registered refusal, never a 500.

ONNX exporter (scripts/export_distillate_onnx.py): the operator step every registration offer names. Deliberately split — tree_to_onnx_spec is pure stdlib (TreeEnsembleRegressor lists, BRANCH_LEQ mirroring predict's goes-left rule) and pinned against distiller.predict on a probe grid without the onnx dependency; serialization imports onnx lazily (absent ⇒ named refusal, nothing written; present ⇒ checker-validated bytes + printed sha256 for the signing step the boss registry requires). Real round-trip tested with onnx installed.

Deploy path (.github/workflows/deploy-growth-scheduler.yml, dispatch-only, rides a protected-path review PR — the registry row says PENDING and status: built until the first successful run, per rule 04): gate = the new suites; build from repo root (image bundles shared libs + contracts + config templates); --no-allow-unauthenticated; env-replace semantics with the SELF_URL fold-in (the PR #770 lesson) and pinned config-path envs; probe = authenticated-as-allowed-caller /health reporting the dark posture plus the dark 422 observations_source_not_configured itself — the assertion only the correctly-deployed, dark service satisfies (401/403 = auth broken; 200 = something armed). Cloud Scheduler cadence stays an operator wiring (terraform is protected).

What now separates each lane from LIVE (the owner's part, unchanged)

  1. F4: real geos + spend_cap_usd_per_cycle + perturbation_pct_bounds for mycocoons in config/f4_geo_candidates.yaml; point F4_OBSERVATIONS_BQ_TABLE at a view with tenant_id, geo, observed_on, value; set CELL36_URL/DCP_URL; flip F4_CALIBRATION=true — all via the workflow env list (PR #730 pattern).
  2. F2: real ltv_blend_weight; F2_ORDER_ECONOMICS_BQ_TABLE (the shipped order_economics DDL — 0 live rows today, so findings stay data_insufficient until ≥ 2 closed quarters exist); LTV_BID_WEIGHTING=true.
  3. MMM: clean cycles must exist first (F4 live is the prerequisite); then MMM_PRIOR_WRITEBACK=true.
  4. Edge: run the exporter on a distillate, sign the bytes, register via the boss lane; EDGE_INFERENCE_SERVE=true.
  5. Supply veto (no caller needed — policy-engine already serves it): real values in config/supply_veto.yaml + image mount + SUPPLY_VETO_CONFIG_PATH + /reload.

Gates (pre-push, fresh venv)

growth-scheduler + exporter suites 25 + 6 (1 designed complementary skip) · full governance suite green (see commit line) · audit 0/14 · both parity checkers OK · canon-status fresh · V1–V3 0 in-diff · registry row honest (built, iam-intended, aspirational deploy path named as such) · no lane flag created or flipped.

← All docsView source on GitHub →