portfolio-optimiser/docs/fase1-spikes/findings-c.md
Kjell Tore Guttormsen 37547fe292
refactor(examples): replace sector-specific example material with generic, fictitious examples
The context sets, the packaged knowledge bases and the example bundles are
replaced by one fictitious example set about IT operations in an invented
organisation: three context sets (serverrom-2027, driftsavtale-2027 and the
two-base drift-og-avtale-2027), two synthetic knowledge bases under
src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under
src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in
tests and fixtures are kept; names, ids and wording change. Dated measurement
documents that only recorded runs on the replaced material are deleted.

Gate figures measured on the new set are not comparable with earlier ones.
The exclusion gate from the previous commit is green: 0 tracked files hit
outside the shared/ subtree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 15:04:21 +02:00

48 lines
2.8 KiB
Markdown

# Spike C findings — blocking deterministic hybrid validator (B1)
**Assumption:** a blocking deterministic hybrid-validator can *structurally* block an
out-of-range proposal from ever reaching the expert — and a valid proposal yields a
risk distribution (P10/P50/P90) with a capped self-repair loop.
## Result — CONFIRMED (fully deterministic, no MAF, no endpoint)
Three stages over a typed Pydantic IR (`SavingsProposal`):
1. **IR schema gate.** Field validators + a cross-field `@model_validator` reject
malformed proposals at construction: negative quantities, and a claimed saving that
exceeds the affected items' own total (both proven to raise `ValidationError`).
2. **PuLP solver-in-the-loop.** A real **CBC** solve (PuLP's bundled binary, found at
`solverdir/cbc/osx/i64/cbc` on this Intel mac) bounds the maximum feasible saving at
`MAX_SAVING_FRACTION = 0.30` of the affected items' cost. CBC absence would
**escalate** (`CbcUnavailable`) — there is no silent LP-relaxation fallback (R2).
*(PuLP 3.x deprecates `PULP_CBC_CMD`; PuLP 4.0 will require `pip install pulp[cbc]` +
`COIN_CMD`. The spike silences the cosmetic warning — a Fase 2 migration note.)*
3. **Monte Carlo.** Seeded stdlib `random` + `statistics.quantiles` over uncertain
unit-costs give **P10 ≤ P50 ≤ P90** of the feasible saving, and are reproducible under
the fixed seed.
**Structural block (the B1 headline):** an out-of-range proposal returns a `Rejection`,
a *different type* from `ValidatedProposal` that carries **no percentiles** — accessing
`.p50` on it raises `AttributeError`, so a rejected proposal can never be consumed as
validated. A valid proposal (claim well under the feasible cap) returns a
`ValidatedProposal` with the percentiles. **`self_repair` is capped** at `max_attempts`
and hard-stops (verified it calls the generator exactly N times and no more, B4).
### Worked example (project KONTOR-IT-E1, codes 05.2 + 03.1)
- Affected items' total ≈ **1 482 500 NOK**; 30% feasible cap ≈ **444 750 NOK**.
- Claim **200 000** → `ValidatedProposal` with ordered P10/P50/P90 around the cap.
- Claim **800 000** → constructs (≤ total) but exceeds P90 feasible → **`Rejection`**.
## Token use
**0 — validator is fully deterministic; live LLM generation is gated.** The
`generate_via_llm` entry point `skip`s without a LOCAL endpoint; all validator / solver /
Monte-Carlo logic is exercised on crafted IR directly, with **zero** model tokens spent.
## Implication for Fase 2
The obligatory, blocking validator is realizable exactly as specified: typed IR +
real solver + risk percentiles + a type-level rejection that cannot leak downstream.
Fase 2 should keep the `Rejection`/`ValidatedProposal` type split (structural block, not
an advisory flag) and the CBC-absent escalate contract.