portfolio-optimiser/src/portfolio_optimiser
Kjell Tore Guttormsen fa1fa5aafd feat(mandate): a domain expert can commission WHICH approaches a run evaluates
Operator feedback: fagpersoner must be able to name the approaches a run shall
evaluate for a project, and/or ask the system for its own. Today the hypothesis
prompt is hardcoded ("Propose ONE concrete cost-saving measure") and the only
expert-facing lever, --dimension-config, FILTERS what may pass the scoping gate
rather than DIRECTING what is spent attempts on. This is the input that was
missing.

`mandate.py` is the typed commission + a fail-fast loader (mirrors
`load_dimension`/`load_goal_config`): missing or malformed refuses, because a run
must never proceed on a silently degraded commission — the coverage report would
then describe work nobody ordered. Stdlib + pydantic only, so it joins
`_MAF_FREE_MODULES` and can be mirrored to the D7 sibling.

Two refusals carry real defect classes: an EMPTY commission (no approaches and no
own proposals) is a caller error, not a result; and a duplicate approach id — or
one claiming the reserved OWN_PROPOSAL_ID — would collapse two coverage rows onto
one key (the S3.2 key-collision class), which is exactly the silence the coverage
report exists to prevent.

The numeric target is deliberately NOT duplicated here: it already lives in
GoalContract, and two copies of one number drift apart ((p) precedent). The
mandate carries intent; `announce` merely restates the figure.

`_build_messages(approach=...)` switches the opening instruction from *find one*
to *quantify THIS one*, carrying the expert's label and description VERBATIM —
the description is the reason the approach is worth trying, the one part the model
cannot infer from cost data. `approach=None` is byte-identical to the previous
prompt, so every existing run and golden is untouched.

The gate is unmoved: `validate_proposal` is called exactly as before. A
commissioned approach gets no discount — the expert directs what is EVALUATED,
never what is APPROVED.

Load-bearing MEASURED against the whole 695-test suite, four mutations all red:
detach the approach injection (2 red, control stayed green) · let a commissioned
approach bypass the validator · make the mandate loader tolerant · drop the
empty-commission refusal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULCqjLF61rehj5cZmdUoR3
2026-08-05 15:43:02 +02:00
..
data feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0) 2026-08-03 17:19:31 +02:00
__init__.py feat(s53): load_dimension fail-fast loader (MAF-free, mirrors load_goal_config) 2026-07-23 21:31:32 +02:00
backends.py fix(s41): pass required credential in AzureFoundryBackend.create_chat_client 2026-07-15 11:18:40 +02:00
budget.py feat(budget): enforce a global portfolio token cap before the call, not after it (S3.4/F10) 2026-07-31 21:34:48 +02:00
contracts.py feat(fase1): GoalContract + GoalConfig standalone fail-fast loader (F1) 2026-07-07 08:01:53 +02:00
costsim.py docs(s54): --report CLI surface + truthful costsim kost_mot_verdi seam note 2026-07-24 01:38:10 +02:00
datasource.py feat(fase2b): OKF-navigated bundle context replaces chunk-stuffing 2026-06-30 06:42:19 +02:00
dimension.py feat(s53): load_dimension fail-fast loader (MAF-free, mirrors load_goal_config) 2026-07-23 21:31:32 +02:00
generate.py feat(mandate): a domain expert can commission WHICH approaches a run evaluates 2026-08-05 15:43:02 +02:00
hitl.py fix(s51): close 2 review MINORs — non-UTF-8 skip + affected_codes parity 2026-07-15 20:59:01 +02:00
ingest.py chore(deps): move the ingest library pin to v0.3.2 — as far as latest goes today 2026-08-05 12:11:12 +02:00
ingest_mcp.py chore(deps): move the ingest library pin to v0.3.2 — as far as latest goes today 2026-08-05 12:11:12 +02:00
ir.py feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0) 2026-08-03 17:19:31 +02:00
ledger.py fix(money): quantize NOK to øre in one order, from one source (kø-p) 2026-08-03 20:08:59 +02:00
mandate.py feat(mandate): a domain expert can commission WHICH approaches a run evaluates 2026-08-05 15:43:02 +02:00
notify.py fix(s52): resolve ConsoleNotifier default stream at call time 2026-07-17 03:18:00 +02:00
okf.py fix(okf): one unquoting rule for frontmatter scalars (kø-(a)/(i)) 2026-08-04 21:23:18 +02:00
outbox.py feat(s42): byte-deterministic write_run_config outbox writer 2026-07-15 12:39:42 +02:00
persona.py feat(shared-root): S3 — configurable shared-root resolver with load-bearing override test 2026-07-03 01:10:05 +02:00
preflight.py fix(s41): close review WARN — OSError refusal + scheme/profile branch coverage 2026-07-15 11:58:10 +02:00
provenance.py feat(fase2): first-class Pydantic provenance stamp 2026-06-24 13:26:55 +02:00
reference_domain.py feat(fase2a): optional Project.bundle_dir + loader (backward-compat, S2.0) 2026-07-15 07:03:15 +02:00
retrieval.py fix(retrieval): fail-closed on uncanonicalisable paths (embedded null byte) 2026-07-21 08:00:11 +02:00
run.py fix(run): --embedder-config is refused, not silently dropped 2026-08-05 11:39:49 +02:00
semretrieval.py fix(semretrieval): refuse a non-finite embedding instead of scoring it (kø-(l)/S3.1 MINOR) 2026-08-03 21:48:50 +02:00
shared_root.py test(okf): lock link_in_index success-path byte/order preservation + honest shared_root claim 2026-07-21 09:38:56 +02:00
simulation.py feat(s42): live_dry_run cut in run_project + DryRunReport 2026-07-15 18:05:16 +02:00
validator.py feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0) 2026-08-03 17:19:31 +02:00
value_report.py feat(s54): value_report text + deterministic JSON formatters 2026-07-24 01:29:31 +02:00
verdicts.py fix(okf): one unquoting rule for frontmatter scalars (kø-(a)/(i)) 2026-08-04 21:23:18 +02:00
workflow.py feat(fase3): make the maker-checker checker actually gate the reasoning 2026-06-30 07:24:30 +02:00