Skip to content

StringIdentity: a module name counts only where a string selects a module; FrozenFieldWrite skips own __post_init__ - #2553

Merged
trissim merged 9 commits into
mainfrom
commander/module-identity
Oct 21, 2026
Merged

trissim merged 9 commits into
mainfrom
commander/module-identity

Conversation

@trissim

@trissim trissim commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Defects

  1. StringIdentity counted 'problem_number' at tools/cutover/carry_problem_numbers.py:33 on fix/P440-fork-owner-repair (numbers_missing and 'problem_number' in stored) as naming the module src/agent_comms/problem_number.py. That literal is a stored SQLite table name, not a module selection.
  2. FrozenFieldWrite counted every object.__setattr__ call, including construction inside the frozen class's own __post_init__ (registry_document.py:716 on 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 @handles method on ModulePositions:

  • the first argument of an importer: import_module, __import__, importlib.util.find_spec, importlib.resources.files (the names come from the functions);
  • a sys.modules key, membership test or method argument (sys.modules is spelled from the sys module object);
  • the argument after an interpreter's -m.

TypePositions (from #2544) and ModulePositions now share DeclaredPositions. DeclaredSpellings pairs 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: a FunctionDef named __post_init__ directly in a ClassDef body, whose own statements (OwnInstanceWrites, which stops at nested functions, lambdas and classes) call object.__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

main's measure this branch
P440 carry_problem_numbers.py:33 'problem_number' 1 0
#2480 head registry_document.py FrozenFieldWrite lines [716] []
origin/main class/module-name literals (NamedDeclaration) 67 42

Real module selections that still count: harness_install.py:73 and :160 (files('agent_comms…')), owner_lifecycle.py:514 and restart_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-1217 spelled four agent-comms module paths by hand (MEMB-4). mechanism_modules now 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.

Site Literal restates Owner that replaces it Pattern
benchmarks/lock_observation.py:81, :93 the package's name, module.__name__.startswith("agent_comms") agent_comms.__name__ MEMB-4
tools/module_layers.py:32 the package's name, PACKAGE = "agent_comms" agent_comms.__name__ MEMB-4
tools/module_layers.py:58 layer membership written as name prefixes ("tool_", "tools") a layer declared by each module or package, with membership derived from it MEMB-3
src/agent_comms/native_pi.py:97 "not_sent" restates NotSentInput.public_status (input_attempt.py:439), and the "input_disposition" key hand-writes a diagnostic record NotSentInput.public_status, plus a declared diagnostic record encoded by FieldCodec IDEN-3, MEMB-5
benchmarks/trace_channel_delivery.py:102 the "response_policy" key restates Message.response_policy's field name FieldCodec.project of Message's declared fields MEMB-5
src/agent_comms/turn_context.py:1016 the "tools" key of a hand-built provider: dict[str, Any] (:1160); the key spelling is the provider's, but the request shape is restated by hand a declared provider-request record encoded by FieldCodec BOUND-1, MEMB-5

External, 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 on ToolResult.
  • src/agent_comms/cli_bridges/codex_rollout.py:213, wire_tag = 'turn_context': Codex's rollout record type, declared once on TurnContextRecord.
  • 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/* and docs/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. Whether docs/ 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 old ProblemNumber declared, which ProblemAllocation.declared_name has since replaced, and the one-shot cutover has to name the old stored form.

Tests

  • New test_a_module_name_counts_only_where_a_string_selects_a_module. Not counted: a table name in in stored, a bare assignment. Counted: find_spec, sys.modules.get, in sys.modules, files(...), -m.
  • New 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 to self.other inside __post_init__, a write in a function nested inside it, another method, and a module-level __post_init__.
  • Two existing tests now spell the module through sys.modules, a selecting position, because they measure where an identity is counted, not what counts.
  • Run locally, one process: 21 passed (string identity, one-word, module-name, frozen, vocabulary, offset-with-deletion).

🤖 Generated with Claude Code

trissim and others added 3 commits October 21, 2026 16:56
…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>
@trissim
trissim marked this pull request as ready for review October 21, 2026 21:03
Copilot AI balanced review requested due to automatic review settings October 21, 2026 21:03
@trissim
trissim enabled auto-merge October 21, 2026 21:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 trissim left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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' at claude_protocol.py:581 and 'turn_context' at codex_rollout.py:213 are external wire tags.
  • "input_disposition": "not_sent" at native_pi.py:97 is a dict key.
  • provider["tools"] at turn_context.py:1016 is 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.

  1. tools/debt_ratchet.py:589-600: the FrozenFieldWrite exemption covers every object.__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 of self and false of anything else. I ran this head's tool on it:
    class A:
        def __post_init__(self):
            object.__setattr__(self.other, "x", 1)   # main tool: 1, this head: 0
    
    A frozen write onto another object, smuggled inside a constructor, now passes the ratchet. The same goes for a closure defined inside __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: in constructing, yield only the object.__setattr__ calls whose first argument is ast.Name(id=member.args.args[0].arg), and don't descend into nested FunctionDef/Lambda. Add the self.other case and a nested-def case to test_a_frozen_write_counts_except_in_its_own_classes_post_init, both expected to count. #2480's registry_document.py:716 writes self directly, so it still clears (1 → 0 under this head).

@trissim
trissim disabled auto-merge October 21, 2026 21:10
trissim and others added 3 commits October 21, 2026 17:29
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>
@trissim
trissim enabled auto-merge October 21, 2026 21:33
trissim and others added 2 commits October 21, 2026 17:35
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>
@trissim
trissim added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 2b95649 Oct 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants