Contracts
symbol-key-resolver.md
Contract: Symbol-Key Resolver + Live Collision Classifier
Module: tests/architectural/_symbol_key.py (NEW; consumed by test_no_dead_symbols.py).
resolve_symbol_key(node, module_path, tree) -> SymbolKey | None
Produces the relocation-tolerant key for one __all__-declared symbol.
Span MUST cover: ClassDef, FunctionDef, AsyncFunctionDef, Assign, AnnAssign (FR-002 — the T001 gap), and ImportFrom scoped to the single aliased name (FR-004 — not the whole statement).
{name:(module,attr)} (2-tuple) and runtime _EXPORT_MODULES {name:module_const} (1-value; the 6 specify_cli.runtime::* entries the gate's _record_facade_edges skips via its len!=2 guard). Re-derive the dict-parse KEY-side to yield name → (module, attr) (needed to locate the body to hash). Reuse ONLY the two PURE helpers _find_facade_lazy_dict_name + _resolve_relative_module; do NOT reuse/edit _record_facade_edges (byte-frozen C-005, caller-graph-shaped, discards the name). Enumerate by shape, not "all 8".
- Content tier (default):
(bare_name, body_hash),module_path=None. - body_hash: hash of
anchoring.code_tokens_by_lineover the definition span. - Facade-dict exports (FR-003 — rescoped): handle BOTH shapes —
sync _LAZY_IMPORTS - Undecidable: return
Nonefor any shape the resolver cannot span. NEVER guess.
Postconditions
single-alias; the spike proved ClassDef/FunctionDef).
whitespace) → identical key (NFR-001).
- Deterministic across 3.11↔3.12 for every supported shape (DoD j proves AnnAssign +
- Pure code motion (module move, sibling reorder, blank/comment insertion, annotation
classify_collisions(all_symbols) -> dict[str, list[Location]] (LIVE, per run — FR-005/D-2)
walk of the src/ tree. Build once per gate run, not per entry (perf).
hard-code it (a future byte-identical pair must be caught automatically).
- Build a
bare_name → [live __all__ locations with matching body_hash]index by ONE - A
bare_namewith ≥2 entries sharing abody_hashis a collisionbare_name. - Today's collision set == the ArtifactKind trio; the classifier MUST re-derive it, not
key_tier(key, collision_index) -> "content" | "module_path" | FAIL (FR-005/FR-009/D-3)
one live location → content (relocation-proof).
locations → escalate to module_path (if module_path disambiguates) OR FAIL (fail-closed) — never silently exempt.
- content key whose
bare_nameis NOT in the collision index AND resolves to exactly - content key whose
bare_nameIS a collisionbare_name, OR that resolves to ≥2 live Nonekey → FAIL (fail-closed).
is_dangling(entry, live_index) -> bool (FR-008/D-4)
also trip the prune ratchet (no double-flag).
- content-tier entry:
(bare_name, body_hash)→ 0 live locations → dangling. - module_path-tier entry:
(bare_name, module_path)→ no live__all__decl → dangling. - A body edit produces exactly ONE signal (offender-refresh), reconciled so it does not
Non-negotiables
path** (C-007), not this module's functions in isolation.
ZERO). Build the (bare_name → [locations]) index + hashes ONCE per run over the cached _walk_modules trees (extend _walk_modules to retain source — it is NOT C-005-frozen); add a perf-budget assertion to the gate self-test so a regression shows.
- No
if key is None: <exempt>fallback ([[no_legacy_resolver_paths]]). - Reuse
anchoring.code_tokens_by_line; do not fork a normalizer (S3776). - The bite battery drives assertions through the **production
_compute_offenders/stale - Perf: the body-hash introduces a net-new
tokenizepass (the current gate makes
warning-remediation.md
Contract: Warning Remediation (FR-016 / NFR-006 / SC-005)
Census (done — analysis/test-change-coupling, full tests/architectural/)
40 warnings; near-all intentional warnings.warn(UserWarning) report-only diagnostics:
| Source | ~count | Class | Disposition |
|---|---|---|---|
tests/architectural/test_migration_chain_integrity.py | ~13 | patch-skip "may warrant verification" | in-mission (owned) |
tests/architectural/test_gate_coverage.py | 1 | 804 tests selected by ≥2 CI gates (report-only) | in-mission (owned) |
tests/architectural/test_template_governance_payload_contract.py | a few | governance payload | in-mission (owned) |
tests/architectural/test_charter_references_resolve.py, test_wp_prompt_build_latency.py | a few | report-only | in-mission (owned) |
tests/contract/test_example_round_trip.py:514 | ~13 | legacy-contract # pydantic_model: backfill | tracked follow-up (cross-package) |
src/doctrine/base.py:108 | 1 | invalid terminology-guard.toolguide.yaml skipped (pydantic extra_forbidden on references) | tracked follow-up OR scoped fix (src-side) |
Remediation rule (binding)
1. Preserve the signal, change the channel. The patch-skip / duplicate-gate / legacy-backfill diagnostics are load-bearing. Route them off the warnings channel (pytest record_property, captured log, or a report line) OR register an expected-warning with an inline rationale. Do NOT delete the diagnostic. 2. NO blanket filterwarnings = ignore. A narrowly-scoped, individually-justified @pytest.mark.filterwarnings (or filterwarnings ini entry) is permitted ONLY for a third-party warning we cannot fix at source, each carrying an inline rationale. 3. Scope boundary. Arch-side emitters (tests/architectural/*) are in-mission and owned. tests/contract/test_example_round_trip.py (legacy-backfill) and src/doctrine/base.py + the invalid toolguide YAML are cross-package/src-side: remediate in-mission ONLY if within a declared owned surface; else split to a tracked follow-up named in issue-matrix.md — never silently suppressed.
Acceptance (SC-005 / NFR-006)
warnings** from the arch suite.
entry OR a tracked src//contract follow-up in issue-matrix.md — verifiable by re-running the census.
- Re-run the census (
pytest tests/architectural/ -W default -r w): **0 first-party - Any residual is a documented, individually-justified third-party
filterwarnings