portfolio-optimiser-claude/src/portfolio_optimiser_claude
Kjell Tore Guttormsen 7d8de32543 feat(okf): navigate hierarchy — escape, not depth, is forbidden
The pulled method-spec (commons 9801d35) retires the "a target containing a
path separator is out-of-bundle" heuristic, which conflated depth with escape
and forbade valid hierarchy. Triage of the pull found FIVE contradictions in
okf.py, not the two STATE had measured on line 127 alone:

1. the separator ban skipped every legal nested target;
2. de-duplication keyed on the RAW target (`resolved` was computed a line
   later), not on the resolved path;
3. navigation never recursed — only the root index's links were read;
4. a leading `/` became filesystem-absolute via pathlib rather than denoting
   the bundle root (safe, because the boundary check caught it, but the right
   outcome for the wrong reason — and wrong the moment `/a/index.md` must be
   FOLLOWED);
5. rendering excluded only `verdict`, so a nested index body would render as
   content.

navigate_bundle is now depth-first in first-seen link order, de-duplicating on
the resolved path (so `./a.md` and `a.md` are one entry and cycles terminate);
resolution and the fail-closed boundary check move to _resolve_target, the sole
in-/out-of-bundle test. The missing-index rule binds the bundle root alone.
bundle_context renders flat regardless of depth and drops nested index bodies:
only the root index is the summary.

The gate is the commons-owned nav-golden pair that arrived with the same pull —
bundle in, expected-read-context out. Its negative case exists so the gate can
go red at all, and carries a real decoy one level up plus a `/etc/passwd` trap.

Detach-proved (mutate, run, restore from copy) — each new seam goes RED:
  D1 reinstate the separator heuristic -> RED
  D2 re-key dedup on the raw target   -> RED
  D3 read a leading `/` as absolute   -> RED
  D4 render nested index bodies       -> RED
Control after restore: 24 passed. Suite 631 -> 637, ruff + mypy --strict clean.

Comments asserting the retired doctrine were corrected rather than left to
document a rule the code no longer follows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M1zp3BxCuzRnUtJPzvEFTQ
2026-07-31 18:13:20 +02:00
..
data feat(portfolio): K6 — pre-run cost simulation, priced what-if (parity row 18) [skip-docs] 2026-07-23 23:01:01 +02:00
__init__.py feat(scaffold): S5 — D7 sibling scaffold: SDK dep, fail-fast startup contracts, CLAUDE.md 2026-07-03 06:06:03 +02:00
artifacts.py fix(run): S10 del 2 — post-mortem: stopp-artefakt, SDK-isolasjon, raw-JSON-direktiv 2026-07-03 10:49:51 +02:00
budget.py feat(portfolio): C3.5 — pre-call run-total USD budget belt (parity row 16/31) [skip-docs] 2026-07-23 22:45:37 +02:00
contracts.py feat(portfolio): K6 — pre-run cost simulation, priced what-if (parity row 18) [skip-docs] 2026-07-23 23:01:01 +02:00
costsim.py feat(portfolio): K6 — pre-run cost simulation, priced what-if (parity row 18) [skip-docs] 2026-07-23 23:01:01 +02:00
experience.py feat(learning): S9 — D7 læringssløyfe: verdict-inbox, fail-closed promoteringsgate, artefakt-sourced persona 2026-07-03 07:36:15 +02:00
goals.py feat(ledger): K1 — savings ledger + goal contract (parity rows 2-3) 2026-07-17 04:00:14 +02:00
hitl.py feat(portfolio): K10 — notification/notifier seam, opt-in webhook egress (parity row 23) [skip-docs] 2026-07-24 20:16:56 +02:00
inbox.py feat(ingest): adopt llm-ingestion-okf as Door A implementation (first consumer) 2026-07-16 20:46:51 +02:00
ingest.py feat(ingest): adopt llm-ingestion-okf as Door A implementation (first consumer) 2026-07-16 20:46:51 +02:00
ir.py fix(validator): C2.6 — finiteness hardening, Infinity can no longer vacuously clear the gate (closes R-2) 2026-07-16 20:10:12 +02:00
ledger.py fix(ledger): normalize every load rejection to ValueError at the ledger's own entrance 2026-07-25 15:29:27 +02:00
loop.py feat(portfolio): C3.5 — pre-call run-total USD budget belt (parity row 16/31) [skip-docs] 2026-07-23 22:45:37 +02:00
notify.py feat(portfolio): K10 — notification/notifier seam, opt-in webhook egress (parity row 23) [skip-docs] 2026-07-24 20:16:56 +02:00
okf.py feat(okf): navigate hierarchy — escape, not depth, is forbidden 2026-07-31 18:13:20 +02:00
outbox.py feat(portfolio): K5 — outbox persistence, run_id-named pairs (parity row 7) [skip-docs] 2026-07-23 22:31:18 +02:00
persona.py feat(learning): S9 — D7 læringssløyfe: verdict-inbox, fail-closed promoteringsgate, artefakt-sourced persona 2026-07-03 07:36:15 +02:00
portfolio.py feat(portfolio): K11 — per-run value report, pure projection over the three layers (parity row 25) [skip-docs] 2026-07-25 06:25:02 +02:00
preflight.py feat(portfolio): K7 — SDK/API preflight, offline pre-spend boundary (parity row 20) [skip-docs] 2026-07-24 01:34:18 +02:00
promotion.py feat(inbox): C2.5 — inbox hardening + SDK version guard (closes C-F7, C-N3, R-6) 2026-07-16 20:26:41 +02:00
provenance.py feat(portfolio): stamp the producing SDK build in provenance (wiki-advisory F1) [skip-docs] 2026-07-25 06:57:30 +02:00
run.py feat(run): stamp the drill's SDK build in the dry-run run-config [skip-docs] 2026-07-25 15:35:16 +02:00
run_s10.py fix(run): S10 del 2 — post-mortem: stopp-artefakt, SDK-isolasjon, raw-JSON-direktiv 2026-07-03 10:49:51 +02:00
sdk_client.py feat(portfolio): stamp the producing SDK build in provenance (wiki-advisory F1) [skip-docs] 2026-07-25 06:57:30 +02:00
simulation.py feat(simulation): K4 — closed-loop two-run simulation binds §11 'Closed loop' (closes R-1) 2026-07-17 03:47:05 +02:00
validator.py feat(validator): S6 — deterministic backbone: typed IR, golden-frozen validator, provenance stamp 2026-07-03 06:27:40 +02:00
valuereport.py test(valuereport): bind _SHARE_DIGITS to its MEASURED band, not to itself 2026-07-26 15:28:20 +02:00