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:
parent
142bfa9666
commit
f5ec8c84f6
5 changed files with 238 additions and 13 deletions
16
CLAUDE.md
16
CLAUDE.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
187
tests/test_shared_packaged_data_loadbearing.py
Normal file
187
tests/test_shared_packaged_data_loadbearing.py
Normal 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue