A rule is not a feature. A feature that is wrong wastes an afternoon. A rule that is wrong generates hundreds of careful human judgements answering the wrong question, and those judgements become evidence rows that are expensive to retract.
Section 1The gap after “it works”
In ordinary software, the distance between a passing test suite and a deployed feature is short: merge, deploy, watch the error rate.
For a rule it is not short, and the reason is that a rule does something no ordinary feature does. It reaches into a production research database holding a quarter of a million rows that represent years of work, and it changes them — through a form, operated by a person, one record at a time, permanently.
Consider what TWP-FIELD-001 actually requires before its first violation can appear:
- Eleven new columns must exist on a 50,902-row table.
- An existing column must be renamed, preserving every value.
- A new evidence foundation — three tables — must exist.
- 41 obsolete rule versions must be archived without losing their 9,010 historical violations.
- The rule must be registered with its exact bindings, dependencies, and segments.
- A bootstrap must set a status on every existing official.
- A scan must run and produce violations.
Each of those touches production. Each can fail in a way that is expensive or impossible to reverse. So each is a separate, separately approved step — and this chapter is about why that is discipline rather than bureaucracy.
Section 2The workshop document
Every rule begins as a document, before any code. It answers six questions, and the first two are the ones that most often reveal that a rule should not be built at all.
| # | Question | Why it comes first |
|---|---|---|
| Q0a | What does a research assistant actually do when this fires? | If it cannot be written in four sentences, the rule is not ready. |
| Q0b | How many violations, and how long does one take? | 40,000 violations at three minutes each is a wall, not a queue. |
| Q1 | What evaluation support does it need — new fields, a view, derived attributes? | Determines the migrations before you write the evaluator. |
| Q2 | How is record provenance kept separate from violation evidence? | Chapter 4. Merging them is unrecoverable. |
| Q3 | What is the future-delivery transaction, and what is the retroactive bootstrap? | Two different code paths. Chapter 5. |
| Q4 | What makes a decision on this rule stale? | Get it wrong and resolved violations silently rot. |
TWP-FIELD-001's answers, in brief:
Q0a. A researcher opens an official whose birth status is unreviewed. They see the raw source string, the legacy value, and the cited source. They either record a validated birth value with its precision and calendar, or select a status — unknown, withheld, conflicting — with a rationale. They submit.
Q0b. Every one of 50,902 officials starts unreviewed, so the unfiltered violation count is 50,902. That is the wall. The answer is priority segments: party leaders, legislative leaders, and family-record principals are high priority; everyone else is low. The high segments are the queue; the long tail waits, indefinitely if necessary.
Q4. Any change to the birth fields themselves. If someone later corrects birth_year, a decision made against the old value is stale and must be re-evaluated.
Section 3The manifest
The workshop document becomes a manifest — a machine-readable declaration of everything the rule is. This is the single source of truth; the registry rows in PostgreSQL are a projection of it, not an independent definition.
RULE_MANIFEST = {
"rule_code": "TWP-FIELD-001",
"name": "Official birth information is reviewed and complete "
"at its supported precision",
"rule_type": "field",
"rule_subtype": "completeness",
"version_number": 1,
"base_priority": "low",
"can_be_batch": True,
"batch_mode": "group_display",
"schema_name": "twpolitics",
"table_name": "officials",
"subject_key": "official_id",
"executor_key": "twpolitics.official_birth_completeness",
"executor_version": "twp-field-001-v1",
"resolution_adapter_key": "field.completeness.birth.v1",
"context_adapter_key": "official.birth_profile.v1",
"segments": SEGMENTS,
"criteria_read": (
{"relation": "twpolitics.officials",
"fields": ("official_id", *READ_BIRTH_FIELDS)},
),
"resolution_write": (
{"relation": "twpolitics.officials",
"fields": WRITE_BIRTH_FIELDS},
),
"context_read": (
{"relation": "twpolitics.officials",
"fields": ("official_id", "official_name",
"legacy_birthyear", *READ_BIRTH_FIELDS)},
{"relation": "twpolitics.record_evidence",
"fields": ("record_evidence_id", "source_id",
"citation_snapshot")},
),
"revalidation_triggers": (
{"relation": "twpolitics.officials",
"fields": READ_BIRTH_FIELDS},
),
"outcomes": (
"save_known", "save_unknown", "save_withheld",
"save_conflicting", "confirm_current",
"insufficient_evidence", "reopen",
),
}
Read it against Chapter 6 and the correspondence is exact. The four declared sets are criteria_read, resolution_write, context_read, and revalidation_triggers. The type and subtype select the form framework. base_priority plus segments is the queueing policy.
Two details reward a closer look.
context_read includes record_evidence. The researcher sees where the value came from — and the manifest declares that as read-only context, so the resolution form cannot alter provenance while recording a decision. Chapter 4's separation, enforced structurally.
There are seven outcomes, not two. Four save a value or status; confirm_current affirms an existing value without changing it; insufficient_evidence defers honestly; reopen supersedes a previous decision. Each is declared, and nothing outside the list can be chosen.
Section 4Checksums
The manifest is hashed. Its digest becomes the rule version's identity.
def digest(value) -> str:
encoded = json.dumps(
value,
sort_keys=True, # order must not matter
separators=(",", ":"), # no incidental whitespace
ensure_ascii=False,
default=_json_default,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
DEFINITION_CHECKSUM = digest(RULE_MANIFEST)
sort_keys=True and the tight separators matter: without them, reordering a dictionary or reformatting the file would change the hash and make the rule appear to have changed when it had not.
But the manifest is only a declaration. The behaviour also depends on the code that reads it. So the registration packet pins a SHA-256 for every participating file:
| Artifact | SHA-256 (first 12) |
|---|---|
| audits/field_completeness_v1.py | 762525575c57 |
| audits/field_completeness_executor.py | 0e91ca95c985 |
| audits/field_completeness_forms.py | ca6850455316 |
| audits/field_completeness_services.py | 0be774312f98 |
| audits/field_completeness_orchestration.py | 5d5179e6ff44 |
| audits/legacy_archive.py | 6c40537dc5c0 |
| provenance/migrations/0001_initial.py | aa156708d4f4 |
| audits/migrations/0007_add_evidence_foundation.py | 388c80ca7250 |
| audits/migrations/0008_add_rule_registry_contract.py | c2d30b74c14a |
| twpolitics/migrations/0005_add_record_evidence.py | f5acc0b125c1 |
| twpolitics/migrations/0006_add_..._constraint.py | 37c063742171 |
| twpolitics/migrations/0007_rename_..._birthyear.py | 47d545e62351 |
a5ab4bd8e611. Truncated here for legibility; the packet records full digests.These are stored on the rule version, and runtime refuses to preview or apply if any of them has moved.
The consequence is worth stating plainly: a developer cannot change the behaviour of a live rule by editing a Python file. The change is detected and the rule stops. Reactivating it requires a new version, a new checksum, and a new approval.
A researcher's decision is only interpretable if you know what question they were answering. If the evaluator could be edited underneath a completed decision, the database would contain a judgement whose meaning cannot be recovered — you would know that Emily chose save_known, but not what save_known required at the moment she chose it.
The packet is explicit that byte changes are never waved through: any byte change requires new hashes and a repeat rehearsal; it must not be accepted as harmless drift.
Section 5Fifty-one dependencies
The four declared sets are not stored as free-form JSON. They are expanded into normalised rows — one per field per set — so that they can be indexed, queried, and validated against the live database catalogue.
class RuleDependency(models.Model):
"""One normalized, versioned relation or field dependency of a rule."""
class DependencyKind(models.TextChoices):
CRITERIA_READ = "criteria_read", ...
RESOLUTION_WRITE = "resolution_write", ...
CONTEXT_READ = "context_read", ...
REVALIDATION_TRIGGER = "revalidation_trigger", ...
rule_version = models.ForeignKey("RuleVersion", ...)
dependency_kind = models.CharField(choices=DependencyKind)
schema_name = models.CharField(max_length=63)
relation_name = models.CharField(max_length=63)
field_name = models.CharField(max_length=63)
semantic_role = models.CharField(max_length=80)
ordinal = models.PositiveSmallIntegerField()
contract = models.JSONField()
TWP-FIELD-001 expands to exactly 51 dependency rows. Registration asserts that number, and asserts it again on replay.
Why insist on an exact count? Because “the rule registered successfully” is a much weaker claim than “the rule registered with exactly one version, 51 dependencies, three segments, one inactive binding, and a current drift state.” The first is compatible with a partial registration; the second is not.
semantic_role is the field that makes dependencies useful rather than merely present. It records what part the field plays — the subject key, the value, the precision, the status — so the resolution form can render an appropriate control for each without hard-coding column names.
Section 6The rehearsal
Before any of this runs against production, the entire ordered sequence is rehearsed against a disposable restore: a throwaway PostgreSQL cluster built from a frozen snapshot of the real database.
The snapshot is pinned by digest, so there is no ambiguity about what was rehearsed against:
production-source-<timestamp>-<digest>c0e9001057e48742d406c98e0749e69df87dda5f817243fbc9ca8a0ea2ceeda40604cd0044fb6ebbd6eaad4027e9de14da38501f5596b5ea68f4a48cc08589b5The rehearsal must prove six things. Only the first is obvious.
- 1It worksThe sequence runs to completion and produces the intended state.
- 2It reversesForward, backward, forward again, for every reversible migration.
- 3It preserves identityTable OIDs, sequence OIDs, row counts, and content fingerprints unchanged where they should be.
- 4It refuses on driftA deliberate drift probe fails for the intended reason and rolls back completely.
- 5It replaysRunning twice changes nothing the second time, and says so.
- 6The backup restoresThe pre-operation backup is actually restorable, proven by restoring it.
Item 4 is the one that separates a rehearsal from a demonstration. It is easy to show that a sequence works when everything is as expected. What matters is whether it refuses when something is not — because in production, something eventually will not be.
The harness is .ai/scripts/test_twp_field_001_postgresql.py, 1,135 lines, and it passes 13 of 13 checks. It exercises the real management command in read-only mode, refuses registry drift and stale archive selections, verifies the exact list of changed identifiers, and proves protected state is preserved.
Note that this is separate from the Django test suite, which passes 20 focused tests and 862 overall. The Django suite runs on in-memory SQLite, so PostgreSQL-specific behaviour — raw check constraints, OID preservation, jsonb semantics, query-plan inspection — is not covered by a green run. Neither suite substitutes for the other, and a rule needs both.
Section 7Eleven gates
With the rehearsal passed, the production path is fixed. Every numbered step needs its own explicit approval.
- 1 · Freeze and recomputeOne exact source candidate. Recompute every pinned hash; abort on any mismatch.
- 2 · Push, PR, mergeA bounded pull request. Merge only after CI passes.
- 3 · Deploy with the runtime disabledThe code is live; the rule is not running.
- 4 · Repeat the read-only preflightConfirm production has not drifted since the rehearsal.
- 5 · Back up and prove restorationNot “take a backup” — restore it somewhere and verify it.
- 6 · Apply only the additive migrationsEvidence foundation, registry contract, record evidence, constraint. Do not let a blanket
migratereach the rename. - 7 · Re-read everythingSchema, counts, fingerprints, registry defaults, and the exact archive selection.
- 8 · Archive cutover41 versions, 33 bindings. Assert the exact changed-ID list; prove idempotent replay.
- 9 · The preserving renameRequire identical row count and birth-state fingerprint before and after.
- 10 · Register inactiveOne version, 51 dependencies, three segments, one inactive binding,
currentdrift state, no change to history. - 11 · Backfill, bootstrap, scan, activateEach one separately reviewed. None implied by any earlier gate.
Two of these deserve comment.
Step 5 says “prove restoration,” not “take a backup.” An unverified backup is a belief, not a safety net. The only evidence that a backup works is a restore.
Step 6 contains a warning about migrate. Django's default behaviour is to apply every pending migration. Here, the additive migrations are safe and the rename at step 9 is not yet approved — so a blanket migrate would execute an ungated production schema change. The migrations must be applied by explicit name.
That is a general hazard worth internalising: a tool's convenient default is often a gate you did not know you were passing through.
Section 8Register, then activate
Steps 10 and 11 are separate, and the separation is one of the more useful ideas in the whole design.
Registering inactive writes the rule into the registry — version, dependencies, segments, binding — with the binding switched off. Nothing evaluates. No violations appear. But everything the registration asserts is now verifiable against the live database: every schema, table, column, and type named in those 51 dependencies is checked against PostgreSQL's catalogue.
This is a real test that cannot be run any other way. A rule can pass every unit test against a fixture and still reference a column that does not exist in production, because the fixture and production were built from different assumptions. Registering inactive finds that, harmlessly.
Activating switches the binding on and permits a scan. Only then do violations exist, and only then can researchers act.
After the first scan, stop. Count the violations per segment. If the high-priority segments yield fewer than about fifty violations, the queue is too thin to occupy a team and the segment predicates need widening. If they yield more than a few thousand, the first tranche needs tightening.
Q0b in §2 asked this question at the workshop stage as an estimate. This is where the estimate meets the data, and it is much cheaper to adjust a segment predicate before five researchers have started work than after.
Section 9Drift
One concept underlies almost every guard in this chapter.
Drift is any difference between a recorded expectation and observed reality: a checksum that has moved, a row count that does not match, a registry state that is not what the last operation left behind, a schema that has changed since the rehearsal.
The rule about drift is absolute:
Drift is always a refusal. Never a warning you proceed past.
This is harder to hold to than it sounds, because drift usually appears at the least convenient moment — mid-sequence, with an approval already granted and a researcher waiting. The temptation to decide that this particular difference is probably harmless is exactly when the discipline matters, because the whole value of the guard is that it fires when your judgement about harmlessness is untested.
The pattern in code is fail closed: on any guard failure, missing precondition, or unexpected value, refuse and roll back rather than continuing on a best-effort basis. Partial completion of an eleven-step production sequence is worse than no completion, because it leaves the system in a state nobody designed.
TWP-FIELD-001's production preflight is a good example of drift-checking done properly. It ran in read-only, repeatable-read transactions and verified: that it was connected to the expected production database under the expected application role; the exact PostgreSQL major version; that transaction_read_only was on; that audits.0006 and twpolitics.0004 were applied and the six later migrations were not; that official_birthyear existed as a nullable integer; that legacy_birthyear was absent; that TWP-FIELD-001 was absent; and that every additive relation was absent.
It also recorded exact history counts that later steps must preserve: 6,228 runs, 258,405 records, 9,010 violations, 19,091 observations, 19,407 actions, and 6,134 changes. Those numbers are how step 8's archive cutover proves it archived rules without touching their history.
Eleven gates for one rule is a lot. It gets cheaper: the second rule of the same subtype reuses the framework, the forms, the templates, and the proven sequence. What does not get cheaper is the workshop, because that is where the thinking happens — and Chapter 8 takes on the hardest case of all, where the thing being decided is not a value but an identity.
Terminology introduced in this chapter
- manifest
- The machine-readable declaration of everything a rule version is. The source of truth; registry rows are its projection.
- definition checksum
- A deterministic SHA-256 over the manifest, computed with sorted keys and no incidental whitespace.
- pinned implementation hash
- A SHA-256 of a participating source or migration file, stored on the rule version. A mismatch stops the rule.
- rule dependency
- One normalised row per field per declared set, validated against the live catalogue at registration.
- semantic role
- What part a field plays in a rule — subject key, value, precision, status — so forms need not hard-code column names.
- disposable restore
- A throwaway database built from a digest-pinned production snapshot, used to rehearse an operation.
- drift probe
- A deliberately corrupted rehearsal that must fail for the intended reason and roll back completely.
- protected state fingerprint
- An aggregate hash of data an operation must not change, captured before and after to prove it did not.
- register inactive
- Writing a rule into the registry with its binding switched off, so its declarations can be validated without evaluating anything.
- drift
- Any difference between a recorded expectation and observed reality. Always a refusal.
- fail closed
- On any guard failure, refuse and roll back rather than proceeding on a best-effort basis.