Skip to content

feat(delphi): storage v2 P6a — per-run input snapshots (capture side) + purge tool#2602

Open
jucor wants to merge 1 commit into
jc/delphi-storage-v2-p5from
jc/delphi-storage-v2-p6a
Open

feat(delphi): storage v2 P6a — per-run input snapshots (capture side) + purge tool#2602
jucor wants to merge 1 commit into
jc/delphi-storage-v2-p5from
jc/delphi-storage-v2-p6a

Conversation

@jucor

@jucor jucor commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Stack 2 / P6a of the Storage V2 effort (design in #2597, §4.2 entity 2 + §4.4): the capture half of input snapshots — reproducibility starts with recording exactly what the pipeline read. The read seam (--input-source, P6b) follows as its own PR, per the design's deliberate 6a/6b isolation of the riskiest step.

Stacked on #2601 (jc/delphi-storage-v2-p5).

What this adds

delphi_storage/inputs.py — snapshot capture

capture_run_inputs(store, job_id, zid, rid) writes the six snapshot kinds as run_inputs items (codec envelopes; votes always zstd-compressed) and returns per-kind fingerprints (sha256 of the canonical uncompressed payload, row_count, max vote created) — exactly what P7's manifest records.

The fidelity decisions, each pinned by a test:

  • Votes = the RAW stream stage 1 consumes: ORDER BY created, raw PG signs — recon confirmed the production math path never flips the sign (fetch_votes()'s flip is dead code) — superseded votes included, order preserved (the math KMeans init deliberately depends on vote-encounter order, see INVESTIGATION_K_DIVERGENCE.md).
  • SQL parity by construction: run_math_pipeline.py now imports VOTES_COUNT_SQL / VOTES_BATCH_SQL from delphi_storage.inputs (strings unchanged — the golden suite and the e2e SQL-pin test stay green), so capture and stage 1 can never diverge.
  • Comments and participants captured in full, including moderated-out rows (mod = -1) and banned participants — moderation filtering is pipeline behavior, not snapshot behavior (the snapshot must let a replay reproduce whatever filtering the code-at-replay-time does).
  • clojure_math_main captured for every math_env — 501 reads the env-less latest while 801 filters by MATH_ENV with a different default ('prod' vs 'dev'); snapshotting all envs keeps both replayable.
  • derive_votes_latest_unique() reproduces PG's RULE-maintained votes_latest_unique from the stream (last vote per (pid, tid) in stream order) — the in-pipeline derivation §4.4 calls for, tested against a seeded PG table that mimics the RULE (including a re-vote).

delphi_storage/purge.py — the §9 GDPR path

purge_job() deletes every generic-entity partition for a job — snapshots copy votes/comments outside the source PG, so deletion needs this from day one.

run_delphi.py hook

--snapshot-inputs (or DELPHI_SNAPSHOT_INPUTS=1) captures before any stage runs and aborts the run on capture failure — a run without recorded inputs defeats the point when enabled. Off by default until P7's DELPHI_WRITE_MODE subsumes it (M1 in the migration plan).

Testing (TDD — 16 tests written first, RED on missing module)

  • Raw stream order + signs (out-of-order inserts, a re-vote, a foreign-zid vote that must not leak), compression, fingerprint contents;
  • derivation equality against the seeded PG votes_latest_unique;
  • full-row comments/participants (moderated-out/banned rows present), conversation meta, selections, all-env math_main;
  • SQL-parity-by-import (run_math_pipeline.VOTES_BATCH_SQL is delphi_storage.inputs.VOTES_BATCH_SQL);
  • purge removes everything; run_delphi hook (flag / env / off-by-default, capture ordered before stages).
  • PG-backed tests use a scratch database seeded with the minimal schema (skip-locally / fail-in-CI convention, services present in CI since feat(delphi): storage v2 P4 — deployable DDL (Delphi2 Dynamo tables + PG migration 000019) #2600's wiring fix).

Full suite: 429 passed, 41 skipped, 58 xfailed (5 pre-existing DynamoDB-unavailable errors unchanged). Golden snapshots ran and passed — math outputs untouched.

Stack

🤖 Generated with Claude Code

@jucor

jucor commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Post-push self-review (Claude subagent) found four issues, now addressed and re-pushed:

  1. Tie semantics in derive_votes_latest_unique (the critical one) — PG's RULE picks winners by INSERTION order while the snapshot stream is ORDER BY created with no tiebreaker (the votes table has no serial/PK column, and adding a tiebreaker to the SQL would change stage 1's vote-encounter order — forbidden by golden invariance). For same-millisecond re-votes on one (pid, tid) the two can legitimately differ. Resolution: the caveat is now documented in the docstring rather than hidden, and a new test seeds a same-created tie and pins the property replay actually needs — the derivation is deterministic w.r.t. the snapshot itself (last-in-stream wins).
  2. rid was accepted but never persisted — every snapshot item's attributes now carry zid and rid, with a test asserting it.
  3. Abort-on-capture-failure was untested — new test: capture raising ⇒ exit code 1 and zero stage subprocesses run.
  4. Purge scope honestypurge.py's docstring now states explicitly that P6a ships per-JOB purge only; the per-zid tool (§9) needs the run manifests and follows in P7 via list_runs(zid=…) + purge_job.

Suite: 430 passed / 58 xfailed, goldens green, same 5 pre-existing DynamoDB-unavailable errors.

@jucor
jucor force-pushed the jc/delphi-storage-v2-p6a branch from b809c80 to 2b8ddfe Compare July 6, 2026 16:08
@jucor
jucor force-pushed the jc/delphi-storage-v2-p5 branch from 95099f9 to abac670 Compare July 6, 2026 18:48
@jucor
jucor force-pushed the jc/delphi-storage-v2-p6a branch from 2b8ddfe to 3770aa7 Compare July 6, 2026 18:48
jucor added a commit that referenced this pull request Jul 6, 2026
…ion notes

Implements the read half of Stack 2 / P6 (docs/STORAGE_V2_DESIGN.md §4.4,
§5): stages can consume a recorded snapshot instead of live PG via
--input-source=store://<job_id> — the seam the replay CLI (P12) drives.
Approved via propose-then-wait (touches the vote-feeding loop in
polismath/run_math_pipeline.py; the seam substitutes data sources only,
never transformations).

Seam mechanisms (in delphi_storage/inputs.py + minimal stage edits):
- SnapshotReader — stage-shaped accessors over a snapshot: exact vote
  stream/batches, stage-1 comment/moderation rows, math_main with BOTH
  consumer semantics (env-less latest for 501/group_data; MATH_ENV-filtered
  for 801).
- Stage 1: fetch_comments/fetch_moderation split into SQL fetch + SHARED
  row→dict transformations; the snapshot path routes through the same
  transformations, reproducing production behavior quirk-for-quirk —
  including the mod == '-1' int-vs-string comparisons that never fire
  (deliberately NOT fixed: golden invariance).
- SnapshotPostgresClient — duck-typed read-only stand-in for the umap
  PostgresClient (conversation/comments/selections/votes_latest_unique with
  the PG-boundary sign flip); run_pipeline/501/801 run UNCHANGED against
  it; validates the requested zid against the snapshot's stamped zid.
- GroupDataProcessor(math_main_override=...) and 801's _get_math_main_data
  branch for the raw-.query() math_main reads.
- run_delphi.py forwards --input-source to the three PG-reading stages;
  --snapshot-inputs and --input-source are mutually exclusive (exit 2).

Proof style: FEED EQUALITY (tests/test_input_source_seam.py, RED first) —
live-vs-snapshot exact equality of every feed (comments dicts, moderation,
each vote batch, stage-2 client structures, votes_latest_unique with flip,
math_main per env), on a seeded scratch PG. Downstream code is untouched
and seeded, so feed equality implies output equality; the golden suite
stays the output gate.

Also adds docs/STORAGE_V2_IMPLEMENTATION_NOTES.md: the decisions,
invariants, and traps from P2 through P6b (cross-language contract, store
semantics, DDL sync, job-id threading, snapshot fidelity, seam mechanisms,
workspace/tooling gotchas, continuation checklist) — written so a future
session, human or model, can continue without this session's context.

Full suite: 442 passed, 57 skipped, 58 xfailed (5 pre-existing
DynamoDB-unavailable errors unchanged); golden snapshots ran and passed.

Stack: P1 #2597 … P6a #2602, P6b this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jucor
jucor force-pushed the jc/delphi-storage-v2-p5 branch from abac670 to df5baee Compare July 6, 2026 22:52
@jucor
jucor force-pushed the jc/delphi-storage-v2-p6a branch from 3770aa7 to 2c2d396 Compare July 6, 2026 22:52
@jucor
jucor requested a review from Copilot July 6, 2026 23:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the “capture side” of Storage V2 input snapshots for Delphi pipeline runs, plus a per-job purge utility, to support reproducible runs by recording exactly what the pipeline read from source Postgres at job start.

Changes:

  • Introduces delphi_storage.inputs.capture_run_inputs(...) to snapshot six input kinds into run_inputs with codec envelopes + fingerprints.
  • Adds delphi_storage.purge.purge_job(...) to delete all generic-entity partitions for a job (GDPR/retention foundation).
  • Hooks run_delphi.py to optionally snapshot inputs before running any stages, and imports shared vote SQL constants into stage 1 to prevent drift.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
delphi/delphi_storage/inputs.py Implements snapshot capture + fingerprints and exposes shared vote SQL constants.
delphi/delphi_storage/purge.py Adds a per-job purge helper that deletes all generic-entity partitions for a job_id.
delphi/run_delphi.py Adds --snapshot-inputs / env flag hook to capture inputs pre-stage and abort on capture failure.
delphi/polismath/run_math_pipeline.py Imports vote SQL constants from snapshot module and uses them for stage 1 vote reads.
delphi/tests/test_input_snapshots.py Adds Postgres-backed tests covering snapshot fidelity, SQL parity, purge behavior, and run_delphi hook behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +62 to +69
return psycopg2.connect(
host=os.environ.get("DATABASE_HOST", "localhost"),
port=os.environ.get("DATABASE_PORT", "5432"),
dbname=os.environ.get("DATABASE_NAME", "polis-dev"),
user=os.environ.get("DATABASE_USER", "postgres"),
password=os.environ.get("DATABASE_PASSWORD", ""),
connect_timeout=timeout,
)
if _IN_GHA:
raise
pytest.skip(f"PostgreSQL unavailable: {e}")
scratch_url = base_url.rsplit("/", 1)[0] + f"/{dbname}"
Comment on lines +138 to +144
vote_rows: list = []
for offset in range(0, max(total_votes, 1), batch_size):
cursor = conn.cursor()
cursor.execute(VOTES_BATCH_SQL, (zid, batch_size, offset))
vote_rows.extend(cursor.fetchall())
cursor.close()
payload = _snapshot_payload(["created", "tid", "pid", "vote"], vote_rows)
jucor added a commit that referenced this pull request Jul 6, 2026
…ion notes

Implements the read half of Stack 2 / P6 (docs/STORAGE_V2_DESIGN.md §4.4,
§5): stages can consume a recorded snapshot instead of live PG via
--input-source=store://<job_id> — the seam the replay CLI (P12) drives.
Approved via propose-then-wait (touches the vote-feeding loop in
polismath/run_math_pipeline.py; the seam substitutes data sources only,
never transformations).

Seam mechanisms (in delphi_storage/inputs.py + minimal stage edits):
- SnapshotReader — stage-shaped accessors over a snapshot: exact vote
  stream/batches, stage-1 comment/moderation rows, math_main with BOTH
  consumer semantics (env-less latest for 501/group_data; MATH_ENV-filtered
  for 801).
- Stage 1: fetch_comments/fetch_moderation split into SQL fetch + SHARED
  row→dict transformations; the snapshot path routes through the same
  transformations, reproducing production behavior quirk-for-quirk —
  including the mod == '-1' int-vs-string comparisons that never fire
  (deliberately NOT fixed: golden invariance).
- SnapshotPostgresClient — duck-typed read-only stand-in for the umap
  PostgresClient (conversation/comments/selections/votes_latest_unique with
  the PG-boundary sign flip); run_pipeline/501/801 run UNCHANGED against
  it; validates the requested zid against the snapshot's stamped zid.
- GroupDataProcessor(math_main_override=...) and 801's _get_math_main_data
  branch for the raw-.query() math_main reads.
- run_delphi.py forwards --input-source to the three PG-reading stages;
  --snapshot-inputs and --input-source are mutually exclusive (exit 2).

Proof style: FEED EQUALITY (tests/test_input_source_seam.py, RED first) —
live-vs-snapshot exact equality of every feed (comments dicts, moderation,
each vote batch, stage-2 client structures, votes_latest_unique with flip,
math_main per env), on a seeded scratch PG. Downstream code is untouched
and seeded, so feed equality implies output equality; the golden suite
stays the output gate.

Also adds docs/STORAGE_V2_IMPLEMENTATION_NOTES.md: the decisions,
invariants, and traps from P2 through P6b (cross-language contract, store
semantics, DDL sync, job-id threading, snapshot fidelity, seam mechanisms,
workspace/tooling gotchas, continuation checklist) — written so a future
session, human or model, can continue without this session's context.

Full suite: 442 passed, 57 skipped, 58 xfailed (5 pre-existing
DynamoDB-unavailable errors unchanged); golden snapshots ran and passed.

Stack: P1 #2597 … P6a #2602, P6b this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jucor
jucor force-pushed the jc/delphi-storage-v2-p5 branch from df5baee to 90dc74d Compare July 6, 2026 23:58
@jucor
jucor force-pushed the jc/delphi-storage-v2-p6a branch from 2c2d396 to 00a4376 Compare July 6, 2026 23:58
… + purge tool

Implements the capture half of Stack 2 / P6 (docs/STORAGE_V2_DESIGN.md §4.2
entity 2, §4.4): reproducibility begins with recording EXACTLY what the
pipeline read. The read seam (--input-source, P6b) follows.

- delphi_storage/inputs.py — capture_run_inputs(store, job_id, zid, rid):
  writes the six snapshot kinds as run_inputs items (codec envelopes,
  zstd-compressed votes) and returns per-kind fingerprints (sha256 of the
  canonical uncompressed payload, row_count, max vote created — the P7
  manifest inputs). Key fidelity decisions, pinned by tests:
  - votes = the RAW stream stage 1 consumes: ORDER BY created, raw PG signs
    (the production math path never flips — fetch_votes()'s flip is dead
    code), superseded votes included, order preserved (KMeans init depends
    on vote-encounter order, INVESTIGATION_K_DIVERGENCE.md).
  - The vote SQL is shared BY CONSTRUCTION: run_math_pipeline.py now imports
    VOTES_COUNT_SQL / VOTES_BATCH_SQL from delphi_storage.inputs (strings
    unchanged; golden suite + the e2e SQL pins stay green).
  - comments/participants captured in FULL, including moderated-out rows
    (mod = -1) and banned participants — filtering is pipeline behavior,
    not snapshot behavior.
  - clojure_math_main captured for ALL math_env rows (501 reads env-less
    latest, 801 filters MATH_ENV with a different default — snapshotting
    every env keeps both replayable).
  - derive_votes_latest_unique() reproduces PG's RULE-maintained
    votes_latest_unique from the stream (last vote per (pid, tid) in stream
    order) — tested against a seeded PG table mimicking the RULE.
- delphi_storage/purge.py — purge_job(): deletes every generic-entity
  partition for a job (the §9 GDPR path for snapshot copies).
- run_delphi.py — --snapshot-inputs flag (or DELPHI_SNAPSHOT_INPUTS=1):
  captures before any stage runs, aborts the run on capture failure
  (a run without recorded inputs defeats the point when enabled).
  Off by default until P7's DELPHI_WRITE_MODE subsumes it.

Tests (TDD; tests/test_input_snapshots.py, RED first): 16 tests — raw
stream order/signs, compression, fingerprints, derivation-vs-PG-RULE,
full-row comments/participants, all-env math_main, SQL-parity-by-import,
purge, and the run_delphi hook (flag, env, off-by-default). PG-backed tests
use a seeded scratch database (skip-locally / fail-in-CI convention).

Full suite: 429 passed, 41 skipped, 58 xfailed (5 pre-existing
DynamoDB-unavailable errors unchanged); golden snapshots ran and passed —
math outputs untouched.

Stack: P1 #2597, P2 #2598, P3 #2599, P4 #2600, P5 #2601, P6a this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jucor
jucor force-pushed the jc/delphi-storage-v2-p5 branch from 90dc74d to cf3898c Compare July 10, 2026 11:40
@jucor
jucor force-pushed the jc/delphi-storage-v2-p6a branch from 00a4376 to 320db2c Compare July 10, 2026 11:40
@github-actions

Copy link
Copy Markdown

Delphi Coverage Report

File Stmts Miss Cover
init.py 2 0 100%
benchmarks/bench_pca.py 128 107 16%
benchmarks/bench_repness.py 81 65 20%
benchmarks/bench_update_votes.py 38 28 26%
benchmarks/benchmark_utils.py 34 24 29%
components/init.py 1 0 100%
components/config.py 165 133 19%
conversation/init.py 2 0 100%
conversation/conversation.py 1108 259 77%
conversation/manager.py 131 42 68%
database/init.py 1 0 100%
database/dynamodb.py 395 189 52%
database/postgres.py 306 206 33%
pca_kmeans_rep/init.py 5 0 100%
pca_kmeans_rep/clusters.py 257 21 92%
pca_kmeans_rep/corr.py 98 17 83%
pca_kmeans_rep/pca.py 135 18 87%
pca_kmeans_rep/repness.py 208 9 96%
regression/init.py 4 0 100%
regression/clojure_comparer.py 188 20 89%
regression/comparer.py 887 720 19%
regression/datasets.py 135 27 80%
regression/recorder.py 36 27 25%
regression/utils.py 138 94 32%
run_math_pipeline.py 265 113 57%
umap_narrative/500_generate_embedding_umap_cluster.py 210 109 48%
umap_narrative/501_calculate_comment_extremity.py 116 49 58%
umap_narrative/502_calculate_priorities.py 139 110 21%
umap_narrative/700_datamapplot_for_layer.py 503 469 7%
umap_narrative/701_static_datamapplot_for_layer.py 310 310 0%
umap_narrative/702_consensus_divisive_datamapplot.py 432 432 0%
umap_narrative/801_narrative_report_batch.py 786 725 8%
umap_narrative/802_process_batch_results.py 268 268 0%
umap_narrative/803_check_batch_status.py 183 183 0%
umap_narrative/llm_factory_constructor/init.py 2 0 100%
umap_narrative/llm_factory_constructor/model_provider.py 192 165 14%
umap_narrative/polismath_commentgraph/init.py 1 0 100%
umap_narrative/polismath_commentgraph/cli.py 270 270 0%
umap_narrative/polismath_commentgraph/core/init.py 3 3 0%
umap_narrative/polismath_commentgraph/core/clustering.py 108 108 0%
umap_narrative/polismath_commentgraph/core/embedding.py 104 104 0%
umap_narrative/polismath_commentgraph/lambda_handler.py 219 219 0%
umap_narrative/polismath_commentgraph/schemas/init.py 2 0 100%
umap_narrative/polismath_commentgraph/schemas/dynamo_models.py 160 9 94%
umap_narrative/polismath_commentgraph/tests/conftest.py 17 17 0%
umap_narrative/polismath_commentgraph/tests/test_clustering.py 74 74 0%
umap_narrative/polismath_commentgraph/tests/test_embedding.py 55 55 0%
umap_narrative/polismath_commentgraph/tests/test_storage.py 87 87 0%
umap_narrative/polismath_commentgraph/utils/init.py 3 0 100%
umap_narrative/polismath_commentgraph/utils/converter.py 283 237 16%
umap_narrative/polismath_commentgraph/utils/group_data.py 354 336 5%
umap_narrative/polismath_commentgraph/utils/storage.py 584 518 11%
umap_narrative/reset_conversation.py 165 50 70%
umap_narrative/run_pipeline.py 455 313 31%
utils/general.py 62 41 34%
Total 10895 7380 32%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants