2.8 KiB
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):
- IR schema gate. Field validators + a cross-field
@model_validatorreject malformed proposals at construction: negative quantities, and a claimed saving that exceeds the affected items' own total (both proven to raiseValidationError). - PuLP solver-in-the-loop. A real CBC solve (PuLP's bundled binary, found at
solverdir/cbc/osx/i64/cbcon this Intel mac) bounds the maximum feasible saving atMAX_SAVING_FRACTION = 0.30of the affected items' cost. CBC absence would escalate (CbcUnavailable) — there is no silent LP-relaxation fallback (R2). (PuLP 3.x deprecatesPULP_CBC_CMD; PuLP 4.0 will requirepip install pulp[cbc]+COIN_CMD. The spike silences the cosmetic warning — a Fase 2 migration note.) - Monte Carlo. Seeded stdlib
random+statistics.quantilesover 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 FV42-GSV-E1, codes 05.2 + 03.1)
- Affected items' total ≈ 1 482 500 NOK; 30% feasible cap ≈ 444 750 NOK.
- Claim 200 000 →
ValidatedProposalwith 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 skips 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.