A research assistant opens the audit dashboard and sees a list of things to fix. Behind each line sits a small stack of objects that most data systems never separate — and the separation is what lets the lab say, two years later, exactly what standard a given record was judged against.
Section 1What “clean” cannot mean
Start with a record from the Taiwan Politics database. An official — a person who has stood for election, led a party branch, or chaired a legislative committee. The database holds 50,902 of them.
Here is one, as it looked the day it was loaded:
| official_id | official_name | legacy_birthyear | birth_status |
|---|---|---|---|
| 3021 | 林文德 | 民國45年 | unreviewed |
| 3104 | 陳淑芬 | NULL | unreviewed |
| 3417 | 黃建宏 | 1961 | unreviewed |
| 3418 | 黃建宏 | 1963 | unreviewed |
Four rows, four different problems. Row 3021 has a birth year written in Republic of China era notation, which is correct in its source and unusable in a calculation. Row 3104 has nothing at all — but we cannot tell whether that means nobody looked, or somebody looked and failed to find it. Rows 3417 and 3418 might be two men who share a common name, or one man entered twice from two sources that disagreed by two years.
Now try to write down what it would mean for this table to be “clean.” You cannot, because the question is malformed. Cleanliness is not a property a row has. It is a property a row has with respect to a particular standard, at a particular moment. Row 3021 is perfectly clean if your standard is “preserve what the source said.” It is unusable if your standard is “every official has a comparable birth date.”
So the audit system never asks whether data is clean. It asks a narrower question, over and over: does this specific record satisfy this specific criterion, as that criterion was defined on this specific date? Everything in this chapter exists to make that question precise enough to answer mechanically, and stable enough to still mean something next year.
Section 2Four kinds of rule
A rule is a positive criterion — something the data ought to satisfy. Writing rules as positives rather than as error descriptions is a small discipline with a large payoff: it forces you to say what good looks like, and it means a rule can be satisfied, which is a reportable fact.
The lab's audit system recognises exactly four types. Not five, not a general-purpose “other.”
| Type | Token | The question it asks a researcher |
|---|---|---|
| uniqueness | UNIQ | Do these identifiers denote the same entity, different entities, or is it still unresolved? |
| referential | REF | Which target is correct, or is no candidate a match? |
| field | FIELD | What evidence-backed value or permitted status completes this field? |
| consistency | CONS | Which underlying fact is wrong, or is an exception supported? |
Classify a new rule by working down that list in order and stopping at the first match. Is it a collision under some identity key? That is uniqueness. Is it about the existence or target of a relationship? Referential. Is it about one atomic value standing on its own? Field. Is it a predicate over two or more values that are already established? Consistency.
The order matters because the categories overlap if you let them. Two officials with the same name and conflicting birth years could be filed as a consistency problem — two values that disagree. Filing it that way would be a mistake, because the actual question a researcher must answer is whether these are one person or two, and that is an identity question. Resolve it as uniqueness and the birth years take care of themselves.
Consistency is the dangerous one. Because it is defined as “a predicate over established values,” almost anything can be forced into it. The system's architecture contract draws an explicit fence: consistency does not decide identity, uniqueness, relationship targets, or the independent validity of one field. If a rule you are drafting seems to be a consistency rule but its resolution would change who a record refers to, you have mis-typed it.
Section 3Definition, version, binding
This is the section that repays careful reading. A “rule” in ordinary speech is actually three nested objects, and collapsing them is the most common design failure in audit systems.
The definition
The reusable idea, in words: “Official birth information should be reviewed and complete at its supported precision.” A definition has no code and no table attached. It is the thing you would explain to a colleague over coffee. It changes almost never.
The version
An immutable pinning of everything needed to execute that idea: which evaluator runs, which resolution form appears, what counts as an acceptable outcome, what priority policy applies — and a checksum over all of it.
Immutable is meant literally. You never edit a version to change what a rule means. You create a new one. This is the single most important constraint in the whole design, and it exists for one reason: a decision a research assistant made in October must still be interpretable in March. If the rule could be quietly edited underneath them, their judgement would become unreadable — you would know what they decided but not what question they were answering.
The lab's first rebuilt rule, TWP-FIELD-001, is declared in backend/audits/field_completeness_v1.py. Its registration packet pins a SHA-256 for that file and for every other file that participates in evaluating or resolving it — the executor, the forms, the services, and the four migrations that create its columns. At runtime, preview and apply both refuse to proceed if any of those hashes has moved.
That means 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 — which is the point.
The binding
And here is the word that gives people trouble.
A binding applies one selected immutable version to one exact place in the database.
The version says what the rule means. The binding says where it is being applied. One definition, with one version, can have many bindings — and that is not an edge case, it is the normal situation.
Section 4One idea, five places
The clearest example in the Taiwan catalogue is a rule the lab has since retired, but which illustrates the structure perfectly. Its idea was: a date should be present, complete, and standardised. One idea. One implementation. Five places it applied:
| # | schema | table | field | Active? |
|---|---|---|---|---|
| 1 | twpolitics | officials | birth | yes |
| 2 | twpolitics | legislative_sessions | session_start | yes |
| 3 | twpolitics | legislative_sessions | session_end | yes |
| 4 | twpolitics | party_leaders | appointment_start | yes |
| 5 | twpolitics | party_leaders | appointment_end | yes |
Why go to this trouble rather than writing five separate rules? Three reasons, and they compound.
- One implementation to get right. The hard part of a date-completeness rule is the precision handling — year versus month versus day, Gregorian versus era calendars. You want to solve that once.
- Independent control. Party-leadership dates might be ready to audit while legislative-session dates are still being restructured. Five bindings let you activate one and hold four.
- Honest reporting. “This rule found 400 problems” is much less useful than “this rule found 400 problems, 380 of them in one table.”
The runtime table is audits.rule_applications, and the older codebase calls a binding an application. The two words mean the same thing; newer documentation prefers binding because “application” collides with the Django sense of the word. Both appear in the database today, and the glossary treats them as synonyms.
One more thing about bindings, and it is a kindness to the people who use the system: a research assistant never sees this three-level structure. In the dashboard, a binding is simply called a rule. The distinction between definition, version, and binding exists for reuse, for history, and for drift control — not to be explained to someone trying to fix a birth date.
Section 5What a binding declares
A binding is not just a table name. It declares four sets, and these four sets are what make the system safe to let researchers loose on.
| Set | What it controls |
|---|---|
| criteria_read_set | The exact schema, table, key, fields, and semantic roles used to evaluate the criterion. Nothing else is consulted. |
| resolution_write_set | The exact records and fields the resolution form may write. Nothing outside this set is writable, by anyone, through this rule. |
| context_read_set | What is displayed to help the researcher decide — profiles, linked records, raw sources, history — but never mutated by this rule. |
| revalidation_trigger_set | The fields whose change makes a current observation or decision stale, scheduling re-evaluation. |
The second one is the safety mechanism. A resolution form is not a database editor. When a researcher opens a birth-completeness violation, the form can write to the birth fields on that official and to nothing else — not the name, not the party, not another official's record. That constraint is declared data, checked at registration against the live PostgreSQL catalogue, and enforced at write time.
The fourth one is the subtle one, and it is where audit systems quietly rot. Suppose a researcher decides that two officials are different people, partly because their recorded birth years are four years apart. Later, someone corrects one of those birth years, and now they match. The earlier decision was reasonable when it was made and is wrong now. Unless the birth year is in the revalidation_trigger_set, nothing will ever reopen it, and a stale judgement will sit in the database forever wearing the costume of a resolved one.
Every rule workshop must answer the question “what makes a decision on this rule stale?” before any code is written. Getting it wrong is not visible in testing — the system looks fine and slowly accumulates confident, obsolete answers.
Section 6Segments: making a big rule small
Here is a practical problem. TWP-FIELD-001 applies to all 50,902 officials, and on the day it is switched on, every single one of them has birth_status = 'unreviewed'. That is 50,902 violations.
At three minutes each, that is roughly 2,500 hours of work. Handing that queue to five research assistants is not a task list; it is a demoralising wall.
A priority segment is a versioned subset of a single binding — not a separate rule — with its own priority and label. TWP-FIELD-001 has three:
| Code | Slug | Priority | Membership predicate |
|---|---|---|---|
| …-S1 | party-leader | High | An extant party_leaders.official_id reference |
| …-S2 | legislative-leader | High | An extant legislative_leaders.official_id reference |
| …-S3 | political-family | High | An extant family_records principal reference |
| — | (base) | Low | Everyone else |
The segments are chosen to match what the lab actually publishes on. Officials who led a party, chaired a legislative body, or appear in the political-families data are the people whose birth dates end up in papers. The long tail can wait, indefinitely if necessary, without blocking anything.
Note what segments are not. They do not change what the rule means, and they do not change whether a record is in violation. A low-priority violation is just as real as a high-priority one. Segments affect queueing only — which is exactly why they are versioned subsets of one binding rather than separate rules that would fragment the reporting.
Section 7Three states, not one
Most task systems give an item one status field: open, in progress, done. An audit system cannot, and the reason is worth understanding because it is the design decision that makes the rest of the system honest.
Consider a researcher who examines officials 3417 and 3418 — the two 黃建宏 records — and concludes they are the same man, entered twice. She records that decision. What is the status of this violation now?
- She has decided. That is true.
- But the two records have not been merged, so nothing has been applied.
- And because nothing has been merged, the collision still exists — the rule, re-run right now, still evaluates false.
One status column cannot express that, and yet it is a completely ordinary state that may persist for weeks. So the system tracks three independent dimensions:
| Dimension | What it describes | Values |
|---|---|---|
| Evaluation | What the evaluator found, last time it ran | satisfied · violated · indeterminate · error |
| Decision | What a human concluded | undecided · decided · deferred · superseded |
| Resolution | What has actually happened to the data | not_attempted · planned · applied · pending_revalidation · validated · failed · stale |
The one that surprises people is indeterminate. It is not a polite word for “violated.” It means the evaluator genuinely could not tell.
An example: a consistency rule checks that an election took place after a candidate was born. One record has a birth date precise to the day; another has only a year. Comparing them is not a failure of the data and not a violation — it is a comparison that cannot be made at the available precision. The system records indeterminate, and the interface is forbidden from presenting it as a proven error. Getting this wrong means sending researchers to fix things that are not broken, which is the fastest way to lose their trust in the system.
Every evaluator returns the same result envelope regardless of type: the binding and version identifiers, a stable subject fingerprint, the evaluation_state, the observed values, the subject revisions, a timestamp, and the executable checksum. Uniform envelopes are what let one violation-index page list findings from four different rule types without special-casing any of them.
Section 8What a researcher sees
All of that structure exists to produce one screen. Here is what 林文德's birth violation looks like when Emily opens it:
林文德 · official 3021
Criterion: official birth information is reviewed and complete at its supported precision.
民國45年民國45年unreviewedRepublic of China era year 45 corresponds to Gregorian 1956. Record the conversion, or select a status if the value cannot be established.
Every element on that screen traces back to something declared earlier. The criterion sentence comes from the definition. The version badge comes from the immutable version. The fields shown come from the criteria_read_set; the source line comes from the context_read_set. The three buttons are exactly the outcomes the version's decision contract permits — no more.
And crucially, “Mark unknown” satisfies the rule. If Emily searches and cannot establish a birth year, recording unknown with a rationale closes the violation honestly. A guessed value is never required to clear a task. This is the rule that protects the data from the system: without it, a completeness audit becomes a machine for manufacturing plausible fiction.
Submitting the form does not write directly to twpolitics.officials. It calls an allowlisted Django service that takes an optimistic-concurrency lock, renders a write preview, checks permissions, writes the domain fields inside the resolution_write_set, creates a violation_decision row, attaches an evidence_target pointing at the cited source, appends an immutable change record, and then re-runs the evaluator. Only if that re-evaluation returns satisfied does the violation close.
The closed violation is not deleted. Its observations, decisions, evidence, and changes stay exactly where they are, which is how the lab can later state which rule version governed a given field in a published dataset.
In the next chapter we follow this single rule all the way through its lifecycle: the migrations that create its columns, the manifest that pins its behaviour, the rehearsal against a disposable copy of the production database, and the eleven separately approved steps between “the code works” and “a researcher can use it.”
Terminology introduced in this chapter
- rule
- A positive criterion that in-scope data ought to satisfy. Informally, the thing a researcher works; formally, three nested objects.
- definition
- The reusable idea and its decision vocabulary. No code, no table. Changes almost never.
- version
- An immutable pinning of evaluator, adapters, contracts, priority policy, and checksums. A semantic change creates a new version and never edits one.
- binding
- One selected version applied to one exact schema, table, key, and field set. Synonym: application. What the interface calls a rule.
- priority segment
- A versioned subset of one binding with its own priority. Affects queueing only, never truth.
- violation
- A concrete subject for which the binding evaluates false. Legacy synonym: issue.
- indeterminate
- An evaluation that genuinely could not be made — not a violation, and never displayed as one.
- criteria read set
- Exactly what the evaluator consults.
- resolution write set
- Exactly what the resolution form may write. Nothing else is writable.
- context read set
- What is shown to support a decision but never mutated.
- revalidation trigger set
- Fields whose change makes a decision stale and schedules re-evaluation.