# P1-0 — controlled vocabulary annex

**Normative.** The table below is **generated in full from the Stage 11 prototype pin
`4ea2e67819535be9c37ce7d3644d018058e41c35`** — every row, every table, not a partial hand edit
over an older generation. Where a P1-0 draft declares a literal that disagrees with this table,
**this table wins** and the draft is corrected. Any value the team decides to normalize instead
becomes an explicit listed exception with a migration note — never a silent rewrite.

**Provenance, stated exactly.** The 2026-08-05 annex was generated at `9359c67` (re-confirmed at
`6146004`); the 2026-08-06 O8-D1 pass regenerated **only** the DOCEFL rows from the immutable
P1-1b pin `b91768513fc638381fbde91f0b576b08220a98f6`; the 2026-08-21 pass re-measured **only four**
DOCEFL rows at Stage 11. Three provenances in one table is exactly how a literal goes missing —
see findings **F-12** to **F-16**. As of 2026-08-22 there is **one** provenance: every row is
Stage 11.

## How it is generated

```powershell
python scripts/generate-vocabulary-annex.py `
  --prototype D:\fileStorage\repos\invoice-skill-build `
  --pin 4ea2e67819535be9c37ce7d3644d018058e41c35 `
  --check docs/p1-0-vocabularies.md
```

`scripts/generate-vocabulary-annex.py` reads the declared annex sources **through `git show` at
the exact pin**, never from the prototype's mutable working tree, and aggregates only the
(table, field) pairs declared in its `SOURCES` map:

| Source file | Table | Fields |
|---|---|---|
| `data.json` → `document_log` | DOCLOG | Source · CaptureQuality · DocumentType · OriginClass |
| `document_type_rules.json` | DOCTYP | DocClass · OriginClass · Treatment |
| `flag_evaluation.json` | DOCEFL | BlockingLevel · FlagCategory · ReviewPriority · ReviewOwnerRole · AutoActionAllowed · AutoReviewable · Active · ResolutionEvidenceRequired · FlowDimension · AppliesToTable · Relevancy |
| `flag_instances.json` | DOCFLG | Status · BlockingLevel · ReviewPriority · ReviewOwnerRole · RelatedRecordType · DetectedBy |
| `docrqe.json` | DOCRQE | ItemClass · Status · Priority · UserDecision · SourceRecordType |
| `recrev.json` | RECREV | Status · Priority · UserDecision |
| `bnkmat.json` | BNKMAT | MatchType |
| `bnkrec.json` | BNKREC | MatchType |
| `bnkmov.json` | BNKMOV | Classified · Currency |

It emits value→count tallies and nothing else: no record, no identifier, no fiscal number and no
free-text source value reaches the output. `ResolutionEvidenceRequired` is declared free-text, so
it contributes a distinct count and no values at all. **Distinct excludes blank, null and
field-absent**; blanks and absences are reported separately in the same cell.

`--compare-pin <sha>` aggregates a second commit and emits the mechanical pin-to-pin findings
table — the diff AGENTS.md requires at every new pin, so that a new literal is a finding rather
than a silent widening.

**Regenerate this file from the pin rather than editing it.** The generated block is delimited by
`<!-- BEGIN/END GENERATED p1-0-vocabularies:pinned-domains -->` markers; `--check` proves the block
between them is byte-identical to generator output.

> **Verification status, 2026-08-22 — stated exactly.** The figures below were measured against
> the pin by an independent tally of the same blobs, and every table reconciles internally (each
> field of a table sums to the same record total) and against the Aug-6 record (DOCEFL 52 → 64
> reconciles exactly, field by field — see the findings). **The generator's `--check` has since
> been run and succeeded**, twice against the exact Stage 11 pin `4ea2e67`: once as the plain
> `--check` above and once with `--compare-pin` added. Both runs reported the generated block
> byte-identical to generator output, and the repeated outputs were byte-identical to each other,
> so the renderer is deterministic on this input. Nothing about the renderer is outstanding. The
> XML and link checks were run separately and are not part of this result.

## Pinned domains

Counts are as at pin `4ea2e67` (Stage 11, 2026-08-21). Record totals implied by the tallies:
DOCLOG 1,675 · DOCTYP 20 · DOCEFL 64 · DOCFLG 3,621 · DOCRQE 2,838 · RECREV 1,381 · BNKMAT 11 ·
BNKREC 2,152 · BNKMOV 2,054.

<!-- BEGIN GENERATED p1-0-vocabularies:pinned-domains -->
| Table | Field | Distinct | Pinned values (verbatim) |
|---|---|---:|---|
| DOCLOG | Source | 3 | `BNK` (700) · `FDR` (871) · `MET` (104) |
| DOCLOG | CaptureQuality | 3 | `Accepted` (6) · `Rejected` (42) · `Review Required` (2) · *blank on 4* · *field missing on 1621* |
| DOCLOG | DocumentType | 11 | `Bank Statement` (94) · `Cancelled Invoice` (10) · `Credit Note` (16) · `Duplicate` (111) · `Financing` (125) · `Invoice` (624) · `Invoice-Receipt` (203) · `NoDocMov` (423) · `Payment Notice` (5) · `Payroll` (58) · `Wrong Document Type` (6) |
| DOCLOG | OriginClass | 2 | `External` (1397) · `Internal` (278) |
| DOCTYP | DocClass | 2 | `Payables` (12) · `Receivable` (7) · *blank on 1* |
| DOCTYP | OriginClass | 2 | `External` (12) · `Internal` (8) |
| DOCTYP | Treatment | 2 | `Exclude` (7) · `Include` (13) |
| DOCEFL | BlockingLevel | 5 | `Block Classification` (4) · `Block Document` (1) · `Block Financial Entry` (9) · `Block Reconciliation` (4) · `Non-Blocking` (46) |
| DOCEFL | FlagCategory | 4 | `High Risk` (12) · `Information` (21) · `Low Risk` (18) · `Medium Risk` (13) |
| DOCEFL | ReviewPriority | 6 | `Before Posting` (18) · `High` (1) · `Immediate` (4) · `Informational` (21) · `Periodic Review` (14) · `Routine` (6) |
| DOCEFL | ReviewOwnerRole | 5 | `AI` (28) · `Accountant` (1) · `Finance Reviewer` (24) · `Master-Data Reviewer` (8) · `Process Owner` (3) |
| DOCEFL | AutoActionAllowed | 3 | `No` (35) · `Yes` (28) · `Yes (provided all Entity Entry Flow requirements are verified)` (1) |
| DOCEFL | AutoReviewable | 2 | `No` (32) · `Yes` (32) |
| DOCEFL | Active | 1 | `Yes` (64) |
| DOCEFL | ResolutionEvidenceRequired | 54 | *free-text / high-cardinality — not a controlled domain; no value is reproduced here* |
| DOCEFL | FlowDimension | 5 | `Bank Reconciliation` (9) · `Document Archive` (2) · `Document Entry` (1) · `Document Registry` (30) · `Entity Item Classification` (22) |
| DOCEFL | AppliesToTable | 15 | `BNKMOV` (9) · `BNKMOV; FDCHDR; BNKREC` (1) · `DOCLOG` (13) · `DOCLOG; DOCFAI` (1) · `DOCLOG; FDCHDR` (11) · `DOCLOG; FDCHDR; BNKMOV` (1) · `DOCLOG; FDCHDR; FDCDTL` (2) · `DOCLOG; FDCHDR; PAYCTR` (1) · `ENTMST` (14) · `ENTMST; ENTALS` (4) · `ENTMST; FDCHDR` (1) · `FDCHDR` (1) · `FDCHDR; ENTMST` (1) · `ITMMST` (3) · `ITMMST; ITMCLS` (1) |
| DOCEFL | Relevancy | 2 | `Consultive` (40) · `Important` (24) |
| DOCFLG | Status | 5 | `Open` (132) · `Rejected` (23) · `Resolved` (2228) · `Superseded` (377) · `Waived` (861) |
| DOCFLG | BlockingLevel | 4 | `Block Classification` (70) · `Block Financial Entry` (181) · `Block Reconciliation` (140) · `Non-Blocking` (3230) |
| DOCFLG | ReviewPriority | 5 | `Before Posting` (388) · `Immediate` (14) · `Informational` (1705) · `Periodic Review` (1372) · `Routine` (141) · *blank on 1* |
| DOCFLG | ReviewOwnerRole | 5 | `AI` (1891) · `Accountant` (7) · `Finance Reviewer` (1576) · `Master-Data Reviewer` (103) · `Process Owner` (43) · *blank on 1* |
| DOCFLG | RelatedRecordType | 4 | `BNKMOV` (1299) · `DOCLOG` (2030) · `ENTMST` (252) · `ITMMST` (40) |
| DOCFLG | DetectedBy | 6 | `build_docflg.py` (757) · `build_docflg.py (Stage 10 Round 4 reconciliation detector)` (1299) · `build_docflg.py (Stage 10 Round 8 gap detector, roadmap 108)` (31) · `build_docflg.py (Stage 10 Round 9 dimension detectors, roadmap 124/130)` (263) · `build_docflg.py (Stage 9 Round 1 backfill)` (1255) · `fix_stage9_round2_entity_fiscalno_migration.py` (16) |
| DOCRQE | ItemClass | 3 | `Annotation` (1347) · `Decision` (136) · `Status` (1355) |
| DOCRQE | Status | 5 | `Applied` (51) · `Decided` (16) · `Open` (31) · `Recorded` (1711) · `Superseded` (1029) |
| DOCRQE | Priority | 5 | `Critical` (20) · `High` (63) · `Information` (2) · `Low` (1148) · `Normal` (1605) |
| DOCRQE | UserDecision | 4 | `Accept` (29) · `Accept with Changes` (3) · `Defer` (12) · `Reject` (23) · *blank on 2771* |
| DOCRQE | SourceRecordType | 14 | `BNKMOV` (111) · `BNKREC` (70) · `DOCFLG` (120) · `DOCLOG` (23) · `ENTBLC` (205) · `ENTITM` (11) · `ENTMST` (100) · `FDCDTL` (391) · `FDCHDR` (655) · `ITMMST` (2) · `PAYCTR` (535) · `PAYDTL` (105) · `RCVCTR` (365) · `RCVDTL` (145) |
| RECREV | Status | 4 | `Applied` (919) · `Decided` (22) · `Open` (79) · `Superseded` (361) |
| RECREV | Priority | 4 | `Critical` (136) · `High` (473) · `Low` (447) · `Normal` (325) |
| RECREV | UserDecision | 4 | `Accept` (885) · `Accept with Changes` (38) · `Accepted` (10) · `Defer` (8) · *blank on 440* |
| BNKMAT | MatchType | 11 | `Auto-Created Document - Many-to-One` (1) · `Auto-Created Document - One-to-One` (1) · `Counterparty Balance Settlement` (1) · `Exact Document Match` (1) · `FX-Tolerant Document Match` (1) · `FX-Tolerant Multi-Document Match` (1) · `Internal Transfer` (1) · `Multiple Movements to One Document` (1) · `One Movement to Multiple Documents` (1) · `Payment Gateway Payout Reconciliation` (1) · `Time-Windowed Multiple Movements to One Document` (1) |
| BNKREC | MatchType | 13 | `Internal` (201) · `Matched` (771) · `Matched (FIFO inferred)` (13) · `Matched (FX-tolerant)` (1) · `Matched (approx)` (9) · `Matched (balance netted)` (136) · `Matched (payment control ground truth)` (54) · `Matched (payout reconciliation)` (82) · `Matched (period aggregate)` (8) · `Matched (receivable control ground truth)` (11) · `Matched (review decision)` (33) · `Matched (vendor statement)` (2) · `Unmatched` (831) |
| BNKMOV | Classified | 1 | `true` (2054) · *(JSON scalar, not a string)* |
| BNKMOV | Currency | 2 | `EUR` (2033) · `USD` (21) |
<!-- END GENERATED p1-0-vocabularies:pinned-domains -->

## The six DOCEFL fields are settled — no rule is blank

The 2026-08-21 re-measure stopped after four DOCEFL rows (`ReviewOwnerRole`, `FlowDimension`,
`AppliesToTable`, `Relevancy`), leaving six rows still summing to the Aug-6 total of 52. That left
an open question: were `BlockingLevel`, `FlagCategory`, `ReviewPriority`, `AutoActionAllowed`,
`AutoReviewable` and `Active` genuinely **blank** on the twelve Stage 11 rules
(`EF0000053`–`EF0000064`), or was the re-measure simply partial?

**It was partial.** Measured at `4ea2e67`: the source holds **64 `EFCode` values, zero blank**, and
each of the six fields tallies to **exactly 64 non-blank values with zero blanks and zero absences**.
All twelve Stage 11 rules carry all six. Every DOCEFL row in the table above now sums to 64.

The Aug-6 → Stage 11 increments reconcile field by field, which is what makes the claim checkable
rather than asserted:

| Field | Aug-6 (`b917685`) | Stage 11 | How the +12 lands |
|---|---|---|---|
| `BlockingLevel` | 52 | 64 | `Block Financial Entry` +1 (`EF0000053`); `Non-Blocking` +11 for the other new rules, **+1 more** because `EF0000019` moved off `Block Classification` (5 → 4, 34 → 46) |
| `FlagCategory` | 52 | 64 | `High Risk` +1 · `Information` +4 · `Low Risk` +5 · `Medium Risk` +2 |
| `ReviewPriority` | 52 | 64 | `Before Posting` +2 · `Informational` +4 · `Periodic Review` +2 · `Routine` +4 |
| `AutoActionAllowed` | 52 | 64 | `No` +10 · `Yes` +2 |
| `AutoReviewable` | 52 | 64 | `No` +8 · `Yes` +4 |
| `Active` | 52 | 64 | `Yes` +12 |

None of the six gained a literal. The CHECK constraints those six rows generate are unchanged by
Stage 11; only the populations moved.

## Pin-to-pin findings — `b917685` (Aug-6) → `4ea2e67` (Stage 11)

A new literal between pins is a finding, never a silent widening. Findings are grouped by what
they actually are, because "absent from the previous annex" and "absent from the previous source"
are different facts and were being conflated.

### A — new at the source (Stage 11 widened the vocabulary)

| # | Table.Field | Literal(s) | Count | Note |
|---|---|---|---:|---|
| **F-1** | `DOCEFL.ReviewOwnerRole` | `Accountant` | 1 | CHECK widened by `20260821103837_S2Stage11RuleVocabulary` on DOCEFL **and** DOCFLG |
| **F-2** | `DOCEFL.FlowDimension` | `Document Archive` | 2 | `EF0000061`, `EF0000062` — the archive dimension |
| **F-3** | `DOCEFL.AppliesToTable` | `ENTMST; FDCHDR` · `ITMMST` · `ITMMST; ITMCLS` | 1 · 3 · 1 | 12 shapes → 15. `ITMMST` is the first item-master target the catalogue has ever carried |
| **F-4** | `DOCEFL.Relevancy` | `Consultive` · `Important` | 40 · 24 | **The whole field is new.** Governed by the parser at `!135` and not persisted — no DOCEFL column exists. See the S2-6 (c) decision |
| **F-5** | `DOCEFL.BlockingLevel` | — *(no new literal)* | — | `EF0000019` moved `Block Classification` → `Non-Blocking`. A **behaviour** change with no vocabulary change: the kind of drift a literals-only diff would miss |

### B — present at Stage 11, absent from the previous annex

These are new relative to the annex. The Aug-6 measurement supplied with the 2026-08-22 pass
classifies them as genuine source additions rather than annex gaps; the `--compare-pin` run is
what re-derives that mechanically.

| # | Table.Field | Literal(s) | Count |
|---|---|---|---:|
| **F-6** | `DOCFLG.Status` | `Rejected` | 23 |
| **F-7** | `DOCFLG.ReviewOwnerRole` | `Accountant` | 7 |
| **F-8** | `DOCFLG.RelatedRecordType` | `ITMMST` | 40 |
| **F-9** | `DOCFLG.DetectedBy` | `build_docflg.py (Stage 10 Round 8 gap detector, roadmap 108)` · `build_docflg.py (Stage 10 Round 9 dimension detectors, roadmap 124/130)` | 31 · 263 |
| **F-10** | `DOCRQE.Status` / `DOCRQE.UserDecision` | `Decided` · `Defer` · `Reject` | 16 · 12 · 23 |
| **F-11** | `RECREV.Status` / `RECREV.UserDecision` | `Decided` · `Accepted` · `Defer` | 22 · 10 · 8 |

**F-8 is the one with a consequence beyond the CHECK.** `DOCFLG.RelatedRecordType = ITMMST` is not
theoretical: 40 instances carry it, all four item-master rules emit it, and 16 + 2 + 15 + 7 = 40
exactly. The decision is recorded as **M-D12** in `master-data-and-ui-plan.md`.

**F-11 carries a data-quality note.** `RECREV.UserDecision` holds **both** `Accept` (885) and
`Accepted` (10) — two spellings of one decision. They are preserved verbatim here, as the annex
always does; normalizing them is a listed exception with a migration note, never a silent rewrite.

### C — prior annex incompleteness, not a source change

The annex's non-DOCEFL rows were last generated at `9359c67` and were never regenerated at the
Aug-6 pin `b917685`, so literals that already existed at Aug-6 never entered the table. That is an
annex defect, and it is exactly the failure the "regenerate and diff at each pin" rule exists to
prevent — three provenances in one table let three literals hide.

| # | What | Literal | Stage 11 count |
|---|---|---|---:|
| **F-12** | `DOCLOG.DocumentType` was missing a literal | `Bank Statement` | 94 |
| **F-13** | `DOCFLG.ReviewPriority` was missing a literal | `Routine` | 141 |
| **F-14** | `BNKREC.MatchType` was missing a literal | `Matched (vendor statement)` | 2 |
| **F-15** | The `AppliesToTable` note named **"the last three joined shapes"** by sort position, which catches `FDCHDR; ENTMST` — a **pre-existing** shape — and misses `ENTMST; FDCHDR`, which is one of the genuinely new ones. Corrected by **F-3** | — | — |
| **F-16** | `BNKMOV.Classified` was rendered `True`. The source holds the JSON boolean `true`; `True` was a Python-repr artifact of the earlier generator. The generator now renders JSON scalars as JSON and marks them | `true` | 2054 |

**F-13 has a governance consequence.** O9-D4 replaced a five-literal `DOCEFL.ReviewPriority` CHECK
with the six-literal table-derived set because the prose had dropped `Routine`. The *same* literal
was simultaneously missing from the `DOCFLG.ReviewPriority` row of this annex, on 141 instances.
The prose defect was found and fixed; the table defect beside it was not, because nothing
regenerated that row.

## What this table changes

1. **`Non-Blocking` is hyphenated.** Every CHECK constraint and the blocking predicate in the drafts
   use `NonBlocking`. Against pinned data every such predicate is false — total enforcement failure
   from one character.
2. **`DOCFLG.Status` has terminal states no draft mentions.** `Waived` on 861 rows — a real business
   act (someone dispensed with a flag), and at 24% of 3,621 instances it is not rare — and, new at
   Stage 11, `Rejected` on 23. Decided: both join the lifecycle as terminal states alongside
   `Resolved` and `Superseded`, each with its own evidence and authority. Mapping either onto
   `Resolved` would erase an audit distinction.
3. **`RECREV` has no `Recorded` status and no `ItemClass` field.** The Roles policy asserts
   "Status and Annotation items are retained as `Recorded`" over RECREV. That sentence is
   inapplicable twice over.
4. **`DOCTYP.DocClass` is `Receivable`, singular** — the drafts declare `Receivables` — and one row
   of twenty carries it **blank**. That blank row is the `Bank Statement` type coded `DT000020` at
   Stage 11, and it is the shape the parser's blank-`DocClass` fingerprinted exclusion exists for.
5. **Severity is not one vocabulary, it is three.** `DOCRQE.Priority` is
   Critical/High/Normal/Low/Information. `DOCEFL.ReviewPriority` has six exact literals:
   `Before Posting`, `High`, `Immediate`, `Informational`, `Periodic Review`, `Routine`;
   `DOCFLG.ReviewPriority` contains the **five-literal** subset shown above — five, not the four
   the annex used to show, because `Routine` was missing (F-13). This is a *when* ladder, not a
   severity ladder. The v5.0 backlog declares a third vocabulary. They must not be modelled as one
   column type. O9-D4's replacement CHECK is exactly the six DOCEFL literals, with no translation
   and no vocabulary change.
6. **`DOCEFL.AutoActionAllowed` holds conditional free text** —
   `Yes (provided all Entity Entry Flow requirements are verified)` — in a column the drafts treat as
   boolean. Same defect class as `DOCTYP.FiscalEligible`; both need a separate condition field.
7. **`DOCEFL.AppliesToTable` holds semicolon-joined multi-values** (`DOCLOG; FDCHDR`,
   `ITMMST; ITMCLS`). The drafts explicitly forbid multi-valued cells for `DOCTYP.OriginClass`
   and did not notice the same shape here. It needs the same one-row-per-pair expansion — and note
   that `ENTMST; FDCHDR` and `FDCHDR; ENTMST` are **two distinct strings for one pair**, which is
   what makes the expansion a correctness fix rather than tidiness.
8. **`BNKREC.MatchType` and `BNKMAT.MatchType` are disjoint vocabularies.** BNKMAT governs 11 named
   match types (`Exact Document Match`, `Internal Transfer`, …); BNKREC records 13 different strings
   (`Matched (FIFO inferred)`, `Matched (balance netted)`, `Unmatched`, …). The mapping draft gives
   BNKREC both a free-text `MatchType` and an FK to BNKMAT with no consistency rule. They are
   measuring different things and need different column names before either is constrained.
9. **Two columns are single-valued today** — `DOCEFL.Active` (`Yes` ×64) and `BNKMOV.Classified`
   (`true` ×2054). Neither has an observed negative case, so neither has a tested false branch.
10. **`DOCLOG.CaptureQuality` is populated on 50 rows of 1,675** while the mapping declares it
    non-nullable — and the shape of the gap changed at Stage 11: the property is now **absent
    from the record entirely on 1,621 rows** and present-but-blank on 4. A NOT NULL column
    modelled on this field would reject 97% of the source.
11. **`ITMMST` is a first-class flag subject.** Stage 11 gives DOCEFL four item-master rules and
    DOCFLG 40 `RelatedRecordType = ITMMST` instances. The previous annex's note that "no DOCFLG
    `RelatedRecordType` exists for ITMMST yet" was true of `9359c67` and is false of Stage 11.

## How to use it

- Derive every `CHECK` constraint and every enum/domain from this file, generated, not hand-written.
- The import validates against it and fails closed on an unlisted value — an unknown literal is a
  finding, not a row to coerce.
- Regenerate on each new prototype pin **with `--compare-pin` against the previous one**, and
  record the findings. A new literal appearing between pins is exactly the signal the freeze exists
  to catch. Regenerating *part* of the table is how F-12…F-16 happened; the generator emits the
  whole block or none of it.

## Annex history

- **2026-08-05 — initial generated annex:** pinned prototype domains at `9359c67` established the
  normative literal source. The `DOCEFL.ReviewPriority` table was authoritative, but explanatory
  prose mistakenly named only five literals and omitted `Routine`.
- **2026-08-06 — O8-D1 regeneration:** the **DOCEFL rows only** were regenerated from immutable pin
  `b917685`; the table measured the same exact six-literal `ReviewPriority` set. Every other row
  stayed at `9359c67` — the split provenance that produced F-12…F-14.
- **2026-08-06 — O9-D4 reconciliation:** the prose/table defect is recorded as history and the
  accepted five-literal CHECK is identified as the defective artifact. The governed decision is to
  replace it with the table-derived six-literal set. This is CHECK completion, not an annex or
  source-vocabulary change; the pinned `Routine` rows import verbatim after the corrective
  migration is implemented.
- **2026-08-21 — partial Stage 11 re-measure:** four DOCEFL rows (`ReviewOwnerRole`,
  `FlowDimension`, `AppliesToTable`, `Relevancy`) were re-measured at `4ea2e67` with the `!135`
  source alignment; the other six DOCEFL rows and every non-DOCEFL row were left as they stood.
  That is what produced the "either those six are blank on twelve rules or the re-measure was
  partial" question carried in `docs/index.html`.
- **2026-08-22 — full Stage 11 regeneration.** The whole table is generated from `4ea2e67` by
  `scripts/generate-vocabulary-annex.py`, so the annex has **one** provenance for the first time.
  The open question is closed: the re-measure had been partial, and all six fields are non-blank on
  all 64 rules. Findings **F-1**…**F-16** record every literal that moved, separating source
  additions (A), literals new to the annex (B) and prior annex incompleteness (C). The generator's
  `--check` was then run twice against the same pin — plain, and with `--compare-pin` — and
  **succeeded both times**, with byte-identical repeat output; see the verification status note
  above. No generator verification remains outstanding.
