Platform Services¶
Persistent workbench state shared by the agents and the API server: the sessions store (SQLite), run-metric history, and read-side services. Dependency-light by design (no web framework).
Platform services: persistent workbench state shared by the agents and the API server.
This subpackage is the dependency-light persistence/service layer that used to
live in mmm_framework.api next to the FastAPI app. The web app itself moved
to the separate mmm-framework-server package (server/ in the repo);
what remains here is business state, not web routing:
sessions— the sessions store (SQLite): orgs/users, projects, chat sessions, model runs, artifacts, the experiment lifecycle registry, preferences/branding.history— per-run metric persistence.runs,pacing,scorecard,triangulation,estimands,portfolio_benchmark— read-side services over that state.backfill,backup,connection_sync,onboarding,observability— maintenance / integration helpers.
Nothing in this subpackage imports FastAPI or the LLM stack at module level;
it must stay importable in a lean pip install mmm-framework environment.
Note: platform shadows nothing for external code — absolute imports mean
import platform anywhere still resolves to the stdlib module.
Sessions¶
Session metadata store + per-session artifact log.
The langgraph SqliteSaver owns its own tables for checkpoints. We add two sibling tables in the same DB file:
sessions(thread_id PK, name, created_at, updated_at) artifacts(id PK, thread_id FK, kind, payload_json, created_at)
kind is one of: ‘code_snippet’, ‘report’, ‘plot’, ‘saved_config’, ‘saved_model’. payload_json is whatever the frontend needs to render or rerun the artifact (e.g. {“code”: “…”} for code_snippet).
- mmm_framework.platform.sessions.resolve_db_path()[source]¶
Resolve the sessions DB location.
MMM_SESSIONS_DBoverrides the package-local default so deployments can keep state on a persistent disk without symlinking into the install tree (see deploy/gcp/vm/vm_setup.sh). The server’s async checkpointer (mmm_framework_server.main) andauth/store.pyhonor the same variable — all three must point at the same file.Before this module moved from
mmm_framework.apitommm_framework.platform(2026-07-24), the package-local default lived in the oldapi/directory. An existing dev DB at that legacy path keeps winning so the move does not silently orphan local sessions/projects.- Return type:
- mmm_framework.platform.sessions.get_session(thread_id)[source]¶
Return a single session row with artifact_count, or None if not found.
- mmm_framework.platform.sessions.create_session(name=None, project_id=None, modeling_mode=None)[source]¶
- mmm_framework.platform.sessions.update_session(thread_id, name=None, project_id=None, modeling_mode=None)[source]¶
Update session name, project_id and/or modeling_mode. Returns True if found.
- Return type:
- mmm_framework.platform.sessions.touch_session(thread_id)[source]¶
Bump updated_at; if the row doesn’t exist (legacy thread), insert it.
- Return type:
- mmm_framework.platform.sessions.update_artifact_payload(artifact_id, payload)[source]¶
Overwrite an artifact’s payload (used by the async experiment-simulation job to flip status pending → running → done/error in place).
- mmm_framework.platform.sessions.record_assumption(thread_id, key, value, rationale, category='other', change_note=None)[source]¶
Create or update an assumption. New version always appended; history preserved.
- mmm_framework.platform.sessions.retract_assumption(thread_id, key, reason)[source]¶
Mark an assumption as retracted (tombstone); preserved in history.
- mmm_framework.platform.sessions.list_assumptions(thread_id, include_history=False)[source]¶
If include_history is False, returns the latest version per key. Otherwise returns every row ordered by (key, version).
- mmm_framework.platform.sessions.register_file(thread_id, path, name, kind, size_bytes=None, preview=None, meta=None)[source]¶
- mmm_framework.platform.sessions.lock_analysis_plan(thread_id, name, payload)[source]¶
Snapshot the current analysis plan (research question + DAG + assumptions) as a locked record.
- mmm_framework.platform.sessions.list_analysis_plans(thread_id=None)[source]¶
List analysis plans for a thread (or all plans if thread_id is None).
- mmm_framework.platform.sessions.get_analysis_plan(plan_id)[source]¶
Get a single analysis plan by ID.
- mmm_framework.platform.sessions.upsert_experiment(*, experiment_id=None, project_id=None, thread_id=None, channel=None, subchannel=None, design_type=None, status=None, start_date=None, end_date=None, estimand=None, value=None, se=None, notes=None, recommending_run_id=None, calibrated_run_id=None, design=None, readout=None, priority=None, allow_calibrated_edit=False)[source]¶
Create (no
experiment_id) or partially update an experiment record.On update, only the non-None fields change. Raises ValueError for an unknown id, a missing channel on create, or an invalid status.
Status is state-machine enforced: creating directly in ‘calibrated’ is rejected (calibration happens via
transition_experiment()at fit close-out); creating in planned/running/completed stays legal (historical import) but the full draft→…→status history chain is backfilled. An update may change status to anything REACHABLE throughALLOWED_TRANSITIONS— multi-hop moves (e.g. planned→completed, the one-call results-recording flow) backfill the intermediate hops into the history — EXCEPT ‘calibrated’, which is only ever set bytransition_experiment()(fit close-out). Same-status (orstatus=None) updates stay silent no-ops for the other fields.A calibrated experiment’s measurement fields (value / se / estimand / start_date / end_date / readout / channel / subchannel) feed the model’s calibration likelihood, so CHANGING any of them raises unless
allow_calibrated_edit=True(therecord_experiment_readouttool’s sanctioned, audited overwrite path). Unchanged re-sends and notes/design/priority-only updates stay allowed.
- mmm_framework.platform.sessions.transition_experiment(experiment_id, new_status, *, note=None, value=None, se=None, estimand=None, start_date=None, end_date=None, readout=None, calibrated_run_id=None)[source]¶
Validated lifecycle move with an append-only audit trail.
Enforces
ALLOWED_TRANSITIONS(ValueError on an illegal move); stampspreregistered_aton draft→planned; merges the lift readout fields on →completed; recordscalibrated_run_idon →calibrated.
- mmm_framework.platform.sessions.append_experiment_event(experiment_id, note, changed=None)[source]¶
Append a non-transition audit event to an experiment’s history.
Records an entry
{status: <current status>, at, note[, changed]}instatus_history_jsonwithout moving the state machine — used to leave a trail when a readout is edited in place (e.g. re-recording a value or attaching off-panel spend on an already-measured experiment). NULL-tolerant liketransition_experiment()(pre-lifecycle rows get their history synthesized from the current status). Bumpsupdated_at.
- mmm_framework.platform.sessions.latest_calibrated_evidence(project_id=None)[source]¶
Newest calibrated experiment per channel — the evidence map that feeds information decay and the calibration-coverage view.
Returns
{channel: {experiment_id, end_date, calibrated_run_id, se, estimand, updated_at}}. Recency is judged by end_date when present, falling back to updated_at.
- mmm_framework.platform.sessions.list_experiments(project_id=None, status=None, channel=None, subchannel=None)[source]¶
Experiments, newest-updated first; optionally filtered.
- mmm_framework.platform.sessions.create_learning_program(*, project_id=None, thread_id=None, name=None, channels, config, state_path=None, status='active')[source]¶
Insert a learning-program row.
channelsis the FLATTENED arm list (the surface dimensions);configis the dollars-at-the-boundary program config (wiring contract §3.1).
- mmm_framework.platform.sessions.list_learning_programs(project_id=None, status=None)[source]¶
Learning programs, newest-updated first; optionally filtered.
- mmm_framework.platform.sessions.update_learning_program(program_id, *, name=None, status=None, thread_id=None, channels=None, config=None, state_path=None, summary=None)[source]¶
Partial update; only the non-None fields change. Raises ValueError for an unknown id or an invalid status.
- mmm_framework.platform.sessions.delete_learning_program(program_id)[source]¶
Delete a program and cascade its waves. Returns True if the program existed. (The on-disk state.npz is left for the caller to reap.)
- Return type:
- mmm_framework.platform.sessions.add_learning_wave(program_id, *, project_id=None, wave_index=None, status='designed', source=None, design=None, observations=None, snapshot=None, experiment_ids=None)[source]¶
Append a wave row (
wave_indexauto-increments per program).sourceis one ofwave/experiment_import/manual;snapshotis the pinned fit_and_plan SNAPSHOT (immutable, like run_metrics rows).
- mmm_framework.platform.sessions.list_learning_waves(program_id)[source]¶
A program’s waves, oldest first (wave_index ascending).
- mmm_framework.platform.sessions.update_learning_wave(wave_id, *, status=None, source=None, design=None, observations=None, snapshot=None, experiment_ids=None)[source]¶
Partial update of a wave row (e.g. designed → ingested with results).
- mmm_framework.platform.sessions.record_ingested_wave(program_id, *, project_id=None, source=None, observations=None, snapshot=None, experiment_ids=None)[source]¶
Record ingested evidence on the wave board (designed → ingested).
A rows-ingest (
source='wave') RESOLVES the program’s latest wave row when that row is still'designed'— so the timeline shows ONE row per real wave instead of a permanently-open ‘designed’ card plus a duplicate ‘ingested’ one. Experiment imports (and ingests with no open design) append a new'ingested'row. Shared by the REST fit worker and the agent tools’record_learning_wave/import_past_experimentspaths.
- mmm_framework.platform.sessions.upsert_budget_plan(*, plan_id=None, project_id=None, org_id, name, description=None, model_id=None, kind='optimization', spend_changes=None, baseline_outcome=None, scenario_outcome=None, outcome_change=None, outcome_change_pct=None, channel_details=None, plan_payload=None)[source]¶
Create or update a budget plan; returns the stored plan dict.
- mmm_framework.platform.sessions.list_budget_plans(org_id, project_id=None, model_id=None)[source]¶
Plans for an org (required), newest-updated first; optionally filtered.
- mmm_framework.platform.sessions.latest_budget_plan_for_project(project_id)[source]¶
The most recently updated saved budget plan for a project (project-scoped, org-agnostic) — the pacing loop auto-sources its planned series from this (issue #123).
Nonewhen the project has no saved plan.
- mmm_framework.platform.sessions.upsert_delivery(project_id, records, *, source=None)[source]¶
Upsert actual-delivery rows for a project (issue #123).
Each record is
{channel, spend, period?}; a row is keyed by(project_id, channel, period)so re-uploading a period overwrites it (the elapsed window can be re-stated as more actuals land). Records with a non-numeric or missing spend/channel are skipped. Returns all of the project’s delivery rows after the write.
- mmm_framework.platform.sessions.list_delivery(project_id)[source]¶
A project’s actual-delivery rows, oldest-first within a channel so a period series reads in order (period is a free-form label — callers align by it).
- mmm_framework.platform.sessions.delete_delivery(project_id, *, channel=None, period=None)[source]¶
Delete delivery rows for a project (optionally one channel / period). Returns the number of rows removed.
- Return type:
- mmm_framework.platform.sessions.upsert_platform_figures(project_id, records)[source]¶
Upsert platform-reported attribution figures for a project (issue #120).
Each record is
{channel, value, source?, period?, metric?, attribution_window?, incremental?}; a row is keyed by(project_id, channel, source, period)so re-uploading overwrites it. Platform figures default to non-incremental (last-touch) unless a record setsincremental=True(a genuine platform geo-lift). Records without a numeric value / a channel are skipped. Returns all of the project’s figures.
- mmm_framework.platform.sessions.list_platform_figures(project_id)[source]¶
A project’s platform-reported attribution figures, newest-updated first.
- mmm_framework.platform.sessions.delete_platform_figures(project_id, *, channel=None, source=None)[source]¶
Delete platform figures for a project (optionally one channel / source). Returns the number of rows removed.
- Return type:
- mmm_framework.platform.sessions.record_signoff(project_id, approver, *, role=None, note='', assumptions=None)[source]¶
Append a sign-off approving the current assumption set (issue #110).
Records who approved (
approver/role), when, a digest of theassumptionssnapshot approved, and a chain hash over the prior record — so the audit trail is append-only and tamper-evident (seeverify_signoff_chain()).
- mmm_framework.platform.sessions.list_signoffs(project_id)[source]¶
A project’s sign-offs, newest first.
- mmm_framework.platform.sessions.verify_signoff_chain(project_id)[source]¶
Re-derive the hash chain oldest→newest:
{intact, n, broken_at?}. AFalsemeans a record was altered or removed after the fact.
- mmm_framework.platform.sessions.GARDEN_STATUSES = ('draft', 'tested', 'published', 'deprecated')¶
Lifecycle states for a registered garden model.
- mmm_framework.platform.sessions.GARDEN_TRANSITIONS: dict[str, set[str]] = {'deprecated': {'draft'}, 'draft': {'deprecated', 'tested'}, 'published': {'deprecated'}, 'tested': {'deprecated', 'draft', 'published'}}¶
Allowed status transitions (mirrors ALLOWED_TRANSITIONS for experiments).
draft -> testedis gated on a passing compatibility report; the human publish gate istested -> published. Published versions are immutable.
- mmm_framework.platform.sessions.DEFAULT_ORG_ID = 'dev-org'¶
Org id used in the single-tenant dev posture (no auth) — MUST match the
_DEV_PRINCIPAL.org_idin api/main.py andAuthSettings.dev_org_idso the agent (resolves org from the project) and the REST layer (resolves org from the principal) agree on where garden models live.
- mmm_framework.platform.sessions.resolve_org_id(project_id)[source]¶
Owning org for a project — the garden registry’s sharing boundary.
Falls back to
DEFAULT_ORG_IDin the single-tenant dev posture (or when the auth schema’sprojects.org_idcolumn isn’t present yet).- Return type:
- mmm_framework.platform.sessions.next_garden_version(org_id, name)[source]¶
Next monotonic version for
(org, name)— 1 when none exist yet.- Return type:
- mmm_framework.platform.sessions.upsert_garden_model(*, org_id, name, version=None, model_id=None, owner_user_id=None, docs=None, manifest=None, source_path=None, base_run_id=None, reference_artifact_path=None, status=None)[source]¶
Create a new draft (no
model_id) or partially update an existing row.On create,
versionauto-increments per(org, name)when omitted and the row starts asdraft. PUBLISHED versions are immutable — editing a published row raisesValueError(re-publish requires a new version).
- mmm_framework.platform.sessions.get_garden_model(*, model_id=None, org_id=None, name=None, version=None)[source]¶
Fetch by id, or by
(org_id, name, version).
- mmm_framework.platform.sessions.get_latest_garden_model(org_id, name, *, status=None)[source]¶
Highest-version row for
(org, name), optionally filtered by status (e.g."published"to resolve what a consumer should load).
- mmm_framework.platform.sessions.list_garden_models(org_id, *, name=None, status=None, latest_only=False)[source]¶
Garden models for an org, newest-updated first; optional name/status filter.
latest_onlycollapses to the highest version per name.
- mmm_framework.platform.sessions.set_garden_compat_report(model_id, report)[source]¶
Store the compatibility-suite report for a model (used to gate testing).
- mmm_framework.platform.sessions.transition_garden_model(model_id, new_status, *, note=None, base_run_id=None)[source]¶
Validated lifecycle move with an append-only audit trail.
Enforces
GARDEN_TRANSITIONS.draft -> testedadditionally requires a stored compatibility report whose blocking tiers passed (the automated testing gate);tested -> publishedis the human publish gate.
- mmm_framework.platform.sessions.delete_garden_model(model_id)[source]¶
Delete a garden row. Only
draft/deprecatedrows are deletable — published history is immutable (deprecate it instead).- Return type:
- mmm_framework.platform.sessions.create_data_connection(project_id, name, kind, config)[source]¶
Persist a reusable data-source connection (no credentials in config).
- mmm_framework.platform.sessions.get_data_connection_by_name(project_id, name)[source]¶
Resolve a connection by name within a project (newest wins).
- mmm_framework.platform.sessions.set_data_connection_schedule(connection_id, interval_minutes, *, now=None)[source]¶
Set (or clear, with
None) a connection’s auto-sync interval.Setting an interval schedules the first run one interval out; clearing it (
None) stops scheduled syncs (manual/on-demand still works).
- mmm_framework.platform.sessions.list_due_data_connections(now, *, limit=100)[source]¶
Scheduled connections whose next_sync_at has arrived (across all projects).
- mmm_framework.platform.sessions.record_data_connection_sync(connection_id, *, status, row_count=None, error=None, snapshot_path=None, now=None)[source]¶
Record a sync outcome and advance next_sync_at.
On
errorthe next run is backed off (up to 4x the interval, capped so a sub-daily schedule waits at most a day) so a permanently broken connection isn’t retried every interval forever — but never sooner than its own interval.- Return type:
- mmm_framework.platform.sessions.record_run_metrics(run_id, metrics, *, thread_id=None, project_id=None, artifact_id=None, created_at=None)[source]¶
Upsert the metrics snapshot for a run.
created_atdefaults to now; backfill passes the original artifact timestamp so series stay ordered.- Return type:
- mmm_framework.platform.sessions.list_run_metrics(project_id=None)[source]¶
Run metrics, OLDEST first (trajectory order).
- mmm_framework.platform.sessions.run_metrics_activity(since_ts)[source]¶
Deployment-wide fit activity: total fits, fits since
since_ts, and the most recent fit time — a lightweight reliability/SLA signal.
- mmm_framework.platform.sessions.ensure_default_project()[source]¶
Create the built-in Default Project if absent; return its id.
- Return type:
- mmm_framework.platform.sessions.list_projects(org_id=None)[source]¶
List projects. When
org_idis given, scope to that tenant’s projects.org_id=Nonepreserves the original behavior (all projects + the built-in Default Project) used by single-tenant/dev mode.
- mmm_framework.platform.sessions.set_project_meta(project_id, meta)[source]¶
Merge
metainto the project’s onboarding profile (None values delete keys). Returns the updated project, or None for an unknown id.
- mmm_framework.platform.sessions.update_project(project_id, name=None, description=None)[source]¶
- Return type:
- mmm_framework.platform.sessions.delete_project(project_id)[source]¶
Delete a project. Its sessions become unassigned (project_id NULL) and its KB documents/chunks are removed. The built-in Default Project cannot be deleted.
- Return type:
- mmm_framework.platform.sessions.resolve_project_id(thread_id)[source]¶
The project a session belongs to, falling back to the Default Project.
Never returns None — the knowledge base always has a home.
- Return type:
- mmm_framework.platform.sessions.set_preference(scope, key, value)[source]¶
Upsert one preference value (stored as JSON) under (scope, key).
- mmm_framework.platform.sessions.set_project_members(project_id, members)[source]¶
Replace the project’s member list:
[{user_id, role?}].
- mmm_framework.platform.sessions.add_kb_document(project_id, name, path, kind, size_bytes=None, status='pending', meta=None)[source]¶
- mmm_framework.platform.sessions.set_kb_document_status(doc_id, status, n_chunks=None, error=None)[source]¶
- Return type:
History¶
Host-side run-metrics persistence and history/coverage/priority assembly.
The kernel computes a model-only metrics snapshot at fit time
(planning.history.compute_run_metrics); this module is the host half:
persist_run_metrics: enrich the snapshot with registry calibration status (experiment-backed vs model-only, evidence age) and write therun_metricsrow. Registry/DB access stays host-side — kernels never touch the sessions store.build_history_series: pivot the stored snapshots into per-channel and portfolio trajectory series for the Performance page (no model loads).build_calibration_coverage: channels × evidence tier, with information decay applied at read time.build_priorities_payload: the latest EIG/EVOI grid with decay/re-test status recomputed against the registry as of today (closed-form over the stored roi_sd — no model load).
- mmm_framework.platform.history.enrich_channel_metrics(metrics, evidence, as_of=None)[source]¶
Stamp per-channel calibration status from the registry evidence map. Mutates and returns
metrics.
- mmm_framework.platform.history.persist_run_metrics(model_run, thread_id, *, artifact_id=None, created_at=None)[source]¶
Enrich + persist the metrics snapshot riding a
model_runrecord. Returns the enriched metrics, or None when the run carries no metrics. Never raises (callers treat this as best-effort).
- mmm_framework.platform.history.build_history_series(project_id)[source]¶
Trajectory series for the Performance page, from run_metrics rows only (oldest first). Channels appearing in any run are included; runs missing a channel simply skip that point (series carry run_id per point).
- mmm_framework.platform.history.latest_model_run_payload(project_id)[source]¶
The newest model_run artifact payload in the project — the design endpoints resolve the dataset path + KPI from here (the design engine is data-only; no model load).
- mmm_framework.platform.history.build_calibration_coverage(project_id, *, as_of=None)[source]¶
Channels × evidence tier with decay applied at read time.
Tier:
running(a test is in market or its readout awaits calibration — the live edge of the program),calibrated(experiment-backed, evidence fresh),stale(experiment-backed but decayed past the re-test threshold AND older than the freshness floor), ormodel_only. Coverage counts calibrated+stale+running-with-evidence as experiment-backed.
- mmm_framework.platform.history.build_priorities_payload(project_id, *, as_of=None)[source]¶
The latest stored EIG/EVOI grid with information decay and registry state applied at read time. Returns None when the project has no metrics.
Decay is closed-form over the stored roi_sd + evidence dates, so this never loads a model: stored
eigreflects fit time;eig_decayed/retest_duereflect TODAY.
Runs¶
MLflow-style run tracking over the existing model_run artifacts.
Every fit already persists a model_run artifact carrying the full
normalized spec, the dataset path, and the summary; fit_mmm_model
additionally stamps a dataset fingerprint, a spec hash, the parent run id, and
a snapshot of the assumption stack at fit time. This module materializes that
into a LINEAGE timeline: for each run, what changed vs its predecessor (spec
leaves, dataset, assumptions) — the provenance needed to audit the process or
write the final report.
- mmm_framework.platform.runs.data_fingerprint(path)[source]¶
Cheap content identity for a dataset file: md5 + size + row count.
- mmm_framework.platform.runs.build_run_timeline(project_id=None)[source]¶
All model runs (newest first), each annotated with its diff vs the chronologically previous run in the same scope.
- mmm_framework.platform.runs.compare_runs(run_a, run_b)[source]¶
Per-channel ROI/spend delta between two fitted runs (B relative to A).
Answers the analyst’s most frequent question — “why did this channel’s ROI change since the last refresh?” — from the persisted
run_metrics, as a structured per-channel + portfolio delta (rather than eyeballing two reports). RaisesValueErrorif either run has no stored metrics.
- mmm_framework.platform.runs.run_timeline_markdown(project_id=None, max_runs=None)[source]¶
The timeline as markdown — the provenance section of a final report.
max_runskeeps only the most recent N runs (used by the agent tool to bound how much lands in the LLM’s context);Nonereturns the full lineage (used for reports).- Return type:
Pacing¶
Server-side in-flight pacing — actual vs plan (issue #123).
Auto-sources the PLANNED series from the project’s latest saved budget plan and
compares it against the stored ACTUAL delivery (the delivery registry), computing
per-channel divergence, off-pace flags, and an alert summary — all model-free (no
fit load), so it can be served from a request. The expected-outcome delta (which
needs the fitted response curves) is left to the check_pacing agent tool /
the report pacing= path.
project_pacing is the pure join (unit-testable without a DB / a model);
build_project_pacing reads the sessions store.
- mmm_framework.platform.pacing.parse_delivery_records(raw, filename='')[source]¶
Parse an uploaded delivery file (CSV/TSV or JSON) into
{channel, period, spend}records (issue #123). Format is chosen by extension, falling back to sniffing the first non-space char ([/{→ JSON). Lenient — bad spend/channel rows are dropped downstream byupsert_delivery.
- mmm_framework.platform.pacing.project_pacing(plan_payload, delivery_rows, *, threshold=0.1)[source]¶
Pure: compute the pacing payload + alert digest from a plan + delivery.
Returns the
PacingResult.to_dict()shape augmented withavailable/reason/plan_basis/alert.availableisFalse(with a machine-readablereason) when there is no saved plan or no delivery yet, so the FE can show the right empty state instead of a misleading zero.
- mmm_framework.platform.pacing.build_project_pacing(project_id, *, threshold=0.1)[source]¶
Pacing for a project, from persisted data only (no model load).
Auto-sources the planned series from the latest saved budget plan and joins the stored actual delivery. Cheap enough to serve from a request.
- mmm_framework.platform.pacing.latest_pacing_alert(project_id)[source]¶
The most recently persisted off-pace alert for a project (issue #123), or
None. Written by the background sweep so a planner is alerted without opening the pacing panel.
- mmm_framework.platform.pacing.sweep_pacing_alerts(now=None)[source]¶
Recompute pacing for every project and persist an off-pace alert artifact (issue #123) — the proactive cadence, so a planner is flagged between fits without opening the panel. Off-pace projects get an upserted
pacing_alertartifact; projects that recover have theirs cleared. Returns a digest{scanned, off_pace, persisted, cleared}.
Scorecard¶
Recommendation scorecard — predicted vs realized (issue #109).
Nothing builds a CMO’s (and CFO’s) trust in a model faster than watching its past calls come true — or seeing it honestly own the misses. The raw material is already persisted: each fitted run’s per-channel ROI (the prediction at recommendation time) and the experiment registry’s calibrated readouts (the realized incremental return). This joins them, per channel, into an accountability view: predicted (with its credible interval) vs realized, the error, and whether the realized value landed inside the predicted interval — so the model’s honesty (interval calibration) is auditable over time.
All from persisted data — no model load. project_scorecard_rows is the pure
join; build_project_scorecard reads the sessions store.
- mmm_framework.platform.scorecard.project_scorecard_rows(estimands, experiments)[source]¶
Join persisted MMM
contribution_roipredictions to realized experiment readouts. Pure — takesbuild_project_estimands()output + registry experiment dicts. Returns{rows, calibration, n_recommendations}.Each row pairs a channel’s realized experiment ROAS with the ROI the model predicted for it — preferring the experiment’s
recommending_run_id(the run that recommended the test), else the latest run that estimated the channel.in_intervalrecords whether the realized value landed inside the predicted credible interval (the calibration signal).
Triangulation¶
Server-side triangulation join — MMM × experiment × platform (issue #119).
The reconciliation engine (mmm_framework.reporting.triangulation) already
puts the three independent estimates of a channel’s return next to each other and
classifies convergent / divergent / platform-inflated. This module wires it to
persisted data so the Oracle / an endpoint can pull the panel WITHOUT
reloading or re-fitting a model:
MMM sources come from the per-channel
contribution_roiestimand rows already persisted on eachmodel_runartifact (viabuild_project_estimands()— no model load);experiment sources come from the experiment registry (
sessions.list_experiments(), calibrated + completed readouts);platform figures are optional (an inline dict until the platform-ingestion follow-up, #120, adds a persistence slot).
project_triangulation_sources is the pure join (unit-testable without a DB);
build_project_triangulation reads the sessions store.
- mmm_framework.platform.triangulation.project_triangulation_sources(estimands, experiments, *, kpi=None, run_id=None, platform=None)[source]¶
Reconcile persisted MMM estimands + registry experiments (+ platform).
Pure: takes the
build_project_estimands()output + a list of experiment registry dicts and returns a serializable payload. No DB / no model.
- mmm_framework.platform.triangulation.parse_platform_records(raw, filename='')[source]¶
Parse an uploaded platform-figures file (CSV/TSV or JSON) into
{channel, value, source?, period?, metric?, attribution_window?, incremental?}records (issue #120). Format is chosen by extension, falling back to sniffing the first non-space char. A CSV is a table with achannel+valuecolumn (+ optionalsource/metric/attribution_window/incremental/period); JSON may be a list of such records or a{channel: value}/{channel: {value, ...}}map.
- mmm_framework.platform.triangulation.platform_dict_for_project(project_id)[source]¶
Reduce a project’s stored platform figures to the one-per-channel
{channel: {value, metric, attribution_window, incremental}}dict the triangulation engine’splatforminput expects (issue #120).Platform figures are return ratios (ROAS), not additive spend, so they are NOT summed; the most-recently-updated figure per channel wins (rows arrive newest-first from
list_platform_figures()).
- mmm_framework.platform.triangulation.build_project_triangulation(project_id, *, kpi=None, run_id=None, platform=None)[source]¶
Triangulation panel for a project, joined from persisted data only.
Reads the project’s persisted MMM
contribution_roiestimands + the experiment registry + the stored platform-attribution figures (issue #120) and reconciles them channel-by-channel. No model is loaded, so this is cheap enough to serve from a request. An explicitplatformdict overrides the stored figures.
Estimands¶
Project estimand aggregation for the Performance page.
Turns the estimand rows persisted on each model_run artifact (see
agents.fitting / agents.estimand_rows) into comparability clusters:
one cluster per (estimand, KPI) so that the same estimand measured on the
same KPI by different models sits side-by-side, while a model on a different
KPI lands in its own cluster and is never silently compared. Two ROI estimands
of the same statistical kind but different methodology (contribution_roi
vs counterfactual_roi) stay distinct clusters — they are different numbers.
The labeling / reference-value / evidence logic mirrors the report’s
EstimandsSection exactly (reporting/sections.py) so the dashboard and the
generated report tell the same story:
reference value:
1.0for ratio kinds (ROI / ROAS / x / multiple), else0.0evidence vs the credible interval: lower > ref -> “strong”; upper < ref -> “below”; otherwise “uncertain”; missing/unsupported -> “na”.
group_estimands is pure (no DB) and unit-tested; build_project_estimands
reads the sessions store.
- mmm_framework.platform.estimands.estimand_label(name)[source]¶
Human label for an estimand name (title-cased fallback for unknowns).
- Return type:
- mmm_framework.platform.estimands.is_ratio_kind(kind, units)[source]¶
A ratio estimand (reference 1.0) — mirrors EstimandsSection._is_ratio_kind.
- Return type:
- mmm_framework.platform.estimands.classify_evidence(*, status, mean, lower, upper, reference)[source]¶
Evidence label vs the no-effect reference: strong / below / uncertain / na.
- Return type:
- mmm_framework.platform.estimands.group_estimands(runs)[source]¶
Cluster per-run estimand rows into
(estimand, KPI)comparability groups.Parameters¶
- runs
One dict per fitted run with keys
run_id,label,model_kind,model_key,kpi,created_atandestimands(a list of rows as produced bymmm_framework.agents.estimand_rows.evaluate_estimand_rows()).
Returns¶
dict with
runs(incl.is_latest_for_modelfor the default selection),kpis, andgroups(each a comparability cluster). Pure; no I/O.
- mmm_framework.platform.estimands.build_project_estimands(project_id)[source]¶
Read the project’s
model_runartifacts and group their persisted estimand rows. Runs fitted before estimand persistence (or with no estimands) are skipped; run the backfill (python -m mmm_framework.platform.backfill --what estimands) to populate them.
Backfill¶
Backfill run snapshots for model runs fitted before a feature existed.
Two backfills over the same model_run artifacts:
run_metrics— for every artifact without a run_metrics row: rebuild the panel from the recorded spec + dataset, deserialize the saved model, runcompute_run_metrics, and persist with the artifact’s original timestamp so trajectory series stay ordered.estimands— for every artifact whose payload has noestimands: load the model, realize its declared/default estimands, and write the rows (+model_kind) back onto the artifact payload so the Performance estimands view can show pre-existing models.evidence— for every MMM artifact whose payload has nochannel_evidence: load the model and compute the per-channel evidence tier + identifiability flag so the live Performance/Estimands dashboard carries the same chip the report does (issue #124).
Skip-and-report anything unrecoverable (empty model dirs, moved datasets) — old runs saved on other machines often are.
- Usage:
- python -m mmm_framework.platform.backfill [–project ID] [–dry-run]
[–max-draws 200] [–what {metrics,estimands,evidence,all}]
- mmm_framework.platform.backfill.backfill_run_metrics(project_id=None, *, dry_run=False, max_draws=200)[source]¶
Returns a per-run status report:
{run_id, status, detail}with status ∈ {done, exists, skipped, error, would-run}.
- mmm_framework.platform.backfill.backfill_estimands(project_id=None, *, dry_run=False, random_seed=42)[source]¶
Populate
payload['estimands'](+model_kind) on every model_run artifact that lacks it, so the Performance estimands view covers models fitted before estimand persistence existed. Returns a per-run status report with status ∈ {done, exists, skipped, error, would-run}.
- mmm_framework.platform.backfill.backfill_evidence(project_id=None, *, dry_run=False)[source]¶
Populate
payload['channel_evidence'](per-channel evidence tier + identifiability flag) on every MMMmodel_runartifact that lacks it, so the live Performance/Estimands dashboard shows the same evidence chip the report does for models fitted before this snapshot existed (issue #124). Returns a per-run status report with status ∈ {done, exists, skipped, error, would-run}.
Backup¶
Online backup / restore for the SQLite sessions + auth store.
The entire platform state (sessions, auth users/tokens, run_metrics, langgraph checkpoints) lives in one SQLite file with no backup tooling — a single disk loss destroys everything. This adds a CONSISTENT online backup (SQLite’s backup API, safe while the app is running, WAL-aware) and a restore, plus a CLI:
python -m mmm_framework.platform.backup backup /backups/sessions-2026-06-25.db
python -m mmm_framework.platform.backup restore /backups/sessions-2026-06-25.db
This is the stop-gap for the single-SQLite risk until the Postgres migration (action plan P1a). See technical-docs/disaster-recovery.md.
- mmm_framework.platform.backup.backup_db(dest, *, db_path=None)[source]¶
Write a consistent snapshot of the live DB to
dest. Returnsdest.- Return type: