Rewrite Verify — Evidence, Not Claims
Phase 5, run once per slice after /phx:work. It answers two questions with
evidence: is this slice Elixir, or the legacy app transliterated (idiom
audit), and which of its obligations are proven (parity audit). Generic
Elixir quality stays with /phx:review.
/phx:work → /rewrite:verify NN <-- YOU ARE HERE → /rewrite:roadmap --next
Iron Laws
- PREFLIGHT BEFORE ANY COMMAND — static proof of the target, a disposable database and no live side effects, printing no secret; a blocked preflight stays
blocked, never a trial run
- EVIDENCE IS RECORDED BY
ev run — exit status, target and source identity before and after, redacted environment and log, written before anyone interprets them; identity that moved means stale
- THE OBLIGATIONS ARE THE DENOMINATOR — from the slice seed, the analyses and a source completeness check, never from the tests that happen to exist
- NO PASS BY EXCLUSION — skipped, excluded, stubbed or stale results never count; pre-existing findings are still findings
- NEVER RUN THE LEGACY APP — evidence comes from the Elixir target only; the source is read and fingerprinted, never executed; suites hit the port, legacy tests are translated
- DONE MEANS THE GATE PASSED — every obligation
pass, or changed or retired by an accepted ADR or a user-approved AR record, and no BLOCKER
Workflow
1. Resolve the slice
Find .claude/plans/rewrite-{NN}-*/ (or the slug). Read its interview.md
(Acceptance Criteria, External Contracts, ## Oracle, ledger IDs) and progress.md if present.
TARGET is the current directory and must contain mix.exs; SOURCE is the legacy path recorded in
.claude/rewrite/status.md. EV=.claude/rewrite/verify/{NN}/evidence.
Every record comes from the bundled script; define ev in every shell call (EV_PATH keeps PATH past a python3 shim):
ev() { EV_PATH="$PATH" python3 "${CLAUDE_SKILL_DIR}/scripts/evidence.py" "$@"; }
- Identity —
scripts/fingerprint.py, which ev calls: head and a content_fingerprint, SHA-256 over every dirty
file (tracked changes plus untracked, non-ignored files), every file in each submodule work tree, or every file outside
git or under a split index (mode: tree). Git runs with hooks, filters and fsmonitor disabled and never writes the index
- Excludes —
.claude/rewrite/verify/, _build/, deps/ and node_modules/ by default; ev init fixes one list for
the run, so add --exclude cover/ there, and only for output the run itself writes
- Capture failed — DIR unreadable, git cannot list changes, or a file flagged
assume-unchanged/skip-worktree (git
hides its edits): ev status says incomplete. Ask the user to clear the flag; never clear it yourself
2. Preflight (static, before running anything)
Write only redacted conclusions to .claude/rewrite/verify/{NN}/preflight.md:
test -f mix.exs && [ -n "$SOURCE" ] && case "$(realpath .)/" in "$(realpath "$SOURCE")"/*) false;; esac # not in legacy
awk 'BEGIN { for (k in ENVIRON) print k }' | grep -E '^(DATABASE_URL|MIX_ENV|AWS_.*|.*(SECRET|TOKEN|KEY).*)$' | sort # names only
u=${DATABASE_URL-}; h=${u#*://}; h=${h%%/*}; h=${h##*@}; h=${h%:*}
case "$u" in "") echo "DATABASE_URL unset";; *) case "$h" in localhost|127.0.0.1|"[::1]") echo "DATABASE_URL local";; *) echo "DATABASE_URL non-local";; esac;; esac
Read these without running Mix:
mix.exs — aliases (what test and compile really run: ecto.reset, seeds, cmd, downloads), compilers: and any System.cmd
- Config chain —
config/config.exs, config/test.exs, every file their import_config reaches, config/runtime.exs (runs under MIX_ENV=test too), and config/prod.exs for the release
- In those files — Repo
url:, hostname:, database: and any System.get_env/fetch_env! feeding them; mailer, push, HTTP and webhook adapters or base URLs; Oban testing:; node connections
grep -nE 'aliases|compilers:|System\.cmd|ecto\.(drop|reset)|"cmd ' mix.exs
grep -nE 'import_config|url:|hostname:|database:|System\.(get_env|fetch_env)|adapter:|testing:|base_url|ip:' config/{config,test,runtime,prod}.exs
Stop with the preflight blocked when any of these holds:
- Database — not shown disposable (local host or tmp path, a test-only database, SQL sandbox pool); a
_test name alone is not proof
- Environment — the database or an endpoint is configured from a variable that is
non-local or unchecked
- Side effects — mail, push, webhooks or HTTP could reach real endpoints, or a black-box command targets a non-local host
- Mix steps — an alias or compiler step is destructive or networked
Never run a command to find out whether it was safe; ask the user with the redacted reason (--no-run skips straight to
the audits with evidence blocked). Record the release's endpoint ip: as read: a wildcard exposes it to the network.
3. Capture evidence
Start the run with ev init {NN} --source "$SOURCE", which moves earlier records to verify/{NN}/superseded/. Then write
the run list, $EV/commands.md, before running anything, one line per command: - `NAME` — command — why. List
MIX_ENV=test mix compile --warnings-as-errors and mix test on the slice's test files (with --full, the whole suite and
mix format --check-formatted); every command an acceptance criterion names, with its arguments as named (the named seeds,
not a random one; --force, the full suite, a test outside the slice paths, a release smoke, a static check such as rg or
git diff); and every ## Oracle command (one per black-box file, whole-file diagnostics, the load script as a short
smoke, with a checks threshold: k6 exits 0 when checks fail). Leave one out only when it cannot run here (a missing tool, a blocked preflight), and say so; a /phx:work log
never stands in.
Run each one as ev run {NN} NAME [--cwd DIR] [--expect-exit N] -- CMD: it captures both identities, runs the command
without a shell, captures them again, writes $EV/{ts}-NAME.md (exit, times, identities, env names, ExUnit counts) and its
.log, and exits with the command's status. Pass variables as env NAME=value (secret-like values are redacted); {uid}
becomes a value unique to the record. Never write a record by hand or copy a run script, this run's or another slice's:
ev run {NN} compile -- env MIX_ENV=test mix compile --warnings-as-errors
ev run {NN} ac17-rg --expect-exit 1 -- rg -n '^\s*([^#\s].*)?Repo\.' lib/{app}_web # must find nothing; skips comment lines
For black-box files, start the port as it ships, never a dev server (its debug_errors page answers HTML where the release
answers the envelope), on a database this run creates and drops; never point a suite at the legacy app. ev port start
runs the release with RELEASE_DISTRIBUTION=none in its own process group, and ev port stop stops only that group;
pkill or killall by pattern stops the user's other BEAM processes:
ev run {NN} release -- env MIX_ENV=prod mix release --overwrite
ev port start {NN} --release _build/prod/rel/{app}/bin/{app} --ready-url http://localhost:{port}/ # export DATABASE_URL and secrets in this call
ev run {NN} oracle-auth --cwd "$ORACLE" -- hurl --test --variable host=http://localhost:{port} --variable uid={uid} hurl/auth.hurl
ev port stop {NN}
4. Audit in parallel
Run ev status {NN}, which checks each commands.md name against its newest record; run every missing name and re-run
every incomplete or outdated one (auditors score records, never a later re-run). Agents cannot run plugin scripts, so
pass ev current {NN} as CURRENT_IDENTITY and, errors included, python3 "${CLAUDE_SKILL_DIR}/../architect/scripts/ledger.py" obligations --slice .claude/plans/{slug}/interview.md as OBLIGATION_IDS. Build COMPLETENESS: the slice's ledger IDs and
source anchors from interview.md and the ledger rows, nothing wider. Spawn both in ONE message with run_in_background: true
and wait for both completion notices; never schedule a wakeup or heartbeat while waiting (it re-enters a finished run):
rewrite:idiom-auditor — TARGET, SCOPE (git diff --name-only from the slice's base commit in progress.md, else the slice's modules), LEDGER, ARCHITECTURE,
CONTRACTS=.claude/rewrite/analysis/contracts.md, KB=.claude/rewrite/kb/, OUTPUT=.claude/rewrite/verify/{NN}/idiom.md
rewrite:parity-auditor — TARGET, SOURCE, COMPLETENESS, OBLIGATIONS (the interview path plus the analyses),
LEDGER, EVIDENCE=$EV, CURRENT_IDENTITY, DECISIONS (.claude/rewrite/architecture/,
.claude/rewrite/verify/accepted-risks.md), CLAIMS (progress.md, scratchpad.md, PR text if any), OBLIGATION_IDS, PREVIOUS (this slice's
verify/{NN}/parity.md renamed to parity-previous.md; none on a first round; never another slice's), OUTPUT=.claude/rewrite/verify/{NN}/parity.md
5. Apply ledger updates
Apply only the parity auditor's Proposed Ledger Updates that have a basis, with the ledger script below: one set --ids {IDs} --impl {state} --evidence {state} per target pair, never sed -i (BSD sed leaves a backup beside the ledger).
Never write a Departure cell, an ADR status or an AR record yourself. Only the user creates or approves an accepted risk, in
.claude/rewrite/verify/accepted-risks.md:
## AR-NN — <title>
- Owner: …
- Scope: <ledger IDs>
- Rationale: …
- Review: <condition or date that reopens it>
- Approved: <who approved, when, where>
Run the ledger checks:
python3 "${CLAUDE_SKILL_DIR}/../architect/scripts/ledger.py" check --rewrite-dir .claude/rewrite
python3 "${CLAUDE_SKILL_DIR}/../architect/scripts/ledger.py" stats --rewrite-dir .claude/rewrite \
--slice .claude/plans/{slug}/interview.md
A failing check, a nonzero stats (1 = malformed ledger, 2 = unreadable slice) or 0 selected rows fails the gate, unless the slice's roadmap.md row lists no ledger rows.
6. Report and gate
Report the parity auditor's states, quoting each row's obligation text from parity.md rather than describing it again. A
record made after the audits changes no state until a new audit scores it. Write
.claude/rewrite/verify/{NN}/report.md: verified parity (pass of the counted obligations), accepted departures apart
(ADR or AR, never verified), source omissions, scope mistakes, idiom BLOCKERs, rejected and stale evidence, unsupported claims, the gate.
The gate passes only when no BLOCKER remains, every black-box file ## Oracle assigns passed in a fresh record
(failing or not run fails the gate), and every counted obligation is one of:
- Verified —
pass
- Changed or retired — by an ADR whose Status is
accepted
- Accepted risk — in the Scope of an
AR-NN record with all five fields filled and an Approved you can confirm came from the user
Never count a proposed or ASSUMED ADR, a bare ## AR-NN heading or an unconfirmed approval; ask instead.
Omissions count until the ledger records them or the user moves them out of the slice. Mark the slice done in
roadmap.md only when the gate passes.
Otherwise list the fixes. Suggest /phx:work with the plan, or /phx:investigate for a failing witness or a load
smoke that errors: quote the error from the server log; a bigger pool or a lighter load only hides the cause. A behavior
the user adds or changes after a round gets a new case ID in the seed's Acceptance Criteria (obligations must still
pass): a plan task alone is outside the denominator. The report ends the run: no wakeup or loop re-checks it.
Arguments
<slice> — slice number (02) or plan slug
--no-run — audit existing evidence only (evidence for new code is not-run). Run ev status {NN} first; when a
record is outdated (any commit, docs included), stop before the audits, which would only score stale, and suggest a full run
--full — whole test suite plus format check