Repository navigation
StringIdentity: a module name counts only where a string selects a module; FrozenFieldWrite skips own __post_init__ - #2553
Conversation
…n write outside construction StringIdentity counted the literal 'problem_number' (a stored table name in carry_problem_numbers.py:33 on P440) as naming src/agent_comms/problem_number.py. A module's name now names it only in a module-selecting position, declared on ModulePositions: an importer's argument (import_module, __import__, find_spec, resources.files), a key, member test or method argument of sys.modules, or the argument after an interpreter's -m. TypePositions and ModulePositions share DeclaredPositions. A module stem that is also a function's name stays counted everywhere, as the function's name. FrozenFieldWrite counted object.__setattr__ inside a frozen class's own __post_init__ (registry_document.py:716 on #2480), where the instance is still being built. The class body's __post_init__ is now outside the measure's sites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bare assignment of a module's name no longer names it; the test writes the spelling into sys.modules, a module-selecting position, so it still measures where the identity is counted, not what counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
trissim
left a comment
There was a problem hiding this comment.
agent-comms/review: failure at b322eaf. The StringIdentity half is verified and good. I ran DeclaredSpellings over every production path at origin/main with the main tool and with this head's tool: 230 → 205, with 25 dropped and 0 added. The drops I read are all false positives:
wire_tag = 'tool_result'atclaude_protocol.py:581and'turn_context'atcodex_rollout.py:213are external wire tags."input_disposition": "not_sent"atnative_pi.py:97is a dict key.provider["tools"]atturn_context.py:1016is a provider field.
The real cases still count: harness_install.py:73, :160, restart_queue.py:290, and the owner_lifecycle.py worker module, which is at :515 on current main. P440's carry_problem_numbers.py:33 clears (it was 1 under the main tool, 0 here). The touched guard tests pass: 12 passed.
tools/debt_ratchet.py:589-600: the FrozenFieldWrite exemption covers everyobject.__setattr__inside a__post_init__, not only the ones writing the instance being constructed. The docstring's reason is "there the instance is still being constructed", which is true ofselfand false of anything else. I ran this head's tool on it:A frozen write onto another object, smuggled inside a constructor, now passes the ratchet. The same goes for a closure defined insideclass A: def __post_init__(self): object.__setattr__(self.other, "x", 1) # main tool: 1, this head: 0__post_init__that runs later. That's IDEN-7: the exemption is wider than the question. Nothing on main exploits it today (I walked every__post_init__), but a ratchet with a hole is a false proof. Fix: inconstructing, yield only theobject.__setattr__calls whose first argument isast.Name(id=member.args.args[0].arg), and don't descend into nestedFunctionDef/Lambda. Add theself.othercase and a nested-def case totest_a_frozen_write_counts_except_in_its_own_classes_post_init, both expected to count. #2480'sregistry_document.py:716writesselfdirectly, so it still clears (1 → 0 under this head).
Head review: the previous shape exempted every object.__setattr__ under any __post_init__, so object.__setattr__(self.other, ...) and writes in nested functions stopped counting. OwnInstanceWrites visits the method's own body, stops at nested scopes, and keeps only calls whose first argument is the method's first parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Class-level aliases of visit_ methods are mixedCase variables to ruff (N815 x3); NodeVisitor dispatches by method name, so each scope gets its own method. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
debt_ratchet.py:1215-1217 spelled four agent-comms module paths by hand (MEMB-4). They are now each module's inspect.getfile, relative to the package's parent; Toad's two stay as path text because they live in another repository. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Defects
'problem_number'attools/cutover/carry_problem_numbers.py:33onfix/P440-fork-owner-repair(numbers_missing and 'problem_number' in stored) as naming the modulesrc/agent_comms/problem_number.py. That literal is a stored SQLite table name, not a module selection.object.__setattr__call, including construction inside the frozen class's own__post_init__(registry_document.py:716on P434: present other-route threads with incarnation-safe display references #2480's head 5e8b016).The rules
A module's name names it only where a string selects a module. Each position is declared once, as a
@handlesmethod onModulePositions:import_module,__import__,importlib.util.find_spec,importlib.resources.files(the names come from the functions);sys.moduleskey, membership test or method argument (sys.modulesis spelled from thesysmodule object);-m.TypePositions(from #2544) andModulePositionsnow shareDeclaredPositions.DeclaredSpellingspairs each set of names with its positions: unmistakable names across the whole tree, all names in type positions, module names in module positions. A module stem that is also a declared function's name stays counted everywhere, as the function's name. Nothing is exempted per file or per line.A frozen write is construction only inside the declaring class's own
__post_init__. That is decided from the syntax tree: aFunctionDefnamed__post_init__directly in aClassDefbody, whose own statements (OwnInstanceWrites, which stops at nested functions, lambdas and classes) callobject.__setattr__with the method's first parameter as the first argument. A write to another object, a write in a nested scope, and writes from other methods or a module-level function of that name all still count.Evidence on real history
carry_problem_numbers.py:33 'problem_number'registry_document.pyFrozenFieldWrite linesReal module selections that still count:
harness_install.py:73and:160(files('agent_comms…')),owner_lifecycle.py:514andrestart_queue.py:290("-m", "agent_comms.…").No sites were added.
python tools/debt_ratchet.py --root tools --base origin/main --head HEAD(one core) reports zero growth lines. mypy and ruff counts on the file equal main's.The 25 sites this rule stops counting, judged against the clean form
The test is whether a typed owner (the class, its registry, a declared attribute) would replace the literal. Being a non-selection isn't enough. 11 of the 25 are real string identities. The measure only caught them because their spelling happens to equal a module name.
Fixed in this PR (4):
tools/debt_ratchet.py:1215-1217spelled four agent-comms module paths by hand (MEMB-4).mechanism_modulesnow derives each one from the module's own file (inspect.getfile, relative to the package's parent). Toad's two paths stay as text, because they live in another repository.Real, listed for later (7): no orders yet. The counter no longer shows these, so this list is their record.
benchmarks/lock_observation.py:81,:93module.__name__.startswith("agent_comms")agent_comms.__name__tools/module_layers.py:32PACKAGE = "agent_comms"agent_comms.__name__tools/module_layers.py:58"tool_", "tools")src/agent_comms/native_pi.py:97"not_sent"restatesNotSentInput.public_status(input_attempt.py:439), and the"input_disposition"key hand-writes a diagnostic recordNotSentInput.public_status, plus a declared diagnostic record encoded by FieldCodecbenchmarks/trace_channel_delivery.py:102"response_policy"key restatesMessage.response_policy's field nameFieldCodec.projectofMessage's declared fieldssrc/agent_comms/turn_context.py:1016"tools"key of a hand-builtprovider: dict[str, Any](:1160); the key spelling is the provider's, but the request shape is restated by handExternal, so the exemption is right (3):
src/agent_comms/cli_bridges/claude_protocol.py:581,wire_tag = 'tool_result': Claude stream-json's content-block type, declared once onToolResult.src/agent_comms/cli_bridges/codex_rollout.py:213,wire_tag = 'turn_context': Codex's rollout record type, declared once onTurnContextRecord.stack/test-native-global-extensions.py:99,extension["tools"]: Pi's extension-registration JSON, another program's output.Retained proof scripts, so no order (11):
docs/refactor/retained-index-cross-version-20261005/*anddocs/refactor/retained-routing-provenance-20261005/*. Seven are manifest keys (p['tools'],read_authority['native_package']). Four are installed-package path text (site / 'agent_comms',package(..., 'agent_comms', 297)). By the definition they are restated facts (raw manifest reads; the package name restated), but they are frozen proof artifacts from 2026-10-05, and editing them changes the evidence. No order. Whetherdocs/should count as production at all is a separate question.The P440 site this PR was opened for (
carry_problem_numbers.py:33,"problem_number" in stored) is external in the same sense. It is the table name the oldProblemNumberdeclared, whichProblemAllocation.declared_namehas since replaced, and the one-shot cutover has to name the old stored form.Tests
test_a_module_name_counts_only_where_a_string_selects_a_module. Not counted: a table name inin stored, a bare assignment. Counted:find_spec,sys.modules.get,in sys.modules,files(...),-m.test_a_frozen_write_counts_except_in_its_own_classes_post_init. Not counted: the class's own__post_init__writing its own first parameter. Counted: a write toself.otherinside__post_init__, a write in a function nested inside it, another method, and a module-level__post_init__.sys.modules, a selecting position, because they measure where an identity is counted, not what counts.🤖 Generated with Claude Code