portfolio-optimiser/pyproject.toml
Kjell Tore Guttormsen 38df79126f
feat(toolbox): the first four doors out of the toolbox, without a chat client on the way
B-gate row 1's premise, made callable. Every path through the framework CLI constructs a chat
client, so an outside caller -- a human at a terminal, or an agent that is NOT po -- could not
reach a single run-path step without paying for a model. These four steps need no model at all.

One CLI, four subcommands, one core call each:

  navigate-bundle  --bundle-dir                         -> okf.navigate_bundle
  cost-baseline    --bundle-dir --project-id            -> okf.derive_cost_baseline
  retrieve-chunks  --query --docs-dir [--top-k]         -> datasource.retrieve_chunks
  prepass-admit    --payload --bundle-dir [--dimension] -> prepass.admit_payload

Each handler is a thin adapter: strings in, the SAME function the run path calls, JSON on stdout,
and an exit code that says what happened (0 ran, 2 malformed call, 3 the step refused, named).
A handler that computed anything of its own would be a second implementation of a run-path step,
and the outside caller would stop getting what the debate gets.

Dispatch is an explicit branch per command, not argparse's `set_defaults(handler=...)`: the table
hides the one thing a reader wants to see, and B-gate row 1 asks the same question of the source
(it walks the call graph from `main` down to the step's symbol), where a callable in a Namespace
is a hop neither can follow.

Probes (`tests/test_toolbox_doors.py`, 10 arms): each starts the door as a SUBPROCESS with the
subcommand in argv and asserts on what it wrote -- never by importing the core function, which is
the whole difference the gate exists to measure. The yardstick is outside the door in every arm:
the filesystem (navigate-bundle, including the one deliberate outside-bundle link), a table
transcribed from the priced fixture (cost-baseline), the in-process seam it must equal byte for
byte (retrieve-chunks), and the producer's own checked-in payload (prepass-admit). Every refusal
arm has an rc-0 control beside it.

`portfolio-optimiser-toolbox` is the THIRD console script, and the pin test now says why: it is
the door the other two cannot be used for. README and CLAUDE.md updated with the command and the
reason it exists; every documented invocation was run.

Row 1: 1 -> 5 of 17 (four subcommands + `gate`, which the class fix in e47be68 stopped rejecting
on a name technicality). No other row moved; exit 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:11:58 +02:00

135 lines
9.6 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[project]
name = "portfolio-optimiser"
version = "1.1.0"
description = "Generic framework on Microsoft Agent Framework for per-project cost-savings optimization"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
# MAF — GA-pakker, IKKE meta-pakken `agent-framework`. Metaen drar `agent-framework-core[all]`,
# og `[all]` trekker inn de fortsatt-beta integrasjonene (azure-ai-search/cosmos/ollama/…) som
# tvinger pre-releases og drar med en ALPHA pydantic. Offisiell guide: installer kun det du trenger.
# Beta-integrasjoner legges til per-fase (med snevert pre-release-scope) når de faktisk trengs.
"agent-framework-core>=1.18.0,<2", # kjerne (GA) — to-sidig pin (S2.5): major-bump krever re-verifisering av privat-API-premissene (test_maf_version_guard). F16 (12.09): løftet fra 1.16.0; 17 private/ugaranterte former, 16 sjekket mekanisk av proben, se docs/2026-09-12-f16-maf-1180.md. F15 (02.09) løftet 1.9.0→1.16.0.
# F16 (12.09): løftet 1.8.2 → 1.13.0 SAMMEN med `-openai` og `core`, og koblingen er MÅLT, ikke antatt.
# foundry 1.13.0 krever selv `core<2,>=1.17.0` OG `openai<2,>=1.14.2`, så de tre kan ikke løftes hver for
# seg. Resolveren ville BEHOLDT 1.8.2 (den krever bare `core<2,>=1.9.0`, målt) — men to av 1.17/1.18s
# BREAKING-endringer navngir core OG foundry i SAMME punkt (#7918 sequence-only middleware-inputer,
# #8127 `SecretString`), så et 1.9.0-æra foundry mot en 1.18.0 core er halvparten av en koordinert
# endring — nøyaktig «importerer fortsatt, semantikken har flyttet i stillhet»-klassen F15 § 4 målte.
"agent-framework-foundry>=1.13.0", # Azure/Foundry-profil: FoundryChatClient (GA)
"azure-identity>=1.25", # S4.1: AzureFoundryBackend passes an explicit AzureCliCredential (Foundry requires it); already transitive via foundry — promoted to a declared direct dep (zero new install weight)
"agent-framework-openai>=1.14.3", # OpenAI + OpenAI-kompatible lokale endpoints (GA) → lokal profil; F16: løftet 1.8.2 → 1.14.3 (foundry 1.13.0 krever `openai>=1.14.2`; se raden over)
# Promotert dev→core i Fase 2 (MVP-runtime, ikke lenger spike-only):
# F16 (12.09): dette gulvet STÅR på 1.1.1, og det er en MÅLING, ikke en utelatelse: 1.1.1 (2026-08-21) er
# fortsatt siste utgivelse på PyPI, så det finnes ingenting å løfte det til. Kravet `core<2,>=1.15.0` er
# tilfredsstilt av core 1.18.0. F15s formulering «de to gulvene løftes SAMMEN — det finnes ingen delvis
# bump» var sann for F15s bump; etter F16 er den MÅLT SVAKERE: core kan løftes uten at orchestrations kan.
"agent-framework-orchestrations>=1.1.1", # GA orchestration builders (GroupChat/Concurrent/Magentic)
"pulp>=2.8", # deterministisk validator-solver; PuLP bundler CBC i wheelen (R2). Installert 3.3.2.
# PuLP 4.0 vil kreve `pip install pulp[cbc]` + COIN_CMD (Fase-migrasjonsnotat).
"mcp>=1.28.0", # tynn lokal-mappe MCP-server (Step 7) — GA (resolverte 1.28.0) per Step 1-beslutning
"anyio>=4.14", # kø-(z): ingest_mcp.py bruker anyio.fail_after direkte (MCP-timeout-stien); allerede transitiv via mcp — promotert til deklarert direkte dep (zero new install weight, resolverte 4.14.0)
"pydantic>=2.11,<3", # IR/validering (B1) — eksplisitt pin til STABIL 2.x, aldri alpha
# S3.1: brute-force cosine for the hybrid verdict retriever — MAF-free, offline (D-C).
# Upper bound is <2.3, NOT <3, and it is load-bearing twice over: numpy 2.3+ requires
# Python >=3.11 (2.4+ requires >=3.12), so a wider range silently contradicts this project's
# `requires-python = ">=3.10"`; and numpy 2.3+ stubs use PEP 695 `type` statements, which
# mypy refuses to parse under `python_version = 3.10` (python/mypy#18701) — breaking
# `uv run mypy src` outright. Raise both floors together or not at all.
"numpy>=2.0,<2.3",
# Door A ingest (§4§6) — the shared implementation of shared/ingest-spec.md, MAF-free (D7).
# NOT "zero runtime deps" since 0.4.0: llm-ingestion-guard is okf's one runtime dependency,
# and okf >=0.8.5 declares `>=1.2,<2.0` for it. That is why the two lines below move together.
"llm-ingestion-okf",
"llm-ingestion-guard",
# U14 (økt 55, operatørbeslutning 23.08): the tracing seam's ONE new runtime dependency.
# `opentelemetry-api` was already here transitively via agent-framework-core, but the SDK is
# what `configure_otel_providers()` needs — without it MAF raises ModuleNotFoundError, so the
# seam cannot land as an opt-in without declaring it. The console exporter this framework uses
# (`opentelemetry.sdk.trace.export.ConsoleSpanExporter`) ships INSIDE this package, so console
# mode costs exactly one declaration. The OTLP exporter packages are deliberately NOT declared:
# they are egress, they drag grpc/protobuf into a published wheel, and MAF already raises a
# named ImportError telling the operator which one to install. Stated honesty limit, not an
# oversight — see tracing.py.
"opentelemetry-sdk>=1.42,<2",
# Door A content gate — scans materialized concepts before they reach the bundle; zero runtime deps, MAF-free (D7)
]
# Console entry points — the install surface a fresh clone gets from `uv sync` (P4 pkt. 5).
# Deliberately TWO, not five: `run` is the framework CLI (three documented modes) and `simulation`
# is the offline end-to-end proof the README points a newcomer at. `costsim` / `hitl` / `preflight`
# keep the `python -m` form — they are operator utilities, not the product's front door, and every
# name here is a name the freeze has to carry. Pinned by tests/test_console_entry_points.py.
[project.scripts]
portfolio-optimiser = "portfolio_optimiser.run:main"
portfolio-optimiser-demo = "portfolio_optimiser.simulation:main"
portfolio-optimiser-toolbox = "portfolio_optimiser.toolbox:main"
# Distribution channel for the shared ingest library (mirrors portfolio-optimiser-claude,
# verified in consumer CI): git pin against the public Forgejo repo — reproducible for every
# consumer, uv.lock pins the exact commit behind the tag. Bump the rev on a new library tag.
[tool.uv.sources]
llm-ingestion-okf = { git = "https://git.fromaitochitta.com/open/llm-ingestion-okf.git", rev = "v0.8.5" }
# Pinned to a TAG, never a floating ref — a security component whose version can move under a
# `uv sync` is a gate that can stop gating without a local diff (the okf `generated`-literal
# lesson, measured 2026-08-09). Bumping this is a decision, not maintenance.
# `tag =`, never `rev =`, and never a lower 1.x: okf >=0.8.5 pins this library itself via its own
# [tool.uv.sources] `tag = "v1.4.0"`, and uv treats `rev = "v1.4.0"` as a DIFFERENT url from
# `tag = "v1.4.0"` at the same value — both spellings were measured to fail resolution as
# "conflicting URLs" (P13 § 2a). The declared floor okf asks for is >=1.2, but 1.2.0 is not
# choosable for the same reason.
llm-ingestion-guard = { git = "https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git", tag = "v1.4.0" }
# Dev tooling as a PEP 735 dependency-group (uv includes it by default in `uv sync`/`uv run`),
# so the documented bare `uv sync` + `uv run pytest` workflow installs it without `--extra`.
#
# INTENTIONAL, RECORDED DEVIATION (Fase 2 review F4): the original plan Step 1 authorized only
# moving `agent-framework-orchestrations` + `pulp` from the dev extra into core. Migrating the
# dev group from `[project.optional-dependencies]` to this PEP 735 `[dependency-groups]` block
# went beyond that stated scope, but is KEPT (not reverted) because it is what makes the brief's
# Success Criterion SC1 ("bare `uv sync` + `uv run pytest`") install dev tooling without
# `--extra` — reverting would break SC1. Logged here so the deviation is no longer silent.
[dependency-groups]
dev = [
"pytest>=8",
"ruff>=0.6",
"mypy>=1.11",
"pytest-asyncio>=0.24", # MAF orchestrations are async (await workflow.run(...)) — test-only
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/portfolio_optimiser"]
# Fase 4a: the wheel CARRIES shared/ (spec, persona skill, example bundles) as packaged data —
# a byte-identical mirror under portfolio_optimiser/_shared/, resolved by shared_root() only when
# no working tree is present (checkout wins; PORTFOLIO_SHARED_ROOT wins over both). This is what
# makes an installed wheel — and a container — work without a clone, while shared/ itself stays a
# pull-only subtree at the repo root. Pinned by tests/test_shared_packaged_data_loadbearing.py.
[tool.hatch.build.targets.wheel.force-include]
"shared" = "portfolio_optimiser/_shared"
[tool.ruff]
line-length = 100
src = ["src", "tests", "spikes"]
[tool.pytest.ini_options]
pythonpath = ["src", "."]
testpaths = ["tests"]
asyncio_mode = "auto"
[tool.mypy]
python_version = "3.10"
# Third-party libs without bundled type stubs (py.typed) — analysed as untyped, not errors.
[[tool.mypy.overrides]]
module = ["pulp.*", "agent_framework_foundry.*", "llm_ingestion_guard.*"]
ignore_missing_imports = true
# NOTE on `llm_ingestion_guard`: the override alone would make the seam type-BLIND, not
# type-safe — every symbol arrives as `Any`, so a field rename upstream would type-check
# happily and fail at runtime. `ingest.materialize_gated` therefore coerces each value it
# reads off the guard's result objects to a concrete type at the boundary (the okf precedent),
# and the seam's behaviour is pinned by tests/test_ingest_content_gate_loadbearing.py.