# P1-0 — group A decisions, C3, and the group B scope call

**Date:** 2026-08-05
**Decided by:** Miguel Teixeira
**Status:** normative for P1-1a. Supersedes the corresponding text in `docs/p1-0-schema-mapping.md`
and `docs/p1-0-residual-triage.md`.

**Precedence:** `docs/p1-0-corrections.md` (C1–C19) → **this file** → the P1-0 drafts. Where this
file and a draft disagree, this file wins and the draft is stale, not authoritative.

Evidence base: prototype at `9359c67`, re-confirmed at `6146004`. The original group decisions were
shape and governance calls. P1-0c then inspected the pin, and P1-0d used that evidence to supersede
A3's extraction premise with the measured identity resolution recorded below.

---

## Why these were the blockers

P1-1a is the migration train and the validation service, written against a disposable database.
It never needed the missing seed rows (C20) — those are reference data and block P1-1b. What it
needed was the seven group-A items, because each one is a column, key or constraint the train
cannot emit without inventing something. C3 joins them because the enforcement-change rule decides
whether a grandfathering audit table and trigger exist at all.

---

## A1 — the FDCHDR source-fiscal identity index

**Decision: build all three missing components as declared columns, and keep the partial unique
index in the database.**

The index as drafted was
`(Company, Entity, NormalizedDocumentID, FlowType) WHERE SourceKey IS NULL`. Three of its four
components do not exist as stored fields at the pin. Each is resolved as follows.

### `Entity` → `CounterpartyCodeName`

A single stored column, populated at import from `ProviderCodeName` on payables and
`EntityCodeName` on receivables. Marked **derived-at-import** under A2. The two pinned spellings
survive as export fields only; nothing downstream reads them for identity.

### `FlowType` → stored `char(1)`, derived from the source collection

`I` payables · `R` receivables · `P` / `F` / `O` bank-generated, taken from the collection the row
sits in. Derivable, and the drafts never said so — now they do. Stored rather than computed at
query time, because it is part of a unique index and a functional index over a collection the row
no longer belongs to is not reconstructible.

### `NormalizedDocumentID` → stored generated column with a version discriminator

Deterministic normalization of `DocumentID`: **uppercase, then remove every character that is not
`[A-Z0-9]`**. Stored as a PostgreSQL generated column so the database, not the application, owns
the value.

Alongside it, a `DocumentIdNormalizationVersion` column pinned at `v1`, and
**`DocumentIdNormalizationVersion` is part of the unique index**. This mirrors C7's
`HashAlgorithm` discriminator and carries the same rule: **values normalized under different
versions are never compared.** Changing the rule is a migration that recomputes the column and
increments the version — never an in-place redefinition.

**Rejected alternatives, and why.** Dropping the index and validating in the application was
rejected because the identity of the central fiscal table would then be guaranteed by whichever
code path happens to run. Indexing raw `DocumentID` was rejected because the same document
arriving with different spacing or punctuation would create a second fiscal row silently — the
exact failure mode the index exists to prevent.

---

## A2 — source, derived, and production-only columns

**Decision: every column in `p1-0-schema-mapping.md` carries an explicit provenance marker.**

Three values, no fourth, and no unmarked column:

| Marker | Meaning | Importer behaviour |
|---|---|---|
| `source` | Exists in the pinned prototype JSON | Read it |
| `derived` | Computed at import from source fields | Compute it; never read it |
| `production` | New in Sibyla, no source counterpart | Default or leave null; never read it |

Applied as a documentation pass in **P1-0d**, not P1-0c or P1-1a. The known list marked
`production` on the first sweep: `Entity` and `FlowType` on FDCHDR (now `derived`),
`NormalizedDocumentID` (`derived`),
`FailureCategory` on DOCFAI (deleted — see A7), `BCCode` on BNKCHK, `PayDtlCode` / `RcvDtlCode`,
`LineNumber` on FDCDTL, `SuccessorReviewID` on DOCRQE, `ItemClass` on DOCFLG. The five fields that
exist in the data and were absent from the draft — `CompanyCodeName`, `ProviderCodeName`,
`ProviderName` on payables, `CompanyCodeName`, `EntityCodeName` on receivables — are added as
`source`.

---

## A3 — `MatchGroupID` and `BRCode`

**Decision: `MatchGroupID` is run-scoped and explicitly non-permanent, with no CodeLedger bucket;
`BRCode` keeps its CodeLedger bucket and uses
`(Company, BMCode, EntryCode, FindingDiscriminator)` as its permanent natural key. `BRCode`
issuance is implemented in P1-1b, not P1-1a.**

P1-0c established that the prototype generates `MatchGroupID` from a per-run group sequence, so
there is no permanent identity to extract. P1-1a creates the run-scoped column without a
CodeLedger FK or bucket. P1-0d measured the `BRCode` candidates under C13's collision method and
selected the four-part natural key above. P1-1a creates the `BRCode` column and its surviving
bucket; P1-1b owns issuance and full-key validation.

---

## A4 — `RECREV.ProposedMatchType`

**Decision: RECREV stores `ProposedMatchBTCode`, an FK to `BNKMAT(BTCode)`.**

`BTCode` is BNKMAT's key; `MatchType` is a unique attribute, not the identity. The drafted FK named
no target column and could not be created as written. The human-readable match type is available by
join and is not stored a second time on RECREV.

---

## A5 — BNKREC recorded status, governed rule, and source reconciliation text

**Decision: the source `MatchType` field is persisted as `RecordedMatchStatus` and rendered back
to the source `MatchType` column. It accepts the 12 source statuses plus the target-only D6 status.
`GovernedMatchRuleCode` is the nullable FK to `BNKMAT(BTCode)`. `ReconciliationMatchType` remains
source-only free text, with no FK and no consistency constraint.**

C4 established that the source `MatchType` and `ReconciliationMatchType` vocabularies are disjoint.
BNKREC records 12 source `MatchType` strings across 2,072 rows; the target adds the distinct D6
`Matched — ledger reference, no entry` result to that recorded-status domain. BNKMAT governance is
linked separately and only when known through nullable `GovernedMatchRuleCode`; it does not govern
either source text field by matching their displayed strings.

There is no open 12 → 11 mapping project item. `ReconciliationMatchType` remains retained and
readable as source-only free text, without an FK or consistency constraint, and is explicitly not
authoritative.

---

## A6 — `ImportBatch`

**Decision: `Id` (identity) is the primary key, `unique (Id, CompanyCode)` is retained to support
company-scoped composite FKs, and the columns are declared in the mapping rather than in prose.**

Minimum column set: `Id`, `CompanyCode`, `Source`, `StartedAt`, `CompletedAt`, `RowCount`, `Status`,
`CreatedBy`. `CompanyCode` is a controlled `CompanyRegistry` code, so the composite unique is what
lets every import-evidence row prove it belongs to the same company as its batch.

---

## A7 — `DOCFAI.FailureCategory`

**Decision: `FailureCategory` is deleted. `Category` is the field, and it is `source`.**

DOCFAI's pinned fields are `Category, Date, FACode, Filename, Reason` across 3 rows. The draft
declared both columns with no stated relationship; only one exists.

---

## C3 — enforcement-date changes

**Decision: `EnforcementStartsAt` is immutable after import or first use. Correcting a rule means
issuing a new DOCEFL rule version, not editing the date.**

Consequences the migration train must implement:

- `EnforcementStartsAt` is seeded to the **Sibyla go-live timestamp**, never from DOCEFL
  `EffectiveFrom` — that is C3's already-settled half, and seeding from `EffectiveFrom` makes all
  29 open blocking instances fire on day one.
- A trigger **rejects any UPDATE of `EnforcementStartsAt`** once the rule has been imported or has
  any DOCFLG instance referencing it. Named reject, not a silent no-op.
- Persisted `IsGrandfathered` verdicts are **never recomputed**. There is no recompute path, no
  audit table for one, and no rollback path to design.
- Rule correction is a new DOCEFL row with its own EFCode and its own enforcement date. The old
  rule stops emitting; its historical instances and their verdicts stay exactly as recorded.

**Rejected alternative:** transactional recompute with audit. It buys the ability to fix a
mis-entered date in place, at the cost of a mechanism that can silently rewrite past verdicts —
and the whole grandfathering design exists to make past verdicts stable.

---

## Group B — all fifteen go into the same migration train

**Decision: one train, group B included.**

The train is squashable and has never been applied anywhere, so adding the FKs, triggers and
constraints now costs a longer P1-1a and nothing else. A second train after sign-off would mean the
schema ships once without its integrity mechanisms and once with them, and the gap between those
two moments is where production data usually arrives.

Concretely, this pulls into P1-1a: **B5** (`CompanyRegistry.Active` enforced relationally, not only
in application code), **B6** (polymorphic `RelatedRecordType`/`RelatedRecordID` and
`SourceRecordType`/`SourceRecordID` get a real mechanism with company scope — the blocking
predicate needs a database behind it), **B7** (append-only enforced by trigger on CodeLedger,
ManualEntryEvidence, ArchiveNameLedger, DOCFLG terminal rows and human observations — asserted five
times, implemented zero), **B8** (`DOCLOG.DocumentType`/`OriginClass` partial FK to DOCTYP, scoped
per C9), **B9** (`SNCACC` and `COCACC` FKs), **B10** (sequence bootstrap and EntryCode sequence
registry **tables** — seeding them stays P1-1b), **B11** (`ArchiveNameLedger` gets the directory
column its uniqueness rule requires).

### B1 — purge scope, the one that can destroy evidence

> **SUPERSEDED 2026-08-05 — do not implement what this section describes.** The reference-counted
> default below was **not confirmed; it was rejected.** B1 was closed instead by specifying the
> storage model that the lifecycle draft never stated: **byte storage is per capture, bytes are
> never shared between captures, and `RetainedContentHash` is a duplicate-detection index and never
> a storage pointer.** Reference counting is therefore not implemented — the sharing that would
> require it is removed. See **`docs/p1-0-b1-decision.md`** and `PROJECT-STATE.md` §2.2. The text
> below is retained as the record of what was proposed and why it was not taken.

Not a schema call, so it is recorded here as a **default requiring confirmation** rather than as a
decision taken: **purge deletes bytes only when no other capture references the canonical
`(company, hash, length)` record; otherwise it unlinks the capture and leaves the bytes.** Byte
deletion becomes reference-counted. Everything else on the current design is retained, including
the unchanged global Document hash index, which stays exactly as the P1-1 prompt says it stays.

Confirm or correct this before P1-1a implements the tombstone path. It is the only item on either
list that can silently destroy data someone still needs.

### B2, B3, B4 and B12 — closed by P1-0d

- **B2:** Discarding a `Posted` or `ReferenceOnly` record requires a request by one authenticated
  actor and a counter-signature by a different actor holding `DispositionCounterSign`; the
  compensating event preserves all prior fiscal, archive, export, integration and audit history.
- **B3:** `RestoreForReview` has exactly one target: disposition `NULL` plus
  `DocumentStatus.AwaitingReview`.
- **B4:** fresh capture enters disposition `NULL` plus `DocumentStatus.Registered`; malformed
  response enters disposition `NULL` plus `DocumentStatus.RetryScheduled` while retryable, or
  `DocumentStatus.NeedsAttention` when retry is not scheduled or has been exhausted; schema-valid
  `NOT_A_DOCUMENT` enters disposition `NULL` plus `DocumentStatus.AwaitingReview`. These are
  existing `DocumentStatus` values, not new generic statuses.
- **B12:** `CounterpartyCodeName` is the canonical token for every archive path and controlled
  filename, mapped from `FDCHDR.CounterpartyCodeName` or `DOCARC.CounterpartyCodeName` as
  applicable.

---

## Sequencing

**P1-0c is superseded; P1-0d and P1-0e are complete; P1-1a is active.**

P1-0c stopped at its mandatory source gate and must not be rerun. P1-0d reused its accurate
findings, transcribed the pinned rows, authored the rows created by the governing decisions with
per-field provenance, applied the A2 markers, measured A3, and folded the closed B2/B3/B4/B12
governance into the drafts. P1-0e completed the nullability and repository-hygiene closure.

P1-1a now writes the migration train and validation service against these closed decisions on a
disposable database. It does not touch a shared database; P1-0 acceptance authorizes no shared or
persistent migration, live-data import, or merge to `main`.
