Intake and the Legacy-Field Pattern

Raw values are faithful and unusable. Computable values are usable and lossy. This chapter shows how to have both, and introduces the six-word vocabulary that stops a completeness audit from becoming a machine for manufacturing plausible fiction.

Chapter 5 · Part II · Getting Data In
Organization of Power Lab
Worked example · twpolitics
~23 min read

The hardest thing a research database has to represent is not a fact. It is the precise shape of an absence: not known, not looked for, known but withheld, or contested. Flatten those into one null and you have destroyed information that took someone an afternoon to establish.

Section 1Two bad options

Return to 林文德, official 3021, whose birth year loaded as 民國45年.

That string is correct. It is the forty-fifth year of the Republic of China, which is 1956. The source said it, the researcher transcribed it faithfully, and nothing is wrong with it — except that no query can compute with it. “What was the average age of party leaders at appointment?” cannot be answered from a text column containing mixed era years, Gregorian years, and nulls.

So something has to change. The two obvious moves are both wrong.

Comparison Two tempting and destructive options

Convert in place

Run a script. 民國45年 becomes 1956. Retype the column to integer.

Lost: that the source used era notation. Lost: the ability to check the conversion. Lost: every value the parser could not handle, which now has to be nulled or dropped.

And the loss is silent. The column looks better afterwards.

Leave it alone

Keep the text column. Ask researchers to interpret it at analysis time.

Lost: nothing. Gained: nothing either. Every downstream user re-solves the same parsing problem, inconsistently, and no two papers agree.

The work is not avoided; it is duplicated and hidden.

The pattern in this chapter takes a third route: add, never alter. Keep the original exactly as it is, under a name that says what it is. Add validated fields beside it. Let a rule and a researcher fill those in, with evidence, one record at a time.

Section 2What intake may not do

Intake is a procedural-control layer. It creates or maps canonical records, preserves source evidence, and reports procedural failures. It does not evaluate research quality and it does not make audit decisions.

Three prohibitions, each of which is violated routinely in systems built without them.

It does not guess

An unparseable value is preserved verbatim and left unreviewed. Converting 民國45年 to 1956 without a recorded human decision manufactures certainty the lab does not have — and, worse, manufactures it at scale, uniformly, so that the resulting errors are correlated and invisible.

This is not an argument against automation. It is an argument about where the automation's output goes. A script that proposes 1956, for a researcher to confirm against the bulletin, is useful. A script that writes 1956 is fabricating evidence.

It does not create audit decisions

A row that loads with a clean, four-digit, Gregorian birth year has not thereby been approved by anyone. The architecture contract states the prohibition explicitly: do not create a synthetic violation, resolved violation, de facto approval, or completeness attestation for a subject that satisfies a rule.

The temptation is real, because it makes reporting tidier — every record would have an audit trail. But an attestation nobody made is worse than no attestation, since a reader cannot distinguish it from one somebody did make.

It does not silently drop rows

Covered in Chapter 3: a row that cannot be represented is a reported procedural failure, not a disappearance.

Section 3The legacy-field pattern

Five steps. This is the core recipe of the chapter, and it generalises to any field where the source format and the analytical format differ.

Procedure The legacy-field pattern
  1. 1Rename the original to legacy_<name>Do not drop it, retype it, or clean it. It is now evidence of what the source said.
  2. 2Add typed, validated companion fieldsValue, precision, source encoding — whatever the domain actually requires.
  3. 3Add a status field defaulting to unreviewedWith an explicit, closed vocabulary. This is the part people leave out.
  4. 4Add a representational check constraintMonths between 1 and 12. Never a completeness constraint.
  5. 5Write the rule against the validated fieldsReading the legacy field as context for the researcher.

Step 1 is a preserving transform — a class C migration in Chapter 2's taxonomy — and it gets the full rehearsal treatment.

In the code

The Taiwan rename is twpolitics.0007_rename_official_birthyear_to_legacy_birthyear. Before it may run against production, the rehearsal must prove that the officials table still holds exactly 50,902 rows and that its aggregate birth-state fingerprint is still 0604cd0044fb6ebbd6eaad4027e9de14da38501f5596b5ea68f4a48cc08589b5.

A rename that changes no data should change neither number. Requiring the proof, rather than reasoning that it must be so, is the difference between a migration that is safe and one that is merely believed to be.

Section 4Eleven columns for a birth year

Applying the pattern to birth information produces this. It looks like a lot for one date; §5 explains why each one is load-bearing.

Schema snapshot twpolitics.officials, birth columns after intake
RoleColumnHolds
Legacylegacy_birthyearThe original official_birthyear. Never edited again.
Rawbirth_raw_valueThe exact source string, e.g. 民國45年
Valuebirth_yearValidated Gregorian year
Valuebirth_month1–12, or null
Valuebirth_day1–31, or null
Derivedbirth_dateA real date, populated only at day precision
Precisionbirth_precisionyear · month · day
Qualifierbirth_qualifierexact · approximate
Encodingbirth_source_calendargregorian · roc_solar
Encodingbirth_source_yearThe era year as given, e.g. 45
Statusbirth_statusThe six-value vocabulary. See §5.
As-ofbirth_as_ofWhen the value was established

Here is 林文德 after a researcher has resolved him:

Data snapshot Before and after resolution
ColumnAt loadAfter resolution
legacy_birthyear民國45年民國45年
birth_raw_valueNULL民國45年
birth_yearNULL1956
birth_monthNULLNULL
birth_precisionNULLyear
birth_qualifierNULLexact
birth_source_calendarNULLroc_solar
birth_source_yearNULL45
birth_dateNULLNULL
birth_statusunreviewedknown
Illustrative. Note that legacy_birthyear is identical in both columns, and that birth_date stays null because year precision cannot produce a date.

Three things are worth reading off that table.

The legacy value never moves. If in three years someone disputes the conversion, the original is still there, unmodified, alongside a recorded decision and a cited source.

birth_date stays null. It is a derived convenience populated only when precision is day. Writing 1956-01-01 and calling it a date would be a fabrication, and a particularly insidious one because it looks like data.

The era encoding is preserved as structure. roc_solar and 45 mean that anyone can independently verify the conversion, and that a systematic parser error could later be found and corrected across every affected record.

Section 5The status vocabulary

This is the most important idea in the chapter, and the one most often absent from otherwise careful systems.

Consider a naive completeness rule: every official must have a birth year. Now give it to a researcher and watch what happens. They work through the queue. Most records resolve. Then they reach 陳淑芬, official 3104, whose birth year is not recorded in any source they can find.

The rule demands a value. The researcher has no value. They have three options: leave it in violation forever, which clogs the queue; skip it, which is untracked; or put something plausible in.

People are conscientious, and under a queue that will not clear they will eventually estimate. A completeness rule with no vocabulary for absence is a machine that converts unknown into wrong — permanently, and at scale, because the resulting values are indistinguishable from established ones.

The fix is to let the researcher say something true in every case:

Vocabulary twpolitics.officials.birth_status
ValueMeansSatisfies the rule?
unreviewedNobody has looked.No
knownEstablished, with evidence.Yes
unknownLooked for. Not found.Yes
not_collectedOut of scope; not yet looked for.Yes
withheldKnown, deliberately not published.Yes
conflictingSources disagree; the conflict is the finding.Yes
Only unreviewed is a violation. Everything else is a researcher having said something true.

The principle, stated in the architecture contract: a guessed value must never be required merely to clear a violation.

Why unknown and not_collected are both needed

They look redundant. They are the difference between a finished task and an unstarted one.

unknown means a researcher searched and the information does not appear to exist in accessible sources. That is a finding. It tells the next person not to repeat the search, and it tells an analyst that the missingness is a property of the historical record.

not_collected means nobody has looked, and by present scope nobody will. That is a scope statement. It tells the next person that the search is still available if their question needs it.

An analyst treating missing data has to know which is which. If birth years are missing because they are genuinely unrecorded, that missingness may correlate with era, region, or prominence — and is itself a finding worth reporting. If they are missing because the lab only collected them for a subset, the missingness is an artefact of scope and must be handled entirely differently.

Where this pays off

In the codebook. A dataset release that reports missing birth years as “NA” has thrown away the distinction. One that reports unknown, not_collected, and withheld separately lets a reader model the missingness correctly — which is the difference between a dataset that supports careful work and one that quietly invites a mistake.

And conflicting

The sixth value is the one that most systems lack and most research needs. 黃建宏's two records differ by two years. Once identity is resolved, if they turn out to be one man, the lab holds two sourced, contradictory birth years.

Forcing a choice would discard evidence. conflicting lets the database record the actual state of knowledge: two sources, both cited, in disagreement. That is not a failure to clean the data. It is a finding, and for some research questions it is a more interesting one than the birth year would have been.

Section 6Representation, not completeness

Step 4 adds a constraint. Its scope is narrow and the narrowness is the point.

Code snapshot twpolitics.0006_add_official_birth_intrinsic_constraint
def add_intrinsic_constraint(apps, schema_editor):
    """Enforce representational validity, not audit completeness."""

-- the constraint body:
ALTER TABLE twpolitics.officials
  ADD CONSTRAINT twp_official_birth_intrinsic_ck CHECK (
        birth_status IN ('unreviewed','known','unknown',
                           'not_collected','withheld','conflicting')
    AND (birth_year   IS NULL OR birth_year   BETWEEN 1 AND 9998)
    AND (birth_month  IS NULL OR birth_month  BETWEEN 1 AND 12)
    AND (birth_day    IS NULL OR birth_day    BETWEEN 1 AND 31)
    AND (birth_precision IS NULL OR
         birth_precision IN ('year','month','day'))
    AND (birth_qualifier IS NULL OR
         birth_qualifier IN ('exact','approximate'))
    AND (birth_source_calendar IS NULL OR
         birth_source_calendar IN ('gregorian','roc_solar'))
    AND CASE WHEN birth_status = 'known' THEN ... END
  );
Abridged. The docstring is quoted in full and is the design principle in one sentence.

Notice what is absent. There is no requirement that birth_year be populated. That question belongs to a rule, because rules are versioned and constraints are not. A lab's completeness standard will change; the fact that months run from 1 to 12 will not.

The test to apply to any proposed constraint: could a correct, honest research record fail this? If yes, it belongs in a rule. If no — if failing it means the value is not even expressible — it belongs in the schema.

The trailing CASE WHEN birth_status = 'known' clause is worth a note, because it is the one place the constraint does encode a relationship: if the status claims the value is known, the value fields have to be coherent with that claim. That is still representational. It does not require anything to be known; it requires that a record not claim knowledge it does not carry.

Section 7Bootstrap versus delivery

Adding eleven columns to a table with 50,902 existing rows raises a question the schema cannot answer: what goes in them, for rows that predate the rule?

There are two distinct transactions here, and conflating them is a documented failure mode.

Comparison Two different operations
BootstrapOngoing delivery
WhenOnce, when the field set is introducedEvery future data load
Applies toRows that predate the ruleNew rows
Must beIdempotent, bounded, previewable, replayableIdempotent, transactional
RiskTouches existing production dataTouches only new data

Setting birth_status = 'unreviewed' across 50,902 existing officials is a bootstrap. It is not intake, it is not a scan, and labelling it as either is how a routine backfill ends up running without the guards a production data mutation deserves. The contract's phrasing is blunt: rule setup must never be mislabeled as new-data intake.

In the code

The bootstrap discipline this project uses, from backend/audits/field_completeness_orchestration.py and its orchestrate_twp_field_001 management command:

  • read-only preview is the default mode — writing requires an explicit flag;
  • the preview is ordered and capped, and its PostgreSQL query plan is inspected to confirm a Limit node and the absence of any ModifyTable node;
  • execution is guarded by exact database name, role, contract identifier, and file checksums, all of which must match or the command refuses;
  • replay produces an identical result and reports that it was a replay;
  • aggregate fingerprints of protected state are captured before and after, and compared.

The query-plan inspection is the detail worth stealing. It does not trust that the preview is read-only; it asks the database to prove that the statement it is about to run contains no write node.

Section 8Minting provisional records

One more intake operation, which will matter a great deal in Chapter 8.

A source records that 吳美玲 chaired a party branch in 2003. The party-leadership row can be created — party, position, dates are all present. But official_id is null, because no existing official record has been matched to her.

Three options. Leave it null: honest but unqueryable, and she appears in no analysis of party leaders. Guess at an existing official with a similar name: fast, and sometimes wrong in a way nobody will ever detect. Or mint a provisional official: create a new record asserting only that a person was observed in this role.

The third is right, because it is the only one that is both true and usable. It asserts exactly what the source supports, and it creates something that can be referenced, analysed, and later merged if she turns out to be someone already in the database.

Four constraints:

  • Conservative. Mint only where the reference is absent. Never re-mint where an evidenced link already exists — that destroys a known-good link and manufactures work.
  • Idempotent. Replaying the routine over the same source rows creates nothing new.
  • Marked. A provisional record is distinguishable from an established one, so downstream consumers and rules can treat it appropriately.
  • Scoped by an explicit exclusion list. For Taiwan, do not mint for family_records alters. A person named as someone's relative is not thereby an official, and minting would defeat the referential rule whose whole job is to match family mentions to existing officials without creating new ones.

Minting is invoked through an explicit contract, never as a side effect of a form. The architecture contract requires it: creating a provisional target, when permitted, is an intake procedure invoked through an explicit contract rather than an implicit FK-form side effect.

In the code

Taiwan's quarantine resolution minted 661 provisional officials while processing 5,473 quarantined source rows into 6,134 canonical destinations. Those 661 are people observed in a role, with no prior match — and they are precisely the population that identity resolution will later examine, because some of them are certainly already in the database under a different identifier.

The data is now in, attributed, and structured so that a researcher can improve it without destroying it. What is still missing is the machinery that decides which records need attention and routes them to people. That is the audit layer, and Chapter 6 takes apart its central object: the rule.

Terminology introduced in this chapter

intake
The procedural layer that creates and maps canonical records and preserves source evidence. Does not judge quality.
legacy field
An original, unvalidated column preserved under a legacy_* name after a validated replacement is added. Never edited.
validated field
A typed, vocabulary-constrained companion field that the audit layer helps complete.
status vocabulary
The closed set of values describing what is known about a field: unreviewed, known, unknown, not_collected, withheld, conflicting.
precision
How exact a value is — year, month, or day. Distinct from whether it is present.
qualifier
Whether a value is exact or approximate. Distinct from precision.
bootstrap
A one-time, idempotent, previewable application of a rule's logic to rows that predate the rule. Not intake, not a scan.
ongoing delivery
The transaction used for future data loads once a rule exists.
minting
Creating a provisional record for an entity observed in a role with no existing reference. Conservative, idempotent, marked, scoped.