Skip to content

feat(delphi): storage v2 P7b — math stage dual-write + verify_dual_write parity checker#2605

Open
jucor wants to merge 1 commit into
jc/delphi-storage-v2-p7afrom
jc/delphi-storage-v2-p7b
Open

feat(delphi): storage v2 P7b — math stage dual-write + verify_dual_write parity checker#2605
jucor wants to merge 1 commit into
jc/delphi-storage-v2-p7afrom
jc/delphi-storage-v2-p7b

Conversation

@jucor

@jucor jucor commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Stack 2 / P7b of the Storage V2 effort (design in #2597, §4.2 artifacts + §6.1 M1 + §7): the math stage-group dual-write — math results become v2 artifacts alongside the legacy Delphi_* tables, gated by DELPHI_WRITE_MODE, with the design's verify_dual_write parity gate.

Stacked on #2604 (jc/delphi-storage-v2-p7a).

What this adds

Artifact builder (polismath/database/v2_artifacts.py)

Derives the v2 payloads from the same objects the legacy writer consumes (conv.to_dynamo_dict() output, conv.proj, conv.group_clusters), converting Decimals back to floats by value. Keys per design §4.2: math#pca (carrying the full consensus dict — legacy keeps only the agree list), math#kmeans, math#repness, math#routing#<chunk>, math#projections#<chunk> (logical chunks of 5000 participants — 18k+ happen in production — layered on the backend's byte-level auto-chunking).

Legacy quirks reproduced deliberately, each pinned by a test:

  • routing priority/consensus_score default to 0 (the legacy rows always carry both attributes);
  • the participant→group map is raw-keyed exactly like the legacy writer — a str/int key-type mismatch yields group_id: -1 on both sides rather than being "fixed" on one.

The persistence seam (run_math_pipeline.py::persist_math_results)

old → legacy export only (the previous write block extracted verbatim into _export_to_legacy_dynamo; behavior unchanged, including its historical log-and-continue). both → legacy AND v2; v2 failures are logged, never raised (M1: the old path keeps serving). v2 → v2 only; failures exit 1 so the poller marks the run FAILED.

The parity gate (scripts/verify_dual_write.py)

Reads the legacy tables (at latest_math_tick) and the decoded v2 artifacts back independently — zero shared code with the writer — and compares numerically (Decimal vs float by value). CLI: --job-id/--zid/--endpoint-url, exit 0/1.

It proved itself during development: the first builder draft failed verification with three real divergences (the consensus agree-list subset, the routing defaults, the raw-keyed group map) — exactly the class of bug it exists to catch, found before any reviewer saw the code.

Testing (TDD — 12 tests written first, RED on missing modules)

Builder payloads/joins/chunking/quirk-parity; envelope round-trip + math_tick_legacy manifest bookkeeping (tolerant of manifest-less standalone runs); mode gating incl. both failure policies; and an end-to-end parity test against real DynamoDB local — legacy write via the real DynamoDBClient, verify passes; corrupt one v2 value, verify catches it (skip-locally / fail-in-CI convention).

Full suite: 480 passed, 59 skipped, 58 xfailed (5 pre-existing DynamoDB-unavailable errors unchanged). Golden snapshots ran and passed — write-side only, math outputs untouched.

Stack

🤖 Generated with Claude Code

@jucor jucor force-pushed the jc/delphi-storage-v2-p7b branch from d53bbaa to 50f30e3 Compare July 7, 2026 00:33
@jucor

jucor commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Post-push self-review (Claude subagent) — findings triaged, fixes pushed:

  1. numpy crash on real conversations (reviewer's Adds pushing together to CORS whitelist #1): already fixed before the review landed (push race) — conv.proj values are numpy arrays in production; the builder now sanitizes numpy/Decimal recursively (_jsonable), pinned by TestNumpySafety (which passes — the reviewer analyzed the pre-fix revision).
  2. Double to_dynamo_dict() ⇒ divergent math_tick (valid, the important one): math_tick is time-derived inside the serializer, so the two independent calls could tag the legacy tables and v2 artifacts with different ticks on any conversation whose legacy write takes over a second — turning verify_dual_write permanently red on real data, plus double serialization cost. Fixed: persist_math_results serializes ONCE and threads the dict through both writers (write_conversation gains an optional precomputed dynamo_data, default = previous behavior). New test pins single-serialization and that the same dict reaches the legacy writer.
  3. Late + breaking resolve_write_mode() (valid): now resolved at the TOP of main() — a misconfigured env fails fast instead of after the full computation — and test_math_pipeline_runs_e2e.py sets DELPHI_WRITE_MODE=old (it invokes main() directly, bypassing run_delphi's env export; the documented local recipe otherwise regressed).

Suite with services: 530 passed / 58 xfailed (only the 4 known legacy-table test_batch_id errors remain); goldens green.

@jucor jucor force-pushed the jc/delphi-storage-v2-p7b branch 2 times, most recently from d950db4 to dcab2b1 Compare July 7, 2026 00:46
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Delphi Coverage Report

File Stmts Miss Cover
init.py 2 0 100%
benchmarks/bench_pca.py 76 76 0%
benchmarks/bench_repness.py 81 81 0%
benchmarks/bench_update_votes.py 38 38 0%
benchmarks/benchmark_utils.py 34 34 0%
components/init.py 1 0 100%
components/config.py 165 133 19%
conversation/init.py 2 0 100%
conversation/conversation.py 1062 301 72%
conversation/manager.py 131 42 68%
database/init.py 1 0 100%
database/dynamodb.py 388 225 42%
database/postgres.py 306 206 33%
database/v2_artifacts.py 60 0 100%
pca_kmeans_rep/init.py 5 0 100%
pca_kmeans_rep/clusters.py 257 22 91%
pca_kmeans_rep/corr.py 98 17 83%
pca_kmeans_rep/pca.py 52 16 69%
pca_kmeans_rep/repness.py 298 35 88%
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 311 83 73%
umap_narrative/500_generate_embedding_umap_cluster.py 210 109 48%
umap_narrative/501_calculate_comment_extremity.py 124 55 56%
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 802 740 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 358 283 21%
umap_narrative/polismath_commentgraph/utils/storage.py 584 458 22%
umap_narrative/reset_conversation.py 165 50 70%
umap_narrative/run_pipeline.py 466 323 31%
utils/general.py 62 41 34%
Total 10942 7376 33%

…ite parity checker

Implements the math stage-group of Stack 2 / P7 (docs/STORAGE_V2_DESIGN.md
§4.2 artifacts, §6.1 M1, §7): the math results become v2 artifacts alongside
the legacy Delphi_* tables, gated by DELPHI_WRITE_MODE.

- polismath/database/v2_artifacts.py — build_math_artifacts derives the v2
  payloads from the SAME objects the legacy writer consumes
  (conv.to_dynamo_dict() output, conv.proj, conv.group_clusters), converting
  Decimals back to floats by value. Artifact keys per design §4.2:
  math#pca (incl. the FULL consensus dict — legacy keeps only the agree
  list), math#kmeans, math#repness, math#routing#<chunk>,
  math#projections#<chunk> (logical chunks of 5000 participants; 18k+
  happen in production — layered over the backend's byte-level
  auto-chunking). LEGACY QUIRKS REPRODUCED DELIBERATELY, each pinned by a
  test: routing priority/consensus default to 0 (never omitted), and the
  participant→group map is RAW-keyed exactly like the legacy writer — a
  str/int key mismatch yields group_id -1 on BOTH sides.
- write_math_artifacts stores codec envelopes under pk=job_id, records
  math_tick_legacy on the manifest and appends a log line (best-effort for
  standalone runs without a manifest row).
- run_math_pipeline.py — the write block becomes persist_math_results(conv,
  job_id, write_mode): old → legacy export only (unchanged behavior,
  extracted verbatim into _export_to_legacy_dynamo); both → legacy AND v2,
  v2 failures logged never raised (old path keeps serving); v2 → v2 only,
  failures exit 1 so the poller marks the run FAILED.
- scripts/verify_dual_write.py — the M1 parity gate: reads the legacy
  tables (at latest_math_tick) and the decoded v2 artifacts back
  INDEPENDENTLY (zero shared code with the writer) and compares numerically
  (Decimal vs float by value). CLI: --job-id/--zid/--endpoint-url, exit
  0/1. During development it caught three real divergences in the first
  builder draft (consensus agree-list only, routing defaults, raw-keyed
  group map) — exactly the failure mode it exists to catch.

Tests (TDD; tests/test_math_dual_write.py, RED first): 12 tests — builder
payloads/joins/chunking/quirks, envelope round-trip + manifest bookkeeping,
mode gating incl. failure policies, and an end-to-end parity test against
real DynamoDB local (legacy write via the REAL DynamoDBClient, then
verify passes; corrupt one v2 value, verify catches it). Skip-locally /
fail-in-CI convention.

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

Stack: P1 #2597 … P7a #2604, P7b this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jucor added a commit that referenced this pull request Jul 10, 2026
…extension

Implements the UMAP stage-group of Stack 2 / P7 (docs/STORAGE_V2_DESIGN.md
§4.2, §6.1 M1, §7): the seven 500s outputs become v2 artifacts alongside the
legacy Delphi_* tables, gated by DELPHI_WRITE_MODE.

- umap_narrative/v2_artifacts.py — the dual-write seam is a DUCK-TYPED
  WRAPPER (DualWriteUmapStorage) over the legacy DynamoDBStorage:
  run_pipeline keeps calling the same seven write methods, and the wrapper
  feeds the SAME already-materialized Pydantic model lists to both stores.
  Serialize-once holds by construction — the models carry baked datetime
  fields (ConversationMeta.processed_date, LLMTopicName.created_at);
  re-instantiating them for a second path would be the math_tick trap again.
  Artifact keys per design §4.2: umap#meta, umap#embeddings#<chunk> (500/
  chunk — fat vector rows), umap#graph#<chunk>, umap#assignments#<chunk>
  (5000/chunk), umap#keywords#<layer>, umap#features#<layer>,
  umap#topic#<layer>#<cluster>. Failure policy: both → logged never raised;
  v2 → raised. finalize() records stage_status.umap + a log line on the
  manifest (best-effort for standalone runs).
- make_umap_storage — old mode returns the PLAIN legacy storage (zero
  overhead, byte-identical); both/v2 return the wrapper (v2-only with no
  legacy inner).
- run_pipeline.py — write mode resolved up front (fail-fast, before
  process_comments and all writes); the storage-init block constructs the
  legacy client only when old writes are enabled and routes everything
  through the factory; ALL SEVEN call sites are untouched. finalize() after
  layer processing.
- scripts/verify_dual_write.py — verify_umap_dual_write (independent reads
  of the seven conversation_id-keyed legacy tables vs decoded umap#
  artifacts; SAME-RUN comparisons only — EVōC cluster ids are not stable
  across runs); CLI gains --stage math|umap|all.

Tests (TDD; tests/test_umap_dual_write.py, RED first): 12 tests — builders
(chunking, per-layer grouping, per-cluster topic keys) from real Pydantic
models, wrapper semantics (same-list dual writes, v2-only synthesis,
failure policies, manifest finalize), factory matrix, run_pipeline wiring
pin, and an end-to-end parity test against real DynamoDB local (legacy
write via the REAL DynamoDBStorage, verify passes; corrupt umap#meta,
verify catches it).

Full suite: 493 passed, 60 skipped, 58 xfailed (5 pre-existing
DynamoDB-unavailable errors unchanged); golden snapshots ran and passed —
write-side only.

Stack: P1 #2597 … P7b #2605, P7c this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jucor jucor force-pushed the jc/delphi-storage-v2-p7a branch from 14c9f43 to c3c973f Compare July 10, 2026 11:40
@jucor jucor force-pushed the jc/delphi-storage-v2-p7b branch from dcab2b1 to 381d2cf Compare July 10, 2026 11:40
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.

1 participant