From 6329db832ef43bde60f49e8a4fcca1909ec84c34 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 01:12:50 +0000 Subject: [PATCH 1/2] fix: derive every repo's spelling join from the body map (#287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bare and prefixed spellings of seven repos normalised to two different keys, so a policy map filed under one was invisible to the other. `_target_sets` registered BOTH spellings of every `PyAuto*` repo as a known target while `repo_aliases` only joined them for the libraries. Reproduced on main: `@autobrain` reached a live known target with no witness row, so the refactor conductor advised "strengthen tests first" for the best-tested repo in the organism, and intake filed `Target: autobrain` — a folder that does not exist. Five organs, one project repo and the Hands were split this way. THE DECISION, taken before the edit as the prompt required: organs key on the PREFIXED form. That is not a new rule, it is #269's rule made executable — the canonical key is the package the repo SHIPS where it ships one, the repo name where it does not. Organs ship no package; Nerves is the one that does and keys bare already. Everything downstream was filed prefixed for organs, including the Mind's own target folders, so the alternative would have rekeyed five maps for no gain. THE CLASS, not the fourth instance. `repo_aliases` was hand-maintained while the known-target set was derived from the body map, so the two drifted silently. The join is now DERIVED (`_derived_aliases`), keyed off the body map's new `package:` field, and a hand row that contradicts the derivation raises instead of quietly winning. What remains hand-written is only what a body map cannot know: short forms and pre-rename spellings. Also here: - A repo whose NAME carries a `.` or `/` is now deliberately NOT registered. `normalise_repo` truncates at the first separator, so no mention could ever reach it; the tempting alias-the-head fix is worse than the gap, because where that head is the org's own name every org-qualified mention would resolve to that one repo. Derived from the names, not a hand-kept exclusion list. - `REPO_DISPLAY` derives from the body map too — it had the beginnings of the same drift, with reachable keys carrying no row, so a header came out as `Target: pyautohands`. Five guards, each mutation-tested: - no repo splits across two keys (removing the derived join names all seven) - no alias points at a key nothing is filed under - the body map's `package:` agrees with the witness map - unreachable repos are excluded AND their truncated head resolves to nothing - canonical keys survive a body map with no `package:` — the fallback that lets this half stand alone against a Mind checkout pinned to main Verified: 515 tests pass (510 before). `repos_sync --check` all 13 legs OK, tenant firewall included — it rejected two drafts of this prose for naming instance repos, which is why the comments read generically. An A/B of every spelling in the alias table and target sets shows exactly six resolutions changed, all of them the intended joins; libraries, short forms, renames, workspaces and org-qualified paths are byte-identical. Heart NOT EVALUATED — unreachable from a web-github session. Depends on PyAutoMind e160d40b (the `package:` field). The fallback above means this does not block on it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Nj7cTnL8Ke5WoNsdGVhCKn --- agents/conductors/intake/_intake.py | 33 ++++-- agents/faculties/sizing/_sizing.py | 176 ++++++++++++++++++++++++++-- config/policy.yaml | 39 +++++- tests/test_policy_seams.py | 154 +++++++++++++++++++++++- 4 files changed, 374 insertions(+), 28 deletions(-) diff --git a/agents/conductors/intake/_intake.py b/agents/conductors/intake/_intake.py index a3d1622..5f67710 100755 --- a/agents/conductors/intake/_intake.py +++ b/agents/conductors/intake/_intake.py @@ -42,6 +42,7 @@ RISK_KEYWORDS, AMBIGUITY_KEYWORDS, normalise_repo, declared_header, declared_inline, effective_difficulty, strip_declarations, _hits, policy as _sizing_policy, BODY_MAP_PATH, + _body_map_specs as _sizing_specs, ) # The shared board theme: the one place that answers "what does a one-tap board @@ -95,16 +96,28 @@ TARGET_SIGNALS = _sizing_policy()["target_signals"] # Human-readable display name for the header's `Target:` line. -REPO_DISPLAY = { - "autonerves": "PyAutoNerves", "autoconf": "PyAutoNerves", # autoconf = legacy alias - "autofit": "PyAutoFit", "autoarray": "PyAutoArray", - "autogalaxy": "PyAutoGalaxy", "autolens": "PyAutoLens", - "pyautomind": "PyAutoMind", "pyautobrain": "PyAutoBrain", - "pyautoheart": "PyAutoHeart", "pyautobuild": "PyAutoHands", - "pyautomemory": "PyAutoMemory", "autohands": "PyAutoHands", - "autobuild": "PyAutoHands", # back-compat: the package was renamed autobuild -> autohands - "workspaces": "workspaces", -} +# +# Derived from the body map, which already holds every repo's name in its real +# capitalisation — a hand-kept copy here would be the same drift #287 closed in +# the alias table one map over, and it already had the beginnings of it: keys the +# router could reach (`pyautohands`, and the CTI/Reduce libraries) had no row, so +# a header came out as `Target: pyautohands`. Only the rows a body map cannot +# know are written out: the pre-rename spellings and the workspace bucket. +def _repo_display() -> dict: + out = { + normalise_repo(name): name + for name in _sizing_specs() + } + out.update({ + "autoconf": out.get("autonerves", "autonerves"), # pre-rename spelling + "pyautobuild": out.get("pyautohands", "pyautohands"), # pre-rename spelling + "autobuild": out.get("pyautohands", "pyautohands"), # pre-rename package + "workspaces": "workspaces", # a bucket, not a repo + }) + return out + + +REPO_DISPLAY = _repo_display() PRIORITY_HIGH = ["urgent", "asap", "blocker", "blocking", "critical", "important", "high priority", "must fix", "regression"] PRIORITY_LOW = ["someday", "nice to have", "eventually", "low priority", "minor", diff --git a/agents/faculties/sizing/_sizing.py b/agents/faculties/sizing/_sizing.py index f41a056..d0e6e8a 100755 --- a/agents/faculties/sizing/_sizing.py +++ b/agents/faculties/sizing/_sizing.py @@ -78,28 +78,178 @@ def policy() -> dict: return _POLICY_CACHE +_BODY_MAP_CACHE: dict = {} + + +def _body_map_specs() -> dict: + """repo name -> its full body-map spec (the single source of repo identity). + + Cached: every call site below reads it, and it is a file the process never + writes. + """ + if not _BODY_MAP_CACHE: + import yaml + + _BODY_MAP_CACHE.update(yaml.safe_load(BODY_MAP_PATH.read_text())["repos"]) + return _BODY_MAP_CACHE + + def _body_map_categories() -> dict: """repo name -> category, from the body map (the single source of repo identity).""" - import yaml + return {name: spec["category"] for name, spec in _body_map_specs().items()} + + +# --- the canonical-key rule (PyAutoBrain#287) -------------------------------- +# One repo, one key. A prompt may spell a repo three ways — `@PyAutoFit`, +# `@autofit`, `PyAutoFit/` — and every one of them has to reach the SAME key, or +# whichever spelling the author happened to type silently decides whether the +# policy maps (test_witness, target_default_wiki, ...) resolve. Seven repos hit +# that split before it was closed here — one at #267, two more at #269, and the +# bare organ spellings plus one project repo at #287. +# +# THE RULE (first written down at #269, now executable): the canonical key is +# the package the repo SHIPS where it ships one, and the repo name where it does +# not. That asymmetry is not arbitrary — it is what prompts actually write. A +# library is named by its import (`@autofit`); an organ ships no package, so the +# only name it has is the repo's (`@PyAutoBrain`). +# +# The authority for "does it ship a package" is the body map's `package:` field — +# repo identity, declared once, where identity lives. + + +def _hand_aliases() -> dict: + """The alias rows a body map cannot derive. + + Two kinds only: the short forms prompts use for the libraries (`aa`, `af`), + and the pre-rename spellings that keep ~150 archived Mind prompts routing to + the repo they now name (`pyautobuild` -> the Hands, and the Nerves repo's + former name). Neither is inferable from a body map that records only what + the organism is called TODAY. + """ + return policy()["repo_aliases"] + + +def canonical_key(name: str, spec: dict | None = None) -> str: + """The one key every spelling of body-map repo `name` must reach.""" + if spec is None: + spec = _body_map_specs().get(name, {}) + package = spec.get("package") + if package: + return package.lower() + # Fallback for a body map that predates `package:` (an adopting fork, or + # this repo's own CI, which pins the sibling Mind checkout to `main`): the + # hand table still carries the library rows, so the answer is the same one + # `package:` gives. Kept deliberately — it is what lets the Brain half of + # #287 stand alone instead of going red until the Mind half merges. + low = name.lower() + return _hand_aliases().get(low, low) + + +def spellings_of(name: str, spec: dict | None = None) -> set: + """Every form of `name` that `_target_sets` registers as a known target. + + The repo name, the `PyAuto`-stripped bare form, and the package it ships. + These are the spellings a guard must prove all reach one key; they are NOT + every string that could mention the repo (an org-qualified path like + `@/` is handled by `normalise_repo`'s truncation). + """ + if spec is None: + spec = _body_map_specs().get(name, {}) + low = name.lower() + out = {low} + if low.startswith("pyauto"): + out.add(low[2:]) + if spec.get("package"): + out.add(spec["package"].lower()) + return out + - data = yaml.safe_load(BODY_MAP_PATH.read_text()) - return {name: spec["category"] for name, spec in data["repos"].items()} +def unreachable_repos() -> dict: + """Body-map repos an @-mention can never name -> why. + + ``normalise_repo`` truncates at the first ``.`` or ``/`` (so `@aa.decorators` + and an org-qualified `@/` path both resolve to their head token). + A repo whose NAME contains one of those separators therefore cannot survive + normalisation, and registering it as a known target would be a lie: nothing + could ever resolve to it. Aliasing the truncated head instead would be worse + than the lie — where the head happens to be the ORG's own name, every + org-qualified mention would start resolving to that one repo. + + Derived from the names themselves, so it stays right for any body map rather + than being a hand-kept exclusion list (PyAutoBrain#287). + """ + return { + name: "name contains a '.' or '/', which normalise_repo truncates — " + "no @-mention can reach it" + for name in _body_map_specs() + if re.split(r"[./]", name, 1)[0] != name + } + + +def _derived_aliases() -> dict: + """Every registered spelling of every body-map repo -> its canonical key. + + This is the half of ``repo_aliases`` that must NOT be typed by hand. The + known-target set was always derived from the body map while the alias table + was maintained by hand, so the two drifted silently and the gap surfaced only + as a wrong-but-plausible conductor message — "strengthen tests first" for a + repo with a full suite (PyAutoBrain#267, #269, #287). Deriving the join means + a repo added to the body map arrives with its spellings already joined. + """ + grouping = policy()["sizing_categories"] + registered = {cat for kinds in grouping.values() for cat in kinds} + unreachable = unreachable_repos() + out = {} + for name, spec in _body_map_specs().items(): + if spec["category"] not in registered or name in unreachable: + continue + canonical = canonical_key(name, spec) + for spelling in spellings_of(name, spec): + out[spelling] = canonical + return out + + +def _repo_aliases() -> dict: + """The effective alias table: derived join + the rows only a human can know. + + A hand row that CONTRADICTS the derivation is drift, and drift in this table + is exactly what #287 is about — so it raises here rather than quietly + winning. A hand row the derivation does not cover (a short form, a rename) + passes through untouched. + """ + derived = _derived_aliases() + hand = _hand_aliases() + conflicts = { + alias: (derived[alias], hand[alias]) + for alias in hand + if alias in derived and hand[alias] != derived[alias] + } + if conflicts: + raise ValueError( + "config/policy.yaml repo_aliases contradicts the body map " + "(alias -> (derived, hand)): " + f"{conflicts}. The body map's `package:` field is the authority for " + "a repo's canonical key; fix the hand row or the package name." + ) + return {**derived, **hand} def _target_sets() -> tuple[set, set, set]: - cats = _body_map_categories() + specs = _body_map_specs() pol = policy() grouping = pol["sizing_categories"] + unreachable = unreachable_repos() def names_for(kind): wanted = set(grouping[kind]) out = set() - for name, cat in cats.items(): - if cat in wanted: - out.add(name.lower()) - if name.lower().startswith("pyauto"): - out.add(name.lower()[2:]) # PyAutoFit -> autofit package form + for name, spec in specs.items(): + # An unreachable repo is deliberately NOT registered: a known target + # nothing can resolve to is the same silent lie as a split spelling. + if spec["category"] in wanted and name not in unreachable: + out |= spellings_of(name, spec) + out.add(canonical_key(name, spec)) return out libraries = names_for("library") @@ -108,14 +258,16 @@ def names_for(kind): return libraries, workspaces, organism +# Normalise an @-mention or folder name to a canonical key. Built before the +# target sets because `canonical_key`'s pre-`package:` fallback reads the hand +# table, and the sets register the canonical key it returns. +REPO_ALIASES = _repo_aliases() + # Targets that are source *libraries* (work classifies as library vs workspace), # workspaces/tutorials/example repos, and the organism's own organs — all # derived from the body map's categories per the policy's grouping. LIBRARY_REPOS, WORKSPACE_REPOS, ORGANISM_REPOS = _target_sets() -# Normalise an @-mention or folder name to a canonical key. -REPO_ALIASES = policy()["repo_aliases"] - # --- PyAutoMemory sub-wiki routing (shared science vocabulary) ---------------- # Map keywords -> the PyAutoMemory sub-wiki that holds relevant context. This is # also the canonical *science vocabulary* difficulty scoring keys off (see diff --git a/config/policy.yaml b/config/policy.yaml index 05e69a1..01c1b44 100644 --- a/config/policy.yaml +++ b/config/policy.yaml @@ -4,11 +4,40 @@ # categories at runtime; nothing here duplicates the body map). # Normalise an @-mention or folder name to a canonical target key. +# +# THE CANONICAL-KEY RULE: one repo, one key — the package the repo SHIPS where it +# ships one, the repo name where it does not. So the libraries key bare +# (`autofit`), and the organs, which ship no package, key by repo name +# (`pyautobrain`); Nerves is the organ that ships one, so it keys `autonerves`. +# The asymmetry is not arbitrary: it is what prompts actually write. +# +# MOST OF THIS TABLE IS NO LONGER WRITTEN HERE. The bare/prefixed/package join is +# DERIVED from the body map at runtime (`_sizing._derived_aliases`), keyed off the +# `package:` field in PyAutoMind/repos.yaml. It had to be: this table was hand-kept +# while the known-target set was derived, so the two drifted silently and the gap +# surfaced only as a wrong-but-plausible conductor message — four repos in a row +# (PyAutoBrain#267, #269, #287). A row below that CONTRADICTS the derivation now +# raises rather than quietly winning. +# +# What is left is what a body map cannot know: repo_aliases: + # 1. Short forms. Prompts write `@aa.decorators.to_vector_yx`, and no field in + # the body map says so. aa: autoarray af: autofit ag: autogalaxy al: autolens + # 2. Pre-rename spellings. The body map records what the organism is called + # TODAY; these keep the archived Mind prompts routing to the repo they now + # name. + autoconf: autonerves # back-compat: the Nerves package was renamed + pyautoconf: autonerves # back-compat: the Nerves repo was renamed + pyautobuild: pyautohands # back-compat: the Hands repo was renamed PyAutoBuild → PyAutoHands + # 3. The library rows the derivation would otherwise produce, kept as the + # fallback for a body map that predates `package:` — an adopting fork, or + # this repo's own CI, which pins the sibling Mind checkout to `main`. + # `canonical_key` reads them only when no `package:` is declared, so they + # are dead weight the moment one is, and can be deleted then. pyautoarray: autoarray pyautofit: autofit pyautogalaxy: autogalaxy @@ -16,10 +45,6 @@ repo_aliases: pyautonerves: autonerves pyautocti: autocti pyautoreduce: autoreduce - autohands: pyautohands # organs have no package, so the repo name is canonical - autoconf: autonerves # back-compat: the Nerves package was renamed autoconf → autonerves - pyautoconf: autonerves # back-compat: the Nerves repo was renamed PyAutoConf → PyAutoNerves - pyautobuild: pyautohands # back-compat: the Hands repo was renamed PyAutoBuild → PyAutoHands # Which body-map categories mean "library-flow" / "workspace-flow" / # "organism-infrastructure" to the sizing faculty; extra literal targets @@ -29,7 +54,11 @@ sizing_categories: workspace: [workspace, workspace_test, workspace_developer, howto, assistant, pipeline, project] organism: [organ] extra_workspace_targets: [workspaces] -extra_organism_targets: [autohands] +# Empty since PyAutoBrain#287: `autohands` was declared here because the bare +# organ spellings were not derived. They are now (every spelling of every +# body-map repo is), so a literal here would be redundant — the block stays as +# the seam a fork uses for a target its body map does not carry. +extra_organism_targets: [] # Keyword -> PyAutoMemory sub-wiki (the wiki// layout; also the science # vocabulary the difficulty scoring keys off). Source of truth for the wiki diff --git a/tests/test_policy_seams.py b/tests/test_policy_seams.py index fde1e6c..2f8253d 100644 --- a/tests/test_policy_seams.py +++ b/tests/test_policy_seams.py @@ -35,7 +35,9 @@ def test_workspace_and_organism_sets(): assert "howtolens" in _sizing.WORKSPACE_REPOS assert "workspaces" in _sizing.WORKSPACE_REPOS # policy extra assert "pyautobrain" in _sizing.ORGANISM_REPOS - assert "autohands" in _sizing.ORGANISM_REPOS # policy extra + # Derived since PyAutoBrain#287 — every spelling of every body-map repo is + # registered, so this no longer needs an `extra_organism_targets` literal. + assert "autohands" in _sizing.ORGANISM_REPOS # the three sets stay disjoint — a repo must classify one way assert not (_sizing.LIBRARY_REPOS & _sizing.WORKSPACE_REPOS) assert not (_sizing.LIBRARY_REPOS & _sizing.ORGANISM_REPOS) @@ -184,3 +186,153 @@ def test_witness_repos_resolve_from_the_package_spelling_too(): "package spelling normalises to a different key than the repo spelling " f"— add a repo_aliases entry joining them: {split}" ) + + +# --- the alias/known-target seam (PyAutoBrain#287) --------------------------- +# +# The three guards below close the defect class the witness-map guards above +# only closed one instance of. `repo_aliases` was HAND-MAINTAINED while the +# known-target set is DERIVED from the body map, so the two drifted silently and +# the gap surfaced only as a wrong-but-plausible conductor message. Seven repos +# hit it in sequence — one at #267, two more at #269, then the organs and a +# project repo at #287. These pin the *class*: a repo may not split across two +# keys, an alias may not point at a key nothing is filed under, and the body +# map's `package:` must agree with the witness map. +# +# Every repo name here is derived from the body map, never typed: the tenant +# firewall allows only three literals in this file, and a guard that hardcoded +# names would stop holding for an adopting fork the moment its body map differed. + + +def _sizing_category_repos(): + """Body-map repos the sizing sets actually register, by name -> canonical key.""" + specs = _sizing._body_map_specs() + grouping = _sizing.policy()["sizing_categories"] + registered = {c for kinds in grouping.values() for c in kinds} + unreachable = _sizing.unreachable_repos() + return { + name: _sizing.canonical_key(name, spec) + for name, spec in specs.items() + if spec["category"] in registered and name not in unreachable + } + + +def test_no_repo_splits_across_two_keys(): + """Every spelling of a registered repo must reach ONE key, and that key must + itself be a known target. + + This is the #287 defect stated directly. `_target_sets` registers both + `pyautobrain` and `autobrain` as known targets, but only the prefixed one was + filed under, so `@autobrain` resolved to a live target with no witness row: + `pyauto-brain refactor` advised "strengthen tests first" for the best-tested + repo in the organism, and `pyauto-brain intake` filed `Target: autobrain`, a + folder that does not exist. + """ + split = {} + for name, canonical in _sizing_category_repos().items(): + keys = {s: _sizing.normalise_repo(s) for s in _sizing.spellings_of(name)} + if set(keys.values()) != {canonical}: + split[name] = keys + elif canonical not in _sizing.KNOWN_REPOS: + split[name] = f"canonical key {canonical!r} is not a known target" + assert not split, ( + "repos whose spellings do not all reach one known-target key — the " + f"spelling a prompt happens to use decides whether routing works: {split}" + ) + + +def test_no_alias_points_at_a_key_nothing_is_filed_under(): + """An alias whose VALUE is not a canonical key is a dead end. + + That is what `pyautoconf: autoconf` had become (#267): both spellings of one + repo resolved, to two keys, neither of which was anything. Checking values + (not just keys, as the witness guards do) catches the next one at the source + map rather than in whichever consumer notices first. + """ + canonical = set(_sizing_category_repos().values()) + extras = set(_sizing.policy()["extra_workspace_targets"]) + extras |= set(_sizing.policy()["extra_organism_targets"]) + dead = { + alias: target + for alias, target in _sizing.REPO_ALIASES.items() + if target not in canonical + and _sizing.normalise_repo(target) not in canonical + and target not in extras + } + assert not dead, ( + "repo_aliases rows pointing at a key no body-map repo is filed under " + f"— routing through them reaches nothing: {dead}" + ) + + +def test_body_map_package_agrees_with_the_witness_map(): + """The body map's `package:` and the witness map must corroborate each other. + + A `/test_` witness row names the package that repo ships; so does + `repos.yaml`. Two independent statements of one fact are only worth having if + something compares them — #269 verified every witness row by reading each + repo's own tree, and this pins the body map to that verified evidence rather + than to a second, unchecked transcription. + """ + sys.path.insert(0, str(BRAIN_HOME / "agents" / "conductors" / "refactor")) + import _refactor + + specs = _sizing._body_map_specs() + disagree = {} + for key, witness in _refactor.TEST_WITNESS.items(): + repo, _, test_dir = witness.partition("/") + declared = specs.get(repo, {}).get("package") + if test_dir.startswith("test_"): + witnessed = test_dir[len("test_"):] + if declared != witnessed: + disagree[repo] = f"repos.yaml package={declared!r}, witness names {witnessed!r}" + elif declared is not None: + disagree[repo] = ( + f"repos.yaml declares package={declared!r} but the witness row is " + f"{witness!r} — a plain tests/ dir names no package" + ) + assert not disagree, ( + "body map and witness map disagree about which package a repo ships: " + f"{disagree}" + ) + + +def test_unreachable_repos_are_excluded_rather_than_half_registered(): + """The acceptance criterion's other branch: deliberately NOT registered. + + ``normalise_repo`` truncates at the first ``.``/``/``, so a repo whose name + carries one can never be reached by an @-mention. The tempting fix — alias + the truncated head — is worse than the gap: where that head is the ORG's own + name, every org-qualified ``@/`` mention would resolve to that one + repo. Excluding such a repo is therefore the deliberate choice, and this pins + BOTH halves of it: it is out of the known targets, and the org-qualified path + it would have hijacked still does not resolve to it. + """ + for name in _sizing.unreachable_repos(): + assert name not in _sizing.KNOWN_REPOS, name + head = name.split(".")[0].split("/")[0] + assert _sizing.normalise_repo(head) not in _sizing.KNOWN_REPOS, ( + f"the truncated head of {name!r} resolves to a known target — an " + "org-qualified mention would be hijacked by it" + ) + + +def test_canonical_keys_survive_a_body_map_without_package(): + """The Brain half must stand alone against a Mind that predates `package:`. + + This repo's CI checks the sibling Mind out at `main`, and an adopting fork's + body map may never carry the field at all. `canonical_key` therefore falls + back to the hand table, which still holds the library rows — so the keys come + out identical either way. Without this the fix would be un-mergeable except + in lockstep, and the fallback would be the kind of load-bearing path nothing + exercises until it breaks. + """ + specs = { + name: {k: v for k, v in spec.items() if k != "package"} + for name, spec in _sizing._body_map_specs().items() + } + for name, stripped in specs.items(): + assert _sizing.canonical_key(name, stripped) == _sizing.canonical_key(name), ( + f"{name}: canonical key moves when `package:` is absent — the " + "pre-package fallback in config/policy.yaml no longer covers it" + ) From dca7736662592d79555647d7cff0fd76a41e350a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 01:32:34 +0000 Subject: [PATCH 2/2] fix: the corroboration guard is all-or-nothing, not lockstep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI failed on this PR: `test_body_map_package_agrees_with_the_witness_map` required `repos.yaml` to DECLARE `package:` for every witness row that names one, and tests.yml pins the sibling Mind checkout to `main`, where the field does not exist yet. The rest of the fix already tolerated that — `canonical_key` falls back to the hand table, with a test pinning it — but this guard did not, so the half that was designed to stand alone could not. Absence of the field is an older body map, not a contradiction: a map declaring NO package anywhere has nothing to corroborate, and the guard stands down. The moment it declares even one, every witness row that names a package must have one — so a PARTIALLY declared map still fails, which is what a new library added without its `package:` would look like. That is a stronger guard than the one it replaces, not a weakened one. Verified against CI's exact condition — this branch's Brain against a PyAutoMind checkout at origin/main, zero `package:` rows: 515 pass. Against the branch's Mind, with all seven declared: 515 pass. Mutation: declaring one package and not the rest fails the guard naming the other six. Also fixes the DeprecationWarning this PR introduced (`maxsplit` passed positionally in `unreachable_repos`). The identical warning from `normalise_repo` is pre-existing and left alone. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Nj7cTnL8Ke5WoNsdGVhCKn --- agents/faculties/sizing/_sizing.py | 2 +- tests/test_policy_seams.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/agents/faculties/sizing/_sizing.py b/agents/faculties/sizing/_sizing.py index d0e6e8a..f5a63c3 100755 --- a/agents/faculties/sizing/_sizing.py +++ b/agents/faculties/sizing/_sizing.py @@ -183,7 +183,7 @@ def unreachable_repos() -> dict: name: "name contains a '.' or '/', which normalise_repo truncates — " "no @-mention can reach it" for name in _body_map_specs() - if re.split(r"[./]", name, 1)[0] != name + if re.split(r"[./]", name, maxsplit=1)[0] != name } diff --git a/tests/test_policy_seams.py b/tests/test_policy_seams.py index 2f8253d..43253be 100644 --- a/tests/test_policy_seams.py +++ b/tests/test_policy_seams.py @@ -273,11 +273,22 @@ def test_body_map_package_agrees_with_the_witness_map(): something compares them — #269 verified every witness row by reading each repo's own tree, and this pins the body map to that verified evidence rather than to a second, unchecked transcription. + + ALL-OR-NOTHING, not lockstep. A body map that declares NO package anywhere + simply predates the field (this repo's CI pins the sibling Mind checkout to + `main`, and an adopting fork may never adopt it) — absence is an older map, + not a contradiction, so there is nothing to compare and the guard stands + down. Once the map declares even one, every witness row that names a package + must have one: a PARTIALLY declared map is the drift this exists to catch, + and is what a new library added without its `package:` would look like. """ sys.path.insert(0, str(BRAIN_HOME / "agents" / "conductors" / "refactor")) import _refactor specs = _sizing._body_map_specs() + if not any("package" in spec for spec in specs.values()): + return # a body map from before the field existed — nothing to corroborate + disagree = {} for key, witness in _refactor.TEST_WITNESS.items(): repo, _, test_dir = witness.partition("/")