Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019any9zfGNNwWJPX5Zq2QRz
9.1 KiB
| type | review_version | created | task | slug | project_dir | brief_path | scope_sha_start | scope_sha_end | reviewed_files_count | verdict | findings | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| trekreview | 1.0 | 2026-06-26 | Fase 3: orkestrator fan-out over N porteføljeprosjekter med state-isolasjon + delt læring | fase3-portfolio-fanout | .claude/projects/2026-06-26-fase3-portfolio-fanout/ | .claude/projects/2026-06-26-fase3-portfolio-fanout/brief.md | 497399e0aa |
207f057075 |
15 | WARN |
|
Review: Fase 3 — orkestrator fan-out over N porteføljeprosjekter med state-isolasjon + delt læring
Executive Summary
Verdict: WARN — 0 BLOCKER, 2 MAJOR. Look first at the fake-model placeholder leak in run.py:197: it stamps a hardcoded sentinel into ProvenanceStamp.model for any deployer who supplies a custom client_factory, falsifying a load-bearing provenance invariant on every project in the fan-out. The second MAJOR is a coverage gap — the portfolio rejected_count / validated-only-sum branch (run.py:240-241) is never exercised by an actual rejection, so a regression there ships green under SC2. Two honesty caveats: SC5 (full-suite green / mypy / ruff) was a runtime gate the reviewers could not execute and declined to fabricate, so its pass is asserted from code shape only, not observed; and this is a HIGH-effort review, so Pass 3 (Cloudflare reasonableness filter) was skipped by design — borderline findings are left for the operator to weigh. Both incoming rule_keys (PLACEHOLDER_IN_CODE, MISSING_TEST) are valid catalogue keys, so no high-effort rule_key normalization was applied.
Coverage
15 files reviewed, all summary-only. 0 files marked skip → 0 silent skips. Working tree clean → no [uncommitted] annotations.
| File | Treatment | Reason |
|---|---|---|
| docs/extending.md | summary-only | SC6 extension-point doc; prose, no executable surface |
| src/portfolio_optimiser/init.py | summary-only | Package exports; mechanical |
| src/portfolio_optimiser/data/docs/BRU-LAKS-REHAB/notes.txt | summary-only | Synthetic project fixture data |
| src/portfolio_optimiser/data/docs/FV42-GSV-E1/notes.txt | summary-only | Synthetic project fixture data |
| src/portfolio_optimiser/data/docs/RV13-RAS-TP/notes.txt | summary-only | Synthetic project fixture data |
| src/portfolio_optimiser/data/docs/SKOLE-VVS-OPPGR/notes.txt | summary-only | Synthetic project fixture data |
| src/portfolio_optimiser/data/reference_projects.json | summary-only | Config-only project registry (SC1 seam) |
| src/portfolio_optimiser/reference_domain.py | summary-only | Domain reference module; no diff-critical logic flagged |
| src/portfolio_optimiser/run.py | summary-only | Orchestrator fan-out + aggregate; both MAJOR findings land here |
| tests/conftest.py | summary-only | Shared fixtures |
| tests/spikes/test_b_footguns.py | summary-only | SC1 no-hardcoded-id / 4th-project spike guard |
| tests/test_portfolio.py | summary-only | SC2 aggregate; asserts rejected_count==0 (root of MISSING_TEST gap) |
| tests/test_portfolio_live.py | summary-only | SC7 gated Azure portfolio arm (skip-gated) |
| tests/test_reference_domain.py | summary-only | Domain reference tests |
| tests/test_vertical_slice_e2e.py | summary-only | End-to-end slice |
Findings (MAJOR)
b3abbdd7035b5463b3bb9e23f83a158d678cb4f1
- file: src/portfolio_optimiser/run.py
- line: 197
- rule_key: PLACEHOLDER_IN_CODE
- brief_ref: NFR — code correctness
- title: fake-model placeholder leaks into provenance via public client_factory seam
- detail:
run.py:197setsmodel = 'fake-model' if client_factory is not None else resolve_model(profile, 'proposer'), writing a hardcoded sentinel intoProvenanceStamp.model(run.py:197-204). The gateclient_factory is not Noneis a public, documented seam threaded throughrun_portfolio(..., client_factory=...)(run.py:251,278), so a deployer with a custom backend factory stamps'fake-model'across all N provenance records, falsifying the load-bearing provenance invariant. Only the default (no injected factory) path is compensated byresolve_model. - recommended_action: Resolve the real model identifier from the injected
client_factory(or require the factory to surface its model) instead of branching to the'fake-model'literal; reserve the sentinel for test-only fixtures, not the public seam.
a2376be90affdffea4c9b4ef83014d0a30243e23
- file: src/portfolio_optimiser/run.py
- line: 241
- rule_key: MISSING_TEST
- brief_ref: NFR — code correctness
- title: Portfolio rejected_count and validated-only sum never exercised by a rejection
- detail:
_aggregatecomputesrejected_count=len(rejected)(run.py:240) and a validated-onlysum_claimed_saving_nok(run.py:241). Every portfolio test feeds proposals that validate —test_portfolio.py:63assertsrejected_count==0— so norun_portfoliotest produces a Rejection. The rejected branch and the validated-only filter are unproven: a regression that sums rejected claims or miscounts rejections would pass green while SC2 still claims the aggregate is correct. - recommended_action: Add a
run_portfoliotest that feeds at least one proposal the deterministic validator rejects, then assertrejected_count >= 1and thatsum_claimed_saving_nokexcludes the rejected claim.
Suppressed Findings
None. No finding was dropped by Pass 1 (dedup) or Pass 2 (HubSpot Judge). Pass 3 (reasonableness) was skipped under HIGH effort mode.
Conformance trace
From brief-conformance-reviewer (findings: [] — zero conformance/scope findings):
- SC1 — config-only new project: PASS (guarded by no-hardcoded-id grep + 4th-project spike).
- SC2 — fan-out aggregate: PASS shape-wise, but see MAJOR
a2376…— the rejected/validated-only branch is unexercised. - SC3 — state-isolation (
==): PASS, load-bearing (shared meter detach breaks the test). - SC4 — shared VerdictStore accumulation: PASS, load-bearing.
- SC5 — full-suite green / mypy / ruff: NOT OBSERVED — runtime gate the reviewers could not execute; no fabricated finding emitted. Operator must confirm at the gate.
- SC6 — extension doc: PASS (
docs/extending.md). - SC7 — both profiles offline; live-Azure skip-gated: PASS.
- Non-Goals — all honored: no concurrency, no dynamic topology, no rich report, one shared store, live-Azure skip-gated, no compliance code. Validator/IR have zero diff (out of scope by absence).
Remediation Summary
- BLOCKER: 0
- MAJOR: 2
- MINOR: 0
- SUGGESTION: 0
/trekplan (--brief review.md) will consume the two MAJOR findings from the trailing JSON block: fix the fake-model provenance leak on the public client_factory seam, and add a rejection-path test for the portfolio aggregate. Fase 3 closes on WARN per the gate (a BLOCK would require a BLOCKER).
{
"verdict": "WARN",
"counts": { "BLOCKER": 0, "MAJOR": 2, "MINOR": 0, "SUGGESTION": 0 },
"findings": [
{
"id": "b3abbdd7035b5463b3bb9e23f83a158d678cb4f1",
"severity": "MAJOR",
"rule_key": "PLACEHOLDER_IN_CODE",
"file": "src/portfolio_optimiser/run.py",
"line": 197,
"brief_ref": "NFR — code correctness",
"title": "fake-model placeholder leaks into provenance via public client_factory seam",
"detail": "run.py:197 sets model = 'fake-model' if client_factory is not None else resolve_model(profile, 'proposer'), writing a hardcoded sentinel into ProvenanceStamp.model (run.py:197-204). The gate client_factory is not None is a public, documented seam threaded through run_portfolio(..., client_factory=...) (run.py:251,278), so a deployer with a custom backend factory stamps 'fake-model' across all N provenance records, falsifying the load-bearing provenance invariant. Only the default (no injected factory) path is compensated by resolve_model.",
"recommended_action": "Resolve the real model identifier from the injected client_factory (or require the factory to surface its model) instead of branching to the 'fake-model' literal; reserve the sentinel for test-only fixtures, not the public seam."
},
{
"id": "a2376be90affdffea4c9b4ef83014d0a30243e23",
"severity": "MAJOR",
"rule_key": "MISSING_TEST",
"file": "src/portfolio_optimiser/run.py",
"line": 241,
"brief_ref": "NFR — code correctness",
"title": "Portfolio rejected_count and validated-only sum never exercised by a rejection",
"detail": "_aggregate computes rejected_count=len(rejected) (run.py:240) and a validated-only sum_claimed_saving_nok (run.py:241). Every portfolio test feeds proposals that validate — test_portfolio.py:63 asserts rejected_count==0 — so no run_portfolio test produces a Rejection. The rejected branch and the validated-only filter are unproven: a regression that sums rejected claims or miscounts rejections would pass green while SC2 still claims the aggregate is correct.",
"recommended_action": "Add a run_portfolio test that feeds at least one proposal the deterministic validator rejects, then assert rejected_count >= 1 and that sum_claimed_saving_nok excludes the rejected claim."
}
]
}