# Sibyla — Master Data Consolidation and UI Plan

**Status:** **approved** (Miguel, 2026-08-11) · rev. 19, 2026-08-26 — **M-D13 and M-D14 added**:
`DOCUMENT` joins the DOCFLG `RelatedRecordType` domain on `EF0000066`'s authored `AppliesToTable`
rather than on observed instances, which `M-D12`'s test cannot reach for a subject that is
Sibyla's own intake record; and which ITMCLS pairs may serve as a related-party kind becomes
governed data in a catalogue the engine enforces, **superseding `M-D11`'s reversal clause** now
that a third kind has appeared · rev. 18, 2026-08-22 — **M-D9…M-D12 added**, closing
the four Stage 11 follow-ups `S2-6` carried as open decisions: the ITMCLS re-key (b), `Relevancy`
persistence (c), RelatedParty/Kind pair resolution (d) and `ITMMST` in the DOCFLG
`RelatedRecordType` domain (e). **Decisions only — no code, no migration, no schema change ships
with this revision**, and M-D9 additionally needs Miguel's explicit yes before it is implemented at
all. Earlier: rev. 17, 2026-08-21 — §5's `S1-9` table brought
current: **S1 closed 2026-08-20** on the demonstrated "CTT" exit, and the `c` remainder, `e`, `f`,
`i`, the `h` read catalogue and `g1`/`g2` all shipped; what remains of `S1-9` is `g3` (on **O-1**)
and the DOCEFL editor (deferred to S2, **O-4**). Earlier: rev. 16 — **M-D8** added with the Stage
11 source alignment (`!135`/`!137`): a source retirement is evidence, not a target mutation, and an
identity pin binds to a natural key, not to a code. rev. 15 — S1.5 complete; S1-9 `a`/`b`
and **all six `c` descriptor screens** merged — the three code lists, then EXCRAT (`!100`, with
the missing-rates panel), ITMALS (`!101`) and ENTALS (`!102`) on 2026-08-20; `S1-6a` closed and
the DOCTYP screen rewritten
read-only; seven `S1-9d` slices merged — the **read-only Identity** shell (`!74`), governed
**Identity/ENTMST writes** (`!79`), governed **Tax identifiers** (`!82`), governed **Roles**
(`!84`), **read-only Aliases** (`!86`), governed **Aliases/ENTALS writes** (`!89`) and governed
**Banking** (`!91`).
**All nine Entity 360 tabs are built** — the read surfaces Documents (`!96`), Balances (`!97`)
and Audit (`!98`) completed them on 2026-08-20, and the demonstrated **"CTT" exit closed `S1-9d`
and S1 on 2026-08-20**, validated by Miguel with screenshots; §3.1, §3.3
and §5 brought current
**Scope:** database structure first, then the human interface over it
**Companion to:** `docs/V6.md` (plan of record) and `docs/project-todo-v6.md` (backlog)
**Slots into:** a new **S1.5 — Party collapse**, between S1 and S2

> **Approved 2026-08-11.** M-D1…M-D7 are decisions of record and `S1.5` is authorised.
>
> **One premise changed between drafting and approval — §2.3a.** M-D1 rested on the
> `P11aSchemaSql` ENTMST being *empty with no writer*. It was, until `S1-2` and `S1-3/S1-4` merged
> the same day and seeded it. The collapse is still the cheaper direction by a wide margin; the
> safety argument changed from *emptiness* to *reproducibility*, and the phase added one item and
> one stop-list entry. Nothing in §0 is reopened.

---

## 0. The fourteen decisions this plan rests on

| # | Decision | Consequence | Reverses if |
|---|---|---|---|
| **M-D1** | **Collapse upward into one table.** The *physical* `BusinessEntity` table survived and was **renamed to ENTMST**, gaining the FDR business columns. The seeded `P11aSchemaSql` ENTMST was dropped and its child FKs re-established against the renamed table. One party table — counterparties and internal companies alike | **Zero foreign-key DDL on the 26 July-spine references.** PostgreSQL carried inbound FK constraints through `ALTER TABLE … RENAME TO`, so all 26 columns across 21 tables followed the rename untouched. At decision time the P11a ENTMST was empty and had no writer; §2.3a records why that premise changed and how reproducible seeding replaced it as the safety argument | The P11a DDL rewrite turns out to disturb more of the 61-test schema suite than the 26-FK retarget would have |
| **M-D2** | **`Id uuid` is the only key anything joins on.** Business codes move off the master entirely — see M-D5 — so the renamed table needs no `CodeName` unique key, and ENTALS, ENTITM, ENTBNK, ENTBNKMethod and ENTBNKAccount FK on `ENTMST.Id` | One key style instead of two. The July graph and the FDR children now join the same way, and `p1-0-schema-mapping.md` §1 is satisfied exactly as written — *"uuid surrogate keys for internal relational joins"*, business codes as export identity and nothing else | Never — two internal key styles is the condition this whole exercise exists to remove |
| **M-D3** | **Internal companies are ENTMST rows.** The existing `EntityRoleAssignments` child is authoritative, using `EntityRole.InternalCompany`; scalar ENTMST `Role='Company'` is denormalised for display and export. `CompanyRegistry` keeps its shipped job and remains the FK target of every FDR `Company` / `CompanyCode` column. Party reads use merged ENTMST | This compatibility realization gives intercompany parties **one** row rather than two without adding a physical role table. The explicit counterparty-not-own-company enforcement has not shipped; it remains `S2-7` | An internal company is found to need attributes that make no sense on a counterparty |
| **M-D4** | **One generic reference grid, plus seven hand-built screens**, and **every master-data write goes through one application service per table** — grid, review-queue learning path (S3-4), or seeder | Thirteen tables maintainable for roughly the effort of six bespoke screens; and a correction made in the review screen behaves identically to one made in the grid. That is what stops the learning path becoming a second, weaker write path | The metadata descriptor accumulates per-table special cases — at three exceptions, hand-build that screen |
| **M-D5** | **Business codes live in `ExternalIdentity (EntityId, Scheme, Code)`, not in columns on ENTMST.** `fdr.entity_code`, `fdr.code_name`, and later `cegid.party` / `moloni.customer` are all schemes in one mapping table | A code becomes a fact about an *integration*, not about the entity. Cegid and Moloni are allowed to disagree, which they eventually will. And **the CodeName rename cascade disappears** — the operation that needed `rename_codename.py` and `validate_codename_immutability.py` in the FDR prototype becomes a single-row update | An ERP is found that cannot accept a code Sibyla did not mint — which would make the code an attribute of the entity after all |
| **M-D6** | **EF migrations are the single schema authority.** The raw `P11aSchemaSql` DDL was retired in `S1.5-4`, not rewritten | The collapse already required that DDL to be reshaped, so folding it into migrations cost the difference rather than the whole. The 61-test `P11aDatabase` suite now exercises migrations rather than a second schema authority | Raw DDL is found to express a constraint EF cannot; then that one object stays as a migration-embedded SQL block, not a second authority |
| **M-D7** | **Rate-like data is effective-dated; master identities are not.** `EXCRAT` was built effective-dated in `S1-5`; other rate-like tables remain deferred. `ENTMST` and the code lists remain current-state with an audit log | The asymmetry is now established in the schema: rates retain their validity periods, while entity and code-list history is reconstructed from audit rather than bitemporal rows | A statutory requirement to reproduce an entity's state on a past date appears — then ENTMST joins the effective-dated set |
| **M-D8** | **A source retirement is evidence, not a target mutation — and an identity pin binds to a natural key, not to a code.** *(taken 2026-08-21 with the Stage 11 source alignment, `!135`/`!137`)* A row the source tombstones (`Status: Retired`) keeps its code reserved, is validated like a live row, is never seeded, and may not be referenced by any child file; a row an earlier seed already placed under that code is **left to the target** — `ITMMST.IsActive` is target-owned, the seeder neither de-activates nor re-activates, it reports the divergence as `Skipped`. Symmetrically, the two governed related-party kinds are pinned by their **(Class, Subclass) pair**, not by the ITMCLS code that happens to carry it | The seeder can take a source that retires rows without the target having to mutate anything, and a renumbering of the codes underneath an identity becomes a **failing test rather than a silent re-binding**: Stage 11 re-issued the ITMCLS codes, and a code-only pin would have re-bound `ENTMST.RelatedPartyKind` to different classes without a word. `!137` put the codes and their bound pairs in one Domain type that the CHECK, the Entity 360 contract, `BusinessEntityService`, the UI labels and the page-contract test all derive from | The source gains a retirement that *must* propagate as a target state change (a legal deletion, say) — then retirement stops being evidence and needs its own governed command, not a seeder side effect |
| **M-D9** | **The ITMCLS re-key exists only for a database that cannot be rebuilt.** *(taken 2026-08-22)* Stage 11 re-issued the ITMCLS codes, so a database still on the Aug-6 numbering cannot take the Stage 11 taxonomy — the alternate-key preflight refuses the upsert and the `!137` re-bind migration refuses the taxonomy, both by design. The remedy is a **one-time re-key by the exact `(Class, Subclass)` alternate key**, in **one governed, audited transaction**, carrying **all child references** and remapping `ENTMST.RelatedPartyKind` with them; a **strict no-op** against a Stage 11 database; and the hole at **`CL000046` is never reused**. It is a **destructive migration and therefore stop-list work: Miguel's explicit yes is required before it is implemented or merged**, not merely before it is run | A rebuildable database rebuilds — the demo already did, on 2026-08-21 — so only a genuinely non-rebuildable database pays for a re-key, and it pays once, under audit, keyed by the natural pair rather than by the code that moved. Nothing happens silently: both refusals stay | **No non-rebuildable affected database turns out to exist** — then the migration is never built at all, and this decision closes having cost nothing |
| **M-D10** | **`Relevancy` is persisted as the source fact, with no runtime meaning attached.** *(taken 2026-08-22)* Stage 11 introduced `DOCEFL.Relevancy` (`Consultive` 40 · `Important` 24, 64 of 64 rules non-blank) and `!135` governs it in the parser but persists nothing. It becomes a **nullable `varchar(16)` on DOCEFL** with an exact `CHECK` admitting only those two literals, written **verbatim from the source and never derived from `FlagCategory`**. **No runtime enforcement semantics yet** — it is recorded, not obeyed | The source's own judgement is retained instead of being discarded at the boundary, without inventing behaviour the source never defined. Nullable because the column is a fact about a rule, not a requirement on one | **The next pin adds a third literal** — then it is promoted to a governed catalogue with an FK rather than a widened `CHECK`; **or the source drops the member** — then the column stays nullable and keeps its history rather than being dropped |
| **M-D11** | **RelatedParty and its Kind resolve by the exact `(Class, Subclass)` pair, and the source's `RelatedParty` truth is carried.** *(taken 2026-08-22)* The source delivers `Class`/`Subclass` **text**; ENTMST keys the kind to ITMCLS. Resolution goes through ITMCLS / `RelatedPartyKinds` **by the exact pair — never by the source code and never by display prose**. `RelatedParty` carries the source's value rather than a hardcoded `false`. An **unbound or ambiguous pair fails closed**, and **human-owned target corrections are preserved** rather than overwritten | This is M-D8's pair-pinning extended from the two governed kinds to the resolution path that feeds them, so the next renumbering is a failing test rather than a silent re-binding — and a hardcoded `false` can no longer quietly contradict a source that says otherwise | **A third governed kind appears** — then it needs a governed catalogue with an FK, not a longer hardcoded pair list |
| **M-D12** | **`ITMMST` joins the DOCFLG `RelatedRecordType` domain at `S2-11` — and only `ITMMST`.** *(taken 2026-08-22)* **The premise that the item-master rules have no DOCFLG subject is wrong and is corrected here.** Measured at Stage 11: DOCFLG **already** carries `RelatedRecordType = ITMMST` on **40** instances — `EF0000057` (16) + `EF0000058` (2) + `EF0000059` (15) + `EF0000060` (7) = exactly 40. The rest of `EF0000053`–`EF0000064` emit subjects that already exist: `EF0000054` → `BNKMOV`; `EF0000055`, `EF0000056`, `EF0000063`, `EF0000064` → `ENTMST`; `EF0000061`, `EF0000062` → `DOCLOG`; `EF0000053` has **no instances at all**. So: add **exactly `ITMMST`** to the domain and **pin the detector's subject mapping by `EFCode`**. Do **not** add `ITMCLS` or `FDCHDR` — no instance carries either, and `EF0000058`'s `ITMMST; ITMCLS` target is a *rule scope*, not a flag subject | The domain widens by what the data actually contains rather than by what the rule catalogue's `AppliesToTable` column suggests, which is the difference between a `CHECK` that admits real rows and one that admits speculation. Also corrects the standing note that "no DOCFLG `RelatedRecordType` exists for ITMMST yet" — true of `9359c67`, false of Stage 11 | **A later pin emits a new primary `RelatedRecordType`** — then that literal gets the same treatment: observed instances first, domain second |
| **M-D13** | **`DOCUMENT` joins the DOCFLG `RelatedRecordType` domain on the authored rule, not on observed instances — a knowing, bounded departure from `M-D12`.** *(taken 2026-08-26)* `EF0000066`, authored in the source at pin `d551734` for the `S2-5` cross-capture resubmission, declares `AppliesToTable = DOCUMENT`. `M-D12`'s test — observed instances first, domain second — **cannot be applied to it**: Sibyla holds **zero** DOCFLG instances of any kind, and the prototype emits none for this rule either, because the subject is Sibyla's own intake record and the prototype has no such lifecycle. Waiting for an observed instance is waiting for something that can only appear *after* the domain admits it. The departure is bounded three ways: the subject is pinned to `EF0000066` **in both directions**, so `DOCUMENT` admits exactly one rule and that rule admits exactly one subject; `CompanyRecordReference` is widened with it, without which the subject is un-insertable and the widening is decoration; and `ITMCLS` and `FDCHDR` **remain excluded** on `M-D12`'s original reasoning | `M-D12` guards against a domain that admits speculation. A rule the source has authored, with its subject pinned both ways and its parent reference widened to match, is not speculation — but it is a different justification, and a different justification is a new decision rather than a silent reuse of an old one | **An `AppliesToTable` value arrives that names no Sibyla record type**, or a second rule needs `DOCUMENT` — the first means the domain is tracking the source's vocabulary rather than Sibyla's records and should stop; the second dissolves the both-ways pin and returns the question to `M-D12`'s test |
| **M-D14** | **Which ITMCLS pairs may serve as a related-party kind is governed data the engine enforces, not a literal list compiled into the assembly.** *(taken 2026-08-26)* `CK_ENTMST_RelatedPartyKind_FundingOnly` was generated from a constant naming exactly two codes, so admitting a third — `Shareholders / Transfers`, which the source issued as `CL000054` and which `EC000121` cites — meant editing an assembly and deploying it. The codes were never the point: they are ITMCLS rows. `ENTMST.RelatedPartyKind` now references a **`RelatedPartyKindCatalogue`**, and the catalogue references ITMCLS by **one composite key** on `(CLCode, Class, Subclass)` — two separate keys would each be satisfied while together describing different rows, letting a catalogue row claim `CL000008` means `Revenue / Intercompany`. `ON UPDATE CASCADE` carries a renumbering ITMCLS → catalogue → ENTMST; `ON DELETE NO ACTION` refuses to let a cited pair leave. P11a's `FK_ENTMST_RelatedPartyKind` is **replaced, not kept**: carrying no `ON UPDATE` clause it blocked exactly the renumbering this design exists to survive — measured, not reasoned — and it is redundant once membership is transitive. `RelatedPartyKinds` survives as the bootstrap for an empty database. **Not done, and not claimed:** the page option lists are still fixed markup, so a kind admitted by INSERT is accepted, resolved and rendered but is not offered in the dropdown without a release | This **is** `M-D11`'s reversal clause, honoured rather than worked around: *"if a third governed kind appears — then it needs a governed catalogue with an FK, not a longer hardcoded pair list"*. `M-D11`'s substance survives intact — the pair is the identity and the code is a handle — but it is now the database that holds that invariant instead of a constant a renumbering could silently outlive | **Eligibility needs to differ per company** (the catalogue gains a scope column and the key grows) · **the UI option lists are read from the catalogue**, at which point the bootstrap list is the only compiled remnant and `RelatedPartyKinds` can go |
---

## 1. What is actually duplicated

The premise of the question was that thirteen tables are repeated. **Three are.** The rest are new
capability the July spine never had — and separately, seven July tables look like duplicates but
are not, and deleting them would remove working behaviour.

### 1.1 The count

| Category | Count | Tables |
|---|---|---|
| **Repeated — replace, but merge missing fields in first** | 3 | ENTMST, ITMMST, DOCTYP |
| **New — no July-spine counterpart at all** | 10 | ENTALS, ENTBNK, ITMALS, ENTITM, ITMCLS, MNGACC, SNCACC, COCACC, EXCRAT, DOCEFL |
| **July tables retired** | 3 | `BusinessEntity`, `DocumentTypeSetting`, `IntegrationProductPlKeyMapping` |
| **July tables that look duplicated but are not — keep and repoint** | 7 | `EntityTaxIdentifier`, `EntityRoleAssignment`, `BusinessEntityExtractionProfile`, `IntegrationProductBinding`, `IntegrationPartyBinding`, `BankAccount`, `CashAccount` |

### 1.2 The matrix

| FDR table | July counterpart | Verdict | What has to happen |
|---|---|---|---|
| **ENTMST** | `BusinessEntity` | **Merge upward — one table** | The two became one physically: `BusinessEntity` was renamed to ENTMST and gained the FDR business columns; the seeded P11a ENTMST was dropped. It also gained five columns the FDR spec never had — *inactive*, *origin*, *merged into*, a *searchable normalised name*, *blocked from new captures* — §2.2 |
| **ITMMST** | `IntegrationProductPlKeyMapping` | **Replace + merge 2 fields** | `ProductCode → PlKey` is the same relationship ITMMST states as `ItemCode → PLMKEY`, done per-provider and per-company. ITMMST subsumes it. Add `NormalizedItemDesc`, `IsActive` |
| **DOCTYP** | `DocumentTypeSetting` + the `FinancialDocumentType` enum | **Replace + merge 4 fields** | `DocumentTypeSetting` is presentation only — a PT/EN label and sort order on a hard-coded enum. DOCTYP is behavioural: `Treatment`, `OriginClass`, `DocClass`, `ProcessingRoute`. It absorbs `LabelPt`, `LabelEn`, `SortOrder`, `IsActive` — the FDR spec has no localisation and the UI is bilingual |
| **ENTALS** | — | **New** | Alias resolution today is an implicit `NormalizedLegalName` match. There is no alias table |
| **ENTBNK** (+ `ENTBNKAccount`, `ENTBNKMethod`) | — | **New** | See §1.3 — this is *not* a duplicate of `BankAccount` |
| **ITMALS** | — | **New** | The nearest thing today is free text in `DocumentLine.Description` |
| **ENTITM** | — | **New** | The per-entity item classification that makes step 7 (learning) possible. Nothing equivalent exists |
| **ITMCLS** | — | **New** | |
| **MNGACC** | — | **New** | |
| **SNCACC** | — | **New** | **The July spine has no chart of accounts at all.** `DocumentLine.Category` is an unconstrained string |
| **COCACC** | — | **New** | One placeholder row today. Real cost centres arrive through the UI, not the seeder |
| **EXCRAT** | — | **New** | No rate table exists; `ExtractionRevision.Currency` records a currency and nothing converts it |
| **DOCEFL** | — | **New** | `ValidationGatesJson` / `WarningsJson` / `AttentionReason` are the ad-hoc equivalent — hard-coded, not a catalogue, not editable without a deploy |

### 1.3 The four "duplicates" that are not duplicates

Getting these wrong would delete working capability, so each is stated explicitly.

**`ENTBNK` is not `BankAccount`.** `BankAccount` and `CashAccount` hold **our own companies'**
accounts — `CompanyEntityId`, `Iban`, `CegidBankAccountId` — and are the *source* side of
`PurchasePaymentInstruction` and `CegidCompanyConnection.PurchasePaymentBankAccountId`. `ENTBNK`
holds **counterparty payment routing**: how we pay this supplier, scoped by company. Opposite
sides of the transaction; both read at step 8.

> The real duplicate of `BankAccount` is **`BNKACC`** (FDR Layer 6), and Layer 6 is deferred to
> Phase 2 by V6-D4. **Do not touch `BankAccount` in Phase 1.** Record the merge as a Phase 2 item
> arriving with bank reconciliation.

**`EntityTaxIdentifier` is not `ENTMST.FiscalNo`.** ENTMST carries one `FiscalNo` plus a country.
`EntityTaxIdentifier` is 1:N with `IdentifierType`, `IsPrimary`, `NormalizedValue` and
`OriginalValue` — a VAT number *and* a national ID *and* an EORI for one entity. Capability ENTMST
lacks. **Keep it, repoint to ENTMST, and treat `ENTMST.FiscalNo` as the denormalised primary**
maintained by the same service that writes the children.

**`EntityRoleAssignment` is not `ENTMST.Role`.** `ENTMST.Role` is a scalar `varchar(64)`. An entity
is routinely a supplier *and* a customer, and under M-D3 an internal company is an entity with
`EntityRole.InternalCompany`. A scalar cannot express that. **The existing
`EntityRoleAssignments` child remains authoritative; ENTMST `Role='Company'` remains a
denormalised primary role for display and export parity**, written by the same service. The draft's
proposed physical `ENTROL` name was not implemented.

**`IntegrationProductBinding` and `IntegrationPartyBinding` are not aliases.** They cache the
*provider's* external identifier, keyed by `(Company, ConnectionKey, Provider)`. An alias is a name
a document uses. Different lifetimes, different owners. **Keep both, repoint to `ITMMST.ItemCode`
and `ENTMST.Id`.** One cleanup: drop `IntegrationPartyBinding.TaxIdentifier`, a stored copy of what
ENTMST already holds.

---

## 2. Target database structure

### 2.1 Why the flow decides this, not the row counts

The brief asked for the merge to be judged against the intake steps rather than the data. Walking
the nine steps of `V6.md` §3 is what proves each table is load-bearing:

| # | Step | Master tables **read** | Master tables **written** |
|---|---|---|---|
| 1 | Document arrives | `CompanyRegistry` (which tenant), `ENTALS` (sender → entity, for channel attribution) | — |
| 2 | It is recognised | **`DOCTYP`** (`Treatment`, `OriginClass`, `ProcessingRoute`), merged **`ENTMST`** plus `EntityRoleAssignments` (whose) | — |
| 3 | It is read | `BusinessEntityExtractionProfile` (per-entity hint, if used) | — |
| 4 | It is classified | **`ENTMST`** → **`ENTALS`** → normalised name · `EntityRoleAssignments` for internal-company role evidence · **`ITMALS`** → **`ITMMST`** → **`ENTITM`** → **`ITMCLS`** → **`MNGACC`** → **`SNCACC`** → **`COCACC`** · **`EXCRAT`** when not EUR | `ENTMST` (provisional entity `[ISO2]-SYN-######`) |
| 5 | It is persisted | `CodeLedger` (EntryCode, LGCode) | DOCLOG · FDCHDR · FDCDTL — through the governed `CommitDocumentRegistry` (S2 slice 2): DOCLOG only on a stable disposition, EntryCode before LG code, one transaction |
| 6 | Uncertainty is surfaced | **`DOCEFL`** (rule catalogue) | DOCFLG instances |
| 7 | The human's answer is learned | — | **`ENTALS`** (new alias) · **`ENTITM`** (new mapping) · **`ITMALS`** · **`ENTMST`** (correction) |
| 8 | It is pushed to Cegid | `ENTMST.FiscalNo` · **`ENTBNK`** (payment routing) · `IntegrationPartyBinding` (external id) · **`MNGACC`** / **`SNCACC`** (account codes) · `BankAccount` (our paying account) | `IntegrationPartyBinding` (newly discovered id) |
| 9 | It is filed | `ENTMST_Export.CodeName` via `ExternalIdentity` (archive path `{Company}/Legal/{Subtype}/{CounterpartyCodeName}/`) | — |

**Every one of the thirteen tables is read at step 4 or step 8, and four are written at step 7.**
That last row is the argument for the maintenance UI: step 7 is the difference between a system
that learns and a tool people abandon, and it writes to exactly the tables this plan puts screens on.

The July equivalents fail the same walk. `DocumentTypeSetting` is read at **no** step — it only
labels. `IntegrationProductPlKeyMapping` is read at step 8 only, for one provider. `BusinessEntity`
is read at steps 1, 2, 4 and 8 — which is why it is the one that merges rather than simply being
replaced.

### 2.2 ENTMST — what it absorbs

| New ENTMST column | Type | From | Why it is needed |
|---|---|---|---|
| `Id` | `uuid PK` | already there — the table is `BusinessEntity` renamed | M-D2. Stays the primary key, so all 26 inbound FKs remain valid through the rename **and** the five FDR children now FK here too. The only key anything joins on |
| `Status` | `varchar(24)` | `BusinessEntity.Status` | ENTMST has `IsProvisional bool` only — it cannot express **Inactive**. Every FK is `RESTRICT`, so an entity with posted documents can never be deleted; retiring is the only available operation and there is no column for it. Domain: `Provisional` · `Active` · `Inactive` |
| `Origin` | `varchar(24)` | `BusinessEntity.Origin` | `Extracted` · `Manual` · `Seeded` · `Integration`. Answers "did a human assert this, or did an agent guess it?" — the first question asked of a suspicious counterparty |
| `MergedIntoId` | `uuid NULL FK ENTMST.Id` | `BusinessEntity.MergedIntoEntityId` — renamed in place | Duplicate-identity merge is an existing, used operator function (`Suppliers.razor` has a merge panel; Diego Berlitz is an open CONFLICT). Self-referencing, `RESTRICT`; a merged row is never deleted |
| `NormalizedEntityName` | `text` | `BusinessEntity.NormalizedLegalName` | S2-6 resolves NIF → alias → **name**. The name leg needs a deterministic normalised key. `CodeName` is a permanent human code, not a search key |
| `IsActiveForCapture` | `bool NOT NULL DEFAULT true` | — | Distinct from `Status`: an entity may be `Active` for history while blocked from *new* captures. Cheap now, expensive once documents reference it |
| `LegacyCode` | `varchar(64) NULL UNIQUE` | `BusinessEntity.Code`, renamed | Transitional. Preserves the July identifier so nothing quoting it breaks. Dropped in Phase 2 once nothing reads it |

`BusinessEntity.LegalName` is renamed to `EntityName`; `CountryCode` folds into the `Country`
tri-state. Because the table is renamed rather than copied, these are `ALTER … RENAME COLUMN`
operations, not data moves.

Two columns do **not** arrive on the table at all. Under M-D5, `EntityCode` and `CodeName` become
rows in `ExternalIdentity` — §2.2a.

### 2.2a Business codes leave the master table (M-D5)

```
ExternalIdentity
  Id         uuid PK
  EntityId   uuid NOT NULL FK ENTMST(Id) RESTRICT
  Scheme     varchar(32) NOT NULL   -- fdr.entity_code · fdr.code_name · cegid.party · moloni.customer
  Code       varchar(64) NOT NULL
  IsPrimary  bool NOT NULL DEFAULT false
  IssuedAt   timestamptz NOT NULL
  UNIQUE (EntityId, Scheme)   -- one code per entity per scheme
  UNIQUE (Scheme, Code)       -- codes unique inside their own scheme, not globally
```

**Why this is the one item that cannot wait.** `S1.5-2` is the moment codes are minted. A minted
code on a master column is permanent and load-bearing from the instant it exists; the same code as
a mapping row is a fact that can be superseded. The window is one migration wide.

**Three things it buys:**

- **The rename cascade disappears.** `CodeName` is currently the FK target of five tables, which is
  why the FDR prototype needed `rename_codename.py` *and*
  `validate_codename_immutability.py` — a rename was a five-table cascade with an invariant checker
  guarding it. With the children FK'd on `ENTMST.Id`, renaming is one `UPDATE` of one row. Both
  scripts stop being needed. That is prototype evidence, not prediction.
- **Cegid and Moloni may disagree.** Two ERPs, two code spaces, one entity. On a single column they
  fight; as two schemes they coexist. `IntegrationPartyBinding` is the same shape and is a candidate
  to fold in — deferred, see the Phase 2 plan `P2-1`.
- **Codes stop being an identity claim.** An entity is the `uuid`. A code is how one external system
  refers to it. That is what `p1-0-schema-mapping.md` §1 already says.

**Two costs, stated plainly.** Every export path gains a join — mitigated by an `ENTMST_Export`
view that renders the flat FDR shape, so the exporter's SQL does not change. And the archive path
at step 9 (`{Company}/Legal/{Subtype}/{CounterpartyCodeName}/`) resolves through that view rather
than a column read.

**What does not change:** `EntityCode` is still issued once by the `CodeLedger` and never
reassigned; both codes are still read-only in the UI; a rename is still a governed command with an
audit event — it is merely no longer a cascade.

### 2.3 Why collapsing *upward* costs no foreign-key DDL at all (M-D1)

There are two directions to collapse in, and they are not symmetric.

**Downward** — create ENTMST properly and retarget every FK onto it — means 26 constraint changes
across 21 tables, plus the two composite FKs. Survivable, but it is 26 chances to get a live
reference wrong.

**Upward** — keep the physical table the FKs already point at, and rename it — costs **zero FK
DDL**. PostgreSQL carries inbound foreign-key constraints through
`ALTER TABLE "BusinessEntity" RENAME TO "ENTMST"`; the constraints follow the table by OID, not by
name. So:

- all 26 columns keep their type, their values, their indexes **and their constraints**;
- `Document (Id, BuyerEntityId)` — which `FDCHDR` composite-FKs against per
  `p1-0-schema-mapping.md` §8 — is untouched;
- `CompanyRegistry (CompanyId, CompanyCode)` is untouched;
- `Membership`, `Department`, both ERP connections and all six `IntegrationSales*` tables are
  untouched.

**The original cost premise moved to the FDR side, then changed before implementation.** When this
decision was drafted, the `P11aSchemaSql` ENTMST was **empty and had no writer**; its only reader
was the unregistered, test-only `RegistryValidationService`. The Layer 2 seeder then populated it
on 2026-08-11. Phase B consequently dropped reproducible seeded data, as §2.3a records, and the
collapse completed through cumulative MR `!59` without retargeting the 26 live constraints.

**What that side actually cost:**

| Work | Note |
|---|---|
| Drop the P11a ENTMST | Originally assessed as empty; populated before Phase B, then dropped under the demonstrated seeder-recovery path |
| Add the FDR business columns to the renamed table | `FiscalNo`, `Country` tri-state, `CurrencyCode`, `Role`, `RelatedParty`(+`Kind`), `DirectDebit`, `InvoiceFrequency`, `ExpectedInvVal`, balances, `IsProvisional`, `TaxIdVerificationStatus`, `FlagReviewNotes` |
| Re-establish five child FKs — now on `ENTMST.Id` | ENTALS · ENTITM · ENTBNK · ENTBNKMethod · ENTBNKAccount. Same count as the FDR DDL had, one key style instead of two |
| Create `ExternalIdentity` and backfill both code schemes | M-D5 · §2.2a |
| Re-point the CHECK constraints | `CK_ENTMST_Country_State` and the named `Company` non-blank checks |
| **Fold the DDL into EF migrations and retire `P11aSchemaSql`** | M-D6. The DDL is being rewritten regardless; this is the difference in cost, not the whole |
| Rewrite the `P11aDatabase` suite | 61 tests, against migrations rather than raw DDL — done once, not twice |

**The one casualty identified at decision time.** Branch `s1/layer2-source-semantics` — the Layer
2 seeder and migration `20260811120000_S1Layer2CoreSourceSemantics` — was then locally green but
unmerged and targeted the ENTMST this decision replaced. That made the rebase cheaper before its
merge; the work was subsequently integrated, and the post-collapse seeder realization completed
in cumulative MR `!59`.


### 2.3a What changed on 2026-08-11, and what it cost

M-D1's cost argument was that the P11a ENTMST is *empty and has no writer* — `STATE.md` said so
explicitly. **That stopped being true the day this plan was written.** `!43` (`6b42d79`) and
`7a93a59` merged the Layer 2 reference seeder, which writes ENTMST, ENTALS, ENTBNK and ENTITM.

| Table | Rows now seeded |
|---|---|
| ENTMST · ENTALS · ENTBNK | 119 · 7 · 43 |
| ENTITM (FK'd on `CodeName`) | 327 |

**What this does not change.** The 26 inbound foreign keys on `BusinessEntity` still do not move.
Collapsing upward is still far cheaper than the downward alternative, and every argument in §2.3
about the rename carrying constraints by OID stands untouched.

**What it changed — three things, stated plainly:**

1. **Phase B became a destructive migration against populated data.** It dropped seeded reference
   rows, not an empty table. That put it on the V6-O1 stop list, so **S1.5 carried two stop-list
   items** — `S1.5-2` code issuance and `S1.5-4` the drop — where §2.5 previously claimed one.
2. **The safety argument changed from emptiness to reproducibility.** The reference layer is
   regenerable: the seeder is idempotent, its source is read-only, and byte-equal replay is already
   covered by its own tests. That is a *better* guarantee than "the table was empty" — but only
   because `S1.5-3` demonstrated rather than assumed it by dropping and re-seeding into a
   disposable database before phase B ran.
3. **The seeder had to be repointed** — item `S1.5-8`. Upsert by natural key formerly resolved
   `CodeName` directly; the completed realization resolves `CodeName` through `ExternalIdentity`
   to `EntityId`, with governed replay coverage.

**And one argument became stronger.** Every further row seeded against a `CodeName` foreign key
would have been another row to migrate at `S1.5-4`; ENTITM already had 327. **The cost of the phase
grew with delay**, which was the practical reason to run it before `S1-9` rather than after.

### 2.4 Internal companies (M-D3)

```
ENTMST  (every party; scalar Role='Company' is a display/export projection)
  ├── EntityRoleAssignments   EntityRole.InternalCompany  ← authoritative role child
  ├── CompanyRegistry (CompanyCode, Active, EffectiveFrom, RetiredAt)
  │     └── every FDR Company / CompanyCode column FKs here — unchanged
  ├── ENTALS · ENTBNK · EntityTaxIdentifier · ExtractionProfile
  └── ENTITM ──► ITMMST ──► ITMCLS ──► MNGACC ──► SNCACC
```

- **The role evidence needed by the S2-7 gate is exact.** The shipped compatibility realization
  identifies internal companies through `EntityRoleAssignments`; the explicit rejection when the
  resolved counterparty is the document's own company remains to be implemented in `S2-7`.
- **Intercompany needs one row, not two.** When Gott invoices Itoorer, Itoorer is one ENTMST row
  playing multiple roles, represented by the authoritative child. `RelatedParty` /
  `RelatedPartyKind` already exist for exactly this.
- **`CompanyRegistry` keeps its shipped job.** `CompanyId` points to merged `ENTMST.Id` with the
  same values; every FDR `Company` column keeps its FK target.
- **Nothing new to seed.** The FDR source's ENTMST rows already include the internal companies —
  `FDCHDR.CompanyCodeName` proves it.

### 2.5 The migration — pin, prepare, rename, contract

| Phase | What | Reversible? |
|---|---|---|
| **0 — pin** | Froze a schema contract: a fixture database plus the query set covering the 26 FK join paths, the `FDCHDR` composite anchor, tenancy joins, ERP binding reads and archive-path resolution. | n/a — it is the evidence |
| **A — prepare** | Added the FDR business columns to `BusinessEntity`, created `ExternalIdentity`, backfilled parties and recorded issued codes. | Additive only |
| **B — rename** | Dropped the seeded P11a ENTMST under stop-list approval, renamed `BusinessEntity` to `ENTMST`, established child FKs and constraints, created `ENTMST_Export`, and folded schema authority into migrations. | Recovery was by the demonstrated seeder path |
| **B′ — verify** | Replayed the unchanged phase-0 query set with an **empty diff**; repointed the seeder (`S1.5-8`) and demonstrated governed byte-equal replay. | Complete |
| **C — contract** | Drop `LegacyCode` and transitional shims once nothing reads them. | Phase 2 plan, `P2-6`; not on the critical path |

**The drop was reversible by regeneration, not by emptiness.** Dropping the P11a ENTMST destroyed
seeded reference rows (§2.3a). The read-only source and idempotent seeder were the restore point,
and `S1.5-3` demonstrated that path before phase B ran.

**The honest risk, and what phase 0 did about it — as it stood when this plan was written.** The
extraction golden set (S2-13) could not exist yet: the client/principal prerequisite was satisfied
but no verified end-to-end GD-1 output existed to pin. *(That changed on 2026-08-18: `S0-4` closed
with the pinned GD-1 bytes carried through `/api/channel-intake/v1`, the Hermes response passing
strict contract validation and persisting as one revision with two lines — so S2-13 is now
unblocked. The reasoning below is kept because it is why the schema contract was built.)* What
*could* be pinned at the time was the schema
contract above, and for a rename that is the more relevant evidence anyway: it proves every join
path that existed before still exists after. It is not a substitute for S2-13, and this plan does
not pretend otherwise — it is the strongest available evidence for the specific change being made,
and it costs about a day.

**Two stop-list items were involved**, revised as recorded in §2.3a: permanent code issuance at
`S1.5-2`, and the phase-B drop at `S1.5-4`, a destructive migration against populated reference
data. The idempotent seeder against the read-only source was demonstrated at `S1.5-3` before the
drop rather than assumed.

### 2.6 DOCTYP — the merge, and one open question

| New DOCTYP column | From | Note |
|---|---|---|
| `LabelPt`, `LabelEn` | `DocumentTypeSetting` | The UI is bilingual (`CultureRoutes`: `pt` default, `en`); the FDR spec has no localisation |
| `SortOrder` | `DocumentTypeSetting` | Drives dropdown order in every document screen |
| `IsActive` | `DocumentTypeSetting` | A retired type must stop being offered without breaking historical rows |

`DocumentTypeSetting` is dropped and `FinancialDocumentType` stops being an enum: the set of
document types becomes data, which is the whole point of S2-3.

> **Done, 2026-08-18.** `S1-6a` closed in three merge requests: `!70` migrated the four
> presentation columns onto DOCTYP (`20260817120000_S1DocumentTypePresentationData`), `!71` put
> `Treatment` / `DocClass` / `ProcessingRoute` behind a DOCTYP routing service across the July
> services, and `!72` dropped the table (`20260818120000_S1DocumentTypeSettingsDrop`) and retired
> the enum. Both names now survive only inside historical migration payloads. The columns stay
> target-owned: the migration wrote them once, the seeder does not.

> **Open question O-1 — `DocClass` spelling.** The seed holds `Payables` (12 rows, plural) and
> `Receivable` (7 rows, singular). Open since the 2026-08-05 letter to Luís (§A.4), unanswered.
> **The DOCTYP screen is where it gets fixed at source** rather than translated on import — but
> somebody has to say whether it is a typo. Until then the screen renders both verbatim and the
> CHECK admits both.

### 2.7 What each remaining table needs before it gets a screen

| Table | Structural note |
|---|---|
| `ENTALS` | `Company` nullable means *unscoped wildcard*, `UNIQUE NULLS NOT DISTINCT` on normalised alias. The UI must make "applies to all companies" an explicit, visible choice — not a blank field |
| `ENTBNK` | Parent holds render-parity comma-joined strings; `ENTBNKAccount` / `ENTBNKMethod` hold the rows payment routing actually reads. **The UI edits the children and regenerates the parent strings** — never the reverse |
| `ENTITM` | FKs on `ENTMST.Id` (M-D2); `(EntityId, ItemCode)` is the natural key. Both `CodeName` and the `EC######` export column are derived projections, never stored |
| `SNCACC` | **3,509 rows** since the Stage 11 source alignment (518 at the Aug-6 pin), self-FK `ContaPai`, `Nivel`. A tree, not a grid |
| `COCACC` | One placeholder row. **O-2: who owns the cost-centre list?** The screen is inert until somebody does |
| `EXCRAT` | `UNIQUE (Period, From, To)`. Missing rates block dependent auto-actions by design — the UI must show *which* rates are missing for the period, not just list what exists |
| `DOCEFL` | Add `LabelPt` / `LabelEn` for queue display. `EnforcementStartsAt` survives V6-D4's removal of grandfathering for one reason: a rule added today must not block documents captured yesterday |

> **Open question O-3 — is `SNCACC` global or per company?** Currently one unscoped chart of 3,509
> rows. The LMD chart-of-accounts cross-check (roadmap 26, open) decides it. Per-company scoping is
> far cheaper now than after entries reference the rows.

---

## 3. UI — the interaction model

### 3.1 What exists today

Blazor Server, Bootstrap, culture-prefixed routes (`/{pt|en}/...`), `UiText.Get(culture, key)` for
strings, policies `sibyla.admin` and `sibyla.financial-operations` over the roles
`Administrator` · `Finance` · `Accounting`.

> **Updated 2026-08-17.** `S1-9a` (MR `!63`) implemented the §4 route migration: the old URLs
> below 301 to the new ones via `LegacyPageRedirects`, and each page now lives at its target
> route. The dispositions stand; only the "today" addresses moved.

| Route (current) | Page | Backing table | Disposition |
|---|---|---|---|
| `/{c}/master/entities` *(was `/entities/suppliers`)* | `Suppliers.razor` | merged `ENTMST` | **Replace** — becomes Entity 360 (`S1-9d`). The shell and a **read-only** Identity tab landed with `!74`; the governed Identity/ENTMST write service landed with `!79`; the governed **Tax identifiers** tab landed with `!82`; the governed **Roles** tab landed with `!84`; a **read-only** Aliases tab landed with `!86`; the governed Aliases/ENTALS write landed with `!89`; governed **Banking** with `!91` and governed **Items** with `!93`; and the read surfaces **Documents** (`!96`), **Balances** (`!97`) and **Audit** (`!98`) completed the nine tabs on 2026-08-20. **The exit was demonstrated 2026-08-20 and S1 closed on it** |
| `/{c}/admin/document-types` *(was `/admin/settings/document-types`)* | `DocumentTypeSettings.razor` | `DOCTYP` | **Rewritten read-only** with `!72`; the `S1-9g` editing surface then shipped `g1` (governed presentation write, `!105`) and `g2` (governed offer toggle, `!117`) on 2026-08-20. `g3`, the routing columns, is blocked on **O-1** |
| `/{c}/admin/companies` | `Companies.razor` (62 KB) | merged `ENTMST` + connections + departments + accounts | **Entity half repointed** onto the governed identity write with `!107`; the connection half is working S4 code and was not touched |
| `/{c}/intake/upload` · `/{c}/review/queue` *(were `/documents/…`)* | `DocumentUpload` · `DocumentReviewQueue` · `DocumentReviewDetail` | `Document` etc. | Keep; extended in S2/S3 |
| `/{c}/integrations/cegid` | `CegidOperations.razor` | Integration ledger | Keep |
| `/{c}/master/item-classes` *(new, `S1-9c`)* | `ItemClasses.razor` | ITMCLS | Read-only first cut over `<ReferenceGrid>` |
| `/{c}/master/accounts/mng` *(new, `S1-9c`)* | `ManagementAccounts.razor` | MNGACC | Read-only first cut over `<ReferenceGrid>` |
| `/{c}/master/accounts/cost-centres` *(new, `S1-9c`)* | `CostCentres.razor` | COCACC | Read-only first cut over `<ReferenceGrid>`; inert until **O-2** |

### 3.2 The generic reference grid (M-D4)

One component, `<ReferenceGrid TEntity TKey>`, driven by a per-table metadata descriptor.

| Capability | Requirement |
|---|---|
| List | Server-side paging (SNCACC is 3,509 rows since Stage 11 — by far the largest reference table), sortable columns, sticky header |
| Filter | Per-column filter row plus one free-text box over the table's declared search columns |
| Edit | Inline, per-row dirty state, explicit save per row, `Esc` to revert |
| Create | New row appended in place, not a modal |
| Delete | **There is no delete.** Every FK is `RESTRICT`. The action is deactivate / retire, and the UI says so |
| Concurrency | Optimistic on PostgreSQL `xmin`; a conflict shows both values and asks — never silently overwrites |
| Validation | The CHECK constraints of §2 are the contract. The UI reproduces their messages in PT and EN rather than merely failing |
| Audit | Per-row drawer reading `AuditEvent` — who changed what, when, from what |
| Export | CSV and XLSX of the current filtered view |
| Bulk | Set one column across selected rows, with a confirmation naming the row count |
| Permissions | Read / write / deactivate resolved per table from the descriptor |
| Company scope | A persistent banner naming the active company. The shared Gott/Itoorer inbox is a documented hazard; a row edited under the wrong company is a correctness bug |

**Descriptor per table:** columns (type, width, editable, required, lookup source), search columns,
natural key, permission policy, validation rules, deactivation column.

**Descriptor-driven:** `ITMCLS`, `MNGACC`, `COCACC`, `EXCRAT`, `ITMALS`, `ENTALS`, and the ITMMST
list view. **Hand-built:** the seven below.

### 3.3 The seven hand-built screens

**Entity 360 — `/{c}/master/entities`** *(replaces `Suppliers.razor`)*
One entity, tabbed: **Identity** (`EntityCode` / `CodeName` read-only, resolved from `ExternalIdentity`; name, NIF, country tri-state,
currency, Role, Status, Origin, RelatedParty + Kind) · **Tax identifiers** · **Roles**
(`EntityRoleAssignments`, with a prominent `InternalCompany` badge — it supplies role evidence for
classification) · **Aliases** (scope shown explicitly) · **Banking** · **Items** (ENTITM with the accounts each posts to) · **Documents**
(recent DOCLOG) · **Balances** (read-only) · **Audit**. The merge panel carries over and writes
`MergedIntoId`. *This screen is the S1 exit criterion: search "CTT", see everything.*
**The new descriptor/reference-grid surfaces remain read-only first cuts.** Entity 360 is
different: six of its tabs now have a governed write path — `!79` introduced the Identity/ENTMST
write service, `!82` reused it for **Tax identifiers**, and `!84` reused it again for **Roles** —
while **Aliases** shipped **read-only** with `!86`, following the Identity precedent of a
read slice before a write slice. The pre-existing supplier Update /
Verify / Merge writes stayed exactly as they were, unaffected and still supplier-scoped, and
supplier/Identity compatibility was preserved. The governed Aliases write shipped in `!89`,
governed **Banking** in `!91` and governed **Items** in `!93`, each reusing
the same pattern.
The whole of it in one MR is past the twenty-minute review threshold §5 and `AGENTS.md` both set,
so it is built one merge request at a time:
- *Slice 1, shipped 2026-08-18 with `!74`:* the tabbed shell and a **read-only** Identity tab over
  merged ENTMST — canonical list/detail, PostgreSQL-backed search and paging, `EntityCode` /
  `CodeName` resolved from `ExternalIdentity`, name, fiscal number, country tri-state, currency,
  Role, Status, Origin, RelatedParty + Kind, external identities, honest placeholders for the tabs
  not yet built, service-side authorization and a test proving the read path writes no audit rows.
  No migration, no snapshot
  and no new write path; the pre-existing supplier Update / Verify / Merge panel stayed
  supplier-scoped. It proved the resolution path and left the write pattern to be reviewed alone.
- *Slice 2, shipped 2026-08-18 with `!79`* (source `03180d1d` → merge `8d924db6`, green head
  pipeline **2770706234**): the **governed Identity/ENTMST write service** — the M-D4 per-table
  application service. It reuses the existing paths rather than duplicating them, keeps the
  permanent codes read-only, enforces authorization service-side (including an `Administrator`
  requirement for `InternalCompany`), reproduces the §2 CHECK messages in PT and EN, appends a
  before/after `AuditEvent` inside the same transaction as the write, and implements and tests
  optimistic concurrency against PostgreSQL `xmin` (no speculative `Version` column, no
  migration). Transaction ownership and savepoints deny by default on unsafe caller-owned
  `ChangeTracker` state, and identity/role authorization races serialize at the PostgreSQL
  transaction level. The writable field set was shaped against the live code at the time. The
  first MR pipeline failed in the `PartyCollapseContract` source/baseline contract and was fixed
  forward without weakening it.
- *Slice 3, shipped 2026-08-19 with `!82`* (source `2e9732e` → merge `3fb806b`, merged
  2026-08-19T04:40:56.137Z; MR head pipeline **2771182592** success on that exact source SHA and
  post-merge `main` pipeline **2771203526** success on that exact merge SHA): **Tax identifiers**,
  reusing the pattern `!79` established rather than inventing a new one. An accessible PT/EN Tax
  identifiers tab; governed 1:N `EntityTaxIdentifier` mutations together with the **atomic
  `ENTMST.FiscalNo` primary projection**, so the child rows and the primary projection move as one;
  service-side `ClaimsPrincipal` authorization; permanent fields not writable — §3.4 rule 3 holds;
  a before/after audit write inside the same transaction as the mutation; `xmin` optimistic
  concurrency; caller transaction / savepoint handling and dirty-tracker deny-by-default;
  duplicate, primary and race handling; and a **current/submitted reload-and-reapply conflict UI**,
  which is §3.2's "shows both values and asks — never silently overwrites" applied to a hand-built
  screen. Its migration `20260819030442_S19dEntityTaxIdentifierInvariants` is **additive** — the
  invariants of §2 made structural as four CHECKs and two unique indexes on `EntityTaxIdentifiers`,
  with no destructive repair or backfill and deterministic count-only preflights. Route unchanged:
  `/{c}/master/entities`.
- *Slice 4, shipped 2026-08-19 with `!84`* (source `b9b332b` → merge `2c7eab4`, merged
  2026-08-19T10:17:17.49Z; MR head pipeline **2771963044** success on that exact source SHA and
  post-merge `main` pipeline **2772015854** success on that exact merge SHA): **Roles**, reusing
  the same pattern. An accessible PT/EN Roles tab; Supplier and Customer **add, deactivate and
  explicit reactivate**, preserving the permanent assignment and its verification evidence rather
  than re-creating either; the `InternalCompany` badge **read-only**; `ENTMST.Role` untouched, so
  the S1.5-7 compatibility realization of M-D3 is unchanged; verification read-only; **one
  `EntityRoleAssignment` mutation seam** used by Entity360, `BusinessEntityService` and Cegid
  alike, which is M-D4 applied to a child table — one application service per table, no second
  write path; automation still refusing inactive roles; service-side Admin / Finance / Accounting
  authorization with `Administrator` required for active `InternalCompany` entities; parent `xmin`
  optimistic concurrency, advisory locking, a same-transaction before/after audit write, savepoints
  and dirty-tracker deny-by-default; a **current/submitted conflict reload-and-reapply**, again
  §3.2's "shows both values and asks — never silently overwrites"; and **merge into an inactive
  destination role fails closed before repointing**. Its migration
  `20260819082319_S19dEntityRoleAssignmentInvariants` is **additive** — six count-only preflights
  and five CHECKs, with no repair, no backfill, no index alteration, no `ENTMST.Role` write and no
  seeder write. Route unchanged: `/{c}/master/entities`.
- *Slice 5, shipped 2026-08-19 with `!86`* (source `d2064e8` → merge `c1cec04`, merged
  2026-08-19T12:26:35Z): a **read-only Aliases** tab over ENTALS, following the Identity precedent
  of a read slice before a write slice. An accessible PT/EN tab at the unchanged
  `/{c}/master/entities` route; rows served only for canonical (non-merged) ENTMST entities;
  PostgreSQL-backed paging with a deterministic order (normalised alias, then scope, then
  `ALCode`); **scope shown explicitly** — an unscoped alias renders as "all companies" in PT/EN
  and is never a blank field, which is §2.7's ENTALS rule honoured on first contact;
  service-side read authorization (Administrator / Finance / Accounting). **No write path**; its
  migration `20260819111951_S19dEntityAliasesReadModel` has an empty `Up`/`Down` — EF
  model-mapping alignment only, no schema change. MR pipeline **2772364485** and post-merge `main`
  pipeline **2772402931** both succeeded.
- *Slice 6, shipped 2026-08-19 with `!89`* (source `a6b70fc2987bf274b7304d44032c2ff55a9b0c5c`
  → merge `35ca2e7016a629292c2a084fe22a4692d74438ba`, merged 2026-08-19T17:42:38.385Z):
  governed update-only existing Aliases/ENTALS writes. `Alias` plus explicit scope writable;
  `ALCode`, `EntityId`, generated `NormalizedAlias`, `CodeName` and `Flag` read-only; Admin / Finance
  / Accounting server authorization, active company, `xmin`, same-transaction audit,
  current/submitted conflict, one update seam and durable `HumanOwned`; seeder lock order preserves
  human Alias/scope while syncing source evidence. Additive migration `20260819144205`: count-only
  preflight, `CK_ENTALS_Alias_NonBlank`, `HumanOwned` default false, no repair, backfill or code
  issuance. MR pipeline **2773586157** and post-merge `main` pipeline **2773640192** succeeded; all
  three jobs succeeded. Local observations: Release 0 warnings / 0 errors, default 1660/1660,
  `FdrDatabase` 305/305, final review ACCEPT.
- *Slice 7, shipped 2026-08-19 with `!91`* (source `22392c3c3402bb9ac0bba8565eb15f6541fea7fc`
  → merge `484250070c6de4376d074bd96db0d227486fd7ff`, merged 2026-08-19T23:04:29.355Z):
  **governed Banking**, reusing the same pattern. An accessible PT/EN Banking tab over ENTBNK per
  `(EntityId, ScopeKey)` with the ENTBNKMethod/ENTBNKAccount children by ordinal; explicit scope —
  unscoped renders as "all companies" and structurally carries no pay accounts
  (`CK_ENTBNK_Unscoped_NoPayAccount`), which is §2's rule made structural; **update-only** edits
  of one child pay value per save through the single `EntityBankingMutation` seam, the parent
  `PayMethod`/`PayAccount` strings **regenerated from the children in the same transaction —
  never the reverse**, and child values non-blank and comma-free because the comma is the
  projection separator; service-side authorization with an active company required; parent `xmin`
  concurrency; same-transaction before/after audit; current/submitted reload-and-reapply; durable
  `HumanOwned` with seeder preservation and difference-gated projection regeneration, so
  steady-state replays rewrite nothing. Additive migration `20260819211044`: count-only
  preflights, `HumanOwned` on both child tables, non-blank/no-comma CHECKs, no repair, backfill or
  code issuance. MR pipeline **2774384047** and post-merge `main` pipeline **2774401009** both
  succeeded; all three jobs succeeded. Local observations: Release 0 warnings / 0 errors, default
  1687/1687, `FdrDatabase` 317/317; the fresh adversarial pass found one blocker in the seeder's
  projection regeneration, fixed forward inside the MR, findings recorded as MR comments.
- *Slice 8, shipped 2026-08-20 with `!93`* (source `7c094bd` → merge
  `01b6b90d10fed4f33207abce236371c091a89a2c`, merged 2026-08-20T00:46:40.668Z): **governed
  Items**, reusing the same pattern. An accessible, paged PT/EN Items tab over ENTITM ordered
  deterministically — `EICode`, item identity and class read-only — with a governed
  **update-only** write of the SNCACC/CostCentre account mapping, which is exactly the write the
  S3-4 learning path will later make; mappings cleared only through **explicit toggles**, never
  inferred from an empty control; `SNCDSC` **regenerated from the mapped SNCACC row's description
  in the same transaction — never edited directly** — and kept live for human-owned rows by a
  difference-gated seeder pass; unknown accounts and cost centres failing closed before any
  write; server authorization, `xmin` concurrency, a same-transaction audit keyed by the
  permanent `EICode`, current/submitted reload-and-reapply, one `EntityItemMappingMutation` seam,
  durable `HumanOwned`, and seeder preservation of the human mapping while source-owned columns
  still sync. Additive migration `20260819235415` adds only `HumanOwned`. MR pipeline
  **2774534974** and post-merge `main` pipeline **2774555750** both succeeded; all three jobs
  succeeded. Local observations: Release 0 warnings / 0 errors, default 1715/1715, focused
  `FdrDatabase` run 113/113; the fresh adversarial pass returned ACCEPT with two should-fixes,
  fixed forward inside the MR, findings recorded as MR comments.
- *Slices 9–11, shipped 2026-08-20 with `!96`/`!97`/`!98`:* the three **read surfaces**,
  completing the nine tabs. **Documents** (`!96`, source `5822188` → merge `ffc8d04`; MR
  pipeline **2774628822** with one recorded-flake `build-test` retry **15997571185**, post-merge
  **2774664385** success) pages recent DOCLOG via the entity's `fdr.code_name` identities and
  names the missing DOCLOG writer (S2) in its empty state; empty-`Up`/`Down` model-alignment
  migration `20260820011243`. **Balances** (`!97`, source `4d1b81d` → merge `dfb7e96`; MR
  pipeline **2774701711**, post-merge **2774717382** success) renders the source-owned
  operational balances labelled explicitly as **not master data** (the S2-15 matrix's R-40
  boundary), faithful to the stored `numeric(19,4)` scale in the page culture; no migration.
  **Audit** (`!98`, source `e6f4e19` → merge `ed9cf25`; MR pipeline **2774744416**, post-merge
  **2774775333** success) is a paged, type-scoped view of the entity's governed-write events with
  its attribution properties recorded in code; no migration. Each carried a fresh adversarial
  pass, findings fixed forward inside the MR and recorded as MR comments.
- *The exit demonstration, 2026-08-20:* Miguel ran the session at 14:45 against the demo
  database — searched "CTT", opened the real `EC000007` row and walked all nine tabs. Identity,
  Items (three rows, all SNC-mapped) and Balances carried data; Aliases and Banking showed honestly
  empty, the source carrying none for CTT. Its two findings were fixed the same day — the Roles-tab
  mojibake (`!121`) and the empty Tax Identifiers tab (`!122` + `!124`) — after which Miguel
  revalidated that CTT shows its NIF on the Tax Identifiers tab.

**`S1-9d` is done and S1 closed on 2026-08-20.** The exit demonstration took place against a real
matching ENTMST row shown to a real person, and Miguel **validated it with screenshots as the
record**; the date, the person and the evidence are exactly those. Two remainders were carried out
of S1 rather than left inside it: `g3`, the routing columns, blocked on **O-1** (Luís), and the
DOCEFL editor, **formally deferred to S2** (**O-4** stays Miguel's).

**Item master — `/{c}/master/items`** Grid plus detail: identity, class/subclass (ITMCLS lookup),
PLMKEY/PLMKO, SNCACC (tree lookup), cost centre, computed `NormalizedItemDesc`, aliases, and the
reverse view — which entities use this item.

**Entity × Item — `/{c}/master/entity-items`** The matrix, filterable by entity or item, with an
"unclassified lines seen in the last 30 days" view that turns a reference table into a work queue.
This is where step 7's learning becomes visible.

**Entity banking — `/{c}/master/entity-banking`** Parent per `(EntityId, scope)`, two child lists.
Unscoped rows are visually distinct with `PayAccount` disabled — `CK_ENTBNK_Unscoped_NoPayAccount`
forbids a routing target without a company, and the UI should make that structural rather than an
error message.

**Chart of accounts — `/{c}/master/accounts/snc`** Virtualised tree on `ContaPai` / `Nivel`, search
revealing the path to a match. Read-mostly; editing is adding a leaf. Shows how many ITMMST /
ENTITM rows point at each account — that is what makes a change safe or unsafe.

**Document types — `/{c}/admin/document-types`** *(rewrite)* One row per `(DocumentType,
OriginClass)`: `Treatment`, `ProcessingRoute`, `DocClass`, labels, order, active. A routing preview
**in words** beside each row — that sentence *is* the behaviour of the table.
*Shipped in three steps. `!72` (2026-08-18) landed the read-only catalogue and the PT/EN routing
preview, because the `DocumentTypeSetting` drop could not leave the screen pointing at a dropped
table. `!105` (2026-08-20) added the governed presentation write — `LabelPt` / `LabelEn` /
`SortOrder` only — and `!117` (2026-08-20) the governed **offer toggle** on `IsActive`, failing
closed without the full presentation, on an inactive row, or against a divergent already-offered
sibling, with withdrawal behind a server-enforced typed confirmation: `g1` and `g2`. What remains
is `g3`, the routing columns, blocked on **O-1**, which is still unanswered, so `Payables` /
`Receivable` render verbatim.*

**Flag rules — `/{c}/admin/flag-rules`** The highest-consequence screen here: it changes what blocks
production without a deploy. Full rule editor, plus two things it must not ship without — a
**pattern tester** that runs the rule against recent documents and shows what it would have flagged,
and an **impact count** before save. A rule saved blind is a production incident.

*Read half shipped 2026-08-20 with `!104`: the DOCEFL rule catalogue, read-only. The editor — with
the pattern tester and the impact count it must not ship without — is **formally deferred to S2**
and still waits on O-4.*

> **Open question O-4 — who may edit DOCEFL rules?** `Administrator` only, or `Finance` too? A
> permissions decision with operational consequences, and not mine to make.

### 3.4 Cross-cutting rules

1. **Two doors, one service (M-D4).** Every master row is reachable from the menu *and* inline from
   the review screen. Both call the same application service.
2. **Provisional rows are badged everywhere** until a human confirms them. `Home.razor` already has
   a "suppliers pending" tile — it becomes the entry point.
3. **Permanent codes are never editable.** `EntityCode`, `CodeName`, `ItemCode`, `EICode` render
   read-only with an explanatory tooltip.
4. **PT/EN throughout** via the existing `UiText` keys. New namespaces: `master.*`, `nav.master*`,
   `rules.*`.
5. **No hard delete anywhere.** Deactivate, retire, supersede.

---

## 4. Menu and modules

Sibyla is the first of three modules (`V6.md` §4: Medusa for CRM, Calypso for marketing). The
shell should exist now, with one module in it — retrofitting a module switcher into a navigation
tree that assumes documents is exactly the rewrite §4 warns about.

Top-level items **1 → 4 are the pipeline in order**. Master Data sits beside them because all four
read it. Administration is what changes behaviour, and is the only group gated on `sibyla.admin`.

```
┌ ● Sibyla — Finance   ○ Medusa — CRM   ○ Calypso — Marketing ┐   ← inactive greyed, not hidden

Sibyla — Finance
├─ Overview                                       /{c}
├─ 1 · Intake
│    ├─ Upload                                    /{c}/intake/upload          ← existing
│    ├─ Inbox                                     /{c}/intake/inbox           ← S5
│    └─ Capture failures        DOCFAI            /{c}/intake/failures
├─ 2 · Review
│    ├─ Queue                   DOCRQE            /{c}/review/queue           ← existing
│    ├─ Flags                   DOCFLG            /{c}/review/flags
│    └─ Approvals                                 /{c}/review/approvals       ← S4-2 gate
├─ 3 · Registry
│    ├─ Documents               DOCLOG            /{c}/registry/documents
│    ├─ Fiscal entries          FDCHDR/FDCDTL     /{c}/registry/entries
│    └─ Archive                 DOCARC            /{c}/registry/archive       ← S6
├─ 4 · Integrations
│    ├─ Cegid izibizi                             /{c}/integrations/cegid     ← existing
│    ├─ Push queue                                /{c}/integrations/queue
│    └─ Sync runs                                 /{c}/integrations/runs
└─ Master Data
     ├─ Entities
     │    ├─ Entities           ENTMST            /{c}/master/entities        ← Entity 360
     │    ├─ Aliases            ENTALS            /{c}/master/entity-aliases
     │    ├─ Banking            ENTBNK            /{c}/master/entity-banking
     │    └─ Entity × Item      ENTITM            /{c}/master/entity-items
     ├─ Items
     │    ├─ Items              ITMMST            /{c}/master/items
     │    ├─ Item aliases       ITMALS            /{c}/master/item-aliases
     │    └─ Item classes       ITMCLS            /{c}/master/item-classes
     └─ Accounting
          ├─ Chart of accounts  SNCACC            /{c}/master/accounts/snc
          ├─ Management accts   MNGACC            /{c}/master/accounts/mng
          ├─ Cost centres       COCACC            /{c}/master/accounts/cost-centres
          └─ FX rates           EXCRAT            /{c}/master/fx-rates

Administration                                    sibyla.admin only
  ├─ Companies                  ENTMST+Registry   /{c}/admin/companies        ← existing, repointed
  ├─ Users & memberships                          /{c}/admin/users            ← S5-6
  ├─ Departments                                  /{c}/admin/departments
  ├─ Channels & allowlists                        /{c}/admin/channels         ← S5
  ├─ Document types             DOCTYP            /{c}/admin/document-types   ← existing, rewritten
  ├─ Flag rules                 DOCEFL            /{c}/admin/flag-rules
  └─ Audit                      AuditEvent        /{c}/admin/audit
```

**Route migration**, all 301 because the old URLs are in circulation:
`/entities/suppliers` → `/master/entities` · `/admin/settings/document-types` →
`/admin/document-types` · `/documents/upload` → `/intake/upload` · `/documents/review` →
`/review/queue`. `/admin/companies` is unchanged.

**Why this shape.** The numbered sections are the flow — an operator reads top to bottom and gets
the pipeline in order; when something is stuck, the number says where to look. Master Data is one
group rather than scattered, because the reference layer is the platform's memory and the thing the
learning path writes to. Administration is separate because editing it changes what the pipeline
*does* rather than what it *knows*. The module switcher is a shell, not an abstraction: no Medusa or
Calypso code is written, and per P-1 Medusa's contacts will point at ENTMST rather than copy it —
which is why the entity screens live under a module-neutral `master` path.

---

## 5. Sequencing

### S1 — reference layer (extends the existing items)

| Item | Work | Status |
|---|---|---|
| `S1-3a` | ITMMST + `NormalizedItemDesc`, `IsActive`; retire `IntegrationProductPlKeyMapping`; repoint `IntegrationProductBinding` | **Done** — MR `!62`, 2026-08-16, migration `20260815142000_S13aItemMasterConsolidation` |
| `S1-6a` | DOCTYP + `LabelPt`, `LabelEn`, `SortOrder`, `IsActive`; drop `DocumentTypeSetting`; retire the `FinancialDocumentType` enum | **Done** — columns in `…143000`; presentation data migrated by `!70`, routing service by `!71`, drop and enum retirement by `!72`, 2026-08-18 |
| `S1-7a` | DOCEFL + `LabelPt`, `LabelEn` | **Done** — `…144000_S1DoceflLabels` |

### S1.5 — Party collapse · complete through S1.5-8

Completed by cumulative MR `!59` on 2026-08-15. The table records the resulting sequence and
outcomes, not prospective work.

| Item | Completed outcome | Gate / evidence |
|---|---|---|
| `S1.5-0` | Layer 2 source-semantics work integrated with the merged-table sequence | — |
| `S1.5-1` | **Phase 0** pinned the schema contract with its fixture and query set | — |
| `S1.5-2` | **Phase A** added the business columns and `ExternalIdentity`, backfilled parties and recorded issued codes | CodeLedger issuance was stop-list work |
| `S1.5-3` | Reconciliation and the seeder-based recovery path were demonstrated | Phase B prerequisite satisfied |
| `S1.5-4` | **Phase B complete** — dropped the seeded P11a ENTMST, renamed the live party table to `ENTMST`, moved child FKs to `Id`, and made EF migrations authoritative | Approved destructive migration |
| `S1.5-5` | **Phase B′ complete** — replay of the unchanged phase-0 contract produced an empty diff | Exit criterion satisfied |
| `S1.5-6` | The 61-test `P11aDatabase` suite now runs against migrations | Pipeline 2762302842: 61/61 |
| `S1.5-7` | Compatibility realization retained authoritative `EntityRoleAssignments` with `EntityRole.InternalCompany`, scalar ENTMST `Role='Company'` for display/export, and party reads on merged ENTMST | No physical `ENTROL`; S2-7 enforcement remains open |
| `S1.5-8` | The reference-data seeder was repointed to the post-collapse shape with governed, byte-equal replay | Pipeline 2762302842: `FdrDatabase` 189/189 |

### S1-9 — UI, replacing the single backlog item

**Every buildable piece is delivered.** `S1-9` stays `[~]` in the backlog only because two
remainders were carried out of the phase, not because a screen is missing.

| Piece | What | Status |
|---|---|---|
| `a` | module shell, menu tree, route redirects | **Done** — MR `!63`, 2026-08-16 |
| `b` | `<ReferenceGrid>` + descriptor + write-service pattern | **Done** — MR `!64`, 2026-08-16. Read, sort, filter, paging, deactivate and optimistic-concurrency contracts. **The grid write path is still reserved and unexercised**: `!105` shipped the DOCTYP presentation write on the plain admin catalogue page instead, so the grid write path's first user remains open |
| `c` | descriptor/list screens — ITMCLS, MNGACC, COCACC, EXCRAT, ITMALS, ENTALS | **Done, all six read-only** — ITMCLS `!65`, MNGACC `!66`, COCACC `!69` (2026-08-17, COCACC inert on its one placeholder row until **O-2**); then EXCRAT `!100` with the **missing-rates panel**, ITMALS `!101` (first `GlobalOrActiveCompany` grid) and ENTALS `!102` (surfacing `HumanOwned`) on 2026-08-20. The ENTALS *descriptor* screen is distinct from Entity 360's Aliases tab, and is read-only **by design** — aliases are edited on the entity's page through the `!89` governed path |
| `d` | **Entity 360** *(S1 exit criterion)* | **Done — eleven slices, and the exit demonstrated 2026-08-20.** Read-only Identity shell `!74` (merge `cdb26c5`, pipeline **2769319314**) · governed Identity/ENTMST write service `!79` (merge `8d924db6`, **2770706234**) · governed Tax identifiers `!82` (merge `3fb806b`, **2771182592**/**2771203526**) · governed Roles `!84` (merge `2c7eab4`, **2771963044**/**2772015854**) · read-only Aliases `!86` (merge `c1cec04`, **2772364485**/**2772402931**) · governed Aliases/ENTALS write `!89` · governed Banking `!91` (merge `4842500`, **2774384047**/**2774401009**) · governed Items `!93` (merge `01b6b90`, **2774534974**/**2774555750**) · the read surfaces Documents `!96`, Balances `!97` and Audit `!98` on 2026-08-20 |
| `e` | item master, entity × item, entity banking | **Done 2026-08-20** — ITMMST item master `!109`, the Entity × Item matrix with the classification queue `!110`, the entity-banking catalogue `!112` |
| `f` | SNCACC tree | **Done 2026-08-20** — `!113` |
| `g` | DOCTYP editing surface | **`g1` and `g2` done**: the read-only catalogue and routing preview with `!72` (2026-08-18), the governed presentation write with `!105` and the governed offer toggle with `!117` (2026-08-20). **`g3`, the routing columns, is blocked on O-1** (Luís) and was carried out of S1 |
| `h` | DOCEFL with pattern tester and impact count | **Read half done** — the read-only rules catalogue `!104`, 2026-08-20. **The editor is formally deferred to S2** and waits on O-4 (Miguel) |
| `i` | repoint the entity half of `Companies.razor` | **Done 2026-08-20** — `!107`; the connection half is untouched S4 code |

**S1 closed 2026-08-20** on the demonstrated "CTT" exit (§3.3), with `g3` and the DOCEFL editor
carried out of the phase rather than left open inside it.

### Phase 2

Seven deferred structural items, each with its real decision point rather than a date, are in the
companion document **`master-data-phase2-plan.md`**. **`P2-2` is closed without consolidation:**
ITMCLS, MNGACC and COCACC stay separate, no `ReferenceValue` table is created, and the question does
not reopen at `S1-9c`. Only **`P2-3`** remains a Phase 1 decision point, at `S2-6`.

---

## 6. Open questions

Five remain. Each changes what gets built; none blocks continuing after §5. O-6 was resolved by the
S1.5-7 compatibility realization recorded in M-D3. O-7 was resolved by observation: S1.5 completed
while S0-4 remained open.

| # | Question | Why it matters | Owner |
|---|---|---|---|
| **O-1** | `DOCTYP.DocClass` — is `Payables` / `Receivable` a typo? | Open since 2026-08-05. The DOCTYP screen fixes it at source or preserves it verbatim forever | Luís |
| **O-2** | Who owns the cost-centre list? | COCACC has one placeholder row; the screen is inert without an owner | Business |
| **O-3** | Is SNCACC global or per company? | Cheap now, expensive once entries reference rows. Tied to the open LMD cross-check | Accounting |
| **O-4** | Who may edit DOCEFL rules? | Changes production blocking behaviour without a deploy | Miguel |
| **O-5** | Does an entity need `Inactive` while carrying posted documents? | Decides whether `Status` and `IsActiveForCapture` are genuinely two columns | Business |

---

## 7. What this plan does not do

- **No document data is imported.** V6-D3 stands. This is structure and screens.
- **No Layer 5 or Layer 6 work.** `BankAccount` is untouched; `BNKACC` waits for Phase 2.
- **No foreign-key migration at all.** The 26 columns follow the table through the rename, and the
  five FDR children are created on `ENTMST.Id`. **One internal key style, not two** — business codes
  become export identity and nothing else, exactly as `p1-0-schema-mapping.md` §1 requires.
- **No second schema authority.** `P11aSchemaSql` is retired rather than rewritten (M-D6), so the
  61-test suite is rewritten once.
- **No bitemporality anywhere yet.** M-D7 records *which* tables get it; none is built in this plan.
- **No Medusa or Calypso code.** A shell with one module in it, and the three standing constraints
  already recorded as P-1 / P-2 / P-3.
- **No grandfathering machinery.** V6-D4 removed the need; `EnforcementStartsAt` survives only so a
  rule added on Tuesday does not block Monday's documents.
- **No new governance process.** V6-O1 applies unchanged: pipeline green → merge, adversarial pass
  as comments, fix forward — with an explicit yes on the **two** stop-list items this plan contained:
  `S1.5-2`, permanent code issuance, and `S1.5-4`, the phase-B drop. *(Corrected 2026-08-12: this
  bullet still read "one … Nothing here destroys populated data", which §2.5 and §4 had already
  revised. The P11a ENTMST was seeded on 2026-08-11, so phase B did destroy populated data.)*
