Skip to content

Commit b28d533

Browse files
committed
fix: Third review sweep
1 parent 5697cdd commit b28d533

5 files changed

Lines changed: 703 additions & 119 deletions

File tree

BUNDLE_API.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,15 @@ the deserialize half is covered by
212212
and checked for forbidden install/uninstall/registry-mutation handlers.
213213
Authors of files with non-literal prefixes can opt in via a
214214
`# router-trust: in-scope` marker.
215+
- Router-trust guard rewritten to use AST-based cross-file resolution.
216+
A forbidden handler in module A is now caught when module B mounts A's
217+
router via `parent.include_router(child, prefix=".../extensions...")`,
218+
and the same applies transitively across multi-hop include_router
219+
chains. An imported router that cannot be statically resolved is
220+
ignored (the guard never flags routes it cannot prove reachable from
221+
`/extensions`); routes co-located with an in-scope router ARE flagged.
222+
- `check_migration_append_only.py` now compares
223+
`ambiguous_bare_names` alongside `entries`. A marker may not be
224+
removed once published, and its `candidates` list may only grow --
225+
shrinking it would silently regress flows from
226+
`component-name-ambiguous` to `component-not-found-with-hint`.

scripts/migrate/check_migration_append_only.py

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
allowed (the runtime does not care about order); changing the ``target``,
88
``legacy_*`` field, or the value any entry maps from is **not**.
99
10+
The same invariant applies to the ``ambiguous_bare_names`` list: a marker
11+
may not be removed once published, and its ``candidates`` list may only
12+
grow -- shrinking it would regress a saved flow that previously surfaced
13+
``component-name-ambiguous`` to ``component-not-found-with-hint``.
14+
1015
Usage::
1116
1217
python scripts/migrate/check_migration_append_only.py
@@ -75,7 +80,12 @@ def _git_show(ref: str, relpath: str) -> str | None:
7580
return completed.stdout
7681

7782

78-
def _parse(raw: str, *, source: str) -> list[dict]:
83+
def _parse(raw: str, *, source: str) -> tuple[list[dict], list[dict]]:
84+
"""Return ``(entries, ambiguous_bare_names)`` from a migration-table JSON.
85+
86+
Both lists default to empty when the field is absent so this script can
87+
compare across baselines that pre-date a given field.
88+
"""
7989
try:
8090
data = json.loads(raw)
8191
except json.JSONDecodeError as exc:
@@ -88,7 +98,11 @@ def _parse(raw: str, *, source: str) -> list[dict]:
8898
if not isinstance(entries, list):
8999
print(f"error: {source} entries field must be a list", file=sys.stderr)
90100
raise SystemExit(2)
91-
return entries
101+
ambig = data.get("ambiguous_bare_names", [])
102+
if not isinstance(ambig, list):
103+
print(f"error: {source} ambiguous_bare_names field must be a list", file=sys.stderr)
104+
raise SystemExit(2)
105+
return entries, ambig
92106

93107

94108
def _compare(baseline: list[dict], current: list[dict]) -> list[str]:
@@ -130,6 +144,60 @@ def _compare(baseline: list[dict], current: list[dict]) -> list[str]:
130144
return violations
131145

132146

147+
def _ambig_name(entry: dict) -> str | None:
148+
"""Return the bare-name key of an ambiguity marker, or ``None`` if malformed."""
149+
name = entry.get("name")
150+
return name if isinstance(name, str) else None
151+
152+
153+
def _ambig_candidates(entry: dict) -> set[str]:
154+
raw = entry.get("candidates", [])
155+
if not isinstance(raw, list):
156+
return set()
157+
return {c for c in raw if isinstance(c, str)}
158+
159+
160+
def _compare_ambiguous(baseline: list[dict], current: list[dict]) -> list[str]:
161+
"""Return human-readable violations for ambiguous_bare_names changes.
162+
163+
Append-only contract:
164+
* No marker may be removed once published.
165+
* The candidate set may only grow; removing a candidate would
166+
regress a saved flow that previously surfaced
167+
``component-name-ambiguous`` (with that target as one of the
168+
fix-hint options) to ``component-not-found-with-hint``.
169+
"""
170+
violations: list[str] = []
171+
current_by_name: dict[str, dict] = {}
172+
for entry in current:
173+
name = _ambig_name(entry)
174+
if name is None:
175+
violations.append(f"current ambiguous_bare_names entry malformed (no name): {entry!r}")
176+
continue
177+
if name in current_by_name:
178+
violations.append(f"duplicate ambiguous_bare_names entry in current table: name={name!r}")
179+
continue
180+
current_by_name[name] = entry
181+
182+
for entry in baseline:
183+
name = _ambig_name(entry)
184+
if name is None:
185+
violations.append(f"baseline ambiguous_bare_names entry malformed (no name): {entry!r}")
186+
continue
187+
match = current_by_name.get(name)
188+
if match is None:
189+
violations.append(
190+
f"ambiguous_bare_names marker removed: name={name!r} (added in {entry.get('added_in')!r})"
191+
)
192+
continue
193+
baseline_candidates = _ambig_candidates(entry)
194+
current_candidates = _ambig_candidates(match)
195+
missing = baseline_candidates - current_candidates
196+
if missing:
197+
violations.append(f"ambiguous_bare_names candidates shrunk for name={name!r}: removed {sorted(missing)!r}")
198+
return violations
199+
200+
133201
def main(argv: list[str] | None = None) -> int:
134202
parser = argparse.ArgumentParser(description=__doc__)
135203
parser.add_argument(
@@ -170,9 +238,10 @@ def main(argv: list[str] | None = None) -> int:
170238
print(f"no baseline migration table at {args.base}:{TABLE_RELPATH}; nothing to compare.")
171239
return 0
172240

173-
baseline = _parse(baseline_raw, source=f"{args.base}:{TABLE_RELPATH}")
174-
current = _parse(current_raw, source=str(args.current))
175-
violations = _compare(baseline, current)
241+
baseline_entries, baseline_ambig = _parse(baseline_raw, source=f"{args.base}:{TABLE_RELPATH}")
242+
current_entries, current_ambig = _parse(current_raw, source=str(args.current))
243+
violations = _compare(baseline_entries, current_entries)
244+
violations.extend(_compare_ambiguous(baseline_ambig, current_ambig))
176245

177246
if violations:
178247
print(
@@ -184,7 +253,8 @@ def main(argv: list[str] | None = None) -> int:
184253
return 1
185254
print(
186255
f"ok: migration table is append-only "
187-
f"({len(current)} entries; {len(current) - len(baseline)} added since baseline)"
256+
f"(entries: {len(current_entries)}, +{len(current_entries) - len(baseline_entries)}; "
257+
f"ambiguous_bare_names: {len(current_ambig)}, +{len(current_ambig) - len(baseline_ambig)})"
188258
)
189259
return 0
190260

0 commit comments

Comments
 (0)