☰ Contents
AISA v2.0 / Technical documentation / Stage S2 — Abacus

Stage S2 — Abacus

F verified factP decided planC open challenge

Purpose

P Multiple templates: CF-05 in Case journeys supplies explicit template work items through the configuration-change contract. Run bounded instances of the existing stage profile, each pinned to template/selector/effective-version/target and a validated leaf specification. Join results by the declared key set; each requires rating proof, intended caller selection, negative/default caller checks and actual target version. Compilation success alone is insufficient. Cache invalidation remains a declared connector effect.

The insurer's tariff documents describe how the premium should be computed; this stage understands those tariffs and creates the product's ratings — then proves the understanding by pricing. Input: the tariff document(s) (through the normalized specification). Test: the getRates skill until the engine's answers match the documents. Output: working rating configuration on the environment plus the configuration summary — human-readable text and the structured list of factors, covers, loadings, discounts, and taxes — which is exactly what the IPAL stage consumes next.

Inputs → Outputs

In: the whitelabel specification's tariff section (from S1), with expected premium examples computed from the documents. Out: deployed rating versions on the working environment (compiled and DEPLOYED on the working environment only, in no deployment set — D113); the configuration summary (text + factors/covers/loadings/discounts/taxes tables with their bases, dimensions, orders, and combination rules); the stage's deployment script set.

What runs this stage

Agents configuration.abacus.stage
Isolated sub-agents configuration.verifier for the phase-5 and phase-10 verdict
Memory domain it owns configuration/abacus — grid shapes, layer arithmetic, what a rule can and cannot hold (Agents Memory)
Skills it runs abacus configure (the write path) · getRates test · abacus seed at the S2→S3 boundary (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

The configuration summary — what this stage signs out

S2 Abacus makes the product price. S3 IPAL makes it exist as a catalogue an operator can sell. Between them sits the configuration summary, defined in § SUM-1…SUM-6 below: the artefact that lets the S2 and S3 authors work in parallel without guessing at each other.

The shape of the problem, established from code [F, verified 01.09.2026]: the IPAL↔ABACUS seam is one-way. sync_products.py:25 pulls IPAL → PPA, and PasSrdSyncService/App/AutoMapper/ServiceProfile.cs fixes exactly what crosses — factors filtered SaleStage == "POL" (:43), cover name set to cover code (:58), datatypes rewritten (:18-19). IPAL is master and nothing returns.

So this summary is precisely the complement: what S2 learned or decided that the sync will never carry back, and that S3 cannot re-derive from the whitelabel specification.

P ⚠ And it is read by a skill, not by a person or an agent typing rows. The abacus seed skill projects it into IPAL catalogue proposals at the boundary; hand entry is not a fallback, and a skill that fails is repaired and re-run rather than worked around — Stage IPAL § The S2 → S3 handover states the payload and the rule.

The honest negative result: names, cover definitions, risks, events, documents, annex types, objects and offers are not in this contract. S3 reads them from the specification. A smaller correct contract beats an invented one.

SUM-1 Context and identity

Field Meaning Why S3 needs it
environment the working environment S2 deployed the rating on S3 applies to the same one; the deployment set is ordered against it
skeleton_source s2_bootstrap | s3_prepass | donor_clone — who created the IPAL rows S2 rated against decides whether S3 creates or adopts the catalogue. Without it S3 duplicates or overwrites
product_code, product_id the rated product, and the PRODUCT_ID actually used proves the sync ran; rating versions join on PRODUCT_ID, not on code
versions[] {version_id, template_code, effective_date, version_stage, ld_rule, tax_rule, custom_filter} the engine reads only DEPLOYED versions; an OPEN/DONE version is not selected, and with no matching deployed version getRates fails. If an older deployed version matches product, template and date, that one is priced, so the S2 test asserts the version_id (D113). effective_date is the earliest quotable date — and the standing warning not to copy it onto PR_BASIC_COVERS.VALID_FROM
template_resolution {default_template, per_object_or_offer, selector_operation, fallback_observed} S4 needs it for LB_OFFER_PRICING_RULE. The chain is TemplateOfferCode → OfferCode → LbOfferPricingRule.TemplateCode → config.DefaultTemplate → "DEFAULT_TEMPLATE", and an unknown code falls back silently

SUM-2 Covers as rated

Field Meaning Why S3 needs it
rated_covers[].cover_code / .object_code the rated cover × object fan-out PR_BASIC_COVERS is one row per product × cover × object; a set difference means an IPAL cover that prices nothing, or a rated cover no operator can select
.bp_rate_dim P percent of base · V fixed amount · M multiplier V means the cover carries no rate, so no IV factor is needed; P means one must exist
.bp_base_factors[] the factor codes appearing in the BP_BASE expression, plus the single BP(<cover>) term if any BP_BASE is an expression (IV_SEAT*SEATS, Max((IV_HOME-10000),0)). Every named factor must exist as a PR_PRICING_FACTORS row or an object attribute, or the engine returns Missing features {…}. The specification's cover_rates.base is one factor; this is the closure
.grid_axes[] {role, factor_code, axis_kind, value_labels[], level_sets[]} — the grid as authored axis labels resolve against PPA_FACTORS.PRICING_FACTOR_NAME, and categorical labels must be the codes PR_PRICING_FACTOR_VALUES holds. This field may legitimately contradict the specification: a purely numeric LIST axis fails to compile, so S2 may have renamed 0/200/400 → D000/D200/D400, and S3 must write the renamed codes with the amount in the value name
.calc_order sequence when one cover's premium depends on another's distinct from the specification's UI order and from PR_BASIC_COVERS.COVER_ORDER
.prem_currency rating currency per cover must agree with the cover definition currency S3 writes
.uw_caps {premium_min, premium_cap, discount_prc_max, discount_amnt_max, renewal_*} PREMIUM_MIN/PREMIUM_CAP are not compiled and not read at rating. A document minimum premium left there does nothing; the enforceable home is SRD_IPROD.PR_OPERATIONS, which is S3's table

SUM-3 Loadings, discounts, taxes as rated

Field Meaning Why S3 needs it
rated_ld[].ld_code the code as actually written PPA_PRODUCT_LD.LD_CODE is VARCHAR2(20), so S2 may have shortened the specification's code; PR_BASIC_LD.LD_CODE and LT_LD_CODES.ID must carry the same shortened form
.ld_type LD | TAX | SR PR_BASIC_LD.LD_TYPE admits LD, TAX and FEE (check constraint PR_BASIC_LD_TYPE_CK, verified on PROD 07.09.2026); FEE is never used (0 of 1 465 rows) — no SR, no DIS. An SR row has no IPAL counterpart, and a discount is LD with sign -1. Getting it wrong is ORA-02290
.scope product | cover:<code>@<object> — the implemented scope, not the documented one The highest-value single field. PR_BASIC_LD.BASIC_COVER_ID must mirror PPA_PRODUCT_LD.COVER_ID. A policy-level LD published in IPAL with no COVER_ID IS NULL rating row is a hard HTTP 450; conversely a product-scope rating row reaches only the lead cover, so S2 normally writes one row per (cover, LD) under SUM — which forces the IPAL side to be cover-scoped too
.rating_file | .flat_amount the LD_RATING filename, or the constant an LD whose rating table is empty prices at 0 silently; S3's acceptance script asserts the file exists and is non-empty
.driver_factors[] the factor codes the LD's grid keys on as with bp_base_factors — each needs a row, or the payload is short a feature
.ld_order, .ld_rate_dim, .ld_rate_base, .ld_currency, .ld_rule, .ld_max_limit, .maxdf_exempt calculation mechanics ld_rule (SUM vs MULT, overriding the version default) is the first thing to check when a premium is "close but wrong"

SUM-4 Factors the rating actually keys on

Field Meaning Why S3 needs it
rating_factors[].pricing_factor_code a factor the rating keys on the set S3 must guarantee exists
.ppa_pricing_factor_name the grid-axis name axis labels resolve against PPA_FACTORS.PRICING_FACTOR_NAME, which the sync copies from PR_PRICING_FACTORS.PRICING_FACTOR_NAME. A mismatch produces a bare Internal error: '<AXIS>' that reads like a missing factor. Set name = code for any grid axis; descriptive labels belong on DESCR_LINK
.datatype_required the datatype the axis kind implies the sync rewrites datatypes (IV→NUMBER, IV_COV→IV); a numeric axis must tile with no (Other), a categorical one must carry (Other)
.qt_required Y/Nthe value the engine needs it is the rating-set gate: ParameterExtractorPPCS.cs:64,69 filters both candidate sets on 'Y', so 'N' drops the factor from the premium call and the completeness check, and on a quotation the engine zeroes the affected groups — a quote silently cheaper than the policy [F: verified 01.09.2026]. Not the same thing as the specification's factors[].required, which means "the operator must answer" (Stage IPAL § PF-6)
.value_codes[] the exact value codes the grid uses PR_PRICING_FACTOR_VALUES.PRICING_FACTOR_VALUE must equal these; may contradict the specification, as with grid_axes
.sale_stage constant "POL" an invariant, not data: the sync filters SaleStage == "POL", so a QT-only factor never becomes a PPA_FACTORS row. See the product-level caveat in Stage IPAL § PF-8b

SUM-5 What the engine demands, and what nothing holds

Field Meaning Why S3 needs it
required_features[] {feature, supplied_by: operator_factor \| object_attribute \| backend_operation:<class> \| constant \| UNRESOLVED} validate_features fails the whole call with Missing features {…}. Crucially, some are not operator questions: PH_ADDRAREA, PH_REGION_CODE, PH_MUNICIPALITY, PH_TYPE, PH_AGE are injected by a registered backend operation — so S3 registers a PR_OPERATIONS row rather than configuring a factor. UNRESOLVED is an open question against S1, never a guess. Not derivable from the specification at all
worked_examples[] {name, date, object_type, template_code, selected_covers[], factors{}, expected{per_cover_bp, per_ld_contribution, wp, total, currency}, tolerance} Stage IPAL requires the quote to render "the same figure S2 reconciled". Without the exact payload S3 cannot reproduce it, and a wrong figure gets blamed on the catalogue. This is S3's acceptance oracle
rates_catalog_note {always_returned[], mutually_exclusive_groups[]} rates[] is a catalogue, not a selection. S3's quote test must not read "priced" as "selected"; S4 depends on this outright
not_held_by_rating[] {rule, source_ref, why_not, destination} rules the grids could not express, with real destinations: S3:PR_OPERATIONS (policy minimum premium, cover dependencies) · S3:PPA_PRODUCT_COVERS.DISCOUNT_PRC_MAX + APPLY_MAX_DF · S4:LBRR_* · Hd:Development (a filter). ⚠ BP_BASE accepts exactly one BP(<cover>) term, so "X % of the total base premium across several covers" has no configuration expression — it must be named here, never approximated
open_or_provisional[] {cover_or_ld, reason, note} what stayed unpriced and the pre-emptive note given for it (D122): S2 invents no temporary rate and no rate 0 and does not block the version; S3 caps the proof level it claims for that cover — priced is not reached

SUM-6 Rules

P Three fields may legitimately contradict the whitelabel specification, and where they do, this summary wins for S3's writes: grid_axes[].value_labels, rating_factors[].value_codes, rated_ld[].ld_code. Each is a rename S2 was forced into by an engine constraint, and each carries the original in its provenance so the divergence is visible at H2 rather than discovered at quote time.

P Every field carries its source the same way the specification does. A summary field with no source is the same defect as a specification field with no source.

P The summary schema is a T0 contract (Components § 2); until it exists, the summary is the 31-field list above, carried in the tariff sign-out and validated as that list at the S2→S3 boundary (D113).

P This is v0, drafted from code rather than from a run. The S2 and S3 authors amend it on the first real slice — and every amendment is an amendment to the Configuration Module.

Facts to hold F

P SALE_STAGE is always POL — the rule and the remark for agents about the QT residue in today's estate are in Stage IPAL (decision 31.08.2026, reaffirmed 02.09.2026). S2's interest is the invariant above: a rating axis exists as a POL row, which the rule guarantees.

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).

Plan (H2): one PPA_PRODUCTS row (PRODUCT_ID = PRODUCT_CODE convention), four templates as the offers name them, one version each with effective_date 01.08.2026, ld_rule SUM, tax 2.5 % as TAX, grids keyed on CLAUSE × VEHICLE_AGE per (deductible, vehicle group). Assertion per step: „PPA_PRODUCT_COVERS holds 8 rows for version V", „genRuleTable compiles version V", „getRates for worked example W1 returns CM_FIRE_NAT BP = 1 234.00 EUR". Apply through the write executor; the known engine traps live in configuration/abacus (BP_BASE literal 'NULL' for V covers, ldRule dropped on create). getRates assertions per cover; 200 with empty rates[] is failure. Configuration summary: skeleton_source = s2_bootstrap (S2 had to create the PR_BASIC_PRODUCTS skeleton so the sync would run — the S2⇄S3 loop-back of Configuration Module § 1), template_resolution per offer, rated_covers[] with bp_rate_dim (P for the four clause covers, V for assist/mech/seats, P-of-BP for replace), bp_base_factors (IV; SEATS), grid_axes — and here the first legitimate contradiction: the deductible axis 0/200/400 is numeric-only LIST and had to become D000/D200/D400 (§ SUM-2/§ SUM-6). not_held_by_rating[]: „CM_REPLACE = 14 % of the clause base premium" — BP_BASE accepts exactly one BP(<cover>) term, and a clause is up to four covers → destination Hd:Development (a filter) or a re-reading with the human. worked_examples[] from the PDF's раздел VIII examples. H3 when the operator accepts the arithmetic.

CS-2 · 2026 EUR tariff on 4710 ГО and 4704 Каско — abacus-only — Bulstrad publishes the 01.01.2026 EUR tariffs (two PDFs in knowledge/sources/).

[gap F] the schema has one tariff.template; 4704 has 71. The plan must create 71 new versions (one per template) with effective_date 01.01.2026, each DEPLOYED, each compiled — or a subset the human names. Nothing in Stage Abacus discusses multi-template products; the configuration summary's template_resolution.per_object_or_offer (§ SUM-1) describes resolution, not authoring fan-out. getRates assertions per template × worked example (the knowledge file's Honda CR-V case: 8 000 × 6.13 % = 490.40 EUR). Two dates trap: the in-force date goes on the version, never on PR_BASIC_COVERS.VALID_FROM — and abacus-only means S3 is not even instantiated, so nobody could copy it wrongly. [ok]. Minimum premium 122.71 EUR and sufficient premium 255.65/306.78 EUR: these are CORRECT_PREM_SERV operations and PPA_PRODUCT_COVERS.PREMIUM_MIN (not compiled). If the values changed, the enforceable home is PR_OPERATIONSS3's table. Configuration Module § 7.5: „no writes outside its own targets"; § 3: deployment set „revisable later". [gap G closed by Configuration Module § 3] revising the set mid-case is a plan revision confirmed at H2 by the root; stages added later run their own H3, and the smoke tests are those of the final set. Failure and Recovery § 5 exemption test would let a single PR_OPERATIONS value change skip simulation only if a teardown already exists.

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".

Loading „≤5 persons" keys on a derived count across groups and individuals — the same defect the thread hit („умножава вече калкулираната премия още веднъж по броя лица"). A grid cannot count; Stage Abacus § Iterate: „derived factors … is a filter — code → gate Hd". not_held_by_rating[] entry with destination Hd:Development. Minimum premium 30 EUR: not_held_by_ratingS3:PR_OPERATIONS. New clause rate 0.1 % of a chosen sum: a flat P cover on its own IV — the IV is per-cover (IV_COV), and Stage IPAL § PF-8 says IV_COV/IV_COVLIST „have no case in the generator — invisible to the operator". [gap J] a cover with its own selectable sum insured is a normal shape (this ticket, 4727 CASCO_R limit, 9951 CM_SEATS) and the model says the datatype that carries it does not render. Is the pattern „definition list on PR_BASIC_COVER_DEF + LIMIT selected in the offer" instead? Neither § PF-2 nor § PF-8 of Stage IPAL says which shape a per-cover chosen sum takes (gap J, open).

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.

Version with a DURATION axis where 65 → 60-month factor. worked_examples[] from live 60-month policies (the operator supplies). [ok]. Which of 2214/2215's templates — again the multi-template question (gap F).

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.

Premium rating: per member tariff with ordered multiplicative corrections (sector −15 % then group size −5 %: 198 × 0.85 × 0.95 = 159.89 EUR). ld_rule MULT on the version [ok]. Rounding rule unstated → unresolved[]. The claims-side rules (SRVPRICE#, CLMIND# — limits, deductibles, co-pay) are also SRD_ANLT.PPA_* configuration but are not premium rating and the getRates skill does not exercise them. [gap Q] Stage Abacus defines S2 as „tariffs → ratings" proven by getRates; the health product's largest ABACUS configuration is a settlement rule set proven by a claim, for which no stage, skill or eval exists. The configuration summary has no field for it.

Challenges