# P1-0 — corrections decided in review

Companion to `docs/p1-0-review-findings.md`. One entry per blocker, in the order they were worked.
Each entry is written so it can be applied to the drafts without re-deriving the reasoning.

Decided by Miguel Teixeira, 2026-08-04, standing in for Luís Nascimento — who was unavailable, so
every decision here was taken on his behalf against pinned data.

**No entry in this document is an open question.** Two kinds of Luís marker appear, and neither blocks
execution:

- ***Needs Luís to confirm*** — the decision **is taken** and is implementable today. He may later
  object, and it is reversible until he does. Seven entries: C1, C2, C3, C4, C12, C19, and the
  `Receivable` singular.
- ***Needs Luís*** (no "confirm") — an **action in his own repository** that we cannot perform:
  merging the 5 duplicate ENTBNK pairs at source (C8) and classifying the 3 residual FL collisions
  (C13). Neither is a decision. C13 already carries a `DetectedAt` tiebreaker so the import passes
  without him; C8 deliberately fails closed, which was a decision taken knowingly.

Evidence base: prototype at `9359c67`, re-confirmed at `6146004`.

---

## C1 — Aggregate payroll documents: split identity from referential integrity

**Blocker:** report A1. `FDCHDR.SourceKey` scalar with
`FOREIGN KEY (Company, SourceKey) REFERENCES BNKMOV(Company, SourceKey)` resolves 419 of 419 single
movement documents and **0 of 7** aggregates. Import fails.

**Measured at the pin:**

- 7 aggregate documents, **all `FlowType='P'`** — payroll only. Component counts 2, 2, 2, 3, 3, 11, 15.
- `SourceBMCode` (comma-joined) and `SourceKey` (joined by the literal `" + "`) always agree in
  cardinality.
- 419 singles + 38 aggregate components = **457 distinct BMCodes referenced, none referenced by more
  than one generated document.** The relationship is one-to-many, not many-to-many.
- Aggregate `SourceKey` lengths: 146, 162, 185, 251, 277, 786, **1083** bytes (≈72 bytes per
  component). Singles: 57–114.

**Diagnosis.** `SourceKey` is not wrong. As an *identity* it works for aggregates — a deterministic
string built from components in official movement order, and it is what makes re-ingestion
idempotent. It fails only as a *foreign key*. One column was doing two jobs.

**Decision — apply all three parts:**

1. **Keep `FDCHDR.SourceKey` as the identity anchor and remove its foreign key.** The mutually
   exclusive partial unique index stays: source-fiscal rows unique on
   `(Company, Entity, NormalizedDocumentID, FlowType) WHERE SourceKey IS NULL`; bank-generated rows
   unique on the identity below `WHERE SourceKey IS NOT NULL`.
2. **Index the identity by hash, not by the raw string.** Add `SourceKeyHash` as a **stored generated
   column** (SHA-256 of `SourceKey`) and move the bank-generated partial unique index to
   `(Company, SourceKeyHash)`. Rationale: at 1,083 bytes today and ~72 bytes per component, a pay run
   above roughly 37 movements exceeds PostgreSQL's ~2,704-byte btree limit and the index fails at
   insert time. `SourceKey` stays readable for audit. **This is the pattern the freeze already uses**
   for `CodeLedger.NaturalKeyHash` — not a new mechanism.
3. **Add a junction table for the actual links:**

   ```
   FdcHdrBankMovement(
     Company        FK CompanyRegistry(CompanyCode),
     EntryCode      -- with Company, FK -> FDCHDR(Company, EntryCode)   ON DELETE RESTRICT
     BMCode         -- with Company, FK -> BNKMOV(Company, BMCode)      ON DELETE RESTRICT
     ComponentOrder int NOT NULL          -- official movement order; the aggregate key depends on it
     PRIMARY KEY (Company, EntryCode, ComponentOrder)
     UNIQUE (Company, BMCode)             -- a movement is consumed by at most one generated document
     UNIQUE (Company, EntryCode, BMCode)  -- no component repeats within a document
   )
   ```

   `UNIQUE (Company, BMCode)` is the constraint that encodes the measured one-to-many property. It
   holds on all 457 referenced BMCodes today; if a future round breaks it, that is a real finding and
   should fail loudly rather than be absorbed.

4. **`SourceBMCode` becomes what it always was** — a human-readable pointer, derivable from the
   junction table by `ComponentOrder`, not a source of truth. It currently appears **zero times** in
   either draft and must be named in the mapping so the import knows what it is reading.

**Acceptance test.** Re-ingest an overlapping bank statement (the pending July extract is the natural
candidate) and require: all generated documents re-anchor, zero duplicates, all 7 aggregates resolve
through the junction table, and `UNIQUE (Company, BMCode)` is not violated.

**Applies to:** `p1-0-schema-mapping.md` §4 and §8; `p1-0-codes-taxonomy-archive.md` §1 (aggregate
SourceKey paragraph — it already states the composite correctly and must stop contradicting the
mapping). *Needs Luís to confirm* that no future pay-run shape breaks one-movement-one-document.

---

## C2 — ENTITM: store `CodeName`, treat the rendered `EntityCode` as a derived export

**Blocker:** report A2, and it sits inside **decision 18 of the nineteen — one the freeze declared
settled.** Both halves of that decision are inverted.

**Measured at the pin:**

- `entity_products.json`, 327 rows, fields `Class, CodeName, CostCentre, EICode, FiscalNo, Flag,
  ItemCode, ItemDesc, PLMKEY, PLMKO, SNCACC, SNCDSC, Subclass`. **There is no `EntityCode` field.**
- `build_workbook.py:176,181` builds `codename_to_entitycode` and writes **`EC######` values** into
  the rendered `EntityCode` column, then resolves `FiscalNo` from that EC code — confirming EC
  semantics in the render.
- Ledger key `(CodeName, ItemCode)`: 327/327 unique, 327/327 CodeNames resolve to ENTMST.

**Diagnosis.** The draft treats "the source" as one thing. There are **two ingestion surfaces**: the
JSON, which carries `CodeName`, and the rendered workbook, which carries `EC######`. A rule written
for one rejects the other. The draft's rule — *"import must reject an EC code"* — would reject 100%
of rendered rows.

**Decision:**

1. **Store `CodeName varchar(128)` with FK to `ENTMST.CodeName`.** Mirrors the JSON source and keeps
   the ledger key consistent with the stored column.
2. **The rendered `EntityCode` column is a derived export projection**, resolved from ENTMST at render
   time. It is not stored and not an import target. Render parity is preserved.
3. **The import accepts both surfaces:** by `CodeName` when reading the JSON, and by resolving
   `EC → CodeName` through ENTMST when reading the workbook. It rejects an *unresolvable* value —
   never an EC code as such.
4. **Rewrite decision 18, do not amend it.** It is inverted, not incomplete. Its closing sentence
   ("this does not reopen the settled EI mapping") remains true: the EI ledger key is correct.

**Generalize the check.** The two-surface confusion is a class of defect, not one instance. Before
sign-off, every table whose draft column list was derived from a *rendered* sheet should be re-checked
against its JSON. A2 was found this way; `FDCDTL.LineNumber` (report Part 2A) is the same shape.

**Applies to:** `p1-0-schema-mapping.md` ENTITM; `p1-0-design-freeze-summary.md` decision 18.
*Needs Luís to confirm* the render is authoritative for export parity and the JSON for storage.

---

## C3 — Grandfathering: `EnforcementStartsAt` is a Sibyla concept, not the FDR `EffectiveFrom`

**Blocker:** report Item 10, the highest-consequence single defect. Reported as "the column has no
value". That was wrong, and what is underneath is worse.

**Measured at the pin:**

- **DOCEFL already carries `EffectiveFrom`**, populated on all 45 rules, none blank:
  `2026-07-31` ×33, `2026-08-01` ×8, `2026-08-02` ×2, `2026-08-03` ×2. The draft invented a new
  `EnforcementStartsAt` and left it unseeded while the comparand already existed.
- The 29 blocking-open instances, cross-checked against their own rule's `EffectiveFrom`:

  | EFCode | n | EffectiveFrom | DetectedAt range |
  |---|---|---|---|
  | EF0000011 | 17 | 2026-07-31 | 07-31 → 08-02 |
  | EF0000036 | 4 | 2026-08-01 | 08-01 |
  | EF0000006, 14, 32, 05, 09 | 8 | 2026-07-31 | 07-31 |

- **Every instance was detected on or after its rule's `EffectiveFrom`.** Seeding
  `EnforcementStartsAt := EffectiveFrom` grandfathers none of them, so **all 29 block on day one** —
  the go-live queue D7 exists to prevent, and it breaks P1-11's acceptance that actively-blocking
  flags reproduce the prototype's zero.

**Diagnosis.** The rules were authored in the same working week as the detections. `EffectiveFrom`
records **when the rule was written in FDR**, not when enforcement should begin in Sibyla. The draft
would have collapsed two different things into one column.

**Decision:**

1. **`EnforcementStartsAt` is a Sibyla enforcement concept, distinct from `EffectiveFrom`.** Both
   columns exist; neither is derived from the other.
2. **Seed `EnforcementStartsAt` for all 45 imported rules to the Sibyla enforcement-start timestamp**
   (go-live). Result: all 2,711 imported instances are grandfathered, 0 actively blocking on day one,
   matching the prototype and satisfying P1-11 acceptance.
3. **Keep the column per-rule**, so a rule added after go-live carries its own later start. The
   uniform seed is an import decision, not a schema shape.
4. **Preserve `EffectiveFrom` as provenance** — it is real FDR history and answers "when did Luís
   write this rule", which `EnforcementStartsAt` cannot.
5. **`IsGrandfathered` is derived once at import** from the instance's own `DetectedAt` against its
   rule's `EnforcementStartsAt`, and persisted. Note the open issue from the review: `EnforcementStartsAt`
   is mutable while `IsGrandfathered` is persisted-once, so a later edit silently staleness-drifts
   stored verdicts. Either make `EnforcementStartsAt` immutable after import or define a recompute
   trigger — **decide this when C-series reaches the DOCFLG items.**

**State it plainly in the drafts:** grandfathered is not hidden. The 29 stay open, visible, audited
and reviewable; they simply do not gate execution. The distinction between "we have 29 things to
work through" and "the system will not start" is the whole point of D7.

**Applies to:** `p1-0-schema-mapping.md` §9 and DOCFLG/DOCEFL; `p1-0-design-freeze-summary.md`
decision 9. *Needs Luís to confirm* that none of the 29 is something he intends to block on.

---

## C4 — Controlled vocabularies: the pinned data is normative, and the annex is generated

**Blocker:** report A4, reported as one hyphen. It is a pattern: the drafts declared enum literals
from memory instead of reading them from the data. Five domains disagree, two "boolean" columns hold
conditional free text, and one whole state was missing.

**Decision:**

1. **Pinned literals win.** Adopt them verbatim; this preserves the render parity P1-12 acceptance
   depends on. Any value the team later wants to normalize (`Receivable` → `Receivables` is the
   obvious candidate) becomes an explicit listed exception with a migration note, and is raised with
   Luís at source rather than hidden behind an import translation.
2. **The vocabulary annex is generated, not written.** `docs/p1-0-vocabularies.md` is produced by
   script from the pin and is **normative**: every CHECK constraint, enum and domain derives from it.
   A human transcribing domains by hand is how this defect class was created; do not repeat it.
   Regenerate on each new pin and diff — a new literal between pins is exactly the signal the freeze
   exists to catch.
3. **The import fails closed on an unlisted value.** An unknown literal is a finding, not a row to
   coerce.
4. **`Waived` joins the DOCFLG lifecycle as a terminal state**, alongside `Resolved` and
   `Superseded`, with its own waiver evidence and authority. It is on **861 rows — 32% of all DOCFLG
   instances**, not an edge case. Mapping it onto `Resolved` would erase an audit distinction across
   a third of the table.

**Found while generating the annex — six further defects now recorded in it:**
`RECREV` has neither `Recorded` nor `ItemClass`, so the policy clause asserting both over it is
inapplicable twice; severity is three different vocabularies, not one, and one of them is a *when*
ladder rather than a severity ladder; `DOCEFL.AutoActionAllowed` carries conditional free text;
`DOCEFL.AppliesToTable` carries semicolon-joined multi-values that need the same one-row-per-pair
expansion the drafts already mandate for `DOCTYP.OriginClass`; `BNKREC.MatchType` and
`BNKMAT.MatchType` are **disjoint** vocabularies in tables the mapping links by FK; and
`DOCLOG.CaptureQuality` is blank on 1,425 of 1,475 rows while declared non-nullable.

**Applies to:** every draft that names a literal. *Needs Luís to confirm* the `Receivable` singular
(typo or intentional) and the semantics of `Waived` versus `Resolved`.

---

## C5 — Byte-identical resubmission: register the capture event, not a DOCLOG row

**Blocker:** report 2C. Two binding rules, one case, opposite outcomes. Document Archiving Policy:
a byte-identical re-appearance is swept to `_to_delete/` and **"gets no new DOCLOG row."**
Discard/Purge §5: the same event **registers a new capture event**, auto-`Discarded`, with an
Annotation finding.

**Resolution — neither side is amended, because the design already contains the distinction.**

The design separates **capture events** from **DOCLOG rows**: that is exactly what the canonical
`(company, hash, length)` hash record with many capture-event links is for. DOCLOG is
*extraction-derived*, and a suppressed resubmission never runs extraction. So:

1. **Register the capture event**, linked to the canonical hash record. Someone re-sent the file;
   that is real provenance and must survive.
2. **Create no DOCLOG row**, because no extraction occurred. The Archiving Policy stays literally
   true and needs no PCV amendment.
3. **Raise the Annotation finding against the capture event**, not against a DOCLOG row.
4. **Correct the design's own claim** that *"every capture event has its own Document/DOCLOG
   identity."* It is false — and it should be false **by design**, not by oversight. The same
   correction covers the malformed-response path, which the review flagged separately.

**Duplicate identity — decided:** adopt **SHA-256 + byte length**, the byte-exact test the design
assumes throughout. The prototype computes **no content hash of source documents** today; duplicate
detection is filename + size, which can fail in both directions. Adding `hashlib.sha256` at capture
is cheap, is one of the two items the portability review called immediately portable, and removes the
divergence between how the two systems recognise the same event.

**Applies to:** `p1-0-discard-purge-lifecycle.md` §5, §6 and §7; `p1-0-schema-mapping.md` (capture
event / canonical hash). No Engagement Rules document needs amending — record that explicitly, since
the review had flagged this as requiring a PCV pre-flight.

---

## C6 — DOCRQE / RECREV: constrain openness, not the terminal state

**Blocker:** report A5. The draft's check requires `Status='Recorded'` for `Status|Annotation` and
**fails on 639 pinned rows** (637 `Superseded`, 2 `Applied`). None is an anomaly: a Status item is
superseded when the finding underneath it changes — normal engine behaviour.

**Diagnosis.** The anti-phantom-backlog rule is about **being open**: *only Decision items may be
Open*. The draft restated it as a rule about the **terminal state**: *non-Decision must be Recorded*.
That is a stronger claim, and the terminal vocabulary is richer than it assumed. "Cannot be open" was
confused with "must be in one specific state".

**Decision — one constraint, nothing more:**

```sql
CHECK (NOT (Status IN ('Open','InReview') AND ItemClass <> 'Decision'))
```

Verified: 0 violations across all 2,668 DOCRQE rows. The terminal vocabulary is governed by
`docs/p1-0-vocabularies.md` (C4), which is where it belongs now that the annex exists and is
generated. Do not duplicate the terminal list into a cross-field constraint — that duplication is
exactly how this defect was born, and it would break again on the next new terminal state.

**Note for RECREV:** it has neither `ItemClass` nor `Recorded` (C4). This constraint is therefore
DOCRQE-only until the RECREV classification gap is resolved — see the Roles-policy item still to be
worked.

**Applies to:** `p1-0-schema-mapping.md` DOCRQE and RECREV.

---

## C7 — `SourceTextHash`: keep the pinned algorithm, add a discriminator

**Blocker:** report A3. The draft declares `char(64)` lowercase hex; the pinned value is **12 hex
characters on all 2,711 rows** — the prototype's `md5-12` convention. Reported as a column-type
error. It is more dangerous than that.

**What it is for.** `build_docflg.py` computes it from the detection-time context text and uses it as
the **recurrence key** `(EFCode, RelatedRecordID, SourceTextHash)`. Line 273: *"same text already
handled to a terminal state — do not reopen."* The hash is compared **against stored historical
values**.

**Therefore:** recomputing with a different algorithm means no new value matches any old value, and
**all 2,562 terminal instances reopen.** Flags someone already resolved or waived come back open.

**Sizing, for confidence in md5-12:** 48 bits, compared **within** `(EFCode, RelatedRecordID)` rather
than globally. At 2,711 instances the collision probability is on the order of 1e-8, and only between
instances of the same rule against the same record. A recurrence fingerprint is not a security hash —
its job is to match itself over time, not to resist an adversary.

**Decision:**

1. **Keep the pinned algorithm.** Size the column to the real value; import verbatim.
2. **Add a `HashAlgorithm` discriminator** recording `md5-12`.
3. **Comparisons never cross algorithms.** A future upgrade is a deliberate migration that recomputes
   both sides together — no flag day.
4. **Explicitly rejected:** "import verbatim, use SHA-256 for new instances". Unchanged text would
   hash differently depending on when it was detected, and terminal flags would reopen. It looks like
   the best of both and is the worst option available.

**Applies to:** `p1-0-schema-mapping.md` DOCFLG; the same reasoning covers DOCRQE/RECREV `ItemKey`,
which uses the same convention.

---

## C8 — ENTBNK: the key is right, the data is not — and the review's own finding was wrong

**Correction to the review.** Report A6 said multi-account entities are represented as multiple rows
and that the draft's comma-join model "inverts the pinned representation". **That is not supported by
the data and is withdrawn.**

**Measured at the pin:** ENTBNK has 48 rows and 5 fields (`CodeName, Company, Flag, PayAccount,
PayMethod`). The 5 duplicate `(CodeName, Company)` groups differ **only on `Flag`** — same entity,
same company, same `PayAccount`, same `PayMethod`, different review note. They are not different
accounts. Separately, multi-value **does** exist as comma-joined cells — `PayMethod` carries
`"Direct Debit,Bank Transfer"`, and 3 rows have a comma in `PayAccount`. The draft's comma-join model
was correct.

Two real problems remain, neither of them the reported one:

**8a — the 5 duplicate rows are a data defect.** Decision: `(CodeName, Company)` stays as the natural
key. The 5 pairs are merged at source in the prototype, retaining both `Flag` notes. **The import
fails closed until that is done.** A duplicate that differs only in a review note is dirt; absorbing
it would teach the schema to accept it, and keying on review text is the exact defect D2 closed for
LGCode.

**8b — the comma-joined values still have no destination.** The draft names `ENTBNKAccount` and
`ENTBNKMethod` as the normalization target and **never defines a single column for either** — the
review flagged both as "referenced but never defined". Decision: the comma-joined cell stays as-is
for render parity, and `ENTBNKAccount` / `ENTBNKMethod` become **defined child tables**, one row per
account and per method, which are what payment routing reads. Routing must not depend on parsing a
string at use time.

The draft had the right idea twice and finished neither.

**Applies to:** `p1-0-schema-mapping.md` ENTBNK. *Needs Luís* to merge the 5 pairs at source.

---

## C9 — DOCTYP coverage: a scope boundary, one real gap, and a reopened decision

**Blocker:** report A7, reported as "640 DOCLOG rows have no DOCTYP rule". That conflated two
unrelated situations.

**Measured at the pin:**

| Source | Rows | Covered by DOCTYP | Uncovered |
|---|---:|---:|---:|
| `BNK` | 547 | **0** | 547 |
| `FDR` | 817 | 724 | 93 |
| `MET` | 111 | 111 | 0 |

**9a — the 547 BNK rows are a scope boundary, not a gap.** They are `NoDocMov` (366), `Financing`
(125) and `Payroll` (56) — precisely the P/F/O types manufactured by the movement pipeline. **100% of
BNK rows sit outside DOCTYP, consistently:** DOCTYP is a *capture classification* table, and
bank-generated documents are never classified — they are manufactured with their type already
determined. Note DOCTYP has `Payroll-Receipt | Internal` (the payslip that arrives as a document) but
not `Payroll | External` (the document the bank pipeline generates). Different things, similar names.

**Decision:** scope the acceptance check to captured documents (`Source <> 'BNK'`) and **write the
boundary into the mapping**. The draft's universal *"every (DocumentType, OriginClass) has a DOCTYP
rule"* is wrong as stated. Do **not** seed rules for the bank types: it would invent classification
rules for documents that are never classified, and create the expectation that editing them changes
behaviour that actually lives in the pipeline.

**9b — the real gap is 93 rows of `Duplicate | External`, all FDR-sourced.** These *did* pass through
capture and have no rule. It matters: the Document Archiving Policy says a confirmed duplicate that is
a physically distinct file **is archived** and its DOCLOG row **carries an EntryCode** pointing at the
real document. A legitimate captured type with no governance. **Decision:** add `Duplicate | External`
as a real DOCTYP rule with **`Treatment = Include`**.

**Why Include, decided here rather than deferred.** The binding Document Archiving Policy states that
a confirmed duplicate which is a physically distinct file **is archived**, and that its DOCLOG row
**carries an EntryCode pointing at the real document**. `Exclude` would mean it never reaches the
fiscal registry at all — which contradicts the EntryCode those 93 rows already carry. `Include` is the
only value consistent with the behaviour already on disk. Reversible if Luís objects.

**9c — decision 1 of the nineteen is reopened.** It states the seeded type is *exactly*
`Invoice Receipt`. The pinned value is **`Invoice-Receipt`**, hyphenated, on 198 DOCLOG rows and in
DOCTYP itself. Under C4 the pinned literal wins. **Decision:** keep `Invoice-Receipt` and
`Payroll-Receipt` as they are and correct decision 1, rather than migrating real data to gain a space.
The draft's rename of `Payroll-Receipt` → `Payroll` must not happen at all: `Payroll` already exists
and means something else (the bank-generated document).

**Also:** the draft's seed is not a superset of the live table — it drops 8 live types
(`Wrong Document Type`, `Unreadable/Scanned`, `Insufficient Data`, `Cancelled Invoice`, `Tax-TSU`,
`Tax-IRC`, `Tax-IES`, `Tax-VAT`) while claiming it "does not discard specialized existing types". The
seed must be regenerated as a superset of the pinned 19 rows plus `Duplicate`.

**Applies to:** `p1-0-codes-taxonomy-archive.md` §2; `p1-0-schema-mapping.md` DOCTYP/DOCLOG;
`p1-0-design-freeze-summary.md` decision 1.

---

## C10 — `PaymentSequence` belongs on the detail, not the header

**Blocker:** report A9. The draft keys PAYCTR on `(CompanyCode, FiscalDocumentID, PaymentSequence)`,
synthesizing `PaymentSequence = 1` because the prototype has no such field, then gives PAYDTL the same
key plus a composite FK to a PAYCTR alternate key that fixes the sequence at 1. Both constraints
cannot hold once a partial payment exists.

**Measured at the pin:**

| Table | Rows | Distinct FiscalDocumentID | Max rows per FDID |
|---|---:|---:|---:|
| PAYCTR | 482 | 482 | **1** |
| PAYDTL | 195 | 194 | **2** |
| RCVCTR | 245 | 245 | 1 |
| RCVDTL | 161 | 161 | 1 |

None of the four has a `PaymentSequence` field. It is the draft's invention.

**Diagnosis.** The header is **one row per document** — 482 of 482, no exception — so a payment
sequence there is decoration. The detail is **N per document**, and that is where the sequence
belongs. The draft placed it on the header, where it is meaningless, and was then left with no way to
express multiplicity where it actually occurs.

**Decision:**

1. **PAYCTR / RCVCTR:** key `(CompanyCode, FiscalDocumentID)`. Drop `PaymentSequence` entirely.
2. **PAYDTL / RCVDTL:** key `(CompanyCode, FiscalDocumentID, PaymentSequence)`, composite FK to the
   header's corrected key.
3. **Derive the sequence deterministically** from ordered `(PayDate, BankReference, PayAmount)` with an
   explicit tiebreaker. It must be **stable under a shuffled cold rebuild** — the D2 requirement is not
   optional here, because `PayDtlCode` is issued on top of it, and an unstable sequence reproduces
   exactly the PAYCODE/RCVCODE defect Luís just closed.
4. **Port the existing code-stability test to PAYDTL/RCVDTL.** Shuffle, blank, reassign, require zero
   movement.
5. `PayDtlCode` / `RcvDtlCode` — named in the draft with no declared type and no unique index (review
   Part 2A) — are the permanent codes for these rows and must be specified alongside the key.

Exactly one partial-payment case exists today. This is the cheap moment to get it right.

**Applies to:** `p1-0-schema-mapping.md` §6; `p1-0-codes-taxonomy-archive.md` §1 (PAY/RCV buckets).

---

## C11 — DOCFLG rows with no `EFCode`: add a reference row, do not weaken the constraint

**Blocker:** report A10. Four DOCFLG rows carry an empty `EFCode` against the draft's
`EFCode NOT NULL` plus the composite `(EFCode, ItemClass)` FK to DOCEFL.

**What they are.** FL0002653, FL0002654, FL0002655 and FL0002661 — near-consecutive, all detected by
`build_docflg.py` within two minutes on 2026-08-02 (three at 11:35, one at 12:04). All
`Status=Resolved`, `Non-Blocking`, `Informational`, `ResolvedBy=Bot`, with an empty `InstanceNote` and
no required action. **They are empty shells**: no rule, no note, no evidence — and they consumed four
permanent FL codes. Almost certainly a detector defect in one specific run. **Tell Luís separately:
whatever produced four can produce four hundred.**

**Decision: add a sentinel DOCEFL row, do not make `EFCode` nullable.**

`EF0000000` — *"imported instance with no rule attached"*, `ItemClass=Annotation`,
`BlockingLevel=Non-Blocking`.

Rationale: making `EFCode` nullable weakens, across **2,711 rows**, the constraint that guarantees an
instance cannot disagree with the rule that produced it — in order to accommodate **4**. That is the
wrong trade. A sentinel keeps the FK intact on every row, keeps the FL sequence unbroken, and makes
the four **queryable** rather than silently special: anyone can ask how many instances arrived without
a rule instead of discovering it by accident.

Excluding them from the import was the alternative, but it opens FL gaps — and FL sequentiality is
itself an unresolved contradiction still to be worked.

**Applies to:** `p1-0-schema-mapping.md` DOCFLG/DOCEFL; the DOCEFL seed.

---

## C12 — OFDGAP is not a missing table, it is an ungoverned detector

**Blocker:** report A8, reported as "OFDGAP missing from a roster reported complete". True, but adding
it to the roster is the wrong fix.

**What it is.** 27 rows: `Provider (CodeName)`, `EntityName`, `Missing Period`, `InvoiceFrequency`,
`ExpectedInvVal (EUR)`, `Note`. It detects expected monthly invoices that never arrived. Scope is
deliberately narrow — only ENTMST providers with `InvoiceFrequency == "Monthly"` exactly; the
builder's docstring explains that "(assumed)" frequencies come from a single historical data point and
flagging a gap against an unconfirmed assumption would manufacture false positives. Good judgement,
worth preserving.

**The decisive fact is in the builder's own docstring:** OFDGAP is a **derived view with no permanent
row identity**. `GPCode` is reassigned on every render, and it is explicitly excluded from the
permanent-code mechanism — alongside DOCRQE.

**DOCRQE was in exactly that position and D1 closed it**, because a recomputed view cannot carry the
decision that a rejected proposal is never re-proposed. **OFDGAP still has that defect, untouched.** A
gap someone reviewed and dismissed — the provider changed cycle, the invoice arrived inside another
document — **returns on the next run.** It is the phantom backlog again, in the one detector left out
when everything else was tidied up.

**Decision: do not import OFDGAP as a table.** Gap detection becomes what every other v5.0 detection
is — a **DOCEFL rule that declares its `ItemClass` and emits findings** into the flag and queue
machinery, gaining permanent identity and decision memory, and satisfying "every detector declares its
ItemClass before it is added". The OFDGAP sheet is still rendered, **from those findings**, for the
export parity P1-12 acceptance requires.

OFDGAP was never a missing table. It was a detector outside governance.

**Applies to:** `p1-0-schema-mapping.md` (roster note and layer map); the DOCEFL seed; P1-12 render
parity. *Needs Luís to confirm* the Monthly-only scope carries forward unchanged.

---

## C13 — FL: use the key the code actually uses, and drop the sequentiality claim

**Blocker:** report Item 5 (three defects). The numbers resolve all three at once.

**Measured at the pin (2,711 DOCFLG rows):**

| Key | Distinct | Collisions |
|---|---:|---:|
| Draft's `(EFCode, RelatedRecordType, RelatedRecordID)` | 2,633 | **78** |
| Actual, + `SourceTextHash` | 2,708 | **3** |

`build_docflg.py:357` uses `(EFCode, RelatedRecordID, SourceTextHash)`; line 325 confirms the same for
the reconciliation dimension. **The draft transcribed the key incompletely** — same defect class as
everything else in this series.

**Decision:**

1. **FL natural key = `(EFCode, RelatedRecordType, RelatedRecordID, SourceTextHash, DetectedAt)`.**
   `SourceTextHash` removes the 78 collisions; `DetectedAt` is the explicit tiebreaker for the
   remaining 3, mirroring what the bank `SourceKey` does with its recomputed occurrence. If those 3
   turn out to be real duplicates they stay visible for Luís to decide — the tiebreaker does not hide
   them.
2. **The successor problem dissolves.** With `SourceTextHash` in the key, changed source text is a
   *different* key and mints a new code. Under the draft's key the successor was byte-identical to its
   predecessor and `ON CONFLICT DO NOTHING` returned the old code. The intended behaviour was
   available all along.
3. **Drop the sequentiality requirement.** *"FL codes remain sequential permanent identities across
   reruns"* is **already false at the pin**: FL numbers run 1–2852 across 2,711 rows — **141 gaps**,
   zero duplicates. It describes an intention the data never had, and it contradicts the "gaps are
   gravestones" rule the same draft applies to every other bucket. The requirement is **permanent and
   stable**, not sequential. Any acceptance test written against sequentiality fails on the first
   rollback.

**Applies to:** `p1-0-codes-taxonomy-archive.md` §1 (FL bucket); `p1-0-schema-mapping.md` §10 and
DOCFLG. *Needs Luís* on the 3 residual collisions.

---

## C14 — DOCFLG succession: make the invariant honest rather than literal

**Blocker:** report Item 4. `AGENTS.md` non-negotiable: *"A state change closes its predecessor and
identifies its successor."* DOCRQE and RECREV carry `SuccessorReviewID`; **DOCFLG does not — and it is
the lifecycle authority.** The worst table to omit it from.

**Measured at the pin:** 204 `Superseded` instances. **Zero of 204 name an FL code anywhere in their
notes.** What the notes say is:

> *"Superseded: EF0000012 redefined to v2.0 (accepted-outcome semantics); this entity does not meet
> the synthetic-…"*

**They were superseded because the rule changed, not because a successor instance was born.** The
condition stopped applying. There is no successor, and there should not be one.

**This exposes an imprecision in the non-negotiable itself.** "Identifies its successor" assumes every
state change has one. Some do not — the finding vanished because the rule underneath was redefined, or
because someone fixed the data at source. Forcing a successor there manufactures data; leaving the
field silently null makes the invariant unenforceable. Both are bad.

**Decision — make the invariant verifiable instead of literal:**

- `SupersededBy` — nullable FK to DOCFLG
- `SupersedeReason` — closed vocabulary: `ReplacedByInstance` / `RuleRedefined` /
  `ConditionResolvedAtSource`
- `CHECK`: `Status='Superseded'` requires **one of the two populated**

Every supersession then either points at a successor **or states explicitly why there is none. Never
blank.**

The 204 pinned rows import themselves as `RuleRedefined` — which their notes already say in prose. The
correction turns free text into queryable structure rather than inventing data.

**Consider the same treatment for DOCRQE/RECREV**, whose nullable `SuccessorReviewID` has the same
ambiguity today.

**Applies to:** `p1-0-schema-mapping.md` DOCFLG (and the queue tables); `AGENTS.md` wording.

---

## C15 — RelatedParty: the funding classification exists; the field and the rule do not

**Correction to the review.** Report Item 11 said the "classified funding item/movement" the matching
rule depends on "is defined nowhere: no ITMCLS seed row, no item code, no movement class". **That is
wrong and is withdrawn.** ITMCLS already carries `Banks / Financing` and `Revenue / Intercompany`, two
of its 51 real classes, each with its own `CLCode`. The draft did not need to define the
classification — it needed to **reference** it, and left prose where two concrete codes were available.

**What is genuinely missing:**

1. **The field does not exist.** ENTMST's union of fields across all 119 rows is `CodeName, Country,
   CurrencyCode, EntityCode, EntityName, ExpectedInvVal, FiscalNo, Flag, InvoiceFrequency,
   OpenPayableBalance, OpenReceivableBalance, Role` — **no `RelatedParty`**. `Role` holds only
   `Company` (2), `Provider` (75), `Customer` (42).
2. **The DOCEFL rule is missing.** P1-11 names the mechanism explicitly: an Information / Non-Blocking
   DOCEFL rule so new movements are auto-recognised on arrival. Without it, a related party is
   identified by hand once and never again — the "ignored by note" behaviour the backlog exists to end.

**Decision:**

- Add `ENTMST.RelatedParty` (bool) and `RelatedPartyKind`, with the value list **derived from the
  existing ITMCLS classes** rather than invented.
- **Write the DOCEFL rule** — Information, Non-Blocking — against the real `CLCode`s for
  `Banks / Financing` and `Revenue / Intercompany`.
- **The matching rule requires both conditions.** `RelatedParty` alone must never govern: an
  intercompany entity can also be an ordinary supplier, and its ordinary invoices must stay eligible.
  Two `Company` entities exist, so the dual-role case is live, not hypothetical.
- **Rejected:** adding a value to `Role`. `Role` is exclusive and an intercompany entity is also a
  Provider or Customer — that would build the dual-role trap into the model itself.

**Also check while here:** ENTMST carries `OpenPayableBalance`, `OpenReceivableBalance`,
`InvoiceFrequency` and `ExpectedInvVal`. The last two are the input to the gap detector decided in C12;
confirm all four are in the mapping.

**Applies to:** `p1-0-schema-mapping.md` ENTMST; the DOCEFL seed; `p1-0-design-freeze-summary.md`
decision 10.

---

## C16 — Nextcloud archive layout: three defects

**Blocker:** report Item 7.

**16a — Payables and Receivables share a directory and a suffix stream.** Every other class carries a
literal segment (`Bank/`, `Legal/`, `Procurement/`, `Other/`); the fiscal path does not —
`{Company}/{YYYY}/{MM}/{CounterpartyCode}/{ControlledFilename}` — and the filename pattern
`{Company}_{DocumentType}_{Entity}_{YYYYMM}_{NN}` is the same on both sides.

**Measured: 9 `(Entity, Period)` pairs appear on both the payables and receivables sides**, all from
**one** entity that trades in both directions — almost certainly the same intercompany counterparty
behind C15. Nine real month/counterparty combinations where an issued and a received invoice land in
the same directory under the same basename pattern and therefore **share one `NN` suffix stream**. The
generic-layout collapse the SVG cross-check declared superseded, surviving in the one place nobody
looked.

**Decision:** literal class segment — `{Company}/Payables/{YYYY}/{MM}/{Counterparty}/` and
`{Company}/Receivables/{YYYY}/{MM}/{Counterparty}/`. Separate suffix streams by construction, and every
class ends up the same shape.

**16b — the bank path exposes the account number twice.**
`{Company}/Bank/{YYYY}/{MM}/{BankAccount}/{BankAccount}_{YYYYMM}_vNNNN{ext}` contradicts BNKACC's own
rule that account values are tokenized at rest and never logged. A file path is logged everywhere —
logs, backups, Nextcloud indexes.

**Decision:** substitute a controlled BNKACC token in both the directory and the filename. The real
value stays only in the tokenized BNKACC row.

**16c — `ArchiveNameLedger` has two conflicting definitions.** The mapping keys on
`(CompanyId, ContextHash, CaptureIdentity)` and `(CompanyId, ContextHash, Version)`; the codes archive
drops `CompanyId` — and the codes-archive form is the one an implementer would build, because it
accompanies the allocation procedure, and it is the one that lets two companies couple their version
streams.

**Decision:** adopt the mapping's stricter version; correct the codes archive so it stops contradicting
it.

**Applies to:** `p1-0-codes-taxonomy-archive.md` §3; `p1-0-schema-mapping.md` §7 (ArchiveNameLedger,
BNKACC).

---

## C17 — D9 period replacement over a current-state table

**Blocker:** report Item 9. D9 requires period-keyed additive imports where re-importing a period
replaces only that period. The draft added `ImportPeriod` as a column **outside the key**, which
delivers neither behaviour.

**Measured at the pin:** PAYCTR has **no period or import field at all**. It is a pure current-state
table — 482 rows, one per document, `Status` = `Paid` (254), `Overdue` (123), `Paid (Unconfirmed)`
(56), `No Payment Due` (34), `Open` (15). The input is
`Inputs/Payments/Sybila_PAYCTRL_Input_260728.xlsx` — **dated in the filename**, which is precisely
D9's complaint: the prototype reads one fixed name and the next period's file has no ingestion path.

**Diagnosis.** PAYCTR rows are **per-document control state, not per-period events**. An August file
does not bring new August documents; it brings a **fresher snapshot of the same documents**, plus any
that appeared since. The period belongs to provenance, not to the key.

**Decision:**

1. **Key stays `(CompanyCode, FiscalDocumentID)`** (per C10).
2. **`ImportPeriod` and `ImportBatchId` record which file governs each row.**
3. **Replacement is scoped by governing period:** re-importing period P replaces only the rows
   governed by P; every other row is untouched.
4. **Monotonic guard — a row's governing period may only advance.** Re-importing an older file is
   rejected, or applies only to rows not yet touched by a later period.

**Why the guard is not optional.** The dated files accumulate in a folder. When August arrives, July
is still sitting there, and nothing stops someone re-running it. Without the guard that **reverts
payment state** — a document already `Paid` returns to `Overdue`, silently. This is a live operational
shape, not a hypothetical.

**Applies to:** `p1-0-schema-mapping.md` §6 and §9; `p1-0-design-freeze-summary.md` decision 12.

---

## C18 — Invariants stated as prose, with no mechanism

**Blocker:** report Item 4 cluster. Four rules the freeze states and never implements.

### 18a — the reconciliation percentage is not derivable

No draft defines numerator or denominator, and P1-11 acceptance demands reproducing **94.6% ±0.1%**.
Six plausible definitions were computed over BNKREC at the pin:

| Definition | Result |
|---|---|
| matched / (matched + unmatched), by row | 54.3% |
| (matched + internal) / all rows | 58.3% |
| distinct movements with any match / BNKMOV | 55.9% |
| excluding ground-truth matches | 53.4% |
| by bank amount | **47.8%** |
| by row, excluding Internal | 54.3% |

**None approaches 94.6%.** The 865 `Unmatched` rows are genuinely unresolved — all carry a `BMCode`, a
non-zero amount, and the flag *"Unresolved: no bank/document link yet"*. The 94.6% figure is not
claimed to be wrong; it is **not reconstructible from BNKREC**, so the acceptance criterion as written
is unrunnable.

**Decision (Miguel): adopt an explicit definition and re-baseline.**

> **Reconciliation rate = distinct BNKMOV movements carrying at least one non-`Unmatched` BNKREC
> match, over all BNKMOV movements.** Internal transfers count as reconciled — they are matched to
> their counterpart. Per D6, the `Matched — ledger reference, no entry` status (a v5.0 addition, not
> present at the pin) is **excluded from the numerator** once it exists.

**Baseline at `9359c67`: 55.9%** (≈1,096 of 1,960). P1-11 acceptance is restated against this number
and this definition, computed as a view or generated column — never as prose.

**Accepted cost, recorded deliberately.** This abandons the prototype comparison that was the proof
the port did not regress. **Communication risk:** anyone reading both numbers will see 55.9% against
94.6% and conclude the port collapsed. State the definitional difference wherever either figure
appears. Ask Luís for his definition when he returns — not as a blocker, but because if his metric
measures something real over a narrower population, we want both numbers reconciled rather than one
quietly replaced.

### 18b — three invariants get schema objects

- **`ReferenceOnly` barred by CHECK and FK, not by import validation.** D5 requires it be unreachable
  "even by accident"; today a ReferenceOnly document still owns a `Document` row that
  `EvidenceMode=NativeExtracted` can legally anchor.
- **BNKMOV origin anchored by a mandatory FK** to a statement-ingestion or approved-integration
  evidence row, mirroring FDCHDR's four evidence anchors. This is the boundary between "official
  extract" and "anything", and it is the one place the freeze left to a sentence.
- **Rejected proposals get their own constraint table**, consulted by **every** matcher. The prototype
  has `review_constraints.json` but only two matcher passes read it — the non-negotiable is currently
  true for two paths out of several.

**Applies to:** `p1-0-schema-mapping.md` (BNKREC/BNKMOV/DOCLOG/RECREV); P1-11 acceptance.

---

## C19 — Roles and Responsibilities policy: scope it to Sibyla, restore the threshold clause

**Blocker:** report 2B. The policy declares itself the fourteenth binding document and **supersedes**
five named ones. It conflicts materially with **eight** it does not claim — so those conflicts are
unresolved, not resolved. And the supersession authority was **added during adaptation**: the source
draft says this should be a "cross-cutting policy **above**" four procedure families; the adapted text
says "**supersedes** … where they conflict", with no stated basis.

**The eight conflicts share one cause.** Every one exists because the policy says *"only authenticated
deterministic .NET code executes"* and is then applied to a Python prototype that has no .NET at all:

- Code Change Governance maps deployment to **the AI copying a script** into live `Scripts/`
- Entity Entry Flow defines `Flag / Review Notes` as **AI-entered**, and its synthetic-FiscalNo path
  runs under a DOCEFL rule with `ReviewOwnerRole = AI`
- Item Entry Flow has **pipeline auto-creation** of ITMMST items and ENTITM mappings, with
  auto-created rows on file
- Document Archiving Policy is, in its entirety, **the AI running `mkdir` and `mv`**
- `apply_review_decisions.py` **executes** accepted decisions as an AI-run script

The policy was adapted for the production harness and then asserted over the system it came from.

**Decision (19a): the policy is Sibyla-scoped.** Two systems, two legitimate role models — in Sibyla
the AI proposes and .NET executes; in FDR the AI executes under its thirteen rules. The policy
supersedes **within Sibyla**. FDR keeps its documents until and unless it migrates. This dissolves all
eight conflicts without amending anything, and restores the cross-cutting nature the source draft
intended.

**Decision (19b): restore the confidence / consistency / risk threshold clause, and bind it to the
real tables.** It was present twice in the source and dropped in adaptation. It was the **only link**
between this policy and the built threshold layer — BNKMAT `MaxDateWindowDays` and `ToleranceAmount`,
DOCEFL `RiskFactor` and `ReviewPriority`, the FX tolerance, the exact-match bands. Without it,
**changing a tolerance no longer triggers the Policy Change Validation pre-flight** — the class of
change most able to silently widen autonomous action. Restore it *and name what it governs*; a generic
clause is an ignorable one.

**Still open, carried forward:**

- The absolute *"Only Decision-class items may be Open"* drops the Document Entry Review carve-out that
  **human engagement outranks the classifier**. Restore the carve-out.
- The same clause is asserted over RECREV, which has neither `ItemClass` nor `Recorded` (C4). Resolve
  before adoption.
- Three other named review triggers were dropped: duplicate payment, balances, and "an approved closed
  result". Duplicate settlement risk is a Mandatory RECREV Review Condition and a live Block
  Reconciliation rule — restore at least that one.

**Applies to:** `p1-0-user-ai-roles-responsibilities-policy.md`. *Needs Luís* to accept the
Sibyla-scoping, which also means the FDR-side pre-flight he was assigned is **no longer required** for
adoption on our side.
