feat(4a): wheelen bærer shared/ som pakkede data — arbeidstreet er overstyringen

README:24 sa det rett ut: shared/ ble lest fra arbeidstreet ved kjøretid, og
derfor kunne repoet verken publiseres som wheel eller kjøre i container. Målt
før endringen: 1.0.0-wheelen bar 58 filer, null under shared/.

Endringen er én søm + én pakkelinje:
- hatchling force-include speiler shared/ byte-identisk til
  portfolio_optimiser/_shared/ (wheel 122 filer, 64 under _shared/; sdist
  bærer treet, målt via uv build sdist→wheel)
- shared_root() løser ved kall-tid: PORTFOLIO_SHARED_ROOT → arbeidstreets
  shared/ når det finnes (en checkout er autoritativ — det holder pull-only-
  subtree-kontrakten og goldenene urørt) → pakket kopi

Iron Law fulgt: tests/test_shared_packaged_data_loadbearing.py skrevet FØRST,
alle tre røde mot dagens kode (ordnings-testen felt av sin egen kontroll på at
pakket kopi finnes). Deretter fiks, deretter MÅLT mutasjon mot hele suiten:
- detach fallbacken → 1 rød (resolusjons-testen)
- detach force-include → 3 røde
- snu rekkefølgen (pakket før arbeidstre) → 1 rød (ordnings-testen, som var
  grønn før fiksen — flip-mutasjonen er beviset på at den diskriminerer)
Kontroll grønn: 813 passed / 4 skipped (baseline 810/4 målt på 142bfa9 samme
økt). Goldenene byte-uendret før og etter (shasum -c på demo-transkript +
begge nav-goldens). shared/ selv er urørt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hfm6sWTk17Cbh6ZHYhvCu
This commit is contained in:
Kjell Tore Guttormsen 2026-08-13 21:13:08 +02:00
commit f5ec8c84f6
5 changed files with 238 additions and 13 deletions

View file

@ -14,7 +14,7 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
- Modell-valg som konfig (modell-map rolle→Foundry-deployment), ikke spredt i kode.
- Metode kodifiseres som **Agent Skill** (`agentskills.io`: `SKILL.md` + `scripts/` + `references/`).
- Datatilgang: in-process `FunctionTool` er default-sømmen i kjørestien. **MCP er wiret som opt-in i kjørestien** (`mcp_tools.py` + `--mcp-config`, Trekk B 2026-08-05): konkrete eksterne servere blir verktøy agentene kan kalle UNDER debatten. Uten konfig gjøres null nettverkskall og verktøylista er uendret. Tre regler er load-bearing: **allowlist er påkrevd** (tom liste ville latt motparten bestemme hva agentene får kalle), **hver server og hvert tillatte verktøy navngis i kunngjøringen før første kall** (også uten `--mandate` — ingen udeklarert egress), og `--live-dry-run` åpner **ingenting**. Egen søm fra `ingest_mcp.py` (kildedokumenter FØR kjøring, null-argument-tools) — samme protokoll, ulik jobb. `build_mcp_server` (`datasource.py`) er fortsatt kun demo. Data-source-konfig JSON-Schema-validert, fail-fast.
- `shared/` er en **git subtree** av [`portfolio-optimiser-commons`](https://git.fromaitochitta.com/open/portfolio-optimiser-commons) (source of truth, R1 realisert 2026-07-03; publisert i `open/` 2026-08-04 — `commons`-remoten peker fortsatt på `ktg/` og virker uendret). Synk er **pull-only**: endringer committes i commons og hentes med `git subtree pull --prefix=shared commons main --squash`. ALDRI `git subtree push` fra konsument — re-split lekker hele konsument-historikken inn i commons (observert + opprydd 2026-07-03). Se `shared/README.md`.
- `shared/` er en **git subtree** av [`portfolio-optimiser-commons`](https://git.fromaitochitta.com/open/portfolio-optimiser-commons) (source of truth, R1 realisert 2026-07-03; publisert i `open/` 2026-08-04 — `commons`-remoten peker fortsatt på `ktg/` og virker uendret). Synk er **pull-only**: endringer committes i commons og hentes med `git subtree pull --prefix=shared commons main --squash`. ALDRI `git subtree push` fra konsument — re-split lekker hele konsument-historikken inn i commons (observert + opprydd 2026-07-03). Se `shared/README.md`. Wheelen bærer treet som pakkede data siden Fase 4a — se invarianten under.
## Kommandoer
- Sync: `uv sync` — installerer to konsoll-kommandoer: `portfolio-optimiser` (CLI, `run:main`) og
@ -125,6 +125,20 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
avvisningstestene stå grønne — beslutningen så dekket ut uten å være testet. Båndet der tieren
faktisk avgjør er høy-entropi-innhold (`quarantine_review` vs `warn`), og testen ble skrevet mot
nøyaktig det.
- **`shared/` leses som PAKKEDE DATA, med arbeidstreet som overstyring (Fase 4a):** wheelen bærer
en byte-identisk speiling av hele `shared/`-treet under `portfolio_optimiser/_shared/`
(hatchling force-include i `pyproject.toml`), og `shared_root()` løser ved KALL-tid i fast
rekkefølge: `PORTFOLIO_SHARED_ROOT` → arbeidstreets `shared/` når det finnes → pakket kopi.
**Arbeidstreet er autoritativt i en checkout** — det er dét som holder pull-only-subtree-
kontrakten og de byte-eksakte goldenene urørt (målt: goldens shasum-identiske før/etter, og
`shared/` selv urørt). Den pakkede kopien er dét som gjør wheel og container mulig uten klone
(målt før: 1.0.0-wheelen bar 58 filer, null under `shared/`; etter: 122, hvorav 64 under
`_shared/`, og sdist→wheel-kjeden bærer treet). Speilingen er ALDRI en redigert derivat —
byte-identitet er egenskapen som lar commons-goldenene fortsatt gate den pakkede kopien.
Load-bearing MÅLT (`tests/test_shared_packaged_data_loadbearing.py`, ekte `uv build` i
fixturen — pakkekonfigen er selv en søm), tre mutasjoner alle røde mot hele suiten: detach
fallbacken (1 rød) · detach force-include (3 røde) · snu rekkefølgen (1 rød — ordnings-testen
var grønn før fiksen; dens kontroll på at pakket kopi FINNES er det som gjør flippen målbar).
- **Stoppkriterier + budsjett-tak påkrevd ved oppstart** (fail-fast, aldri ubegrenset loop).
- **Group Chat maker-checker** som debatt-default (IKKE Magentic, som er eksperimentell).
- **To falsifiserere, samme kandidat (Steg 3/4, målbilde §2/§6):** den deterministiske validatoren

View file

@ -23,8 +23,11 @@ cd portfolio-optimiser
uv sync
```
Clone rather than install into an existing environment: the shared spec, the persona skill and the
example bundles under [`shared/`](shared/README.md) are read from the working tree at run time.
The clone is the documented path because the walkthrough below points at files in the tree. It is
no longer a technical requirement: a built wheel carries the shared spec, the persona skill and the
example bundles under [`shared/`](shared/README.md) as packaged data, and the resolver prefers a
working tree when one is present (`PORTFOLIO_SHARED_ROOT` overrides both) — so an installed
distribution works without a checkout.
`uv sync` installs two commands: `portfolio-optimiser` (the CLI) and `portfolio-optimiser-demo`
(the offline end-to-end proof). They are equivalent to the `python -m portfolio_optimiser.run` and

View file

@ -74,6 +74,14 @@ 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"]

View file

@ -1,10 +1,15 @@
"""Resolver for the location of the shared framework-neutral core (S3, R1-forberedelse).
"""Resolver for the location of the shared framework-neutral core (S3, R1-forberedelse; 4a pakket).
``shared/`` is today an in-repo directory, but is slated for extraction into its own commons repo
(R1/S4). Every MAF-side runtime consumer (``persona``, ``simulation``) resolves its location through
this ONE seam so the extraction is a re-point (env var), not a code change. Test fixtures pointing at
the real in-repo bundle hardcode the path deliberately they must not be redirected by a production
env var. Pure stdlib the shared core itself stays framework-free.
``shared/`` is a pull-only subtree of the commons repo at the repository root. Every MAF-side
runtime consumer (``persona``, ``simulation``) resolves its location through this ONE seam, so a
re-point is an env var, not a code change. Since Fase 4a the wheel also carries a byte-identical
mirror of the tree as packaged data (``portfolio_optimiser/_shared/``, hatchling force-include in
pyproject.toml), so an installed distribution and a container built from it works without a
checkout. Resolution order at CALL time: explicit env override the working tree's ``shared/``
when present (a repo checkout stays authoritative, which is what keeps the subtree contract and
the byte-level goldens untouched) the packaged copy. Test fixtures pointing at the real in-repo
bundle hardcode the path deliberately they must not be redirected by a production env var. Pure
stdlib the shared core itself stays framework-free.
"""
from __future__ import annotations
@ -14,12 +19,20 @@ from pathlib import Path
ENV_VAR = "PORTFOLIO_SHARED_ROOT"
_DEFAULT = Path(__file__).resolve().parents[2] / "shared"
_WORKTREE = Path(__file__).resolve().parents[2] / "shared"
_PACKAGED = Path(__file__).resolve().parent / "_shared"
def shared_root() -> Path:
"""Resolve the shared-core root at CALL time: ``PORTFOLIO_SHARED_ROOT`` if set (non-empty),
else the in-repo default. The call-time read is what keeps the override testable and the S4
extraction re-pointable without touching consumers."""
else the working tree's ``shared/`` when it exists (a checkout), else the packaged copy
shipped inside the distribution. The call-time read is what keeps the override testable and
the ordering observable; when neither directory exists the packaged path is returned so the
consumer's own fail-fast names the installed distribution's gap, not a checkout that was
never there."""
override = os.environ.get(ENV_VAR)
return Path(override) if override else _DEFAULT
if override:
return Path(override)
if _WORKTREE.is_dir():
return _WORKTREE
return _PACKAGED

View file

@ -0,0 +1,187 @@
"""Load-bearing tests for shared/ as packaged data with the working tree as override (Fase 4a).
README's install story is a clone because the shared spec, persona skill and example bundles under
``shared/`` were read from the WORKING TREE at run time an installed wheel had no shared data at
all (measured 2026-08-13: the 1.0.0 wheel carried 58 files, zero of them under ``shared/``). Fase
4a packages the whole ``shared/`` tree into the wheel as data (``portfolio_optimiser/_shared/``,
hatchling force-include) and teaches ``shared_root()`` a call-time resolution order: env override
working tree (when present) packaged copy. The working tree stays authoritative in a checkout
that is what keeps the pull-only subtree contract and the byte-level goldens untouched.
Three seams, each one measured by mutation:
- the wheel really CARRIES the tree, byte-identical to ``shared/`` (RED when the force-include is
dropped from pyproject the packaged mirror silently disappears from every future wheel);
- an installed distribution RESOLVES to the packaged copy and a real consumer (the persona loader)
reads it (RED when the fallback in ``shared_root()`` is detached the subprocess imports from
the unpacked wheel, never from the repo, so the working tree cannot mask the regression);
- the working tree still WINS when both copies exist (RED when the resolution order is flipped;
green both before and after the 4a change, so its discriminating power is proven by the flip
mutation, not by the fix itself its control assert on the packaged copy is what made it red
pre-4a).
The wheel is built by the REAL build backend (``uv build``) once per session: the packaging config
is itself a seam under test, and nothing short of a genuine build can measure it.
"""
from __future__ import annotations
import os
import subprocess
import sys
import zipfile
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
SHARED = REPO_ROOT / "shared"
PACKAGED_PREFIX = "portfolio_optimiser/_shared/"
@pytest.fixture(scope="session")
def built_wheel(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""One genuine wheel build per test session. ``uv`` is the repo's documented toolchain (README
sends a fresh clone through ``uv sync``), so its absence is a broken environment a hard
failure, never a skip: a skipped gate is no gate."""
out_dir = tmp_path_factory.mktemp("wheel-dist")
proc = subprocess.run(
["uv", "build", "--wheel", "--out-dir", str(out_dir)],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
assert proc.returncode == 0, f"uv build failed:\n{proc.stderr}"
wheels = sorted(out_dir.glob("*.whl"))
assert len(wheels) == 1, f"expected exactly one wheel, got {wheels}"
return wheels[0]
@pytest.fixture
def installed_dist(built_wheel: Path, tmp_path: Path) -> Path:
"""A per-test unpacked copy of the wheel (an installed distribution is an unpacked wheel on
``sys.path``). Per test, NOT per session: the override test plants a ``shared/`` directory at
the working-tree position beside the copy, and a shared copy would leak that sibling into the
resolution test."""
site_dir = tmp_path / "installed"
with zipfile.ZipFile(built_wheel) as zf:
zf.extractall(site_dir)
return site_dir
def _child_env(site_dir: Path) -> dict[str, str]:
"""Subprocess env: the unpacked wheel FIRST on ``sys.path`` (before the dev venv's editable
install), and the production override unset so the resolver's own ordering is what gets
measured."""
env = dict(os.environ)
env.pop("PORTFOLIO_SHARED_ROOT", None)
env["PYTHONPATH"] = str(site_dir)
return env
def _run_child(script: str, args: list[str], site_dir: Path, cwd: Path) -> str:
cwd.mkdir(exist_ok=True)
proc = subprocess.run(
[sys.executable, "-c", script, *args],
capture_output=True,
text=True,
check=False,
env=_child_env(site_dir),
cwd=cwd,
)
assert proc.returncode == 0, f"child failed:\n{proc.stderr}"
return proc.stdout
def test_wheel_carries_shared_tree_byte_identical(built_wheel: Path) -> None:
"""Every file under ``shared/`` lands in the wheel under ``portfolio_optimiser/_shared/`` —
same file set, same bytes. Byte identity is the property that lets the commons-owned goldens
keep gating the packaged copy: the mirror IS the subtree, never an edited derivative."""
worktree_files = sorted(
p.relative_to(SHARED).as_posix() for p in SHARED.rglob("*") if p.is_file()
)
assert worktree_files, "control: shared/ in the working tree must not be empty"
with zipfile.ZipFile(built_wheel) as zf:
packaged = {
name[len(PACKAGED_PREFIX) :]: zf.read(name)
for name in zf.namelist()
if name.startswith(PACKAGED_PREFIX)
}
assert sorted(packaged) == worktree_files, (
"the wheel's packaged copy does not mirror shared/ — is the force-include still in "
"pyproject.toml?"
)
drifted = [rel for rel in worktree_files if packaged[rel] != (SHARED / rel).read_bytes()]
assert not drifted, f"byte drift between shared/ and the packaged copy: {drifted}"
def test_installed_distribution_resolves_the_packaged_copy(
installed_dist: Path, tmp_path: Path
) -> None:
"""From an installed distribution (no working tree anywhere above the package), the resolver
yields the packaged copy and the persona loader a real consumer, not just the resolver
reads it. The child first proves it imported the unpacked wheel (never the repo's editable
install), so a green here cannot be the working tree answering in the wheel's name."""
script = (
"import sys\n"
"from pathlib import Path\n"
"import portfolio_optimiser\n"
"from portfolio_optimiser.persona import load_persona_example\n"
"from portfolio_optimiser.shared_root import shared_root\n"
"site_dir = Path(sys.argv[1]).resolve()\n"
"pkg_file = Path(portfolio_optimiser.__file__).resolve()\n"
"assert pkg_file == site_dir / 'portfolio_optimiser' / '__init__.py', (\n"
" f'control: child imported {pkg_file}, not the unpacked wheel'\n"
")\n"
"root = shared_root()\n"
"expected = site_dir / 'portfolio_optimiser' / '_shared'\n"
"assert root == expected, f'resolved {root}, expected the packaged copy {expected}'\n"
"assert (root / 'method-spec.md').is_file(), f'packaged spec missing under {root}'\n"
"example = load_persona_example()\n"
"assert example.marker, 'persona example unreadable from the packaged copy'\n"
"print('PACKAGED-OK')\n"
)
stdout = _run_child(script, [str(installed_dist)], installed_dist, tmp_path / "cwd")
assert "PACKAGED-OK" in stdout, "child never reached its final assert"
def test_working_tree_overrides_the_packaged_copy(installed_dist: Path, tmp_path: Path) -> None:
"""When a ``shared/`` directory exists at the working-tree position (two levels above the
package the layout of a repo checkout, and of this test's planted sibling), it wins over the
packaged copy. The control assert that the packaged copy EXISTS is load-bearing: without it,
an ordering flipped to packaged-first would still return the working tree whenever the
packaged copy is missing, and the assert below could not tell the two orderings apart."""
worktree_shared = (
tmp_path / "shared"
) # parents[2] of installed/portfolio_optimiser/shared_root.py
worktree_shared.mkdir()
(worktree_shared / "method-spec.md").write_text("worktree override marker\n", encoding="utf-8")
script = (
"import sys\n"
"from pathlib import Path\n"
"import portfolio_optimiser\n"
"from portfolio_optimiser.shared_root import shared_root\n"
"site_dir = Path(sys.argv[1]).resolve()\n"
"worktree = Path(sys.argv[2]).resolve()\n"
"pkg_file = Path(portfolio_optimiser.__file__).resolve()\n"
"assert pkg_file == site_dir / 'portfolio_optimiser' / '__init__.py', (\n"
" f'control: child imported {pkg_file}, not the unpacked wheel'\n"
")\n"
"packaged = site_dir / 'portfolio_optimiser' / '_shared'\n"
"assert packaged.is_dir(), (\n"
" 'control: the packaged copy must exist, or the ordering below is unmeasurable'\n"
")\n"
"root = shared_root()\n"
"assert root == worktree, (\n"
" f'resolved {root}, expected the working-tree override {worktree}'\n"
")\n"
"print('WORKTREE-OK')\n"
)
stdout = _run_child(
script, [str(installed_dist), str(worktree_shared)], installed_dist, tmp_path / "cwd"
)
assert "WORKTREE-OK" in stdout, "child never reached its final assert"