# 05 — Governance and Privacy

**Measured against:** `/home/datashield/library` at v0.22.188 (read-only pass, 2026-09-22).
**Audience:** data-governance and privacy teams, developers, and AI agents driving the MCP surface.
**How to read this document:** every claim carries a `file:line` citation into the tree it was measured from. Where the code and a stated objective disagree, the disagreement is written down rather than smoothed over (CLAUDE.md META-CALIBRATION 5). Normative keywords (MUST / SHOULD / MAY) are used per RFC 2119 only where the cited standard uses them.

---

## 1. The doctrine in one page — STANDARD-009

The privacy doctrine lives in one document: `docs/standards/STANDARD-009-privacy-classification-and-masking.md` (status MANDATORY, `:3`). It governs **what a column is** (classification) and **what the platform then does about it** (governance, masking, refusal) (`:33-38`). It binds every producer and consumer of a column classification, every masking seam, the PHI egress gate, and every committed artifact that records a classification (`:4-6`, `:42-59`).

The fifteen rules (PC-1 … PC-15) in plain language:

| Rule | Plain-language meaning | Where it is measured |
|---|---|---|
| **PC-1 One kernel, pure** | Every classification decision comes from one function in `lib/classification/`. It never reads a database, file, clock, network, env var, or random source. Private "is this PII?" helpers elsewhere are forbidden and are deleted, not deprecated. | `STANDARD-009:80-87`; kernel entry `lib/classification/classify.ts:73` (`classifyColumn(e: ColumnEvidence)`), `:519` (`classifyTable`); gate `audit:classification-kernel-purity` |
| **PC-2 One governance producer** | Only `deriveGovernance(decision, regime)` produces the governance block (`piiDetected`, `phiDetected`, `securityLevel`, `encryptionRequired`, `maskingRules`, `accessPolicy`). Every field is derived through a config ladder, never typed literally at a call site. | `STANDARD-009:89-95`; `lib/classification/governance.ts:46`; ladder `config/classification-evidence.config.ts:871` (`GOVERNANCE_DERIVATION`); gate `audit:classification-governance-coherence` |
| **PC-3 One pattern library, as data, joined on `ClassId`** | The pattern corpus is configuration data, consumed by server lanes and the browser editor from the same artefact. Every join (mask token, severity, regulation, category) keys on `ClassId`, never on a display name — two distinct classes already shipped both displaying as "DEA Number". | `STANDARD-009:97-105`; `config/classification-patterns.config.ts:77` (`CLASSIFICATION_PATTERNS`, 129 `"id"` rows measured; `:5312` index `CLASSIFICATION_PATTERN_BY_ID`) |
| **PC-4 `unclassified` is not `public`** | Five honest states: `unscanned` (we did not look), `unclassified` (we looked, nothing cleared its floor), `candidate` (plausible), `classified`, `overridden`. `unclassified` and `candidate` derive a non-PUBLIC floor (default `INTERNAL`). A surface that renders an unlabelled column as safe is non-conformant. | `STANDARD-009:109-116`; vocabulary table `:63-72` |
| **PC-5 Fail-closed readers** | A consumer meeting an unknown class, unknown state, missing decision, stale `classification_ref`, or unavailable classifier must not serve raw values or publish. | `STANDARD-009:118-124`; `lib/analytics/classificationRef.ts` |
| **PC-6 Value-free artifacts** | Fixtures, corpora, baselines, probe captures, shadow diffs, impact ledgers, bug files, vet records and release notes must not contain a customer value, secret, credential or anything derived from one — shapes only. | `STANDARD-009:128-139`; `lib/classification/valueLeak.ts`; `config/dataset-field-projection.config.ts:153` (`FIELD_VALUE_BEARING_KEYS`) |
| **PC-7 Masking OFF by default; legally-required only** | Operator rulings R2/R3 (2026-09-06) are reproduced verbatim and may not be re-opened. Posture default `development` (raw-by-default). Classification is still computed and listed regardless — visibility is not masking. Explicit requests and posture-independent surfaces (the `mdm.golden_v1_*` views) always mask. A caller-selectable masking `framework` may not appear on a read surface. No agent introduces a new privacy rule without an operator ruling. | `STANDARD-009:143-170`; `config/masking-policy.config.ts:77-89` |
| **PC-8 Error paths may not change the masking decision** | Unknown-state policies are config-declared (`RELOAD_MASKING_ON_UNKNOWN` = `keep_previous`, `QUERY_MASKING_ON_UNKNOWN` = `refuse`) and disclosed. A `catch` that yields "unmasked" is forbidden; so is a fix that turns masking on where doctrine says off. | `STANDARD-009:172-178`; `lib/query/maskingPolicy.ts` |
| **PC-9 Proposers propose, rules veto, humans on disagreement** | See §1.1. | `STANDARD-009:182-195`. The policy is declared in the standard's own table; `config/classification-arbitration.config.ts` is named there but **does not exist in the tree** at v0.22.188, and its gate `audit:classification-arbitration-integrity` is **planned — P4** (`:320`) |
| **PC-10 Protective-only overrides** | An override carries actor, reason and evidence legs and writes a ledger row. Relaxing a corroborated PII verdict is refused with a typed refusal, whoever asks. One reclassification verb. | `STANDARD-009:197-202`; gate `audit:classification-decision-integrity` **planned — P3** (`:321`) |
| **PC-11 Browser, session and model proposals never write directly** | A payload from a browser engine, a session agent, or a model is never written verbatim to `pii_class`, `semantic_type`, `governance`, `classification_state`, `has_pii`, `has_phi`, `compliance_level`. | `STANDARD-009:204-214` |
| **PC-12 Inheritance is declared** | A decision inherited through a derivation (join/union/transform) is declared as inherited, not assumed fresh. | `STANDARD-009:216-225`; live inheritance leg **planned — P2** (`:322`) |
| **PC-13 Append-only decisions, provenance mandatory** | Every decision is a new row in an append-only ledger; the live column holds a pointer to the head that is gate-checked against the derived head. Every row carries `source`, `tier`, model/prompt version. | `STANDARD-009:229-238`; `lib/analytics/evidenceLedger.ts`. `audit:audit-log-immutability` gates the general audit log, not the classification decision ledger: the standard's own PC-13 gates (`audit:column-writer-ownership`, `audit:classification-decision-integrity`) are **planned — P3** (`:323`) |
| **PC-14 Measure before and after** | No classification, masking or PHI-gate change ships without a before and an after measurement using the committed script. | `STANDARD-009:242-266`; `audit:classification-baseline`, `audit:classification-shadow-diff`, `audit:classification-impact-ledger` |
| **PC-15 The doctrine is gated or it is theatre** | A change to classification, masking or the PHI gate updates this standard, `docs/codebase/` and the release notes in the same change, and every rule above names the gate that checks it. A rule with no gate is recorded as **planned** and may not be described anywhere as enforced. Doc claims about this seam are themselves checkable facts. | `STANDARD-009:270-277`; enforcement table `:307-328` |

**Which of these are actually enforced:** `STANDARD-009:298-328` is the authority, and its own status vocabulary (`:300-305`) distinguishes *chained* (a `release:gates` link today) from *unchained* (exists, run deliberately) from *planned* (does not exist yet). PC-1, PC-2 (legs 1–3), PC-3, PC-5's golden-boundary leg, PC-6's residue gate and PC-14's regression/quality gates are chained; the PC-4/PC-5 fail-closed gate, the PC-9/PC-11 arbitration gate, the PC-10 override-direction gate, the PC-12 live inheritance leg and the PC-13 writer-ownership gates do **not** exist yet. Nothing in that table may be read as enforced until it is chained.

### 1.1 Who decides (PC-9, A1–A8)

`STANDARD-009:186-195` declares the arbitration policy as a table; the operational meaning:

1. **The rule floor vetoes.** A proposal failing gates G1–G7 is suppressed with a disclosed reason — "a veto a vote could reverse is not a veto" (A1). The kernel comment at `lib/classification/classify.ts:195` names the forbidden case: a vote that moves `unclassified` to `classified` by removing a deterministic suppression.
2. **Votes are over the vocabulary bucket** (`pii` | `secret` | `format`, `config/conduit-analysis.config.ts:273` `PATTERN_CATEGORY_VOCABULARY`), never the pattern id; the id is chosen by the specificity lattice (`lib/classification/lattice.ts`) (A2).
3. **Abstention is not dissent** — an engine outage cannot change a classification (A3).
4. **Disagreement produces `candidate` plus a steward-queue entry.** That is the only point where a human appears; silent tie-breaks are forbidden (A4), and the interim tie-break is the more protective bucket, with the dissent recorded verbatim (A5).
5. **Every proposal, including losers, is recorded** with tier and model/prompt version (A7); the governance bucket is derived from the id, never from a proposer's self-report (A8 — measured contradicting its own id 3 times in 40).

---

## 2. The kernel and its configuration

| Module | Role (measured) |
|---|---|
| `lib/classification/classify.ts` | 522 lines. `classifyColumn(e: ColumnEvidence): ClassificationDecision` (`:73`); candidate set built from regex hits, name-lexicon rules, secret rules, custom detectors and generalizer hints (`:113-124`); per-class floors resolve to `classified` / `candidate` / below-floor (`:12`). `classifyTable` (`:519`). |
| `lib/classification/governance.ts:46` | `deriveGovernance(decision, regime)` — the single PC-2 producer. |
| `lib/classification/frameworks.ts` | `obligationsFor(classId)` — the ONE reader of the compliance-obligation table (`config/classification-frameworks.config.ts:24`). |
| `lib/classification/index.ts`, `patterns.ts`, `lattice.ts`, `regexSafety.ts`, `valueLeak.ts`, `shadowSink.ts`, `digest.ts`, `wire.ts`, `types.ts`, `collect/`, `signals/`, `text/` | Pattern access, specificity lattice, regex-safety gate, value-leak detector (PC-6), shadow-run sink (PC-14), evidence digesting, wire shapes. |

### 2.1 Compliance frameworks and `obligationsFor(classId)`

`config/classification-frameworks.config.ts` declares **seven** frameworks (`COMPLIANCE_FRAMEWORKS`, `:52-59`) with edition, jurisdiction and a citation URL — five `active` and two `charter`. A `charter` row is a crosswalk placeholder and MUST carry zero obligations; an `active` row MUST carry at least one (`:22-23`, validator `:209`):

| id | Framework | Edition cited | Status | Legacy tags absorbed |
|---|---|---|---|---|
| `hipaa` | HIPAA Privacy Rule — Safe Harbor de-identification | 45 CFR §164.514 (2013 Omnibus) | active | `HIPAA` |
| `gdpr` | General Data Protection Regulation | Regulation (EU) 2016/679 | active | `GDPR`, `UK-GDPR` |
| `pci_dss` | PCI Data Security Standard | v4.0.1 (2024) | active | `PCI-DSS`, `PCI` |
| `soc2` | AICPA SOC 2 Trust Services Criteria | TSC 2017 (2022 revised points of focus) | active | `SOC2` |
| `fedramp` | FedRAMP Moderate baseline | Rev. 5 (2023) | active | `NIST` |
| `iso27701` | ISO/IEC 27701 Privacy Information Management | 2019 | charter | `ISO27001` |
| `nist_800_53` | NIST SP 800-53 Security and Privacy Controls | Rev. 5 (2020) | charter | — |

Obligations are rows joined on `ClassId` (`COMPLIANCE_OBLIGATIONS`, `:108-163`): HIPAA Safe-Harbor letters **A–Q** are attached to the kernel's `hipaa_*` classes across 21 rows (e.g. `hipaa_email` → letter F, `:115`; `hipaa_ssn_dashed`/`hipaa_ssn_nodash` → G, `:116-117`); letter R, the statute's catch-all "any other unique identifying number", has no kernel class and so no row. One builder per active framework — HIPAA Safe Harbor, GDPR personal-data, PCI PAN, SOC 2 credential, FedRAMP PII (`:78-102`) — emits 49 obligations in total. Obligation text is bounded to 240 characters and must carry an RFC 2119 keyword (`:74-76`). The `compliance[]` free strings that predate this table (`PCI-DSS` vs `PCI`) are recorded as the disagreement this file resolved (`:8`).

**Count discipline (a live disagreement, verified twice):** STANDARD-009 `:72` warns that the 18 statutory HIPAA identifiers and the number of `hipaa_*` pattern rows are different numbers and must not be conflated, and puts the pattern-row figure at **10** after the Safe-Harbor taxonomy split (restated at `:276`). The live config does not reproduce it: `grep -c '"id": "hipaa_' config/classification-patterns.config.ts` measures **40** rows, of which 12 carry the `PHI` category and 33 carry `PII_QUASI_IDENTIFIER` — so 10 is not the count of `hipaa_`-id rows, nor of `PHI`-category rows, under any reading. Treat the live config as authoritative and the standard's figure as a stale snapshot; PC-15 (`:270-277`) makes the mismatch a defect of the standard, not a cosmetic one.

### 2.2 The regex-safety gate

`config/classification-regex-safety.config.ts` makes every kernel regex answer to a structural bound and an adversarial time budget (`:7`):

- `REGEX_TIME_BUDGET = { perInputMs: 25, samples: 3, warmupRuns: 1, reportSlowest: 8 }` (`:66`), against an `ADVERSARIAL_CORPUS` (`:83`) over 4 KiB inputs.
- Discovery roots `["lib/classification", "config"]` (`:236`); carrier exclusions declared as data (`:170`).
- A **grandfathered-findings ledger** (`:208`) capped at `MAX_GRANDFATHERED_FINDINGS = 4` (`:219`); **four** entries today (`:209-216`), each with a prepared rewrite (`REGEX_SAFETY_PREPARED_REWRITES`, `:228`): two `nested_unbounded_quantifier` — `hipaa_udi` and `hipaa_name_labeled`, both timed linear (`:209-212`) — and two `time_budget` — `hipaa_email` (measured 29 ms against the 25 ms budget) and `email_standard` (37 ms) (`:213-216`). The ceiling is exact, not slack: the config's own validator refuses a `MAX_GRANDFATHERED_FINDINGS` above the row count and tells the committer to lower it when an entry retires (`:275-276`). The gate is `audit:classification-regex-safety` (in `package.json`), backed by `lib/classification/__tests__/regexSafety.test.ts` (`:223`).

### 2.3 A naming disagreement to know about

The MCP umbrella named **`classification`** is *not* the PII kernel. Its published description (`docs/versions/v0.22.188/tools/classification.md:14-16`) is "Product/industry classification catalog lookup + AI dataset tagging with a human-review workflow" — six commands: `systems_list`, `lookup` (UNSPSC-style codes), `dataset_classify`, `batch_classify`, `proposals_list`, `proposal_decide` (`:18-23`). It shares the STANDARD-009 posture that proposals never auto-apply (`batch_classify` "never auto-applies"; `proposal_decide` is the only way a proposal becomes live), but its subject is product/industry taxonomy tags. The PII/PHI kernel is reached through the catalog and loader seams (`lib/data_catalog/piiLabel.ts`, `lib/query/loader.ts`), not through a same-named umbrella. Agents searching for "classification" should read both.

---

## 3. Masking

### 3.1 Posture

`config/masking-policy.config.ts` is "the privacy-posture knob" (`:4`). Two postures (`:19-26`): `development` (DEFAULT — the loader applies no auto-masking at any compliance level) and `protected` (auto-mask per reconciled authority). `MASKING_POLICY_CONFIG.posture = "development"` (`:89`), typed as the union so `=== "protected"` comparisons still type-check (`:87`). Three things are posture-independent by design (`:43-69`): the MDM read boundary (the inner compliance boundary, `always_protected`, `:50-54`), at-rest write maskers (a value captured under `development` must not become exposable by a later flip, `:59-61` — `lib/mdm/goldenRecordMasking.ts maskFreeTextPiiAlways`), and any explicit per-dataset `masking_mode`. The documented consequence — one golden served raw by the read boundary while the `golden_v1_*` view masks it, under `development` — is stated so it "can never again be read as a defect" (`:64-69`).

### 3.2 `masking_set` and the masked-view family

- Changing a dataset's masking mode is routed through **`dataset_admin.masking_set`** (change mode) and **`dataset_admin.masking_reload`** (`lib/query/maskingPolicy.ts:144-145`, `MASKING_CHANGE_ROUTE` / `MASKING_APPLY_ROUTE`); applying at load is `dataset_ingest.load` with `masking_mode`. `masking_set`'s confirm is "the human ack", while destruction happens in the loader (`lib/query/loader.ts:122`).
- Masked golden consumption reads the `mdm.golden_v1_<category>` views (`lib/mdm/goldenConsumptionRead.ts:10`; `stewardship.golden_read`), already-masked at the read boundary (`lib/mdm/goldenRagRender.ts:11`).
- Exported masked views are rendered by ONE projection writer with SQL dialects as data (`config/masked-view-dialects.config.ts:2-27`, `MASKED_VIEW_DIALECTS` `:99`, `DEFAULT_PARTIAL_KEEP_LAST = 4` `:36`); DuckDB is the materialisation dialect (`:133`, LIB-BUG-1412). Mask generators themselves (`MASK_SQL`, `goldenRecordMasking.maskAttributeValue`, `mdm.golden_mask_scalar`, `tokenizer.GENERALIZERS`) are out of STANDARD-009's scope by design (`STANDARD-009:36-38`).

### 3.3 `generalization` — privacy-preserving column transforms

Umbrella `generalization` (7 commands: `list`, `apply`, `preview`, `update`, `delete`, `bulk_apply`, `stats`; `docs/versions/v0.22.188/tools/generalization.md:14`). View-mode (reversible masking at query/prompt egress) works; **physical in-place transform is not implemented** and returns `not_implemented` (FEAT-033) (`:16`). `config/generalization.config.ts` declares a strategy↔column domain-fit preflight (`domainFitMinHitRate`, `:29-44`, LIB-BUG-482) and a per-regime strategy map (`COMPLIANCE_STRATEGY_MAP`, `:96-101`): `hipaa` → `dob_generalize`, `zip_generalize`, `age_band`; `gdpr` → `email_domain`, `phone_partial`; `ccpa` → `ssn_partial`, `email_domain`; `pci` → **empty**, deliberately — no built-in strategy addresses PAN (`:93`).

---

## 4. Sample values, statistics and acknowledgements

### 4.1 The sample-value ladder

`config/data-catalog.config.ts` defines `SAMPLE_VALUE_POLICIES = ["plain", "masked", "omit"]` (`:1110`) and `SAMPLE_VALUE_POLICY` with `default: "omit"` (`:1116`). The prior default was `plain` (operator ruling 2026-09-06, `:1088`); `plain` remains available but `requiresAcknowledgement: ["plain"]` (`:1133`) — a caller must pass `acknowledge_plain_samples: true`, and the acknowledgement is written to the job record (`:1106`). `masked` is `shapeOnly` (`:1158`); `omit` is the `withheldPolicy` (`:1146`). The ladder's rungs are properties of a `catalog.*` asset: `column` / `scope` / `provider` / `platform` (`config/dataset-field-projection.config.ts:22`), with `scope` outranking `provider` (`config/data-catalog.config.ts:1123`).

`config/dataset-field-projection.config.ts` closes the read side: the registry detail read had bypassed the ladder (`:16`); it now resolves the same ladder (no second ladder, no new rule — `:11-13`) and, when it withholds, writes a `sample_values_withheld: { reason, policy }` marker (`SAMPLE_WITHHELD_KEY`, `:107`) citing GDPR Art. 5(2) accountability (`:55`). Reasons are a closed vocabulary of five (`:87-102`): `audience_not_entitled` (the audience floor — a `public` audience never receives values), `sample_policy_omit`, `plain_samples_not_acknowledged`, `sample_policy_masked`, `sample_policy_plain`. Value-bearing keys are absent, not nulled, so the marker is not an existence probe for anonymous callers (`:59-61`).

### 4.2 Export statistics and sidecars

`lib/catalog/exportCatalog.ts` writes sidecars into the customer's destination bucket beside the data (`config/catalog-export-statistics.config.ts:5`): `catalog/_metadata/governance.json` carries "PII exposure, masking rules, framework applicability" (`exportCatalog.ts:9`) and has exactly one writer (`:134`). Sidecar format versions (`config/catalog-export-statistics.config.ts:75-88`): `dataset 1.0`, `schema 1.1`, `lineage 1.0`, `quality 1.1`, `governance 1.3` — 1.3 added `data_file`, the first governance claim about the data artefact itself rather than about the generated masked view (LIB-BUG-1412), after LIB-BUG-1394 found a sidecar claiming masking no artefact applied (`:22`).

`EXPORT_STATISTICS_POLICIES = ["omit", "aggregate_only", "plain"]` (`:29`); `statistics_policy` is stamped on each document (`:62-70`). `lib/catalog/statisticsPolicy.ts` resolves the policy (`resolveExportStatisticsPolicy`, `:81`) and throws `ExportStatisticsAcknowledgementError` when a plain-persisting policy is requested without `acknowledgePlainStatistics: true` (`:90-91`); the value-exposure predicate is shared with the catalog ladder (`policyPersistsPlainValues`, `:26`) so this module "contributes no policy vocabulary" (`:14`).

### 4.3 The acknowledgements audit

`audit:catalog-plain-sample-sweep` (`scripts/audit/catalog-plain-sample-sweep.ts`, LIB-BUG-1041 item 3) reports every row still at `plain`. It is **read-only by default**: an earlier version healed legacy acknowledgements forward into the immutable ledger as a side effect of reading; the heal is now opt-in (`--heal`) and announced in output (`:5-9`). Sibling gates measured in `package.json`: `audit:sample-store-integrity`, `audit:classification-config-agreement`, `audit:classification-row-predicate`, `audit:classification-single-seam`, `audit:governance-vocab-integrity`, `audit:catalog-classification-quality`, `audit:classification-coverage-floor`, `audit:classification-ootb`, `audit:classification-golden-file`, `audit:classification-golden-bakeoff`, `audit:classification-regression`.

---

## 5. Stewardship — what a steward does

### 5.1 The MCP surface

Umbrella `stewardship` (29 commands, `docs/versions/v0.22.188/tools/stewardship.md:14-16`): worklists (`worklist_get`), supervised proposals (`proposal_get`, `proposal_decide`), pattern trust (`trust_status`), notification config, `audit_queue`, disposition **rules** (`rule_enact` / `rule_release` / `rule_suspend` / `rule_resume` / `rule_retire` / `rule_backfill` / `rule_status`), masked golden consumption (`golden_read`, `golden_events`), quality KPIs (`quality_recovered`, `quality_status`, `quality_refresh`), predictive recommendations (`recommendations`, `recommendation_summary`, `recommendation_decide`, `recommendation_efficacy`, `recommendation_refresh`), and dashboard reads (`flow_summary`, `subflow_summary`, `workbench`, `alerts_queue`, `impact_preview`). Golden changes go through one decision writer (`lib/mdm/stewardshipDecisionWriter.ts`) and surface an events envelope `{emitted, visible_after, read_with}` — an empty read before `visible_after` is expected lag, not a lost event (`:20`).

### 5.2 The UI

`app/app/stewardship/page.tsx` mounts `StewardshipDashboard.tsx`. Slots (`app/app/stewardship/slots/`): `HeaderBand`, `TabsRail`, `FilterBar`, `StatTiles`, `StewardCards`, `FlowSankey` (the Sankey flow), `RecoveryTrend`, `AlertList`, `RecordsTable`, `WhyColumnMenu`, `GovernancePopout`, `Chat`, `ReportsPage`, `SubmapHeader`, `PopupHost`. Popups (`popups/`): `Workbench`, `RecordDetailDrawer`, `CompareRecordsTable`, `LineageWorkspace`, `SourcesPopup`, `StagePopup`, `StepDrillPopup`, `StepLensPopup`, `RibbonPopover`. A steward therefore: sees the flow and its bottlenecks (age buckets are ISO 8601 durations from `config/stewardship-aging.config.ts:18,62-86`), opens a queue item, compares candidate records, reads lineage, decides (approve / reject / reopen / assign / escalate / undo — `lib/mdm/stewardshipUndo.ts`), previews blast radius (`lib/mdm/stewardshipImpactPreview.ts`), and enacts disposition rules that dispose of future items automatically. Chat grounding and governance help are config-driven (`config/stewardship-chat-grounding.config.ts`, `config/stewardship-help.config.ts`) and surface-tested (`__tests__/chat-grounding.surface.test.tsx`, `governance-help.surface.test.tsx`).

---

## 6. Master data — `mdm` and `mdm_project`

### 6.1 `mdm`

Published purpose (`docs/versions/v0.22.188/tools/mdm.md:14-17`): golden entities, source links, relationships; a stewardship review queue with survivorship-based golden materialisation; match-config authoring (`config_create` / `config_trial` / `match_explain` / `config_train` / `battery_run` / `config_promote` / `config_demote`) with an **absolute-identifier tier** that routes identifier conflicts to review instead of blind-merging; normalization and enrichment registries; curated reference sets; parent-scoped hierarchies; governance overrides and confirmed goldens; an **open entity-type vocabulary** (`entity_type_list` / `entity_type_get` / `entity_type_create` / `entity_type_retire`) with per-type `matching_mode`. Resolution runs are `resolution_run`; goldens are read via `entity_get` / `golden_explain` and masked via the read boundary (§3.1).

Key configuration:

| Concern | Config | Measured detail |
|---|---|---|
| Blocking + scoring | `config/mdm-resolution.config.ts` | chunked blocking-key buckets with a memory bound (`:39-57`, LIB-BUG-592); cross-parent threshold `min(match_threshold + delta, 1)` (`:69-76`); sentinel-fraction backstop against constant collapse — the case that "mega-merged 286 people into one" (`:412-415`); mistyped safety controls can never fail open as always-blocking (`:195`) |
| Enrichers | `config/mdm-enricher-kinds.config.ts` | stages `pre_match` / `post_golden` / `post_master` (`:40`); kinds `regex`, `builtin`, `reference_lookup`, `sql_lookup`, `rest_lookup`, `classification_lookup` (`:71-138`); DB CHECK ≡ matrix ≡ dispatch enforced by a release gate (`:13`) |
| Consent, forbid-clearing, authz | `config/mdm-consent.config.ts`, `config/mdm-forbid-clearing.config.ts`, `config/mdm-enricher-authz.config.ts` | consent fixture domain seeded by `scripts/mdm/seed-consent-fixture-domain.ts` |
| Stewardship, quality, validation | `config/mdm-stewardship.config.ts`, `mdm-quality.config.ts`, `mdm-validation.config.ts` | — |

**Known limitation — LIB-BUG-1450 (open):** a match configuration with **empty blocking rules succeeds** rather than being refused; recorded as hardening owed in `docs/audit/catalog-elt-3-BOOT-PROMPT.md:53` alongside 1448 (tests excluded from typecheck), 1449 (duplicate detector blind to lowercase) and an identity-mask quality note (`^\d+` regexp on city/state columns). Until it lands, a `config_trial` / `resolution_run` over a config with no blocking keys should be treated by governance reviewers as unbounded pairwise comparison, not as a validated model.

### 6.2 `mdm_project`

Guided onboarding by interview (15 commands, `docs/versions/v0.22.188/tools/mdm_project.md:14-16`): `project_start` → `interview_next` / `interview_answer` / `interview_skip` → `project_status` / `systems_map_get` / `project_brief` → `spec_generate` → `spec_get` / `decision_answer` / `group_accept` / `gate_status` → `model_generate` / `generation_status`; `project_abandon` is terminal. `config/mdm-project.config.ts` holds the interview bank (`INTERVIEW_QUESTION_BANK`, `:119`), system roles `source_of_record` / `contributing_source` / `destination` (`:54`), and an **`entity_types` question** (`:165`). The privacy-relevant question is the exclusion list: a field the business names is dropped from the matching model — no comparison, no blocking key — "so a sensitive field is never used to decide that two records are the same, even internally"; display masking is unaffected because it is governed elsewhere (`:195-203`).

---

## 7. Evidence and fingerprints

- **`evidence`** (4 commands: `show`, `verify`, `list`, `determine`; `docs/versions/v0.22.188/tools/evidence.md:14-16`): content-hashed aggregate proofs behind analytics claims, written at `story.scan`, each pinning its snapshot in the retention index and hashed over **RFC 8785** canonical JSON. `lib/analytics/evidenceLedger.ts`, `lib/analytics/classificationRef.ts` (binds a determination to the classification in force — PC-5). Limits in `config/evidence.config.ts:6-9` (`listDefaultLimit 20`, `listMaxLimit 100`).
- **`fingerprint`** (6 commands: `detect`, `list`, `get`, `validate`, `create`, `delete`; `tools/fingerprint.md:14-16`): source-system fingerprints (Salesforce, SAP, Workday, …) — **95 built-in** plus custom, counted from `BUILT_IN` and exported as `BUILT_IN_COUNT` (`lib/server/sourceFingerprints.ts:65-680`), so the published figure is derived rather than typed. Detection keys mainly on column-name shape (`columnExacts` / `columnPatterns`), but it is **not** value-free: definitions may carry `valuePatterns` with a `minMatchRatio` (`:50`, e.g. the Salesforce 15/18-char id and `stagename` vocabulary at `:74`), and `detect` accepts `sample_values` — so a `detect` call over real rows passes customer values to the matcher. Bounds and tiers are config (`config/fingerprint.config.ts:17`, generic tokens `:129`, command auth tiers `:154`). Governance use: knowing a table is a Salesforce Opportunity export changes which classes are plausible before any value is read.

---

## 8. Governance evidence harvested from providers

`config/governance-control.config.ts` declares, per provider kind, which native governance surfaces are *real evidence*, which are *not a surface of* that kind, and why — so an absent label is never read as "nothing is sensitive". Capability vocabulary `GOVERNANCE_CAPABILITIES` (`:67-86`) includes `sensitivity_label` (a vendor's own taxonomy), `policy_tag`, `security_label`, `dynamic_masking`, `field_encryption`, `column_comment`, `field_security`.

| Provider | Measured posture (`GOVERNANCE_CONTROL`, `:244+`) |
|---|---|
| SQL Server | `sensitivity_label` **real**: `sys.sensitivity_classifications` label, information type and rank, each under its own tag namespace (`:328-332`); DDM **real**: `sys.masked_columns` is a per-column binding, so the masking function token (`partial`, `email`, `default`) is collectable evidence (`:340`) |
| BigQuery | `policy_tag` is the governed taxonomy whose bindings *are* the access control (`:222`); masking is a data policy bound to a policy tag, so no separate per-column DDM surface is read (`:426`) |
| MySQL Community 8.4 | no sensitivity catalog, no catalog-level dynamic masking (`:376`, `:392`) — declared absent, not approximated |
| Snowflake | schema-inventory masking policies are OBJECTS, not per-column bindings, and are declared under `masking_policy` (`:224`) |
| Salesforce | **the field-classification gap**: Salesforce ships `SecurityClassification`, `ComplianceGroup`, `DataOwner`, `DataSensitivityLevel` on `FieldDefinition`, reachable only via the Tooling API or SOQL, both refused by this kind's metadata-only allowlist; "its absence here MUST NOT be read as 'the org classified nothing'" — closing it needs a second transport with its own value-freedom argument and is recorded as owed (`:560`). Write is `unsupported` (Metadata API only, `:568`). Shield Platform Encryption is the one `field_encryption` surface (`:86`, `:227`). |

`row_level_security` is its own declared capability (`:83`), not a synonym for `field_security` or `security_label`: it is **real** on SQL Server (`:346-348`, captured by `lib/data_catalog/harvest/sqlserver.ts`), and on BigQuery (`:428`), Databricks (`:475`) and PostgreSQL (`:522`) it is declared **readable but unread by this release's harvest** — an honest "not looked at", distinct from the `NOT_A_SURFACE_OF` declarations that cover `tag` / `policy_tag` / `security_label` for the SaaS and generic-SQL kinds (`:234`, `:322`). Harvested tags are mapped to kernel classes by `PROVIDER_TAG_CLASS_MAP` rules and enter the kernel as evidence — a proposer, never a direct writer (PC-11). Snowflake `SYSTEM$CLASSIFY` opt-in is an open operator decision, "as proposer only" (`docs/audit/catalog-elt-3-BOOT-PROMPT.md:56`).

---

## 9. Pre-production checklist posture

`docs/audit/pre-production-checklist.md` (31 lines) is the single place to re-raise security-hygiene items the operator ruled to defer until cutover; they "should not be re-raised as standing 'owed' items before then" (`:3-6`). Listed, without urging action:

| # | Item | Id | Recorded step |
|---|---|---|---|
| 1 | Redact 4 plaintext `ai.oauth_token` rows in `registry.app_settings_history` | Decision 18 | under the STANDARD-007 lock, `--plan` then `npm run ops:redact-app-settings-history -- --apply --operator-authorised` (`:12`) |
| 2 | Rotate the AI OAuth token | Action 19 / LIB-BUG-1392 (P1, open) | requires the vault-backing bundle first (`:13`) |
| 3 | Re-store the Snowflake key-pair credential for connection `f45c3042…` | Action 13 | fixture credential for acceptance, not a production rotation (`:14`) |
| 4 | Out-of-schedule `pdl-billing-reconcile` run | LIB-BUG-1369 / Decision 21 | operator-attended, quiet hour (`:15`) |
| 5 | Build the FEAT-027 admin credential vault | FEAT-027 (not built) | `registry.connections.config_encrypted` is the only credential store; the `vault` umbrella is the PII detokenization vault (`:16`) |

Warn-mode gates to flip at cutover (`:20-23`): `audit:consumer-env-currency`; `audit:lineage-emitter-enrolment` (chain-linked from v0.22.188 as `release:gates` link 26; measured 1 of 11 declared writers enrolled, so the platform openly declares `lineage_pending`). Each row is to be re-verified against current state before actioning (`:27-31`).

---

## 10. Quick reference for agents

| Want to… | Use | Constraint |
|---|---|---|
| Know what a column is | catalog field projection / `data_catalog` reads | states per PC-4; `unclassified` ≠ public; withheld samples carry `sample_values_withheld` |
| Change masking on a dataset | `dataset_admin.masking_set` → `masking_reload` | posture is config; never expose a `framework` on a read (PC-7) |
| See plain sample values | pass `acknowledge_plain_samples: true` | recorded on the job (`config/data-catalog.config.ts:1106`) |
| Export with plain statistics | `statistics_policy: "plain"` + `acknowledgePlainStatistics: true` | otherwise `ExportStatisticsAcknowledgementError` |
| Review a proposal | `stewardship.proposal_decide`, `classification.proposal_decide` | proposals never auto-apply (PC-11) |
| Read a golden | `stewardship.golden_read` (masked view) | always masked regardless of posture |
| Propose or override a class | no such verb is on the MCP surface at v0.22.188 — proposals enter the kernel as evidence (PC-11) | PC-10's "exactly ONE reclassification verb", protective-only, is doctrine awaiting its gate (`audit:classification-decision-integrity`, **planned — P3**, `STANDARD-009:321`) |
| Add a privacy rule or threshold | do not — operator ruling required | `STANDARD-009:169-170` |
