GOTT · Sibyla · approved 2026-08-11
Database structure first, then the human interface over it. Companion to docs/V6.md and docs/project-todo-v6.md; the party collapse landed as S1.5 between S1 and S2.
M-D1's cost argument was that the P11aSchemaSql ENTMST is empty and has no writer — STATE.md said so explicitly. That stopped being true the same day this plan was written. !43 (6b42d79) and 7a93a59 merged the Layer 2 reference seeder, which writes ENTMST, ENTALS, ENTBNK and ENTITM.
| Seeded now | Rows |
|---|---|
| ENTMST · ENTALS · ENTBNK | 119 · 7 · 43 |
ENTITM — FK'd on CodeName | 327 |
BusinessEntity still do not move. Collapsing upward is still far cheaper than the downward alternative, and every argument below about the rename carrying constraints by OID stands untouched.S1.5-3 demonstrated the idempotent, read-only-source recovery path before phase B ran. (3) The seeder was repointed in S1.5-8; upsert by natural key now resolves CodeName through ExternalIdentity to EntityId.CodeName foreign key would have been another row to migrate at S1.5-4. ENTITM already had 327. The cost of this phase grew with delay, which was the practical reason to run it before S1-9.The premise was that thirteen tables are repeated. Three are. The rest are capability the July spine never had — and separately, seven July tables look like duplicates but are not, and deleting them would remove working behaviour.
| FDR table | July counterpart | Verdict | Structural resolution |
|---|---|---|---|
| ENTMST | BusinessEntity | Merge upward — one table | The two became one physically: BusinessEntity was renamed to ENTMST and gained the FDR business columns; the seeded, reproducible 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. |
| ITMMST | IntegrationProductPlKeyMapping | Replace + merge 2 | ProductCode → PlKey is the same relationship ITMMST states as ItemCode → PLMKEY, done per-provider. ITMMST now subsumes it and carries NormalizedItemDesc and IsActive. |
| DOCTYP | DocumentTypeSetting + FinancialDocumentType enum | Replace + merge 4 | DocumentTypeSetting was presentation only — a PT/EN label on a hard-coded enum. DOCTYP is behavioural and now carries LabelPt, LabelEn, SortOrder and IsActive. |
| ENTALS | — | New | The July spine had only an implicit NormalizedLegalName match; ENTALS adds governed aliases. |
| ENTBNK +2 children | — | New | Not a duplicate of BankAccount — see §3. |
| ITMALS | — | New | The July spine's nearest equivalent was free text in DocumentLine.Description. |
| ENTITM | — | New | The per-entity item classification that makes the learning path possible; the July spine had no equivalent. |
| ITMCLS | — | New | — |
| MNGACC | — | New | — |
| SNCACC | — | New | The July spine had no chart of accounts. DocumentLine.Category was an unconstrained string. |
| COCACC | — | New | One placeholder row today. Real cost centres arrive through the UI, not the seeder. |
| EXCRAT | — | New | The July spine had no rate table; ExtractionRevision.Currency records a currency but does not convert it. |
| DOCEFL | — | New | The July equivalents — ValidationGatesJson, WarningsJson and AttentionReason — were ad hoc and hard-coded rather than a catalogue. |
| Pair | Verdict | Why they are different things |
|---|---|---|
| ENTBNK ↔ BankAccount / CashAccount | Keep both | BankAccount holds our own companies’ accounts — the source side of PurchasePaymentInstruction. ENTBNK holds counterparty routing — how we pay a supplier. Opposite sides of the transaction; both read at step 8. The real duplicate of BankAccount is BNKACC (Layer 6), deferred to Phase 2 — do not touch it in Phase 1. |
| ENTMST.FiscalNo ↔ EntityTaxIdentifier | Keep child | ENTMST carries one FiscalNo. EntityTaxIdentifier is 1:N with type, primary flag, normalised and original value — a VAT number and a national ID and an EORI. Capability ENTMST lacks. Repoint to ENTMST; FiscalNo becomes the denormalised primary. |
| ENTMST.Role ↔ EntityRoleAssignment | Keep child | ENTMST.Role is a scalar. An entity is routinely supplier and customer, and an internal company is an entity with a Company role. A scalar cannot express that. Keep the child as authoritative; keep the scalar as denormalised primary role for display and export parity. |
| ENTALS ↔ IntegrationProduct/PartyBinding | Keep both | Bindings cache the provider’s external id, keyed by (Company, ConnectionKey, Provider). An alias is a name a document uses. Different lifetimes, different owners. One cleanup: drop IntegrationPartyBinding.TaxIdentifier, a stored copy of what ENTMST already holds. |
One party table — counterparties and internal companies alike. The authoritative EntityRoleAssignments child identifies InternalCompany; scalar ENTMST Role='Company' remains a display/export projection. ENTMST is the physical BusinessEntity table renamed, so all 26 inbound foreign keys followed it untouched. CompanyRegistry keeps its shipped job.
BusinessEntity table renamed. PostgreSQL carries inbound FK constraints through the rename, so there is no foreign-key DDL at all.There are two directions to collapse in, and they are not symmetric. Downward — create ENTMST properly and retarget every FK onto it — is 26 constraint changes across 21 tables, and 26 chances to get a live reference wrong. Upward — keep the table the FKs already point at, and rename it — costs zero. PostgreSQL carries inbound foreign-key constraints through ALTER TABLE "BusinessEntity" RENAME TO "ENTMST"; they follow the table by OID, not by name.
| Untouched by the rename | What the FDR side cost |
|---|---|
| All 26 uuid FK columns · Document(Id, BuyerEntityId) — the FDCHDR composite anchor · CompanyRegistry(CompanyId, CompanyCode) · Membership · Department · both ERP connections · all six IntegrationSales* tables · every index | Drop the P11a ENTMST and its seeded rows · add the FDR business columns · re-establish five child FKs — now on ENTMST.Id, not CodeName (ENTALS, ENTITM, ENTBNK, ENTBNKMethod, ENTBNKAccount) · create ExternalIdentity and backfill both code schemes · re-point the CHECK constraints · fold the DDL into EF migrations and retire P11aSchemaSql · rewrite the 61-test suite once |
P11aSchemaSql ENTMST was empty when the decision was drafted, then the Layer 2 seeder populated it. Phase B therefore dropped reproducible seeded data under the demonstrated recovery path. The collapse still completed without retargeting the 26 live constraints. The source-semantics work was integrated with the merged-table sequence, and the post-collapse seeder realization completed in cumulative MR !59.| Phase | What | Reversible? |
|---|---|---|
| 0 — pin | Froze a schema contract 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 and 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 made EF migrations authoritative. | Recovery was 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 |
S1.5-3 demonstrated that path before phase B ran. The phase-0 schema contract was the evidence specific to the rename and proved the existing join paths survived. Two stop-list items were involved: CodeLedger issuance in phase A, and the phase-B drop.| Column | Type | Why ENTMST cannot do without it |
|---|---|---|
| Id | uuid PK | Already there — the table is BusinessEntity renamed. Stays the primary key, so all 26 inbound FKs remain valid and the five FDR children now FK here too. The only key anything joins on. |
| Status | varchar(24) | ENTMST has IsProvisional only — it cannot express Inactive. Every FK is RESTRICT, so retiring is the only available operation and there is no column for it. |
| MergedIntoId | uuid NULL FK ENTMST(Id) | Duplicate-identity merge is an existing, used operator function. Self-referencing, RESTRICT; a merged row is never deleted. |
| Origin | varchar(24) | Extracted · Manual · Seeded · Integration. Answers “did a human assert this, or did an agent guess it?” — the first question asked of a suspicious counterparty. |
| NormalizedEntityName | text | Resolution is NIF → alias → name. The name leg needs a deterministic search key; CodeName is a permanent human code and cannot serve. |
| IsActiveForCapture | bool | An entity may be Active for history while blocked from new captures. Cheap now, expensive once documents reference it. |
| LegacyCode | varchar(64) NULL | Transitional — preserves the July Code so nothing quoting it breaks mid-migration. Dropped in Phase 2. |
EntityCode and CodeName do not arrive as columns at all. They become rows in a mapping table, one per entity per scheme — fdr.entity_code, fdr.code_name, and later cegid.party / moloni.customer.
ExternalIdentity Id uuid PK EntityId uuid NOT NULL FK ENTMST(Id) RESTRICT Scheme varchar(32) NOT NULL 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) -- unique inside its own scheme, not globally
S1.5-2 is the moment codes are minted; the window is one migration wide. A minted code on a master column is permanent and load-bearing the instant it exists — the same code as a mapping row is a fact that can be superseded.CodeName is currently the FK target of five tables, which is why the FDR prototype needed rename_codename.py and validate_codename_immutability.py. With the children FK'd on ENTMST.Id, a rename is one UPDATE of one row and both scripts stop being needed. That is prototype evidence, not prediction.ENTMST_Export view that renders the flat FDR shape, so the exporter's SQL does not change. And the step-9 archive path resolves through that view rather than a column read. What does not change: codes are still issued once, still read-only in the UI, a rename is still a governed command with an audit event — it is merely no longer a cascade.Every one of the thirteen tables is read at step 4 or step 8, and four of them 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.
| # | Step | Master tables read | Written |
|---|---|---|---|
| 1 | A document arrives | CompanyRegistry · ENTALS | — |
| 2 | It is recognised | DOCTYP · EntityRoleAssignments | — |
| 3 | It is read | ExtractionProfile | — |
| 4 | It is classified | ENTMST → ENTALS → name · EntityRoleAssignments gate · ITMALS → ITMMST → ENTITM → ITMCLS → MNGACC → SNCACC → COCACC · EXCRAT if not EUR | ENTMST (provisional) |
| 5 | It is persisted | CodeLedger | DOCLOG · FDCHDR · FDCDTL |
| 6 | Uncertainty is surfaced | DOCEFL | DOCFLG |
| 7 | The human’s answer is learned | — | ENTALS · ENTITM · ITMALS · ENTMST |
| 8 | It is pushed to Cegid | ENTMST.FiscalNo · ENTBNK · PartyBinding · MNGACC / SNCACC · BankAccount | PartyBinding |
| 9 | It is filed | ExternalIdentity via ENTMST_Export (archive path) | — |
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 exactly why it has to merge rather than simply be replaced.One reusable <ReferenceGrid> driven by a per-table metadata descriptor covers the flat tables; seven get purpose-built screens. Every master write — from the grid, from the review queue’s learning path, or from the seeder — goes through the same application service.
| Screen | Route | Table | Build | The thing that makes it non-trivial |
|---|---|---|---|---|
| Entity 360 | /master/entities | ENTMST | hand | Nine tabs over one entity. Replaces Suppliers.razor and is the S1 exit criterion. Delivered: read-only Identity shell !74, governed Identity !79, Tax identifiers !82, Roles !84, read-only Aliases !86, governed Aliases/ENTALS writes !89, governed Banking !91, governed Items !93, read-only Documents !96, Balances !97 and Audit !98. All nine tabs built, and the exit was demonstrated 2026-08-20 — Miguel walked them against the real EC000007 row and validated it with screenshots; S1 closed on it. |
| Item master | /master/items | ITMMST | hand | Reverse view: which entities use this item. |
| Entity × Item | /master/entity-items | ENTITM | hand | “Unclassified lines seen in the last 30 days” turns a reference table into a work queue. |
| Entity banking | /master/entity-banking | ENTBNK | hand | Edit the children, regenerate the parent render-parity strings — never the reverse. Unscoped rows disable PayAccount structurally. |
| Chart of accounts | /master/accounts/snc | SNCACC | hand | 3,509 rows since the Stage 11 source alignment, self-FK tree (518 at the Aug-6 pin; the 3,509 figure was withdrawn as unmeasured under v6 §5 and was re-measured by !135). Shows how many ITMMST/ENTITM rows point at each account — that is what makes a change safe. |
| Document types | /admin/document-types | DOCTYP | rewrite | A routing preview in words next to each row — that sentence is the behaviour of the table. Read-only catalogue + PT/EN preview shipped with !72; g1 (governed presentation write, !105) and g2 (governed offer toggle, !117) shipped 2026-08-20; g3, the routing columns, is blocked on O-1. |
| Flag rules | /admin/flag-rules | DOCEFL | hand | Highest consequence in the plan — changes what blocks production without a deploy. Needs a pattern tester against recent documents and an impact count before save. Read half shipped 2026-08-20 (!104, read-only catalogue); the editor is formally deferred to S2 and waits on O-4. |
| Aliases · classes · accounts · FX | /master/… | ENTALS ITMALS ITMCLS MNGACC COCACC EXCRAT | generic | Descriptor-driven. EXCRAT must show which rates are missing for the period, not just list what exists. |
| Companies | /admin/companies | ENTMST + Registry | repoint | Repoint the entity half only. The connection half is working S4 code — do not touch it. |
| Capability | Requirement |
|---|---|
| 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 are the contract. The UI reproduces their messages in PT and EN rather than merely failing. |
| 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. |
| Permanent codes | EntityCode, CodeName, ItemCode, EICode render read-only with an explanatory tooltip. |
| Also | Server-side paging · per-column filter row · inline edit with per-row dirty state · audit drawer over AuditEvent · CSV/XLSX export of the filtered view · bulk column set with a row-count confirmation · per-table permissions. |
S1.5 is complete through S1.5-8. Cumulative MR !59 completed the party collapse on 2026-08-15. The table records outcomes, not prospective work.
| Item | Work | Note |
|---|---|---|
| S1.5-0 | Layer 2 source-semantics work integrated with the merged-table sequence | Complete |
| S1.5-1 | Phase 0 pinned the schema contract with its fixture and query set | Complete |
| 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 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′ replayed the unchanged phase-0 contract with an empty diff | Exit criterion satisfied |
| S1.5-6 | The 61-test P11aDatabase suite now runs against migrations | Complete |
| S1.5-7 | Compatibility realization retained authoritative EntityRoleAssignments with InternalCompany, scalar ENTMST Role='Company', and party reads on merged ENTMST | No physical ENTROL; S2-7 remains open |
| S1.5-8 | The reference-data seeder was repointed to the post-collapse shape with governed byte-equal replay | Complete |
| Phase 2 | Seven deferred structural items are in master-data-phase2-plan.md. P2-2 is closed without consolidation; the tables stay separate and the decision does not reopen at S1-9c. Only P2-3 remains a Phase 1 decision point, at S2-6. | see companion |
Five remain. Each changes what gets built; none blocks continuing the work above.
| # | 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? | Editing them 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 |