Contracts
non-goal-invariants.md
Contract: source_module non-goal invariants (G1–G6)
These guards live in tests/unit/test_symbol_key.py and MUST be present and green. They pin the #3552 non-goals so a future edit cannot silently violate them.
G6 — keystone: field is non-comparing
import dataclasses
f = {f.name: f for f in dataclasses.fields(SymbolKey)}["source_module"]
assert f.compare is False
Fails the instant someone makes the field comparing (closes critical risk R1).
G1 — equality ignores source_module
Two SymbolKeys equal in (bare_name, body_hash[, module_path]) but differing in source_module (incl. None vs set) compare equal.
G2 — hash / frozenset membership ignores source_module
The same two keys hash-equal and are mutual members of a frozenset; a resolver-minted key with source_module=None is in an allowlist frozenset whose entry carries a non-None source_module (models final_key in allowlist).
G3 — no tier escalation
A content-tier key (module_path=None) with source_module set is still is_content_tier, and key_tier(...) returns it unescalated (no module_path minted from provenance). Preserves D-1 relocation-tolerance (non-goal: no content→collision).
G4 — as_tuple() excludes source_module
SymbolKey("Foo", "h", source_module="pkg.mod").as_tuple() == ("Foo", "h"); collision-tier → ("Foo", "m", "h"). Provenance never enters the identity tuple.
G5 — body_hash unaffected; resolver key has no provenance
body_hash is identical with or without source_module; a resolver-minted key for a still-dead symbol has source_module is None. Adding a provenance peer does not change any other key's body_hash.
Acceptance anchor (FR-005 / SC-002) — comment-independent recovery
With source_module set on an allowlist entry, deleting or garbling its # module::Name provenance comment does not change the module the helper recovers or the refresh decision it reaches. Red-first constructible against today's comment-parsing path (which would change the decision when the comment is broken).
Explicitly NOT the anchor (post-plan squad): a genuine live same-bare_name+same-body_hash collision still escalates to NEEDS_MODULE_PATH — correct, and unchanged by this mission. source_module is compare=False, so it cannot enter final_key in allowlist and cannot exempt a live collision (that would forfeit relocation-tolerance, C-002/G3). Such a collision is resolved by a hand-authored collision-tier module_path= entry, orthogonal to source_module. The two #3558 Finding-1 tests (test_decide_escalates_content_tier_entry_needing_collision_tier, ..._escalates_end_to_end) assert NEEDS_MODULE_PATH and stay green.
Completeness + integrity guards (FR-006 / FR-007)
- FR-006 completeness:
test_every_content_tier_entry_has_source_module— fails if any allowlist-scoped content-tierSymbolKey(...)call lacks asource_module=kwarg. Replaces the deleted parseable-comment gate so SSOT is kept as the corpus grows. - FR-007 integrity: every entry's
source_modulenames a module in the live importable corpus that declares the symbol (reuseclassify_collisions' corpus walk; do not re-parse the# module::Namecomment — that would recreate the machine comment-parser SC-004 retires, under a new name). Replaces_recover_provenance'sName == bare_namecross-check so drift cannot silently move onto the field. If any comment-adjacent parsing is unavoidable, it must be added by name to SC-004's grep-ban so it can't be cloned silently.