When to Use
Invoke this skill at the moment a build is about to move between environments — a CI/CD promotion step, or the pre-flight check a trading process runs before it opens its broker socket. It answers one question: is this environment the one this release was approved to run in?
The five vectors it audits are the ones whose drift produces silent, expensive failures rather than loud ones: a Python patch release that differs from the one the strategy was validated on, a dependency set that is not the locked set, a database missing the migration the new code expects, mandatory configuration that never made it into the environment, and — the one that costs real money — a paper environment wired to a live broker endpoint.
It sits alongside configuration-drift-detection-across-environments, which compares a
full configuration tree key-by-key. This skill is deliberately narrower: five named
vectors, an all-or-nothing verdict, cheap enough to run on every promotion.
When NOT to Use
- As evidence about a running host. Every value audited is declared by the caller
in an
EnvironmentSpec. Nothing connects to an interpreter, a database or a broker. The audit is exactly as trustworthy as the collection step that built the spec — a spec hand-written to match the baseline passes cleanly. Collect the values from the live target, not from a config file describing it. - As proof that two environments have the same packages installed. A matching
lockfile hash proves the lockfile file is byte-identical. Dependency specifiers
carry environment markers —
python_version,sys_platform,platform_machine— so the same file resolves to different distributions on different hosts. Usedependency-pinning-and-reproducible-buildsto audit the lockfile itself, and verify the installed set separately. - As a config-tree differ. Only the presence of named environment variables is
checked, never their values. For arbitrary nested configuration use
configuration-drift-detection-across-environments. - As an API contract check. That a staging endpoint is labelled TESTNET says
nothing about whether its payload schema still matches production's — see
sandbox-vs-production-endpoint-drift. - For research-vs-production signal parity. Comparing model outputs, float
precision and feature definitions is a different problem; see
research-environment-vs-production-environment-parity.
Prerequisites
- An
EnvironmentSpecfor the environment under audit, withenv_nameone of exactlyDEV,STAGING,PRODUCTION. Other names are rejected, not guessed at. - An
EnvironmentSpecfor the release specification, namedPRODUCTION. This is the Python release, lockfile hash and schema head the release was approved to run on — not a snapshot of whatever is live in production right now. See the decision point in step 3. python_versionas a fullmajor.minor.patchrelease (current_python_version()returns it in the right form).3.11is rejected.lockfile_sha256as a 64-character hex digest. Produce it withsha256_of_lockfile(path)so every environment digests the file identically.db_schema_revisionas the completealembic headsoutput — every head, not the first line.- The list of mandatory environment variable names (defaults:
BROKER_API_KEY,MAX_POSITION_LIMIT,DATABASE_URL).
Workflow
-
Collect specs, and let collection failures fail loudly. Build one
EnvironmentSpecper environment from the live target.- Decision point — a value you could not resolve is not an empty string. A blank
field raises
ValueErrorrather than being audited. This is the single most important behaviour in the module: under naive string equality, two environments that both failed to resolve a lockfile hash compare"" == "", pass, and the gate reports 100% parity on no evidence at all. If a collection step fails, fail the pipeline step — never substitute a default.
- Decision point — a value you could not resolve is not an empty string. A blank
field raises
-
Python runtime and dependency audit. Compare the full release version and the lockfile SHA-256 against the baseline.
- Decision point — a lockfile mismatch is not automatically a build error. Decide whether the lockfile legitimately changed (a dependency was intentionally bumped, so the baseline is stale and needs re-approval) or whether the environment is running something nobody locked. Re-approve the baseline in the first case; rebuild the environment in the second. Do not edit the spec to make the audit pass.
-
Database schema revision audit. Compare migration heads order-independently —
alembic headsprints multiple heads on a branched history in no guaranteed order, so"a, b"and"b, a"are the same state.- Decision point — which direction is the drift? If staging is ahead of live production because it is validating a migration, that is the normal workflow and the baseline is the release spec, which already includes the new head. If the environment is behind the baseline, the new code will hit a missing column on its first query: apply the migration before promoting, never after.
-
Broker endpoint mode verification.
DEVandSTAGINGmust be wired toTESTNET;PRODUCTIONmust be wired toMAINNET. Both directions are failures, not just one.- Decision point — the reverse misconfiguration is also a failure. A production release pointed at a testnet endpoint will start, connect, log fills and report P&L, all against paper. It is silent for as long as nobody reconciles against the broker statement. Do not treat "at least it isn't trading real money" as safe.
-
Mandatory environment variable audit. Presence and non-emptiness only. Values are never compared —
DATABASE_URLandBROKER_API_KEYare supposed to differ per environment — and never copied into the report. -
Gate on the verdict, not the score.
- Decision point —
parity_score_pctis diagnostic,is_deployment_allowedis the gate. A staging environment wired to MAINNET scores 80%. Eighty percent is not a near-pass; it is one failed vector away from executing live orders from a paper strategy. Branch onis_deployment_allowed(oraudit_status == "PARITY_VIOLATION_BLOCKED"), never on a score threshold.
- Decision point —
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Auditing missing evidence as parity. The failure mode that makes a parity gate
worse than no gate: a collection script returns empty strings for every vector, each
vector compares equal to the equally-empty baseline, and the pipeline records
PARITY_VERIFIED_PASSEDat 100%. Absent evidence must raise. Never default a parity field. - Free-form environment names. Classifying anything that is not literally
PRODUCTIONas non-production means an environment namedPRODorprod-eu-1is expected to be on TESTNET — so a genuinely live deployment wired to a paper endpoint passes the endpoint check. Environment names are matched against a closed set and an unrecognised name is rejected. - Pinning only the minor version.
3.11on both sides compares equal while one host runs 3.11.2 and the other 3.11.8. Patch releases ship stdlib bug fixes and security patches; pinmajor.minor.patchand let the gate enforce it. (Note the verifiable risk is behavioural change in the standard library and its C extensions — not floating-point arithmetic, which CPython does not vary across patch releases.) - Reading an identical lockfile hash as an identical environment. It is not. Environment markers (PEP 508) resolve the same file differently per Python version, platform and architecture. Two hosts can hold the same lockfile and different site packages.
- Line endings silently breaking the hash. A
requirements.lockchecked out on Windows withcore.autocrlf=truehas CRLF endings; on a Linux build host it has LF. Same content, different SHA-256, gate blocked forever. Fix the checkout (core.autocrlf=input, or mark the lockfile-textin.gitattributes) — do not "fix" it by hashing in text mode, which would also hide genuine changes. - Passing one Alembic head when the history has two.
alembic currenton a branched database reports several revisions. Pass the complete head set; a single head compared against a two-head baseline is drift, but a single head passed as though it were the whole story is a partially-migrated database that the gate cannot see. - Comparing environment variable values. They must differ. Auditing them produces permanent false failures and puts secrets in CI logs.
- Leaking secrets through a dataclass repr.
EnvironmentSpec.env_varsholds live credentials; an unguardedreprputsBROKER_API_KEYinto every traceback and CI log line. The field is declaredrepr=Falsefor exactly this reason — keep it that way. - Swapping the two arguments. Both are
EnvironmentSpec, so an order swap type-checks and produces a plausible-looking report about the wrong environment. The baseline is required to be aPRODUCTIONspec so the mistake raises instead.
Verification
- Construct a
PRODUCTIONbaseline and a matchingSTAGINGspec onTESTNET; verifyparity_score_pct == 100.0,is_deployment_allowedisTrue, andaudit_status == "PARITY_VERIFIED_PASSED". - Flip the staging spec to
MAINNET; verify the audit blocks withfailed_vector_names == ["BROKER_ENDPOINT"]at 80%. Repeat with aPRODUCTIONspec onTESTNET— that must block too. - Regression checks against fail-open behaviour: a blank
python_version,lockfile_sha256,db_schema_revisionorbroker_endpoint_modemust raise, not pass;"3.11"must raise; a 16-character "hash" must raise;env_name="PROD"must raise. - Verify
"27c6a30d7c24, ae1027a6acf"and"ae1027a6acf\n27c6a30d7c24"compare equal, and that a single head against that two-head baseline blocks. - Verify a secret placed in
env_varsappears in neitherrepr(spec)norrepr(report). - Run
python -m unittest discover -s skills/environment-parity-dev-staging-production/scriptsand confirm a 100% pass rate.