# Diff harness — old side

The regression oracle for the rebuild (05-migration-plan step 0).
`spec.md` in this directory is the canonical-JSON contract; this side
extracts it from the **legacy** data. The new-side emitter (built
per-milestone from M2 on) implements the same spec against the new
Postgres; `compare.py` is the green/red gate between them.

```
harness/
  spec.md              the contract (spec_version 1) — read first
  extract_legacy.py    old-side extractor (stdlib + psycopg only)
  compare.py           field-level diff, allowlist-aware, exit 0/1
  allowlist.json       accepted diffs, each with a reason
  tests/               pytest (pure functions — no DB needed)
  out/                 extraction output (baselines land here)
```

## Running against live Supabase (now, host-side)

Credentials live in `/srv/apps/with/.supabase-export.env` (mode 600,
`SUPABASE_DB_*` vars). The extractor reads that file itself — the
secret never touches your shell or argv:

```bash
/srv/apps/with/.venv/bin/python /srv/apps/with/harness/extract_legacy.py \
    --env-file /srv/apps/with/.supabase-export.env \
    --out /srv/apps/with/harness/out/baseline-$(date +%F).json
```

The session is read-only belt-and-suspenders: SELECTs only, plus
`default_transaction_read_only=on` — the live books cannot be written
even by a bug.

## Running against the landed legacy schema (later, in-container)

The nightly job (snapshot → scrub → land as `legacy.*` in the sidecar)
gives the same tables under the `legacy` schema. Same script, same
queries — it auto-detects the mode (a `legacy.time_entries` table wins
over `public.*`; override with `--schema` if a database ever has
both):

```bash
python harness/extract_legacy.py \
    --dsn "$SIDECAR_DSN" --out harness/out/legacy-$(date +%F).json
```

In-container it needs `psycopg` — satisfied by the app image's
requirements (contract note for the scaffold workstream: keep
`psycopg[binary]>=3.1` installed in the image).

## Comparing

```bash
/srv/apps/with/.venv/bin/python /srv/apps/with/harness/compare.py \
    out/baseline-2026-07-07.json out/new-side.json
```

Exit 0 = green: every difference matched an `allowlist.json` entry
(each entry = fnmatch path pattern + reason; keep them surgical —
stale entries are reported). Exit 1 = red with a field-level report.
`meta/*` (timestamps, source mode, physical row counts) is
allowlisted by design; everything else gates.

## Tests

```bash
/srv/apps/with/.venv/bin/python -m pytest /srv/apps/with/harness/tests -q
```

Covers: canonical output is byte-deterministic across row orderings;
the pinned grouping/formatting rules; compare flags injected diffs,
passes allowlisted ones, reports stale allowlist entries.

## Host tooling

`/srv/apps/with/.venv` (Python 3.12, `psycopg[binary]` + `pytest`) —
created for host-side harness runs; recreate anytime with
`python3 -m venv .venv && .venv/bin/pip install "psycopg[binary]" pytest`.
