---
phase: 01-unify-duplicated-discovery-loops
plan: 04
type: execute
wave: 1
depends_on: []
files_modified:
  - src/lifx/network/transport.py
  - tests/test_network/test_transport.py
autonomous: true
requirements:
  - D-09
  - D-12
user_setup: []

must_haves:
  truths:
    - "UdpTransport.receive_many emits a DeprecationWarning at call time naming v2.0 as the removal target and pointing to receive()/discovery loops as the alternative (D-09)"
    - "The receive_many docstring carries a `.. deprecated::` note (D-09)"
    - "The receive_many method body is otherwise unchanged — same return type and behaviour (D-09)"
    - "A test asserts receive_many emits DeprecationWarning via pytest.warns (D-12)"
    - "Existing receive_many tests still pass (the method is deprecated this phase, removed only in v2.0) (D-10)"
  artifacts:
    - path: "src/lifx/network/transport.py"
      provides: "receive_many with DeprecationWarning + deprecated docstring note"
      contains: "DeprecationWarning"
    - path: "tests/test_network/test_transport.py"
      provides: "test asserting receive_many emits DeprecationWarning"
      contains: "pytest.warns(DeprecationWarning"
  key_links:
    - from: "src/lifx/network/transport.py UdpTransport.receive_many"
      to: "warnings.warn(..., DeprecationWarning, stacklevel=2)"
      via: "warnings.warn at top of method body"
      pattern: "DeprecationWarning"
    - from: "tests/test_network/test_transport.py"
      to: "UdpTransport.receive_many deprecation"
      via: "pytest.warns(DeprecationWarning, match=\"v2.0\")"
      pattern: "pytest.warns\\(DeprecationWarning"
---

<objective>
Deprecate `UdpTransport.receive_many` (confirmed zero production callers — all multi-response
flows go through `connection.request_stream()`): emit a `DeprecationWarning` at call time and add a
`.. deprecated::` docstring note targeting removal in v2.0, leaving the method body otherwise
untouched (D-09). Add one test asserting the warning fires (D-12).

Purpose: Begin the deprecation cycle so `receive_many` can be removed cleanly in v2.0 without
breaking any current caller this phase.

Output: Deprecated `receive_many` + `test_receive_many_emits_deprecation_warning`. This plan is
independent of the discovery refactor (different files) and runs in parallel with Plan 01.
</objective>

<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-unify-duplicated-discovery-loops/01-CONTEXT.md
@.planning/phases/01-unify-duplicated-discovery-loops/01-RESEARCH.md
@.planning/phases/01-unify-duplicated-discovery-loops/01-PATTERNS.md
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Deprecate UdpTransport.receive_many</name>
  <files>src/lifx/network/transport.py</files>
  <read_first>
    - src/lifx/network/transport.py (top-of-file imports at 1–22; the receive_many method at 282–333 — note the body starts with the `if self._protocol is None:` guard at 297)
    - .planning/phases/01-unify-duplicated-discovery-loops/01-PATTERNS.md (transport.py section lines 280–342 — exact deprecation pattern, docstring note, and stacklevel=2 guidance)
    - .planning/phases/01-unify-duplicated-discovery-loops/01-RESEARCH.md (DeprecationWarning for receive_many lines 509–532)
  </read_first>
  <behavior>
    - Calling UdpTransport.receive_many emits exactly one DeprecationWarning whose message contains "v2.0" and points to receive()/discovery loops as the alternative.
    - The warning is emitted with stacklevel=2 so it points at the caller, not at receive_many itself.
    - receive_many otherwise returns the same list[tuple[bytes, tuple[str, int]]] and preserves all existing behaviour (protocol-closed guard, packet-size filtering, timeout handling).
  </behavior>
  <action>
    Add `import warnings` to the top-of-file import block (stdlib; keep alphabetical/grouping
    consistent with the existing imports). At the very start of the `receive_many` method body —
    BEFORE the `if self._protocol is None:` guard — call
    `warnings.warn(<message>, DeprecationWarning, stacklevel=2)`. The message text is Claude's
    discretion but MUST name `v2.0` as the removal target and point callers to `receive()` (in a
    loop) or the discovery loops as the replacement (e.g. "UdpTransport.receive_many is deprecated
    and will be removed in v2.0. Use receive() in a loop or _discover_with_packet() instead.").

    Add a `.. deprecated::` reStructuredText note to the `receive_many` docstring (after the
    existing Raises section) stating it is deprecated and will be removed in v2.0, pointing to
    `receive`. Do NOT modify any other line of the method body (D-09: body otherwise untouched).
    Australian-English spelling in the docstring note.
  </action>
  <verify>
    <automated>uv run pyright src/lifx/network/transport.py && uv run ruff check src/lifx/network/transport.py</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "import warnings" src/lifx/network/transport.py` returns 1.
    - `grep -c "DeprecationWarning" src/lifx/network/transport.py` returns >= 1 and the `warnings.warn` call uses `stacklevel=2`.
    - The receive_many docstring contains `.. deprecated::` and the string `v2.0`.
    - The `warnings.warn` call appears before the `if self._protocol is None:` guard inside receive_many.
    - `uv run pyright src/lifx/network/transport.py` reports 0 errors.
  </acceptance_criteria>
  <done>receive_many emits a stacklevel=2 DeprecationWarning naming v2.0 and has a `.. deprecated::` docstring note; body otherwise unchanged; Pyright clean.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Add DeprecationWarning test for receive_many</name>
  <files>tests/test_network/test_transport.py</files>
  <read_first>
    - tests/test_network/test_transport.py (existing receive_many tests — match their fixture/async style and how UdpTransport is opened in tests; the file currently has ~21 receive_many references which MUST remain — only ADD one test)
    - src/lifx/network/transport.py (the deprecated receive_many from Task 1 — exact warning message to match against)
    - .planning/phases/01-unify-duplicated-discovery-loops/01-PATTERNS.md (pytest.warns pattern lines 460–467)
    - .planning/phases/01-unify-duplicated-discovery-loops/01-RESEARCH.md (pytest.warns test for D-12 lines 534–543)
  </read_first>
  <behavior>
    - test_receive_many_emits_deprecation_warning: with an open UdpTransport, calling receive_many inside `pytest.warns(DeprecationWarning, match="v2.0")` succeeds (the warning is captured).
    - The new test does not interfere with the existing receive_many behaviour tests, all of which must continue to pass (D-10: receive_many tests stay until v2.0 removal).
  </behavior>
  <action>
    ADD a single async test `test_receive_many_emits_deprecation_warning` to
    `tests/test_network/test_transport.py`, placed near the existing receive_many tests and using
    the same fixture/transport-open convention already present in the file. Open a `UdpTransport`
    (matching the existing tests' setup) and assert the deprecation fires:
    `with pytest.warns(DeprecationWarning, match="v2.0"): await transport.receive_many(timeout=0.1)`.
    Use the test class/`@pytest.mark.asyncio` style already used in this file. Do NOT delete or
    modify any existing receive_many test (D-10). Ensure `pytest` is imported (it already is in
    this file).
  </action>
  <verify>
    <automated>uv run --frozen pytest tests/test_network/test_transport.py -v -k "deprecation or receive_many"</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "pytest.warns(DeprecationWarning" tests/test_network/test_transport.py` returns >= 1.
    - `grep -c "test_receive_many_emits_deprecation_warning" tests/test_network/test_transport.py` returns 1.
    - `uv run --frozen pytest tests/test_network/test_transport.py -v -k "deprecation"` passes.
    - `uv run --frozen pytest tests/test_network/test_transport.py -q` passes in full (all pre-existing receive_many tests still green — D-10).
    - `uv run ruff check tests/test_network/test_transport.py` reports no errors.
  </acceptance_criteria>
  <done>test_receive_many_emits_deprecation_warning passes via pytest.warns(DeprecationWarning, match="v2.0"); all existing transport tests still pass.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| (none new) | Deprecation signalling only; no change to network input handling |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-01-08 | (none — maintenance) | UdpTransport.receive_many | accept | Deprecation is a non-security maintenance change; the method body (including its existing oversized/undersized packet-drop DoS guards at lines 317–323) is untouched, so no protection is removed. The DeprecationWarning has no security impact. |

ASVS L1: No HIGH-severity items. No package installs (Package Legitimacy Gate N/A — stdlib only).
</threat_model>

<verification>
- `uv run pyright` reports 0 errors (strict).
- `uv run ruff check .` reports no errors.
- `uv run --frozen pytest tests/test_network/test_transport.py -q` passes (existing + new test).
</verification>

<success_criteria>
- receive_many emits a stacklevel=2 DeprecationWarning naming v2.0 with a `.. deprecated::` docstring note (D-09).
- One pytest.warns test asserts the warning (D-12); all existing receive_many tests remain green (D-10).
- Method body otherwise unchanged.
</success_criteria>

<artifacts_produced>
## Artifacts this phase produces (Plan 04)

- Modified `src/lifx/network/transport.py`:
  - `UdpTransport.receive_many` now calls `warnings.warn(..., DeprecationWarning, stacklevel=2)` and carries a `.. deprecated::` docstring note (D-09).
  - New import: `import warnings`.
- Modified `tests/test_network/test_transport.py`:
  - New test method `test_receive_many_emits_deprecation_warning` (D-12).
- No public API surface symbols added or renamed; `receive_many` signature preserved.
</artifacts_produced>

<output>
Create `.planning/phases/01-unify-duplicated-discovery-loops/01-04-SUMMARY.md` when done.
</output>
