Layer 1
CLAUDE.md
Always-loaded context
Identity, the four boundaries, and routing to everything else. Kept under 120 lines because it
is paid for on every request — anything longer belongs in knowledge/.
Serdica · IPAL / ABACUS · design
How to build the thing the requirements describe: a workspace that turns an insurer's product specification into configuration for four surfaces across two databases. The design turns on one decision — a single canonical model, and every emitted file a projection of it — and on two isolations that keep the model honest. Below: the architecture, the workspace laid out file by file, seven prompts that carry most of the behaviour, the one-way ABACUS seed and its limits, and the decisions with the alternatives they beat.
Provenance tags appear throughout, and are the same three the model uses:
SPEC the specification says so ·
SYS derived from the system ·
DEC a decision taken here.
ISOLATED marks a component deliberately denied context that the rest
of the workspace has. Sections marked + open to the prompt text or the evidence.
Companion document: Product Configurator Requirements — what the thing must do. This one is how it is built.
Configuration for one product has to be stated in four places that no constraint ties together. Authored four times it drifts; derived one from another it loses the rates. So the design puts a written model in the middle and makes every surface a projection of it — and then refuses to let the model verify itself.
Layer 1
CLAUDE.md
Always-loaded context
Identity, the four boundaries, and routing to everything else. Kept under 120 lines because it
is paid for on every request — anything longer belongs in knowledge/.
Layer 2
skills/
Procedures
Four: configure a product, read a tariff, explore an unfamiliar column, verify. Each is a sequence with gates, loaded only when its trigger matches.
Layer 3 · ISOLATED
agents/
Two denied contexts
tariff-reader is given no schema vocabulary; estate-prober is given
no product intent. Each returns a fact the caller cannot bias.
Layer 4
prompts/
The reusable wordings
Seven texts that recur across products — the identity, the source inventory, the scope question, the model header, the open question, the handover note, the customer reply. Versioned, so a phrasing that worked is not re-invented.
Layer 5
knowledge/
Domain reference
Thirteen documents behind one index — the two schemas, the tariff engine, the INSIS seam,
the pricing-factor glossary, the SRD_INTEGR packages, premium theory, environments.
Read on demand, never all at once. Facts about the system, not about any one product, cut to
product-configuration scope on 2026-09-02.
Layer 6
memory/
What changed the behaviour
Corrections, measured counts with their dates, products in flight. The index loads every session; the entries load when relevant.
Layer 7
scripts/
Determinism
Whatever should give the same answer every time: domain probes, cross-surface diffs, document assembly. Anything a script can settle is not left to judgement.
Layer 8
deliverables/
The output
One directory per product, self-contained: model, SQL per part, acceptance, rollback, document. Nothing flat at the root.
Everything the configurator needs and nothing it does not. The comment on each line says why the file exists — a file whose reason cannot be stated in one line is usually two files.
product-configurator/
│
├── CLAUDE.md # always loaded · ≤120 lines · identity, boundaries, routing
├── README.md # how to open the workspace · start here · before the first run
├── _PROVENANCE.md # exported from the AISA workspace 2026-09-02 · what came, what was cut
├── .mcp.json # sqlcl read-only · the binary lives in tools/, git-ignored
├── .gitignore # deliverables/ stays local until a run is signed off · tools/sqlcl/
├── .gitattributes # LF in the repo · binaries never normalised
│
├── .claude/
│ ├── skills/
│ │ ├── configure-product/skill.md # ENTRY · the 11 steps and the 3 gates
│ │ ├── read-tariff/skill.md # a tariff document → a rate structure, in the doc's terms
│ │ ├── explore-schema/skill.md # what does this column actually do · constraint + writer + count
│ │ ├── verify-product/skill.md # acceptance script, then the manual premium proof
│ │ └── workspace-initialize/skill.md # JDK · SQLcl into tools/ · MCP starts · connections reachable
│ │
│ └── agents/
│ ├── tariff-reader.md # ISOLATED · no schema vocabulary · returns a rate structure
│ └── estate-prober.md # ISOLATED · SELECT only · returns counts, never advice
│
├── prompts/
│ ├── 00_identity.md # who the configurator is · the four refusals
│ ├── 05_source_inventory.md # N files in → one table: what each says, which part it owns
│ ├── 10_scope_question.md # the A/B/C question put to the customer before any work
│ ├── 20_model_header.md # model front matter · the SPEC/SYS/DEC legend
│ ├── 30_open_question.md # how a DEC line becomes a numbered, answerable question
│ ├── 40_internal_note.md # handover to the dev team · table.column, wrong → right
│ └── 50_customer_reply.md # the reply that exposes no table name and no row count
│
├── knowledge/
│ ├── system.md # THE INDEX · one row per file · consulted before any read
│ ├── product_config_new_product.md # START HERE · the twelve parts · counted domains · CreateProduct's 91 tables
│ ├── product_configuration.md # a configured product walked table by table · the four surfaces live
│ ├── insurance_premium_pc.md # the vocabulary · base/written/earned · order of application · 17 defects
│ ├── factor_glossary.md # 134 pricing-factor codes · BG description · products · LOV values
│ ├── abacus_schema.md # SRD_ANLT · versions, rate files, the deploy chain, version selection
│ ├── abacus_engine_vs_abc_access_schema.md # design side (SRD_ANLT) vs resident side (ABC_ACCESS)
│ ├── ipal_schema.md # SRD_IPROD · SRD_CUST · SRD_PR · SRD_SYS · tables, keys, writers
│ ├── insis_schema.md # the incumbent PAS · only what the mapping seam targets
│ ├── insis_seam.md # CFG_MAPPING_* · product-aware pre-processors · ABC_ACCESS convert hooks
│ ├── db_packages.md # the 95 SRD_INTEGR packages · the 15 product-aware · config-table → reader
│ ├── currency_and_rates.md # main units · EUR default · the 1.95583 fixing
│ ├── roles_and_authorization.md # what makes a configured product reachable by an operator
│ ├── environments.md # DEV/TEST/QA/PROD deltas · which one a request targets
│ └── sources/<code>/ # ONE DIRECTORY PER PRODUCT · every file byte-identical
│ └── _inventory.md # file → date → parts it covers → authoritative for
│
├── memory/
│ ├── MEMORY.md # THE INDEX · one line per memory · loaded every session
│ ├── feedback/ # 18 corrections · each says what to do differently, and why
│ ├── reference/ # 43 measured facts · every count carries its date
│ └── project/ # products in flight · decisions awaiting an answer
│
├── scripts/
│ ├── probe_domains.sql # one GROUP BY per configurable column · feeds estate-prober
│ ├── model_checks.py # model → the questions answerable before SQL exists
│ ├── abacus_seed.sql # ABACUS → IPAL skeleton · 3 of 12 parts · one-way, once
│ ├── diff_surfaces.sql # IPAL ↔ ABACUS · cover, factor and LD agreement
│ ├── check_links.py # every pointer resolves · CORE vs MEMORY split · should print zero
│ ├── finalize_doc.ps1 # markers → the product document, shared CSS inlined
│ └── assets/ # product_doc.css · product_doc.js · the shared document chrome
│
├── sql/
│ ├── configure_connections.sql # the eleven BST-{ENV}-{SYSTEM}-{USER} connections · prompts for passwords
│ └── setup_connections.bat # runs the above through tools/sqlcl
│
├── tools/ # git-ignored · SQLcl per workspace · connections per user in %APPDATA%
│
└── deliverables/
└── <code>_<slug>/ # one directory per product · never a flat file at the root
├── 00_README.md # what is here and in what order to read it
├── 01_findings.md # what the system taught me · where it contradicts itself
├── 05_model.md # THE MODEL · every line SPEC · SYS · DEC
├── 06_model_checks.md # answered against the model, before any SQL is written
├── 20_abacus_design.sql # group B · emitted first · versions, covers, LD, factors
├── 21_abacus_rates.sql # group B · the rate files · the only never-derivable part
├── 22_seed_report.md # what the seed filled, what it refused, coverage here
├── 10_texts.sql … 19_access.sql # group A · catalogue → screen → packages → print → roles
├── 50_insis_core.sql … 52_*.sql # group C · only when the customer commissioned it
├── 90_verify.sql # completeness · omissions · cross-layer · the manual test
├── 99_rollback.sql # exactly what was added, in reverse dependency order
└── <Product>.html # the product document · decisions, questions, how to undo
Two ordering choices are load-bearing. ABACUS files are numbered 20
and 21 yet are emitted before the 10–19 IPAL
files: the numbers are reading order for a human, the emission order is the dependency
PR_BASIC_LD has on the ABACUS loading list. And 99_rollback.sql is written
in the same sitting as the SQL it undoes, not afterwards — a rollback written later is a
rollback written from memory.
Seven texts do most of the work. They are files rather than habits because a phrasing that reliably produces a good answer is an asset, and because a prompt in a file can be reviewed by someone who disagrees with it.
The specification is a SET of files, not one document. Before reading any of them
closely, inventory the set.
One row per file:
file · date · from whom · which parts of the product it speaks to ·
is it a source, or a revision of another file in this set
Then, one row per part of the product (catalogue, covers, limits, factors, tariff,
packages, screen, texts, annexes, print, access, integration):
part · the file that is AUTHORITATIVE for it · the other files that touch it
Rules for building that second table:
· A later file supersedes an earlier one only where it SAYS it does. Otherwise two
files covering the same part is a CONFLICT, and conflicts are reported, not
resolved by date.
· A part with no file is a gap. Name it. Do not fill it from a comparable product.
· A part with two files that agree is fine — say which one the model will cite.
· Reconcile ACROSS files, not within each. A cover priced in the tariff and absent
from the cover list is a finding; so is a cover listed and never priced.
Output this inventory as the first section of the model, and cite files by name
everywhere afterwards. "The specification says" is not traceable when there are nine
of them.
Why this shape. The two failures are symmetrical and both common: reading the set as one document loses the fact that a later email overrode one clause; reading the files independently loses everything that only shows up between them — the priced cover nobody listed, the listed cover nobody priced. The inventory is the cheapest place to catch both, and it makes every later citation specific.
You configure insurance products for Serdica IPAL from a written specification. Your deliverable is FILES. You never execute DDL or DML against any environment — not DEV, not inside a block that rolls back. Auditing, triggers and redo capture the attempt regardless of the rollback. You never alter a core table or column without explicit permission. If a part cannot be configured without an ALTER, stop and offer the alternative that needs none. You never report a configuration complete on the strength of a check derived from your own model. Completeness means a quote rendered in the running system and reconciled against the tariff by hand. You never fill a gap silently. A value the specification does not give and the system does not settle is a decision — tag it DEC, and put it in the numbered questions. Read memory/MEMORY.md at the start of every session. Read knowledge/system.md before reading anything else in knowledge/.
Why this shape. Each refusal names the failure it prevents, because a rule whose cost is invisible gets relaxed by the next person under a deadline. The rollback clause exists because "it rolls back" is the most persuasive wrong argument in this domain.
Before I write anything, confirm which groups are in scope.
A — the IPAL product catalogue, covers, risks, limits, loadings, factors,
screen, packages, annexes, texts, print, access.
Makes the product reachable and quotable.
B — the ABACUS tariff versions, covers, LD list, factors, rate files.
Makes the premium a number rather than a zero.
Longest lead time; can be exercised on its own.
C — the INSIS integration mapping, validations, INSIS core rows, the ABC
request map. Makes the policy transferable.
Expect some of the 15 product-aware SRD_INTEGR
packages to need a DEV CHANGE, not a config row.
A and B together give a product that quotes and issues with no INSIS anywhere.
C is separate work with a separate risk profile.
Which groups, and against which environment?
Why this shape. It names what each group buys in outcomes, not in tables, so a commercial reader can answer it. And it warns about the dev-change tail inside group C before that cost is discovered halfway through.
You read insurance tariff documents and return their pricing structure.
You have no access to any database and no knowledge of any schema. That is
deliberate. If you find yourself wanting to know a table name, a column name or how
a value is stored, you have drifted out of your job — the storage is one realisation
of the pricing taxonomy, not the taxonomy itself.
Return, in the document's own vocabulary:
· every cover the document prices, and every cover it names without pricing
· the base rate for each: the number, its unit (percent / promille / lump sum /
per day / per mile), and what it applies to
· every loading and discount: its value, whether it is a percent or an amount,
at what level it applies, and whether it is automatic or given by hand
· the composition rule if the document states one, and say so if it does not
· every limit, deductible, minimum and maximum
· every condition attached to any of the above
Quote the clause for anything a reader would otherwise have to take on trust.
Where the document is silent, say "not stated" — never infer a market-standard value.
Why this shape. A reader that knows the schema starts mapping while it reads, and mapping is lossy in a way that is invisible afterwards: the factor with no column becomes the factor that was not mentioned. Denying the vocabulary makes the omission surface as an unmapped item later, where somebody notices it.
You measure what the existing configuration contains. You are given a column, or a set of columns. You are NOT told what product is being built or what answer would be convenient, and you must not ask. For each column return, from the environment named in the request: · the constraint — type, length, NOT NULL, CHECK, foreign key · the live distribution — SELECT, COUNT(*) ... GROUP BY , every value · the count of rows and the count of distinct products · the date you measured it Return counts. Do not return a recommendation, a "usual value", or a default. Two distinctions the caller depends on you keeping: · a value permitted by a constraint but present in zero rows is PERMITTED, NOT USED · a value present in one or two rows out of thousands is reported as such, with the count, and never described as a convention SELECT only. If a question cannot be answered without a write, say so and stop.
Why this shape. The prober exists to stop the estate being read as a template. Withholding the intent removes the pull toward the convenient finding, and forcing raw counts out rather than adjectives leaves the judgement where it belongs — with the model, where it gets a DEC tag and a question number.
# <Product name> — <code> — canonical model
Source: <file>, <date>, <version> Environment: <DEV|TEST|QA|PROD>
Scope: group A / B / C as confirmed <date>
Projections: every SQL file in this directory names the section of this file it projects.
Provenance tag on every line — no line is untagged:
SPEC the specification says so — the customer wrote it
SYS derived from the system — a CHECK, a key, a foreign key, a counted
convention. Not a choice; the database already settled it.
DEC a decision taken here, and therefore something to confirm
Every DEC line appears again in § Open questions, numbered. A model with no DEC lines
means the gaps were filled silently.
Why this shape. Three things are fixed at the top because each is expensive to establish later: which source the model answers, which environment it targets, and what the tags mean. The last line is the honest one — it tells a reviewer what a suspiciously clean model actually indicates.
№ <n> — <the decision, as a question>
Blocks: <what cannot be finished until this is answered>
Options: A <option> — <consequence>
B <option> — <consequence>
Assumed: <what the model currently says, so work continues>
Cost of
being
wrong: <what has to be redone if the assumption is rejected>
Why this shape. A question with a stated assumption does not block; work continues under it and the answer either confirms or triggers a known amount of rework. Naming the cost of being wrong is what lets the reader decide how quickly they need to answer — and it stops a list of twelve questions arriving with no order of importance.
Once the tariff is in SRD_ANLT, some of the IPAL product can be written from it
instead of typed again. Measured across the 63 products ABACUS knows: the covers
transfer almost completely, the loadings nearly so, the factors less than half, and everything else
not at all. That shape decides what the component is — a seed run once at
creation, not a synchronisation that could be run again.
| Object | In IPAL | ABACUS knows | Coverage | IPAL-only | ABACUS-only |
|---|---|---|---|---|---|
| Covers | 547 | 538 | 98.4 % | 9 | 290 |
| Loadings & taxes | 415 | 380 | 91.6 % | 35 | 181 |
| Pricing factors | 1 443 | 638 | 44.2 % | 805 | 210 |
The averages hide the variance that matters. On product 2200 ABACUS knows 95 of 122 factors — a genuine head start. On 4800 it knows 6 of 57. The seed must therefore report its own coverage for this product before anyone leans on it.
| Target | Filled from ABACUS | Left for IPAL |
|---|---|---|
| PR_BASIC_PRODUCTS | BASIC_PRODUCT_CODE, LOB | GENERIC_NAME, validity |
| PR_BASIC_COVERS | COVER_CODE, OBJECT_CODE — 537 of 538 shared covers agree on the object | COVER_TYPE DEC, LIABILITY, COVER_ORDER, validity |
| PR_BASIC_LD | LD_CODE, LD_TYPE, LD_SIGN | SEP_ACCOUNT (derive), NCB, FINST, REFUNDABLE, cover-vs-product level |
| PR_PRICING_FACTORS | PRICING_FACTOR_CODE, PRICING_FACTOR_NAME, DATATYPE, QT_REQUIRED | PRICING_TYPE, POLICY_TABLE, VALUE_SOURCE, UI_FLAG, UI_ORDER, UI_GROUP, DEFAULT_VALUE, COVER_CODE, OBJECT_CODE, SALE_STAGE |
One near-miss to refuse. CALC_ORDER on the ABACUS side looks like
COVER_ORDER and is not: one is the sequence the engine prices in, the other the
sequence an operator reads. Seeding one from the other gives a screen ordered by arithmetic.
| Part | Rows | Why the engine cannot know it |
|---|---|---|
| PR_PRICING_FACTOR_VALUES | 2 639 | ABACUS stores the values that differentiate a rate, never the list an operator chooses from |
| PR_BASIC_COVER_DEF | 2 278 | Limits and deductibles are policy terms; the engine sees only their effect on price |
| LB_OFFER_COVERS | 1 541 | Packaging is commercial and has no rating meaning |
| CFG_FLD_VALIDATION | 530 | Field rules live in the integration layer |
| PR_BASIC_COVER_RISKS | 320 | Perils are the wording of the cover, invisible to a rate |
| LB_OFFERS | 289 | The layer whose absence let a product reconcile to the cent and fail to quote |
| PR_BASIC_COVER_EVENTS | 216 | Claim events, same reason as perils |
| PR_ANNEX_TYPES | 37 | A lifecycle concern |
| PR_BASIC_PRODUCT_DOCUMENTS | 20 | Print configuration |
And with no row count because they are not per-product tables: every SR_MESSAGES
label and its Bulgarian translation, CFG_POLICY_NO_SEQ, the roles and routes that
make the product reachable at all, and the entire screen layer. A seeded factor arrives
with a code, a name and a datatype — nothing that puts it on a screen or tells the backend
where to read its value.
98 % of the covers, 44 % of the factors, and none of the nine parts that make the product reachable.
— why the component is called a seed and not a sync
The name is the design. Sync implies the two sides can be held in agreement, which
invites a second run — and a second run would overwrite COVER_TYPE,
UI_GROUP and every other column IPAL alone owns. Seed is a one-time head start
that removes typing, not judgement. It fills the skeleton, tags what it could not decide
DEC, refuses the 290 covers and 210 factors that exist only in ABACUS,
and leaves the twelve-part completeness check exactly where it was.
knowledge/sources/<code>/Every file byte-identical to what was sent. _inventory.md records what each says and which part it is authoritative for; conflicts between files are reported, not resolved by date.tariff-reader returns a rate structureIn the tariff's vocabulary, with clauses quoted and silences marked not stated. No schema words in the output.estate-prober returns countsConstraints and live distributions for every column the model will set. Counts, not advice.06_model_checks.md — cover set, factor set, value sets, the tariff's arithmetic. Free, and before any SQL exists.memory/A newly counted convention, a defect found in an existing product, a correction. The next run reads it.| Decision | Chosen | Rejected | Why |
|---|---|---|---|
| Hub | One canonical model; every file a projection | Author each surface directly | No FK ties the four surfaces, so nothing catches divergence. Four authorings drift, and the drift is invisible until a premium is wrong. |
| Derivation | Neither surface generates the other | Generate IPAL from ABACUS, or the reverse | Measured on 2215: 49 factors in IPAL, 29 in ABACUS, 3 in the rate files. Every direction is lossy; the losses differ. |
| Emission order | ABACUS before IPAL | IPAL first, tariff last | Rates are the only never-derivable artefact and carry the longest lead time; they can be exercised before an IPAL row exists. PR_BASIC_LD also derives from the ABACUS list. |
| Tariff reading | An isolated agent with no schema vocabulary | Read the tariff in the main context | A reader that knows the schema maps while it reads, and an unmappable factor silently becomes an unmentioned one. |
| Estate reading | An isolated prober returning raw counts | Ask the main agent for "the usual value" | Withholding the intent removes the pull toward the convenient finding and keeps permitted distinct from used. |
| Completeness | A rendered premium, reconciled by hand | The acceptance script alone | Counts derived from the model prove internal agreement. A product has been handed over reconciled to the cent and unable to quote. |
| Execution | Emit files; a human runs them | Let the configurator apply its own SQL | The blast radius of a wrong configuration is every policy written afterwards, and the review has to happen before the write, not after. |
| IPAL → ABACUS | The backend calls the engine directly | Route IPAL pricing through SRD_INTEGR.ABC_PREM | That proxy exists for INSIS — every entry point is typed policy@insis%ROWTYPE and it writes results back into INSIS. IPAL assembles the request from PR_PRICING_FACTORS and calls rpc.abacus itself. An IPAL-only product needs none of the 7 packages or 14 tables. |
| ABACUS → IPAL | A one-way seed, run once at creation | A standing two-way sync | ABACUS knows 98 % of covers but 44 % of factors and none of the nine remaining parts. A second run would overwrite COVER_TYPE, UI_GROUP and everything else IPAL alone owns. |
| Input | A set of files with an authority table per part | One merged specification document | Merging first loses which file said what, and the conflicts between files are findings. Reading them independently loses everything visible only across them. |
| Unpriced covers | Configure at rate 0 in both surfaces | Omit until a tariff arrives | CHANGEABLE='Y' on all 7 035 157 base-premium rows ever written means an underwriter can price it by hand. An absent cover cannot be sold at all. |
"Both reported five packages. A check that agrees with itself cannot detect an absent step."
— the post-mortem that put gate 9 in the sequence
This is the single observation the verification design rests on. A product was handed over complete, its pricing reconciled to the cent, and it could not produce a quote: the packaging layer had never been wired, and the check that should have caught it measured the work against the plan that produced it. Everything in the diagram's accent path exists because of it.
In this order, because each step is only checkable once the one before it exists.
| Step | Do | Done when |
|---|---|---|
| 1 | Create the tree; write CLAUDE.md from prompts/00_identity.md | A session starts and states its own boundaries unprompted |
| 2 | Wire .mcp.json: SQLcl read-only, installed into tools/ by /workspace-initialize | SELECT 1 FROM dual answers on every environment named in environments.md |
| 3 | Write knowledge/system.md and the thirteen topic documents it indexes | Every claim in them carries a count or a source, and a date |
| 4 | Run scripts/probe_domains.sql against PROD; seed memory/reference/ | Every configurable column has a measured distribution with a date |
| 5 | Add the two isolated agents and confirm the isolations hold | The tariff-reader's output contains no schema identifier; the prober's contains no adjective |
| 6 | Write the five skills, entry point last | The entry skill only sequences the others — it holds no procedure of its own |
| 7 | Rehearse on a product that already exists | The emitted SQL reproduces the live configuration, and every difference is explained |
| 8 | Configure a real product against TEST | A quote renders and reconciles by hand — gate 9 |
Step 7 is the one people skip. Reproducing a product that already works is the
only rehearsal that has an answer key, and the differences it surfaces are exactly the parts the
knowledge base got wrong. Expect it to fail the first time in a way that improves
knowledge/ rather than the skill.
Named, so that nobody assumes otherwise.
Moving a finished product between environments. Nothing in the estate carries a complete product across: the migration tool touches neither the tariff nor the INSIS-side ABACUS configuration. The configurator re-emits against the target instead, which is correct but not cheap.
Keeping a configured product correct as the system changes. The model is a statement about one moment. When a shared register or a validation framework changes underneath, no part of this design notices.
Anything requiring a write. Backend branches for product-aware
SRD_INTEGR packages, INSIS user exits, a column too short for a code — all of
them leave the workspace as a request, not a file.
Judging the tariff. The configurator implements pricing and raises numbered questions about it. Whether the price is right is an actuarial decision, and it stays one.