Stage S2 — Abacus
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
- Plan: which products/versions/templates will exist, which factors the grids key on, what each loading applies to — confirmed with the human before writing (the plan is refutable: it names the expected premium per worked example).
- Apply: author in the maintained source (
PPA_*metadata + rating files, never generated output), compile and deploy — the engine reads only DEPLOYED versions F: [ABACUS schema]. P The test asserts theversion_idit priced: an older DEPLOYED version matching product, template and effective date is otherwise priced instead, silently (D113). The mechanics — where a rate lives, the four calls, the grid grammar and the five ways a grid compiles cleanly and prices wrongly — are Rating Files; ⚠ a rate file is shared by name, so two products can point at one file and editing it changes both, and the compile's cache reset does not reach every replica. - Test — the getRates skill: build the factor payload from what the product declares (queried, never remembered), call the engine (computes and stores nothing — the safe inner loop), assert per cover: base premium, every loading/tax row, the WP roll-up — against the documents' expected examples F: [getRates recipe].
- Iterate: differences are shown per cover with the arithmetic; the human corrects readings or accepts; a rule a grid cannot hold (cross-cover minimums, derived factors, compounding rewrites) is a filter — code → gate Hd to the Development module (id
source), never approximated into a grid [F: the PC 9951 run]. ⚠ P That escalation is for a factor which must be computed. A factor which is merely a grouping of an existing factor's values — nine vehicle types falling into four tariff groups — is a level set on the underlying axis, not code, and escalating it is a needless change request (Rating Files § 3); the tariff analyst reports which of the two shapes it read. - Scripts/deploy: the stage's script set can deploy alone — a tariff change on an existing product is an abacus-only deployment set.
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/N — the 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
- The template code is the only offer↔rating bridge, and an unknown code falls back silently to another tariff — verify which template resolved, not which was configured.
- Missing rates are never faked. P A cover the specification names but the tariff does not price stays unpriced — no invented rate 0, no blocked version — and the tariff analyst adds a pre-emptive note (a specification line and a customer-preview line) that it is unpriced, because customers sometimes intend the opposite; it becomes an
unresolved[]item only when the specification is ambiguous about the cover (D122). - Failure decode:
400 missing featureslists exactly what to add;500 internal: 'X'is usually a missing factor, not a platform fault;200with emptyrates[]is a hard failure (silent zero premium); zero premium → check the engine's log rows inABC_POLPR_LOGfirst, because never invoked and priced zero lead to opposite investigations (Quote Verification § 5 carries the whole decode, the payload build and the three levels). - A reconciling total is not evidence: a loading authored to apply unconditionally never errors, and four such loadings cancelled to exactly the right premium on a product wrong in four places — so the worked examples separate the loadings and the acceptance asserts each contribution row (Rating Files § 5).
- ⚠
getRatesignoresselectedCoversand prices the whole catalogue, so a total is never the assertion — reconcile per cover (Quote Verification § 2). - ⚠ A rate file is a lower bound on a value set, never the set. A grid carries only the values that differentiate a rate, so a value list can never be reconstructed from one: two bands in a file are compatible with a dozen values an operator must be able to pick. The value list comes from the specification, and where the specification is silent it is an
unresolved[]line — never an inference from the grid. - The seed is a head start, not a synchronisation. Projecting the rating into catalogue proposals fills a skeleton — codes, names, datatypes, the loading list — and writes none of the parts that make a product reachable: the value lists an operator picks from, limits and deductibles, perils and claim events, packaging, field rules, annex types, print, labels, numbering, routes and roles, and the whole screen layer. P It therefore runs once, at creation — a second run would overwrite the columns the catalogue alone owns — and it reports its own coverage for this product before anyone leans on it, because coverage varies by product far more than any average suggests. Every proposal it cannot decide (a cover's commercial type above all) arrives tagged as a decision, never as a value.
- Two dates: the tariff's in-force date belongs on the rating version — on the covers it makes them not-yet-in-force while rating looks perfect.
- Factors that feed rating are configured
SALE_STAGE = 'POL'— the IPAL→ABACUS catalogue sync hard-filtersSaleStage == "POL", so a QT-only factor never becomes aPPA_FACTORSrow and no grid can key on it [F:PasSrdSyncService/App/AutoMapper/ServiceProfile.cs:43, verified 01.09.2026]. - ⚠ Three different fields are called "QT", and the 31.08.2026 decision retires exactly one [F: verified 01.09.2026]:
PR_PRICING_FACTORS.SALE_STAGE(QT/POL— the one retired, read by the wizard and by offer resolution) · thegetRatesrequestsaleStage(QT/APL, built fromPOLICY.POLICY_TYPE, and which the engine never reads — zero occurrences insrc/Analytics/) ·PPA_FACTORS.QT_REQUIRED+ the request'sisQuote, which is the engine's real quote mode. Retiring the first says nothing about the other two.
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_OPERATIONS — S3'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_rating → S3: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
- P A published rate covering a bundle of covers does not tell the split — always a question to the human, never arithmetic.
- P Rating may need a minimal IPAL catalogue first (the catalogue-sync dependency) — the module allows the S2⇄S3 loop.
- C Confirm the QT obsoletion state on the target branch before a production-bound product whose process carries a quotation step (Stage IPAL remark); UI evidence so far is from
serdica-uimaster, backend frombulstrad-staging(KI-054 documents drift). - P Filter authoring/registration knowledge is thin in the estate's memory — the first filter case builds
source/knowledge with the backend owner.