Planning-only artifacts (no code yet). Plan A- after adversarial review (critic REVISE -> revised; scope MIXED -> addressed; 19 findings, 0 overlap). Ground truth: agent-framework-orchestrations is a separate GA 1.0.0 pkg (-> dev dep); core is 1.9.0; MAF orchestrations are async. Next: /trekexecute. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9FyyENxebxVThjrn9et8C
41 KiB
Fase 1 — De-risk spikes (A–D)
Plan quality: A- (post adversarial review + revision) — APPROVE_WITH_NOTES
Generated by trekplan v2.0 on 2026-06-23 —
plan_version: 1.7
Context
Fase 0 delivered the skeleton, locked decisions (D1–D6), and a synthetic reference domain (D4). Before committing to the full architecture in Fase 2, we empirically de-risk the four assumptions that — if wrong — force a redesign (brief Intent): (A) Group Chat maker-checker beats a single-agent baseline by enough to justify its multiplicative token cost (U3 / G7); (B) the known MAF footguns behave as predicted and our guards hold — Magentic unbounded termination when limits=None (G1/B4) and shared-builder/fan-out state corruption (G2/B7); (C) a blocking deterministic hybrid-validator (B1) can structurally block an out-of-range proposal; (D) ExpeL retrieval (B2) surfaces a relevant prior verdict. These are throwaway spikes whose only job is to turn §15-register assumptions into measured facts, cheaply, under cost-discipline (D6: LOCAL profile default, hard token/round caps).
Decisive planning finding (premise-verification): the GA-slimmed Fase 0 install deliberately dropped the agent-framework[all] meta, so the orchestration builders (GroupChatBuilder, ConcurrentBuilder, MagenticBuilder) are not importable — they live in the separate package agent-framework-orchestrations, which has a clean GA release 1.0.0 depending on agent-framework-core<2,>=1.9.0 (exactly our installed core). Because the spikes are throwaway, this package is added to the dev dependency group (not core) — exactly like the PuLP solver — deferring any core-runtime commitment to Fase 2 (revised after scope review). Also resolved: the installed agent-framework-core is 1.9.0 (STATE.md correct; project CLAUDE.md says 1.8.0 and is corrected in Step 1; the project-core user-memory also says 1.8.0 — that lives outside the repo and is a session-end follow-up).
Gate-green contract (the central design choice). Each spike is split into a logic layer (pure functions/classes we author — always exercised by the quality gate, no live endpoint and no full MAF workflow run) and an integration/live layer (drives the actual MAF builders and/or a real LOCAL LLM — runs when available, otherwise skips, except Spike B where driving the builders IS the de-risk; see Step 2/Step 4). The headline empirical claims (Spike A's better/cheaper verdict; live token numbers) live in the integration/live layer and are honestly reported as endpoint-dependent. The gate stays green from the logic layer alone.
Architecture Diagram
graph TD
subgraph "Reused (Fase 0, unchanged — src/ never touched)"
RD["reference_domain.py<br/>(D4 synthetic projects)"]
BC["agent_framework.BaseChatClient<br/>(subclassed by FakeChatClient)"]
end
subgraph "New — throwaway spikes/ package (dev-only)"
H["_harness.py<br/>Budget+RoundCap (B4)<br/>FakeChatClient + minimal Agent<br/>live_local_client_or_skip() -> OpenAIChatClient"]
SM["builder smoke (Step 2)<br/>proves FakeChatClient drives the GA builders"]
A["a_groupchat.py — Spike A (U3/G7)"]
B["b_footguns.py — Spike B (G1, G2/B7)"]
C["c_validator.py — Spike C (B1)"]
D["d_verdictstore.py — Spike D (B2)"]
end
subgraph "Deliverables (tracked)"
F["docs/fase1-spikes/<br/>README + findings-{a,b,c,d}.md"]
end
H --> SM --> A & B & C & D
RD --> A & C & D
BC --> H
A --> F
B --> F
C --> F
D --> F
Codebase Analysis
- Tech stack: Python ≥3.10 (venv on 3.12), MAF GA (
agent-framework-core1.9.0,-foundry1.8.2,-openai1.8.2),pydantic2.13.4 (stable),uv,ruff0.15.18,mypy2.1.0,pytest9.1.1. Build: hatchling (wheel packagessrc/portfolio_optimiseronly — top-levelspikes/is never shipped). - Key patterns: small typed package;
from __future__ import annotations; frozen dataclasses for the domain;Protocol+runtime_checkablefor the D2 seam; fail-fast (ValueError/NotImplementedError); tests are plain pytest functions with-> None,parametrize,pytest.raises, module-scope fixtures, importing fromportfolio_optimiser. - Relevant files:
src/portfolio_optimiser/backends.py(D2 seam —Profile,ChatBackend,get_backend,LocalBackend; live-wiring of the seam is a Fase 2 concern, NOT touched here),src/portfolio_optimiser/reference_domain.py(D4 —Project,CostItem,load_reference_projects,.total_cost),src/portfolio_optimiser/data/reference_projects.json(3 synthetic projects),tests/test_*.py(test style),pyproject.toml. - Reusable code:
load_reference_projects()→ spike fixtures (A/C/D);Project.total_cost/CostItem.total_cost→ savings-IR constraint source (C);agent_framework.BaseChatClient→ subclassed by the harnessFakeChatClient;agent_framework.openai.OpenAIChatClient→ built directly by the harness for the live arm (the D2LocalBackendseam is intentionally left un-wired until Fase 2, sosrc/stays untouched and the throwaway spikes add no core surface). - External tech (verified by introspection + PyPI, not docs): orchestration builders in
agent_framework.orchestrations(needagent-framework-orchestrations); chat clients inagent_framework.openai(OpenAIChatClient);ContextProvider/MemoryContextProvidertop-level. MAF workflows/orchestrations are async (await workflow.run(...)), so the spike tests usepytest-asyncio(added in Step 1). Exact builder method signatures are confirmed by introspection at coding time (brief-sanctioned) and de-risked once up-front by the Step 2 builder smoke. - Recent git activity:
b57aa83Fase 0 (D4+D2+tests),491a746deps GA-fix + lockfile. Clean tree onmain, synced to origin.
Research Sources
Ground-truth API/dependency verification — introspection + PyPI metadata are authoritative here (no web docs needed).
| Topic | Source | Key Findings | Confidence |
|---|---|---|---|
| Orchestration builders' real import path | import agent_framework.orchestrations (introspection) |
GroupChatBuilder, ConcurrentBuilder, MagenticBuilder, StandardMagenticManager, SequentialBuilder, TerminationCondition, MagenticPlanReviewRequest live there; not top-level |
high |
| Orchestrations package availability/stability | PyPI JSON | GA 1.0.0 (uploaded 2026-06-18, not yanked); requires_dist: agent-framework-core<2,>=1.9.0; rest b/rc |
high |
| Installed core version (resolves brief ambiguity) | uv pip show agent-framework-core |
1.9.0 (STATE correct; CLAUDE.md 1.8.0 → corrected Step 1) | high |
| MAF orchestrations are async | introspection (WorkflowBuilder/orchestration .run) |
tests need pytest-asyncio; integration arm is async |
high |
| Solver lib for Spike C | PyPI / project knowledge | pulp (PuLP) MIT, pure-Python, bundles a CBC binary in its wheel → faithful solver-in-the-loop (R2) |
high |
Implementation Plan
Each step targets one focused change and follows the repo's TDD-ish style. Per the gate-green contract above, the deterministic logic layer is always exercised; integration/live layers are gated.
Step 1: Scaffold throwaway spikes package and pin dev-only orchestration + solver + async deps
- Files:
pyproject.toml,CLAUDE.md,spikes/__init__.py,tests/spikes/__init__.py,tests/spikes/test_imports.py,docs/fase1-spikes/README.md,uv.lock - Changes: In
pyproject.tomladd to[project.optional-dependencies].dev(NOT core — spikes are throwaway):agent-framework-orchestrations>=1.0.0(GA; resolves with core 1.9.0),pulp>=2.8(CBC solver),pytest-asyncio>=0.24. Set[tool.pytest.ini_options] pythonpath = ["src", "."](repo-root added soimport spikesresolves;spikes/has no name-collision with any installed dist, so it cannot shadow them) andasyncio_mode = "auto". Set[tool.ruff] src = ["src", "tests", "spikes"]. Createspikes/__init__.pyandtests/spikes/__init__.py(docstring: "Throwaway Fase 1 de-risk spikes — dev-only, never shipped in the wheel; safe to delete after findings recorded"). Createdocs/fase1-spikes/README.md(the four spikes, the resolved version gate, a Disposal subsection:rm -rf spikes tests/spikes docs/fase1-spikes+ revert the threedev/toolpyproject edits). InCLAUDE.mdcorrect the stale stack lineMAF (agent-framework1.8.0)→MAF (agent-framework-core1.9.0)(honors the brief's Spike-A version precondition). Runuv sync --extra dev, then re-confirmuv pip show agent-framework-orchestrationsresolves cleanly (premise re-verification). (new files: all exceptpyproject.toml,CLAUDE.md) - Reuses: existing
pyproject.toml/CLAUDE.mdstructure (Fase 0). - Test first:
- File:
tests/spikes/test_imports.py(new) - Verifies:
import spikes;from agent_framework.orchestrations import GroupChatBuilder, ConcurrentBuilder, MagenticBuilder, StandardMagenticManager, TerminationCondition;import pulp;import pytest_asyncio. - Pattern:
tests/test_smoke.py
- File:
- Verify:
uv run --extra dev pytest tests/spikes/test_imports.py -q→ expected: passed;uv pip show agent-framework-orchestrations→Version: 1.0.0;uv run ruff check .→ exit 0 - On failure: retry — if the orchestrations import fails after sync, confirm the lock resolved
agent-framework-orchestrations==1.0.0; if anything pulls a pre-release, pin tighter. Revert withgit checkout -- pyproject.toml CLAUDE.md uv.lockif unrecoverable. - Checkpoint:
git commit -m "build(fase1): add dev orchestration + solver + async deps, scaffold spikes" - Manifest:
manifest: expected_paths: - pyproject.toml - CLAUDE.md - spikes/__init__.py - tests/spikes/__init__.py - tests/spikes/test_imports.py - docs/fase1-spikes/README.md - uv.lock min_file_count: 7 commit_message_pattern: "^build\\(fase1\\): add dev orchestration \\+ solver \\+ async deps" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/backends.py - src/portfolio_optimiser/reference_domain.py must_contain: - path: pyproject.toml pattern: "agent-framework-orchestrations" - path: pyproject.toml pattern: "pytest-asyncio" - path: CLAUDE.md pattern: "agent-framework-core` 1.9.0"
Step 2: Shared spike harness + builder smoke (budget caps, fake client, live gate, async builder de-risk)
- Files:
spikes/_harness.py,tests/spikes/test_harness.py - Changes: Implement the cost/stop invariant (B4) and the test seams shared by all spikes. (1)
@dataclass Budget(max_tokens: int, max_rounds: int)— constructor raisesValueErrorfor non-positive caps (refuse to start without caps, A4); aTokenMeterpluscharge(tokens)/tick_round()raiseBudgetExceeded(kind, limit, observed)the moment a cap is crossed. (Two exception types is intentional and documented in the module:ValueError= bad construction argument;BudgetExceeded= runtime breach.) (2)FakeChatClient(BaseChatClient)returning scripted deterministic responses and counting tokens by word-count; plusfake_agent(client, name, instructions)building a minimal realagent_framework.Agentbacked by the fake client so the orchestration builders get genuine participants. (3)live_local_client_or_skip()→ buildsagent_framework.openai.OpenAIChatClient(base_url=env PORTFOLIO_LOCAL_BASE_URL, model=env PORTFOLIO_LOCAL_MODEL)directly, orpytest.skip(...)when env is unset (the D2LocalBackendseam is deliberately not used — its live-wiring is Fase 2). (4) A builder smoke that front-loads the biggest risk: construct aGroupChatBuilderand aConcurrentBuilderwith twofake_agents andawaita trivial run, asserting they execute — this is the one place that provesFakeChatClientcan drive the GA builders. If it cannot, that is itself a recorded de-risk finding (escalate), surfaced before any spike is built on the assumption. (new file) - Reuses:
agent_framework.BaseChatClient,agent_framework.Agent;agent_framework.openai.OpenAIChatClient;agent_framework.orchestrations.{GroupChatBuilder, ConcurrentBuilder}; the D6 cost contract. - Test first:
- File:
tests/spikes/test_harness.py(new) - Verifies:
Budget(0, 5)andBudget(5, 0)raiseValueError;charge/tick_roundraiseBudgetExceededpast their caps;FakeChatClientreturns scripted replies in order and accumulates tokens;live_local_client_or_skip()skips when env unset; builder smoke (async test) constructs and runsGroupChatBuilder+ConcurrentBuilderwith fake agents and asserts completion. - Pattern:
tests/test_backends.py(parametrize+pytest.raises)
- File:
- Verify:
uv run --extra dev pytest tests/spikes/test_harness.py -q→ expected: passed;uv run ruff check spikes tests/spikes→ exit 0 - On failure: escalate — if the builder smoke proves
FakeChatClientcannot drive the GA builders, STOP and record it indocs/fase1-spikes/README.mdas a primary de-risk finding (it changes Spikes A/B); decide with the operator whether to back the builders with a tiny canned-LLM stub or restructure. Otherwise revert withgit checkout -- spikes/_harness.py tests/spikes/test_harness.py. - Checkpoint:
git commit -m "feat(fase1): shared spike harness + builder smoke (budget caps, fake client, live gate)" - Manifest:
manifest: expected_paths: - spikes/_harness.py - tests/spikes/test_harness.py min_file_count: 2 commit_message_pattern: "^feat\\(fase1\\): shared spike harness \\+ builder smoke" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/backends.py must_contain: - path: spikes/_harness.py pattern: "class BudgetExceeded" - path: spikes/_harness.py pattern: "def live_local_client_or_skip" - path: tests/spikes/test_harness.py pattern: "GroupChatBuilder"
Step 3: Spike A — Group Chat maker-checker vs single-agent (U3 / G7)
- Files:
spikes/a_groupchat.py,tests/spikes/test_a_groupchat.py,docs/fase1-spikes/findings-a.md - Changes: Logic layer (always green):
Armresult dataclass (rounds, stalls, tokens, caught_flaw);make_termination(n_rounds=3)(N fixed at 3 per reviewer refinement #1);verdict(mc: Arm, single: Arm) -> dictcomputingbetter = mc.caught_flaw and not single.caught_flawandaffordable = mc.tokens <= 3 * single.tokensandpassed = better and affordable, plus a comparison table renderer. Integration/live layer (gated):run_live(project)builds the 3-role maker-checker (proposer · critic · validator) viaGroupChatBuilder+make_termination()and a single-agent baseline, runs both againstload_reference_projects()[0]carrying a planted flaw (a savings candidate violating an obvious constraint, e.g. negative residual quantity), measures real numbers, writes them + the computed verdict tofindings-a.md. (new file) - Reuses:
spikes._harness(Budget, FakeChatClient, fake_agent, live gate);reference_domain.load_reference_projects;agent_framework.orchestrations.GroupChatBuilder/TerminationCondition. - Test first:
- File:
tests/spikes/test_a_groupchat.py(new) - Verifies (logic, non-tautological — inputs are varied, not a single scripted constant):
verdict()returnspassed=Truewhen mc caught & single missed & tokens within 3×;passed=Falsewhen mc tokens = 5× single (affordable=False) even if better;passed=Falsewhen both caught the flaw (better=False);make_termination(3)stops at 3 rounds. A@pytest.mark.skipif(no endpoint)live test runsrun_liveand asserts ≤3 rounds, cap respected, a findings table emitted. - Pattern:
tests/test_reference_domain.py+tests/test_backends.py
- File:
- Verify:
uv run --extra dev pytest tests/spikes/test_a_groupchat.py -q→ expected: logic tests passed (live test skipped without endpoint) - On failure: retry — confirm
GroupChatBuildermethod names by introspection and adjustrun_live; the logic layer is independent of the MAF surface. Revert withgit checkout -- spikes/a_groupchat.py tests/spikes/test_a_groupchat.py docs/fase1-spikes/findings-a.mdif unrecoverable. - Checkpoint:
git commit -m "feat(fase1): spike A - group chat maker-checker vs single-agent" - Manifest:
manifest: expected_paths: - spikes/a_groupchat.py - tests/spikes/test_a_groupchat.py - docs/fase1-spikes/findings-a.md min_file_count: 3 commit_message_pattern: "^feat\\(fase1\\): spike A" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/reference_domain.py must_contain: - path: spikes/a_groupchat.py pattern: "GroupChatBuilder" - path: tests/spikes/test_a_groupchat.py pattern: "skipif" - path: docs/fase1-spikes/findings-a.md pattern: "token"
Step 4: Spike B — Magentic limits=None footgun + ConcurrentBuilder state isolation (G1 / G2)
- Files:
spikes/b_footguns.py,tests/spikes/test_b_footguns.py,docs/fase1-spikes/findings-b.md - Changes: Driving the builders is the de-risk here, so these tests run in the gate (no live LLM —
FakeChatClient/fake_agent), having been de-risked by the Step 2 builder smoke. (a) Magentic unbounded (G1):MagenticBuilder+StandardMagenticManagerwithmax_round_count=Noneover a fake agent that never finalizes; drive it under the harness round/iteration guard (the deterministic assertion — wall-clock is only a secondary safety net, not asserted) and assert it does NOT self-terminate before the guard fires → confirms an explicit limit is required (B4). With explicit limits → terminates cleanly. (b) Fan-out state isolation (G2/B7):fresh_workflow()factory + a stateful executor; assert a SHARED instance bleeds state across the 3 projects and a FRESH instance per run shows zero bleed (B7). Writes observations + a token-use line ("0 — no live LLM") tofindings-b.md. (new file) - Reuses:
spikes._harness(round guard, FakeChatClient, fake_agent);reference_domain.load_reference_projects;agent_framework.orchestrations.{MagenticBuilder, StandardMagenticManager, ConcurrentBuilder}. - Test first:
- File:
tests/spikes/test_b_footguns.py(new) - Verifies: unbounded Magentic hits the round guard without self-terminating (asserts guard fired, not a natural stop); bounded Magentic stops within its limit; shared-instance fan-out leaks state (assertion of bleed); fresh-instance fan-out asserts no bleed across all 3 projects.
- Pattern:
tests/test_backends.py
- File:
- Verify:
uv run --extra dev pytest tests/spikes/test_b_footguns.py -q→ expected: passed - On failure: escalate — if
MagenticBuilder/ConcurrentBuildercannot be driven by a fake agent despite the Step 2 smoke, record it as a genuine de-risk finding infindings-b.md(it answers the footgun question by other means) before revertinggit checkout -- spikes/b_footguns.py tests/spikes/test_b_footguns.py docs/fase1-spikes/findings-b.md. - Checkpoint:
git commit -m "feat(fase1): spike B - magentic unbounded + concurrent state isolation" - Manifest:
manifest: expected_paths: - spikes/b_footguns.py - tests/spikes/test_b_footguns.py - docs/fase1-spikes/findings-b.md min_file_count: 3 commit_message_pattern: "^feat\\(fase1\\): spike B" bash_syntax_check: [] forbidden_paths: [] must_contain: - path: spikes/b_footguns.py pattern: "ConcurrentBuilder" - path: spikes/b_footguns.py pattern: "fresh_workflow" - path: docs/fase1-spikes/findings-b.md pattern: "token"
Step 5: Spike C — blocking hybrid validator (IR→PuLP→Monte Carlo, self-repair) (B1)
- Files:
spikes/c_validator.py,tests/spikes/test_c_validator.py,docs/fase1-spikes/findings-c.md - Changes: Fully deterministic — no MAF builders. Pydantic IR
SavingsProposal(project_id, measure, affected cost codes, claimed saving NOK, assumptions) with field validators + a cross-field@model_validator(claimed saving ≤ affected items' total; quantities ≥ 0).validate_proposal(proposal) -> ValidatedProposal | Rejection: (1) Pydantic schema validation; (2) PuLP feasibility/optimization over the project's cost items (uses PuLP's bundled CBC binary — if CBC is genuinely absent the step escalates; no silent LP-relaxation fallback); (3) Monte Carlo over uncertain unit-costs → P10/P50/P90 (stdlibrandomseeded +statistics.quantiles); (4) structural block: an out-of-range/infeasible proposal returns aRejectionthat can never be consumed as aValidatedProposal.self_repair(generate, max_attempts=N)capped, then hard-stops. The LLM-generation entry is a thin wrapper gated bylive_local_client_or_skip(); all validator/solver/MC logic is tested with crafted IR directly. Writes a token-use line ("0 — validator deterministic; live generation gated") + percentiles tofindings-c.md. (new file) - Reuses:
pydantic.BaseModel/model_validator;pulp;reference_domain.Project/CostItem/load_reference_projects;spikes._harness(Budget for the self-repair cap, live gate). - Test first:
- File:
tests/spikes/test_c_validator.py(new) - Verifies: an out-of-range proposal is structurally blocked —
validate_proposalreturnsRejectionand raises if treated as validated; a valid proposal returnsValidatedProposalwith P10 ≤ P50 ≤ P90; Monte Carlo reproducible under the fixed seed;self_repairstops atmax_attempts. - Pattern:
tests/test_reference_domain.py+pytest.raises
- File:
- Verify:
uv run --extra dev pytest tests/spikes/test_c_validator.py -q→ expected: passed - On failure: escalate (if CBC absent — record platform finding) / otherwise revert
git checkout -- spikes/c_validator.py tests/spikes/test_c_validator.py docs/fase1-spikes/findings-c.md. - Checkpoint:
git commit -m "feat(fase1): spike C - blocking hybrid validator (IR/solver/monte-carlo)" - Manifest:
manifest: expected_paths: - spikes/c_validator.py - tests/spikes/test_c_validator.py - docs/fase1-spikes/findings-c.md min_file_count: 3 commit_message_pattern: "^feat\\(fase1\\): spike C" bash_syntax_check: [] forbidden_paths: [] must_contain: - path: spikes/c_validator.py pattern: "model_validator" - path: spikes/c_validator.py pattern: "class Rejection" - path: tests/spikes/test_c_validator.py pattern: "P10"
Step 6: Spike D — VerdictStore + ExpeL retrieval (B2)
- Files:
spikes/d_verdictstore.py,tests/spikes/test_d_verdictstore.py,docs/fase1-spikes/findings-d.md - Changes: Minimal in-memory VerdictStore: a list of
Verdict(id, proposal_features, decision, rationale)seeded with 10–20 synthetic verdicts from the reference domain. Similarity defined operationally (reviewer refinement #2): a weighted score over structured fields — Jaccard on the affected cost-code set + match onsavings_measure_type+ a magnitude-bucket match on claimed saving — not raw description text.retrieve(proposal, k) -> list[Verdict]ranks by that score (this is the always-tested unit that satisfies SC-D). A thin customContextProvidersubclass wrapsretrieve()for ExpeL few-shot injection; its injection is asserted only against the introspected interface (if the abstract surface differs at coding time, theretrieve()ranking remains the deliverable and the SC-D assertion). The embedding-based similarity path is removed as out of scope for a throwaway spike (noted as a Fase-2 option infindings-d.md). Writes a token-use line ("0 — deterministic retrieval") + a top-K example tofindings-d.md. (new file) - Reuses:
agent_framework.ContextProvider(top-level);reference_domain.load_reference_projects;spikes._harness. - Test first:
- File:
tests/spikes/test_d_verdictstore.py(new) - Verifies (non-tautological by construction): seed a store where the true match shares the structured similarity fields with the query but uses different description text, while 2–3 decoys share surface description text but differ in the structured fields; assert the true match is top-1 and within top-K; assert deterministic ordering. (ContextProvider-injection assertion included only if the interface is confirmed;
retrieve()ranking is the guaranteed SC-D check.) - Pattern:
tests/test_reference_domain.py
- File:
- Verify:
uv run --extra dev pytest tests/spikes/test_d_verdictstore.py -q→ expected: passed - On failure: retry — confirm
ContextProviderabstract methods by introspection; keepretrieve()ranking separable. Revertgit checkout -- spikes/d_verdictstore.py tests/spikes/test_d_verdictstore.py docs/fase1-spikes/findings-d.mdif unrecoverable. - Checkpoint:
git commit -m "feat(fase1): spike D - verdictstore + expel retrieval" - Manifest:
manifest: expected_paths: - spikes/d_verdictstore.py - tests/spikes/test_d_verdictstore.py - docs/fase1-spikes/findings-d.md min_file_count: 3 commit_message_pattern: "^feat\\(fase1\\): spike D" bash_syntax_check: [] forbidden_paths: [] must_contain: - path: spikes/d_verdictstore.py pattern: "def retrieve" - path: tests/spikes/test_d_verdictstore.py pattern: "decoy"
Step 7: Consolidate findings, document disposal, confirm the green quality gate
- Files:
docs/fase1-spikes/README.md - Changes: Update
docs/fase1-spikes/README.mdto a consolidated summary table (spike → assumption → result → confirmed/refuted → implication for Fase 2), pulling each per-spike note's verdict and token-use line (eachfindings-{a,b,c,d}.mdalready exists from Steps 3–6). Confirm the Disposal subsection (from Step 1) is complete. Run the full quality gate across the whole repo. (no new findings files here — they are owned by their spike steps) - Reuses: the per-spike findings written in Steps 3–6.
- Test first: n/a (documentation + gate; the gate IS the verification).
- Verify:
uv run ruff check .→ exit 0;uv run ruff format --check .→ exit 0;uv run mypy src→ exit 0;uv run --extra dev pytest -q→ expected: all passed (originals + spike logic layers; live arms skipped without endpoint) - On failure: escalate — a red gate means a prior step regressed; bisect by step and fix before consolidating.
- Checkpoint:
git commit -m "docs(fase1): consolidate spike findings + confirm green quality gate" - Manifest:
manifest: expected_paths: - docs/fase1-spikes/README.md - docs/fase1-spikes/findings-a.md - docs/fase1-spikes/findings-b.md - docs/fase1-spikes/findings-c.md - docs/fase1-spikes/findings-d.md min_file_count: 5 commit_message_pattern: "^docs\\(fase1\\): consolidate spike findings" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/backends.py must_contain: - path: docs/fase1-spikes/README.md pattern: "Fase 2" - path: docs/fase1-spikes/README.md pattern: "Disposal"
Failure recovery rules
- revert — undo this step's changes (
git checkout -- {files}), do not proceed. - retry — confirm the real MAF method signature by introspection, adjust, then revert if still failing.
- escalate — stop entirely; needs human judgment (Step 2 builder smoke fails / Step 4 builder un-fakeable / Step 5 CBC absent / Step 7 gate regression). For Steps 2 and 4, an escalate that records the MAF limitation is itself a valid de-risk outcome.
- Checkpoint — commit after each green step.
Alternatives Considered
| Approach | Pros | Cons | Why rejected |
|---|---|---|---|
Spikes under src/portfolio_optimiser/spikes/ |
Single import root | Ships throwaway code in the wheel | Top-level spikes/ keeps the wheel clean (hatchling packages only src/portfolio_optimiser) |
agent-framework-orchestrations in core deps |
One home for the locked debate default | Permanent Fase-2 commitment made under a throwaway task (scope creep, flagged in review) | Put it in dev like PuLP; promote to core in Fase 2 when the real flow is built |
Wire LocalBackend.create_chat_client() in Step 2 for the live arm |
Fills the D2 seam | Touches src/ under a throwaway task; forces editing test_backends.py; non-throwaway commitment |
Build OpenAIChatClient directly in the throwaway harness; defer seam-wiring to Fase 2; src/ stays untouched |
| Require a live LOCAL endpoint for all spike tests | Tests the real thing | Gate red without an endpoint; violates SC-gate + D6 | Logic layer always green; integration/live arm gated |
| numpy for Monte Carlo | Familiar | New dep for trivial sampling | stdlib random + statistics.quantiles — zero new dep |
agent-framework[all] to get orchestrations |
One line | Re-introduces the beta/--prerelease/alpha-pydantic problem Fase 0 removed (491a746) |
Pin the GA agent-framework-orchestrations>=1.0.0 only |
| Embedding similarity (Spike D) | "Real" RAG | Needs live endpoint; non-deterministic; serves no SC; over-builds throwaway | Deterministic structured-field similarity; embeddings = Fase-2 note only |
| Hand-rolled feasibility instead of PuLP | No dep | Not a faithful solver-in-the-loop (B1/R2) | PuLP (bundled CBC) keeps the spike honest |
Test Strategy
- Framework: pytest 9.1.1 +
pytest-asyncio(asyncio_mode = "auto"); plain functions with-> None,parametrize,pytest.raises; fixtures fromload_reference_projects(). Async tests for the MAF-builder integration. - Existing patterns:
tests/test_backends.py,tests/test_reference_domain.py,tests/test_smoke.py. - New tests in this plan: 6 modules (imports, harness+builder-smoke, A, B, C, D). Determinism via
FakeChatClient+ fixed seeds; live arms isolated behind@pytest.mark.skipif(no endpoint). Spike B drives the builders in-gate (no endpoint) — de-risked by the Step 2 smoke.
Tests to write
| Type | File | Verifies | Model test |
|---|---|---|---|
| Import | tests/spikes/test_imports.py |
orchestrations + pulp + pytest-asyncio + spikes import |
tests/test_smoke.py |
| Unit+async | tests/spikes/test_harness.py |
Budget caps; FakeChatClient; skip gate; builder smoke | tests/test_backends.py |
| Unit | tests/spikes/test_a_groupchat.py |
varied-input verdict logic; ≤3-round termination; gated live arm | tests/test_reference_domain.py |
| Unit+async | tests/spikes/test_b_footguns.py |
unbounded Magentic needs guard; zero fan-out bleed | tests/test_backends.py |
| Unit | tests/spikes/test_c_validator.py |
structural block; P10≤P50≤P90; self-repair cap | tests/test_reference_domain.py |
| Unit | tests/spikes/test_d_verdictstore.py |
non-tautological top-K retrieval | tests/test_reference_domain.py |
Risks and Mitigations
| Priority | Risk | Location | Impact | Mitigation |
|---|---|---|---|---|
| High | FakeChatClient cannot drive the async GA builders |
spikes/_harness.py (smoke), A/B |
Spikes A/B integration blocked | Step 2 builder smoke front-loads this; failure is a recorded de-risk finding (escalate), not a late surprise; logic layers (A verdict, B isolation) stay green regardless |
| High | Real builder method signatures differ from planned wiring | A/B integration | Integration code won't run as written | Confirm by introspection at coding time (brief-sanctioned); per-step retry; logic layers independent of the MAF surface |
| Medium | No LOCAL endpoint during the session | A/C live arms | Live numbers not captured | Logic layers prove the machinery; live arms skip; findings note records "live arm not run" honestly |
| Medium | PuLP CBC binary missing on Intel mac | spikes/c_validator.py |
Solver call fails | PuLP wheels bundle CBC; if genuinely absent → escalate (recorded), no silent fallback |
| Low | Async-mode misconfig breaks existing sync tests | pyproject.toml |
Original 12 tests affected | asyncio_mode="auto" only auto-marks coroutine tests; sync tests unaffected — Step 7 gate catches any regression |
| Low | 2 ExperimentalWarnings (SKILLS/HARNESS) at import (G8) |
all imports | Noise only | Harmless; do not enable filterwarnings=error |
| Low | .claude/projects/** committed to a to-be-public repo |
repo | Planning scratch in history | Out of scope here; flag at Fase 4 release prep |
Assumptions
| # | Assumption | Why unverifiable now | Impact if wrong |
|---|---|---|---|
| 1 | LOCAL profile = reachable OpenAI-compatible endpoint via PORTFOLIO_LOCAL_* env |
Depends on operator's running Ollama/LM Studio | Live arms skip; logic layers still pass — low impact |
| 2 | FakeChatClient can drive the GA builders |
Not yet installed/introspected to method level | Step 2 smoke decides early; failure is a documented finding, not silent breakage |
| 3 | PuLP's bundled CBC works on the Intel-mac venv | Not installed yet | Step 5 escalates with a recorded platform finding |
| 4 | N = 3 rounds is a fair maker-checker bound | Judgment from G7 token-discipline | If too tight to converge live, bump to ≤5 and re-record (documented range) |
Verification
Per-step manifests are checked automatically by trekexecute. These map to the brief Success Criteria.
- Spike A:
uv run --extra dev pytest tests/spikes/test_a_groupchat.py -q→ varied-input verdict logic passes; gated live arm (when run) converges ≤3 rounds, cap respected, comparison table emitted (SC-A; empirical better/cheaper verdict is endpoint-dependent and so reported) - Spike B:
uv run --extra dev pytest tests/spikes/test_b_footguns.py -q→ unbounded Magentic needs an external guard; fresh-instance fan-out zero state-bleed (SC-B) - Spike C:
uv run --extra dev pytest tests/spikes/test_c_validator.py -q→ out-of-range blocked structurally; valid proposal yields P10/P50/P90; self-repair capped (SC-C) - Spike D:
uv run --extra dev pytest tests/spikes/test_d_verdictstore.py -q→ top-K retrieval returns the structurally-similar verdict over surface-text decoys (SC-D) - Gate:
uv run ruff check .exit 0;uv run ruff format --check .clean;uv run mypy srcexit 0;uv run --extra dev pytest -qall passed (SC-gate) - Findings:
docs/fase1-spikes/has README (summary table + Disposal) + findings-{a,b,c,d}.md, each with a confirmed/refuted verdict and a token-use line (brief Goal + NFR)
Estimated Scope
- Files to modify: 2 (
pyproject.toml,CLAUDE.md;uv.lockregenerated) - Files to create: ~19 (4 spike modules +
_harness.py+ 6 test modules + 2__init__.py+ 5 docs notes + README owned by Step 1/7) - Complexity: medium (logic is modest; the real unknown — MAF builder ergonomics with a fake client — is de-risked up-front by the Step 2 smoke)
Execution Strategy
Session 1: Foundation
- Steps: 1, 2
- Wave: 1
- Depends on: none
- Scope fence:
- Touch:
pyproject.toml,CLAUDE.md,spikes/__init__.py,spikes/_harness.py,tests/spikes/{__init__,test_imports,test_harness}.py,docs/fase1-spikes/README.md,uv.lock - Never touch:
src/**, the four spike modules
- Touch:
Session 2: Spike A
- Steps: 3
- Wave: 2
- Depends on: Session 1
- Scope fence:
- Touch:
spikes/a_groupchat.py,tests/spikes/test_a_groupchat.py,docs/fase1-spikes/findings-a.md - Never touch: other spike modules,
src/**,pyproject.toml,CLAUDE.md
- Touch:
Session 3: Spike B
- Steps: 4
- Wave: 2
- Depends on: Session 1
- Scope fence:
- Touch:
spikes/b_footguns.py,tests/spikes/test_b_footguns.py,docs/fase1-spikes/findings-b.md - Never touch: other spike modules,
src/**,pyproject.toml,CLAUDE.md
- Touch:
Session 4: Spike C
- Steps: 5
- Wave: 2
- Depends on: Session 1
- Scope fence:
- Touch:
spikes/c_validator.py,tests/spikes/test_c_validator.py,docs/fase1-spikes/findings-c.md - Never touch: other spike modules,
src/**,pyproject.toml,CLAUDE.md
- Touch:
Session 5: Spike D
- Steps: 6
- Wave: 2
- Depends on: Session 1
- Scope fence:
- Touch:
spikes/d_verdictstore.py,tests/spikes/test_d_verdictstore.py,docs/fase1-spikes/findings-d.md - Never touch: other spike modules,
src/**,pyproject.toml,CLAUDE.md
- Touch:
Session 6: Consolidation
- Steps: 7
- Wave: 3
- Depends on: Sessions 2–5
- Scope fence:
- Touch:
docs/fase1-spikes/README.md - Never touch:
spikes/**,src/**,pyproject.toml,CLAUDE.md
- Touch:
Execution Order
- Wave 1: Session 1 (foundation — includes the builder smoke that de-risks all spikes)
- Wave 2: Sessions 2, 3, 4, 5 (parallel — independent spike modules sharing only the read-only harness)
- Wave 3: Session 6 (after Wave 2)
Grouping rules applied
- Steps sharing files → same session (1+2 = foundation).
- Independent spike modules → separate sessions (parallelizable in Wave 2).
- Consolidation depends on all spikes → Wave 3.
Plan Quality Score
| Dimension | Weight | Score | Notes |
|---|---|---|---|
| Structural integrity | 0.15 | 90 | Foundation→spikes→consolidation; src/ untouched so scope fences are accurate |
| Step quality | 0.20 | 88 | TDD-first; false LocalBackend reuse removed; findings owned per step |
| Coverage completeness | 0.20 | 90 | SC-A/B/C/D + gate + findings mapped; A's empirical verdict honestly endpoint-dependent |
| Specification quality | 0.15 | 86 | Real import paths; builder ergonomics de-risked by Step 2 smoke; CBC fallback decided |
| Risk & pre-mortem | 0.15 | 90 | Biggest risk (fake-drives-builders) front-loaded with a green-keeping logic layer |
| Headless readiness | 0.10 | 92 | On-failure + Checkpoint + Manifest per step; expected_paths ⊆ Files |
| Manifest quality | 0.05 | 90 | All manifests checkable; orphan paths fixed |
| Weighted total | 1.00 | 89 | Grade: A- |
Adversarial review:
- Plan critic: REVISE → addressed (2 blockers + 8 major + 4 minor); see Revisions.
- Scope guardian: MIXED → addressed (orchestrations moved to dev; embedding path dropped; token-reporting + findings-location ratified); see Revisions.
Revisions
Added after Phase 9 adversarial review (plan-critic + scope-guardian, deduped: 19 findings, 0 overlap).
| # | Finding (rule_key) | Severity | Resolution |
|---|---|---|---|
| 1 | live-arm/impossible-localbackend-wiring | blocker | Harness builds OpenAIChatClient directly; dropped the false LocalBackend reuse; D2 seam-wiring deferred to Fase 2; src/ untouched |
| 2 | fragile-assumption/fakeclient-drives-async-builders | blocker | Added Step 2 builder smoke (front-loads the risk); added pytest-asyncio + asyncio_mode=auto; split each spike into always-green logic layer + gated integration/live layer; failure is a recorded escalate |
| 3 | structural/scope-fence-vs-live-arm | major | Resolved by #1 — src/** is now genuinely never touched; fences accurate |
| 4 | missing-setup/async-test-runner | major | pytest-asyncio>=0.24 added (Step 1); async integration tests |
| 5 | verification/spike-a-verdict-tautological | major | Logic test now varies inputs to prove the verdict function; empirical U3/G7 verdict explicitly endpoint-dependent (live arm) |
| 6 | underspecified/contextprovider-inject-no-target | major | retrieve() ranking is the guaranteed SC-D unit; ContextProvider injection asserted only against the introspected interface |
| 7 | manifest/expected-paths-not-subset-of-files | major | All Files: lines now include test/findings/uv.lock/CLAUDE.md paths |
| 8 | ordering/findings-files-creation-ambiguous | major | Each spike step (3–6) owns and creates its findings-X.md; Step 7 only consolidates the README |
| 9 | missing-error-handling/cbc-fallback-deferred | major | Committed to PuLP's bundled CBC; absence → escalate (recorded), no silent LP-relaxation |
| 10 | coverage/spike-a-precondition-downgraded | major | Step 1 corrects CLAUDE.md version; installed version confirmed 1.9.0; memory fix noted as session follow-up |
| 11 | creep — orchestrations in core deps | major | Moved agent-framework-orchestrations to dev (throwaway scope), like PuLP |
| 12 | creep — Spike D embedding path | minor | Removed; deterministic similarity only; embeddings = Fase-2 note |
| 13 | gap — token reporting B/C/D | minor | Each findings note carries a token-use line (incl. "0 — no live LLM") |
| 14 | gap — findings location vs brief | minor | Kept docs/fase1-spikes/ (tracked, survives, consistent with research/plan) — ratified as a deliberate refinement; operator can object |
| 15 | dependency — re-confirm orchestrations resolves | minor | Step 1 runs uv pip show agent-framework-orchestrations post-sync |
| 16 | minor/pythonpath-root-shadowing | minor | Documented: repo-root added for import spikes; no name collision with installed dists |
| 17 | minor/budget-two-exception-types | minor | Documented intent: ValueError = bad ctor arg, BudgetExceeded = runtime breach |
| 18 | minor/wall-clock-flaky-assertion | minor | Spike B asserts the round/iteration guard; wall-clock is a secondary safety net only |
| 19 | minor/no-teardown-for-throwaway-spikes | minor | Step 1/7 README Disposal subsection: rm -rf + revert the pyproject dev/tool edits |