New website under development. In the meantime, follow EV Fleet (Pty) Ltd on LinkedIn for updates.
What this is: the phase-gated execution plan for building Postgres summary storage into
ev-fleet-sim — Step 2 (“Saving summaries into PostgreSQL”) and the Postgres-read half of
Step 4 (“Set up methods to read summary … data”) from Chris Abraham’s FigJam board, as
recorded in ../brain/skills/SIM_DATABASE_IMPLEMENTATION.md → “Current Direction”. That
document is the source of truth for why this project exists and how it fits the larger
Sim-becomes-a-database-service pivot; this document is the source of truth for how this one
slice gets built, phase by phase, gated the same way that guide gates its own phases.
Technical design reference: docs/docs/postgres-setup.md (same repo) has the actual schema DDL,
config format, docker-compose file, and library choice with justification. This plan sequences
that design into gated phases with tests and sign-off — read docs/docs/postgres-setup.md for what to
build, this doc for in what order and how each step gets verified.
Ownership split (per the board): Chris owns Step 1 (Postgres/DuckDB/Parquet plumbing — shared foundational setup, starred as his current task), Step 3 (Parquet/DuckDB time-series), the DuckDB-read half of Step 4, and Step 5 (REST API). This plan covers the Postgres-only slice: standing up Postgres plumbing (the Postgres half of Step 1, since nobody has started it yet), Step 2, and the Postgres-read half of Step 4.
Sim Boundary note: this plan modifies ev-fleet-sim’s own source
(src/data_processing_ev/). That is normally off-limits per Rule 0 of
SIM_DATABASE_IMPLEMENTATION.md — but this entire project traces to Chris’s own explicit
direction (his FigJam board), which is exactly the Maintainer-directed exception that rule
carves out. Scope discipline still applies: build what’s specified below, nothing extra “while
we’re in here.”
SIM_DATABASE_IMPLEMENTATION.md §1, applied here)grep/re-Read it — line numbers below were verified once; they drift as code changes.psycopg[binary] is approved for this slice (see
docs/docs/postgres-setup.md §3 for why). Anything else — stop and flag before adding.docs/docs/postgres-setup.md §4) was confirmed 2026-07-23 (owner decision): append-only,
run_id UNIQUE. Phase 1’s schema task and Phase 2 are unblocked.stats_fleet.json/stats_*.json writes.Status: Complete (2026-07-23)
ev-fleet-sim-master was a solo git init whose HEAD (7e3d7f2) tracked only 3
files; the rest of the working tree (71 staged additions under src/data_processing_ev/,
plus untracked repo-root files — README.md, LICENSE, setup.cfg, pyproject.toml,
.gitlab-ci.yml, .gitmodules, docs/, sim_worker/, both Postgres design docs) had never
been committed anywhere. Owner decision: commit the full tree as-is on a new postgres-baseline
branch (commit 06a6bb4, off 7e3d7f2, not on brain-phase1-skip-plots-flags), then branch
postgres-phase1-config-connection-schema off that for Phase 1 work. origin/master’s
362-commit gap is intentionally left unreconciled — separate concern, not blocking this slice.docs/docs/postgres-setup.md §4) with Chris — append-only
(recommended) vs. upsert/overwrite. Resolved 2026-07-23 (owner decision): append-only,
run_id UNIQUE. Recorded in docs/docs/postgres-setup.md §4.SIM_DATABASE_IMPLEMENTATION.md’s “MinIO deferred, not urgent” decision — this
is current, real scope now, not aspirational.| Task | Trigger prompt | Test |
|—|—|—|
| PG-T0.1 | “Run PG-T0.1: fetch origin, diff against brain-phase1-skip-plots-flags, decide target branch with the owner.” | Branch decision recorded in writing (this doc or a commit message), git status clean or intentionally staged only |
| PG-T0.2 | “Run PG-T0.2: confirm append-only vs. upsert run-identity with Chris before Phase 2 starts.” | Decision recorded in docs/docs/postgres-setup.md §4, no longer marked “not yet confirmed” |
| PG-T0.3 | “Run PG-T0.3: confirm with Chris/owner whether the FigJam diagram supersedes the MinIO-deferred decision.” | Decision recorded in writing in this doc, no longer marked PENDING |
| PG-T0.4 | “Run PG-T0.4: confirm ownership of the diagram’s scope (Phases 5-8) with Chris/owner.” | Decision recorded in writing in this doc, no longer marked PENDING |
postgres-baseline (06a6bb4) + postgres-phase1-config-connection-schema branched off it, git status clean.docs/docs/postgres-setup.md §4, confirmed 2026-07-23.N/A — no code changes in this phase, only decisions and git hygiene.
Status: Complete (2026-07-27) — commit ad4c55b on
postgres-phase1-config-connection-schema (branched off postgres-baseline, 06a6bb4).
(This is the Postgres half of the board’s Step 1 — nobody has started it yet, confirmed with the user directly.)
src/data_processing_ev/db/ subpackage skeleton: __init__.py,
config.py, connection.py, schema.sql (empty/stub files at this stage — content in
T1.2-T1.4).db/config.py — load_db_config(path=DEFAULT_CONFIG_PATH), INI format,
fixed path ~/.config/ev-fleet-sim/db_config.ini, returns None if absent. Exact format
and rationale in docs/docs/postgres-setup.md §1.db/connection.py — get_connection() (returns None if unconfigured,
raises only on a genuine connection failure with a valid config) and ensure_schema(conn)
(idempotent CREATE TABLE IF NOT EXISTS from schema.sql).db/schema.sql per docs/docs/postgres-setup.md §4 — fleet_stats, vehicle_stats
tables, GIN indexes on raw_stats. Requires PG-T0.2’s run-identity decision to be settled
first (determines whether run_id is UNIQUE (append-only) or the table is keyed/upserted
on scenario_name instead).psycopg[binary] >= 3.1 to setup.cfg’s install_requires, and a new
[options.extras_require] test = pytest >= 7.0 section.docker-compose.yml (repo root) and db_config.ini.example per
docs/docs/postgres-setup.md §2. Add bare db_config.ini to .gitignore.| Task | Trigger prompt | Test |
|—|—|—|
| PG-T1.1 | “Run PG-T1.1: create the db/ subpackage skeleton.” | python -c "from data_processing_ev import db" imports with no error |
| PG-T1.2 | “Run PG-T1.2: implement db/config.py per docs/docs/postgres-setup.md §1.” | Unit test: fixture INI parses correctly; missing file returns None, not an exception |
| PG-T1.3 | “Run PG-T1.3: implement db/connection.py’s get_connection() and ensure_schema().” | Against docker-compose Postgres: get_connection() returns a live connection; ensure_schema() run twice is a no-op the second time |
| PG-T1.4 | “Run PG-T1.4: write db/schema.sql per docs/docs/postgres-setup.md §4, using the confirmed run-identity decision from PG-T0.2.” | ensure_schema() creates both tables with the exact columns listed in docs/docs/postgres-setup.md §4 (verify via \d fleet_stats / \d vehicle_stats) |
| PG-T1.5 | “Run PG-T1.5: add psycopg[binary] and the test extra to setup.cfg.” | pip install -e . succeeds; pip install -e .[test] succeeds |
| PG-T1.6 | “Run PG-T1.6: add docker-compose.yml and db_config.ini.example.” | docker compose up -d postgres exits 0, pg_isready healthy |
ev_results_analysis.py or any other existing module — this phase is
purely additive new files. If a task seems to require touching an existing file, stop.db_config.ini (the real, non-example file) must never be committed — verify .gitignore
before finishing this phase.docker compose up -d postgres --wait exits 0, healthy. Note: host port changed to
5442 — lsof -i :5432 during this self-test found a pre-existing native Postgres already
bound to 5432 on this dev machine, which silently intercepted connections meant for the
container (role "ev_fleet_sim" does not exist — that role belongs to the container, not the
native instance). docker-compose.yml, db_config.ini.example, and docs/docs/postgres-setup.md §1-2
all updated to 5442 to match.ensure_schema() run against a fresh container creates both tables correctly — verified
via information_schema.tables/.columns/pg_indexes (not assumed): both tables present
with exact columns from §4, both GIN indexes on raw_stats present. Run twice, second call a
confirmed no-op.load_db_config() unit test: present-file and absent-file cases both pass (direct
isolated-module test, real output captured).pip install -e .[test] succeeds with the new dependency. Does not pass — pre-existing,
unrelated to this diff. Tried on Python 3.14 (fails: matplotlib==3.3.4’s versioneer.py
calls configparser.SafeConfigParser, removed in 3.12+) and Python 3.11 (fails: pyproj==3.2.1
needs pkg_resources, absent from current build-isolation setuptools). Both pins predate
this commit; psycopg[binary] and pytest themselves install and run cleanly in isolation
(verified separately). This package has only ever been run via its Docker image
(sim-worker-full), never a bare local pip install, per prior IMPLEMENTATION_LOG.md
history — same class of gap, not new. Real verification of this specific box needs that image
or a compatible legacy Python env; flagging rather than silently checking it off.fleet_stats/vehicle_stats schema via psql against the compose Postgres.db_config.ini.example matches the compose file’s credentials exactly..gitignore excludes the real db_config.ini.db/ subpackage exists with config, connection, schema all working against a real
local Postgres.setup.cfg updated (psycopg + test extra) — dependency itself verified working; full
package install still blocked by pre-existing, unrelated pins (see Agent Self-Test above).docker compose down -v && up --wait.Delete src/data_processing_ev/db/, revert setup.cfg, remove docker-compose.yml and
db_config.ini.example — no existing code touched, clean revert.
Status: Complete (2026-07-27) — commit 36c2d56.
db/summary_writer.py — write_ev_stats(scenario_dir, run_id, ev_name,
stats_tree) and write_fleet_stats(scenario_dir, run_id, stats_tree). No-ops silently if
unconfigured; catches and logs (never raises) on a configured-but-failing write. Exact
behavior spec in docs/docs/postgres-setup.md §5.ev_results_analysis.py:
run_ev_results_analysis() (~line 1404): kwargs.setdefault('run_id', uuid.uuid4()).save_ev_stats() (~lines 946-952, immediately after the existing json.dump): call
write_ev_stats(...).save_fleet_stats() (~lines 1197-1200, immediately after the existing json.dump): call
write_fleet_stats(...).| Task | Trigger prompt | Test |
|—|—|—|
| PG-T2.1 | “Run PG-T2.1: implement db/summary_writer.py per docs/docs/postgres-setup.md §5.” | Unit test with a mock/fixture stats_tree: row appears in Postgres with correct values; with no config file present, function returns cleanly with no exception |
| PG-T2.2 | “Run PG-T2.2: hook write_ev_stats/write_fleet_stats into ev_results_analysis.py at the verified line numbers.” | Real my-scenario run with db_config.ini present: rows appear in both tables AND stats_fleet.json/stats_*.json are still written unchanged |
pip install caveat) — raw_stats
byte-identical to the input stats_tree fixture, confirmed by direct equality assertion, not
eyeballed. Re-write with the same run_id confirmed ON CONFLICT DO NOTHING (count stays 1).db_config.ini: returns cleanly, one debug-level log line, no exception.db_config.ini present but Postgres unreachable (pointed at a closed port): first
attempt failed this test — get_connection() was called outside the try/except, so the
connection error propagated uncaught. Fixed by moving it inside a shared _write() helper’s
try block; re-tested, now returns cleanly with one warning logged, confirmed via real
psycopg.OperationalError triggered against a closed port, not simulated.git diff on ev_results_analysis.py shows +9/-0 lines, i.e. the existing json.dump(...)
blocks are untouched by construction.my-scenario end-to-end with Postgres up; inspect rows via psql.stats_fleet.json content still matches what’s in fleet_stats.raw_stats exactly.sim-worker-full) to run my-scenario end-to-end. Not silently marked done.Revert the two-line-plus-one-import hook in ev_results_analysis.py; delete
summary_writer.py. JSON-writing behavior is untouched throughout, so rollback carries zero
risk to existing consumers.
Status: Complete (2026-07-27) — commit 063d025.
db/reader.py — get_fleet_stats(conn, run_id),
get_vehicle_stats(conn, run_id, ev_name), list_vehicle_stats(conn, run_id),
get_latest_run_id(conn, scenario_name). Spec in docs/docs/postgres-setup.md §6.| Task | Trigger prompt | Test | |—|—|—| | PG-T3.1 | “Run PG-T3.1: implement db/reader.py per docs/docs/postgres-setup.md §6.” | Round-trip test: write a known stats_tree fixture, read it back via each reader function, assert deep-equality with the original |
psycopg.Connection, returns
plain dicts, so Step 5 (Chris’s REST layer) can call it directly later.get_fleet_stats/get_vehicle_stats
return raw_stats byte-identical to the input fixture; list_vehicle_stats returns exactly
the vehicles written for a run_id, ordered, raw_stats intact for each. Against
Phase-2-written synthetic-fixture data (real-Sim-run data still pending, see Phase 2).get_latest_run_id correctly resolves the most recent run when a scenario has been run
more than once — wrote two runs a second apart under the same scenario_name,
get_latest_run_id returned the second run’s id, and list_vehicle_stats for that run
returned only its own vehicle, not the first run’s. Exercised directly, not skipped.my-scenario twice, confirm get_latest_run_id returns the second run’s id, and
list_vehicle_stats for that run only returns that run’s vehicles (not the first run’s).Delete db/reader.py — no other module depends on it yet (Step 5 is Chris’s later work).
Status: Complete (2026-07-27) — commit 22dca31.
tests/test_db_config.py, tests/test_db_summary_writer.py,
tests/test_db_reader.py, tests/README.md documenting the docker compose up -d postgres
&& pytest tests/ -q workflow. (This also happens to be the first test suite this package
has ever had — .gitlab-ci.yml today runs only a Jekyll docs build, confirmed via grep.).gitlab-ci.yml test job. This
is a visible change to shared project infra — confirm with Chris before merging.| Task | Trigger prompt | Test |
|—|—|—|
| PG-T4.1 | “Run PG-T4.1: write the db test suite per docs/docs/postgres-setup.md §7.” | docker compose up -d postgres && pytest tests/ -q exits 0 |
| PG-T4.2 | “Run PG-T4.2: propose a Postgres-backed CI job to Chris, do not merge without his sign-off.” | Explicit approval recorded before merge |
docker compose down -v && up --wait:
pytest tests/ -q → 11 passed (2 config + 5 writer + 4 reader). Notable finding along the
way: importing anything under data_processing_ev — even db.config alone — forces
data_processing_ev/__init__.py to run first, which pulls in folium/matplotlib/libsumo
(confirmed: bare import data_processing_ev.db.config fails on
ModuleNotFoundError: No module named 'folium' here). Since db/ only needs psycopg +
stdlib, tests/conftest.py loads its submodules directly and stubs just the parent’s
LOGGERS attribute — test-only, falls back to the real package automatically if it’s ever
actually importable. Documented in tests/README.md, not hidden..gitlab-ci.yml.docker compose down -v && up.22dca31).docs/docs/ci-proposal-postgres-tests.md lays out a proposed job and three options for the
matplotlib/pyproj-pin wall a real CI run would hit, without picking one. Needs Chris’s
sign-off before anything lands in .gitlab-ci.yml.Delete tests/; revert .gitlab-ci.yml if PG-T4.2 shipped.
Status: Complete (2026-07-28) — commit 021b065 on postgres-implementation, in
ev-fleet-sim-master.
(New scope introduced by the “posgres - parquet integration” FigJam diagram, shown directly by the user 2026-07-27 — not part of the original board text this plan was built from.)
fleet_stats/vehicle_stats, doesn’t
replace them).scenarios table. Done. scenario_name (PK — matches the naming already
used in fleet_stats/vehicle_stats, not a new scenario_id concept), created_by,
created_at. # runs/averages are computed via query over runs, not stored columns — avoids
a second place that can drift out of sync.runs table. Done. run_id (PK, matches the existing run_id already
used in fleet_stats/vehicle_stats), scenario_name (FK → scenarios, nullable —
sim-worker doesn’t always know it upfront), execution_status
(queued/running/succeeded/failed, CHECK constraint), execution_start, execution_end,
triggered_by, storage_bytes, runtime_cpu_seconds, runtime_ram_bytes, error.sim_worker/main.py (not reused from this doc, which flagged it unverified) found job_id
(HTTP-level tracking) and run_id (minted inside ev_results_analysis.py) were disconnected
UUIDs. Unified: sim-worker now mints run_id at submission, passes it to the Sim subprocess
via a new BRAIN_RUN_ID env var (same passthrough pattern as BRAIN_SIM_ONE_BIG_CLUSTER),
and run_ev_results_analysis() reads it instead of minting its own.resource.getrusage(RUSAGE_CHILDREN), before/after delta around the subprocess call — no new
dependency. Documented limitation: this is process-wide cumulative, not per-child, so
concurrent runs under sim-worker’s threading model can cross-contaminate each other’s
numbers. A per-PID method (psutil) would fix it but isn’t an approved dependency for this
slice. Storage via a real directory-size walk of scenario_dir — no such limitation, one dir
per job.db/run_tracker.py. Done. create_run(run_id, scenario_name, triggered_by),
update_run_status(run_id, status), finalize_run(run_id, status, storage_bytes,
runtime_cpu_seconds, runtime_ram_bytes, error). Same safety contract as summary_writer.py.db/reader.py: get_scenario, get_run,
list_runs_for_scenario.| Task | Trigger prompt | Test |
|—|—|—|
| PG-T5.1 | “Run PG-T5.1: add the scenarios table to schema.sql.” | ensure_schema() creates scenarios with the agreed columns |
| PG-T5.2 | “Run PG-T5.2: add the runs table to schema.sql, FK’d to scenarios.” | ensure_schema() creates runs; FK constraint verified (insert with bad scenario_id fails) |
| PG-T5.3 | “Run PG-T5.3: hook execution-status/timing writes into the real run-launching code, re-grepped fresh.” | A real (or simulated) run writes queued → running → succeeded/failed with real timestamps |
| PG-T5.4 | “Run PG-T5.4: implement CPU/RAM/storage measurement for a run.” | Measured values are non-null and plausible for a real run |
| PG-T5.5 | “Run PG-T5.5: implement db/run_tracker.py.” | Unit + integration tests: create/update/finalize a run, values land correctly; failure path never raises |
| PG-T5.6 | “Run PG-T5.6: implement scenario/run reader functions.” | Round-trip test against real written data |
fleet_stats/vehicle_stats, only add alongside.scenarios/runs tables created correctly, FK enforced — verified via
information_schema (exact columns) and a real ForeignKeyViolation triggered by inserting
a runs row with a nonexistent scenario_name, confirmed programmatically.runs row — create_run → queued,
update_run_status(running) → sets execution_start, finalize_run(succeeded, ...) → sets
execution_end + all three cost numbers, verified against the live compose Postgres with real
reads after each transition (not assumed).db/run_tracker.py’s three functions all confirmed non-raising with no
config file present. sim-worker side: 14/14 tests passing (9 pre-existing + 5 new) via real
pytest — a resolve-time failure finalizes as failed with the real error message; the
stub path (WORKER_SKIP_SIMULATOR) still finalizes as succeeded, unchanged from before
Phase 5.runs row mid-execution (running) and after (succeeded
or failed) via psql.scenarios-level averages update (or compute correctly, if a view) after multiple
runs of the same scenario.scenarios/runs tables live and verified against real Postgres writes/reads — real
Sim-subprocess execution still pending (same environment gap as every phase since Phase 2).resource.getrusage call) — plausibility against an actual Sim run’s real
resource usage is part of the still-outstanding real-run gap, not yet checked.Delete the new tables and db/run_tracker.py; revert the instrumentation hook in
sim_worker/. fleet_stats/vehicle_stats and their write path are untouched throughout.
Status: Complete (2026-07-29) — commit 4720f3c on postgres-implementation, in
ev-fleet-sim-master.
runs table must exist as the FK target for catalog entries).run_files table (not two) — one row per
real file, matching the diagram’s “each file with its own file ID” (a raw input’s original
upload and its ETL’d/Parquet conversion are two rows, not two columns on one). file_kind
CHECK enumerates the six real kinds (config, raw_input_original,
raw_input_etl_parquet, run_log, summary_stats, raw_output). s3_file_id/file_format
nullable — Phase 8 is what creates the objects these point at, not this phase.db/file_catalog.py writer. Done. One function per file kind
(record_config, record_raw_input_original, record_raw_input_etl_parquet,
record_run_log, record_summary_stats, record_raw_output), shared _record() helper,
same never-raises contract as the other writers. record_summary_stats() takes no
s3_file_id by design — the real data is the fleet_stats/vehicle_stats rows, this is a
pointer/marker row.list_input_files/list_output_files, filterable
by ev_name and/or file_kind.| Task | Trigger prompt | Test |
|—|—|—|
| PG-T6.1 | “Run PG-T6.1: add the file-catalog table(s) to schema.sql.” | ensure_schema() creates the table(s) with agreed columns and FK to runs |
| PG-T6.2 | “Run PG-T6.2: implement db/file_catalog.py per the diagram’s Inputs/Outputs spec.” | Fixture test: recording each file kind produces a correct row; never raises on failure |
| PG-T6.3 | “Run PG-T6.3: implement file-catalog reader functions.” | Round-trip test against real written data, filtered correctly by kind/vehicle |
summary_stats confirmed to have a
null s3_file_id, by design.ForeignKeyViolation on a nonexistent run_id, swallowed not raised) and
CHECK enforcement (bad direction value) both verified programmatically. Both negative
paths — no config file, and a real FK violation — confirmed to no-op cleanly.docker compose down -v && up --wait: pytest tests/ -q →
24 passed (11 Phase 1-4 + 6 run_tracker + 7 file_catalog). Also retroactively closed a
Phase 5 gap: that phase only had ad-hoc scratch verification, not a committed pytest file —
tests/test_db_run_tracker.py added here.psql, cross-check against what files actually
exist for that run.Delete the new table(s) and db/file_catalog.py. Nothing else depends on this phase yet.
Status: Complete (2026-07-29) — commit 2099d27 on postgres-implementation, in
ev-fleet-sim-master.
runs table is what these aggregates roll up).runs_passed, runs_failed,
runs_completed, runs_running, avg_runtime_cpu_seconds, avg_runtime_ram_bytes,
avg_storage_bytes (the “cost split by storage/CPU/RAM” from the diagram), and
avg_runtime_wall_seconds (execution_end − execution_start). Chose a single plain query over
a Postgres VIEW: no other consumer needs a standalone schema object for a one-caller
aggregate, and it keeps this phase’s diff a pure db/ addition with zero schema.sql change.db/dashboard_reader.py. Done. get_global_stats(conn) -> dict, same
dict_row/single-query shape as db/reader.py’s existing functions — for whatever consumes
a dashboard view (Step 5’s REST API, later, per the ownership split; this phase only makes the
query available).| Task | Trigger prompt | Test | |—|—|—| | PG-T7.1 | “Run PG-T7.1: add the global-stats aggregate view/queries.” | Query against known fixture data returns exactly the expected counts/averages | | PG-T7.2 | “Run PG-T7.2: implement get_global_stats().” | Returns correct values against real multi-run data |
db/reader.py — plain dicts, no dashboard/API code here.tests/test_db_dashboard_reader.py::test_get_global_stats_matches_independently_computed_values
inserts three known runs (one succeeded with storage_bytes=1000/runtime_cpu_seconds=2.0/
runtime_ram_bytes=2000, one failed with 3000/4.0/4000, one running), then
cross-checks every field against separately written SQL (different SELECT per field,
independent of get_global_stats()’s own query) — real compose Postgres, not mocked.pytest tests/ -q → 26 passed (24 pre-existing + 2 new
test_db_dashboard_reader.py tests), against the live docker compose up -d postgres --wait
instance.ast.parse clean on all four changed/new files
(db/dashboard_reader.py, db/__init__.py, tests/conftest.py,
tests/test_db_dashboard_reader.py).get_global_stats()’s output against a manual SELECT count(*) ... for each
field.Delete the view/queries and db/dashboard_reader.py.
Status: Not Started
(This is the piece most likely to belong to Chris/the Parquet-DuckDB side of the original ownership split, not this Postgres-only plan — flagged loudly, not assumed. Confirm ownership via PG-T0.4 before starting, even after Phases 5-7 are done.)
SIM_DATABASE_IMPLEMENTATION.md’s “Future” section explicitly lists MinIO as
deferred, owner TBD, “not urgent until there’s a stable pipeline that needs it.” This phase is
the literal thing that was deferred.brain/docker-compose.spike.yml, already has a throwaway MinIO — check whether
that’s meant to become real infra or whether this needs its own instance).ADR-storage-partitioning.md, in the brain/ repo) already covers Parquet conversion
strategy for a related but distinct purpose (time-series data from battery.out.csv.gz) —
read that first, don’t re-derive a second, inconsistent Parquet strategy from scratch.Run ID → Input Folder / Output Folder → raw input data /
input parquets / output parquets, each file with its own file ID, per the diagram exactly.s3_file_id/link fields
should resolve to real MinIO objects created by PG-T8.1-T8.3, not placeholder values.| Task | Trigger prompt | Test |
|—|—|—|
| PG-T8.1 | “Run PG-T8.1: confirm/stand up MinIO for this pipeline.” | MinIO reachable, bucket created |
| PG-T8.2 | “Run PG-T8.2: implement Parquet conversion for run input/output files, per the existing ADR-storage-partitioning.md strategy.” | Converted Parquet files readable, row-count parity vs. source confirmed (same method as Phase 0’s spike) |
| PG-T8.3 | “Run PG-T8.3: implement the Run-ID-based bucket layout.” | Real run’s files land in the correct Input Folder/Output Folder structure |
| PG-T8.4 | “Run PG-T8.4: wire file-catalog entries to the real MinIO objects.” | A file-catalog row’s link actually resolves to the real stored object |
ADR-storage-partitioning.md) — this phase applies that strategy to a new (input/output
catalog) context, it doesn’t invent a second one.Remove the MinIO wiring and Parquet conversion step; file catalog (Phase 6) degrades to storing links that don’t resolve, but doesn’t break — Postgres-side data is unaffected.
| Phase | Work | Gate to next phase |
|---|---|---|
| 0 | Git reconciliation + run-identity decision | Both confirmed in writing |
| 1 | Config/connection/schema plumbing, deps, docker-compose | Self-test passing on real local Postgres |
| 2 | Write path + hook into ev_results_analysis.py |
Real-run verification, zero JSON regression |
| 3 | Read path | Round-trip + multi-run verification |
| 4 | Tests + CI | Suite green, CI decision explicit |
| 5 | Scenario & run metadata (execution tracking) | Real run lifecycle verified, PG-T0.3/T0.4 resolved first |
| 6 | File catalog (inputs/outputs per run) | Real run’s file catalog verified against actual files |
| 7 | Database-level aggregate metadata | Aggregates verified correct against real multi-run data |
| 8 | Parquet + MinIO storage pipeline | Real round-trip verified, ownership (PG-T0.4) re-confirmed |
Do not start a phase’s tasks before the prior phase’s Completion Markers are checked — same
gating discipline SIM_DATABASE_IMPLEMENTATION.md uses for its own phases. Phases 5-8 are
additionally gated on PG-T0.3/PG-T0.4 (diagram-scope and ownership), unresolved as of
2026-07-27 — do not start Phase 5 until those are answered, even though Phases 0-4 are done.
Do not start a phase’s tasks before the prior phase’s Completion Markers are checked — same
gating discipline as SIM_DATABASE_IMPLEMENTATION.md.