From d4aa64fe321e759cae57fb7531326e65785a9526 Mon Sep 17 00:00:00 2001 From: Julien Cornebise Date: Mon, 27 Jul 2026 17:56:37 +0200 Subject: [PATCH] =?UTF-8?q?python-math=20#42:=20feat(delphi):=20mode=20col?= =?UTF-8?q?lapse=20C7=20=E2=80=94=20harness=20identifier=20purge=20+=20fla?= =?UTF-8?q?g=20machinery=20deletion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Final chunk of the `GOAL_CUTOVER_READY.md` Phase 2 mode collapse — the removal of the engine-mode switch between clojure-legacy (the setting that reproduces the old Clojure engine exactly) and improved behavior. The goal doc's DONE-gate grep (`ENGINE_MODE|engine_mode|resolve_engine_mode` over `delphi/polismath/`) now returns ZERO hits. ## Changes - `certify.py` (the certification-battery harness): `BatteryEntry` / `derive_schedule_id` / `parse_battery_entry` drop the mode. Schedule ids keep their historical `-clojure-legacy` suffix via the frozen `_LEGACY_SUFFIX` literal, and fingerprints bake the same literal into the digest — so recording directories AND every historical key in `divergences.json` (the ledger of observed Python-vs-Clojure divergences) stay valid. The manifest drops its mode key, riding the same Python-side re-record that the engine change already forced. `run_py_driver` no longer sets any env; `compare_recording_pair` drops its dead mode parameter; battery reports and ledger observations drop the mode field (historical ledger values keep theirs on disk). - `poller_equiv.py` (the live Python-vs-Clojure poller equivalence harness): env plumbing deleted (`build_py_env` / `PyPollerRunner` / `EquivConfig`). `store.py` provenance drops the env record. - `polismath/utils/engine_mode.py` DELETED; `scripts/certify_battery.json` entries drop the key. - Test sweep: engine-mode imports, fixtures, and setenv calls removed across 20 test files; `test_engine_mode.py` and the two env-guard conftest fixtures deleted; 9 default-mode tests re-pinned to the now-only legacy semantics (arrival-order tid export, inert bans, bucket votes-base, the 1x1 real-math edge case, base-cluster-id (bid) group members, greedy-floor-aware D2c); the D9/D10 comparisons on the vw dataset's cold-start variant now XPASS (parity improved). ## Testing Full suite: 1155 passed / 22 skipped / 44 xfailed / 2 xpassed. commit-id:7f42df81 --- delphi/docs/CLJ-PARITY-FIXES-JOURNAL.md | 54 ++++ delphi/docs/CLOJURE_QUIRKS.md | 8 +- delphi/docs/MATH_POLLER_DESIGN.md | 8 +- delphi/polismath/replay/certify.py | 152 +++++------ delphi/polismath/replay/poller_equiv.py | 15 +- delphi/polismath/replay/store.py | 5 - delphi/polismath/utils/engine_mode.py | 49 ---- delphi/scripts/certify_battery.json | 244 ++++++++---------- delphi/scripts/poller_equiv.py | 15 +- delphi/tests/conftest.py | 17 -- delphi/tests/poller/conftest.py | 30 --- .../tests/poller/test_integration_postgres.py | 2 +- delphi/tests/replay_harness/test_certify.py | 86 +++--- .../tests/replay_harness/test_certify_cli.py | 10 +- delphi/tests/replay_harness/test_driver.py | 9 - .../replay_harness/test_poller_equiv_seed.py | 11 +- delphi/tests/replay_harness/test_store.py | 1 - delphi/tests/test_base_cluster_lineage.py | 7 - delphi/tests/test_clj_hash_order.py | 3 - delphi/tests/test_conversation.py | 25 +- delphi/tests/test_degenerate_tick_parity.py | 2 - delphi/tests/test_discrepancy_fixes.py | 82 +++--- delphi/tests/test_edge_cases.py | 14 +- delphi/tests/test_engine_mode.py | 221 ---------------- delphi/tests/test_env_flags.py | 19 +- delphi/tests/test_group_k_smoother.py | 3 - delphi/tests/test_in_conv_greedy_carry.py | 2 - delphi/tests/test_legacy_blob_shape.py | 4 +- .../test_math_writer_numpy_serialization.py | 18 +- delphi/tests/test_mod_ptpt_leak_parity.py | 2 - delphi/tests/test_mod_update_parity.py | 3 +- delphi/tests/test_pca_warm_start.py | 3 - delphi/tests/test_priority_unmirror.py | 2 - delphi/tests/test_serialization_unfolding.py | 18 +- docker-compose.yml | 3 - example.env | 4 - 36 files changed, 397 insertions(+), 754 deletions(-) delete mode 100644 delphi/polismath/utils/engine_mode.py delete mode 100644 delphi/tests/poller/conftest.py delete mode 100644 delphi/tests/test_engine_mode.py diff --git a/delphi/docs/CLJ-PARITY-FIXES-JOURNAL.md b/delphi/docs/CLJ-PARITY-FIXES-JOURNAL.md index 1a785b18c..801ad0a3a 100644 --- a/delphi/docs/CLJ-PARITY-FIXES-JOURNAL.md +++ b/delphi/docs/CLJ-PARITY-FIXES-JOURNAL.md @@ -4159,3 +4159,57 @@ changes. Recovered from jj's last auto-snapshot (`git show green. Rule: in this repo, undo scratch edits with a targeted edit (sed/ editor), NEVER `git checkout --`/`git restore` (index = parent, not @), and NEVER `jj restore --from @-` for a file carrying uncommitted work. + +### Phase 2 — mode collapse EXECUTED (s7, same session) + +Seven chunks, one commit each on the spr stack (per-item split so the +improvements/* park commits can be minted as exact reverse patches): + +- **C1** (item 2 parked): degenerate guards — PCA empty-only short-circuit, + no <2-in-conv / <2-base-cluster early returns, repness at every size. +- **C2a** (item 4 parked): Q15 watermark drop unconditional. +- **C2b** (item 5 parked): Q2 prev-tick group-votes unconditional. +- **C3** (item 8 parked): powerit warm-start PCA + legacy_kmeans (base + lineage + per-k group loop + smoother) as the only solvers; sklearn arms + deleted. POLISMATH_PCA_IMPL left in pca.py but ENGINE-INERT (unconditional + require_powerit always falls back to powerit) — full removal ships with + item 8; deleting now would cascade through 9 test files for zero behavior + change (scope ruling). +- **C4**: delete-only branches — Q1 ban filter (dropped feature, item 1), + tally sources always raw, carry+greedy always, bucket votes-base, + every-group gac product, unconditional legacy blob shape + restore seam, + clustered-only repness rest domain. +- **C5+C6**: driver mod_update-only mod semantics; poller engine_mode + config/passthrough deleted. +- **C7**: harness identifier purge (certify/poller_equiv/store/battery + JSON), engine_mode.py + test_engine_mode.py deleted, 20-test-file sweep, + 9 default-mode tests re-pinned to legacy semantics. + +Evidence: DONE-gate grep = 0 hits over delphi/polismath/. Full suite +1155/22/44 green (+2 XPASS: D9/D10 vw-cold_start now match Clojure — +parity IMPROVED by the collapse). Schedule ids, recording dirs, and all +historical divergences.json fingerprint keys preserved via the frozen +"clojure-legacy" literal. Battery re-record launched on the collapsed +tree (py re-replay; results in the next entry). + +Review notes on #2664 (Phase 0, review subagent): no findings; two +non-blocking observations recorded — no stress test for concurrent +same-key verdict-cache writes, and the parallel clj-side path has not +been exercised with a cold clj cache (failure mode would be a loud +ERROR, not a silent MATCH). + +### Post-collapse battery: 20/20 MATCH ×2 (s7) + +First pass on the collapsed tree (full py re-replay, clj oracle cached): +**20/20 MATCH, zero divergences, 19m21s wall / 37m48s user** — the collapse +is bit-exact vs the Clojure recordings on every battery entry. Cached +second pass immediately after: 20/20 MATCH (~22s). NOTE (evidence +coherence): DONE condition 4's two consecutive clean passes must re-run on +the FINAL tree after Phase 3 (clarity refactor) + Phase 4 (goldens) — these +runs certify the collapse itself. + +Also purged post-suite: scripts/poller_equiv.py CLI --engine-mode plumbing +(would have crashed the equiv gate CLI: kwargs no longer exist), +docker-compose delphi-math-poller env line, example.env comment, +CLOJURE_QUIRKS preamble + MATH_POLLER_DESIGN updated to collapse-era +wording (historical spec docs left as records). diff --git a/delphi/docs/CLOJURE_QUIRKS.md b/delphi/docs/CLOJURE_QUIRKS.md index 93b65b3cb..75fe1663f 100644 --- a/delphi/docs/CLOJURE_QUIRKS.md +++ b/delphi/docs/CLOJURE_QUIRKS.md @@ -41,9 +41,11 @@ Status legend: **REPLICATED** (Python legacy mode reproduces it) · Notes: -- "Replicate the bug" always means **behind `POLISMATH_ENGINE_MODE=clojure-legacy` - only**. `improved` mode keeps (or gains) the correct behavior; each row's - "Later fix" column is the improved-mode / upstream story. +- "Replicate the bug" historically meant **behind + `POLISMATH_ENGINE_MODE=clojure-legacy` only**. Since the mode collapse + (2026-07-27) the legacy semantics ARE the engine's only path — the flag is + gone; each row's "Later fix" column is now the post-cutover queue story + (POST_CUTOVER_IMPROVEMENTS.md). - Never fix a Q-row silently as a side effect of another change: certification compares against real Clojure output, and an accidental "fix" in legacy mode shows up as a divergence. diff --git a/delphi/docs/MATH_POLLER_DESIGN.md b/delphi/docs/MATH_POLLER_DESIGN.md index b7f45ddc4..d94a459fe 100644 --- a/delphi/docs/MATH_POLLER_DESIGN.md +++ b/delphi/docs/MATH_POLLER_DESIGN.md @@ -53,10 +53,8 @@ scripts/math_poller.py (CLI) │ coalescing (drain queue, merge batches, votes-then-moderation order, │ mirroring take-all!/split-batches), bounded pool across zids ├─ engine: Conversation chain in-memory (update_votes/update_moderation → - │ recompute) — POLISMATH_ENGINE_MODE honored; recommend clojure-legacy - │ during shadow/parity phases (warm-started PCA + k-smoother = the - │ sequential semantics Clojure has; improved mode is the post-cutover - │ option, pending persisted-comps sign stability) + │ recompute) — Clojure-exact legacy semantics, the engine's only + │ path since the mode collapse (2026-07-27) ├─ load-or-init: on first message for a zid, restore from math_main │ (from_dict) + rebuild rating matrices from full vote history │ (conv-poll offset 0 analog), mirroring conv_man.clj:188-207; @@ -77,7 +75,7 @@ Config (mirrors Clojure + delphi's existing unwired poller config, `delphi/polismath/components/config.py:216-269`): `DATABASE_URL`, `MATH_ENV` (the math_env string written), `VOTE_POLLING_INTERVAL` (ms, default 1000), `MOD_POLLING_INTERVAL` (1000), `POLL_FROM_DAYS_AGO` (10), `MATH_ZID_ALLOWLIST` / -`MATH_ZID_BLOCKLIST`, `POLISMATH_ENGINE_MODE`, worker-pool size. +`MATH_ZID_BLOCKLIST`, worker-pool size. ## 4. Cutover phases diff --git a/delphi/polismath/replay/certify.py b/delphi/polismath/replay/certify.py index 9f3ca7dce..e2e167698 100644 --- a/delphi/polismath/replay/certify.py +++ b/delphi/polismath/replay/certify.py @@ -6,12 +6,10 @@ result. This module turns that into a repeatable, cheap-to-re-run BATTERY: - A committed battery config (``scripts/certify_battery.json``) declares which - (dataset, schedule) pairs to certify, and under which - :mod:`polismath.utils.engine_mode` the Python side runs. ``clojure-legacy`` - is the parity target (Clojure's warm-start behavior); ``improved`` is - Python's production default. certify sets the mode EXPLICITLY per entry - (never inherited from the ambient environment) by launching the Python - driver in a subprocess with the env var freshly set — see :func:`run_py_driver`. + (dataset, schedule) pairs to certify. Since the mode collapse (2026-07-27) + the engine has exactly ONE code path — Clojure-exact legacy semantics — so + the battery needs no per-entry mode; schedule ids keep their historical + ``-clojure-legacy`` suffix so recordings and ledger keys stay valid. - Both engines' recordings are CACHED on disk, keyed by content hashes (votes CSV, resolved schedule, and — for Python — the ``polismath`` source tree, or — for Clojure — ``dev/replay.clj`` + the ``math/src`` tree). Re-running @@ -26,18 +24,13 @@ — see CLOJURE_QUIRKS.md Q7). This is never silent: every certify run prints :data:`ACCEPTANCE_NOTICE`. - Every divergence is FINGERPRINTED (index/step-stripped path pattern + family - + engine_mode -> 10 hex chars) and tracked in a committed ledger + + frozen legacy suffix -> 10 hex chars) and tracked in a committed ledger (``docs/divergences.json``) so recurring, already-diagnosed divergences are annotated instead of re-discovered cold every run. -Design note — the clj cache is scoped PER (dataset, schedule_id) directory (as -literally specified), not globally content-addressed across engine-mode -siblings of the same underlying schedule. Two battery entries that share a -preset+cuts but differ only in ``engine_mode`` will each get their own -``/clj/`` (and therefore each pay for one clojure driver run) even -though the clj recording would be byte-identical — engine_mode has no effect -on the Clojure reference. The starter battery (all ``clojure-legacy``) never -hits this; flagged here for whoever adds a second engine_mode to the battery. +Design note — the clj cache is scoped PER (dataset, schedule_id) directory +(as literally specified), not globally content-addressed across schedules +with identical cuts. """ from __future__ import annotations @@ -66,12 +59,12 @@ ) from polismath.replay.stepcompare import DEFAULT_TOLERANT_STAT_KEYS, StepComparer from polismath.replay.types import ReplayDataset -from polismath.utils.engine_mode import ( - ENGINE_MODE_CHOICES, - ENGINE_MODE_DEFAULT, - ENGINE_MODE_ENV_VAR, - ENGINE_MODE_LEGACY, -) + +#: Frozen schedule-id suffix + fingerprint component. Battery schedule ids +#: and ledger fingerprint keys were minted while the engine still had a mode +#: flag; this literal keeps recording directories and the historical +#: divergences.json keys stable across the mode collapse (2026-07-27). +_LEGACY_SUFFIX = "clojure-legacy" # --------------------------------------------------------------------------- # Paths. @@ -140,10 +133,9 @@ def compare_step(self, blob_a: dict, blob_b: dict, index: int) -> dict[str, Any] class BatteryEntry: """One parsed ``certify_battery.json`` entry (either preset- or schedule-file-based), with its collision-free ``schedule_id`` already - resolved (bakes in ``engine_mode`` — see :func:`derive_schedule_id`).""" + resolved (see :func:`derive_schedule_id`).""" dataset: str - engine_mode: str schedule_id: str preset: str | None = None n_cuts: int | None = None @@ -152,17 +144,18 @@ class BatteryEntry: def derive_schedule_id( - *, engine_mode: str, preset: str | None = None, n_cuts: int | None = None, + *, preset: str | None = None, n_cuts: int | None = None, base_schedule_id: str | None = None, ) -> str: - """Collision-free schedule id: ``{base}-{engine_mode}``. + """Collision-free schedule id: ``{base}-clojure-legacy`` + (:data:`_LEGACY_SUFFIX` — historical, keeps recording dirs stable). ``base`` is either an explicit ``base_schedule_id`` (schedule-file-based entries — the id the file itself declares) or ``{preset}{n_cuts}`` for presets that take a cut count (``uniform8``, ``front-loaded6``, …) or bare ``preset`` for those that don't (``single-cut``, ``every-vote``, ``per-day``). - Distinct (preset, n_cuts, engine_mode) triples always yield distinct ids - because the preset name is embedded verbatim in ``base``. + Distinct (preset, n_cuts) pairs always yield distinct ids because the + preset name is embedded verbatim in ``base``. """ if base_schedule_id is not None: base = base_schedule_id @@ -172,16 +165,14 @@ def derive_schedule_id( base = f"{preset}{n_cuts}" else: base = preset - return f"{base}-{engine_mode}" + return f"{base}-{_LEGACY_SUFFIX}" def parse_battery_entry(e: dict[str, Any], *, battery_dir: Path | None = None) -> BatteryEntry: """Parse one battery entry — either ``{"schedule": ""}`` (base id read verbatim from the referenced schedule.json) or ``{"preset": ..., - "n_cuts": ...}``. ``engine_mode`` defaults to ``clojure-legacy`` (the - parity target) when absent — the starter battery spells it out anyway.""" + "n_cuts": ...}``.""" dataset = e["dataset"] - engine_mode = e.get("engine_mode", ENGINE_MODE_LEGACY) if "schedule" in e: schedule_path = Path(e["schedule"]) @@ -196,8 +187,8 @@ def parse_battery_entry(e: dict[str, Any], *, battery_dir: Path | None = None) - f"would disagree on which dataset's votes to replay/cache" ) base_id = schedule_json["schedule_id"] - schedule_id = derive_schedule_id(engine_mode=engine_mode, base_schedule_id=base_id) - return BatteryEntry(dataset=dataset, engine_mode=engine_mode, schedule_id=schedule_id, + schedule_id = derive_schedule_id(base_schedule_id=base_id) + return BatteryEntry(dataset=dataset, schedule_id=schedule_id, schedule_path=schedule_path, notes=e.get("notes", "")) preset = e.get("preset") @@ -209,8 +200,8 @@ def parse_battery_entry(e: dict[str, Any], *, battery_dir: Path | None = None) - n_cuts = e.get("n_cuts") if preset in _NCUTS_PRESETS and n_cuts is None: raise ValueError(f"preset {preset!r} requires n_cuts in battery entry {e!r}") - schedule_id = derive_schedule_id(engine_mode=engine_mode, preset=preset, n_cuts=n_cuts) - return BatteryEntry(dataset=dataset, engine_mode=engine_mode, schedule_id=schedule_id, + schedule_id = derive_schedule_id(preset=preset, n_cuts=n_cuts) + return BatteryEntry(dataset=dataset, schedule_id=schedule_id, preset=preset, n_cuts=n_cuts, notes=e.get("notes", "")) @@ -266,7 +257,7 @@ def _spec_from_preset(entry: BatteryEntry, ds: ReplayDataset) -> sched.ScheduleS def build_effective_spec(entry: BatteryEntry, ds: ReplayDataset) -> sched.ScheduleSpec: """The :class:`ScheduleSpec` actually run, with ``schedule_id`` overridden - to ``entry.schedule_id`` (the collision-free, engine_mode-baked id) so the + to ``entry.schedule_id`` (the collision-free suffixed id) so the recording lands in the right directory regardless of preset or file origin. """ base = (sched.ScheduleSpec.from_json_file(entry.schedule_path) if entry.schedule_path @@ -376,22 +367,24 @@ def normalize_path(path: str) -> str: return ".".join(parts) -def _fp_from_normalized(norm_path: str, family: str, engine_mode: str) -> str: - digest = hashlib.sha1(f"{norm_path}|{family}|{engine_mode}".encode()).hexdigest() +def _fp_from_normalized(norm_path: str, family: str) -> str: + # _LEGACY_SUFFIX is baked into the digest so every historical + # divergences.json key stays valid across the mode collapse. + digest = hashlib.sha1(f"{norm_path}|{family}|{_LEGACY_SUFFIX}".encode()).hexdigest() return digest[:10] -def compute_fingerprint(path: str, family: str, engine_mode: str) -> str: - return _fp_from_normalized(normalize_path(path), family, engine_mode) +def compute_fingerprint(path: str, family: str) -> str: + return _fp_from_normalized(normalize_path(path), family) -def fingerprint_key_for(path_pattern: str, family: str, engine_mode: str) -> str: +def fingerprint_key_for(path_pattern: str, family: str) -> str: """Ledger key for an ALREADY-normalized path pattern.""" - return f"FP-{_fp_from_normalized(path_pattern, family, engine_mode)}" + return f"FP-{_fp_from_normalized(path_pattern, family)}" -def fingerprint_key(path: str, family: str, engine_mode: str) -> str: - return fingerprint_key_for(normalize_path(path), family, engine_mode) +def fingerprint_key(path: str, family: str) -> str: + return fingerprint_key_for(normalize_path(path), family) def _abbrev(value: Any) -> Any: @@ -424,18 +417,19 @@ def update_ledger(ledger: dict[str, Any], observations: list[dict[str, Any]]) -> ``status=open``. NEVER overwrites an existing entry — a human-entered ``diagnosis``/``status`` on a known fingerprint is always preserved. - Each observation: ``{"path_pattern", "family", "engine_mode", "dataset", + Each observation: ``{"path_pattern", "family", "dataset", "schedule_id", "step"}`` (``path_pattern`` already normalized). + (Historical ledger entries carry a mode field from before the collapse; + it is preserved on disk and simply no longer written for new entries.) """ updated = dict(ledger) for obs in observations: - key = fingerprint_key_for(obs["path_pattern"], obs["family"], obs["engine_mode"]) + key = fingerprint_key_for(obs["path_pattern"], obs["family"]) if key in updated: continue updated[key] = { "path_pattern": obs["path_pattern"], "family": obs["family"], - "engine_mode": obs["engine_mode"], "first_seen": {"dataset": obs["dataset"], "schedule": obs["schedule_id"], "step": obs["step"]}, "status": "open", @@ -479,16 +473,13 @@ def _run_subprocess(cmd: list[str], *, cwd: Path, env: dict[str, str], text=True, timeout=timeout) -def run_py_driver(spec_path: Path, *, out_root: Path, engine_mode: str) -> subprocess.CompletedProcess: +def run_py_driver(spec_path: Path, *, out_root: Path) -> subprocess.CompletedProcess: """Runs ``scripts/replay_driver.py run --schedule --out `` in a SUBPROCESS (cwd=delphi/) with ``OMP_NUM_THREADS`` / - ``OPENBLAS_NUM_THREADS`` pinned to 1 and ``POLISMATH_ENGINE_MODE`` set - explicitly — so the mode is picked up fresh per entry, never inherited - from whatever happens to be in the calling shell's environment.""" + ``OPENBLAS_NUM_THREADS`` pinned to 1.""" env = dict(os.environ) env["OMP_NUM_THREADS"] = "1" env["OPENBLAS_NUM_THREADS"] = "1" - env[ENGINE_MODE_ENV_VAR] = engine_mode cmd = ["uv", "run", "python", "scripts/replay_driver.py", "run", "--schedule", str(spec_path), "--out", str(out_root)] try: @@ -592,7 +583,7 @@ def ensure_py_recording( refresh: bool = False, ) -> tuple[Path, bool]: """Reuse ``///py/`` iff its cache manifest matches (votes - sha256, schedule hash, engine_mode, ENGINE-scoped tree hash); else (re)run + sha256, schedule hash, ENGINE-scoped tree hash); else (re)run the Python driver in a subprocess. Returns ``(py_dir, was_cached)``. The 2026-07-27 switch from the full-``polismath`` tree hash to the @@ -605,14 +596,13 @@ def ensure_py_recording( expected = { "votes_sha256": votes_sha, "schedule_hash": canonical_schedule_hash(spec), - "engine_mode": entry.engine_mode, "engine_tree_sha256": _engine_tree_hash_cached(), } if not refresh and _manifest_matches(manifest_path, expected): return py_dir, True tmp_schedule = _write_temp_schedule(spec, root) - result = run_py_driver(tmp_schedule, out_root=root, engine_mode=entry.engine_mode) + result = run_py_driver(tmp_schedule, out_root=root) if result.returncode != 0: raise CertifyError( "py-driver", (result.stderr or result.stdout or "non-zero exit").strip()[:1000] @@ -672,7 +662,7 @@ def _step_verdict_cache_path(cache_root: Path, clj_hash: str, py_hash: str, cfg_ def compare_recording_pair( - clj_dir: str | Path, py_dir: str | Path, *, engine_mode: str, cache_root: str | Path, + clj_dir: str | Path, py_dir: str | Path, *, cache_root: str | Path, comparer: StepComparer | None = None, ) -> dict[str, Any]: """Hash-first, cached comparison of one clj/py recording pair. @@ -736,7 +726,7 @@ def compare_recording_pair( } -def _summarize_divergences(cmp_result: dict[str, Any], *, engine_mode: str) -> dict[str, Any]: +def _summarize_divergences(cmp_result: dict[str, Any]) -> dict[str, Any]: """Aggregate ALL divergences across every divergent step into distinct (normalized path, family) patterns, ranked by frequency (ties broken alphabetically for determinism) — top ≤3 for display, full set for the @@ -761,7 +751,7 @@ def _summarize_divergences(cmp_result: dict[str, Any], *, engine_mode: str) -> d top_paths = [ { "path_pattern": path_pattern, "family": fam, "count": count, - "fingerprint": fingerprint_key_for(path_pattern, fam, engine_mode), + "fingerprint": fingerprint_key_for(path_pattern, fam), "a": _abbrev(examples[(path_pattern, fam)][0]), "b": _abbrev(examples[(path_pattern, fam)][1]), } @@ -792,8 +782,7 @@ def _certify_entry_heavy( the strictly-serial ledger fold (:func:`_fold_entry_into_ledger`).""" if not dataset_available(entry.dataset): return {"dataset": entry.dataset, "schedule_id": entry.schedule_id, - "engine_mode": entry.engine_mode, "verdict": "SKIPPED", - "reason": "dataset-unavailable"} + "verdict": "SKIPPED", "reason": "dataset-unavailable"} try: votes_csv = votes_csv_path(entry.dataset) @@ -815,32 +804,27 @@ def _certify_entry_heavy( py_dir, _ = ensure_py_recording(entry, spec, votes_sha, root=root, refresh=refresh_py) except CertifyError as exc: return {"dataset": entry.dataset, "schedule_id": entry.schedule_id, - "engine_mode": entry.engine_mode, "verdict": "ERROR", - "stage": exc.stage, "reason": str(exc)} + "verdict": "ERROR", "stage": exc.stage, "reason": str(exc)} except Exception as exc: # noqa: BLE001 - one bad entry must not crash the battery return {"dataset": entry.dataset, "schedule_id": entry.schedule_id, - "engine_mode": entry.engine_mode, "verdict": "ERROR", - "stage": "setup", "reason": str(exc)} + "verdict": "ERROR", "stage": "setup", "reason": str(exc)} - cmp_result = compare_recording_pair(clj_dir, py_dir, engine_mode=entry.engine_mode, - cache_root=root) + cmp_result = compare_recording_pair(clj_dir, py_dir, cache_root=root) if cmp_result["step_count_mismatch"]: return {"dataset": entry.dataset, "schedule_id": entry.schedule_id, - "engine_mode": entry.engine_mode, "verdict": "ERROR", - "stage": "step-count-mismatch", + "verdict": "ERROR", "stage": "step-count-mismatch", "reason": f"clj={cmp_result['n_steps_clj']} steps, " f"py={cmp_result['n_steps_py']} steps"} div_steps = [s for s in cmp_result["per_step"] if not s["match"]] if not div_steps: return {"dataset": entry.dataset, "schedule_id": entry.schedule_id, - "engine_mode": entry.engine_mode, "verdict": "MATCH", - "n_steps": cmp_result["aligned_steps"]} + "verdict": "MATCH", "n_steps": cmp_result["aligned_steps"]} - summary = _summarize_divergences(cmp_result, engine_mode=entry.engine_mode) + summary = _summarize_divergences(cmp_result) return {"dataset": entry.dataset, "schedule_id": entry.schedule_id, - "engine_mode": entry.engine_mode, "verdict": "DIVERGENCE", + "verdict": "DIVERGENCE", "first_div_step": summary["first_div_step"], "n_div_steps": summary["n_div_steps"], "_summary": summary} @@ -861,7 +845,7 @@ def _fold_entry_into_ledger( observations = [ {"path_pattern": o["path_pattern"], "family": o["family"], - "engine_mode": result["engine_mode"], "dataset": result["dataset"], + "dataset": result["dataset"], "schedule_id": result["schedule_id"], "step": o["step"]} for o in summary["all_observed"] ] @@ -1001,15 +985,6 @@ def render_run_lines(report: dict[str, Any], *, max_lines: int = 40) -> list[str # --------------------------------------------------------------------------- # Focuser: first-divergence-only inspection of an EXISTING recording pair. # --------------------------------------------------------------------------- -def _engine_mode_from_schedule_id(schedule_id: str) -> str: - """certify's own ``derive_schedule_id`` always suffixes ``-{engine_mode}``; - recover it from known suffixes (best-effort fallback to the default).""" - for mode in ENGINE_MODE_CHOICES: - if schedule_id.endswith(f"-{mode}"): - return mode - return ENGINE_MODE_DEFAULT - - def run_focus( dataset: str, schedule_id: str, *, root: Path | None = None, ledger_path: str | Path | None = None, @@ -1029,13 +1004,12 @@ def run_focus( "stage": "recording-missing", "reason": f"expected clj/ and py/ both present under {rec_dir}"} - engine_mode = _engine_mode_from_schedule_id(schedule_id) ledger = load_ledger(ledger_path) - cmp_result = compare_recording_pair(clj_dir, py_dir, engine_mode=engine_mode, cache_root=root) + cmp_result = compare_recording_pair(clj_dir, py_dir, cache_root=root) div_steps = [s for s in cmp_result["per_step"] if not s["match"]] _write_json(rec_dir / "focus-report.json", { - "dataset": dataset, "schedule_id": schedule_id, "engine_mode": engine_mode, + "dataset": dataset, "schedule_id": schedule_id, "n_steps_clj": cmp_result["n_steps_clj"], "n_steps_py": cmp_result["n_steps_py"], "step_count_mismatch": cmp_result["step_count_mismatch"], "first_divergent_step": div_steps[0]["step"] if div_steps else None, @@ -1043,7 +1017,7 @@ def run_focus( }) if not div_steps: - return {"dataset": dataset, "schedule_id": schedule_id, "engine_mode": engine_mode, + return {"dataset": dataset, "schedule_id": schedule_id, "verdict": "MATCH", "n_steps": cmp_result["aligned_steps"], "focus_report_path": str(rec_dir / "focus-report.json")} @@ -1054,20 +1028,20 @@ def run_focus( for d in step["families"][fam]: path = d.get("path") or "" norm = normalize_path(path) - key = fingerprint_key_for(norm, fam, engine_mode) + key = fingerprint_key_for(norm, fam) families[fam].append({ "path": path, "path_pattern": norm, "a": _abbrev(d.get("a")), "b": _abbrev(d.get("b")), "fingerprint": key, "known": annotate_by_key(ledger, key), }) - observations.append({"path_pattern": norm, "family": fam, "engine_mode": engine_mode, + observations.append({"path_pattern": norm, "family": fam, "dataset": dataset, "schedule_id": schedule_id, "step": step["step"]}) ledger = update_ledger(ledger, observations) save_ledger(ledger, ledger_path) - return {"dataset": dataset, "schedule_id": schedule_id, "engine_mode": engine_mode, + return {"dataset": dataset, "schedule_id": schedule_id, "verdict": "DIVERGENCE", "step": step["step"], "families": families, "focus_report_path": str(rec_dir / "focus-report.json")} diff --git a/delphi/polismath/replay/poller_equiv.py b/delphi/polismath/replay/poller_equiv.py index 7b8802c6d..9ba776d04 100644 --- a/delphi/polismath/replay/poller_equiv.py +++ b/delphi/polismath/replay/poller_equiv.py @@ -138,7 +138,6 @@ from polismath.replay.stepcompare import DEFAULT_TOLERANT_STAT_KEYS, StepComparer from polismath.replay.store import _safe_path_component from polismath.replay.types import ModEvent, ReplayDataset -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR, ENGINE_MODE_LEGACY from polismath.utils.general import delphi_vote_to_postgres # poller_equiv.py -> replay -> polismath -> delphi -> repo root (mirrors @@ -818,16 +817,13 @@ def build_py_env( database_url: str, math_env: str, poll_from_days_ago: float = 10000, - engine_mode: str = ENGINE_MODE_LEGACY, database_ssl_mode: str = "disable", base_env: Optional[dict[str, str]] = None, ) -> dict[str, str]: """Env for ``scripts/math_poller.py``, keyed to ``PollerConfig.from_env`` (service.py:150-182): ``DATABASE_URL``, ``MATH_ENV``, ``POLL_FROM_DAYS_AGO`` (same names as the clj side — see - :func:`build_clj_env`), plus ``POLISMATH_ENGINE_MODE`` (engine_mode.py:30 - ``ENGINE_MODE_ENV_VAR``), applied at service start via - ``apply_engine_mode`` (service.py:207-220). + :func:`build_clj_env`). ``DATABASE_URL`` is normalized to the ``postgresql://`` scheme (:func:`_url_with_scheme`) — the mirror-image guard of the clj side's @@ -852,7 +848,6 @@ def build_py_env( env["DATABASE_URL"] = _url_with_scheme(database_url, "postgresql") env["MATH_ENV"] = math_env env["POLL_FROM_DAYS_AGO"] = _format_days_ago(poll_from_days_ago) - env[ENGINE_MODE_ENV_VAR] = engine_mode env["DATABASE_SSL_MODE"] = database_ssl_mode return env @@ -868,7 +863,6 @@ def __init__( database_url: str, math_env: str, poll_from_days_ago: float = 10000, - engine_mode: str = ENGINE_MODE_LEGACY, database_ssl_mode: str = "disable", base_env: Optional[dict[str, str]] = None, log_path: Optional[Path] = None, @@ -877,7 +871,6 @@ def __init__( database_url=database_url, math_env=math_env, poll_from_days_ago=poll_from_days_ago, - engine_mode=engine_mode, database_ssl_mode=database_ssl_mode, base_env=base_env, ) @@ -1602,7 +1595,6 @@ def run_equiv_stream( dbname: str = DEFAULT_DBNAME, zid: int = DEFAULT_ZID, poll_from_days_ago: float = 10000, - engine_mode: str = ENGINE_MODE_LEGACY, wait_timeout: float = 120.0, poll_interval: float = 0.5, engine_factory: Callable[[str], Any] | None = None, @@ -1662,7 +1654,7 @@ def run_equiv_stream( ), "py": lambda: PyPollerRunner( database_url=target_url, math_env=py_env, poll_from_days_ago=poll_from_days_ago, - engine_mode=engine_mode, log_path=out_dir_path / f"{py_env}.runner.log", + log_path=out_dir_path / f"{py_env}.runner.log", ), } builders = dict(default_builders) @@ -2679,7 +2671,6 @@ class FullRunConfig: zid: int = DEFAULT_ZID clj_env: str = "clj-ref" py_env: str = "py-shadow" - engine_mode: str = ENGINE_MODE_LEGACY poll_from_days_ago: float = 10000 wait_timeout: float = 120.0 poll_interval: float = 0.5 @@ -2758,7 +2749,7 @@ def run_full_equiv_protocol( seam_after=config.seam_after, math_envs=(config.clj_env, config.py_env), restart_clj_at_seam=config.restart_clj_at_seam, dbname=f"{config.dbname}_main", zid=config.zid, poll_from_days_ago=config.poll_from_days_ago, - engine_mode=config.engine_mode, wait_timeout=config.wait_timeout, + wait_timeout=config.wait_timeout, poll_interval=config.poll_interval, engine_factory=engine_factory, runner_builders=runner_builders or None, sleep=sleep, now=now, wait_for_clj_poll_cycle=config.wait_for_clj_poll_cycle, diff --git a/delphi/polismath/replay/store.py b/delphi/polismath/replay/store.py index 0bd537a51..b9a72b7ac 100644 --- a/delphi/polismath/replay/store.py +++ b/delphi/polismath/replay/store.py @@ -158,11 +158,6 @@ def build_provenance( # Engine impl flags that change the numbers (design §7). "engine_flags": { "POLISMATH_PCA_IMPL": os.environ.get("POLISMATH_PCA_IMPL", "powerit"), - # The engine mode changes warm-start behavior across steps — two - # recordings of the same schedule under different modes are - # materially different trajectories. - "POLISMATH_ENGINE_MODE": os.environ.get( - "POLISMATH_ENGINE_MODE", "improved"), "OMP_NUM_THREADS": os.environ.get("OMP_NUM_THREADS"), "OPENBLAS_NUM_THREADS": os.environ.get("OPENBLAS_NUM_THREADS"), }, diff --git a/delphi/polismath/utils/engine_mode.py b/delphi/polismath/utils/engine_mode.py deleted file mode 100644 index 788600970..000000000 --- a/delphi/polismath/utils/engine_mode.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Engine-mode switch: Clojure-parity warm-start vs improved cold-recompute. - -Python's delphi engine does a full COLD recompute on every conv-update tick. -Clojure instead THREADS warm-start state across ticks: - - - PCA :start-vectors — the previous tick's post-normalization unit - components are fed back in as the power-iteration starting vectors - (math/src/polismath/math/conversation.clj:381-387 -> pca.clj:86-105). - - group-k-smoother — {:last-k :last-k-count :smoothed-k} state carried in - the conv, so the group count K only changes after `:group-k-buffer` (4) - consecutive ticks agree on a new K (conversation.clj:454-478). - -`POLISMATH_ENGINE_MODE` selects between the two families: - - - 'improved' (default): today's cold-recompute behavior, byte-for-byte. - - 'clojure-legacy' : threads the warm-start state described above. - -The flag is resolved AT CALL TIME (never cached at import) by the shared -`polismath.utils.env_flags.resolve_impl_flag`: unknown values fall back to the default -with a warning so a typo in a deployment env cannot crash the math worker. -This lives in a shared spot (polismath.utils) because the mode cross-cuts both -PCA (conversation._compute_pca) and clustering (conversation._compute_clusters). -""" - -from typing import Sequence - -from polismath.utils.env_flags import resolve_impl_flag - -ENGINE_MODE_ENV_VAR = 'POLISMATH_ENGINE_MODE' -ENGINE_MODE_LEGACY = 'clojure-legacy' # warm-start parity with Clojure -ENGINE_MODE_IMPROVED = 'improved' # cold recompute every tick (default) -ENGINE_MODE_DEFAULT = ENGINE_MODE_IMPROVED -ENGINE_MODE_CHOICES: Sequence[str] = (ENGINE_MODE_LEGACY, ENGINE_MODE_IMPROVED) - - -def resolve_engine_mode() -> str: - """ - Resolve `POLISMATH_ENGINE_MODE` from the environment, at call time. - - Reuses `polismath.utils.env_flags.resolve_impl_flag` so the resolution rules - (strip + lowercase, unknown -> default with a warning) are identical to - the PCA-solver switch. - - Returns: - Either 'improved' (default) or 'clojure-legacy'. - """ - return resolve_impl_flag( - ENGINE_MODE_ENV_VAR, ENGINE_MODE_DEFAULT, ENGINE_MODE_CHOICES) diff --git a/delphi/scripts/certify_battery.json b/delphi/scripts/certify_battery.json index 5ca2817ad..75c35d58c 100644 --- a/delphi/scripts/certify_battery.json +++ b/delphi/scripts/certify_battery.json @@ -1,134 +1,114 @@ [ - { - "dataset": "vw", - "preset": "uniform", - "n_cuts": 8, - "engine_mode": "clojure-legacy", - "notes": "8 evenly-spaced recomputes over the full vw conversation" - }, - { - "dataset": "vw", - "preset": "front-loaded", - "n_cuts": 6, - "engine_mode": "clojure-legacy", - "notes": "6 front-loaded recomputes \u2014 early-conversation warm-start stress" - }, - { - "dataset": "vw", - "preset": "single-cut", - "engine_mode": "clojure-legacy", - "notes": "single cold-start recompute over all votes" - }, - { - "dataset": "biodiversity", - "preset": "uniform", - "n_cuts": 8, - "engine_mode": "clojure-legacy", - "notes": "8 evenly-spaced recomputes over the full biodiversity conversation" - }, - { - "dataset": "FLI", - "preset": "uniform", - "n_cuts": 6, - "engine_mode": "clojure-legacy", - "notes": "smallest private dataset (~91k votes) \u2014 pilot for the private-size regime; calibrates clj/py wall-clock before scheduling bg2018/pakistan/engage/bg2050" - }, - { - "dataset": "bg2018", - "preset": "uniform", - "n_cuts": 8, - "engine_mode": "clojure-legacy", - "notes": "~226k votes; revote-rich production conversation" - }, - { - "dataset": "pakistan", - "preset": "uniform", - "n_cuts": 8, - "engine_mode": "clojure-legacy", - "notes": "~400k votes" - }, - { - "dataset": "engage", - "preset": "uniform", - "n_cuts": 8, - "engine_mode": "clojure-legacy", - "notes": "~443k votes" - }, - { - "dataset": "bg2050", - "preset": "uniform", - "n_cuts": 6, - "engine_mode": "clojure-legacy", - "notes": "largest (~1.03M votes) \u2014 6 cuts to bound wall-clock" - }, - { - "dataset": "vw", - "schedule": "schedules/vw-every-vote-56.json", - "engine_mode": "clojure-legacy", - "notes": "every-vote prefix \u2014 see the schedule file for the Q11 truncation rationale; also the battery's degenerate-tick coverage: its early steps are 1-participant/1..N-comment ticks (goal-doc edge-case list)" - }, - { - "dataset": "pc-revote-02", - "preset": "uniform", - "n_cuts": 6, - "engine_mode": "clojure-legacy", - "notes": "prodclone: revote-heavy small conversation (~28% revotes, ~8.4k votes, 34 ptpts, 277 comments). Replaces pc-revote-01 (~56k votes, 97% revotes, 205 ptpts): its warm-chain split-loop hits a 4-way knife-edge tie (within-engine gaps <=2.5e-16 vs ~1e-5 cross-engine PCA noise) - extraction order irreducible cross-language; carved out per divergences.json + journal 2026-07-22 session 4." - }, - { - "dataset": "pc-banned-01", - "preset": "uniform", - "n_cuts": 6, - "engine_mode": "clojure-legacy", - "notes": "prodclone: banned participants present (participants.mod=-1; Q1 leak territory)" - }, - { - "dataset": "pc-smallmix-01", - "preset": "uniform", - "n_cuts": 6, - "engine_mode": "clojure-legacy", - "notes": "prodclone: small mixed conversation (~5k votes)" - }, - { - "dataset": "pc-midmix-01", - "preset": "uniform", - "n_cuts": 6, - "engine_mode": "clojure-legacy", - "notes": "prodclone: mid mixed conversation (~49k votes)" - }, - { - "dataset": "pc-zerovote-01", - "preset": "single-cut", - "engine_mode": "clojure-legacy", - "notes": "prodclone: zero-vote conversation \u2014 empty-conversation edge" - }, - { - "dataset": "pc-modheavy-01", - "schedule": "schedules/pc-modheavy-01-single-cut-mod.json", - "engine_mode": "clojure-legacy", - "notes": "prodclone: moderation-heavy (~81% modout, ~12k votes, 215 ptpts); single cold cut, full mod weave (Q18 carve - warm chain knife-edges; warm mod coverage: pc-meta-02)" - }, - { - "dataset": "pc-meta-01", - "schedule": "schedules/pc-meta-01-single-cut-mod.json", - "engine_mode": "clojure-legacy", - "notes": "prodclone: meta-rich (~25% is-meta, ~4.7k votes, 109 ptpts); single cold cut, full mod weave (Q18 carve - warm chain knife-edges; warm meta coverage: pc-meta-02)" - }, - { - "dataset": "vw", - "schedule": "schedules/vw-uniform8-restart4.json", - "engine_mode": "clojure-legacy", - "notes": "restart seam: uniform8 with load-or-init worker-restart replay after step 4" - }, - { - "dataset": "pc-midmix-01", - "schedule": "schedules/pc-midmix-01-uniform6-restart3.json", - "engine_mode": "clojure-legacy", - "notes": "restart seam: uniform6 with worker-restart replay after step 3 (medium prodclone)" - }, - { - "dataset": "pc-meta-02", - "schedule": "schedules/pc-meta-02-uniform6-mod.json", - "engine_mode": "clojure-legacy", - "notes": "prodclone: moderate mod density (20 modout + 15 meta of 146 cmts, 2.4k votes, 33 ptpts; ptpt-per-live 0.26); the Q18-safe warm-chain mod/meta entry" - } + { + "dataset": "vw", + "preset": "uniform", + "n_cuts": 8, + "notes": "8 evenly-spaced recomputes over the full vw conversation" + }, + { + "dataset": "vw", + "preset": "front-loaded", + "n_cuts": 6, + "notes": "6 front-loaded recomputes \u2014 early-conversation warm-start stress" + }, + { + "dataset": "vw", + "preset": "single-cut", + "notes": "single cold-start recompute over all votes" + }, + { + "dataset": "biodiversity", + "preset": "uniform", + "n_cuts": 8, + "notes": "8 evenly-spaced recomputes over the full biodiversity conversation" + }, + { + "dataset": "FLI", + "preset": "uniform", + "n_cuts": 6, + "notes": "smallest private dataset (~91k votes) \u2014 pilot for the private-size regime; calibrates clj/py wall-clock before scheduling bg2018/pakistan/engage/bg2050" + }, + { + "dataset": "bg2018", + "preset": "uniform", + "n_cuts": 8, + "notes": "~226k votes; revote-rich production conversation" + }, + { + "dataset": "pakistan", + "preset": "uniform", + "n_cuts": 8, + "notes": "~400k votes" + }, + { + "dataset": "engage", + "preset": "uniform", + "n_cuts": 8, + "notes": "~443k votes" + }, + { + "dataset": "bg2050", + "preset": "uniform", + "n_cuts": 6, + "notes": "largest (~1.03M votes) \u2014 6 cuts to bound wall-clock" + }, + { + "dataset": "vw", + "schedule": "schedules/vw-every-vote-56.json", + "notes": "every-vote prefix \u2014 see the schedule file for the Q11 truncation rationale; also the battery's degenerate-tick coverage: its early steps are 1-participant/1..N-comment ticks (goal-doc edge-case list)" + }, + { + "dataset": "pc-revote-02", + "preset": "uniform", + "n_cuts": 6, + "notes": "prodclone: revote-heavy small conversation (~28% revotes, ~8.4k votes, 34 ptpts, 277 comments). Replaces pc-revote-01 (~56k votes, 97% revotes, 205 ptpts): its warm-chain split-loop hits a 4-way knife-edge tie (within-engine gaps <=2.5e-16 vs ~1e-5 cross-engine PCA noise) - extraction order irreducible cross-language; carved out per divergences.json + journal 2026-07-22 session 4." + }, + { + "dataset": "pc-banned-01", + "preset": "uniform", + "n_cuts": 6, + "notes": "prodclone: banned participants present (participants.mod=-1; Q1 leak territory)" + }, + { + "dataset": "pc-smallmix-01", + "preset": "uniform", + "n_cuts": 6, + "notes": "prodclone: small mixed conversation (~5k votes)" + }, + { + "dataset": "pc-midmix-01", + "preset": "uniform", + "n_cuts": 6, + "notes": "prodclone: mid mixed conversation (~49k votes)" + }, + { + "dataset": "pc-zerovote-01", + "preset": "single-cut", + "notes": "prodclone: zero-vote conversation \u2014 empty-conversation edge" + }, + { + "dataset": "pc-modheavy-01", + "schedule": "schedules/pc-modheavy-01-single-cut-mod.json", + "notes": "prodclone: moderation-heavy (~81% modout, ~12k votes, 215 ptpts); single cold cut, full mod weave (Q18 carve - warm chain knife-edges; warm mod coverage: pc-meta-02)" + }, + { + "dataset": "pc-meta-01", + "schedule": "schedules/pc-meta-01-single-cut-mod.json", + "notes": "prodclone: meta-rich (~25% is-meta, ~4.7k votes, 109 ptpts); single cold cut, full mod weave (Q18 carve - warm chain knife-edges; warm meta coverage: pc-meta-02)" + }, + { + "dataset": "vw", + "schedule": "schedules/vw-uniform8-restart4.json", + "notes": "restart seam: uniform8 with load-or-init worker-restart replay after step 4" + }, + { + "dataset": "pc-midmix-01", + "schedule": "schedules/pc-midmix-01-uniform6-restart3.json", + "notes": "restart seam: uniform6 with worker-restart replay after step 3 (medium prodclone)" + }, + { + "dataset": "pc-meta-02", + "schedule": "schedules/pc-meta-02-uniform6-mod.json", + "notes": "prodclone: moderate mod density (20 modout + 15 meta of 146 cmts, 2.4k votes, 33 ptpts; ptpt-per-live 0.26); the Q18-safe warm-chain mod/meta entry" + } ] diff --git a/delphi/scripts/poller_equiv.py b/delphi/scripts/poller_equiv.py index 622651ac8..57a27a450 100644 --- a/delphi/scripts/poller_equiv.py +++ b/delphi/scripts/poller_equiv.py @@ -113,12 +113,11 @@ def run_clj(database_url, math_env, poll_from_days_ago): @click.option("--database-url", required=True) @click.option("--math-env", required=True) @click.option("--poll-from-days-ago", type=float, default=10000, show_default=True) -@click.option("--engine-mode", default="clojure-legacy", show_default=True) -def run_py(database_url, math_env, poll_from_days_ago, engine_mode): +def run_py(database_url, math_env, poll_from_days_ago): """Start the python math_poller (blocks; Ctrl-C stops it).""" runner = pe.PyPollerRunner( database_url=database_url, math_env=math_env, - poll_from_days_ago=poll_from_days_ago, engine_mode=engine_mode, + poll_from_days_ago=poll_from_days_ago, ) _run_and_stream(runner, label="py") @@ -143,13 +142,12 @@ def run_py(database_url, math_env, poll_from_days_ago, engine_mode): @click.option("--zid", type=int, default=pe.DEFAULT_ZID, show_default=True) @click.option("--clj-env", default="clj-ref", show_default=True) @click.option("--py-env", default="py-shadow", show_default=True) -@click.option("--engine-mode", default="clojure-legacy", show_default=True) @click.option("--poll-from-days-ago", type=float, default=10000, show_default=True) @click.option("--wait-timeout", type=float, default=120.0, show_default=True, help="Seconds to wait for EACH math_env to reflect a batch " "before giving up on it.") def feed(dataset, admin_url, cuts, out_dir, seam_after, restart_clj_at_seam, dbname, zid, - clj_env, py_env, engine_mode, poll_from_days_ago, wait_timeout): + clj_env, py_env, poll_from_days_ago, wait_timeout): """Stage C feeder: seed the equiv DB, start both runners, then insert vote batches one at a time — waiting for each math_env to reflect a batch before snapshotting math_main/math_bidtopid/math_ptptstats and @@ -160,7 +158,7 @@ def feed(dataset, admin_url, cuts, out_dir, seam_after, restart_clj_at_seam, dbn admin_url, dataset, cut_slots, out_dir=out_dir, seam_after=seam_after, math_envs=(clj_env, py_env), restart_clj_at_seam=restart_clj_at_seam, dbname=dbname, zid=zid, - poll_from_days_ago=poll_from_days_ago, engine_mode=engine_mode, + poll_from_days_ago=poll_from_days_ago, wait_timeout=wait_timeout, ) except pe.PollerEquivStreamError as exc: @@ -214,7 +212,6 @@ def compare(out_dir, clj_env, py_env): @click.option("--zid", type=int, default=pe.DEFAULT_ZID, show_default=True) @click.option("--clj-env", default="clj-ref", show_default=True) @click.option("--py-env", default="py-shadow", show_default=True) -@click.option("--engine-mode", default="clojure-legacy", show_default=True) @click.option("--poll-from-days-ago", type=float, default=10000, show_default=True) @click.option("--wait-timeout", type=float, default=120.0, show_default=True, help="Seconds to wait for EACH math_env to reflect a batch " @@ -232,7 +229,7 @@ def compare(out_dir, clj_env, py_env): help="Seconds to wait for the poll-cycle gate signal before " "aborting (only used when --wait-for-clj-poll-cycle).") def full_run(dataset, admin_url, cuts, seam_after, out_root, dbname, zid, clj_env, py_env, - engine_mode, poll_from_days_ago, wait_timeout, restart_clj_at_seam, + poll_from_days_ago, wait_timeout, restart_clj_at_seam, wait_for_clj_poll_cycle, poll_cycle_gate_timeout): """Stage D full protocol orchestration (spec §2/§3): (a) clj-ref run 1, (b) fresh DB + clj-ref run 2 -> self-jitter envelope, (c) fresh DB + @@ -248,7 +245,7 @@ def full_run(dataset, admin_url, cuts, seam_after, out_root, dbname, zid, clj_en config = pe.FullRunConfig( dataset=dataset, admin_url=admin_url, out_root=str(out_root), cuts=tuple(cut_slots), seam_after=resolved_seam, dbname=dbname, zid=zid, - clj_env=clj_env, py_env=py_env, engine_mode=engine_mode, + clj_env=clj_env, py_env=py_env, poll_from_days_ago=poll_from_days_ago, wait_timeout=wait_timeout, restart_clj_at_seam=restart_clj_at_seam, wait_for_clj_poll_cycle=wait_for_clj_poll_cycle, diff --git a/delphi/tests/conftest.py b/delphi/tests/conftest.py index 68d026704..3632e438e 100644 --- a/delphi/tests/conftest.py +++ b/delphi/tests/conftest.py @@ -25,23 +25,6 @@ from tests.common_utils import load_votes, load_comments -@pytest.fixture(autouse=True) -def _guard_engine_mode_env(): - """Restore POLISMATH_ENGINE_MODE around every test. - - Production code (e.g. MathPollerService.apply_engine_mode) writes this var - straight into os.environ; without this guard a single test exercising that - path leaks clojure-legacy mode into every later test in the same worker, - flipping in-conv/warm-start semantics suite-wide (bit us in CI on #2637). - """ - prev = os.environ.get("POLISMATH_ENGINE_MODE") - yield - if prev is None: - os.environ.pop("POLISMATH_ENGINE_MODE", None) - else: - os.environ["POLISMATH_ENGINE_MODE"] = prev - - def require_dynamodb( endpoint: str | None = None, timeout: float = 3.0, diff --git a/delphi/tests/poller/conftest.py b/delphi/tests/poller/conftest.py deleted file mode 100644 index a245fb6a1..000000000 --- a/delphi/tests/poller/conftest.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Shared fixtures for the poller test suite. - -Test-isolation guard: `MathPollerService.apply_engine_mode()` writes -`POLISMATH_ENGINE_MODE` into `os.environ` for the life of the process (the -service is a long-running daemon in production, so it has no reason to restore -it). Under pytest's single serial process, any poller test that constructs a -service with `engine_mode="clojure-legacy"` would otherwise leak legacy mode -into every later-collected test — flipping e.g. the in-conv greedy floor on -tests that assume the default 'improved' mode (this exact leak broke -TestD2cVoteCountSource in CI once #2637 made the postgres integration test run -there instead of skipping). -""" - -import os - -import pytest - -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR - - -@pytest.fixture(autouse=True) -def _restore_engine_mode_env(): - """Snapshot and restore POLISMATH_ENGINE_MODE around every poller test.""" - was_set = ENGINE_MODE_ENV_VAR in os.environ - saved = os.environ.get(ENGINE_MODE_ENV_VAR, "") - yield - if was_set: - os.environ[ENGINE_MODE_ENV_VAR] = saved - else: - os.environ.pop(ENGINE_MODE_ENV_VAR, None) diff --git a/delphi/tests/poller/test_integration_postgres.py b/delphi/tests/poller/test_integration_postgres.py index 5155b211f..a4cdb8d7d 100644 --- a/delphi/tests/poller/test_integration_postgres.py +++ b/delphi/tests/poller/test_integration_postgres.py @@ -83,7 +83,7 @@ def _make_service(url, math_env): pg.initialize() cfg = PollerConfig( database_url=url, math_env=math_env, poll_from_days_ago=1, - worker_pool_size=2, engine_mode="clojure-legacy", + worker_pool_size=2, ) return MathPollerService(pg, cfg), pg diff --git a/delphi/tests/replay_harness/test_certify.py b/delphi/tests/replay_harness/test_certify.py index 645cf5257..0823bba2b 100644 --- a/delphi/tests/replay_harness/test_certify.py +++ b/delphi/tests/replay_harness/test_certify.py @@ -24,7 +24,6 @@ from polismath.replay import certify as cert from polismath.replay import schedule as sched from polismath.replay.crosslang import PREP_MAIN_KEYS -from polismath.utils.engine_mode import ENGINE_MODE_CHOICES CERTIFY_BATTERY_PATH = Path(__file__).resolve().parents[2] / "scripts" / "certify_battery.json" @@ -64,7 +63,7 @@ def _fake_completed(returncode: int = 0, stderr: str = "") -> subprocess.Complet def _make_entry(**overrides) -> "cert.BatteryEntry": - defaults = dict(dataset="vw", engine_mode="clojure-legacy", + defaults = dict(dataset="vw", schedule_id="single-cut-clojure-legacy", preset="single-cut", n_cuts=None, schedule_path=None, notes="") defaults.update(overrides) @@ -75,28 +74,27 @@ def _make_entry(**overrides) -> "cert.BatteryEntry": # Schedule-id derivation. # --------------------------------------------------------------------------- def test_derive_schedule_id_ncuts_preset(): - assert cert.derive_schedule_id(engine_mode="clojure-legacy", preset="uniform", + assert cert.derive_schedule_id(preset="uniform", n_cuts=8) == "uniform8-clojure-legacy" def test_derive_schedule_id_no_ncuts_preset(): - assert cert.derive_schedule_id(engine_mode="clojure-legacy", - preset="single-cut") == "single-cut-clojure-legacy" + assert cert.derive_schedule_id( + preset="single-cut") == "single-cut-clojure-legacy" def test_derive_schedule_id_from_explicit_base_id(): - assert cert.derive_schedule_id(engine_mode="improved", - base_schedule_id="hb-3cut") == "hb-3cut-improved" + assert cert.derive_schedule_id( + base_schedule_id="hb-3cut") == "hb-3cut-clojure-legacy" -def test_derive_schedule_id_collision_free_across_preset_ncuts_engine_mode(): +def test_derive_schedule_id_collision_free_across_preset_ncuts(): ids = { - cert.derive_schedule_id(engine_mode="clojure-legacy", preset="uniform", n_cuts=8), - cert.derive_schedule_id(engine_mode="improved", preset="uniform", n_cuts=8), - cert.derive_schedule_id(engine_mode="clojure-legacy", preset="front-loaded", n_cuts=8), - cert.derive_schedule_id(engine_mode="clojure-legacy", preset="uniform", n_cuts=6), + cert.derive_schedule_id(preset="uniform", n_cuts=8), + cert.derive_schedule_id(preset="front-loaded", n_cuts=8), + cert.derive_schedule_id(preset="uniform", n_cuts=6), } - assert len(ids) == 4 + assert len(ids) == 3 # --------------------------------------------------------------------------- @@ -104,18 +102,17 @@ def test_derive_schedule_id_collision_free_across_preset_ncuts_engine_mode(): # --------------------------------------------------------------------------- def test_parse_battery_entry_preset_form(): e = cert.parse_battery_entry( - {"dataset": "vw", "preset": "uniform", "n_cuts": 8, "engine_mode": "clojure-legacy"} + {"dataset": "vw", "preset": "uniform", "n_cuts": 8} ) assert e.dataset == "vw" - assert e.engine_mode == "clojure-legacy" assert e.preset == "uniform" and e.n_cuts == 8 assert e.schedule_id == "uniform8-clojure-legacy" assert e.schedule_path is None -def test_parse_battery_entry_defaults_engine_mode_to_clojure_legacy(): +def test_parse_battery_entry_schedule_id_keeps_legacy_suffix(): e = cert.parse_battery_entry({"dataset": "vw", "preset": "single-cut"}) - assert e.engine_mode == "clojure-legacy" + assert e.schedule_id.endswith("-clojure-legacy") def test_parse_battery_entry_ncuts_preset_requires_n_cuts(): @@ -152,7 +149,7 @@ def test_load_battery_starter_file_shape(): for private_ds in ("FLI", "bg2018", "pakistan", "engage", "bg2050"): assert any(e.dataset == private_ds for e in entries), private_ds assert len(ids) == len(entries), "duplicate (dataset, schedule) entries" - assert all(e.engine_mode in ENGINE_MODE_CHOICES for e in entries) + assert all(e.schedule_id.endswith("-clojure-legacy") for e in entries) # --------------------------------------------------------------------------- @@ -322,7 +319,7 @@ def spy(a, b, i): monkeypatch.setattr(comparer, "compare_step", spy) result = cert.compare_recording_pair( - tmp_path / "clj", tmp_path / "py", engine_mode="clojure-legacy", + tmp_path / "clj", tmp_path / "py", cache_root=tmp_path, comparer=comparer, ) assert calls["n"] == 0 @@ -345,7 +342,7 @@ def spy(a, b, i): monkeypatch.setattr(comparer, "compare_step", spy) result = cert.compare_recording_pair( - tmp_path / "clj", tmp_path / "py", engine_mode="clojure-legacy", + tmp_path / "clj", tmp_path / "py", cache_root=tmp_path, comparer=comparer, ) assert calls["n"] == 1 @@ -367,7 +364,7 @@ def spy1(a, b, i): monkeypatch.setattr(comparer1, "compare_step", spy1) r1 = cert.compare_recording_pair(tmp_path / "clj", tmp_path / "py", - engine_mode="clojure-legacy", cache_root=tmp_path, + cache_root=tmp_path, comparer=comparer1) assert calls1["n"] == 1 @@ -383,7 +380,7 @@ def spy2(a, b, i): monkeypatch.setattr(comparer2, "compare_step", spy2) r2 = cert.compare_recording_pair(tmp_path / "clj", tmp_path / "py", - engine_mode="clojure-legacy", cache_root=tmp_path, + cache_root=tmp_path, comparer=comparer2) assert calls2["n"] == 0 assert r2["per_step"][0]["families"] == r1["per_step"][0]["families"] @@ -403,27 +400,25 @@ def test_normalize_path_strips_dict_numeric_segments(): def test_compute_fingerprint_stable_across_indices(): - fp1 = cert.compute_fingerprint("step_1.pca.comps[0][1]", "tolerant", "clojure-legacy") - fp2 = cert.compute_fingerprint("step_9.pca.comps[3][7]", "tolerant", "clojure-legacy") + fp1 = cert.compute_fingerprint("step_1.pca.comps[0][1]", "tolerant") + fp2 = cert.compute_fingerprint("step_9.pca.comps[3][7]", "tolerant") assert fp1 == fp2 assert len(fp1) == 10 -def test_compute_fingerprint_differs_by_family_and_engine_mode(): - a = cert.compute_fingerprint("step_1.pca.comps[0][1]", "tolerant", "clojure-legacy") - b = cert.compute_fingerprint("step_1.pca.comps[0][1]", "exact", "clojure-legacy") - c = cert.compute_fingerprint("step_1.pca.comps[0][1]", "tolerant", "improved") - assert len({a, b, c}) == 3 +def test_compute_fingerprint_differs_by_family(): + a = cert.compute_fingerprint("step_1.pca.comps[0][1]", "tolerant") + b = cert.compute_fingerprint("step_1.pca.comps[0][1]", "exact") + assert a != b # --------------------------------------------------------------------------- # Ledger. # --------------------------------------------------------------------------- def test_update_ledger_appends_new_as_open(): - obs = [{"path_pattern": "pca.comps[][]", "family": "tolerant", "engine_mode": "clojure-legacy", - "dataset": "vw", "schedule_id": "uniform8-clojure-legacy", "step": 3}] + obs = [{"path_pattern": "pca.comps[][]", "family": "tolerant", "dataset": "vw", "schedule_id": "uniform8-clojure-legacy", "step": 3}] updated = cert.update_ledger({}, obs) - key = cert.fingerprint_key_for("pca.comps[][]", "tolerant", "clojure-legacy") + key = cert.fingerprint_key_for("pca.comps[][]", "tolerant") assert key in updated assert updated[key]["status"] == "open" assert updated[key]["diagnosis"] is None @@ -431,13 +426,11 @@ def test_update_ledger_appends_new_as_open(): def test_update_ledger_preserves_existing_diagnosis(): - key = cert.fingerprint_key_for("pca.comps[][]", "tolerant", "clojure-legacy") + key = cert.fingerprint_key_for("pca.comps[][]", "tolerant") ledger = {key: {"path_pattern": "pca.comps[][]", "family": "tolerant", - "engine_mode": "clojure-legacy", "first_seen": {"dataset": "vw", "schedule": "uniform8-clojure-legacy", "step": 1}, "status": "diagnosed", "diagnosis": "PCA power-iteration seed differs (see #123)"}} - obs = [{"path_pattern": "pca.comps[][]", "family": "tolerant", "engine_mode": "clojure-legacy", - "dataset": "biodiversity", "schedule_id": "uniform8-clojure-legacy", "step": 7}] + obs = [{"path_pattern": "pca.comps[][]", "family": "tolerant", "dataset": "biodiversity", "schedule_id": "uniform8-clojure-legacy", "step": 7}] updated = cert.update_ledger(ledger, obs) assert updated[key]["status"] == "diagnosed" assert updated[key]["diagnosis"] == "PCA power-iteration seed differs (see #123)" @@ -615,7 +608,7 @@ def fake_run(cmd, *, cwd, env): "clojure": {"warm_start": "chain"}, "notes": "", })) entry = cert.parse_battery_entry( - {"dataset": "vw", "schedule": str(schedule_path), "engine_mode": "clojure-legacy"}, + {"dataset": "vw", "schedule": str(schedule_path)}, ) cert.certify_entry(entry, root=tmp_path, ledger={}) @@ -696,8 +689,7 @@ def test_run_focus_missing_recording_is_error(tmp_path): # --------------------------------------------------------------------------- def test_render_run_lines_within_budget_for_many_entries(): results = [ - {"dataset": "vw", "schedule_id": f"s{i}-clojure-legacy", "engine_mode": "clojure-legacy", - "verdict": "MATCH", "n_steps": 5} + {"dataset": "vw", "schedule_id": f"s{i}-clojure-legacy", "verdict": "MATCH", "n_steps": 5} for i in range(100) ] report = {"battery": results, "root": "/tmp/x"} @@ -708,10 +700,8 @@ def test_render_run_lines_within_budget_for_many_entries(): def test_render_run_lines_small_battery_one_line_per_entry(): results = [ - {"dataset": "vw", "schedule_id": "single-cut-clojure-legacy", "engine_mode": "clojure-legacy", - "verdict": "MATCH", "n_steps": 3}, - {"dataset": "vw", "schedule_id": "no-dataset", "engine_mode": "clojure-legacy", - "verdict": "SKIPPED", "reason": "dataset-unavailable"}, + {"dataset": "vw", "schedule_id": "single-cut-clojure-legacy", "verdict": "MATCH", "n_steps": 3}, + {"dataset": "vw", "schedule_id": "no-dataset", "verdict": "SKIPPED", "reason": "dataset-unavailable"}, ] report = {"battery": results, "root": "/tmp/x"} lines = cert.render_run_lines(report) @@ -722,8 +712,7 @@ def test_render_run_lines_small_battery_one_line_per_entry(): def test_render_focus_lines_within_budget_many_divergences(): result = { - "dataset": "vw", "schedule_id": "uniform8-clojure-legacy", "engine_mode": "clojure-legacy", - "verdict": "DIVERGENCE", "step": 2, + "dataset": "vw", "schedule_id": "uniform8-clojure-legacy", "verdict": "DIVERGENCE", "step": 2, "families": { "exact": [{"path": f"step_2.n.{i}", "path_pattern": f"n.{i}", "a": i, "b": i + 1, "fingerprint": f"FP-{i:010d}", "known": None} for i in range(20)], @@ -738,8 +727,7 @@ def test_render_focus_lines_within_budget_many_divergences(): def test_render_focus_lines_caps_exact_divergences_shown(): result = { - "dataset": "vw", "schedule_id": "uniform8-clojure-legacy", "engine_mode": "clojure-legacy", - "verdict": "DIVERGENCE", "step": 0, + "dataset": "vw", "schedule_id": "uniform8-clojure-legacy", "verdict": "DIVERGENCE", "step": 0, "families": { "exact": [{"path": f"step_0.n.{i}", "path_pattern": f"n.{i}", "a": i, "b": i + 1, "fingerprint": f"FP-{i:010d}", "known": None} for i in range(8)], @@ -784,7 +772,7 @@ def test_certify_entry_real_drivers_vw_single_cut(tmp_path): compare their results end to end. """ entry = cert.parse_battery_entry( - {"dataset": "vw", "preset": "single-cut", "engine_mode": "clojure-legacy"} + {"dataset": "vw", "preset": "single-cut"} ) result, ledger = cert.certify_entry(entry, root=tmp_path, ledger={}) assert result["verdict"] in ("MATCH", "DIVERGENCE"), result @@ -876,7 +864,7 @@ def test_step_verdict_cache_write_leaves_no_tmp_files(tmp_path): blob = _acceptance_blob() _write_clj_step(clj_dir, 0, blob) _write_py_step(py_dir, 0, dict(blob, n=99)) - cert.compare_recording_pair(clj_dir, py_dir, engine_mode="clojure-legacy", + cert.compare_recording_pair(clj_dir, py_dir, cache_root=tmp_path) cache_dir = tmp_path / ".certify_cache" / "stepverdicts" files = list(cache_dir.iterdir()) diff --git a/delphi/tests/replay_harness/test_certify_cli.py b/delphi/tests/replay_harness/test_certify_cli.py index c3d97f2ef..72942f78b 100644 --- a/delphi/tests/replay_harness/test_certify_cli.py +++ b/delphi/tests/replay_harness/test_certify_cli.py @@ -29,7 +29,7 @@ def test_run_exits_zero_on_all_match(monkeypatch): mod = _module() report = {"battery": [ {"dataset": "vw", "schedule_id": "single-cut-clojure-legacy", - "engine_mode": "clojure-legacy", "verdict": "MATCH", "n_steps": 3}, + "verdict": "MATCH", "n_steps": 3}, ], "root": "/tmp/x"} monkeypatch.setattr(mod.cert, "load_battery", lambda path: ["entry"]) monkeypatch.setattr(mod.cert, "run_battery", lambda entries, **kw: report) @@ -44,7 +44,7 @@ def test_run_exits_nonzero_on_divergence(monkeypatch): mod = _module() report = {"battery": [ {"dataset": "vw", "schedule_id": "uniform8-clojure-legacy", - "engine_mode": "clojure-legacy", "verdict": "DIVERGENCE", + "verdict": "DIVERGENCE", "first_div_step": 2, "n_div_steps": 1, "top_paths": []}, ], "root": "/tmp/x"} monkeypatch.setattr(mod.cert, "load_battery", lambda path: ["entry"]) @@ -57,7 +57,7 @@ def test_run_exits_nonzero_on_divergence(monkeypatch): def test_run_exits_nonzero_on_error(monkeypatch): mod = _module() report = {"battery": [ - {"dataset": "vw", "schedule_id": "x", "engine_mode": "clojure-legacy", + {"dataset": "vw", "schedule_id": "x", "verdict": "ERROR", "stage": "py-driver", "reason": "boom"}, ], "root": "/tmp/x"} monkeypatch.setattr(mod.cert, "load_battery", lambda path: ["entry"]) @@ -70,7 +70,7 @@ def test_run_exits_nonzero_on_error(monkeypatch): def test_run_skipped_ok_by_default_but_fails_with_strict(monkeypatch): mod = _module() report = {"battery": [ - {"dataset": "vw", "schedule_id": "x", "engine_mode": "clojure-legacy", + {"dataset": "vw", "schedule_id": "x", "verdict": "SKIPPED", "reason": "dataset-unavailable"}, ], "root": "/tmp/x"} monkeypatch.setattr(mod.cert, "load_battery", lambda path: ["entry"]) @@ -130,7 +130,7 @@ def test_run_stdout_budget_with_large_mocked_battery(monkeypatch): report = { "battery": [ {"dataset": "vw", "schedule_id": f"s{i}-clojure-legacy", - "engine_mode": "clojure-legacy", "verdict": "MATCH", "n_steps": 3} + "verdict": "MATCH", "n_steps": 3} for i in range(200) ], "root": "/tmp/x", diff --git a/delphi/tests/replay_harness/test_driver.py b/delphi/tests/replay_harness/test_driver.py index dcb14ab5f..303c65622 100644 --- a/delphi/tests/replay_harness/test_driver.py +++ b/delphi/tests/replay_harness/test_driver.py @@ -199,7 +199,6 @@ def _run_legacy(ds, spec): def test_legacy_mode_applies_mod_events_via_mod_update(monkeypatch): - monkeypatch.setenv("POLISMATH_ENGINE_MODE", "clojure-legacy") mods = [ModEvent(35, 100, -1), ModEvent(55, 101, 1)] ds = ReplayDataset.build(_MOD_RAW_VOTES, mod_events=mods) records = _run_legacy(ds, _mod_spec(mods)) @@ -216,7 +215,6 @@ def test_legacy_mode_un_moderation_disjs_the_set(monkeypatch): # The un-moderating sequence that DEFEATS update_moderation/_guard in # improved mode (test_driver_fails_loudly_on_moderation_set_emptying) # must be representable in legacy mode via mod_update's disj semantics. - monkeypatch.setenv("POLISMATH_ENGINE_MODE", "clojure-legacy") mods = [ModEvent(35, 100, -1), ModEvent(35, 101, 1), ModEvent(55, 100, 0)] ds = ReplayDataset.build(_MOD_RAW_VOTES, mod_events=mods) records = _run_legacy(ds, _mod_spec(mods)) @@ -231,7 +229,6 @@ def test_legacy_mode_none_moderation_never_calls_mod_update(monkeypatch): calls, not even with an empty list, so schedules with moderation="none" stay bit-identical (mod_update unconditionally flips moderation_applied, so a stray call would be observable even with nothing in the sets).""" - monkeypatch.setenv("POLISMATH_ENGINE_MODE", "clojure-legacy") calls = [] original = Conversation.mod_update @@ -268,7 +265,6 @@ def _spy(self, mods): def test_restart_after_does_not_change_step_count(monkeypatch): - monkeypatch.setenv("POLISMATH_ENGINE_MODE", "clojure-legacy") ds = ReplayDataset.build(_RESTART_RAW_VOTES) spec = sched.ScheduleSpec.from_dict({ "dataset": "t", "schedule_id": "restart-e2e", "source": "votes-csv", @@ -283,7 +279,6 @@ def test_restart_after_does_not_change_step_count(monkeypatch): def test_restart_after_none_is_a_no_op(monkeypatch): # restart_after absent (None, the default) must not touch the replay at # all — same step count/content as never having the field. - monkeypatch.setenv("POLISMATH_ENGINE_MODE", "clojure-legacy") ds = ReplayDataset.build(_RESTART_RAW_VOTES) spec_no_restart = sched.ScheduleSpec.from_dict({ "dataset": "t", "schedule_id": "no-restart", "source": "votes-csv", @@ -295,7 +290,6 @@ def test_restart_after_none_is_a_no_op(monkeypatch): def test_restart_conversation_rebuilds_matrices_and_drops_smoother_state(monkeypatch): - monkeypatch.setenv("POLISMATH_ENGINE_MODE", "clojure-legacy") ds = ReplayDataset.build(_RESTART_RAW_VOTES) spec = sched.ScheduleSpec.from_dict({ "dataset": "t", "schedule_id": "restart-unit", "source": "votes-csv", @@ -356,7 +350,6 @@ def test_restart_replays_only_woven_mods_not_dataset_mods(monkeypatch): # of them into steps. clj restart-conv replays only the woven mods # ((mapcat :mods steps-so-far), replay.clj) — the py restart must not # smuggle dataset-level mods the chain never saw into the warm state. - monkeypatch.setenv("POLISMATH_ENGINE_MODE", "clojure-legacy") mods = [ModEvent(t_ms=150, tid=10, mod=-1)] ds = ReplayDataset.build(_RESTART_RAW_VOTES, mod_events=mods) spec = sched.ScheduleSpec.from_dict({ @@ -372,7 +365,6 @@ def test_restart_replays_only_woven_mods_not_dataset_mods(monkeypatch): def test_restart_replays_woven_mods_so_far(monkeypatch): # Control for the test above: mods that ARE woven into steps up to the # seam must survive the restart (replayed via mod_update). - monkeypatch.setenv("POLISMATH_ENGINE_MODE", "clojure-legacy") mods = [ModEvent(35, 100, -1)] ds = ReplayDataset.build(_MOD_RAW_VOTES, mod_events=mods) spec = sched.ScheduleSpec.from_dict({ @@ -388,7 +380,6 @@ def test_restart_replays_woven_mods_so_far(monkeypatch): def test_restart_after_out_of_range_raises(monkeypatch, bad): # replay.clj CLI parity: restart_after must be a step index with at least # one step after it (0 <= r <= n_steps-2); 4 cuts -> valid r in [0, 2]. - monkeypatch.setenv("POLISMATH_ENGINE_MODE", "clojure-legacy") ds = ReplayDataset.build(_RESTART_RAW_VOTES) spec = sched.ScheduleSpec.from_dict({ "dataset": "t", "schedule_id": "restart-range", "source": "votes-csv", diff --git a/delphi/tests/replay_harness/test_poller_equiv_seed.py b/delphi/tests/replay_harness/test_poller_equiv_seed.py index ba78b3e7c..afcc25ce4 100644 --- a/delphi/tests/replay_harness/test_poller_equiv_seed.py +++ b/delphi/tests/replay_harness/test_poller_equiv_seed.py @@ -22,7 +22,6 @@ from polismath.replay import poller_equiv as pe from polismath.replay.types import CommentMeta, ModEvent, ReplayDataset -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR, ENGINE_MODE_LEGACY from polismath.utils.general import delphi_vote_to_postgres, postgres_vote_to_delphi PG_URL = os.environ.get("POLLER_EQUIV_PG_URL") @@ -495,20 +494,15 @@ def test_build_clj_env_logging_level_is_overridable(self): ) assert env["LOGGING_LEVEL"] == "debug" - def test_build_py_env_sets_required_vars_including_engine_mode(self): + def test_build_py_env_sets_required_vars(self): env = pe.build_py_env( database_url="postgresql://x/polis_equiv", math_env="py-shadow", - poll_from_days_ago=10000, engine_mode="clojure-legacy", + poll_from_days_ago=10000, base_env={}, ) assert env["DATABASE_URL"] == "postgresql://x/polis_equiv" assert env["MATH_ENV"] == "py-shadow" assert env["POLL_FROM_DAYS_AGO"] == "10000" - assert env[ENGINE_MODE_ENV_VAR] == "clojure-legacy" - - def test_build_py_env_default_engine_mode_is_legacy(self): - env = pe.build_py_env(database_url="x", math_env="e", base_env={}) - assert env[ENGINE_MODE_ENV_VAR] == ENGINE_MODE_LEGACY def test_build_py_env_forces_postgresql_scheme(self): """Symmetric guard to the clj-side scheme fix: SQLAlchemy/psycopg2 no @@ -570,7 +564,6 @@ def test_py_poller_runner_cmd_cwd_env(self): # .name == "app" (python-ci run 30071088647, 2026-07-24). assert runner.cwd == pe._DELPHI_ROOT assert runner.env["MATH_ENV"] == "py-shadow" - assert runner.env[ENGINE_MODE_ENV_VAR] == "clojure-legacy" assert runner.env["DATABASE_SSL_MODE"] == "disable" assert runner._proc is None diff --git a/delphi/tests/replay_harness/test_store.py b/delphi/tests/replay_harness/test_store.py index 72389344b..72eaf7d09 100644 --- a/delphi/tests/replay_harness/test_store.py +++ b/delphi/tests/replay_harness/test_store.py @@ -114,7 +114,6 @@ def test_provenance_completeness(tmp_path): assert "POLISMATH_PCA_IMPL" in prov["engine_flags"] # Engine mode changes warm-start behavior across steps — it MUST be pinned # in provenance (review finding B, 2026-07-18). - assert "POLISMATH_ENGINE_MODE" in prov["engine_flags"] # Dataset sha256 recorded (public vw dataset is resolvable on this branch). assert prov["dataset"]["name"] == "vw" assert prov["dataset"].get("votes_sha256") diff --git a/delphi/tests/test_base_cluster_lineage.py b/delphi/tests/test_base_cluster_lineage.py index 3218e031c..89cc62d40 100644 --- a/delphi/tests/test_base_cluster_lineage.py +++ b/delphi/tests/test_base_cluster_lineage.py @@ -31,7 +31,6 @@ import polismath.conversation.conversation as conv_mod from polismath.conversation.conversation import Conversation -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR from polismath.pca_kmeans_rep.pca import PCA_IMPL_ENV_VAR @@ -67,7 +66,6 @@ class TestColdStartInvariance: def _run(self, monkeypatch, mode): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) # default (powerit) both modes - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, mode) return Conversation('cold').update_votes(_many_ptpt_votes()) def test_legacy_base_is_clojure_faithful_up_to_q11_merges(self, monkeypatch): @@ -137,7 +135,6 @@ def _spy(self, monkeypatch): def test_legacy_base_warm_start_threaded(self, monkeypatch): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'clojure-legacy') spy = self._spy(monkeypatch) conv = Conversation('x').update_votes(_many_ptpt_votes()) base_calls = [c for c in spy.calls if c['level'] == 'base'] @@ -152,7 +149,6 @@ def test_legacy_base_warm_start_threaded(self, monkeypatch): def test_legacy_group_warm_start_threaded(self, monkeypatch): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'clojure-legacy') spy = self._spy(monkeypatch) conv = Conversation('x').update_votes(_many_ptpt_votes()) group_calls = [c for c in spy.calls if c['level'] == 'group'] @@ -168,7 +164,6 @@ def test_legacy_group_warm_start_threaded(self, monkeypatch): def test_group_clusterings_are_id_carrying_dicts(self, monkeypatch): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'clojure-legacy') conv = Conversation('x').update_votes(_many_ptpt_votes()) assert set(conv.group_clusterings.keys()) == {2, 3} for k, clustering in conv.group_clusterings.items(): @@ -196,7 +191,6 @@ def _votes(self, indexed_pids, n_cmnts=6): def test_ids_stable_and_new_participant_gets_larger_id(self, monkeypatch): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'clojure-legacy') conv = Conversation('lineage').update_votes( self._votes([(i, f'p{i}') for i in range(5)])) @@ -232,7 +226,6 @@ def _vw_conv(self, monkeypatch): except (ImportError, FileNotFoundError): pytest.skip('vw dataset unavailable') monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'clojure-legacy') votes = [{'pid': v.pid, 'tid': v.tid, 'vote': v.sign, 'created': v.t_ms} for v in ds.votes] return Conversation('vw').update_votes({'votes': votes}) diff --git a/delphi/tests/test_clj_hash_order.py b/delphi/tests/test_clj_hash_order.py index 23f17494c..f6f84dd49 100644 --- a/delphi/tests/test_clj_hash_order.py +++ b/delphi/tests/test_clj_hash_order.py @@ -14,7 +14,6 @@ clojure_hash_map_key_order, clojure_long_hash, ) -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR # Clojure REPL ground truth: (map hash (range 1 6)) and friends — hasheq of @@ -69,7 +68,6 @@ def _tie_conv(): def test_legacy_greedy_tie_follows_clojure_hash_order(monkeypatch): - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, "clojure-legacy") conv = _tie_conv() in_conv = conv._get_in_conv_participants() assert len(in_conv) == 15 @@ -95,7 +93,6 @@ def test_legacy_greedy_tie_follows_clojure_hash_order_string_pids(monkeypatch): """Same greedy-floor tie as above but with the PRODUCTION data shape: string pids (poll_votes casts str(pid); conversation preserves the type). The tie must still resolve by Clojure hash order of the numeric value.""" - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, "clojure-legacy") votes = [] for j in range(8): votes.append({"pid": "1", "tid": j, "vote": 1}) diff --git a/delphi/tests/test_conversation.py b/delphi/tests/test_conversation.py index bdce64e69..f0ac08e34 100644 --- a/delphi/tests/test_conversation.py +++ b/delphi/tests/test_conversation.py @@ -299,15 +299,18 @@ def test_natural_sorting_numeric_only_with_export(self): assert pids == expected_pids, f"PIDs not in encounter order: {pids} != {expected_pids}" assert tids == expected_tids, f"TIDs not in natural order: {tids} != {expected_tids}" - # Check exported data maintains same order and types + # Exported blobs emit tids in ARRIVAL (first-vote) order — the + # Clojure column order (_apply_legacy_blob_shape, unconditional + # since the mode collapse); internal columns above stay natsorted. conv_dict = conv.to_dict() exported_tids = conv_dict.get('tids', []) assert all(isinstance(t, int) for t in exported_tids), \ f"Not all exported TIDs are ints: {[type(t).__name__ for t in exported_tids]}" - assert exported_tids == expected_tids, \ - f"Exported TIDs not in expected order: {exported_tids} != {expected_tids}" + expected_export_tids = [10, 5, 20] # arrival order + assert exported_tids == expected_export_tids, \ + f"Exported TIDs not in arrival order: {exported_tids} != {expected_export_tids}" def test_incremental_updates_maintain_sorting(self): """Test row/column ordering across incremental vote updates. @@ -346,10 +349,10 @@ def test_incremental_updates_maintain_sorting(self): assert all(isinstance(t, int) for t in tids), f"TID types not preserved" assert all(isinstance(p, int) for p in pids), f"PID types not preserved" - # Check initial sorting in exported data + # Exported blobs emit tids in ARRIVAL order (legacy blob shape). conv_dict = conv.to_dict() exported_tids = conv_dict.get('tids', []) - assert exported_tids == expected_initial_tids, f"Initial exported tids incorrect: {exported_tids} != {expected_initial_tids}" + assert exported_tids == [10, 5], f"Initial exported tids not in arrival order: {exported_tids}" # Second batch adds new participants and comments in unsorted order # These should be inserted in natural order (numeric) @@ -379,10 +382,12 @@ def test_incremental_updates_maintain_sorting(self): assert all(isinstance(t, int) for t in tids), f"TID types not preserved after update" assert all(isinstance(p, int) for p in pids), f"PID types not preserved after update" - # Check that sorting is maintained in exported data + # Exported blobs emit tids in ARRIVAL order (legacy blob shape): + # first batch [10, 5], then new tids as first voted on: [1, 20, 3]. conv_dict = conv.to_dict() exported_tids = conv_dict.get('tids', []) - assert exported_tids == expected_tids, f"Exported tids order incorrect: {exported_tids} != {expected_tids}" + assert exported_tids == [10, 5, 1, 20, 3], \ + f"Exported tids not in arrival order: {exported_tids}" def test_moderation(self): """Test conversation moderation.""" @@ -421,10 +426,12 @@ def test_moderation(self): # Check filtered rating matrix: # - Moderated-out comments are ZEROED, not removed (D15 fix) - # - Moderated-out participants are still removed (rows dropped) + # - Banned participants are NOT removed: bans are not a Polis + # feature (mode collapse 2026-07-27, POST_CUTOVER_IMPROVEMENTS.md + # item 1 dropped) — the set is ingested but never applied. assert 'c2' in moderated_conv.rating_mat.columns # column kept assert (moderated_conv.rating_mat['c2'] == 0.0).all() # but zeroed - assert 'p3' not in moderated_conv.rating_mat.index # participant removed + assert 'p3' in moderated_conv.rating_mat.index # ban NOT applied (Q1) # Raw matrix should still have all data assert 'c2' in moderated_conv.raw_rating_mat.columns diff --git a/delphi/tests/test_degenerate_tick_parity.py b/delphi/tests/test_degenerate_tick_parity.py index 434274ed2..d5e4c6e73 100644 --- a/delphi/tests/test_degenerate_tick_parity.py +++ b/delphi/tests/test_degenerate_tick_parity.py @@ -27,7 +27,6 @@ sys.path.append(os.path.abspath(os.path.dirname(__file__))) from polismath.pca_kmeans_rep.pca import PCA_IMPL_ENV_VAR -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR from polismath.conversation.conversation import Conversation @@ -74,7 +73,6 @@ def _single_ptpt_votes(): @pytest.fixture def legacy_mode(monkeypatch): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'clojure-legacy') # --------------------------------------------------------------------------- diff --git a/delphi/tests/test_discrepancy_fixes.py b/delphi/tests/test_discrepancy_fixes.py index 6824ee416..2a7142f8e 100644 --- a/delphi/tests/test_discrepancy_fixes.py +++ b/delphi/tests/test_discrepancy_fixes.py @@ -316,20 +316,23 @@ def test_n_cmts_includes_moderated_out_comments(self): assert n_cmts_filtered == 10, f"rating_mat should keep all 10 columns (zeroed, not removed), got {n_cmts_filtered}" # The threshold used by _get_in_conv_participants should be min(7, 10) = 7, - # not min(7, 5) = 5. Verify indirectly: participant with exactly 6 votes - # should NOT be in-conv (threshold=7), but would be if n_cmts=5 (threshold=5). + # not min(7, 5) = 5. Since the mode collapse the greedy floor admits + # below-threshold participants whenever in-conv < 15, so satisfy the + # floor with 16 over-threshold participants first — then a 6-vote + # participant is excluded iff the threshold is really 7 (it would be + # admitted if n_cmts wrongly used the filtered count 5). conv2 = _build_conv_with_moderation( n_comments=10, mod_out_tids=[0, 1, 2, 3, 4], participant_votes={ - 0: list(range(10)), # 10 raw votes → in-conv - 1: list(range(4, 10)), # 6 raw votes (tids 4..9; tid=4 moderated-out) → NOT in-conv + **{p: list(range(10)) for p in range(16)}, # 16 over threshold + 16: list(range(4, 10)), # 6 raw votes -> below threshold=7 }, ) in_conv = conv2._get_in_conv_participants() assert 0 in in_conv, "P0 (10 raw votes) should be in-conv" - assert 1 not in in_conv, ( - "P1 (6 raw votes) should NOT be in-conv with threshold=7, " + assert 16 not in in_conv, ( + "P16 (6 raw votes) should NOT be in-conv with threshold=7, " "but would be if n_cmts wrongly used filtered count (5)" ) @@ -2440,15 +2443,14 @@ def test_compute_vote_stats_uses_raw_rating_mat(self): f"n_votes must count raw votes only; got {conv.vote_stats['n_votes']}, expected 18" ) - def test_vote_counts_exclude_moderated_out_participants(self): - """Moderated-out *participants* (mod_out_ptpts) must NOT appear in vote stats. + def test_banned_participants_are_ingested_but_inert(self): + """mod_out_ptpts is ingested but NEVER applied to the matrix. - D15 fixed moderated comment *columns* (zeroed, not removed). Polis also - supports moderated-out *participants* via `mod_out_ptpts`, which - `_apply_moderation` drops from `rating_mat.index`. The raw_rating_mat - routing for vote counting must NOT leak these participants — otherwise - excluded users' votes would still show up in `user-vote-counts`, - `votes-base`, and `_compute_vote_stats`. + Participant bans are not a Polis feature (mode collapse 2026-07-27, + POST_CUTOVER_IMPROVEMENTS.md item 1 dropped): no engine has ever + honored them — the Clojure worker's ingest path has no + participants.mod filter (CLOJURE_QUIRKS Q1). `_apply_moderation` + must keep banned rows in `rating_mat`. """ import pandas as pd @@ -2467,30 +2469,31 @@ def test_vote_counts_exclude_moderated_out_participants(self): conv.mod_out_ptpts = {3} # ban pid 3 conv._apply_moderation() - # rating_mat should have dropped pid 3 - assert 3 not in conv.rating_mat.index, "_apply_moderation should drop mod_out_ptpts" + # rating_mat KEEPS pid 3 — the ban set is stored but never applied. + assert 3 in conv.rating_mat.index, "bans must be inert (Q1: never applied)" + assert conv.mod_out_ptpts == {3}, "the set itself is still ingested" - # user-vote-counts must not include pid 3 + # Banned pid 3's votes stay in every downstream stat — exactly like + # the Clojure worker (Q1: the ban never reaches the math). counts = conv._compute_user_vote_counts() - assert 3 not in counts, ( - f"moderated-out pid 3 leaked into user-vote-counts: {sorted(counts.keys())}" + assert 3 in counts, ( + f"banned pid 3 must still be counted (Q1): {sorted(counts.keys())}" ) - # votes-base counts must reflect 4 participants (0,1,2,4), not 5. - # tid 1 (not moderated): pid 0=-1 (D), pid 1=1 (A), pid 2=0 (pass), - # pid 3 dropped, pid 4=-1 (D) → A=1, D=2, S=4 + # votes-base counts reflect ALL 5 participants. + # tid 1: pid 0=-1 (D), pid 1=1 (A), pid 2=0 (pass), pid 3=1 (A), + # pid 4=-1 (D) → A=2, D=2, S=5 vb = conv._compute_votes_base() - assert vb[1] == {'A': 1, 'D': 2, 'S': 4}, ( - f"tid 1 votes-base must exclude moderated-out pid 3; " - f"got {vb[1]}, expected {{A:1, D:2, S:4}}" + assert vb[1] == {'A': 2, 'D': 2, 'S': 5}, ( + f"tid 1 votes-base must include banned pid 3 (Q1); got {vb[1]}" ) - # vote_stats global n_votes: only count over the 4 remaining participants. - # pid 0: 3, pid 1: 4, pid 2: 4, pid 4: 4 → total 15 (not 18). + # vote_stats global n_votes counts all 5 participants: + # pid 0: 3, pid 1: 4, pid 2: 4, pid 3: 3, pid 4: 4 → total 18. conv._compute_vote_stats() - assert conv.vote_stats['n_votes'] == 15, ( - f"n_votes must exclude moderated-out participants: got " - f"{conv.vote_stats['n_votes']}, expected 15" + assert conv.vote_stats['n_votes'] == 18, ( + f"n_votes must include banned participants (Q1): got " + f"{conv.vote_stats['n_votes']}, expected 18" ) def test_to_dict_and_to_dynamo_dict_serialize_user_vote_counts_and_votes_base(self): @@ -2550,9 +2553,14 @@ def test_to_dict_and_to_dynamo_dict_serialize_user_vote_counts_and_votes_base(se f"to_dict votes-base tid must be int (numpy-safe), got {type(tid)}") assert set(entry.keys()) == {'A', 'D', 'S'}, ( f"to_dict votes-base entry must have Clojure-style A/D/S keys, got {set(entry.keys())}") + # Since the mode collapse, values are Clojure-exact per-base- + # cluster bucket VECTORS (agg-bucket-votes-for-tid parity), + # not scalar totals. for k, v in entry.items(): - assert isinstance(v, int) and not isinstance(v, bool), ( - f"to_dict votes-base {k} must be int, got {type(v)}") + assert isinstance(v, list), ( + f"to_dict votes-base {k} must be a bucket list, got {type(v)}") + assert all(isinstance(x, int) and not isinstance(x, bool) for x in v), ( + f"to_dict votes-base {k} bucket values must be ints") # ---- to_dynamo_dict ---- try: @@ -2598,9 +2606,13 @@ def test_to_dict_and_to_dynamo_dict_serialize_user_vote_counts_and_votes_base(se # And A/D/S vs agree/disagree/total must agree per-tid. for tid in vb: - assert vb[tid]['A'] == dyn_vb[tid]['agree'] - assert vb[tid]['D'] == dyn_vb[tid]['disagree'] - assert vb[tid]['S'] == dyn_vb[tid]['total'] + # to_dict carries per-base-cluster bucket vectors whose domain + # is CLUSTERED participants only (FP-81fda13ef6); this bare conv + # has no base clusters, so buckets are empty while the dynamo int + # totals still count every vote. Bucket sum can never exceed it. + assert sum(vb[tid]['A']) <= dyn_vb[tid]['agree'] + assert sum(vb[tid]['D']) <= dyn_vb[tid]['disagree'] + assert sum(vb[tid]['S']) <= dyn_vb[tid]['total'] # ============================================================================ diff --git a/delphi/tests/test_edge_cases.py b/delphi/tests/test_edge_cases.py index 0f8fb3537..834de1348 100644 --- a/delphi/tests/test_edge_cases.py +++ b/delphi/tests/test_edge_cases.py @@ -53,9 +53,15 @@ def test_insufficient_data_for_pca(): } conv = conv.update_votes(votes) conv = conv.recompute() + # Since the mode collapse the engine runs the REAL math on any non-empty + # matrix (Clojure parity — every-vote step-0 oracle): a 1x1 conversation + # yields a rank-capped single component, the greedy floor admits the lone + # participant, and the full base->group->repness chain runs (one base + # cluster, one group, a best-agree repness entry). assert conv.pca is not None - assert conv.pca['comps'].shape == (2, 1) + assert conv.pca['comps'].shape == (1, 1) + assert len(conv.base_clusters) == 1 + assert len(conv.group_clusters) == 1 assert conv.repness is not None - # With insufficient data (1 participant), no one meets the vote threshold, - # so no base clusters are formed and group_repness is empty. - assert conv.repness['group_repness'] == {} + [entry] = conv.repness['group_repness'][0] + assert entry['best_agree'] is True and entry['n_agree'] == 1 diff --git a/delphi/tests/test_engine_mode.py b/delphi/tests/test_engine_mode.py deleted file mode 100644 index 87ca2d001..000000000 --- a/delphi/tests/test_engine_mode.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python3 -""" -Tests for the POLISMATH_ENGINE_MODE switch (Clojure-parity warm-start vs -improved cold-recompute) and the cold-start invariance guard. - -The engine mode selects between two families of behavior: - - 'improved' (default): full cold recompute every tick — today's behavior. - - 'clojure-legacy' : threads warm-start state across ticks, matching - Clojure (PCA :start-vectors, group-k-smoother). - -On the FIRST tick (cold start) the two modes MUST coincide STRUCTURALLY, -because Clojure's warm-start state is empty on the first tick (no previous -comps, no smoother state). Since the 2026-07-22 session-3 parity port, the -modes legitimately differ at cold start in a DOCUMENTED set of stat/selection -semantics and in the clojure-legacy blob emission surface (see -_recompute_to_dict for the list); the invariance gate covers everything else — -memberships, cluster structure, pca, vote aggregates. This module asserts: - 1. Flag resolution semantics (default, valid, invalid, case/whitespace, - read-at-call-time) — mirrors tests/test_powerit_pca.py::TestPcaImplFlag. - 2. Cold-start invariance: a single-shot vw pipeline run is identical under - both modes (guards the warm-start commits from diverging on the first tick). - - Since PR-C the legacy clustering computes cluster CENTERS via the ported - Clojure weighted-mean (np.average) instead of sklearn's centroid. On vw the - cold clustering STRUCTURE is bit-identical across modes (same base/group - memberships, ids, counts, and all downstream repness/priorities/group-votes) - but the center COORDINATES differ at floating-point precision (~1e-13: - e.g. group center y 2.0147429038868094 vs 2.01474290388681). The invariance - check therefore compares numbers with a tight tolerance and everything else - (ids, memberships, strings) exactly — a real structural regression (a moved - participant, a relabelled cluster) still fails. -""" - -import os -import sys - -import pytest - -sys.path.append(os.path.abspath(os.path.dirname(__file__))) - -from polismath.utils.engine_mode import ( - ENGINE_MODE_ENV_VAR, - ENGINE_MODE_LEGACY, - ENGINE_MODE_IMPROVED, - ENGINE_MODE_DEFAULT, - ENGINE_MODE_CHOICES, - resolve_engine_mode, -) - - -# --------------------------------------------------------------------------- -# POLISMATH_ENGINE_MODE flag resolution -# --------------------------------------------------------------------------- - -class TestEngineModeFlag: - - def test_default_is_improved(self, monkeypatch): - """Unset env var -> 'improved' (today's behavior is the default).""" - monkeypatch.delenv(ENGINE_MODE_ENV_VAR, raising=False) - assert resolve_engine_mode() == ENGINE_MODE_IMPROVED - assert ENGINE_MODE_DEFAULT == ENGINE_MODE_IMPROVED - - def test_legacy_flag_selects_legacy(self, monkeypatch): - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, ENGINE_MODE_LEGACY) - assert resolve_engine_mode() == ENGINE_MODE_LEGACY - - def test_improved_flag_selects_improved(self, monkeypatch): - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, ENGINE_MODE_IMPROVED) - assert resolve_engine_mode() == ENGINE_MODE_IMPROVED - - def test_invalid_flag_value_falls_back_to_default(self, monkeypatch): - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'not-a-mode') - assert resolve_engine_mode() == ENGINE_MODE_DEFAULT - - def test_case_and_whitespace_insensitive(self, monkeypatch): - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, ' Clojure-Legacy ') - assert resolve_engine_mode() == ENGINE_MODE_LEGACY - - def test_flag_read_at_call_time(self, monkeypatch): - """Env var read per call, not cached at import time.""" - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, ENGINE_MODE_LEGACY) - assert resolve_engine_mode() == ENGINE_MODE_LEGACY - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, ENGINE_MODE_IMPROVED) - assert resolve_engine_mode() == ENGINE_MODE_IMPROVED - - def test_choices_are_exactly_the_two_modes(self): - assert set(ENGINE_MODE_CHOICES) == {ENGINE_MODE_LEGACY, ENGINE_MODE_IMPROVED} - - -# --------------------------------------------------------------------------- -# Cold-start invariance: improved vs clojure-legacy must coincide on tick 1 -# --------------------------------------------------------------------------- - -def _strip_volatile(d): - """Remove wall-clock fields that legitimately differ between two runs. - - `math_tick` is `25000 + (time_ms % 10000)` (conversation.py:1985) and - `last_updated` is a wall-clock timestamp — both are version counters, not - math output, so they are expected to differ between two independent runs. - """ - d = dict(d) - d.pop('last_updated', None) - d.pop('math_tick', None) - return d - - -# Tolerance for cluster-center coordinates (see module docstring): PR-C's ported -# weighted-mean and sklearn's centroid agree to ~1e-13 on identical memberships; -# 1e-6 is far below any real structural divergence yet absorbs the float noise. -_COLD_IDENTITY_TOL = 1e-6 - - -def _almost_equal(a, b, path='', tol=_COLD_IDENTITY_TOL): - """Deep equality that tolerates float noise in numbers but is EXACT on - everything else (dict keys, list lengths, strings, ints such as cluster ids). - - Returns (ok, message). - """ - # bool is an int subclass — treat it as exact, not numeric-tolerant. - if isinstance(a, bool) or isinstance(b, bool): - return (a == b, f"{path}: {a!r} != {b!r}") - # int-vs-int compares EXACTLY (ids, counts): a relative tolerance would - # accept e.g. two large cluster ids that differ. Mixed int/float (0 vs 0.0 - # from a JSON round-trip) still takes the tolerant branch below. - if isinstance(a, int) and isinstance(b, int): - return (a == b, f"{path}: {a!r} != {b!r} (int exact)") - if isinstance(a, (int, float)) and isinstance(b, (int, float)): - if abs(float(a) - float(b)) <= tol * max(1.0, abs(a), abs(b)): - return (True, '') - return (False, f"{path}: {a!r} != {b!r} (>|tol|)") - if isinstance(a, dict) and isinstance(b, dict): - if set(a) != set(b): - return (False, f"{path}: dict keys differ {set(a) ^ set(b)}") - for k in a: - ok, msg = _almost_equal(a[k], b[k], f"{path}.{k}", tol) - if not ok: - return (ok, msg) - return (True, '') - if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): - if len(a) != len(b): - return (False, f"{path}: length {len(a)} != {len(b)}") - for i, (x, y) in enumerate(zip(a, b)): - ok, msg = _almost_equal(x, y, f"{path}[{i}]", tol) - if not ok: - return (ok, msg) - return (True, '') - return (a == b, f"{path}: {a!r} != {b!r}") - - -class TestColdStartInvariance: - """A single-shot (first-tick) pipeline run must be identical under both - engine modes UP TO floating-point cluster-center coordinates (see module - docstring). Structure — every id, membership, count, and all downstream - outputs — must be bit-identical. This is the hard gate protecting 'improved' - mode from any structural drift introduced by the legacy warm-start plumbing. - """ - - def _recompute_to_dict(self, monkeypatch, mode): - from common_utils import create_test_conversation - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, mode) - conv = create_test_conversation('vw') - # Pin last_updated so the two runs share a deterministic value. - conv.last_updated = 0 - result = conv.recompute() - # Serialize BOTH runs under IMPROVED emission: since session-3 - # (2026-07-22) clojure-legacy has its own blob SURFACE (negated - # center/x/y, bids-vs-pids group members, bucketed votes-base, - # arrival-order tids, finalize-cmt-stats repness shape, null - # moderation seam) — pinned bidirectionally in - # test_legacy_blob_shape.py. Forcing improved emission here makes the - # comparison test what the legacy PLUMBING COMPUTED, not how legacy - # serializes it. - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, ENGINE_MODE_IMPROVED) - d = _strip_volatile(result.to_dict()) - # Documented first-tick mode differences (each pinned elsewhere): - # - comment_priorities (Q2): Clojure reads the PREVIOUS tick's - # group-votes (conversation.clj:658) — nil on tick 1, so legacy - # tick-1 priorities come from zero counts (test_priority_unmirror). - # - repness + consensus (session 3): legacy rest-stats sum over the - # OTHER GROUPS only (repness.clj:125 — unclustered voters excluded, - # changing ra/rat values) and exact-score ties resolve by first-vote - # ARRIVAL order instead of tid-ascending (test_legacy_blob_shape). - # - group-aware-consensus (session 3): legacy multiplies the 1/2 - # factor of zero-S groups instead of skipping (conversation.clj:639). - # Everything else — memberships, in-conv, base/group clusters, pca, - # votes-base, vote aggregates — keeps the cold-start invariance gate. - for key in ('comment_priorities', 'repness', 'consensus', - 'group-aware-consensus'): - d.pop(key, None) - return d - - def test_vw_cold_run_identical_across_modes(self, monkeypatch): - improved = self._recompute_to_dict(monkeypatch, ENGINE_MODE_IMPROVED) - legacy = self._recompute_to_dict(monkeypatch, ENGINE_MODE_LEGACY) - ok, msg = _almost_equal(improved, legacy) - assert ok, ( - "Cold-start (first-tick) vw run diverged STRUCTURALLY between " - "'improved' and 'clojure-legacy' engine modes (beyond float-level " - f"cluster centers): {msg}" - ) - - def test_vw_cold_structure_bit_identical_ignoring_centers(self, monkeypatch): - """Belt-and-braces: with cluster CENTER coordinates dropped, the two cold - blobs are EXACTLY equal — proving the ~1e-13 divergence is confined to - center coordinates and nothing structural moved.""" - improved = _drop_centers(self._recompute_to_dict(monkeypatch, ENGINE_MODE_IMPROVED)) - legacy = _drop_centers(self._recompute_to_dict(monkeypatch, ENGINE_MODE_LEGACY)) - assert improved == legacy - - -def _drop_centers(d): - """Recursively drop cluster-center coordinate fields ('center', 'x', 'y') - so the remaining structure (ids, members, counts, downstream) is compared - exactly. base-clusters are folded to {id, members, x, y, count}; group - clusters carry {id, members, center}.""" - if isinstance(d, dict): - return {k: _drop_centers(v) for k, v in d.items() - if k not in ('center', 'x', 'y')} - if isinstance(d, list): - return [_drop_centers(v) for v in d] - return d diff --git a/delphi/tests/test_env_flags.py b/delphi/tests/test_env_flags.py index 9a440d382..715d9f990 100644 --- a/delphi/tests/test_env_flags.py +++ b/delphi/tests/test_env_flags.py @@ -3,11 +3,12 @@ implementation-switch resolver. The resolver started life as `pca._resolve_impl_flag` (pca.py) and was imported -from there by `utils.engine_mode`, which dragged the whole numpy/pandas pca -import chain into anything that only wanted to read POLISMATH_ENGINE_MODE, and +from there by the (since-deleted) `utils.engine_mode`, which dragged the +whole numpy/pandas pca import chain into anything that only wanted to read an +impl flag, and emitted resolution warnings under the pca logger. These tests pin the move to `polismath.utils.env_flags`: identical resolution rules, warnings under the -env_flags logger, and a light `utils.engine_mode` import. +env_flags logger, and light imports for its consumers. """ import logging @@ -61,22 +62,18 @@ def test_pca_uses_shared_resolver(self): from polismath.utils import env_flags assert pca.resolve_impl_flag is env_flags.resolve_impl_flag - def test_engine_mode_uses_shared_resolver(self): - from polismath.utils import engine_mode, env_flags - assert engine_mode.resolve_impl_flag is env_flags.resolve_impl_flag - - def test_engine_mode_import_does_not_load_pca(self): - # The point of the move: reading POLISMATH_ENGINE_MODE must not drag + def test_env_flags_import_does_not_load_pca(self): + # The point of the move: reading a light impl flag must not drag # the numpy/pandas pca import chain. Fresh interpreter so this # process's already-imported modules can't mask a regression. code = ( - "import sys; import polismath.utils.engine_mode; " + "import sys; import polismath.utils.env_flags; " "sys.exit(1 if 'polismath.pca_kmeans_rep.pca' in sys.modules else 0)" ) proc = subprocess.run([sys.executable, '-c', code], capture_output=True, text=True) assert proc.returncode == 0, ( - "importing polismath.utils.engine_mode pulled in " + "importing polismath.utils.env_flags pulled in " "polismath.pca_kmeans_rep.pca:\n" + proc.stderr ) diff --git a/delphi/tests/test_group_k_smoother.py b/delphi/tests/test_group_k_smoother.py index 494d5527f..b4c243771 100644 --- a/delphi/tests/test_group_k_smoother.py +++ b/delphi/tests/test_group_k_smoother.py @@ -25,7 +25,6 @@ group_k_smoother_update, GROUP_K_BUFFER, ) -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR from polismath.pca_kmeans_rep.pca import PCA_IMPL_ENV_VAR import polismath.conversation.conversation as conv_mod from polismath.conversation.conversation import Conversation @@ -168,7 +167,6 @@ class TestSmootherPipeline: def _setup(self, monkeypatch, mode, prefs): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) # default powerit - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, mode) stub = _SilStub(prefs) monkeypatch.setattr(conv_mod, 'calculate_silhouette_sklearn', stub) return stub @@ -209,7 +207,6 @@ class TestDegenerateTickSmoother: def _mode(self, monkeypatch, mode): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, mode) def test_legacy_degenerate_tick_advances_smoother(self, monkeypatch): self._mode(monkeypatch, 'clojure-legacy') diff --git a/delphi/tests/test_in_conv_greedy_carry.py b/delphi/tests/test_in_conv_greedy_carry.py index c066f12aa..1de370839 100644 --- a/delphi/tests/test_in_conv_greedy_carry.py +++ b/delphi/tests/test_in_conv_greedy_carry.py @@ -26,7 +26,6 @@ sys.path.append(os.path.abspath(os.path.dirname(__file__))) from polismath.conversation.conversation import Conversation -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR from polismath.pca_kmeans_rep.pca import PCA_IMPL_ENV_VAR @@ -59,7 +58,6 @@ def _clustered_pids(conv): def _mode(monkeypatch, mode): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, mode) class TestGreedyFloor: diff --git a/delphi/tests/test_legacy_blob_shape.py b/delphi/tests/test_legacy_blob_shape.py index 3efc386ab..9e86ea9fb 100644 --- a/delphi/tests/test_legacy_blob_shape.py +++ b/delphi/tests/test_legacy_blob_shape.py @@ -36,7 +36,6 @@ import pytest from polismath.conversation.conversation import Conversation -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR # --------------------------------------------------------------------------- @@ -77,7 +76,8 @@ def conv(): @pytest.fixture() def legacy(monkeypatch): - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, "clojure-legacy") + """No-op since the mode collapse — the engine always runs the legacy + (Clojure-exact) semantics; retained so test signatures stay stable.""" def _sorted_base_clusters(conv): diff --git a/delphi/tests/test_math_writer_numpy_serialization.py b/delphi/tests/test_math_writer_numpy_serialization.py index ed1ed506f..a21c94b92 100644 --- a/delphi/tests/test_math_writer_numpy_serialization.py +++ b/delphi/tests/test_math_writer_numpy_serialization.py @@ -73,24 +73,26 @@ class TestWritersSerializeNumpy: def test_math_main_round_trips_real_blob_with_numpy(self): conv = _two_group_conv() blob = conv.to_dict() - # Real repness records are present (writer's fidelity-critical input). - recs = blob["repness"]["comment_repness"] - assert recs and "gid" in recs[0] + # Legacy blob shape (the only shape since the mode collapse): repness + # is {gid: [kebab-key entries]} — the writer's fidelity-critical input. + gid0 = sorted(blob["repness"].keys())[0] + recs = blob["repness"][gid0] + assert recs and "repful-for" in recs[0] blob_np = _numpy_ints(blob) - # The gid is now the numpy int64 that repness.py:847 astype(int) produces. - assert any(isinstance(r["gid"], np.integer) - for r in blob_np["repness"]["comment_repness"]) + assert any(isinstance(r["n-success"], np.integer) + for r in blob_np["repness"][gid0]) # RED precondition (environment-independent): bare json.dumps rejects it. with pytest.raises(TypeError, match="int64 is not JSON serializable"): json.dumps(blob_np) - # The writer serializes and the blob round-trips (gid back as JSON int). + # The writer serializes and the blob round-trips (values back as ints). client, cap = _client_capturing() client.write_math_main(1, blob_np, last_vote_timestamp=123, math_tick=0) restored = json.loads(cap["params"]["data"]) - got = restored["repness"]["comment_repness"][0]["gid"] + first_gid_key = sorted(restored["repness"].keys())[0] + got = restored["repness"][first_gid_key][0]["n-success"] assert isinstance(got, int) and not isinstance(got, bool) def test_bidtopid_round_trips_real_blob_with_numpy(self): diff --git a/delphi/tests/test_mod_ptpt_leak_parity.py b/delphi/tests/test_mod_ptpt_leak_parity.py index 094bddb31..d157f213d 100644 --- a/delphi/tests/test_mod_ptpt_leak_parity.py +++ b/delphi/tests/test_mod_ptpt_leak_parity.py @@ -22,7 +22,6 @@ sys.path.append(os.path.abspath(os.path.dirname(__file__))) from polismath.pca_kmeans_rep.pca import PCA_IMPL_ENV_VAR -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR from polismath.conversation.conversation import Conversation @@ -48,7 +47,6 @@ def _clustered_pids(conv): @pytest.fixture def legacy_mode(monkeypatch): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'clojure-legacy') class TestLegacyBanLeak: diff --git a/delphi/tests/test_mod_update_parity.py b/delphi/tests/test_mod_update_parity.py index 22be4be02..478037b07 100644 --- a/delphi/tests/test_mod_update_parity.py +++ b/delphi/tests/test_mod_update_parity.py @@ -20,12 +20,11 @@ import pytest from polismath.conversation.conversation import Conversation -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR @pytest.fixture def legacy_mode(monkeypatch): - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'clojure-legacy') + """No-op since the mode collapse — retained for signature stability.""" def _conv(**sets): diff --git a/delphi/tests/test_pca_warm_start.py b/delphi/tests/test_pca_warm_start.py index 281776167..95a303507 100644 --- a/delphi/tests/test_pca_warm_start.py +++ b/delphi/tests/test_pca_warm_start.py @@ -31,7 +31,6 @@ pca_project_dataframe, PCA_IMPL_ENV_VAR, ) -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR from polismath.conversation.conversation import Conversation @@ -171,7 +170,6 @@ def _tick2_new_ptpt(self): def _run_two_ticks(self, monkeypatch, mode): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, mode) recorded = _spy_powerit(monkeypatch) conv0 = Conversation('warm') conv1 = conv0.update_votes(self._tick1()) @@ -206,7 +204,6 @@ def test_legacy_prev_pca_without_comps_falls_back_to_cold(self, monkeypatch): """A prev_pca whose 'comps' is missing/None must NOT be turned into a np.asarray(None) garbage seed — it falls back to the cold draw.""" monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'clojure-legacy') recorded = _spy_powerit(monkeypatch) conv = Conversation('warm').update_votes(self._tick1()) for degenerate in ({'center': None, 'comps': None}, {}): diff --git a/delphi/tests/test_priority_unmirror.py b/delphi/tests/test_priority_unmirror.py index e014efcbb..4a1d4bc28 100644 --- a/delphi/tests/test_priority_unmirror.py +++ b/delphi/tests/test_priority_unmirror.py @@ -34,7 +34,6 @@ compute_comment_extremity, pca_project_cmnts, ) -from polismath.utils.engine_mode import ENGINE_MODE_ENV_VAR N_CMTS = 6 @@ -84,7 +83,6 @@ def _expected_priorities(conv, group_votes): @pytest.fixture def legacy_mode(monkeypatch): monkeypatch.delenv(PCA_IMPL_ENV_VAR, raising=False) - monkeypatch.setenv(ENGINE_MODE_ENV_VAR, 'clojure-legacy') diff --git a/delphi/tests/test_serialization_unfolding.py b/delphi/tests/test_serialization_unfolding.py index 2bf406940..3dc497175 100644 --- a/delphi/tests/test_serialization_unfolding.py +++ b/delphi/tests/test_serialization_unfolding.py @@ -131,12 +131,18 @@ def result(self, conv): return conv.to_dict() def test_group_clusters_hyphen(self, result, conv): - pids = _participant_ids(conv) - bc_ids = _base_cluster_ids(conv) - _assert_members_are_participant_ids( - result["group-clusters"], pids, bc_ids, "to_dict['group-clusters']") - _assert_members_cover_all_participants( - result["group-clusters"], pids, "to_dict['group-clusters']") + # Legacy blob shape (the only shape since the mode collapse): the + # hyphen-key group-clusters carry BASE-CLUSTER ids as members + # (Clojure convention; test_legacy_blob_shape pins the bid mapping). + bc_ids = set(_base_cluster_ids(conv)) + all_members = [] + for gc in result["group-clusters"]: + assert set(gc["members"]) <= bc_ids, ( + f"group-clusters members must be base-cluster ids, " + f"got {gc['members']}") + all_members.extend(gc["members"]) + assert sorted(all_members) == sorted(bc_ids), ( + "every base cluster must land in exactly one group") def test_group_clusters_underscore(self, result, conv): pids = _participant_ids(conv) diff --git a/docker-compose.yml b/docker-compose.yml index 792f51240..2acca4cc9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -174,9 +174,6 @@ services: # Optional zid allow/block lists (comma-separated). - POLL_ALLOWLIST=${POLL_ALLOWLIST:-} - POLL_BLOCKLIST=${POLL_BLOCKLIST:-} - # Engine family: 'clojure-legacy' (the effective default below, for - # warm-start parity during shadow validation) or 'improved'. - - POLISMATH_ENGINE_MODE=${POLISMATH_ENGINE_MODE:-clojure-legacy} # Per-zid serialized workers; concurrency across zids. - MATH_WORKER_POOL_SIZE=${MATH_WORKER_POOL_SIZE:-4} # Error dump dir + retry cap (dump -> retry -> park circuit breaker). diff --git a/example.env b/example.env index 8ff9ab88d..59c7e9209 100644 --- a/example.env +++ b/example.env @@ -67,10 +67,6 @@ INSTANCE_SIZE=dev # Poll cadences in ms. Defaults 1000/1000 # POLL_VOTE_INTERVAL_MS=1000 # POLL_MOD_INTERVAL_MS=1000 -# Engine family: improved (cold recompute) or clojure-legacy (warm-start parity). -# Code default is improved; the docker-compose delphi-math-poller service overrides -# to clojure-legacy. Set explicitly for non-compose runs (e.g. the CLI directly). -# POLISMATH_ENGINE_MODE=clojure-legacy # Conversations processed concurrently (each zid stays serialized). Default 4 # MATH_WORKER_POOL_SIZE=4 # errorconv dump dir + retries before parking a failing zid. Defaults scratch/errorconv, 1