Commit graph

129 commits

Author SHA1 Message Date
7e86896bc8 fix(s51): close 2 review MINORs — non-UTF-8 skip + affected_codes parity
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.
2026-07-15 20:59:01 +02:00
5807ac428c style(s51): ruff format hitl module + tests
Gate: ruff format --check . clean.
2026-07-15 19:43:41 +02:00
c4c55fd417 feat(s51): hitl CLI — python -m portfolio_optimiser.hitl pending|route
Gate: pytest tests/test_hitl.py -k cli → 4 passed.
2026-07-15 19:39:49 +02:00
62d6b40eae feat(s51): route pending proposals to experts by code-prefix
Gate: pytest tests/test_hitl.py tests/test_hitl_loadbearing.py -k route → 5 passed.
2026-07-15 19:38:00 +02:00
e9179271e6 feat(s51): fail-fast self-contained routing config loader
Gate: pytest tests/test_hitl.py -k routing_config → 6 passed.
2026-07-15 19:35:31 +02:00
b50e3fdff2 feat(s51): pending registry — outbox↔inbox id-join (MAF-clean)
Gate: pytest tests/test_hitl.py tests/test_hitl_loadbearing.py tests/test_okf.py → 26 passed.
2026-07-15 19:33:59 +02:00
ce5b1151c8 fix(s42): close review WARN — hermetic dry-run env + scoped azure hint
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.
2026-07-15 18:30:30 +02:00
d7313593bc feat(s42): --live-dry-run CLI flag + dry-run summary 2026-07-15 18:08:28 +02:00
0e986fe6c4 feat(s42): live_dry_run cut in run_project + DryRunReport 2026-07-15 18:05:16 +02:00
ebae4639dd feat(s42): byte-deterministic write_run_config outbox writer 2026-07-15 12:39:42 +02:00
f3b21db105 fix(s41): close review WARN — OSError refusal + scheme/profile branch coverage
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.
2026-07-15 11:58:10 +02:00
871999a55b feat(s41): offline Azure/Foundry preflight CLI (env-contract + placeholder refusal) 2026-07-15 11:21:44 +02:00
82c85d5e7c fix(s41): pass required credential in AzureFoundryBackend.create_chat_client 2026-07-15 11:18:40 +02:00
eb552f854e feat(s41): PORTFOLIO_MODEL_MAP override + placeholder fail-fast in resolve_model 2026-07-15 11:17:06 +02:00
76d9f793c6 feat(s36): costsim CLI + no-network + no-hardcoded-price guards
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
2026-07-15 10:05:29 +02:00
37625435c4 feat(s36): estimate table (kost-mot-verdi + placeholder rows + sourced quality guidance)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
2026-07-15 10:02:00 +02:00
eb889a7f0e feat(s36): deterministic integer-ore estimate scaling with model x effort
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
2026-07-15 09:59:55 +02:00
cfa93799a5 feat(s36): PricingContract + fail-fast loader + placeholder path + quality guidance
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
2026-07-15 09:57:48 +02:00
47147e5f7c feat(s36): MAF-free costsim skeleton + model-map reader + registration
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
2026-07-15 09:54:25 +02:00
620d5cfb83 refactor(fase2a): konsolider fire skriptede klienter til én kanonisk (S2.5) 2026-07-15 07:37:43 +02:00
0a227a103b feat(fase2a): herd load_verdicts_from_dir — vokabular-SKIP + caps (S2.5) 2026-07-15 07:26:22 +02:00
a706184bdd feat(fase2a): wire run_project(outbox_dir, run_id) → outbox-skriving, load-bearing (S2.1) 2026-07-15 07:22:41 +02:00
79c6e44f92 feat(fase2a): MAF-fri outbox.py — byte-deterministisk proposal/outcome-writer (S2.1) 2026-07-15 07:18:21 +02:00
9311813080 feat(fase2a): percent-mål mot baseline 0 reiser ValueError (S2.0) 2026-07-15 07:14:29 +02:00
8ec71c9814 feat(fase2a): thread bundle_dir/verdict_dir i run_portfolio — kryssprosjekt-læring load-bearing (S2.0) 2026-07-15 07:12:50 +02:00
9659045fee feat(fase2a): optional Project.bundle_dir + loader (backward-compat, S2.0) 2026-07-15 07:03:15 +02:00
3497e64bdc test(fase2a): repo-lokal mini-bundle-fixture for kryssprosjekt-læring (S2.0) 2026-07-15 07:01:06 +02:00
847ed90135 feat(fase1): export domain-model public API + green full suite (F1) 2026-07-07 08:17:10 +02:00
69ca508677 feat(fase1): assessment-method encoding — persisted dimension-scoped example + validator rule (F1) 2026-07-07 08:15:44 +02:00
16b6d80b82 feat(fase1): hard/soft goal-stop in run_portfolio on accumulated ledger (F1) 2026-07-07 08:11:43 +02:00
c6f62d41db feat(fase1): GoalContract + GoalConfig standalone fail-fast loader (F1) 2026-07-07 08:01:53 +02:00
9720acb18c feat(fase1): fail-closed expert realize gate (F1) 2026-07-07 08:00:23 +02:00
e0778d2230 feat(fase1): dimension-free dedup key + cross-dimension overlap flag (F1) 2026-07-07 07:57:22 +02:00
13905193f7 feat(fase1): typed savings ledger with deterministic provenance (F1) 2026-07-07 07:53:31 +02:00
d2029964cc feat(fase1): dimension in run_project — context scope + candidate constraint (F1) 2026-07-07 07:50:26 +02:00
525ff0dda1 feat(fase1): dimension filter on bundle_context, default unchanged (F1) 2026-07-07 07:44:33 +02:00
a44256a994 feat(fase1): dimension IR + admits, MAF-free guard extended (F1) 2026-07-07 07:42:07 +02:00
bbdcd62ae6 feat(ingest): gate http behind allow_network run-flag + dispatch (I6) 2026-07-04 17:04:05 +02:00
58dda468dc feat(ingest): http read_http connector + injectable transport seam (I6) 2026-07-04 16:57:29 +02:00
d7e5f2fec7 feat(ingest): sqlite sql-source connector + typed cell rendering + dispatch (I4)
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
2026-07-04 06:55:54 +02:00
e66a2b4015 style(ingest): ruff format (I2) 2026-07-03 18:40:40 +02:00
676c11a498 feat(ingest): index generation and stamped-replacement semantics (I2) 2026-07-03 18:33:56 +02:00
bfb3f9afb8 feat(ingest): deterministic materialization with §5/§7 provenance stamp (I2) 2026-07-03 18:31:50 +02:00
f331cb4763 feat(ingest): CSV connector with boundary check, row cap and escaped table body (I2) 2026-07-03 18:29:53 +02:00
e4ee8bd52e feat(ingest): fail-fast manifest contract with verdict reservation (I2) 2026-07-03 18:28:07 +02:00
cebba7d954 feat(shared-root): S3 — configurable shared-root resolver with load-bearing override test
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
2026-07-03 01:10:05 +02:00
ae01127510 docs(truth): S1 truth maintenance — CHANGELOG, stack line, MCP claim, conflict semantics, 90% cut-list
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
2026-07-03 00:34:56 +02:00
705c5dd49a chore(format): ruff format the 4 drifted files (no behavior change)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
2026-07-03 00:32:49 +02:00
6f861a0078 feat(persona): build the shared expert-reviewer persona as a framework-neutral Agent Skill
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
2026-06-30 13:59:42 +02:00
a9144cb9bb feat(sim): offline end-to-end simulation proving the learning loop closes
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
2026-06-30 12:55:15 +02:00