Tasks: Harden the Dead-Symbol Gate
Mission: harden-dead-symbol-gate-01KW0RJR Planning base branch: feat/harden-dead-symbol-gate Final merge target: main (via PR from the feature branch) Spec: spec.md · Plan: plan.md · Research: research.md · Data model: data-model.md
Overview
A single atomic work package. The parser fix un-blinds 57 modules → ~119 symbols surface in ONE gate run; the 4 detectors must absorb the live ones, the residue must be disposed, and the BANNED_FLAGS security fix must land too (un-blinding envelope.py surfaces BANNED_FLAGS — wiring it is what makes it live). The gate (tests/architectural/test_no_dead_symbols.py) is green only when ALL of this is coherent, so it cannot be split into independent lanes. Decisions baked in: register-arg symbols are DEMOTEd (no 5th detector); making the symbol-gate baselines a real ratchet is OUT OF SCOPE (follow-up).
The load-bearing invariant (C-001 / NFR-001): every detector binds proof-of-life to a RESOLVED declaring module, never a bare name — and a no-false-negative regression test proves a synthetic dead symbol is still flagged.
Subtask Index
| ID | Description | WP | Parallel |
|---|---|---|---|
| T001 | FR-001: fix _extract_all_literal (continue on non-__all__ AnnAssign) + unit test | WP01 | |
| T002 | FR-002: per-tree alias map + module-style detector (a) [subsumes (c) Typer] + unit test | WP01 | |
| T003 | FR-002: getattr-string detector (d-getattr) + __getattr__ facade detector (b) + unit tests | WP01 | |
| T004 | NFR-001: no-false-negative regression test (synthetic dead symbol still flagged) | WP01 | |
| T005 | FR-003/004/006/007: delete _daemon_root, DEMOTE the residue, allowlist-defer auth trio, update _baselines.yaml | WP01 | |
| T006 | FR-005: wire the BANNED_FLAGS security check in envelope.py + --yolo-rejected test | WP01 | |
| T007 | Verify: gate green (57 modules now inspected), full architectural+contract suite, no net growth, ruff/mypy | WP01 |
Work Packages
WP01 — Harden the dead-symbol gate
- Goal: Fix the parser bug, add 4 structurally-anchored caller detectors so the ~119 surfaced symbols are recognized live with no allowlist growth, dispose of the genuinely-dead residue, wire the
BANNED_FLAGSsecurity check, and leavetests/architectural/+tests/contract/green — without weakening the gate (no false negatives). - Priority: P1 (the whole mission)
- Dependencies: none
- Prompt: tasks/WP01-harden-dead-symbol-gate.md
- Estimated prompt size: ~520 lines (7 subtasks)
- Independent test:
PWHEADLESS=1 uv run pytest tests/architectural/test_no_dead_symbols.py tests/architectural/ tests/contract/ -qgreen; the un-blinded gate inspects the previously-hidden 57 modules;category_a/category_bfrozenset counts ≤ base;--yolorejected byparse_and_validate_policy.
Included subtasks:
- ✅ T001 FR-001: in
tests/architectural/test_no_dead_symbols.py, fix_extract_all_literal(~L938) so a non-__all__top-levelAnnAssigncontinues instead of early-returningfrozenset(); add a focused unit test (module whose first top-level node is a non-__all__AnnAssign followed by a real__all__) (WP01) - ✅ T002 FR-002: build a per-tree import-alias map (
local name → resolved dotted modulefromast.Import+ast.ImportFrom asname); add detector (a) — walkast.Attribute(value=Name(id in alias_map)), resolve to module, record the edge; this subsumes (c) Typer registration; fold into_imports_by_targetso_symbol_has_caller's re-export rules apply; add a unit test (WP01) - ✅ T003 FR-002: add detector (d-getattr) —
Call(getattr, [Name in alias_map, Constant str])→ edge; and detector (b) — module withdef __getattr__+ static dict-literal(submodule, "name")tuples → mark the submodule's canonical symbol live; add unit tests for each (WP01) - ✅ T004 NFR-001: add the no-false-negative regression test — a synthetic module with an
__all__symbol that has NO caller of any recognized kind MUST still be flagged dead (proves the detectors didn't blind the gate) (WP01) - ✅ T005 FR-003/004/006/007: DELETE
specify_cli.sync.owner::_daemon_root(re-export); DEMOTE from__all__(keep defs, verify no externalfrom-import/star-import first):auth.transport::reset_user_facing_dedup,sync.owner::check_daemon_owner_match,compat.safety_modes::{_ORCHESTRATOR_API_UNSAFE_SUBCOMMANDS, SafetyPredicate, _orchestrator_api_predicate, _mission_state_predicate},legacy_detector::LEGACY_LANE_DIRS,readiness.upgrade_ux::{PromptCallback, UpgradeUxOutcome},doctrine.versioning::migrate_v1_to_v2, plus any residual annotation-only/test-only symbol the detectors don't rescue; ALLOWLIST-as-deferred theauth.transporttrio (get_client/get_async_client/reset_clients) with ONE justified entry; updatetests/architectural/_baselines.yamlsocategory_a/category_bshow no net growth, each edit with a# justification:comment (WP01) - ✅ T006 FR-005: in
src/specify_cli/orchestrator_api/envelope.py, makeparse_and_validate_policyreject anydangerous_flagsentry that is a member ofBANNED_FLAGS(currently defined but never enforced), returning the typed validation error the contract expects; add a test (e.g.--yolorejected) intests/agent/test_envelope_unit.py(WP01) - ✅ T007 Verify:
PWHEADLESS=1 uv run pytest tests/architectural/test_no_dead_symbols.py -q, thentests/architectural/ tests/contract/ -q; confirm the ~119 surfaced symbols are recognized live with ZERO new allowlist entries (only the deferred auth trio adds one);category_a/category_bcounts ≤ base;uv run ruff check(diff-scoped) +uv run mypy src/specify_cli/orchestrator_api/envelope.pyclean (WP01)
Implementation sketch (research D-05 order): 1. T001 parser fix (un-blinds — gate will be RED with ~119 until detectors+dispositions land). 2. T002 alias map + module-style detector (a/c). 3. T003 getattr + facade detectors (d/b). 4. T004 no-false-negative guard. 5. T005 dispose the residue (delete/demote/allowlist) + baselines. 6. T006 wire BANNED_FLAGS (also makes that surfaced symbol live). 7. T007 verify green + no growth.
Parallel opportunities: none — single atomic gate-green unit.
Risks:
- Masking real dead code (C-001): a too-loose matcher. Mitigation — every rule resolves the alias to the EXACT declaring module before counting; the T004 no-false-negative test is the guard; reuse the stale-allowlist reverse check (L1093–1099).
- Demoting an externally-imported symbol: verify no
from mod import sym/import *before each demote (C-004). - Baseline drift (C-003): re-confirm
category_a/category_blive sizes vs the base (depends on #2159/#2152 merge state) before setting counts. - BANNED_FLAGS coupling: un-blinding surfaces it; T006 must land in the same WP or the gate flags it dead.
Requirement coverage
FR-001 … FR-007 all map to WP01. NFR-001…004 and C-001…004 are verified by T004/T007 and the WP constraints.