Both live in the tolerant inbox-read path and both contradicted the module's
own "skipped, never raised" / loader-parity docstrings (S5.1 review: ALLOW,
2 MINOR, non-gating):
- UnicodeDecodeError (a *.json hand-saved in Latin-1 with Norwegian æ/ø/å is
invalid UTF-8) now SKIPPED in hitl._load_json_dict AND
verdicts.load_verdicts_from_dir — was: crashed pending/route with a raw
traceback (a ValueError subclass, caught by neither OSError nor
JSONDecodeError). Fix symmetric across both readers.
- hitl._inbox_verdict_ids now skips a non-iterable affected_codes exactly as
load_verdicts_from_dir does (frozenset() raises TypeError) — was: marked the
proposal judged on key-presence alone → a silent false-negative in the
operator pending queue. hitl-only: verdicts is the correct reference.
TDD: 3 RED-then-green (both UnicodeDecodeError twins + the parity gap) + 1
ground-truth pin locking the loader side so the parity cannot rot.
Gate: pytest → 389 passed, 4 skipped (was 385); ruff check + format clean; mypy clean.
Post-hoc /trekreview of S4.2 surfaced two confirmed findings; both closed via TDD.
S42-001 (MAJOR): the new --live-dry-run CLI tests read PORTFOLIO_MODEL_MAP /
PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT via resolve_model/AzureFoundryBackend but did
not isolate them, so both arms inverted their rc in a Foundry-configured env.
Add an autouse fixture that delenvs both, mirroring test_backends/test_preflight.
S42-002 (MINOR): the --live-dry-run except ValueError attached the azure-preflight
remediation to every offline-path ValueError (unknown project_id, empty docs_dir,
bundle mismatch). Scope the hint to args.profile == "azure"; structured refusal +
rc 1 preserved for all. New test proves a LOCAL unknown-project refusal carries no
azure hint.
Gate: pytest 358 passed / 4 skipped, ruff check + format clean, mypy 24 files.
Post-hoc /trekreview (WARN, scope 0670b2a..b513e64) surfaced 2 findings, both closed:
- MAJOR MISSING_TEST: the non-https scheme guard (preflight.py:61) and non-azure
profile refusal (:82-86) had zero test coverage — added
test_non_https_endpoint_refused + test_non_azure_profile_refused so an inverted
condition can't regress silently.
- MINOR MISSING_ERROR_HANDLING: an existing-but-unreadable PORTFOLIO_MODEL_MAP
raised PermissionError (an OSError, not ValueError) past the except at :95 →
widened to OSError (subsumes FileNotFoundError). test_unreadable_override_is_
structured_refusal proves the traceback-free invariant (red pre-fix).
Suite 348->351/4, ruff+format+mypy clean.
read_sql resolves connection_ref from the environment to a sqlite path, opens it read-only (mode=ro so a write in the query fails at the DB), and converts cells to their spec-5 text form: INTEGER plain decimal, REAL shortest round-trip (repr), TEXT verbatim, SQL NULL empty string, BLOB/other IngestError (never silent coercion). materialize now dispatches file->read_csv, sql->read_sql; http stays I6-refused. Pure stdlib (sqlite3/os/contextlib) — MAF-free context-seam guard intact. No spec change (frozen); the SQL type/number rules were delegated to I4.
Refs: shared/ingest-spec.md 4/5/8 · sesjonsplan I4
One call-time resolver (env PORTFOLIO_SHARED_ROOT, default the in-repo
shared/) consumed by both MAF-side readers of the shared core:
persona._example_path() (the _EXAMPLE_PATH monkeypatch seam is kept) and
simulation._default_bundle_dir() (replaces the _BUNDLE_DIR module global).
De-risks the S4 extraction: re-pointing the commons becomes an env var,
not a code change. Override test proves the marker follows a tmp copy of
the whole shared tree; both detach points proven RED. Suite 155->157.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
Findings 4-7 from the 2026-07-02 status analysis, per the session plan (S1):
- CHANGELOG rewritten truthfully (was: 'Plan phase - no framework code yet')
- README stack line names the split GA packages, not the agent-framework meta-package
- CLAUDE.md: MCP downgraded to extension point (in-process FunctionTool is the default seam)
- Verdict conflict semantics documented as chosen (store first-write-wins per id,
disk/wiki last-write-wins per file; full B10 taxonomy deliberately deferred)
- docs/extending.md: explicit 90%-principle cut-list (B10, B11, U12, U14, concurrent fan-out)
- .gitignore covers .trekexecute-progress-* (docs/.DS_Store was already untracked/ignored -
the plan's git rm --cached assumption was stale; no-op)
No code behavior changed (docstring only in verdicts.py). Suite 152/4 green, mypy clean,
ruff format --check clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
The expert reviewer was only a hardcoded verdict_input dict inside the offline
simulation. Build it as the real, shared artifact target picture §8 calls for:
shared/skills/expert-reviewer/ — a SKILL.md persona prompt (energy-advisor / M&V
role + the realization-gap methodology the validator cannot compute) plus a
canonical references/example-verdict.json. shared/ stays pure data; the MAF side
reads it via portfolio_optimiser.persona.load_persona_example (call-time,
fail-fast) and the Claude-SDK sibling reads the same JSON with its own loader.
This de-stubs the simulation: its persona judgement (decision + rationale + traced
marker) is now sourced from the artifact at call time, not an inline literal — so
the shared persona is genuinely consumed and cannot rot silently. decision is
binary (approved/rejected, the FeedbackContract the run path accepts);
approved_with_adjustment is rejected there and lives only in the bundle seed
frontmatter + the promotion gate, so the realization correction is carried in the
rationale prose.
Load-bearing trio (tests/test_persona_skill_loadbearing.py), each proven RED on its
own detach: structure + framework-neutrality, the example is valid pipeline input
(incl. FeedbackContract, on a throwaway copy), and the simulation's marker follows
the artifact file. Suite 149->152.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
The primary method proof, offline — a deliberate, cost-driven substitution
for målbilde §11.8's real-model run (the operator runs MAF against no real
model; API for both repos is too costly privately).
`portfolio_optimiser.simulation` drives `run_project` with a scripted
synthetic chat client across two runs separated by a promotion, and shows
the learning loop close end to end:
- ScriptedChatClient subclasses the LAYERED OpenAIChatCompletionClient (not
bare BaseChatClient — else the always-attached BudgetMiddleware no-ops),
constructs offline (loopback url + dummy key), role-keys proposer/checker
replies, and records every prompt into a shared sink.
- simulate_learning_loop: Run A (fresh wiki) -> validated, persona-approved
verdict carrying a realization marker absent from the bundle -> promote_verdict
into the OKF wiki -> seed_store_from_bundle re-reads it -> Run B's hypothesis
prompt carries the marker. An empty-wiki control on Run A proves causality.
- `python -m portfolio_optimiser.simulation` prints an honest trace.
Honesty (§1): this proves the plumbing, the deterministic spine, and that the
learning dataflow closes — NOT that a live LLM would produce the proposal or
verdict (scripted stand-ins). The genuine model-behaviour comparison lives on
the Claude-SDK side (a minimal API run); the scripted client is MAF-side
scaffolding, not part of the framework-neutral shared/ core.
Load-bearing: tests/test_simulation_loadbearing.py goes red when promotion is
detached (the marker never crosses into Run B). Suite 148->149.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE