Stage S1 — Normalization
P Existing-product changes and template sets: S1 may emit the configuration-change contract, selecting only affected stages from a fresh estate export plus the requested delta. It records source/target baselines, skipped-stage reasons and prerequisite evidence bindings. Multiple broker/channel templates are explicit work items, each referencing a separately validated single-template product specification, selector/role scope, effective version and target assertions. The full product schema remains the leaf contract. Case journeys § 5 owns the fan-out/join and release rules.
Purpose
The insurer's material is never a specification: a tariff PDF, a half-filled template workbook, emails that contradict each other, a message on the client surface with "and make it like product X but cheaper". This stage pre-checks the inputs, asks for what is obviously missing — immediately, before any work builds on the gap — and normalizes everything into the one shape the other stage agents understand (the whitelabel specification). Its product is shared understanding: after S1, every later question about "what did the customer mean" has one written answer with a source.
Inputs → Outputs
In: anything, from any connector — files, emails, tickets, the client surface, prior correspondence. Out: the whitelabel specification (product, objects, covers + definitions, factors, tariff section, loadings/discounts/taxes, offers wishes, wiring needs, expected premium examples) — every field citing file/sheet/range or message id — plus the open-questions list (what is missing, why it is needed, where it should come from, blocking or not).
What runs this stage
| Agents | configuration.normalizer |
| Isolated sub-agents | configuration.tariff_analyst (no database connector) · configuration.estate_prober (told nothing about the product) |
| Memory domain it owns | configuration/normalization — the field catalogue, the reading traps, the source-precedence rules (Agents Memory) |
| Skills it runs | product.schema.json + validator · render-product · estate probe (Agents Memory and Skills) |
| Its branch of the graph | Configuration Module § 0 — the module's agent tree, its memory branch and its skills · the component list is Components — Configuration |
Every stage also uses the module's shared profiles — configuration.root (the only one that reaches the human), configuration.verifier (isolated, no author context) and configuration.deployer — and the platform's write_executor and write_auditor, which are what actually touch a target (Gating).
The protocol here
- Gather: connector readers extract every source to text/structure; nothing is interpreted from a file name F: [verify, don't guess].
- Pre-check (the early ask): obvious misses are raised to the human now — a tariff with no legend mapping files to covers, covers named in the tariff but absent from the template, amounts without currency, a referenced document that never arrived. This is the stated purpose of the stage: request the information while it is cheap.
- Apply: the normalizer fills the specification; the pricing half is read by the isolated tariff analyst (no DB context, so storage cannot bend the reading [F: the PC tariff-analyst profile]).
- Test: counts reconciled both ways with evidence — covers across every sheet repeating the list, tariff cells as tables × rows × columns; a mismatch is a stop, not a warning [F: the 9951 layer lesson — the real count was 9 = 3 deductible variants × 3 vehicle groups, both axes unannounced].
- Iterate: every contradiction is a question to the user — which two sources, both quotes, the consequence of each reading — and is never resolved by rule or by precedence [P: decision 01.09.2026]. Contradictions are batched into one question set rather than asked one at a time. The human confirms the specification (gate H1) and answers the open questions; the case parks and resumes losslessly.
P Existing products: mapping to live codes is an S1 step (Configuration Module § 8 E). When the material changes a selling product, the normalizer — not the tariff analyst, which has no database — reads the live catalogue through a read connector and fills live_code / live_match on covers, factors and loadings; a document element with no live match is an unresolved[] entry. The specification may carry several templates (F): tariff.templates[] with the selector each applies to (4704 has 71 broker templates). C The first multi-template product settles the selection and fan-out protocol.
P Personal data is removed at intake, not stopped on (O; CG-9, D57): the connector substitutes an ЕГН pattern and customer names found on the policyholder surfaces with case-local handles before the material is stored, so the case is operator-agnostic; reporter names, staff named as role holders and synthetic identities pass untouched; a census-shaped worked example must be synthetic and is marked so. What the normalizer rejects is material that is personal data (a real census file) — wrong input, not identifiers in the right one.
The whitelabel specification — what this stage produces
The insurer's material is a tariff PDF, a half-filled workbook and a thread of contradicting emails. Four stage agents and five authors cannot each read that material their own way. The whitelabel specification is the one shape all of them read: the semantic model of a product, in business vocabulary, independent of any schema — produced by S1 Normalization, consumed by every stage after it (Configuration Module § 7.1).
It is also the round-trip currency: an existing selling product exports into this shape, and a diff of two specifications is how a configuration run is graded (Configuration Module § 6).
Seed and reference: the PC agent's product-model.json, a 2,305-word spec proven on the 9951 run [F: artifact].
WS-1 Principles
F All five are proven in the PC 9951 run and are load-bearing, not stylistic:
- Business vocabulary, not table names. A cover has a key, names, a type and definitions — not a
BASIC_COVER_ID. Mapping to tables happens at apply time, inside the stage that owns those tables. - Every element cites its source — which file, which sheet, which range, which clause, or which message id. An element without provenance is a guess.
- What is unknown is recorded, not filled in.
unresolved[]is a deliverable, not a failure. - The specification is complete before anything is written. Incomplete is acceptable; silently incomplete is not.
- The schema is product-agnostic (decision Vladimir, 02.09.2026 — D67). It defines the shape of a product specification — sections, counts, cross-references — and never carries product- or family-specific vocabulary: no motor, health or cargo fields, no customer codes. Whatever is specific to a product family is configuration — the field catalogue and the family packs in the customer plug-in (
products.families, Whitelabel Catalogue) — validated against the generic schema, never added to it. Schema versions change the shape only, additively; a new product family is a plug-in change, not a schema change.
WS-2 Representation — JSON is the source, Markdown and HTML are renderings
P (decision 01.09.2026) The specification exists in three forms, and exactly one is authored:
| Form | Role | Authored? |
|---|---|---|
product.json |
the machine contract — what S2–S5 actually read, what the validator checks, what the round-trip diff compares | yes — this is the source of truth |
product.md |
the human-readable rendering reviewed at H1: sections as headings, tariff grids as tables, unresolved[] as a checklist, every field showing its source |
no — generated |
| HTML presentation | the shareable view for the insurer, produced later from the same source | no — generated |
P product.schema.json is published alongside and versioned with the field catalogue. Validation runs at the S1→S2 boundary and blocks H1 (§ WS-4, § WS-5).
Why one authored form: two hand-edited representations of the same product drift, and the drift surfaces as a stage reading one thing while the human approved another. Generating the Markdown means the reviewed document and the consumed document cannot disagree. If a reviewer wants to change something, the change is made in the source and the rendering is regenerated — the Markdown is never edited in place.
F The schema and its validator exist: contracts/product.schema.json and contracts/validate_product.py, with a deliberately broken fixture at contracts/fixtures/broken.product.json that the validator must reject with nine semantic blockers — structural errors are additional when jsonschema is installed (written 02.09.2026; collected_at_quote removed the same day with the POL-only rule). The split is deliberate: the schema checks structure, the validator checks the semantic rules a schema cannot — it recomputes every reconciliation count from the body instead of reading in_model, requires a layer_model whenever the tariff repeats, resolves every cross-reference, and holds H1 open on an unanswered contradiction.
P The renderer is a skill (decision Vladimir, 02.09.2026 — D40): an executable skill render-product owned by the S1 domain, deterministic (no model call), binding the product.json to the customer plug-in's templates and producing product.md and the HTML view; invoked right after the validator. Why a skill and not a platform service or a script: the architecture stays reusable — another customer, another product family, another presentation ships another template set in its plug-in and the platform does not change. The v1 analysis-HTML assets are a template source, not a dependency.
WS-3 Sections
| Section | Holds | Consumed by |
|---|---|---|
meta |
code, names per language, line of business, class, validity, business line, the donor product when cloning, and the source list with each file's role (structure / tariff / requirements / terms) | all |
objects |
insured object codes, status, cardinality | S3 |
covers |
key, names, type, object, order, risks, events, and definitions (limits and deductibles with dimension, range, currency, list) | S3, S4 |
ld |
loadings, discounts, taxes: code, type, sign, scope (product or cover), rate dimension and rate, rate base, combination rule, order, and the rule as written in the source | S2 |
ancillary_premiums |
fixed-amount add-ons that are neither a cover rate nor a loading — assistance cards, breakdown fees: amount and currency, not a rate | S2 |
tariff_constraints |
tariff-level rules that are not themselves a loading — minimum premium, sufficient premium, maximum discount, loading conflicts, calculation order | S2 |
factors |
code, datatype, level (policy or object), required, default, dependency, UI grouping and order, and the value list | S2, S3 |
tariff |
the rating template, versions with effective dates and loading-combination rule, the layer model (§ 4), and cover_rates per cover — shape, base, dimension, grid axes and cells, conditions, min, cap |
S2 |
offers |
packages: code, segment, object, covers with extent (M/R/O) and preset values, restrictions, dependencies, conflicts | S4 |
requirements |
business rules that are not structure — visibility, role gating, process wishes — each with its source, what it maps to, and a confidence. A visibility wish is classified by one question (D138): who is the group — the insured or the seller? the insured → maps_to: offer.segment (S4); the seller or servicing side → maps_to: routes.allowed_roles (S5); a wish naming both → unresolved[] for H1 |
S4 · S5 |
reconciliation |
§ WS-5 | H1 |
unresolved |
§ WS-6 | H1, all |
cover_rules · ld_constraints · documents · annex_types |
schema 1.1 — see § Challenges. There is no insis section: the incumbent side is one script, not configuration (D85, Stage IPAL) |
S4 · S2/S3 · S5 · S3 |
WS-4 The layer model — the field that stops the miscount
Required whenever the source repeats its rate tables under more than one qualifier (§ WS-4). It names the axes explicitly, says what carries each axis in the document, and states what a linear read would miss.
P layer_model.axes is an array of {name, cardinality, carried_by} — not free text. Without the cardinality per axis the layer product is not computable, so the validator cannot check the layer count at all (amendment 01.09.2026).
F The 9951 tariff defeated three different naive readings: the clauses are the layers → 7 (clauses are the grid's row axis); the blocks on page 1 are the layers → 3 (that is one deductible variant's vehicle-group axis); one grid per page → 6 (pages 2, 4 and 6 are loadings and conditions, not rates). The true count is 9 = 3 deductible variants × 3 vehicle groups, and neither axis is announced — the deductible appears only in a table title on pages 1, 3 and 5; the vehicle group only in a merged left-hand column [F: the 9951 layer lesson].
A specification whose tariff repeats and whose layer_model is absent does not pass H1.
WS-5 Reconciliation — mandatory, two-way
P Every count the source claims is matched against the count in the specification, with the evidence range named: covers, loadings/discounts/taxes, factors, tariff layers, rate cells. A mismatch is a stop, not a warning (Stage Normalization).
P The validator recomputes these counts from the document body rather than reading the reconciliation block — a schema can only check that a number is present, and a present-but-wrong number is worse than an absent one. Blocking conditions at H1: schema invalid · a recomputed count disagreeing with its claim · a repeating tariff with no layer_model (§ 4) · a field without a source.
Counting rule F: reconcile the document's covers against distinct cover codes, never row counts — one document cover legitimately becomes N rows, one per object (Stage IPAL).
P One reconciliation runs the other way and is easy to miss: every risk the specification names is a cover to configure, priced or not. A document that describes a peril and never prices it has still described a cover — the tariff's silence is a decision to record, not a licence to drop the row. The check is that the specification's cover set covers the document's risk set, and a cover with no rate leaves S1 as an explicit line rather than as an absence.
WS-6 unresolved[]
P Each entry carries: what is missing · why it is needed (which configuration step it blocks) · where it should come from · blocking or not. Blocking entries hold H1; non-blocking entries travel with the case into the stages and appear in the MISSING paper (Agent Runtime § 11.2).
The early ask is the point: an obvious gap becomes a question before work builds on it, while it is still cheap (Stage Normalization).
WS-7 Provenance and precedence
P Every field carries source as file + sheet + range, clause reference, or message id — and, from schema 1.1, provenance.kind ∈ {spec, system, decision} beside it (D124): spec the customer wrote it, system the estate settled it and the line cites the prober's count, decision a decision taken here and by construction an unresolved[] item. Contradictions are never resolved by rule. Where two sources disagree — informal against formal, or two formal documents against each other — the normalizer records both readings with their quotes and asks the user (decision 01.09.2026). There is no precedence order that decides it automatically, and no source class that silently outranks another (Configuration Details CG-1).
P Tariff reading is verified by double extraction — two different methods, disagreements becoming the suspect-cell list — plus structural checks: monotonic axes, richer cover ≥ thinner cover (Stage Normalization).
WS-8 Reading a document into the shape
P A tariff or requirements document almost never uses the platform's words. The recurring correspondences, from the PC 9951 run [F: the PC 9951 run]:
| The document says | What it is |
|---|---|
| a question with a fixed set of answers | a LIST factor and its values |
| "only if …", "when X is Y, then Z can be …" | a dependency — one row per admissible pair, and a missed pair dead-ends in an empty required dropdown |
| a sum insured, a limit, a coverage amount | the IV family — the scope decision of Stage IPAL § PF-2, not formatting |
| "mandatory", "required" | flat required-ness, which cannot be conditional |
| a table whose rows are keyed by an answer | that answer is a rating axis, and must be a POL row |
| a grouping of values with one rate — "tariff group 3" | a level set on the underlying factor, not a derived factor (Rating Files § 3) |
P ⚠ "Mandatory for commercial vehicles" has no direct configuration: required-ness is flat. It is modelled with the dependency and visibility mechanisms or raised as an unresolved[] entry — never silently made always-required, and never silently made optional.
Facts to hold F
- Never invent — a missing value is an open question, not a plausible guess; an invented value looks finished and prices wrongly.
- The known template traps are catalogued and live in
configuration/ipalmemory (transposed cover sheet, label-matched rows, stale Ref legend,/-ambiguity, questionnaire counting) [source: PCinput-contract, 3,235 measured words]. - Tariff reading is verified by double extraction (two different methods, disagreements are the suspect-cell list) and structural checks (monotonic axes, richer ≥ thinner) [proven checks].
Real material — what live cases asked of this stage
Eight configuration cases were walked against this design (Configuration Module § 8 carries each case in full). What each one demanded here:
CS-1 · 9951 «Каско Максимум» replayed through v2 — The PC agent built 9951 on Ablera dev from a 13-sheet template (Продукт · Тарифни групи · Клаузи · Покрития · Зависимости · Оферти · Правила за премия · ЗС/лимити · Подписвачески въпросник 91 rows · Зависимости между въпроси 65 rows · Такси · Ref) and a 10-page tariff PDF (raздели I–VIII).
Normalizer fills meta (9951, EUR, BG, valid_from 01.08.2026, sources: template=structure, PDF=tariff), objects (CAR, one_only), covers (8 keys, CM_FIRE_NAT COV, seven OPT, definitions from „ЗС, Премийни лимити" — 2 000–500 000 EUR for the four clause covers, fixed 6 000 EUR per seat, 700 EUR replacement), ld (раздели VI–VII), tariff_constraints (мутуално изключващи се клаузи, „при авансов бонус не се прилагат отстъпките по раздел VII" — an ld_conflict), factors (from Q1–Q91: vehicle type → tariff group, seats, engine volume, card series, age band), tariff with the mandatory layer_model (deductible 3 carried by table titles on pp. 1/3/5; vehicle_group 3 carried by a merged left column), offers (five), requirements. The isolated tariff analyst reads the PDF positionally; double extraction flags the orphan 20 %/30 %/10 % cells. Reconciliation: covers 8/8 (Покрития row 2), ld n/n, factors n/n, tariff_layers 9/9, rate_cells 378/378. unresolved[]: the CM_ASSIST card series legend (which series prices which band — Раздел II — по серия на картата names series С and Д but the template does not map series to a factor value list); CM_REPLACE „14 % от базовата премия по избраната клауза" — 14 % of a clause bundle, which needs the BP(<cover>) single-term rule checked. H1 answered by the operator. Validator: schema-valid, counts recomputed, layer model present. [ok] — this is the run the spec was built from.
CS-2 · 2026 EUR tariff on 4710 ГО and 4704 Каско — abacus-only — Bulstrad publishes the 01.01.2026 EUR tariffs (two PDFs in knowledge/sources/).
meta.derived_from_product = 4704; sources: two PDFs = tariff. Covers: the spec must name the existing cover codes the grids attach to (CA_FULL, CA_FR_ND, …) — and the tariff analyst has no DB (Agents § 0; Components — Configuration § 3). [gap E] Stage Normalization says „connector readers extract every source" and „reconcile against what exists" is a PC pipeline stage (extract: „Cover codes … are platform-wide vocabulary. Check before creating any of them"), but S1 has no defined step that maps document covers to live codes; the PC live_code / live_match: exact_name | semantic | absent fields are absent from product.schema.json. The 4704 clause names in the PDF („Пълно каско", „Бонус каско с авансов бонус") map to CA_FULL, CA_FULL_B only through SR_MESSAGES_LNG BG labels (extract: „map covers by translated name, not by code"). Layer model for 4704: deductible 3 × vehicle_class n (леки, автобуси >16, товарни >3.5 т, прикачни, машини) — the PDF is the only source of n, and the knowledge file admits it did not transcribe those. Reconciliation of rate_cells therefore needs the PDF read positionally; 7 clauses × 6 age bands × 3 deductibles × classes. ld: discounts cap 35/45 % (tariff_constraints.discount_prc_max), „САМО ЕДНА отстъпка" on 4710 (ld_conflict), сумарно on 4704; short-term scale = an SR LD type (schema admits SR; Stage Abacus § SUM-3 says PR_BASIC_LD.LD_TYPE has no SR — fine for abacus-only, the IPAL side already exists). ancillary_premiums: ГФ+ОФ вноска, карта „Г" 18.41 EUR, СОП 7.67 EUR/място. Region I–V is not an operator question: required_features[].supplied_by = backend_operation (PH_REGION_CODE) — already registered on 4710, so no S3 step. unresolved[]: which of the 71 templates receive the new grids (all? per-broker deltas?) — blocking; „премиите включват стикера" — is the sticker an ancillary or already in the grid — non-blocking.
CS-3 · HelpDesk 00097807 — 3607 „трудова злополука": new clause, loading, minimum premium — Bulstrad (Зорница Йорданова, 16.03.2026 onward) asks for: (1) +50 % loading when ≤5 insured persons, counted across individually entered persons and list counts; (2) minimum premium 30 EUR per policy, message at proposal level, „без опция да се конвертира в полица"; (3) a new optional clause „Временна загуба на работоспособност от избрана ЗС", limit free up to 3 000 EUR, rate 0.15 % — changed mid-thread by the board to 5 000 EUR and 0.1 %; later (4) all groups on one policy must carry identical covers; (5) the ЗС of the new clause must be identical across groups; (6) the short-term tariff split into ticket 102052; (7) override of the minimum only for named users (Зорница, Валя, Захариев) — later withdrawn to „message + proposal stays proposal, only we may convert".
Contradictions: 3 000 vs 5 000 EUR, 0.15 % vs 0.1 % — two readings from the same source at different dates. § WS-7 above: „never resolved by rule … asks the user". [ok], though the later message is obviously the decision; the rule costs a question. contradictions[] carries both quotes and resolved_by_user. Requirements (4)/(5) are requirements[] entries with maps_to = ? — they are validations, not structure. Requirement (7) maps to a role (LT_USER_ROLES + overrule group), and Stage Serdica's „roles, never users" is exactly right: S5 creates the role; assigning Зорница is the implementer's administration. [ok].
CS-4 · HelpDesk 00098747 — 2214/2215 promo „65 години Булстрад": pay 60 months, get 65 — Home products 2214/2215: for the campaign, term 65 months, premium for 60; everything else unchanged.
tariff_constraints[] kind — none of the five enumerated kinds (premium_min, sufficient_premium, discount_prc_max, ld_conflict, calculation_order) expresses „bonus months". [gap M] the enum is closed (additionalProperties: false) and v0; the page says extensions are amendments to Configuration Module § 7.1 — fine, but the first HelpDesk case already needs one. requirements[]: „valid for the campaign period only" → maps_to = ? A validity window is a version effective_date plus an end that ABACUS versions do not carry (they are superseded, not ended). The DURATION value 65 with DURATION_DIMENSION = M: the four force-included codes (Stage IPAL § PF-5); the value list is PR_PRICING_FACTOR_DEPENDENT rows per dimension — one row per driver value; and CFG_ABC_NUMINST_DURATIONRULE (installments × duration) is an ABC_ACCESS table on the INSIS DB — belongs to no stage.
CS-5 · Product 8000 „Здравна грижа" — packages × levels × options, cover modes, a Camunda process, CG-9 — Group health, EUR, QA only (10.239.82.106), 10 UI covers + 52 level-variant child rows (IP_O/IP_L/IP_E), consumption modes PRV/REIM per cover (PREVENTION and EXTRA_CARE PRV-only), tariff per member × package × level × option with ordered corrections (sector, group size, age band, −5 % commercial), Профилактика individually rated (no rate), Критични заболявания only above 150 persons, claims-side ABACUS templates SRVPRICE# and CLMIND#, BPMN in Core.Plugin.Bulstrad, HealthBasicProductId=8000 hardcoded.
Covers: the document says 10 covers × 3 levels × 3 options; the catalogue has 62 distinct codes. § WS-5 above: „reconcile against distinct cover codes, never row counts" — here the variants are distinct codes, so the document count (10) and the distinct-code count (62) both are right and the rule does not decide. [gap P] the spec has no shape for level variants of a cover (child covers), nor for consumption modes; covers[].definitions cannot express „PRV-only". Two tariff files disagree on the age-band wording — contradictions[] → H1 question [ok]. Профилактика: no rate → open_or_provisional[], version left OPEN, proof level capped [ok] (Stage Abacus facts). Критични заболявания only for groups > 150: a tariff_constraints kind that does not exist (gap M); its rate is per 1 000 EUR of sum insured by age band — rate_dim P with base IV [ok].
CS-6 · Cargo 1101/1102 — a product that transfers to INSIS, with no INSIS structure in the spec — Bulstrad asks to add HSSC (HelpDesk 00102452 „Добавяне на HSSC по всички кодове по Карго") — a new ICC cover across 1100/1101/1102/1103 — and, separately, a new print variant.
Covers: HSSC key, names, type OPT, objects (cargo object), definitions: none (limits are the framework's). tariff: not required by the schema — product.schema.json:7 lists only schema_version, meta, covers, factors, reconciliation, unresolved; what holds is that when tariff is present, cover_rates is required. [gap S closed] What cargo still needs is the stated reason („rating is per-policy data on POL_PREM_RATE") carried in meta.rating_owner, and a reconciliation that does not demand tariff_layers. requirements[]: „HSSC must transfer to INSIS as CARGOHSSC" — maps_to CFG_MAPPING_COVERS. [gap L closed by D85] the seam rows and the INSIS clause × perils (CFG_GEN_COVERS) are the one PL/SQL script run once per product, a named deliverable with an owner rather than a stage (Stage IPAL § The INSIS side).
Challenges
- P The whitelabel field catalog v1: seed from the PC
product-model.json+ its named gaps; extend on the first real inputs. - P This is v0. Schema version
1(schema_versionis aconst) is the v0 contract; the 1.1 additions below are proposals until the first real input set (T2). The field catalogue is the PC model plus the sections the estate's own products need; it is extended on the first real inputs, and every extension is an amendment to Configuration Module because four stages read it. - P Known shape gaps of v0 (Configuration Module § 8 D, M, P, S): conflict groups; LBRR object scoping;
LB_COVERS_DEFdimension and currency; loading rating file / formula / cap / per-LD combination rule;ld_constraints; top-levellimit_restrictions;documents;annex_types;tariff_constraints.kindlacks bonus-term and group-size gating; level-variant child covers and consumption modes (product 8000); nothing in the schema records who rates a product, so a cargo specification with notariffsection cannot say why it has none. P Schema 1.1, additive (Challenge Rounds § R2 § 8) — every addition is a product-agnostic shape (principle 5, D67); family vocabulary stays in the plug-in's field catalogue:cover_rules {dependencies[], conflicts[], limit_restrictions[]}each carryingobject(theLBRR_*tables key on it);offers[].covers[].values[]withdim,currencyand arule(LB_COVERS_DEF+_RULES);ld[]gainsrating {kind: flat|grid|formula, file, formula, grid},cap,applies_after; top-levelld_constraints[];documents[](engine, template, languages, stage, gates,insis_rep_id→CFG_PRINT_DOCS,CFG_PRINT_DOC*);annex_types[]withchange_sensitive_factors[]→PR_PRICING_FACTOR_ANNEX;tariff_constraints.kindbecomes an open string withparams(known:bonus_term,group_size_gate,min_persons_loading,age_gate);covers[].variants[](level O/L/E) andconsumption_modeswith a second reconciliation count for parents;meta.rating_owner ∈ {abacus, insis, policy_data, none}— thetariffsection is expected iffabacus(cargo =policy_data);factors[].collected_at_quoteremoved (POL only). The validator cross-references every new key againstcovers[],ld[],factors[]; the broken fixture gains one violation per rule.