Skip to content

Commit 6329db8

Browse files
committed
fix: derive every repo's spelling join from the body map (#287)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nj7cTnL8Ke5WoNsdGVhCKn
1 parent 5d816a7 commit 6329db8

4 files changed

Lines changed: 374 additions & 28 deletions

File tree

agents/conductors/intake/_intake.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
RISK_KEYWORDS, AMBIGUITY_KEYWORDS, normalise_repo, declared_header,
4343
declared_inline, effective_difficulty, strip_declarations, _hits,
4444
policy as _sizing_policy, BODY_MAP_PATH,
45+
_body_map_specs as _sizing_specs,
4546
)
4647

4748
# The shared board theme: the one place that answers "what does a one-tap board
@@ -95,16 +96,28 @@
9596
TARGET_SIGNALS = _sizing_policy()["target_signals"]
9697

9798
# Human-readable display name for the header's `Target:` line.
98-
REPO_DISPLAY = {
99-
"autonerves": "PyAutoNerves", "autoconf": "PyAutoNerves", # autoconf = legacy alias
100-
"autofit": "PyAutoFit", "autoarray": "PyAutoArray",
101-
"autogalaxy": "PyAutoGalaxy", "autolens": "PyAutoLens",
102-
"pyautomind": "PyAutoMind", "pyautobrain": "PyAutoBrain",
103-
"pyautoheart": "PyAutoHeart", "pyautobuild": "PyAutoHands",
104-
"pyautomemory": "PyAutoMemory", "autohands": "PyAutoHands",
105-
"autobuild": "PyAutoHands", # back-compat: the package was renamed autobuild -> autohands
106-
"workspaces": "workspaces",
107-
}
99+
#
100+
# Derived from the body map, which already holds every repo's name in its real
101+
# capitalisation — a hand-kept copy here would be the same drift #287 closed in
102+
# the alias table one map over, and it already had the beginnings of it: keys the
103+
# router could reach (`pyautohands`, and the CTI/Reduce libraries) had no row, so
104+
# a header came out as `Target: pyautohands`. Only the rows a body map cannot
105+
# know are written out: the pre-rename spellings and the workspace bucket.
106+
def _repo_display() -> dict:
107+
out = {
108+
normalise_repo(name): name
109+
for name in _sizing_specs()
110+
}
111+
out.update({
112+
"autoconf": out.get("autonerves", "autonerves"), # pre-rename spelling
113+
"pyautobuild": out.get("pyautohands", "pyautohands"), # pre-rename spelling
114+
"autobuild": out.get("pyautohands", "pyautohands"), # pre-rename package
115+
"workspaces": "workspaces", # a bucket, not a repo
116+
})
117+
return out
118+
119+
120+
REPO_DISPLAY = _repo_display()
108121
PRIORITY_HIGH = ["urgent", "asap", "blocker", "blocking", "critical", "important",
109122
"high priority", "must fix", "regression"]
110123
PRIORITY_LOW = ["someday", "nice to have", "eventually", "low priority", "minor",

agents/faculties/sizing/_sizing.py

Lines changed: 164 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,28 +78,178 @@ def policy() -> dict:
7878
return _POLICY_CACHE
7979

8080

81+
_BODY_MAP_CACHE: dict = {}
82+
83+
84+
def _body_map_specs() -> dict:
85+
"""repo name -> its full body-map spec (the single source of repo identity).
86+
87+
Cached: every call site below reads it, and it is a file the process never
88+
writes.
89+
"""
90+
if not _BODY_MAP_CACHE:
91+
import yaml
92+
93+
_BODY_MAP_CACHE.update(yaml.safe_load(BODY_MAP_PATH.read_text())["repos"])
94+
return _BODY_MAP_CACHE
95+
96+
8197
def _body_map_categories() -> dict:
8298
"""repo name -> category, from the body map (the single source of repo
8399
identity)."""
84-
import yaml
100+
return {name: spec["category"] for name, spec in _body_map_specs().items()}
101+
102+
103+
# --- the canonical-key rule (PyAutoBrain#287) --------------------------------
104+
# One repo, one key. A prompt may spell a repo three ways — `@PyAutoFit`,
105+
# `@autofit`, `PyAutoFit/` — and every one of them has to reach the SAME key, or
106+
# whichever spelling the author happened to type silently decides whether the
107+
# policy maps (test_witness, target_default_wiki, ...) resolve. Seven repos hit
108+
# that split before it was closed here — one at #267, two more at #269, and the
109+
# bare organ spellings plus one project repo at #287.
110+
#
111+
# THE RULE (first written down at #269, now executable): the canonical key is
112+
# the package the repo SHIPS where it ships one, and the repo name where it does
113+
# not. That asymmetry is not arbitrary — it is what prompts actually write. A
114+
# library is named by its import (`@autofit`); an organ ships no package, so the
115+
# only name it has is the repo's (`@PyAutoBrain`).
116+
#
117+
# The authority for "does it ship a package" is the body map's `package:` field —
118+
# repo identity, declared once, where identity lives.
119+
120+
121+
def _hand_aliases() -> dict:
122+
"""The alias rows a body map cannot derive.
123+
124+
Two kinds only: the short forms prompts use for the libraries (`aa`, `af`),
125+
and the pre-rename spellings that keep ~150 archived Mind prompts routing to
126+
the repo they now name (`pyautobuild` -> the Hands, and the Nerves repo's
127+
former name). Neither is inferable from a body map that records only what
128+
the organism is called TODAY.
129+
"""
130+
return policy()["repo_aliases"]
131+
132+
133+
def canonical_key(name: str, spec: dict | None = None) -> str:
134+
"""The one key every spelling of body-map repo `name` must reach."""
135+
if spec is None:
136+
spec = _body_map_specs().get(name, {})
137+
package = spec.get("package")
138+
if package:
139+
return package.lower()
140+
# Fallback for a body map that predates `package:` (an adopting fork, or
141+
# this repo's own CI, which pins the sibling Mind checkout to `main`): the
142+
# hand table still carries the library rows, so the answer is the same one
143+
# `package:` gives. Kept deliberately — it is what lets the Brain half of
144+
# #287 stand alone instead of going red until the Mind half merges.
145+
low = name.lower()
146+
return _hand_aliases().get(low, low)
147+
148+
149+
def spellings_of(name: str, spec: dict | None = None) -> set:
150+
"""Every form of `name` that `_target_sets` registers as a known target.
151+
152+
The repo name, the `PyAuto`-stripped bare form, and the package it ships.
153+
These are the spellings a guard must prove all reach one key; they are NOT
154+
every string that could mention the repo (an org-qualified path like
155+
`@<org>/<repo>` is handled by `normalise_repo`'s truncation).
156+
"""
157+
if spec is None:
158+
spec = _body_map_specs().get(name, {})
159+
low = name.lower()
160+
out = {low}
161+
if low.startswith("pyauto"):
162+
out.add(low[2:])
163+
if spec.get("package"):
164+
out.add(spec["package"].lower())
165+
return out
166+
85167

86-
data = yaml.safe_load(BODY_MAP_PATH.read_text())
87-
return {name: spec["category"] for name, spec in data["repos"].items()}
168+
def unreachable_repos() -> dict:
169+
"""Body-map repos an @-mention can never name -> why.
170+
171+
``normalise_repo`` truncates at the first ``.`` or ``/`` (so `@aa.decorators`
172+
and an org-qualified `@<org>/<repo>` path both resolve to their head token).
173+
A repo whose NAME contains one of those separators therefore cannot survive
174+
normalisation, and registering it as a known target would be a lie: nothing
175+
could ever resolve to it. Aliasing the truncated head instead would be worse
176+
than the lie — where the head happens to be the ORG's own name, every
177+
org-qualified mention would start resolving to that one repo.
178+
179+
Derived from the names themselves, so it stays right for any body map rather
180+
than being a hand-kept exclusion list (PyAutoBrain#287).
181+
"""
182+
return {
183+
name: "name contains a '.' or '/', which normalise_repo truncates — "
184+
"no @-mention can reach it"
185+
for name in _body_map_specs()
186+
if re.split(r"[./]", name, 1)[0] != name
187+
}
188+
189+
190+
def _derived_aliases() -> dict:
191+
"""Every registered spelling of every body-map repo -> its canonical key.
192+
193+
This is the half of ``repo_aliases`` that must NOT be typed by hand. The
194+
known-target set was always derived from the body map while the alias table
195+
was maintained by hand, so the two drifted silently and the gap surfaced only
196+
as a wrong-but-plausible conductor message — "strengthen tests first" for a
197+
repo with a full suite (PyAutoBrain#267, #269, #287). Deriving the join means
198+
a repo added to the body map arrives with its spellings already joined.
199+
"""
200+
grouping = policy()["sizing_categories"]
201+
registered = {cat for kinds in grouping.values() for cat in kinds}
202+
unreachable = unreachable_repos()
203+
out = {}
204+
for name, spec in _body_map_specs().items():
205+
if spec["category"] not in registered or name in unreachable:
206+
continue
207+
canonical = canonical_key(name, spec)
208+
for spelling in spellings_of(name, spec):
209+
out[spelling] = canonical
210+
return out
211+
212+
213+
def _repo_aliases() -> dict:
214+
"""The effective alias table: derived join + the rows only a human can know.
215+
216+
A hand row that CONTRADICTS the derivation is drift, and drift in this table
217+
is exactly what #287 is about — so it raises here rather than quietly
218+
winning. A hand row the derivation does not cover (a short form, a rename)
219+
passes through untouched.
220+
"""
221+
derived = _derived_aliases()
222+
hand = _hand_aliases()
223+
conflicts = {
224+
alias: (derived[alias], hand[alias])
225+
for alias in hand
226+
if alias in derived and hand[alias] != derived[alias]
227+
}
228+
if conflicts:
229+
raise ValueError(
230+
"config/policy.yaml repo_aliases contradicts the body map "
231+
"(alias -> (derived, hand)): "
232+
f"{conflicts}. The body map's `package:` field is the authority for "
233+
"a repo's canonical key; fix the hand row or the package name."
234+
)
235+
return {**derived, **hand}
88236

89237

90238
def _target_sets() -> tuple[set, set, set]:
91-
cats = _body_map_categories()
239+
specs = _body_map_specs()
92240
pol = policy()
93241
grouping = pol["sizing_categories"]
242+
unreachable = unreachable_repos()
94243

95244
def names_for(kind):
96245
wanted = set(grouping[kind])
97246
out = set()
98-
for name, cat in cats.items():
99-
if cat in wanted:
100-
out.add(name.lower())
101-
if name.lower().startswith("pyauto"):
102-
out.add(name.lower()[2:]) # PyAutoFit -> autofit package form
247+
for name, spec in specs.items():
248+
# An unreachable repo is deliberately NOT registered: a known target
249+
# nothing can resolve to is the same silent lie as a split spelling.
250+
if spec["category"] in wanted and name not in unreachable:
251+
out |= spellings_of(name, spec)
252+
out.add(canonical_key(name, spec))
103253
return out
104254

105255
libraries = names_for("library")
@@ -108,14 +258,16 @@ def names_for(kind):
108258
return libraries, workspaces, organism
109259

110260

261+
# Normalise an @-mention or folder name to a canonical key. Built before the
262+
# target sets because `canonical_key`'s pre-`package:` fallback reads the hand
263+
# table, and the sets register the canonical key it returns.
264+
REPO_ALIASES = _repo_aliases()
265+
111266
# Targets that are source *libraries* (work classifies as library vs workspace),
112267
# workspaces/tutorials/example repos, and the organism's own organs — all
113268
# derived from the body map's categories per the policy's grouping.
114269
LIBRARY_REPOS, WORKSPACE_REPOS, ORGANISM_REPOS = _target_sets()
115270

116-
# Normalise an @-mention or folder name to a canonical key.
117-
REPO_ALIASES = policy()["repo_aliases"]
118-
119271
# --- PyAutoMemory sub-wiki routing (shared science vocabulary) ----------------
120272
# Map keywords -> the PyAutoMemory sub-wiki that holds relevant context. This is
121273
# also the canonical *science vocabulary* difficulty scoring keys off (see

config/policy.yaml

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,47 @@
44
# categories at runtime; nothing here duplicates the body map).
55

66
# Normalise an @-mention or folder name to a canonical target key.
7+
#
8+
# THE CANONICAL-KEY RULE: one repo, one key — the package the repo SHIPS where it
9+
# ships one, the repo name where it does not. So the libraries key bare
10+
# (`autofit`), and the organs, which ship no package, key by repo name
11+
# (`pyautobrain`); Nerves is the organ that ships one, so it keys `autonerves`.
12+
# The asymmetry is not arbitrary: it is what prompts actually write.
13+
#
14+
# MOST OF THIS TABLE IS NO LONGER WRITTEN HERE. The bare/prefixed/package join is
15+
# DERIVED from the body map at runtime (`_sizing._derived_aliases`), keyed off the
16+
# `package:` field in PyAutoMind/repos.yaml. It had to be: this table was hand-kept
17+
# while the known-target set was derived, so the two drifted silently and the gap
18+
# surfaced only as a wrong-but-plausible conductor message — four repos in a row
19+
# (PyAutoBrain#267, #269, #287). A row below that CONTRADICTS the derivation now
20+
# raises rather than quietly winning.
21+
#
22+
# What is left is what a body map cannot know:
723
repo_aliases:
24+
# 1. Short forms. Prompts write `@aa.decorators.to_vector_yx`, and no field in
25+
# the body map says so.
826
aa: autoarray
927
af: autofit
1028
ag: autogalaxy
1129
al: autolens
30+
# 2. Pre-rename spellings. The body map records what the organism is called
31+
# TODAY; these keep the archived Mind prompts routing to the repo they now
32+
# name.
33+
autoconf: autonerves # back-compat: the Nerves package was renamed
34+
pyautoconf: autonerves # back-compat: the Nerves repo was renamed
35+
pyautobuild: pyautohands # back-compat: the Hands repo was renamed PyAutoBuild → PyAutoHands
36+
# 3. The library rows the derivation would otherwise produce, kept as the
37+
# fallback for a body map that predates `package:` — an adopting fork, or
38+
# this repo's own CI, which pins the sibling Mind checkout to `main`.
39+
# `canonical_key` reads them only when no `package:` is declared, so they
40+
# are dead weight the moment one is, and can be deleted then.
1241
pyautoarray: autoarray
1342
pyautofit: autofit
1443
pyautogalaxy: autogalaxy
1544
pyautolens: autolens
1645
pyautonerves: autonerves
1746
pyautocti: autocti
1847
pyautoreduce: autoreduce
19-
autohands: pyautohands # organs have no package, so the repo name is canonical
20-
autoconf: autonerves # back-compat: the Nerves package was renamed autoconf → autonerves
21-
pyautoconf: autonerves # back-compat: the Nerves repo was renamed PyAutoConf → PyAutoNerves
22-
pyautobuild: pyautohands # back-compat: the Hands repo was renamed PyAutoBuild → PyAutoHands
2348

2449
# Which body-map categories mean "library-flow" / "workspace-flow" /
2550
# "organism-infrastructure" to the sizing faculty; extra literal targets
@@ -29,7 +54,11 @@ sizing_categories:
2954
workspace: [workspace, workspace_test, workspace_developer, howto, assistant, pipeline, project]
3055
organism: [organ]
3156
extra_workspace_targets: [workspaces]
32-
extra_organism_targets: [autohands]
57+
# Empty since PyAutoBrain#287: `autohands` was declared here because the bare
58+
# organ spellings were not derived. They are now (every spelling of every
59+
# body-map repo is), so a literal here would be redundant — the block stays as
60+
# the seam a fork uses for a target its body map does not carry.
61+
extra_organism_targets: []
3362

3463
# Keyword -> PyAutoMemory sub-wiki (the wiki/<domain>/ layout; also the science
3564
# vocabulary the difficulty scoring keys off). Source of truth for the wiki

0 commit comments

Comments
 (0)