portfolio-optimiser/shared/method-spec.md
Kjell Tore Guttormsen 5de1c93b69 feat(spec): S2 — normative framework-neutral method spec as the fourth shared artifact
Author shared/method-spec.md: the 8-step loop (normative, RFC-2119), the
verdict JSON contract incl. the id-minting algorithm and the chosen conflict
semantics, the inbox/outbox folder contract, the fail-closed promotion-gate
semantics, the IR projection + golden suite as the only ground truth (incl.
the reproducible Monte Carlo procedure), and the budget/stop, provenance and
startup-contract requirements — every normative claim cross-checked against
the load-bearing tests/code. The sibling implementation builds from this spec
alone.

Load-bearing trio (tests/test_method_spec_loadbearing.py, persona-trio
style): required structure, a name-shaped framework-neutrality guard over the
spec + the persona skill tree, and a cross-check-completeness test driven
from the REAL artifacts and the REAL verdict serializer (red on code drift).
All three detach points proven RED (missing file / framework name / dropped
field). shared/README.md: the "(planned)" line replaced with the real entry.

Suite 152 -> 155 passed / 4 skipped; ruff check+format clean; mypy src clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
2026-07-03 00:50:42 +02:00

27 KiB
Raw Blame History

Method specification — portfolio cost-saving loop (framework-neutral)

Status: normative. This document specifies the method both reference implementations build: an agentic loop that finds cost savings inside each project of a portfolio, with a mandatory deterministic validator, expert judgement in the loop, and learning from the verdicts. It is written so the method can be implemented from this spec alone — without reverse-engineering any existing implementation. The prose is framework-neutral by rule: it never names a concrete agent toolkit or vendor stack, and a guard test keeps it that way.

The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as in RFC 2119. Requirements are labelled normative; anything marked (reference) documents the reference implementation's concrete choice and is informative, not binding — except where the golden suite (§7) freezes it.

1. Scope and conformance

The method is the product: a swarm of agents generates candidate cost-saving measures for one project at a time; a deterministic validator decides the numbers (mandatory, blocking); domain experts judge the outcomes (human-in-the-loop); the system learns from the verdicts across runs. A conforming implementation:

  1. implements the 8-step loop of §3 with the contracts of §4§10;
  2. reproduces the shared golden suite's decided outcomes (§7) on the shared example bundle;
  3. proves every load-bearing seam with a test that FAILS when that seam is detached (§11).

Honesty rule (unwaivable): no artifact — code, docstring, README, or report — may claim more than the implementation does. Scripted stand-ins (synthetic clients, seeded verdicts) MUST be labelled as such wherever their output is presented.

Boundary: this is a purely technical framework. The deploying organisation owns all processing purposes and impact assessments; implementations provide only the technical prerequisites (local-only operation, provenance, no silent egress) and a disclaimer.

2. Terms and architecture

Three layers, strictly separated (the separation is load-bearing — see §3 Step 1 and §6):

  • Context layer — one curated, version-controlled knowledge bundle per project (an "LLM wiki") in the open OKF format: a directory of markdown files with YAML frontmatter, one required field type, a reserved index.md entry point, and intra-bundle cross-links. This layer holds project documents, methodology, verified literature, constraints, and approved verdicts (type: verdict files). It is what runtime reading and experience retrieval draw from.
  • Output layer — a run-scoped folder structure of raw results: proposals pending verdict, rejections with reasons, and raw verdict files (plain JSON, one per file). Append-heavy, never part of the wiki.
  • Promotion gate — the only path from the output layer into the context layer (§6). Only expert-approved knowledge crosses it.

Other terms: the IR is the typed intermediate representation of a candidate measure (§7.1); the store is the in-memory collection of historical verdicts retrieval ranks over (§4.2); the fold is the injection of retrieved prior verdicts into the hypothesis prompt (§3 Step 1); the two falsifiers are the deterministic validator (numbers) and the debate checker (reasoning) — they judge the same candidate and are never conflated (§3 Step 4, §9).

3. The loop (normative)

Eight steps. Steps 16 happen within one run; steps 78 close the learning loop across runs separated in time.

Step 1 — Understand the context (navigate, never stuff)

The agent read-context for a project MUST be built by navigating its OKF bundle with progressive disclosure — never by stuffing the whole bundle (or keyword-retrieved chunks of it) into the prompt:

  • Navigation starts at index.md and follows its intra-bundle markdown cross-links (](target.md)). Targets containing a path separator are out-of-bundle and MUST be skipped. Repeated links are de-duplicated; order is deterministic (index first, then links in first-seen order). (reference: the link pattern is \]\(([^)]+\.md)\))
  • A missing index.md is an error (a bundle has no entry point without it). A broken or bundle-escaping cross-link MUST be tolerated — skipped, never raised (OKF robustness rule); path resolution MUST be boundary-checked against the bundle directory, fail-closed.
  • Frontmatter is the leading ----delimited block, parsed line-oriented as key: value strings; the single required field is type; unknown fields MUST be preserved.
  • The rendered read-context is the index body (the summary) followed by each non-index concept file as a ## {type}: {title} section; empty sections are dropped.
  • type: verdict files MUST be excluded from the read-context. Prior verdicts reach the hypothesis prompt ONLY via the gated experience fold below — never via context rendering, and never via a query-time retrieval tool pointed at the bundle (which would re-leak the verdict layer).

Experience fold (ExpeL-style, the learning seam): before generation, the candidate's prior verdicts are retrieved from the store and folded into the hypothesis prompt:

  • The retrieval query key is the bundle's candidate features, read from the IR projection (§7.1) — available before any proposal exists.
  • Seeding: every type: verdict file in the bundle becomes a store entry keyed on those candidate features, with decision from frontmatter (default approved) and a rationale built from the description frontmatter plus, when present, the structured learning fields rendered as [realiseringsgrad={realization_rate}; forventet_faktisk_NOK= {expected_actual_saving_nok}].
  • Ranking is structural, never textual — surface text MUST NOT contribute to similarity. Similarity is the weighted sum 0.60 × Jaccard(affected-code sets) + 0.25 × [measure-type equality] + 0.15 × [same magnitude bucket], with magnitude buckets [0, 1e5), [1e5, 5e5), [5e5, 1e6), [1e6, ∞) over the claimed saving and Jaccard of two empty sets defined as 1. Retrieval returns the top-k by similarity, ties broken by verdict id ascending (deterministic); k MUST be positive.
  • The fold prepends the retrieved verdicts (id, decision, rationale per line) to the generation context. The rationale is the carrier of the learning signal — the fold is what lets an expert's realization-rate correction reach the next hypothesis.

Step 2 — Hypothesise (structured candidate generation)

The proposer model is asked for exactly one candidate measure as a JSON object for the IR (§7.1): project_id, measure, affected_items (list of {code, quantity, unit_cost}), claimed_saving_nok, optional assumptions. A reply that fails to parse into the typed IR MUST be retried (a blind parse-retry), never silently accepted or repaired downstream — bounded by the budget meter (§8). project_id MAY be defaulted from the project when the model omits it. IR schema invariants (§7.1) are enforced at construction, so a malformed proposal can never exist as a value.

Step 3 — Debate (maker-checker)

Candidate reasoning is debated by a two-role maker-checker pair — a proposer and a checker — alternating turns, with the debate's converged proposer output feeding generation (Step 2's context). Requirements:

  • The debate MUST be round-capped (a hard maximum-rounds bound) with an additional turn-count termination safety net above it; an unbounded debate is forbidden (§8).
  • Debate state MUST be fresh per run — no conversation state may survive from one project run into the next.
  • The checker MUST be instructed to end its reply with exactly one verdict line: VERDICT: APPROVE if the reasoning holds, or VERDICT: REJECT - <short reason> if not.
  • An optional synchronous in-run review gate on the checker MAY be enabled (the short feedback timescale, §3 Step 7); durable in-run checkpointing is NOT required.

Step 4 — Validate / falsify (two falsifiers, same candidate)

The deterministic validator gates the numbers — mandatory, blocking, never an optional plugin. It is the one endpoint-free judge that anchors the loop against swarm self-confirmation. Semantics (frozen by the golden suite, §7.2):

  1. Schema invariants already hold (IR construction, §7.1).
  2. Feasibility bound: the maximum feasible saving is capped at a policy fraction (0.30) of the affected items' total cost. (reference: computed with an LP solve whose closed form here is 0.30 × Σ quantity·unit_cost; a missing solver MUST escalate, never silently fall back.)
  3. Risk simulation: seeded Monte Carlo over uncertain unit costs yields p10/p50/p90 percentiles of the feasible saving (§7.2 fixes the procedure).
  4. Structural block: a claim above the optimistic feasible bound (p90) yields a rejection that is a distinct type from a validated proposal — carrying the claimed and feasible figures in its reason, and no percentiles — so it can never be consumed as validated.

The checker gates the reasoning (the second falsifier), parsed from the checker's LAST surfaced debate output, case-insensitively; the reject marker takes precedence and its trailing text is the reason. The gate is opt-in-reject (fail-open): VERDICT: APPROVE or a missing/unparseable marker never blocks — the validator remains the sole gate on such runs. An explicit checker REJECT MUST override an otherwise-validated outcome into a rejection (reason prefixed with the checker's reason). A validator rejection stands regardless of the checker. The two falsifiers MUST be recorded separately (§9): the provenance field mirrors ONLY the validator; the checker's decision (approve / reject / absent) is reported as its own result field. Either the checker actually gates, or the debate must not be called maker-checker.

Step 5 — Refine, informed and bounded

When the validator rejects, the next attempt MUST be informed by the falsification: the previous attempt's rejection reason is fed verbatim into the next attempt's prompt as a revision instruction. Constraints (all normative):

  • Only the most recent rejection reason is carried — never an accumulated history (bounded prompt growth).
  • Only the reason is carried — never the prior proposal JSON (the model must address the falsification, not parrot the rejected candidate).
  • The refinement loop runs under the EXISTING caps — the attempt bound (max_attempts, reference default 3) and the token/round meter (§8). "Refine until good enough" without a cap is forbidden; no new loop may be introduced.
  • The only per-attempt falsifier in this loop is the deterministic validator. Seeding generation with the checker's critique is a run-level concern outside this loop's scope.
  • Attempt 1 MUST use the unchanged base prompt (the informed block appears only after a rejection).

Step 6 — Discard or propose

The run's outcome is either the validated proposal (with its percentiles) or a typed rejection with its reason — never a bare failure. Raw results (proposals pending verdict, rejections, captured verdict files) belong to the output layer (§2) as plain JSON — the raw layer deliberately does NOT use the wiki format (that is reserved for promoted knowledge).

Step 7 — Respond to feedback (two timescales)

  • Short loop: an expert may review synchronously in-run (the optional Step-3 gate).
  • Long loop (the async file inbox): days or weeks later, an expert (or, in simulation, a persona) drops a verdict file into an inbox folder; a separate, later run picks it up whenever it lands. The system MUST be fully resumable across runs separated in time — no live-session assumption. Contract in §5. Role split (unwaivable): the system READS the inbox; the expert writes it. A run MUST NOT persist its own captured verdict back into the inbox (writing is the authoring primitive's and the promotion gate's job).

In simulation a dedicated expert persona plays the human; in production a human uses the SAME folder interface. The persona is defined once, as a shared skill artifact (§4.3).

Step 8 — Promote approved knowledge (optional + gated)

When an expert APPROVES an outcome, it may be promoted from the raw output layer into the context layer as a type: verdict concept file, navigable by the next run's seeding (closing the learning loop through the file system). Promotion is an opt-in public primitive — it MUST NOT be wired into the run itself (the system reads context; the gate/persona promotes). Full gate semantics in §6.

4. The verdict contract

Three machine-readable shapes carry expert judgement. All three share the decision vocabulary rule: the run-path decision is binaryapproved or rejected. The adjusted-approval case (the signature case in practice: the measure is worth doing but the modelled saving overstates the expected actual) is an approved decision whose rationale records the correction. approved_with_adjustment exists ONLY in bundle-seed frontmatter and in the promotion gate's accepted set (§6) — a run-path feedback contract MUST reject it.

4.1 Run-path feedback

The expert decision consumed by a run: decision ∈ {approved, rejected} (exactly two values) and a non-empty rationale string. Validated fail-fast at startup (§10).

4.2 The verdict file (raw output layer / inbox)

One verdict per JSON file, named {id}.json. Top-level fields (all required):

Field Meaning
id The learning-loop key (see minting, below). Read VERBATIM on load — never re-minted.
decision The expert decision (§4 vocabulary).
rationale Prose carrying the knowledge the validator cannot compute.
proposal_features The structural features of the judged candidate (below).

proposal_features fields: affected_codes (emitted as a SORTED list), measure_type (string), claimed_saving_nok (number), description (string; surface text — deliberately excluded from both similarity ranking and id minting).

Id minting (normative): id is the first 16 hex characters of the SHA-256 of the canonical JSON {"affected_codes": <sorted list>, "claimed_saving_nok": <number>, "measure_type": <string>} with keys sorted and separators ,/: (no whitespace). The id therefore keys on the candidate measure, not the verdict event: a structurally identical proposal maps to the same id. Because raw JSON number formatting participates in the hash (30000 vs 30000.0 differ), a loaded verdict's id MUST be kept verbatim — re-minting could diverge.

Conflict semantics (chosen, documented): the in-memory store is FIRST-write-wins per id (repeated inbox merges are idempotent); the disk layers — inbox files and promoted wiki files — are LAST-write-wins per file. Two verdicts about the same candidate share an id and hence a filename. A full verdict-conflict taxonomy is deliberately deferred until real experts produce conflicting verdicts.

4.3 The persona example artifact

The expert-reviewer persona is a shared skill: a persona prompt plus one canonical example verdict JSON with fields decision, marker, and rationale. Requirements:

  • decision MUST be a run-path value (§4.1); the canonical example is approved.
  • marker MUST be a substring of rationale — it is the traceable payload (the realization rate) a simulation follows from the persona's judgement into a later run's prompt.
  • Implementations MUST source the persona judgement from this artifact at call time (a loader, fail-fast on a missing/malformed file — it is required input), never from an inlined copy. The persona prompt's prose never names a concrete agent toolkit.

5. The inbox/outbox folder contract

The long feedback loop's folder interface (§3 Step 7). Normative:

  • One verdict per file, {id}.json, shape per §4.2. The authoring primitive creates the directory if needed and writes deterministically (reference: sorted keys, 2-space indent).
  • Tolerant load (the raw layer is written out of band; half-written or foreign files are realistic): a missing folder yields zero verdicts; files that are not .json, fail to parse, or lack a required top-level key (id, decision, rationale, proposal_features) are SKIPPED, never raised. Contrast: required inputs (the IR projection §7.1, the persona example §4.3) are fail-fast.
  • Deterministic order: files are processed sorted by filename.
  • Merge, never replace: a run ingests the inbox INTO its store (per-verdict add, first-write-wins per id) BEFORE the Step-1 fold, so a passed-in store's existing verdicts survive (cross-project threading) and repeated merges are idempotent.
  • Role split: the system reads; the expert/persona writes (§3 Step 7).

6. The promotion gate

promote lifts one APPROVED verdict from the raw output layer into the OKF context layer. Normative semantics:

  • Fail-closed: a verdict whose decision is not in {approved, approved_with_adjustment} MUST be refused with an error, writing and linking NOTHING. Only human/persona-approved knowledge enters the wiki — never raw agent output (self-contamination).
  • Provenance-stamped: the promoted file records who approved, which experiment, and when. The timestamp MUST be an explicit required argument — no wall-clock default — so promotion is deterministic and reproducible.
  • Minimal promoted file: frontmatter type: verdict, the decision, the verdict's rationale as the description field (the learning signal as prose), the verbatim verdict id, the provenance stamp, and tags. The promoted file MUST NOT reproduce a hand-authored seed's structured learning fields (realization_rate etc.) — the raw verdict model carries the signal only as rationale prose, and seeding (§3 Step 1) folds the description in.
  • Navigability: the file is written into the bundle (path-safe, fail-closed against escaping names; filename promoted-verdict-{token}.md where the token is the id sanitised to [A-Za-z0-9._-], a degenerate token falling back to a content hash) and linked from index.md — navigation follows only index cross-links, so an unlinked file is unreachable. Linking MUST be idempotent (re-promotion never double-links).
  • Neutral label: the index link label is FIXED and carries NO verdict signal. The index body flows verbatim into the rendered read-context, so a descriptive label (e.g. the rationale) would leak the learning signal around the gated fold.
  • Per-candidate growth: ids key on candidate features (§4.2), so two approvals of the same candidate share a filename — last-write-wins; the wiki grows one curated verdict file per distinct candidate, not one per verdict event.
  • (reference limitation) the index read-modify-write is not atomic — single-process use is assumed for the MVP.

7. Ground truth: IR projection and golden suite

The shared example bundle ships two JSON files that are the only ground truth ("fasit") for cross-implementation equivalence. Implementations MUST consume them unchanged.

7.1 The IR projection (validator-input.json)

The candidate measure projected into the typed cost-IR the validator consumes:

  • project_id (string), measure (string), affected_items — a non-empty list of {code: string, quantity: number ≥ 0, unit_cost: number > 0}claimed_saving_nok (number > 0), and assumptions: a map code → [low_unit_cost, high_unit_cost] giving the uncertainty band per cost code for the risk simulation (empty = degenerate, no spread).
  • Construction-time invariant: the claimed saving MUST NOT exceed the affected items' own total (Σ quantity·unit_cost); violation is a schema error, not a validator rejection.
  • Loading the IR projection from a bundle is FAIL-FAST: a missing file raises (required input — contrast the tolerant inbox, §5).

7.2 The golden suite (golden.json)

Two parts, both normative on their decided fields:

  • validator — the frozen deterministic outcome of validating the IR projection: outcome (the validated type's name), validates (true — the claim sits within the feasible range), claimed_saving_nok, nominal_feasible, and the percentiles p10, p50, p90. A conforming implementation MUST reproduce these values (approx-equality on floats) — either with the reference procedure below or an equivalent deterministic method that reproduces the golden outcomes. The meaningful assertion is validates = true (claimed ≤ p90); the frozen numbers are the regression net.

    Reference procedure (what generated the golden): nominal_feasible = 0.30 × Σ quantity·unit_cost; Monte Carlo with a Mersenne-Twister PRNG seeded 20260624, 512 samples; per sample, iterate affected_items in order and draw the unit cost uniformly from the item's assumptions band (fixed cost when no band), the sample's feasible saving being 0.30 × the sampled total; percentiles are the 1st, 5th and 9th cut points of the 10-quantiles (inclusive method) over the 512 feasible values.

  • learning_surface — what the validator CANNOT compute, encoded by the seed verdict: modelled_saving_nok, realization_rate (strictly between 0 and 1 — a realization gap), expected_actual_saving_nok (= realization_rate × modelled_saving_nok, internal consistency required), gap_source, and context_key (the context the correction holds for). This is the ExpeL seed's anchor: the signal Step 1's fold must carry into the next hypothesis, and the reason the learning loop exists at all.

8. Budget and stop criteria

Never an unbounded loop, anywhere. Normative:

  • Required at startup: positive max_rounds and max_tokens caps (the termination contract, §10). Cap objects MUST refuse construction with non-positive values.
  • Real usage, never a proxy: token accounting MUST come from the provider-reported usage (total token count) after each model call — never a word-count or character proxy. On counting paths, a response missing usage MUST fail closed (an error), not silently stop counting.
  • Structured stop: crossing a cap raises a structured stop event carrying the breached kind (tokens or rounds), the limit, and the observed value — never a silent hang.
  • Every retry loop is attempt-bounded (Steps 2 and 5); the debate is round-capped (Step 3); round ticks are charged between attempts so the meter also bounds parse-retries.

9. Provenance

Every proposal carries a first-class provenance stamp — authoritative data, not display metadata:

  • At least one citation into the source documents (file + exact text span + snippet); a run whose context yields no citable content MUST fail fast.
  • The model and role that produced the proposal. An injected test client's real model id is stamped when available; a neutral unknown is the fallback — never a fabricated name.
  • validator_decision ∈ {validated, rejected} — mirrors the DETERMINISTIC VALIDATOR only, stamped from the validator's outcome BEFORE any checker override, so a checker-gated proposal whose numbers passed is never mislabelled as validator-rejected. The checker's decision is a separate result field (§3 Step 4); the two falsifiers are never conflated.
  • The run's token usage (from the meter, §8).
  • Promotion provenance is §6 (who/experiment/when, explicit timestamp).

10. Startup contracts

ALL configuration MUST be schema-validated fail-fast at startup, BEFORE any model client is constructed: the data source (a docs directory + a positive top-k), the model map (role → model id per backend profile, each profile REQUIRING a default entry), the termination contract (§8), and the feedback shape (§4.1). The first malformed contract raises; a run never starts on a bad config.

11. Load-bearing conformance tests

A conforming implementation MUST prove each seam with a test that FAILS when the seam is detached ("green-but-dead" tests are the failure mode this rule exists to prevent). The required red-conditions, mirroring the reference suite (test names cited for cross-reference):

Seam The test MUST fail when… Reference test
Step-1 fold a prior verdict no longer reaches the next hypothesis prompt; control: an empty store changes the outcome signal test_step1_expel_loadbearing.py
Verdict-layer exclusion the realization signal appears in the rendered read-context test_okf.py (bundle-context exclusion)
Checker gate the checker's surfaced output is detached OR its REJECT no longer overrides a validated outcome test_checker_gate_loadbearing.py
Informed refinement the prior rejection reason no longer appears verbatim in the next prompt / the outcome never flips test_step5_refine_loadbearing.py
Async file loop a verdict dropped after Run A fails to reach Run B's prompt via a FRESH store; control: an empty inbox test_step7_async_loop_loadbearing.py
Promotion gate a non-approved verdict reaches the wiki; an approved one is not navigable; the index label leaks the signal test_step8_promotion_loadbearing.py
Persona artifact the example drifts from the pipeline schema, or the judgement is re-inlined instead of artifact-sourced test_persona_skill_loadbearing.py
Closed loop the two-run simulation's marker crosses runs without the promotion (or fails to cross with it) test_simulation_loadbearing.py
Golden regression the validator's decided fields diverge from golden.json test_bygg_energi_mikro.py
Context-seam purity the navigation/context module imports an agent toolkit test_okf.py (import guard)
Spec integrity this spec goes missing, names a framework, or stops documenting a consumed contract field test_method_spec_loadbearing.py

12. Cross-check table

Every field of the machine-readable contracts, mapped to its normative section (completeness is enforced by the spec-integrity test):

Field Contract Section
decision persona example / verdict file / run-path feedback §4, §4.1§4.3
marker persona example §4.3
rationale persona example / verdict file / run-path feedback §4.1§4.3
id verdict file §4.2
proposal_features verdict file §4.2
affected_codes verdict file (features) §4.2
measure_type verdict file (features) §4.2
claimed_saving_nok verdict file (features) / IR projection / golden §4.2, §7.1, §7.2
description verdict file (features) / promoted frontmatter §4.2, §6
project_id IR projection §7.1
measure IR projection §7.1
affected_items IR projection §7.1
code, quantity, unit_cost IR projection (affected item) §7.1
assumptions IR projection §7.1
outcome, validates golden (validator) §7.2
nominal_feasible, p10, p50, p90 golden (validator) §7.2
modelled_saving_nok, realization_rate, expected_actual_saving_nok golden (learning surface) §7.2
gap_source, context_key golden (learning surface) §7.2
approved, rejected decision vocabulary (run path, binary) §4, §4.1
approved_with_adjustment decision vocabulary (seed frontmatter + gate only) §4, §6
type OKF frontmatter (required field) §2, §3 Step 1
realization_rate (frontmatter) bundle seed (structured learning fields) §3 Step 1, §6