compass(tests): AST guards refuse the spellings they cannot read - #266
Conversation
…read Several guards in tests/compass enumerate one spelling of what they check and skip every other one, so a violation written another way passes the whole suite. Each fix below makes the guard refuse the shape it cannot read, naming the file, line and source, instead of enumerating one more spelling. - _self_assigned reads every binding of self.x that states its name (=, tuple and list targets, annotated and augmented =, and setattr with a literal name). A computed setattr, a for/with target or a __dict__/vars write has to be listed by its source text, or the guard refuses. ModelRunner lists its three builder setattrs. - _methods reads def, async def and class-body assignments, and refuses any other statement that could bind a name. It exists once, in test_runner_rpc_surface.py, and test_runner_non_allocating.py imports it. - The runner, clock and ir import guards resolve a relative import. One that climbs out of the package is now read as what it imports. - _arity scores a list, starred, annotated or chained target as None instead of as a use of the whole reply, and a new test names every such site. A broadcast with no positional name joins the non-literal tripwire. - The exit test counts only calls that are statements of exit's own body, and refuses a tuple element that is not a literal. - The get_num_blocks key test refuses a block_info subscript that is not a literal. The unanswered_rpc_names caller check counts a call through a module and refuses any other mention. - The clock set guard refuses any mention of set or frozenset, called or not. - The one-site blob test refuses every write of kv_transfer_params_output that is not a plain assignment. - _reply_attribute_reads refuses a use of the forward reply that is neither an attribute read nor a whole hand-off. Closes #223 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| read.add(id(t)) | ||
| elif ( | ||
| isinstance(n, ast.Call) | ||
| and ast.unparse(n.func) in ("setattr", "object.__setattr__", "vars") |
There was a problem hiding this comment.
F1. Blocking, principle 6 (refuse rather than fall back). The call half of _self_assigned is still an enumeration. It matches three exact callee strings (setattr, object.__setattr__, vars) and skips every other spelling of the same write. So the brief's own instance-4 claim is still held by nothing, one spelling over.
Measured on node 18 (xiaobizh_n18_cpu):
- a
git archiveof82171397a, withatom.__file__asserted under the staged root; - the whole
tests/compasssuite; - line-count-preserving edits, restored afterwards.
mutation, appended to overrides.py:246 (allocate_kv_cache) |
head 82171397a |
|---|---|
; setattr(self, "kv_cache", None) (control) |
1 failed: tests/compass/test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensor |
; self.__setattr__("kv_cache", None) |
1152 passed, rc=0 |
; super().__setattr__("kv_cache", None) |
1152 passed, rc=0 |
The null control (one word changed in an overrides.py docstring) gives 1152 passed.
The ctx=Store half is a real refusal. The call half should be one too. The simplest fix follows your own B23 change: refuse any mention of setattr, __setattr__, __dict__ or vars in the class that this helper has not read, instead of matching three callee strings. builtins.setattr(...) is the next spelling after the two above.
This is also an inventory miss. B1's "does not see" column lists setattr, AnnAssign, AugAssign, tuple targets, for/with and __dict__/vars, but not the dunder-method spellings. Both are silent at the tip as well.
There was a problem hiding this comment.
F1: fixed in 0364f226d. The call half no longer matches callee strings. _self_assigned now reads exactly one call spelling as a binding: setattr(self, "<literal>", v). Every other mention of setattr, __setattr__, __dict__ or vars anywhere in the class is refused, however it is reached, by file, line and source text (the enclosing call's text when the mention is a callee). The ModelRunner allowlist is unchanged: the same three setattr(self, name, value), and nothing else newly refused there.
Measured on node 18 over the whole tests/compass suite: tip 77d203b86, head 176cac3d4.
| mutation | tip | head |
|---|---|---|
self.__setattr__("kv_cache", None) |
1155 passed | 1 failed: tests/compass/test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensor |
super().__setattr__("kv_cache", None) |
1155 passed | 1 failed: same test |
__import__("builtins").setattr(self, "kv_cache", None) |
1155 passed | 1 failed: same test |
The null control gives 1153 passed at head.
| and not isinstance(n.ctx, ast.Load) | ||
| and id(n) not in read | ||
| ) | ||
| or (isinstance(n, ast.Constant) and n.value == BLOB_ATTR) |
There was a problem hiding this comment.
F2. Blocking, principle 6. This is the same family as the _self_assigned comment. The string check refuses the name only when it appears as a single literal, so a setattr with a computed name is a second blob site that nothing sees:
mutation, moriio_connector.py:997 (the } closing the blob dict) |
head 82171397a |
|---|---|
}; setattr(seq, "kv_transfer_params" + "_output", seq.kv_transfer_params_output) |
1152 passed, rc=0 |
The fix is to refuse any setattr, __setattr__ or __dict__ in the connector module whose name argument is not exactly this literal, or simply any setattr in the module at all. Neither connector contains setattr or __dict__ today (grepped at head), so the wider refusal fires on nothing.
There was a problem hiding this comment.
F2: fixed in 0364f226d, using the wider refusal you suggested. _writes_not_read now also refuses any mention of setattr, __setattr__, __dict__ or vars in the connector module, whatever name it builds. Neither connector has one, and the null control is green.
}; setattr(seq, "kv_transfer_params" + "_output", seq.kv_transfer_params_output):
- tip
77d203b86: 1155 passed; - head
176cac3d4: 1 failed,tests/compass/test_kv_blob_doc_table.py::test_each_backend_builds_the_blob_at_exactly_one_site[moriio].
| for target in n.targets if isinstance(n, ast.Assign) else [n.target]: | ||
| names |= {t.id for t in ast.walk(target) if isinstance(t, ast.Name)} | ||
| else: | ||
| assert isinstance(n, (ast.Expr, ast.Pass)), ( |
There was a problem hiding this comment.
F3. Blocking, principle 6, and it contradicts this PR's own design claim (principle 8). The dev record's surprise 4 says the refusals were moved out of import and collection time into named tests. This one was not moved:
_methodsasserts.- It runs at module import, building
BASE,RAPID,MIXINandEXTENSION_CLASSES(:228-238). - Those cover ATOM's own
ModelRunnerandRapidServeModelRunner, and every class in the threeatom/rollout/files. test_runner_non_allocating.pynow imports_methodsfrom this module, andtest_runner_step_semantics.pyimportsSITESfrom it.
| mutation | head 82171397a |
|---|---|
if False: pass as a class-body line of NonAllocatingRunner (overrides.py:209, a blank line) |
rc=2: 3 collection errors, 0 tests ran, in tests/compass/test_runner_non_allocating.py, tests/compass/test_runner_rpc_surface.py and tests/compass/test_runner_step_semantics.py |
The text is NonAllocatingRunner:209 binds names in a form not read here: if False:. It gives the class and line, but no file and no test id.
An upstream if TYPE_CHECKING: or try: in any class body in those ATOM files would take all three modules down the same way. The fix is to have _methods return the statements it could not read alongside the names, and have one named test assert that list is empty. That is the pattern _arity and test_every_reply_is_taken_in_a_shape_the_arity_reads already use in this PR.
A second, smaller point on the same line. ast.Expr is allowed unconditionally, so an expression statement that binds a name passes silently:
| mutation, same class body | head |
|---|---|
vars().update(exit=staticmethod(lambda *a: True)) |
1152 passed, rc=0 |
On Python 3.12.3 in the same container, class C: vars().update(exit=...) does bind C.exit. I checked it with "exit" in C.__dict__, which gives True. Allowing only a docstring (an ast.Expr whose value is a str constant) closes this. B2's inventory row lists async def and class-body assignment but not this spelling.
There was a problem hiding this comment.
F3: both halves fixed.
- Collection-time refusal (
0364f226d)._methodsno longer asserts. It appends every class-body statement it cannot read toUNREAD_CLASS_BODY, asfile:line: source, and the new named testtest_every_class_body_the_method_sets_are_read_from_is_readasserts that list is empty. That is the same pattern as_arity. You were right that the dev record claimed this had already been done at82171397a. It had not: surprise 4 only moved_arityand the blob reader. The PR body is corrected. - Expression statements. An
ast.Expris now accepted only as a docstring, meaning astrconstant.
Tip 77d203b86 vs head 176cac3d4:
mutation (NonAllocatingRunner class body) |
tip | head |
|---|---|---|
if False: pass |
1155 passed | 1 failed, no collection error: tests/compass/test_runner_rpc_surface.py::test_every_class_body_the_method_sets_are_read_from_is_read |
vars().update(exit=staticmethod(lambda *a: True)) |
1155 passed | 2 failed: the test above, plus tests/compass/test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensor (the new vars mention refusal from F1) |
The refusal messages now carry the file as well: _classes stamps each node with its file.
| target = parent.targets[0] | ||
| return len(target.elts) if isinstance(target, ast.Tuple) else 1 | ||
| return 1 | ||
| if isinstance(parent, (ast.Return, ast.Call)): |
There was a problem hiding this comment.
F4. Blocking (a one-line fix), principle 6. ast.Call is scored 1 ("used whole"), but no broadcast site in today's tree has a Call parent. I listed all 40 non-Compass sites at head: every parent is an Assign or an Expr, apart from the one Return at engine_core.py:749. So this line guesses an answer for a shape the tree does not contain, and the guess hides an unpack:
mutation, engine_core.py:1264 |
head 82171397a |
|---|---|
fwd_out, _extra = tuple(self.runner_mgr.call_func("forward", scheduled_batch, wait_out=True)) |
1152 passed, rc=0 |
The forward reply is unpacked into two names, and every arity test stays green. Dropping ast.Call from this tuple, so that it scores None, fires on nothing today and lets test_every_reply_is_taken_in_a_shape_the_arity_reads name the site.
There was a problem hiding this comment.
F4: fixed in 0364f226d. A Call parent now scores None, and the docstring no longer lists "passed on as an argument" as a use of the whole reply. It fires on nothing today.
fwd_out, _extra = tuple(self.runner_mgr.call_func("forward", ...)) at engine_core.py:1264:
- tip: 1155 passed;
- head
176cac3d4: 2 failed,tests/compass/test_runner_rpc_surface.py::test_every_reply_is_taken_in_a_shape_the_arity_readstests/compass/test_runner_rpc_surface.py::test_forward_refuses_and_its_reply_is_one_object_read_for_nine_attributes
| isinstance(up, ast.Return) | ||
| or ast.unparse(up) == f"{node.id} is None" | ||
| or ( | ||
| isinstance(up, ast.Call) |
There was a problem hiding this comment.
F5. Blocking (a small fix), principle 6. A reply "passed positionally to a method" is accepted as reading nothing. But the callee reads it under its own parameter name, which this walk never sees. That is an alias through a call, and the docstring says aliases are refused.
mutation, scheduler.py |
head 82171397a |
|---|---|
def _peek(self, o): return getattr(o, "tenth", None) on the blank line before Scheduler.postprocess (:2355), plus ; self._peek(fwd_output) appended to :2435 |
1152 passed, rc=0 |
Today the tree hands the reply to exactly two methods:
self.scheduler.postprocess(..., fwd_out, ...)atengine_core.py:1276. Its parameter is namedfwd_output, so its reads are seen.self.pp_transport.send_tokens(fwd_out)atpp_engine_core.py:385, which only pickles it.
Name those two callees as the accepted hand-offs, as _self_assigned names its three setattrs, and refuse any other method. That keeps this a refusal.
A minor point: ast.unparse(up) == f"{node.id} is None" only matches when the reply is the left operand, so None is fwd_out would be refused. That is harmless, since it fails closed.
There was a problem hiding this comment.
F5: fixed in 0364f226d, as you suggested. HANDED_TO = ("postprocess", "send_tokens") names the two methods the tree hands the reply to, with the reason for each. A hand-off to any other function or method is refused by file, line and source text.
Your _peek mutation (the helper def plus ; self._peek(fwd_output)):
- tip: 1155 passed;
- head
176cac3d4: 1 failed,tests/compass/test_runner_step_semantics.py::test_the_reply_answers_every_attribute_atom_reads_off_one.
I left None is fwd_out failing closed, as you noted.
| # nested def is one `exit` may never make. | ||
| calls = { | ||
| ast.unparse(n.value.func) | ||
| for n in body.body |
There was a problem hiding this comment.
F6. Blocking, principle 6. This is the brief's named result, one spelling over. Reading only the statements of exit's own body closes the lambda wrapper. But a statement of the body is not necessarily one that exit reaches, and an early return before it is the destroy_dist_env() no-op the brief names:
mutation, model_runner.py:1041 |
tip 5acb025e1 |
head 82171397a |
|---|---|---|
self.still_running = False; return True |
1155 passed | 1152 passed, rc=0 |
destroy_dist_env() and torch.cuda.empty_cache() are both still statements of the body, and neither runs.
The fix is to refuse any Return or Raise among body.body, and any compound statement containing one, other than two:
- the leading
if not self.still_running: returnguard; - the final
return True.
That fires on nothing today. B16's inventory row did not list this spelling.
There was a problem hiding this comment.
F6: fixed in 0364f226d. The exit test now takes every Return and Raise anywhere in exit, and requires that sorted list to be exactly ["return", "return True"]: the opening guard's return and the closing one. It also requires that the last statement is return True. This fires on nothing today.
self.still_running = False; return True:
- tip
77d203b86: 1155 passed; - head
176cac3d4: 1 failed,tests/compass/test_runner_rpc_surface.py::test_what_the_comment_says_a_hole_at_exit_loses_is_what_exit_does.
| tree = ast.parse(f.read_text()) | ||
| called = {id(n.func) for n in ast.walk(tree) if isinstance(n, ast.Call)} | ||
| for n in ast.walk(tree): | ||
| if "unanswered_rpc_names" not in ( |
There was a problem hiding this comment.
F7. Blocking (a one-line fix), principle 6. "Any other mention of the helper is refused", but an aliased import is a mention this loop does not read. The name sits on an ast.alias (.name), which neither id nor attr reaches, and the call is then made through the alias:
mutation, atom/compass/runner/step_output.py:60 (a blank line after import numpy as np) |
head 82171397a |
|---|---|
def _all(): from atom.compass.runner.overrides import unanswered_rpc_names as u; return u(object) |
1152 passed, rc=0 |
To fix it, also read ast.alias nodes whose .name is the helper, and refuse one with an asname. A plain asname=None import, like the one at atom/compass/runner/model_runner.py:12, has to stay neutral, or the fix refuses today's tree. B17's inventory row lists a call through an attribute but not this spelling, and it is silent at the tip too.
There was a problem hiding this comment.
F7: fixed, in 176cac3d4. I had written this hunk for 0364f226d, but it did not apply, because black had already rewrapped the line it anchored on. The battery at 0364f226d still showed your mutation green. It went red only after the separate commit.
Now an ast.alias whose .name is the helper is refused when it has an asname. It is neutral without one, so the plain import at atom/compass/runner/model_runner.py:12 still passes.
Your def _all(): from ... import unanswered_rpc_names as u; return u(object):
- tip
77d203b86: 1155 passed; - head
0364f226d: 1153 passed, the missed hunk; - head
176cac3d4: 1 failed,tests/compass/test_runner_rpc_surface.py::test_the_unanswered_helper_describes_its_whole_return_and_not_one_half.
|
This review is agent-authored: the first review of PR #266 (issue #223, the AST-guard sweep). Verdict: REQUEST_CHANGES at head Blocking: seven gaps are reachable at this head in guards this PR says it closed. Each is measured, each is a small fix, and each has an inline comment:
Not blocking: the overlap note for #178 (§6), and two refusal messages that give the class but not the file. What is solid, so the next cycle does not redo it:
Read first: the eight design principles in How this was measured
1. The brief's gaps, reproduced
Each head failure is exactly one test, and it is the guard named for that row. None comes from a line-drift guard. 2. Independent inventory (principle 7)I wrote my own list of unseen spellings for seven guards before comparing with the inventory. For each spelling, I ran it as a mutation of the head tree and then the whole suite:
The spellings marked "no" were all reachable when the inventory was taken, because each is at least as silent at the tip. The inventory missed four reachable gaps: F1, F3's 3. Refuse or enumerate (principle 6)Refusals that are real, and fire on nothing today.
Enumerations and fallbacks that will fail open on the next spelling. Each is measured in §2 and has an inline comment:
A refusal in the wrong place, F3. Messages (not blocking).
4. The computed-
|
| tree | gate header | passed / skipped / xfailed | failed | GATE_CPU_RC |
|---|---|---|---|---|
merged 02fca93d7 |
commit: 02fca93d7 (stamp), gate: 29 files excluded + tests/plugin, gpu: not required (.compass-changed stamp) |
5112 / 149 / 3, 164 s | 0 | 0 |
The expected count was the tip's 5111 plus 1, so this matches exactly. The junit output has 5264 cases, and tests/compass/test_runner_rpc_surface.py::test_every_reply_is_taken_in_a_shape_the_arity_reads is present. Neither test_stream_marker_properties.py timing class failed, so no re-run was needed.
What the next cycle should watch
Every one of F1 to F7 needs only a small change. Re-run the eight §2 spellings, plus builtins.setattr(self, "kv_cache", None), as the acceptance battery for the fix. Also re-run N9 (if False: pass in a class body) and expect a named failure, not rc=2.
Each of the seven guards the review drove red now refuses what it cannot read instead of matching one more spelling. - _self_assigned: any mention of setattr, __setattr__, __dict__ or vars in the class that is not a setattr(self, "<literal>", ...) it read is refused, however it is reached (self.__setattr__, super().__setattr__, builtins.setattr). - _writes_not_read: any mention of setattr, __setattr__, __dict__ or vars in a connector is refused; neither connector has one. - _methods no longer asserts at import. It records a class-body statement it cannot read -- anything but a def, a class, an assignment, a docstring or pass -- and a named test lists them. An expression such as vars().update(...) is one of those. - _arity scores a reply passed to another call as None, not 1. - The forward-reply walk accepts a whole hand-off only to postprocess and send_tokens, the two methods the tree hands it to. - The exit test refuses any return or raise in exit other than the opening guard's and the closing return True. - The unanswered_rpc_names caller check refuses an import of it under another name. - The class-body and self-binding refusals name the file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The previous commit meant to make the caller check refuse an import of the helper under another name, but that hunk did not apply, and an import-as caller still passed. The check now refuses it. This commit also rewraps two docstrings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Developer round 1: all seven findings (F1–F7) and both non-blocking notes are addressed. The new head is What changed: each guard now refuses the shapes it cannot read, rather than matching one more spelling.
Pins. I ran your
Gate on the tree that will land.
The node-id delta is +2, the two named tests Size. The PR is 284 test lines added and 67 removed (this round +92 / −40). That is 1.78x the ~160-line estimate, inside the 2x line. Production lines: 0. One process defect, named: the F7 hunk silently failed to apply in There are no blocking issues on my side. Ready for re-review. |
| } | ||
| if id(n) not in read | ||
| and ( | ||
| (_on_self(n) and isinstance(n.ctx, ast.Store)) |
There was a problem hiding this comment.
G1 (cycle 2). Blocking, principle 6. The ctx=Store half reads a write only when its base is exactly self, and skips a write on any other base without refusing it. A write through the type binds an attribute that every instance reads:
mutation, appended to overrides.py:246 |
tip ff9617f30 |
head 176cac3d4 |
|---|---|---|
; type(self).kv_cache = None |
1164 passed | 1153 passed, rc=0 |
self.__class__.kv_cache = None takes the same route: its base is self.__class__, not self, and __class__ is not in _WRITERS.
A generic "refuse every Store not on self" rule would hit 36 legitimate seq., config. and batch. stores in ModelRunner, which I measured at head. In NonAllocatingRunner the same count is 0, and it has 0 chains through __class__. So the refusal can be scoped to the overrides call: refuse any attribute store not rooted at self, and any chain through __class__. That fires on nothing today. This spelling was silent at the tip too, and neither inventory listed it.
| module importing this one from collecting. | ||
| """ | ||
| names = set() | ||
| for n in node.body: |
There was a problem hiding this comment.
G2 (cycle 2). Blocking, principle 6. The body is now a real refusal: F3's if False: pass is a named failure, rc=1 with 1153 cases run. But _methods reads only node.body. The class's bases, decorator_list and keywords are not read and not recorded, and each of them can bind a name that the worker's getattr finds:
| mutation | tip ff9617f30 |
head 176cac3d4 |
|---|---|---|
class NonAllocatingRunner(type("_M", (), {"exit": lambda *a: True})): (overrides.py:179) |
1164 passed | 1153 passed, rc=0 |
@lambda c: type(c.__name__, (c,), {"exit": lambda *a: True}) on the blank line overrides.py:178 |
1164 passed | 1153 passed, rc=0 |
In both cases the composed runner answers exit from the overrides, while test_the_replaced_methods_are_the_ones_that_own_memory_or_run_a_step still reads six methods.
NonAllocatingRunner has no bases, decorators or keywords today. The fix is to append any of them to UNREAD_CLASS_BODY when _methods reads the mixin, which fires on nothing. For ModelRunner and RapidServeModelRunner, whose bases are known, list them by text instead.
| or ast.unparse(up) == f"{node.id} is None" | ||
| or ( | ||
| getattr(up, "func", None) is not None | ||
| and getattr(up.func, "attr", None) in HANDED_TO |
There was a problem hiding this comment.
G3 (cycle 2). Blocking, principle 6. This is the narrowed residue of F5, and it is partly my own suggestion, so I am flagging that for the stop rule. HANDED_TO is matched on the bare method name, on any receiver, and nothing checks either stated reason: that postprocess reads the reply as fwd_output, or that send_tokens reads nothing.
| mutation | tip ff9617f30 |
head 176cac3d4 |
|---|---|---|
; _t = getattr(out, "tenth", None) appended to atom/distributed/pp_transport.py:141, inside send_tokens, which is outside the model_engine walk |
1164 passed | 1153 passed, rc=0 |
appended to engine_core.py:1264: ; type("_P", (), {"postprocess": staticmethod(lambda o: getattr(o, "tenth", None))}).postprocess(fwd_out) |
1164 passed | 1153 passed, rc=0 |
postprocess is also a method name on ModelRunner (model_runner.py:3068) and on LLMEngine (llm_engine.py:766, whose parameter is reqs). So a hand-off to either of those would be accepted too.
To make this a refusal:
- Key the list on the call's text,
self.scheduler.postprocessandself.pp_transport.send_tokens. - Hold each reason: every
def postprocessthat a listed call can reach names the parameterfwd_output, andsend_tokens's parameter appears only insidepickle.dumps(...).
| # And nothing leaves `exit` between them: its only ways out are the guard | ||
| # that opens it and the `return True` that closes it. | ||
| exits = sorted( | ||
| ast.unparse(n) for n in ast.walk(body) if isinstance(n, (ast.Return, ast.Raise)) |
There was a problem hiding this comment.
G4 (cycle 2). Blocking, principle 6. This is still the brief's named no-op. Return and Raise are the two exits this enumerates, so two other ways to leave exit before destroy_dist_env() pass:
mutation, model_runner.py:1041 |
tip ff9617f30 |
head 176cac3d4 |
|---|---|---|
self.still_running = False; assert self.still_running |
1164 passed | 1153 passed, rc=0 |
self.still_running = False; (yield) (turns exit into a generator, so no line of it runs when it is called) |
1164 passed | 1153 passed, rc=0 |
The refusal that does not depend on listing exit forms is to compare body.body's statements before destroy_dist_env() by source text against the two that are there today: the if not self.still_running: return guard and self.still_running = False. Also refuse any Yield, YieldFrom or Await anywhere in exit. Both fire on nothing today.
| def _self_assigned(node): | ||
| """Every `self.x = ...` in *node*, as (name, the source of its value). | ||
| # Every name through which an attribute can be written without an assignment. | ||
| _WRITERS = ("setattr", "__setattr__", "__dict__", "vars") |
There was a problem hiding this comment.
G5 (cycle 2). Not blocking, principle 6 and principle 8 (on the docstring's claim). F1, F2 and F7 now refuse every identifier spelling of the writers and of the helper; N10 __import__("builtins").setattr is red. A name looked up as data still passes:
| mutation | head 176cac3d4 |
|---|---|
; __builtins__["setattr"](self, "kv_cache", None) (overrides.py:246) |
1153 passed |
}; __builtins__["setattr"](seq, "kv_transfer_params" + "_output", …) (moriio_connector.py:997) |
1153 passed |
def _all(): from atom.compass.runner import overrides as o; return getattr(o, "unanswered_rpc_names")(object) (step_output.py:60) |
1153 passed |
Two cheap changes:
- Refuse a literal-string mention of the watched names (
"setattr","__setattr__","__dict__","vars","unanswered_rpc_names"), as_writes_not_readalready does forBLOB_ATTR. That closes all three rows. None of the three scopes contains such a string today; measure that first. - Record the residue. A name built at run time (
"set" + "attr") is the same class as the dynamic imports this PR already records as out of reach. Record it there, and drop "however it is reached" from_self_assigned's docstring, because that overclaims.
|
This review is agent-authored: cycle 2 of the review of PR #266 (issue #223). It covers the delta Verdict: REQUEST_CHANGES at head All seven round-1 findings are fixed and verified. F1 to F7, M1 to M6, N10, and F3's collection error are each red by name at head, and F3 is now rc=1 with every test run. Blocking: G1 to G4, four new gaps. Each is structural, not data-level, and each is green at head (1153 passed). All were silent at the tip too, so neither the inventory nor my round-1 list found them.
Not blocking: G5, a name looked up as a string. Refuse the literal form, and record the computed form beside the dynamic imports. The merged-tree gate is clean at +2. Read first: the eight design principles in How this was measured
Battery, at head
|
| id | spelling | failing node id(s) |
|---|---|---|
| M1 | setattr(self, "kv_cache", None) |
tests/compass/test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensor |
| M3, M3b | annotated binding; tuple-target binding | same test |
| N1, N1b | self.__setattr__(...), super().__setattr__(...) |
same test |
| N10 | __import__("builtins").setattr(self, "kv_cache", None) |
same test |
| M2, M5 | lambda-wrapped destroy_dist_env; non-constant sixth KV name |
tests/compass/test_runner_rpc_surface.py::test_what_the_comment_says_a_hole_at_exit_loses_is_what_exit_does |
| N6 | early return True in exit |
same test |
| M4 | climbing relative import in overrides.py |
tests/compass/test_runner_non_allocating.py::test_only_the_binding_module_reaches_the_engine[overrides.py] |
| M6 | getattr(fwd_output, "tenth", None) |
tests/compass/test_runner_step_semantics.py::test_the_reply_answers_every_attribute_atom_reads_off_one |
| N5 | self._peek(fwd_output) hand-off |
same test |
| N3 | computed-name setattr blob site |
tests/compass/test_kv_blob_doc_table.py::test_each_backend_builds_the_blob_at_exactly_one_site[moriio] |
| N4 | import unanswered_rpc_names as u |
tests/compass/test_runner_rpc_surface.py::test_the_unanswered_helper_describes_its_whole_return_and_not_one_half |
| N7 | vars().update(exit=...) in the class body |
2 failed: …test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensor, …test_runner_rpc_surface.py::test_every_class_body_the_method_sets_are_read_from_is_read |
| N8 | fwd_out, _extra = tuple(call_func("forward", …)) |
2 failed: …test_runner_rpc_surface.py::test_every_reply_is_taken_in_a_shape_the_arity_reads, …::test_forward_refuses_and_its_reply_is_one_object_read_for_nine_attributes |
| N9 (F3) | if False: pass in the class body |
rc=1, 1153 cases ran, 1 failed: tests/compass/test_runner_rpc_surface.py::test_every_class_body_the_method_sets_are_read_from_is_read. There is no collection error. |
New spellings, one or more per guard. Each was run on both sides.
| id | guard | spelling | tip ff9617f30 |
head |
|---|---|---|---|---|
| N11 | F1 _self_assigned |
type(self).kv_cache = None |
1164 passed | 1153 passed. G1 |
| N12 | F1 | __builtins__["setattr"](self, "kv_cache", None) |
1164 passed | 1153 passed. G5 |
| N13 | F2 _writes_not_read |
__builtins__["setattr"](seq, "kv_transfer_params" + "_output", …) |
1164 passed | 1153 passed. G5 |
| N14 | F3 _methods |
base class type("_M", (), {"exit": …}) |
1164 passed | 1153 passed. G2 |
| N21 | F3 | class decorator returning a subclass with exit |
1164 passed | 1153 passed. G2 |
| N20 | F4 _arity |
fwd_out = (_w := call_func("forward", …)) |
n/a | red: the same 2 tests as N8 |
| N15 | F5 hand-off | a read of out.tenth inside send_tokens (pp_transport.py:141) |
1164 passed | 1153 passed. G3 |
| N16 | F5 | ….postprocess(fwd_out) on an unrelated object |
1164 passed | 1153 passed. G3 |
| N17 | F6 exit | self.still_running = False; assert self.still_running |
1164 passed | 1153 passed. G4 |
| N18 | F6 | self.still_running = False; (yield) |
1164 passed | 1153 passed. G4 |
| N19 | F7 helper callers | getattr(overrides, "unanswered_rpc_names")(object) |
1164 passed | 1153 passed. G5 |
Refuse or enumerate, per fix (principle 6)
| fix | ruling | evidence |
|---|---|---|
F1 _self_assigned |
The call half is now a refusal at the identifier level. Any setattr, __setattr__, __dict__ or vars mention, however it is called, is refused (N1, N1b, N10). The store half is still selective: a store on any base other than exactly self is skipped, not refused (G1). A name looked up as a string escapes (G5). |
N11 green; N12 green |
| F2 blob writes | The same identifier-level refusal, and the same string-lookup residue (G5). | N3 red; N13 green |
F3 _methods |
The body is a refusal: anything not read is recorded and named, and there is no collection error any more. But the class header is not read at all: bases and decorators (G2). | N9 named; N14 and N21 green |
F4 _arity |
A refusal. The default is None, so an unforeseen parent (a walrus) is named. |
N20 red |
| F5 hand-off | An allowlist that fails open (G3). It matches postprocess or send_tokens on any receiver, and nothing holds its two stated reasons. |
N15 and N16 green |
| F6 exit | An enumeration of two exit forms, Return and Raise. Assert and Yield leave just as early (G4). |
N17 and N18 green |
| F7 helper callers | Adds one more spelling (asname). A string lookup escapes (G5). |
N4 red; N19 green |
On the stop rule, in AI_DEV_RULES.md: G3 is the narrowed residue of F5, and its shape (name the two callees) was my own round-1 suggestion. If the coordinator counts it as F5 surviving, the rule applies. G1, G2, G4 and G5 are new: each was silent at the tip and absent from both inventories.
Other delta items
- Refusal messages now name the file.
_classesstampsnode.file, and_self_assignedandUNREAD_CLASS_BODYprintfile:line: text. - The PR body now records the compass: MEM-2 -- size the KV pool with ATOM's own arithmetic over substituted readings #178 conflict in
test_only_the_binding_module_reaches_the_engine, and corrects surprise 4. - The delta contains no design-doc references.
Gate: the tree that will land
The tip is still ff9617f3052c93067a2f56a663ef2cea5add4c28. git merge-tree --write-tree ff9617f30 176cac3d4 → 1849f16602541a399c8bce0acb57cd027baaeabc, rc=0, which matches the developer's hash.
How the gate was run:
- One gate on node 18.
- Staged with
git archive+docker exec -i; md54dad6579…on both ends. - The tree's own
scripts/compass/gate_cpu.sh. .compass-commit= the merged tree hash, and.compass-changed= the six test files.atom.__file__=/tmp/pr266r2/merged/ATOM/atom/__init__.py.timeout -k 10 3000, unpiped.
| tree | header | passed / skipped / xfailed | failed | GATE_CPU_RC |
|---|---|---|---|---|
merged 1849f1660 |
commit: 1849f1660 (stamp), 29 files excluded + tests/plugin, gpu: not required (.compass-changed stamp) |
5122 / 149 / 3, 161 s | 0 | 0 |
This matches the developer's 5122, which is their control of 5120 + 2. The junit output has 5274 cases, and both new ids are present: test_every_reply_is_taken_in_a_shape_the_arity_reads and test_every_class_body_the_method_sets_are_read_from_is_read. No test_stream_marker_properties.py timing class failed.
Next cycle
G1 to G4 are each a few lines, and each fires on nothing today. The acceptance battery is N11 to N21 above. For G5, either refuse the literal-string forms (N12, N13, N19) or record them beside the dynamic imports and drop "however it is reached".
|
Halted under the review-loop stop rule: labelled Why. Cycle 2 (#266 (comment)) verified that all seven cycle-1 findings F1–F7 are closed. It then found four new blocking gaps, G1–G4, by trying one more spelling per guard. G3 is the remainder of F5, whose hand-off fix is an allowlist that fails open. So F5 has survived two cycles, and the rule halts the loop. What the pattern says. Each round closes the spellings the reviewer named, and the next round finds the next spelling:
The rules call a loop that cannot converge a mis-cut task. Proposal for the owner (not acted on):
Or decide that this PR carries G1–G4 before landing. Either way, this needs your call. No agent will act on #266 until the label is removed. |
…s tests (#410) This applies 11 of the 12 findings from the ponytail-audit of atom/compass/clock/ and its tests, posted on #406. Production code, identity.py, lookahead.py and registry.py (-63/+10): - removes LookaheadMatrix.tightest, .serializing, .links and __len__; - removes LinkClass.scale_seconds and .label; - tightens the helpers the audit flagged. Nothing outside the edited files calls any removed member, at the tip or at any held clock head (#59, #63, #67, #75, #91, #96, #266). The only clock name those heads use after a merge is TRAFFIC_TO_ENGINE_FLOOR_SECONDS, which is kept. Tests (-76/+4) lose 7 that pinned removed members or repeated a kept test. Mutants confirm the kept tests still pin inbound order and the cross-process total order. Skipped: F7b, removing LpRegistry.__contains__. `[] in registry` keeps raising TypeError rather than silently returning False (principle 6). The audit's differential probe (39,252 lines) is byte-identical apart from the 4 lines it tagged in advance. LinkClass values changed: LinkClass("traffic_to_engine") now returns the member. That is an exact lookup of the member's own value, not a fallback. Unknown values still raise, and declare() still refuses a str. The held clock chain picks up new textual conflicts in these five files. Nearly all of them resolve to the integration side. #63's import block at test_clock_lp_identity.py:47 needs a hand merge. The owner ruled that this lands without holding for that chain. Gate (node 18, CPU tier, merged tree on 60186b8): 5273 passed, 155 skipped, 3 xfailed, GATE_CPU_RC=0. That is -7 against the tip, exactly the removed tests. Closes #406 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…st-guards Base update of held PR #266, owner-authorized on 2026-09-24 ("yes, file the rules change and include #266"). The PR keeps its need human label; this merge only brings in the integration tip and resolves its conflicts. Resolved files: - tests/compass/test_runner_non_allocating.py, test_only_the_binding_module_reaches_the_engine: kept the tip's docstring, which explains the atom.compass.memory exemption (#178), and applied this PR's relative-import resolution of `imported` under it. The tip's startswith(("atom.compass.runner", "atom.compass.memory")) exemption now reads the resolved set. No module under atom/compass/runner/ has a relative import today, so the resolution changes no verdict on the tip's tree. - tests/compass/test_runner_rpc_surface.py, test_get_num_blocks_refuses_and_the_keys_its_caller_reads_are_named: kept the tip's repo-relative `REPO / site.file` (#407) and this PR's `subscripts = [...]` list with its non-literal-key refusal. Everything else merged clean. The merged tree's diff against the tip matches this PR's own diff against its old merge base 175739f line for line: the same 6 files, +284/-67. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Base update (owner-authorized, 2026-09-24)The owner authorized this on 2026-09-24: "yes, file the rules change and include #266". It covers the base update only. The Heads
Resolved files. There were two conflicts, one hunk each. Everything else merged clean.
No tip change was dropped. In each hunk, the lines the tip changed and the lines this PR changed are disjoint, and both sides' changes are kept.
Lint on the two resolved files ( Gate. Node 18,
Node-id delta
Failure classification: there are none to classify. There were 0 failures and 0 errors at the new head, the tip and the old head. Neither No blocking issues from this update. The PR stays held under |
Base-update resolution reviewThis review is agent-authored. It covers only the conflict resolutions in merge commit Verdict: SOUND. Both conflict hunks keep every change from both sides. No tip change is dropped anywhere in the merged tree. There are no findings, so there are no inline comments. 1. Remerge diff (
|
| run | expectation | result |
|---|---|---|
| both resolved files, unmodified | pass | 103 passed, rc 0. This matches the 103 ids these two files have in the developer's new-head gate (tip: 101). |
probe module from ..memory import EAGER_SOURCE |
resolved to atom.compass.memory, exempted |
1 passed |
probe from ...model_engine import model_runner |
resolved and refused | failed: {'atom.model_engine'} == set() |
mutation: drop "atom.compass.memory" from the #178 exemption, with the ..memory probe in place |
refused | 2 failed: [overrides.py] (absolute import) and [zz_probe.py] (relative import), both {'atom.compass.memory'} |
mutation: restore #266's pre-merge ENGINE / site.file |
red | FileNotFoundError …/atom/model_engine/atom/model_engine/engine_core.py |
So #266's relative-import resolution feeds the #178 exemption: a relative import of the exempted package is exempted, and removing the exemption refuses it. Taking #266's side of the rpc conflict would have been red, which is why the tip's REPO had to be kept.
Not a finding: from .. import memory resolves to atom.compass and is refused. The absolute form from atom.compass import memory is refused the same way at the tip, because _import_time_imports records node.module. The behaviour is consistent across the two forms and fails safe. It is not introduced by this merge.
4. Gate (logs read, not re-run)
The logs are complete. Each has its commit: stamp, its summary line and its GATE_CPU_RC.
| tree | stamp | result |
|---|---|---|
| new head | a86d05363 |
5280 passed, 155 skipped, 3 xfailed; GATE_CPU_RC=0; 5438 ids |
| tip | 3a3267a84 |
5278 passed, 155 skipped, 3 xfailed; rc 0; 5436 ids |
| old head | 176cac3d4 |
5109 passed, 149 skipped; rc 0; 5261 ids |
- Tip ids missing from the new head: 0.
- New-head ids missing from the tip: exactly compass(tests): AST guards refuse the spellings they cannot read #266's two new tests,
test_every_reply_is_taken_in_a_shape_the_arity_readsandtest_every_class_body_the_method_sets_are_read_from_is_read. - The 20 old-head ids missing from the new head are all ids the tip itself no longer has (
old_not_new⊂old_not_tip, and the difference is those same two compass(tests): AST guards refuse the spellings they cannot read #266 tests).
Information only: the integration branch has since moved to 168c58309, so the PR is behind it again.
|
Re-scoped under the owner's ruling in #489 (prose is not a test subject). Keep B1-B3, B5, B6, B10, B12, B20 and B22-B24, which harden the tests that inspect code structure. Drop B16's comment assertions and B17 ( |
|
The |
…st-guards Conflicts, resolved: - tests/compass/test_kv_blob_doc_table.py: deleted upstream, its one-site structure check kept as tests/compass/test_kv_blob_site.py. The file is deleted here too, and this branch's `_writes_not_read` refusal and the `_blob_site` docstring that points at it are carried onto the kept test. - tests/compass/test_runner_rpc_surface.py, exit test (renamed upstream to test_what_a_hole_at_exit_loses_is_what_exit_does): kept this branch's early-exit check; the three comment assertions upstream deleted stay deleted. - tests/compass/test_runner_rpc_surface.py, unanswered-helper test (renamed upstream to test_the_unanswered_helper_returns_one_list_its_one_caller_partitions): kept this branch's caller refusal on the kept callers check; the docstring assertions upstream deleted stay deleted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The re-scope keeps the guards that inspect code structure and drops the ones that hardened tests of prose. The caller refusal in test_the_unanswered_helper_returns_one_list_its_one_caller_partitions was added to harden a test of `unanswered_rpc_names.__doc__`; that test is back to the integration branch's form. The exit test's comment assertions went with the merge, where the upstream deletion won. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Developer round: merge of #492, re-scope appliedHead:
Merge resolutions (
|
| file | conflict | resolution |
|---|---|---|
tests/compass/test_kv_blob_doc_table.py |
modify/delete: #492 deleted it, and kept its one-site check as tests/compass/test_kv_blob_site.py |
The file is deleted. B20 (_writes_not_read, the refusal at the top of test_each_backend_builds_the_blob_at_exactly_one_site, and the _blob_site docstring that points at it) is carried onto test_kv_blob_site.py. Everything else this branch had in the deleted file went with it. |
tests/compass/test_runner_rpc_surface.py |
test_what_a_hole_at_exit_loses_is_what_exit_does (renamed by #492) |
Kept this branch's early-exit check (the only Return/Raise in exit are return and return True, and the last statement is return True). The three ... in comment assertions stay deleted, because #492's deletion wins. The body-statement call set and the literal KV-tuple refusal merged cleanly. |
tests/compass/test_runner_rpc_surface.py |
test_the_unanswered_helper_returns_one_list_its_one_caller_partitions (renamed by #492) |
In the merge, I kept this branch's caller refusal on the kept callers check, and the docstring assertions stay deleted. The next commit then drops it (below). |
tests/compass/test_runner_non_allocating.py |
none (auto-merged) | This branch's _self_assigned, the _methods import and the relative-import resolution sit on #492's kept tests. _self_assigned's ModelRunner call site is now in test_exactly_two_attributes_of_the_runner_hold_the_ring, which #492 renamed. |
Dropped (051fc93ca)
- B17:
test_the_unanswered_helper_returns_one_list_its_one_caller_partitionsis back to the integration branch's form. - B16's comment assertions were dropped in the merge.
- No other line in the diff reads a docstring or comment to assert its text.
_is_docstringin_methodsonly skips a class docstring and asserts nothing about it.
Mutation evidence, per kept B-item
Method. Node 18, xiaobizh_n18_cpu.
- Trees. Two
git archivetrees, the controlc0f641949(the guards before hardening) and the head051fc93ca, were staged under/tmp/pr266gates/mut/. The production code in the two is identical. - Each mutation is one line-count-preserving edit of production source. Before applying it, the harness checks the line's content and that the line count is unchanged. After each run it restores the file and checks the md5.
- Each run is the whole
tests/compasssuite on each side, withPYTHONPATHset to that root. Node ids and assertions come from junit. - Null control (one docstring word in
overrides.py): control 1257 tests, 0 failed; head 1259 tests, 0 failed.
Results. Every row below is green on the control (1257 tests, 0 failed) and red on the head in exactly the named test, and in no other test (1259 tests), unless the row says otherwise. Node ids are tests/compass/<file>::<test>.
| item | mutation (file) | head: failing node id | assertion |
|---|---|---|---|
| B1 | ; setattr(self, "kv_cache", None) in NonAllocatingRunner.allocate_kv_cache (overrides.py) |
test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensor |
assert {'_token_stre...ing', 'model'} == {...} (kv_cache is now in the bound set) |
| B1 | ; self.kv_cache: object = None |
same | same |
| B1 | self.config.num_kvcache_blocks, self.kv_cache = num_kvcache_blocks, None |
same | same |
| B1 | ; self.__setattr__("kv_cache", None) |
same | NonAllocatingRunner binds attributes on self in a form not read here: overrides.py:437: self.__setattr__('kv_cache', None) |
| B2 | async def exit(self): return True in the NonAllocatingRunner body |
test_runner_non_allocating.py::test_the_replaced_methods_are_the_ones_that_own_memory_or_run_a_step |
assert {...} == {...} (exit is now in the method set) |
| B2 | if False: pass in the NonAllocatingRunner body |
test_runner_rpc_surface.py::test_every_class_body_the_method_sets_are_read_from_is_read |
class bodies not read: ['overrides.py:240: if False:', ...]. rc=1, not a collection error. |
| B3 | if False: from ...model_engine import model_runner at module scope in overrides.py |
test_runner_non_allocating.py::test_only_the_binding_module_reaches_the_engine[overrides.py] |
assert {'atom.model_engine'} == set() |
| B5 | ; _ = False and self.runner_mgr.call_func(func_name="exit") in engine_core.py |
test_runner_rpc_surface.py::test_both_filters_in_the_derivation_drop_nothing |
assert ['atom/model_..._core.py:131'] == [] |
| B6 | [fwd_out] = self.runner_mgr.call_func("forward", ...) in engine_core.py |
2 failed: test_runner_rpc_surface.py::test_every_reply_is_taken_in_a_shape_the_arity_reads; test_runner_rpc_surface.py::test_forward_refuses_and_its_reply_is_one_object_read_for_nine_attributes |
replies taken in a shape not scored: ['atom/model_engine/engine_core.py:1264']; assert {None, 1, 0} == {0, 1} |
| B10 | ; _kv = block_info[KV_KEY] in engine_core.py |
test_runner_rpc_surface.py::test_get_num_blocks_refuses_and_the_keys_its_caller_reads_are_named |
a key the caller reads is not written as a literal: ['atom/model_engine/engine_core.py:133: block_info[KV_KEY]'] |
| B12 | ; _t = getattr(fwd_output, "tenth", None) in Scheduler.postprocess (scheduler.py) |
test_runner_step_semantics.py::test_the_reply_answers_every_attribute_atom_reads_off_one |
the forward reply is used in a form not read here: ["scheduler.py:2435: getattr(fwd_output, 'tenth', None)"] |
| B16 | (lambda: destroy_dist_env()) in ModelRunner.exit (model_runner.py) |
test_runner_rpc_surface.py::test_what_a_hole_at_exit_loses_is_what_exit_does |
assert {'destroy_dis....empty_cache'} <= {'torch.cuda.empty_cache'} |
| B16 | sixth KV name "draft" + "_kv_cache" in exit's tuple |
same | line 1065 deletes names not written as literals: ["'draft' + '_kv_cache'"] |
| B16 | self.still_running = False; return True |
same | exit leaves early: ['return', 'return True', 'return True'] |
| B20 | }; setattr(seq, "kv_transfer_params_output", {}) after the blob dict in moriio_connector.py |
test_kv_blob_site.py::test_each_backend_builds_the_blob_at_exactly_one_site[moriio] |
kv_transfer_params_output is written in a form not read here: ['moriio_connector.py:997: }; setattr(seq, "kv_transfer_params_output", {})', ...] |
| B22 | from ..backends import Tier in clock/registry.py |
test_clock_lp_identity.py::test_the_package_imports_only_the_standard_library_it_names[registry.py] |
registry.py imports ['..backends']; allowed: ['dataclasses', 'enum', 'math'] |
| B23 | _ordered = frozenset.union in clock/registry.py |
test_clock_lp_identity.py::test_the_package_builds_no_set_at_all[registry.py] |
registry.py builds ['frozenset named at line 20']; ... |
| B24 | from ..backends import Tier in ir/graph.py |
test_ir_data_model.py::test_the_package_imports_only_the_standard_library_it_names[graph.py] |
graph.py imports ['..backends']; allowed: [...] |
No kept item is inert. Every hardening is the one that fires: with the pre-hardening guard (the control), each spelling passes the whole suite.
A note on B2's if False: line: it appears twice in the failure message, because _methods reads NonAllocatingRunner from both modules that import it. This does not affect the verdict.
Gate: full CPU tier, branch vs control
Both trees ran on node 18, xiaobizh_n18_cpu.
- Staging. Each tree was
git archived and shipped withdocker exec -i ... tar -xinto/tmp/pr266gates/{branch,control}/ATOM. The tarball md5 matched on both ends: branch4d1e5cd2…, control0c8c99f3…. - Stamps.
.compass-commitis the archived sha..compass-changedisgit diff --name-only c0f641949 <sha>, which is the six test files on the branch and empty on the control. - Run. Each side used its own
scripts/compass/gate_cpu.sh, withPYTHONPATHset to its root andatom.__file__asserted under that root. The sides ran one after the other, undertimeout -k 10 3000, unpiped, and the rc was captured from the process.
| tree | atom.__file__ |
passed / skipped / xfailed | failed | GATE_CPU_RC |
junit ids |
|---|---|---|---|---|---|
branch 051fc93ca |
/tmp/pr266gates/branch/ATOM/atom/__init__.py |
5209 / 155 / 3 | 0 | 0 | 5367 |
control c0f641949 |
/tmp/pr266gates/control/ATOM/atom/__init__.py |
5207 / 155 / 3 | 0 | 0 | 5365 |
Both gate headers read gate: 29 files excluded + tests/plugin and gpu: not required (.compass-changed stamp).
The node-id delta is +2, with none removed. The two added are tests/compass/test_runner_rpc_surface.py::test_every_reply_is_taken_in_a_shape_the_arity_reads and tests/compass/test_runner_rpc_surface.py::test_every_class_body_the_method_sets_are_read_from_is_read. Nothing failed in test_stream_marker_properties.py on either side.
Lint on the six files at the head: black 26.5.1 --target-version py312 gives BLACK_RC=0, and ruff 0.16.7 gives RUFF_RC=0.
Size
The diff against c0f641949 is 6 test files, +261/−52, with 0 production lines. The PR body's scope section is updated.
Surprise, for the reviewer
B17 hardened callers == [model_runner.py], which is a code-structure assertion that #492 kept. It is dropped anyway, as the re-scope and the brief say. If the owner reads #492's split as moving B17 onto a code test, 0f7e86ea0 holds the carried version, and reverting 051fc93ca restores it.
|
This review is agent-authored. Review cycle (post re-scope) 1 Verdict: CHANGES REQUESTED — Head covered: 1. B17: the drop is wrong under #489 (blocking)
The Evidence that the dropped check is live. I appended
Fix: 2. Merge resolutions in
|
| item | mutation | control c0f641949 |
head 051fc93ca |
|---|---|---|---|
| B6 | [fwd_out] = self.runner_mgr.call_func( in EngineCore (engine_core.py) |
1257 passed | 2 failed: test_runner_rpc_surface.py::test_every_reply_is_taken_in_a_shape_the_arity_reads (replies taken in a shape not scored: ['atom/model_engine/engine_core.py:386']) and ::test_forward_refuses_and_its_reply_is_one_object_read_for_nine_attributes |
| B20 | }; setattr(seq, "kv_transfer_params_output", {}) closing the blob dict (moriio_connector.py) |
1257 passed | 1 failed: test_kv_blob_site.py::test_each_backend_builds_the_blob_at_exactly_one_site[moriio] (kv_transfer_params_output is written in a form not read here: [...]) |
| B1 | ; setattr(self, "kv_cache", None) in NonAllocatingRunner.allocate_kv_cache (overrides.py) |
1257 passed | 1 failed: test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensor |
These match the developer's rows. None of the kept items I tested is inert. No mutant was discarded.
4. Prose
- No
+line ingit diff c0f641949 051fc93careads__doc__,ast.get_docstring, a comment or a doc file. _is_docstringonly skips a class-body docstring when reading method sets, and it asserts nothing about the text.- There are no design-doc references and no line-number citations.
- Non-blocking, from earlier cycles and outside this delta: the comment
# The three \setattr`s bind ...intest_runner_non_allocating.pycarries an item count that the adjacent* 3already states in code. "Thesetattr` loops bind ..." would not go stale.
5. Merge-tree against the fresh tip
fork/feature/atomcompass_newisc59351778, compass(spec): pin the first-refusal walk and the width-table read on a fragment #493.git merge-tree --write-tree c59351778 051fc93cagives rc=0, treedd5e3cb5f. That is not equal to051fc93ca^{tree}(3ee7f6e66).- The only difference is compass(spec): pin the first-refusal walk and the width-table read on a fragment #493's
tests/compass/test_spec_schema.py(+19), which this PR does not touch. The merge is clean.
6. ponytail-review (delta)
The merge carries _writes_not_read as written, the conflict resolutions add nothing, and the drop commit removes a guard rather than simplifying one. No cuts proposed.
Lean already. Ship.
This reverts commit 051fc93. The caller refusal in test_the_unanswered_helper_returns_one_list_its_one_caller_partitions reads syntax-tree nodes only: import aliases, Name ids, Attribute attrs and the identity of each Call's func. It reads no docstring, comment or design document, so it stays under #489. The __doc__ assertions that were deleted by #492 were never part of it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Developer round: B17 restoredHead:
Mutation evidence (B17)
These match the review's Gate: full CPU tier at the new headI ran the branch only. The head's tree equals
The count is unchanged, as expected. Nothing in SizeThe diff against |
|
This review is agent-authored. Review cycle (post re-scope) 2 Verdict: APPROVE Head covered: 1. Tree equality
2. B17 mutation, reproducedRun in the local
Each run was restored with 3. PR body scope section
One non-blocking wording note. The B17 row says the test now refuses "any other mention of the helper". A string-keyed mention is not refused. I ran the blank line → 4. Merge against the moved tip
Gate 4: ponytail-review (delta)Lean already. Ship. |
…, held by a test (#498) Adds tests/compass/test_kv_mooncake_per_req_cache.py. It imports the real Mooncake connector, stubbing only aiter.dist.parallel_state, and calls its real update_state_after_alloc. A request with per-request cache state must take the full transfer even when hash_block_size matches. Deleting, inverting or weakening the has_per_req_cache guard, which the CPU gate did not notice before, now fails this test. CPU gate on node 18 over the combined tree of this PR, #266 and 99c6107: 5213 passed, rc 0. Closes #496 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Closes #223
What this does
Several AST guards in
tests/compass/check one spelling of what they claim. They skip every other spelling, so a violation written a different way passes the whole suite. This PR makes each reachable guard refuse a shape it cannot read, and name the file, line and source text. It does not add one more spelling to an enumeration.The inventory came first, as the brief asks. It was posted before any code moved:
The row numbers below (B1, B2, and so on) are the inventory's.
Re-scoped under #489 (prose is not a test subject). This PR keeps B1-B3, B5, B6, B10, B12, B17, B20 and B22-B24, and the code-structure parts of B16 (the
exitbody statements, the early-exit check and the literal KV tuple). These guards inspect code. It drops B16's comment assertions. B17 (theunanswered_rpc_namescaller refusal) is kept: it reads syntax-tree nodes and no prose. The__doc__assertions next to it were deleted by #492 and are not part of this PR. After #492 (c0f641949),test_kv_blob_doc_table.pyis gone and its one-site check lives intest_kv_blob_site.py, which now carries B20. The sections below from before the re-scope are kept as the record. Their B16 comment rows no longer describe the branch, and their B17 rows use the test's name from before #492 renamed it. Current evidence is in the latest Developer round comment.Scope: tests only, 0 production lines. Diff against
c0f641949:tests/compass/test_runner_rpc_surface.pytests/compass/test_runner_non_allocating.pytests/compass/test_runner_step_semantics.pytests/compass/test_kv_blob_site.pytests/compass/test_clock_lp_identity.pytests/compass/test_ir_data_model.pyThe estimate was ~160 test lines. After review round 1 the actual was 284 added / 67 removed. After the re-scope, with B17 kept, it is 284 added / 62 removed, inside the 2x halt line (320).
The gaps closed, and how each guard now refuses
_self_assigned(test_runner_non_allocating.py)self.xas a target of=, including inside a tuple, list or starred target; annotated and augmented=;setattr(self, "x", …)with a literal namesetattr, afor/withtarget, and any__dict__/vars(self)write, unless the caller lists them by source text.ModelRunnerlists its threesetattr(self, name, value)loops, which bind what the attention builders return (KV cache and per-request state)._methods(one copy now, intest_runner_rpc_surface.py;test_runner_non_allocating.pyimports it, the waytest_runner_step_semantics.pyalready importsSITES)def,async def, nestedclass, and class-body=/ annotated=/ augmented=if,try, loop)test_only_the_binding_module_reaches_the_engine_arityNone, not1. The newtest_every_reply_is_taken_in_a_shape_the_arity_readsnames its site._call_sitestest_get_num_blocks_refuses_and_the_keys_its_caller_reads_are_namedblock_info[...]subscript that is not a literal_reply_attribute_reads(test_runner_step_semantics.py)fwd_out.x/fwd_output.x; a whole hand-off (positional argument to a method,return,is None)getattr, an alias, or a plain function calltest_what_a_hole_at_exit_loses_is_what_exit_does(renamed by #492; its comment assertions are gone)exit's own bodytest_the_unanswered_helper_returns_one_list_its_one_caller_partitions(renamed by #492; its__doc__assertions are gone)test_each_backend_builds_the_blob_at_exactly_one_site(via new_writes_not_read)kv_transfer_params_outputthat is not a plain=, and the name as a string anywhere in the connectortest_the_package_builds_no_set_at_allset/frozenset, called or notGate 3: the named result, by node id (round 0, at
82171397a; round 1 is re-measured in its own section below)Every mutation is line-count-preserving (checked by the harness before each run), applied to a
git archiveof the tree, and restored after it. The wholetests/compasssuite was run each time, and failures were read by node id from junit XML.f5003b255(the integration tip when these runs were made). Suite: 1048 passed.1955aa708, whose test files are byte-identical to this PR's head82171397a.git diff 1955aa708 82171397aover all six edited tests and every mutated production directory is empty. The rebase moved nothing but compass: hold each non-KV memory term to its own gate, never a sum (MEM-3) #176 and compass(tests): the abandoned-staging pin checks what is inside, not only that it stands #260, which touch neither.overrides.py): green on both sides.Test names below are abbreviated. The full node ids are
tests/compass/<file>::<test>.Named result (the brief's own):
f5003b255setattr(self, "kv_cache", None)inNonAllocatingRunner.allocate_kv_cachetest_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensorself.kv_cache = Nonedestroy_dist_env()→pass # destroy_dist_env()test_runner_rpc_surface.py::test_what_the_comment_says_a_hole_at_exit_loses_is_what_exit_does. #208 closed this spelling before this PR.destroy_dist_env()→(lambda: destroy_dist_env())Every other reachable gap:
self.kv_cache: object = Nonetest_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensorself.config.num_kvcache_blocks, self.kv_cache = n, Noneasync def exit(self): return Trueadded toNonAllocatingRunnertest_runner_non_allocating.py::test_the_replaced_methods_are_the_ones_that_own_memory_or_run_a_stepexit = staticmethod(lambda *a: True)in the class bodytry: from ...model_engine import model_runner/except Exception: passat module scope inoverrides.pytest_runner_non_allocating.py::test_only_the_binding_module_reaches_the_engine[overrides.py][fwd_out] = self.runner_mgr.call_func("forward", …)inengine_core.pytest_runner_rpc_surface.py::test_every_reply_is_taken_in_a_shape_the_arity_reads,…::test_forward_refuses_and_its_reply_is_one_object_read_for_nine_attributes_kv = block_info[KV_KEY]inengine_core.pytest_runner_rpc_surface.py::test_get_num_blocks_refuses_and_the_keys_its_caller_reads_are_named_t = getattr(fwd_output, "tenth", None)inscheduler.pytest_runner_step_semantics.py::test_the_reply_answers_every_attribute_atom_reads_off_one"draft" + "_kv_cache"inModelRunner.exittest_runner_rpc_surface.py::test_what_the_comment_says_a_hole_at_exit_loses_is_what_exit_does(lambda: torch.cuda.empty_cache())inexit_all = lambda overrides: overrides.unanswered_rpc_names(object)inrunner/step_output.pytest_runner_rpc_surface.py::test_the_unanswered_helper_describes_its_whole_return_and_not_one_halfsetattr(seq, "kv_transfer_params_output", {})inmoriio_connector.pytest_kv_blob_doc_table.py::test_each_backend_builds_the_blob_at_exactly_one_site[moriio]seq.kv_transfer_params_output: dict = {}from ..backends import Tierinclock/registry.pytest_clock_lp_identity.py::test_the_package_imports_only_the_standard_library_it_names[registry.py]_ordered = frozenset.unioninclock/registry.pytest_clock_lp_identity.py::test_the_package_builds_no_set_at_all[registry.py]from ..backends import Tierinir/graph.pytest_ir_data_model.py::test_the_package_imports_only_the_standard_library_it_names[graph.py]Covered-by-a-neighbour rows, re-run on both sides as controls. Each has the same result at tip and head, except where this PR adds a second, correct name:
call_func(func_name="exit")fails 3 at tip and 4 at head; the addition istest_both_filters_in_the_derivation_drop_nothing. A keyword-named, unwaitedflush_pp_sendsite fails 2 at tip (test_sync_inventory) and 3 at head, with the same addition.async defreadingfwd_out.tenthfails 2 on both sides.all_reduceoutside the guard fails 1 on both sides,test_sync_inventory.py::test_every_scanned_site_is_classified.capture_cudagraphsite fails 1 at tip, where it is red only because it scores 1. At head it fails 2, addingtest_every_reply_is_taken_in_a_shape_the_arity_reads.Gate 1: ATOM's suite, unmodified, as a delta (round 0; the round-1 gate on the merged tree is in the review round 1 section)
Both runs were on node 18 in
xiaobizh_n18_cpu:git archive+docker cpinto my own root,/tmp/i223gates/<label>/ATOM. Nothing was written into the shared mount.scripts/compass/gate_cpu.sh..compass-commitand.compass-changedstamps were written from the samerev-parse.atom.__file__was asserted under the staged root and printed before any count.timeout -k 10 2400, run one at a time, never piped, and read from its log file. Node ids came from--junitxml.atom.__file__GATE_CPU_RC175739f87(the current integration tip)/tmp/i223gates/ctl3/ATOM/atom/__init__.py82171397a(the round-0 head)/tmp/i223gates/br4/ATOM/atom/__init__.pyBoth runs printed
gate: 29 files excluded + tests/pluginandgpu: not required (.compass-changed stamp), and both printed theirGATE_CPU_RCline, so neither hung.The node-id delta, from junit XML (5259 → 5260 ids):
tests/compass/test_runner_rpc_surface.py::test_every_reply_is_taken_in_a_shape_the_arity_readsThe flaky
TestTheRegionIsNotCopiedPerChunkdid not fail on either side. The same pair measured one tip earlier, atf5003b255/1955aa708, gave 5056 → 5057 passed with the same single added id andGATE_CPU_RC=0on both.git merge-tree --write-tree 175739f87 82171397a→03e9e1f809b7c0e278d7bbb1d9370a39d398a243, rc=0. This equals the head's own tree, so it merges clean.Lint over the six edited files (
black 26.5.1 --target-version py312,ruff 0.16.7):BLACK_RC=0,RUFF_RC=0.Review round 1 (
82171397a→176cac3d4)The review's seven findings are each fixed by making the guard refuse, not by adding one more spelling.
Commits:
0364f226d: F1–F6, plus the file named in the_self_assignedand_methodsmessages.176cac3d4: F7. Its hunk did not apply in0364f226d, because black had already rewrapped the line the edit anchored on. The battery caught this: the F7 mutation was still green at0364f226d._self_assignedsetattr(self, "<literal>", v)is read as a call binding. Every other mention ofsetattr,__setattr__,__dict__orvarsin the class is refused._writes_not_readsetattr,__setattr__,__dict__orvarsin a connector is refused. There are none today._methodstest_every_class_body_the_method_sets_are_read_from_is_read. AnExpris read only as a docstring._arityCallparent scoresNone, not1.HANDED_TO = ("postprocess", "send_tokens"). Any other hand-off is refused.returnorraiseallowed inexitare the opening guard's and the closingreturn True.import unanswered_rpc_names as <x>is refused. A plain import is neutral.Pins. Node 18,
xiaobizh_n18_cpu.mut.py, was run unchanged except for one added spelling (N10).77d203b86and head176cac3d4were each staged bygit archive+docker exec -i, and md5 matched on both ends.atom.__file__was asserted under each root.tests/compasssuite ran per mutation, and results were read from junit XML.tests/compass/…node ids.176cac3d4, failing node id(s)overrides.pysetattr(self, "kv_cache", None)test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensor(lambda: destroy_dist_env())test_runner_rpc_surface.py::test_what_the_comment_says_a_hole_at_exit_loses_is_what_exit_doesself.kv_cache: object = Nonetest_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensorif False: from ...model_engine import model_runnertest_runner_non_allocating.py::test_only_the_binding_module_reaches_the_engine[overrides.py]"draft" + "_kv_cache"test_runner_rpc_surface.py::test_what_the_comment_says_a_hole_at_exit_loses_is_what_exit_doesgetattr(fwd_output, "tenth", None)test_runner_step_semantics.py::test_the_reply_answers_every_attribute_atom_reads_off_oneself.__setattr__("kv_cache", None)test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensorsuper().__setattr__("kv_cache", None)__import__("builtins").setattr(self, "kv_cache", None)setattr(seq, "kv_transfer_params" + "_output", …)test_kv_blob_doc_table.py::test_each_backend_builds_the_blob_at_exactly_one_site[moriio]if False: passtest_runner_rpc_surface.py::test_every_class_body_the_method_sets_are_read_from_is_read. Named; no collection error (rc=1, 1153 cases ran).vars().update(exit=…)test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensorfwd_out, _extra = tuple(call_func("forward", …))test_runner_rpc_surface.py::test_every_reply_is_taken_in_a_shape_the_arity_reads,test_runner_rpc_surface.py::test_forward_refuses_and_its_reply_is_one_object_read_for_nine_attributesself._peek(fwd_output)with_peekreadinggetattr(o, "tenth", None)test_runner_step_semantics.py::test_the_reply_answers_every_attribute_atom_reads_off_oneself.still_running = False; return Truetest_runner_rpc_surface.py::test_what_the_comment_says_a_hole_at_exit_loses_is_what_exit_doesfrom … import unanswered_rpc_names as utest_runner_rpc_surface.py::test_the_unanswered_helper_describes_its_whole_return_and_not_one_halfGate on the tree that will land.
git merge-tree --write-tree ff9617f30 176cac3d4→1849f16602541a399c8bce0acb57cd027baaeabc, rc=0.ff9617f30is the integration tip when the gate ran.git archiveinto my own root..compass-commitis the merged hash, and.compass-changedis the six test files.atom.__file__=/tmp/i223r2/merged3/ATOM/atom/__init__.py. Bounded bytimeout -k 10 2400, unpiped.GATE_CPU_RC=0.Control gate on the tip
ff9617f30, run the same way (atom.__file__=/tmp/i223r2/ctl/ATOM/atom/__init__.py):GATE_CPU_RC=0.tests/compass/test_runner_rpc_surface.py::test_every_reply_is_taken_in_a_shape_the_arity_readsandtests/compass/test_runner_rpc_surface.py::test_every_class_body_the_method_sets_are_read_from_is_read.test_stream_marker_properties.pytiming class failed on either side.Not split out. Everything fits inside the 2x line, so nothing is proposed for a follow-up. If one had been needed, F5 would have been first:
HANDED_TOis a named allowlist of two callees, the same data-flow class as the other items recorded as undone.What surprised me
{"destroy_dist_env", "torch.cuda.empty_cache"} <= calls, so thepassspelling was red atfa5180b62. But that set was built from every call anywhere inexit's walk, so wrapping the call in a lambda left it silent. That is the fifth instance's family: the guard counted a call it never checked was made.test_sync_inventory, which classifies call sites by form. I measured rather than assumed, and I recorded these instead of fixing them._writes_not_readcompared nodeid()s across two separateast.parsecalls, so every connector looked unread and the null mutation went red. That was fixed before this head._arityand inside the blob reader.SITESis computed at import andtest_kv_remote_prefill.pyparametrizes at collection, so the refusal becamerc=2collection errors across three modules and no test ran. The refusals now live in named tests instead:_arityscoresNone, and the blob refusal sits in the one-site test.test_kv_remote_prefill.pyis left as it was. Correction after review round 1: at82171397athis was true only of_arityand the blob reader._methodsstill asserted at import, so one class-bodyif False: passgaverc=2across three modules. It now records intoUNREAD_CLASS_BODYand a named test asserts that list is empty (F3, in the review round 1 section).ModelRunneralready binds through three computedsetattrs (the KV builder loops) that_self_assignednever read. They are not ring holders, but nothing said so; the call site now lists them.getattr(fwd_output, "tenth")without a default raised at run time in the driven scheduler, giving 13 step-semantics failures. It was re-run with a default so that only the guard could redden.Left undone, with the reason
globwalks intest_runner_rpc_surface.py,test_runner_step_semantics.py,test_memory_readings.py)atom/compass/runner/,atom/model_engine/andatom/compass/memory/have no subdirectory. This is #218's shape.__import__,importlib.import_module,exec) in B3, B22, B24, B25sys.modules,builtins,getattrorexecwith any string, so no finite refusal exists.a.keys() | b.keys()) in B23out = func(); a, b = out) in B18wait_outread asFalsewhen it is not a literaltest_sync_inventoryclassifies the site by its form.test_kv_remote_prefill.pytest_spec_schema.py)"twelve"s from #208's reviewtest_kv_simulated_connector.py's clock-name scan andtest_memory_compare.py's design-reference scan (landed with #176 after the inventory)Overlaps, for the restacker
tests/compass/test_runner_non_allocating.pyis shared with compass: MEM-2 -- size the KV pool with ATOM's own arithmetic over substituted readings #178 and compass(runner): the projection that builds a BatchView, and the rung it supplies #252.from test_runner_rpc_surface import _methodsimport; the removed local_methods;_on_selfplus the rewritten_self_assigned; theModelRunnercall site intest_the_docstring_names_every_attribute_that_holds_the_ring; and the relative-import resolution intest_only_the_binding_module_reaches_the_engine.test_only_the_binding_module_reaches_atoms_runnerwith"atom.model_engine.model_runner" in imported. When restacking, keep the resolution. Without it,from ...model_engine import model_runneris blind again (B3)._import_time_importsrecordsfrom atom.model_engine import model_runnerasatom.model_engine, so compass(runner): the projection that builds a BatchView, and the rung it supplies #252'sin importedcheck misses that spelling even when it is absolute. compass(runner): the projection that builds a BatchView, and the rung it supplies #252's runtime import test covers it on the CPU tier.test_runner_non_allocating.py, intest_only_the_binding_module_reaches_the_engine(measured by the reviewer withgit merge-treeagainst4213246a0). compass: MEM-2 -- size the KV pool with ATOM's own arithmetic over substituted readings #178 addsatom.compass.memoryto the exemption and a docstring paragraph. To resolve: keep compass: MEM-2 -- size the KV pool with ATOM's own arithmetic over substituted readings #178's docstring and itsstartswith(("atom.compass.runner", "atom.compass.memory")), applied to this PR's resolvedimportedset.tests/compass/test_runner_rpc_surface.pyis shared with compass: MEM-2 -- size the KV pool with ATOM's own arithmetic over substituted readings #178. This PR's hunks:_methods,_arity, theSite.arityannotation, the_call_sitesfilter, the newtest_every_reply_is_taken_in_a_shape_the_arity_reads, the top oftest_get_num_blocks_refuses_and_the_keys_its_caller_reads_are_named, the exit test, and the unanswered-helper test. compass: MEM-2 -- size the KV pool with ATOM's own arithmetic over substituted readings #178 edits the end of the sameget_num_blockstest (itsmatch=) and theEXTENSION_CLASSESblock, so the hunks are adjacent but not overlapping.No design-doc references in code, test names, comments or emitted data.
🤖 Generated with Claude Code