---
phase: 05-reliability-documentation
plan: 05
type: execute
wave: 4
depends_on: ["05-04"]
files_modified:
  - docs/user-guide/troubleshooting.md
  - docs/user-guide/ceiling-lights.md
  - docs/faq.md
  - docs/user-guide/advanced-usage.md
  - docs/architecture/overview.md
  - docs/user-guide/animation.md
  - docs/api/animation.md
  - docs/api/network.md
  - CLAUDE.md
  - src/lifx/network/connection.py
  - src/lifx/network/discovery.py
  - src/lifx/api.py
autonomous: true
gap_closure: true
requirements: [DOCS-01, DOCS-02]
must_haves:
  truths:
    - "Every discovery example in docs/user-guide/troubleshooting.md consumes discover_devices() as the async generator it is: zero awaited-generator calls remain and all three sites (the network-connectivity check, diagnose_discovery, and the partial-discovery recipe) collect via async-for into a list before len()/truthiness/iteration — truth #23 closed per D5-16"
    - "The measure_latency example in troubleshooting.md imports asyncio before calling asyncio.gather"
    - "docs/user-guide/ceiling-lights.md's MatrixLight Compatibility example imports FirmwareEffect and calls get_device_chain() and set_effect(effect_type=FirmwareEffect.MORPH, speed=5.0) — every name resolves against src/lifx/devices/matrix.py (:378, :926) and the generated protocol_types, and the speed value is in seconds (D5-17)"
    - "No published page or repo instruction claims lock-based request serialisation: faq.md, advanced-usage.md, CLAUDE.md, the DeviceConnection class docstring (connection.py:64, D5-18) and the hand-written docs/api/network.md Concurrency section (D5-20) all describe the shipped model — a background receiver routing responses to per-request queues keyed by (source, sequence, serial)"
    - "docs/architecture/overview.md characterises request retries as retransmits on an escalating schedule (matching REQUEST_RETRANSMIT_GAPS, const.py:53-65) and its Device layer lists CeilingLight and ceiling.py, matching its own mermaid diagram (D5-17)"
    - "Every Colors reference in advanced-usage.md's three fixed examples is imported, and wave_effect awaits its colour-change tasks before returning — collected list + asyncio.gather, runnable on Python 3.10 (D5-17)"
    - "faq.md's wake-tail entry carries the on-healthy-networks qualifier matching the canonical troubleshooting.md section it links to (D5-17)"
    - "src/lifx/api.py:620's docstring example uses Colors.WARM — an existing preset (color.py:873); zero references to the non-existent preset remain anywhere in src/ (D5-18)"
    - "docs/api/network.md's Device Discovery example iterates discover_devices() with async-for, prints only fields DiscoveredDevice has (serial, ip, port — discovery.py:52-54), and carries no stale 3.0 timeout figure (D5-20)"
    - "src/lifx/network/discovery.py's create_device() docstring Example consumes discover_devices() with async-for and guards the Device | None return before use, so the Example is runnable even with an out-of-scope responder (e.g. a Switch) on the network — the third and final narrow D5-11 override (D5-20/D5-21)"
    - "docs/api/animation.md's Direct UDP Delivery bullets state the shipped contract — delivery paced against device acknowledgements, latest-frame-wins, no frame retries — replacing the pre-flow-control claims (D5-10 staleness mandate within the D5-18-widened hand-written-prose fence)"
    - "Australian English holds on every pinned line (D5-17): initialisation (api/animation.md:138), Binary Serialisation / UDP acknowledgement / Deserialise (overview.md), Performance Optimisation ×2 + Synchronised (advanced-usage.md), Normalise + vectorised (user-guide/animation.md), Optimised (CLAUDE.md:160) — plus the same-file companions pinned in the tasks"
    - "uv run zensical build exits 0 at the exact pre-existing 8-issue baseline (5× api/effects.md, 3× api/index.md); no issue names any edited page"
    - "uv run --frozen pytest passes — the three docstring-only source edits (connection.py:64, api.py:620, the discovery.py create_device Example) break nothing"
    - statement: "The rewritten discovery, wave-effect, correlation-model and animation-contract prose reads naturally in place, preserves every behavioural claim, introduces no factual claims beyond what the shipped source states, and names no internal tuning constants"
      verification: backstop
  artifacts:
    - path: "docs/user-guide/troubleshooting.md"
      provides: "Three runnable async-for discovery examples + asyncio import in measure_latency (D5-16, D5-17)"
      contains: "async for device in discover_devices("
    - path: "docs/user-guide/ceiling-lights.md"
      provides: "MatrixLight Compatibility example against the real matrix API + complete inherited-state list (D5-17)"
      contains: "FirmwareEffect"
    - path: "docs/faq.md"
      provides: "Correlation-model performance bullet + healthy-networks qualifier (D5-17)"
      contains: "Response Correlation"
    - path: "docs/user-guide/advanced-usage.md"
      provides: "Correlation-model claim, three complete import lines, awaited wave effect, AU headings (D5-17)"
      contains: "responses are correlated to each request"
    - path: "docs/architecture/overview.md"
      provides: "Escalating-schedule retry wording, CeilingLight/ceiling.py in Device layer, AU spellings (D5-17)"
      contains: "escalating schedule"
    - path: "docs/user-guide/animation.md"
      provides: "AU spelling on the RGB-conversion comment and the numpy example pointer (D5-17)"
      contains: "vectorised"
    - path: "docs/api/animation.md"
      provides: "Accurate ack-paced delivery contract in the hand-written Performance Characteristics prose (D5-10 × D5-18/D5-20)"
      contains: "paced against device acknowledgements"
    - path: "docs/api/network.md"
      provides: "Runnable async-for discovery example over real DiscoveredDevice fields + correlation-model Concurrency section (D5-20); ::: blocks untouched"
      contains: "async for device in discover_devices():"
    - path: "CLAUDE.md"
      provides: "Accurate concurrency-model bullet, flow.py in the Animation Layer list, AU spelling (D5-17)"
      contains: "(source, sequence, serial)"
    - path: "src/lifx/network/connection.py"
      provides: "DeviceConnection class docstring Features bullet describing the receiver/correlation model (D5-18)"
      contains: "routes each reply to its request"
    - path: "src/lifx/network/discovery.py"
      provides: "create_device() docstring Example consuming the generator with async-for (D5-20 — third narrow D5-11 override)"
      contains: "async for discovered in discover_devices():"
    - path: "src/lifx/api.py"
      provides: "filter_by_group docstring example using an existing Colors preset (D5-18)"
      contains: "set_color(Colors.WARM)"
  key_links:
    - from: "docs/user-guide/troubleshooting.md"
      to: "src/lifx/network/discovery.py"
      via: "every example consumes the AsyncGenerator return type with async-for, matching the shipped signature at discovery.py:477-485"
      pattern: "async for device in discover_devices\\("
    - from: "CLAUDE.md"
      to: "src/lifx/network/connection.py"
      via: "the concurrency bullet names the same correlation key the shipped _pending_requests dict uses (connection.py:147-152)"
      pattern: "source, sequence, serial"
    - from: "docs/user-guide/ceiling-lights.md"
      to: "src/lifx/devices/matrix.py"
      via: "the compatibility example calls only methods that exist on MatrixLight, with speed in seconds"
      pattern: "set_effect\\("
    - from: "docs/api/animation.md"
      to: "docs/user-guide/animation.md"
      via: "both pages state the same behavioural contract wording for ack-paced delivery"
      pattern: "paced against device acknowledgements"
    - from: "docs/api/network.md"
      to: "src/lifx/network/discovery.py"
      via: "the API-reference discovery example consumes the same async-generator shape the shipped signature declares, printing only real DiscoveredDevice fields"
      pattern: "async for device in discover_devices\\("
  prohibitions:
    - statement: "MUST NOT attribute the wire-reliability behaviour to any release number — neither the internal milestone name nor a predicted semantic-release version; python-semantic-release derives the version at release time, so any pin recreates the closed CR-01 (D5-12)"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT edit any source docstring other than the DeviceConnection Features bullet at src/lifx/network/connection.py:64, the preset name at src/lifx/api.py:620 (both D5-18), and the create_device() Example block in src/lifx/network/discovery.py (D5-20/D5-21) — the api.py:758 override is already spent under D5-13; D5-11 otherwise holds for every other docstring in src/"
      category: values
      requirement_id: DOCS-01
    - statement: "MUST NOT edit the create_device() docstring outside its Example: block — the Returns: section (which omits None) and the Raises: section (which promises exceptions the catch-all swallows) stay byte-identical, routed to API-01(d); rewriting Raises: to match the except-Exception-return-None code would document a swallow-everything catch as intended behaviour, the exact paper-over D5-19 prohibits (D5-21)"
      category: values
      requirement_id: DOCS-01
    - statement: "MUST NOT edit mkdocstrings-fed docs/api content (the ::: directive blocks and everything they render) or docs/changelog.md — only hand-written prose in docs/api/animation.md (D5-18) and docs/api/network.md (D5-20) is editable; every other docs/api page stays fenced even where it carries known defects (surfaced as findings, not fixed)"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT fix, re-document, or paper over the three deferred src/lifx/api.py behavioural defects (API-01 per D5-19) — no edits at api.py lines 260-331, 515-557, or 974-1016, and no docstring rewrites making the promises match the broken behaviour"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT restate internal flow-control or retry tuning constants (ack-probe placement, outstanding-ack gate threshold, ack expiry, retransmit gap values) in the rewritten correlation and pacing prose — behavioural contract only (D5-09)"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT frame latest-frame-wins drops as a defect or advise retrying a dropped frame in the rewritten docs/api/animation.md bullets (carried forward from 05-02/05-04)"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT change mkdocs.yml nav or add new pages (D5-01)"
      category: values
      requirement_id: DOCS-01
---

<objective>
Close the single remaining 05-VERIFICATION.md gap — goal-derived accuracy truth
#23: the phase-authored partial-discovery recipe at
docs/user-guide/troubleshooting.md:110 awaits `discover_devices()`, an async
generator (`src/lifx/network/discovery.py:477-485` returns
`AsyncGenerator[DiscoveredDevice, None]`), so the recommended recipe raises
TypeError. Fix it and its two pre-existing same-file companions (lines 27 and
70-84) as async-for collection per D5-16, then close the full operator-opted
residual set (D5-17: dead matrix APIs, the false lock-serialisation claims, the
backoff mischaracterisation, missing imports, the fire-and-forget wave effect,
missing CeilingLight/flow.py/tile_orientations entries, the healthy-networks
qualifier, and the AU spelling list), the two narrow D5-18 source overrides
(connection.py:64 and api.py:620), and the operator's D5-20 fence extension:
docs/api/network.md's two hand-written sections (the triply-broken discovery
example and the lock-claim Concurrency section) plus the create_device()
docstring Example in discovery.py — the third and final narrow D5-11 override.

Attribution note (load-bearing): line 110 was written by plan 05-01 (commit
015a112). The re-review's claim that 05-04 introduced it is factually wrong —
verified by `git log -L 110,110` and a zero-hit grep over the whole 05-04 diff.
There is no 05-04 regression to revert; this is an original-execution defect
that slipped both the first review and the first verification.

Every target value in this plan was re-verified against the shipped source
during planning (2026-07-17): the discover_devices signature and
DiscoveredDevice fields, get_device_chain/set_effect/FirmwareEffect
(matrix.py:378/:926, protocol_types.py:30-37, speed is seconds with default
3.0), the _pending_requests correlation model (connection.py:147-152),
Colors.WARM (color.py:873), MatrixLightState.tile_orientations
(matrix.py:283-289), set_color_zones(0, 8, color) (multizone.py:505), and
DeviceGroup.lights (api.py:153). Review findings were treated as leads only.

Purpose: the phase goal is *accurate* guidance; 36/37 truths already verify.
This plan removes the last broken recipe and the confirmed-real residuals so no
reader copy-pastes an example that raises TypeError/NameError/AttributeError,
and no page contradicts the shipped concurrency model.

Output: twelve files corrected in place. No new pages, no nav changes
(D5-01/D5-02 untouched), no behavioural code changes (D5-19 defects stay
deferred as API-01).

Decisions implemented: D5-16, D5-17, D5-18, D5-20, D5-21. Boundaries held:
D5-01, D5-09, D5-11 (outside the three narrow overrides), D5-12, D5-14 (as
widened by D5-18/D5-20 — mkdocstrings-fed content and the changelog stay
absolutely fenced), D5-19 (including the create_device Returns:/Raises:
sections, routed to API-01(d) per D5-21).
</objective>

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

<context>
@.planning/phases/05-reliability-documentation/05-CONTEXT.md
@.planning/phases/05-reliability-documentation/05-VERIFICATION.md
@.planning/phases/05-reliability-documentation/05-REVIEW.md
</context>

## Source coverage audit (second gap-closure pass)

| SOURCE   | ID     | Item                                                                 | Task | Status  |
|----------|--------|----------------------------------------------------------------------|------|---------|
| GOAL     | —      | Accurate guidance on guaranteed wire behaviour + quirks               | 1–3  | COVERED |
| REQ      | DOCS-01| Discovery/wake-tail guidance accuracy (truth #23 sites, measure_latency import, faq qualifier) | 1, 2 | COVERED |
| REQ      | DOCS-02| Streaming + concurrency-model accuracy (correlation claims, animation contract, flow.py entry) | 2, 3 | COVERED |
| RESEARCH | —      | All §Stale-Content findings #1–#13 + F1 delivered by 05-01..05-04; §Package Legitimacy Audit: no installs | —  | COVERED (prior plans) |
| CONTEXT  | D5-16  | Three troubleshooting.md discovery examples → async-for collection    | 1    | COVERED |
| CONTEXT  | D5-17  | Full doc-side residual set (every file:line pinned in the decision)   | 1–3  | COVERED |
| CONTEXT  | D5-18  | connection.py:64 + api.py:620 overrides; docs/api/animation.md hand-written prose editable | 3 | COVERED |
| CONTEXT  | D5-19  | Three api.py behavioural defects → deferred as API-01                 | —    | EXCLUDED (deferred; guarded by prohibition 4) |
| CONTEXT  | D5-20  | network.md discovery example (3 defects) + network.md Concurrency section + discovery.py create_device Example + the animation.md:131-134 authorisation | 3 | COVERED |
| CONTEXT  | D5-21  | create_device Example None guard; Returns:/Raises: fenced and routed to API-01(d) | 3 | COVERED |

No MISSING rows. Exclusions: D5-01–D5-15 were delivered by plans 05-01..05-04
and are not re-planned; Deferred Ideas (API-01) are out of scope by operator
decision.

## Flagged planner assumptions (spec-less edge probe — surfaced, not dropped)

No SPEC exists; the deterministic edge probe returned three `unresolved` rows.
Per the no-silent-drop rule each is surfaced (3 probe rows == 0 authored
resolutions + 3 flagged assumptions):

1. **DOCS-01 / idempotency** ("What happens if this runs twice on the same
   input?") — Planner assumption: every edit is an exact-string replacement
   gated by exact-count greps whose target text no longer exists post-edit, so
   re-execution is a detectable no-op. Flagged for verification.
2. **DOCS-01 / concurrency** ("If interrupted or run in parallel, what is
   guaranteed?") — Planner assumption: this is the only unexecuted plan in the
   phase (wave 4, all dependencies complete); tasks run sequentially with one
   atomic signed commit per task, and each task independently ends with a green
   `uv run zensical build`, so an interruption leaves a clean, buildable state.
   Flagged for verification.
3. **DOCS-02 / unclassified** ("unclassified — review manually") — Kept live,
   per the prior round's instruction: the verifier must independently probe the
   factual claims in every edited file rather than trust these truths — that
   exact probe is what surfaced truth #23 last round. This round the planner's
   own probe surfaced four additional items; the operator reviewed all four and
   authorised them via **D5-20** (05-CONTEXT.md, commit f7d4617):
   - **Fixed in Task 3 under D5-10 ∧ D5-18, explicitly ratified by D5-20:**
     docs/api/animation.md:131-134 — the Direct UDP Delivery bullets still
     describe the pre-flow-control layer (denying any acknowledgement use and
     claiming unpaced throughput), which Phase 4 made false.
   - **Fixed in Task 3 under D5-20's fence extension** (all pre-existing since
     initial commit 5d62622; the same two defect classes D5-16/D5-17 close):
     (a) docs/api/network.md:50-59 — the Device Discovery example awaits the
     async generator, prints a field DiscoveredDevice does not have, and
     carries the stale 3.0 timeout figure D5-13 corrected at api.py:758;
     (b) docs/api/network.md:63-84 — the hand-written Concurrency section
     repeating the disproven lock-serialisation claim this plan removes
     everywhere else; (c) src/lifx/network/discovery.py:78-84 — the
     create_device() docstring Example awaits discover_devices() then iterates
     (the third and final narrow D5-11 override).
   - **Fixed in Task 3 under D5-21** (operator-authorised after this finding
     was surfaced): the create_device() Example called get_label() on the
     result without a None guard, but create_device() returns Device | None —
     the None is reachable because discover_devices() yields every responder
     unfiltered (discovery.py:544), relay/button-only products raise in
     detection and are caught to None, and the catch-all
     (discovery.py:115-116) turns any transient failure into None. The Example
     now guards it. **Routed to API-01(d), byte-identical, NOT an open
     finding:** the same docstring's Returns: (omits None) and Raises:
     (promises exceptions the catch-all swallows) sections — correcting them
     requires first deciding whether the catch-all is intended behaviour,
     which is the D5-19-deferred code question.
   - **Observations only (cosmetic, left untouched):** the overview.md
     Discovery Process mermaid (lines ~300-318) shows an explicit `timeout=3.0`
     argument and a list-shaped outcome — a conceptual diagram, not copy-paste
     code; docs/api/network.md's single-connection example imports asyncio
     without using it and pairs an "automatically closes" comment with an
     explicit close() call.

Prohibition recall (spec-less §B): seven kept prohibitions authored
descriptor-less into `must_haves.prohibitions` above (the D5-12
version-neutrality constraint; the D5-11 override set — now exactly three
docstring lines per D5-18/D5-20; the widened-D5-14 fence — hand-written prose
in docs/api/animation.md and docs/api/network.md only, with the ::: blocks and
the changelog absolutely fenced; the D5-19 deferral guard; the D5-09
tuning-constant fence and the latest-frame-wins framing guard, both
re-triggered because this plan rewrites the exact prose they cover; the D5-01
nav fence). Canon-referral: no canon security/compliance candidates surfaced
for a docs-accuracy pass; nothing referred to /gsd-secure-phase.

<capability_checkpoints>
Dispositions for the detector runs performed at planning time (2026-07-17):

- **api-coverage** (`detected: true`): FALSE POSITIVE, same both signals. The
  matches are (a) the 05-04 plan's own prior false-positive disposition text
  and (b) lifx-async's own `Animator` API described in its own docs. No
  external API/SDK/service is integrated; no COVERAGE.md is warranted.
- **assumption-delta** (`detected: true` on "also" in the ROADMAP dependency
  note): FALSE POSITIVE — the identical signal 05-04 dispositioned. No
  singular→plural, required→optional, or derived→chosen transition exists in a
  docs-accuracy gap closure. Decision: `no-change`.
- **schema-gate**: NOT APPLICABLE — zero-dependency Python UDP library, no ORM
  schema files anywhere in the repo.
</capability_checkpoints>

<tasks>

<task type="auto">
  <name>Task 1: Close truth #23 — rewrite the three awaited-generator discovery examples as async-for collection (D5-16) + troubleshooting.md companions (D5-17)</name>
  <files>docs/user-guide/troubleshooting.md</files>
  <read_first>
    - .planning/phases/05-reliability-documentation/05-CONTEXT.md (### Second gap-closure amendments — D5-16 is the operative decision)
    - docs/user-guide/troubleshooting.md lines 20-115 (the three broken discovery sites), 219-228 (the page's own correct async-for pattern — the wording anchor), 236-262 (measure_latency), 474-479 (Next Steps links)
    - src/lifx/network/discovery.py lines 477-485 (discover_devices signature: async generator, params timeout and broadcast_address both exist) and lines 39-58 (DiscoveredDevice dataclass: `serial: str` and `ip: str` fields exist, so the diagnose_discovery prints are valid once iteration works)
  </read_first>
  <action>
    Four surgical edits to docs/user-guide/troubleshooting.md, all matching the
    page's own correct collection pattern at lines 223-225. Do not touch the
    wake-tail section, the Connection Drops solution, or any other example.

    1. Lines 27-28 (the "Devices not on same network" fenced block, 3-space
       indented): the example assigns the generator via await and then calls
       len(). Replace the single assignment line so the block body becomes:
       `   devices = []` / `   async for device in discover_devices(timeout=5.0):` /
       `       devices.append(device)` — the existing
       `   print(f"Found {len(devices)} devices")` line below stays unchanged,
       and the two import lines above stay unchanged.

    2. Lines 69-73 (diagnose_discovery): keep the `# Try with extended timeout`
       comment; the multi-line assignment becomes async-for collection with the
       two argument lines byte-identical:
       `    devices = []` / `    async for device in discover_devices(` /
       `        timeout=10.0,` / `        broadcast_address="255.255.255.255"` /
       `    ):` / `        devices.append(device)`.
       The `if not devices:` branch, the else-branch iteration, and both print
       loops below work unchanged once devices is a list (D5-16) — leave every
       other line of the function byte-identical.

    3. Line 110 (the Partial Device Discovery recipe — the failing truth): the
       one-line fenced block becomes:
       `devices = []` / `async for device in discover_devices(timeout=30.0):` /
       `    devices.append(device)`.
       The surrounding prose about the escalating re-broadcast schedule is
       correct and stays untouched.

    4. measure_latency (lines 238-241): the block presents itself as complete
       but calls asyncio.gather at line 259 without importing asyncio. Insert
       `import asyncio` as a new first line of the fenced block, above
       `import time` (imports at top, per project rule). Nothing else in the
       block changes.

    5. AU companion (planner-added under the project Australian-English rule,
       same file): line 477's Next Steps link description becomes exactly
       `- [Advanced Usage](advanced-usage.md) — Optimisation patterns`.

    Commit with `git commit -s` (GPG-signed, automatic).
  </action>
  <verify>
    <automated>! grep -qF 'await discover_devices(' docs/user-guide/troubleshooting.md && test "$(grep -cF 'async for device in discover_devices(' docs/user-guide/troubleshooting.md)" = "3" && test "$(grep -cF 'devices = []' docs/user-guide/troubleshooting.md)" = "6" && grep -qF 'if not devices:' docs/user-guide/troubleshooting.md && test "$(grep -cF 'import asyncio' docs/user-guide/troubleshooting.md)" = "6" && grep -qF 'Optimisation patterns' docs/user-guide/troubleshooting.md && uv run zensical build</automated>
  </verify>
  <acceptance_criteria>
    - `grep -cF 'await discover_devices(' docs/user-guide/troubleshooting.md` outputs 0 (was 3: lines 27, 70, 110)
    - `grep -cF 'async for device in discover_devices(' docs/user-guide/troubleshooting.md` outputs 3 (one per fixed site)
    - `grep -cF 'devices = []' docs/user-guide/troubleshooting.md` outputs 6 (was 3 — the three existing correct examples at lines 48, 54, 223 plus the three fixes)
    - `grep -cF 'Found {len(devices)} devices' docs/user-guide/troubleshooting.md` outputs 3, unchanged — every len() call now operates on a list
    - `grep -cF 'if not devices:' docs/user-guide/troubleshooting.md` outputs 1, unchanged — truthiness now operates on a list
    - `grep -cF 'import asyncio' docs/user-guide/troubleshooting.md` outputs 6 (was 5) — measure_latency's gather now resolves
    - `grep -cF 'Optimisation patterns' docs/user-guide/troubleshooting.md` outputs 1 and `grep -cF 'Optimization patterns' docs/user-guide/troubleshooting.md` outputs 0
    - `uv run zensical build` exits 0
  </acceptance_criteria>
  <done>All three discovery examples on the page collect via async-for exactly as the page's own correct example does; the recommended partial-discovery recipe is runnable; no example on the page awaits an async generator; measure_latency's imports are complete.</done>
</task>

<task type="auto">
  <name>Task 2: D5-17 residual closure across the four prose pages — dead matrix APIs, false serialisation claims, backoff wording, imports, wave effect, omissions, AU headings</name>
  <files>docs/user-guide/ceiling-lights.md, docs/faq.md, docs/user-guide/advanced-usage.md, docs/architecture/overview.md</files>
  <read_first>
    - .planning/phases/05-reliability-documentation/05-CONTEXT.md (D5-17 — every item pinned by file:line)
    - docs/user-guide/ceiling-lights.md lines 157-171 (inherited-state list) and 254-274 (MatrixLight Compatibility example)
    - docs/faq.md lines 190-200 (performance bullets) and 244-254 (wake-tail entry)
    - docs/user-guide/advanced-usage.md lines 5-15 (TOC), 224-231 (Connection Management benefits), 336-355 (best_effort_control), 366-410 (both capability blocks), 428-475 (Synchronised heading + wave_effect + Performance heading)
    - docs/architecture/overview.md lines 112-142 (Protocol layer bullets + example comment), 143-158 (Network layer bullets), 169-186 (Device layer), 275-296 (Sending a Command mermaid), 392-412 (Concurrency Model — the correct correlation-model wording anchor)
    - src/lifx/devices/matrix.py lines 274-295 (MatrixLightState fields: chain, tile_orientations, tile_colors, tile_count, effect), line 378 (get_device_chain), lines 926-935 (set_effect signature: effect_type: FirmwareEffect, speed: float = 3.0 — SECONDS)
    - src/lifx/protocol/protocol_types.py lines 30-38 (FirmwareEffect enum: OFF, MOVE, MORPH, FLAME, SKY — the generator merged the old tile enum away, generator.py:302-338)
    - src/lifx/network/connection.py lines 143-153 (the shipped model: _pending_requests dict keyed (source, sequence, serial), background receiver task)
  </read_first>
  <action>
    Twelve pinned edits, each verified against the shipped source in read_first.
    Fix exactly what is pinned; do not restyle surrounding code.

    ceiling-lights.md:
    1. Line 262: the chain query becomes exactly
       `    device_chain = await ceiling.get_device_chain()` (the real method,
       matrix.py:378; the old name has zero definitions in src/).
    2. Line 268: the import becomes exactly
       `    from lifx.protocol.protocol_types import FirmwareEffect` (the
       generator merges the old tile-effect enum into FirmwareEffect).
    3. Lines 269-272: the effect call becomes exactly
       `    await ceiling.set_effect(` / `        effect_type=FirmwareEffect.MORPH,` /
       `        speed=5.0,  # seconds` / `    )` — set_effect's speed is a
       float in SECONDS with default 3.0 (matrix.py:926-935), so the old
       5000 value was also wrong by three orders of magnitude. Lines 259-261
       and 264-267 stay byte-identical (get_all_tile_colors and
       set_matrix_colors are real: matrix.py:543, :776).
    4. Line 170: insert `tile_orientations` after `chain` so the inherited
       list reads: `` `chain`, `tile_orientations`, `tile_colors`,
       `tile_count`, `effect` `` — matching the dataclass field order
       (matrix.py:283-287).

    faq.md:
    5. Line 199: the performance bullet becomes exactly
       `- **Response Correlation**: Per-request response routing prevents response mixing on the same connection`
       (the lock-based claim it replaces is false — zero such lock in src/;
       the shipped model is the receiver + per-request queues,
       connection.py:147-152).
    6. Lines 246-248 (wake-tail entry): add the healthy-networks qualifier the
       canonical troubleshooting section carries. Rewrap the paragraph so the
       sentence reads: `On healthy networks, zero packets are lost — it is a
       small, bounded latency cost, not a reliability problem.` with the
       phrase `On healthy networks, zero packets are lost` kept on a single
       line (required for the gate): end the previous line at
       `...minute or more of inactivity.` and start the next line with
       `On healthy networks, zero packets are lost — it is a small, bounded latency`
       continuing `cost, not a reliability problem.` on the line after.

    advanced-usage.md:
    7. Line 229: the Benefits bullet becomes exactly
       `- Concurrent requests are supported: responses are correlated to each request, so they never mix`
       — this also resolves the page's self-contradiction with its own
       asyncio.gather demo at lines 241-253.
    8. Line 338: the import becomes exactly
       `from lifx import discover, DeviceGroup, Colors, LifxError`
       (best_effort_control uses Colors.GREEN at line 351).
    9. Line 369: the import becomes exactly `from lifx import Colors, Light`
       (check_capabilities uses Colors.BLUE at line 380).
    10. Line 392: the import becomes exactly
        `from lifx import discover, DeviceGroup, Colors`
        (capability_aware_control uses Colors.PURPLE and Colors.RED; its
        set_color_zones(0, 8, Colors.RED) call matches the shipped signature,
        multizone.py:505-512, and stays untouched).
    11. Lines 462-466 (wave_effect): the fire-and-forget loop discards its
        tasks and returns, so under asyncio.run() the loop closes before any
        colour change and the unreferenced tasks may be GC'd mid-flight.
        Replace the for-loop (keeping everything above and the
        delayed_color_change helper below byte-identical) with:
        `    # Each device changes colour with a delay` / `    tasks = [` /
        `        delayed_color_change(device, Colors.BLUE, delay=i * 0.3)` /
        `        for i, device in enumerate(group.devices)` / `    ]` /
        `    await asyncio.gather(*tasks)`.
        Collected-list + gather chosen over a TaskGroup (D5-17 allows either)
        because the project supports Python 3.10 and TaskGroup is 3.11+.
    12. AU headings: line 14 (TOC) becomes exactly
        `- [Performance Optimisation](#performance-optimisation)`; line 430
        becomes exactly `### Synchronised Multi-Device Effects`; line 473
        becomes exactly `## Performance Optimisation`. Anchor renames are
        safe: zero inbound links to either old anchor exist anywhere in docs/
        or mkdocs.yml (verified by grep at planning time). The
        `synchronized_flash` function name at line 436 is a code identifier —
        leave it.

    overview.md:
    13. Line 150: the retry bullet becomes exactly
        `- **Retry Logic**: Automatic retransmits on an escalating schedule within each request's timeout`
        — REQUEST_RETRANSMIT_GAPS (const.py:53-65) is a stepped escalating
        schedule, and the phase's own prose elsewhere already says
        "escalating schedule". Do not name the gap values (D5-09).
    14. Line 173: the Device Types bullet gains CeilingLight at the end:
        `- **Device Types**: Base, Light, HevLight, InfraredLight, MultiZoneLight, MatrixLight, CeilingLight`
        and the Key Files list gains, after the matrix.py line (185):
        `` - `ceiling.py` - CeilingLight class `` — the page's own mermaid
        (line 36) already shows CeilingLight.
    15. AU spellings (pinned items plus two same-word same-page companions
        under the project rule, both in blocks this task already edits or
        their immediate neighbours): line 118 becomes
        `- **Binary Serialisation**: Pack/unpack protocol messages`; the code
        comment at line 139 becomes `# Serialise to bytes`; mermaid line 288
        becomes `    Connection->>Connection: Serialise packet`; line 291
        becomes `    Device-->>Transport: UDP acknowledgement`; line 293
        becomes `    Connection-->>Connection: Deserialise packet`.

    Australian English in every touched line. Commit with `git commit -s`.
  </action>
  <verify>
    <automated>! grep -qE 'get_tile_chain|set_tile_effect|TileEffectType' docs/user-guide/ceiling-lights.md && test "$(grep -cF 'FirmwareEffect' docs/user-guide/ceiling-lights.md)" = "2" && grep -qF 'get_device_chain()' docs/user-guide/ceiling-lights.md && grep -qF 'speed=5.0' docs/user-guide/ceiling-lights.md && grep -qF 'tile_orientations' docs/user-guide/ceiling-lights.md && ! grep -qF 'Request Serialization' docs/faq.md && grep -qF 'Response Correlation' docs/faq.md && grep -qF 'On healthy networks, zero packets are lost' docs/faq.md && ! grep -qF 'Zero packets' docs/faq.md && ! grep -qF 'Requests are serialized' docs/user-guide/advanced-usage.md && grep -qF 'responses are correlated to each request' docs/user-guide/advanced-usage.md && test "$(grep -cE '^from lifx import .*Colors' docs/user-guide/advanced-usage.md)" = "8" && ! grep -qF 'asyncio.create_task' docs/user-guide/advanced-usage.md && test "$(grep -cF 'await asyncio.gather(*tasks)' docs/user-guide/advanced-usage.md)" = "2" && test "$(grep -cF 'Performance Optimisation' docs/user-guide/advanced-usage.md)" = "2" && grep -qF '#performance-optimisation' docs/user-guide/advanced-usage.md && grep -qF 'Synchronised Multi-Device Effects' docs/user-guide/advanced-usage.md && ! grep -qF 'exponential backoff' docs/architecture/overview.md && grep -qF 'escalating schedule' docs/architecture/overview.md && test "$(grep -cF 'CeilingLight' docs/architecture/overview.md)" = "3" && grep -qF 'ceiling.py' docs/architecture/overview.md && ! grep -qE 'Serializ|Deserializ|acknowledgment' docs/architecture/overview.md && uv run zensical build</automated>
  </verify>
  <acceptance_criteria>
    - `grep -cE 'get_tile_chain|set_tile_effect|TileEffectType' docs/user-guide/ceiling-lights.md` outputs 0 (was 4 hits across lines 262, 268, 269, 270); the migration guide docs/migration/effect-api-changes.md keeps its old-API mentions untouched — gates are scoped to ceiling-lights.md only
    - `grep -cF 'FirmwareEffect' docs/user-guide/ceiling-lights.md` outputs 2 (import + MORPH member); `grep -cF 'get_device_chain()'` outputs 1; `grep -cF 'set_effect('` outputs 1; `grep -cF 'speed=5.0'` outputs 1 and `grep -cF 'speed=5000'` outputs 0
    - `grep -cF 'tile_orientations' docs/user-guide/ceiling-lights.md` outputs 1, matching MatrixLightState's real fields
    - `grep -cF 'Request Serialization' docs/faq.md` outputs 0 (was 1); `grep -cF 'Response Correlation' docs/faq.md` outputs 1
    - `grep -cF 'On healthy networks, zero packets are lost' docs/faq.md` outputs 1; `grep -cF 'Zero packets' docs/faq.md` outputs 0 (case-sensitive — the unqualified absolute is gone)
    - `grep -cF 'Requests are serialized' docs/user-guide/advanced-usage.md` outputs 0 (was 1); `grep -cF 'responses are correlated to each request'` outputs 1
    - `grep -cE '^from lifx import .*Colors' docs/user-guide/advanced-usage.md` outputs 8 (was 5 — the three fixed examples now import Colors); `grep -cF 'from lifx import discover, DeviceGroup, Colors, LifxError'` outputs 1; `grep -cF 'from lifx import Colors, Light'` outputs 1
    - `grep -cF 'asyncio.create_task' docs/user-guide/advanced-usage.md` outputs 0 (was 1); `grep -cF 'await asyncio.gather(*tasks)'` outputs 2 (was 1) — wave_effect now awaits its tasks
    - `grep -cF 'Performance Optimisation' docs/user-guide/advanced-usage.md` outputs 2 (TOC + heading) and `grep -cF 'Performance Optimization'` outputs 0; `grep -cF '#performance-optimisation'` outputs 1; `grep -cF 'Synchronised Multi-Device Effects'` outputs 1 and `grep -cF 'Synchronized Multi-Device Effects'` outputs 0
    - `grep -cF 'exponential backoff' docs/architecture/overview.md` outputs 0 (was 1); `grep -cF 'escalating schedule' docs/architecture/overview.md` outputs 1 — troubleshooting.md's resilient_operation docstring legitimately keeps its own exponential wording (it genuinely doubles) and is untouched
    - `grep -cF 'CeilingLight' docs/architecture/overview.md` outputs 3 (mermaid + Device Types bullet + Key Files); `grep -cF 'ceiling.py'` outputs 1
    - `grep -cE 'Serializ|Deserializ|acknowledgment' docs/architecture/overview.md` outputs 0 (was 5); positives each output 1: `grep -cF 'Binary Serialisation'`, `grep -cF 'Serialise to bytes'`, `grep -cF 'Serialise packet'`, `grep -cF 'UDP acknowledgement'`, `grep -cF 'Deserialise packet'`
    - `uv run zensical build` exits 0 (the two renamed advanced-usage anchors have zero inbound links, so no new issue may appear)
  </acceptance_criteria>
  <done>The ceiling example calls only APIs that exist with a seconds-valued speed; every lock-serialisation claim in the prose pages is replaced with the shipped correlation model; the retry wording matches the escalating schedule the library ships; all three Colors-using examples import Colors; wave_effect completes its colour changes under asyncio.run(); the Device layer prose matches the page's own diagram; AU spelling holds on every pinned line.</done>
</task>

<task type="auto">
  <name>Task 3: D5-18/D5-20 source overrides + docs/api hand-written prose (network.md, animation.md) + CLAUDE.md concurrency model + fences and full suite</name>
  <files>CLAUDE.md, docs/api/animation.md, docs/api/network.md, docs/user-guide/animation.md, src/lifx/network/connection.py, src/lifx/network/discovery.py, src/lifx/api.py</files>
  <read_first>
    - .planning/phases/05-reliability-documentation/05-CONTEXT.md (D5-18 and D5-20 — the three narrow overrides and the widened D5-14 fence; D5-19 — what must NOT be touched in api.py)
    - CLAUDE.md lines 154-163 (Animation Layer file list; line 160 is the US-spelled bullet) and 242-249 (Concurrency Considerations; line 244 is the false bullet)
    - docs/api/animation.md lines 1-10 (hand-written intro) and 125-152 (hand-written Performance Characteristics section; lines 129-134 are the stale pre-flow-control bullets, line 138 the US spelling; everything expressed as ::: directives above line 125 is mkdocstrings-fed and absolutely fenced)
    - docs/api/network.md in full — 138 lines; the ::: mkdocstrings blocks sit at lines 9, 15, 25, 36 and 111 and are absolutely fenced; the editable hand-written spans are the Device Discovery example (lines 48-60) and the Concurrency section (lines 63-84); the two request examples' packet classes are real (Device.GetLabel packets.py:126, Light.GetColor :527, Light.GetPower :602)
    - docs/user-guide/animation.md lines 178-190 (RGB-conversion comment at 186), 305-310 (numpy pointer at 308), and 326-336 (the streaming contract paragraph — the wording anchor for the api-page bullets; do not edit it)
    - src/lifx/network/connection.py lines 55-70 (the Features docstring list; line 64 is the false bullet) and 143-155 (the shipped model: _pending_requests keyed (source, sequence, serial), background receiver task)
    - src/lifx/network/discovery.py lines 39-58 (DiscoveredDevice fields: serial, ip, port, timeout, max_retries, first_seen, response_time — there is no label field) and 60-120 (create_device(); the docstring Example at lines 78-84 is the D5-20/D5-21 override target; the method returns Device | None because relay/button-only products raise in detection and are caught to None, and the except-Exception catch at ~115-116 turns any transient failure into None; the docstring's Returns: and Raises: sections are FENCED — routed to API-01(d))
    - src/lifx/api.py lines 610-625 (filter_by_group docstring example; line 620 is the broken preset)
    - src/lifx/color.py lines 869-880 (Colors.WARM exists at 873)
    - src/lifx/animation/flow.py lines 1-25 (the behavioural contract the api-page bullets must state: ack-gated pacing, gated frames dropped not queued, never retried — do NOT copy its tuning constants into prose, D5-09)
  </read_first>
  <action>
    Eleven edits: three authorised source docstring lines, three CLAUDE.md
    lines, two docs/api/animation.md and two docs/api/network.md
    hand-written-prose edits, two AU comment fixes.

    src (the ONLY three permitted source edits — D5-18 plus the D5-20 third
    override):
    1. src/lifx/network/connection.py line 64: the Features bullet claiming
       lock-style serialisation is the same false claim as the prose sites and
       renders into the published API reference. The bullet becomes exactly:
       `    - Response correlation: a background receiver routes each reply to its request, so concurrent requests never mix`
       (4-space indent matching the sibling bullets). Touch nothing else in
       the file.
    2. src/lifx/api.py line 620: the docstring example calls a preset that
       does not exist (its only occurrence repo-wide; AttributeError if run).
       The line becomes exactly:
       `            await bedroom.set_color(Colors.WARM)`
       — Colors.WARM exists (color.py:873). Touch nothing else in the file;
       in particular the D5-19-deferred defects at lines 260-331, 515-557 and
       974-1016 stay byte-identical (API-01).
    3. src/lifx/network/discovery.py (the create_device() docstring Example —
       the D5-20 third override, extended to the Example body by D5-21): the
       Example assigns the generator via await, iterates the assigned name,
       and uses the Device | None result unguarded. Two changes inside the
       Example block only:
       - lines 80-81 collapse into one:
         `            async for discovered in discover_devices():`
       - a None guard goes between the unchanged create_device line and the
         unchanged print line, so the Example is runnable even with an
         out-of-scope responder (e.g. a Switch) on the network:
         `                if device is None:` /
         `                    continue  # unsupported product or transient failure`
       The create_device call and the print line stay byte-identical (body
       indent 16 spaces; the continue sits at 20). Touch nothing else in the
       file — the same docstring's Returns: and Raises: sections stay
       byte-identical (routed to API-01(d) per D5-21; see the prohibition).

    CLAUDE.md:
    4. Line 244: the first Concurrency Considerations bullet names a lock that
       has zero occurrences in src/. It becomes exactly:
       `- Concurrent requests on a single connection are supported: a background receiver task routes each response to its request via per-request queues keyed by (source, sequence, serial), so responses never mix`
       — matching connection.py:147-152 and overview.md's Concurrency Model
       section. The neighbouring bullets (async generators, sequence
       allocation, no rate limiting) are accurate and stay unchanged.
    5. Animation Layer file list: insert after the animator.py bullet
       (line 156) a new bullet, matching overview.md:211's description:
       `   - \`flow.py\`: Ack-gated flow control`
       (3-space indent matching the sibling bullets; flow.py exists on disk).
    6. Line 160 becomes exactly:
       `   - Optimised for high-frequency frame delivery (up to ~20 FPS) for real-time effects`

    docs/api/animation.md (hand-written prose only — the D5-18-widened fence;
    the ::: mkdocstrings blocks and docs/changelog.md stay absolutely fenced):
    7. Line 138 becomes exactly:
       `Packets are constructed once at initialisation:`
    8. Lines 131-134 (the four Direct UDP Delivery bullets): they describe the
       pre-flow-control animation layer — the first bullet denies any
       acknowledgement use and the third claims unpaced throughput, both made
       false by the shipped ack-gated pacing (flow.py). This is a
       v1.1-staleness fix under D5-10's standing mandate, executable now that
       D5-18 makes this file's hand-written prose editable
       (planner-surfaced item — flagged in the plan preamble). The intro line
       129 ("bypasses the connection layer entirely") is still true and stays.
       The bullets become exactly:
       `- Frame packets are sent via a raw UDP socket; \`send_frame()\` never blocks` /
       `- Delivery is paced against device acknowledgements internally — when a device falls behind, new frames are dropped, never queued (latest-frame-wins)` /
       `- A dropped frame is never retried; occasional frame loss under saturation is by design (visual artefacts are brief)`
       (three bullets replace four). Wording anchor: the user-guide streaming
       contract paragraph. Do not name the gate threshold, probe placement, or
       ack expiry (D5-09); do not frame drops as a defect.

    docs/user-guide/animation.md (AU, D5-17):
    9. Line 186 becomes exactly `    # Normalise to 0-1` and line 308's
       pointer becomes `For a complete example including vectorised RGB to HSBK conversion, see`
       (the linked filename examples/animation_numpy.py is an identifier —
       unchanged).

    docs/api/network.md (hand-written prose only — the D5-20 fence extension;
    the five ::: blocks at lines 9, 15, 25, 36 and 111 are absolutely fenced
    and must remain byte-identical):
    10. Device Discovery example (lines 50-59) — all three defects in one
        pass: the example awaits the async generator, prints a field
        DiscoveredDevice does not have, and passes the stale 3.0 timeout
        figure D5-13 corrected at api.py:758. Keep the import line, the
        `async def main():` line and the `# Discover all devices on the
        network` comment; the function body becomes exactly:
        `    async for device in discover_devices():` /
        `        print(f"Found: {device.serial} at {device.ip}")` /
        `        print(f"  Port: {device.port}")`
        — direct iteration of the generator, no explicit timeout argument
        (the shipped default is DISCOVERY_TIMEOUT, 15.0 s — naming any figure
        here risks re-staling), and only fields DiscoveredDevice defines
        (serial, ip, port — discovery.py:52-54).
    11. Concurrency section (lines 63-84): the heading, the intro sentence and
        the in-example comment all repeat the disproven lock claim. Keep the
        fenced example otherwise byte-identical (its three request lines call
        real packet classes — verified in read_first). Apply exactly:
        - the heading becomes `### Requests on a Single Connection`
        - the intro sentence becomes
          `` Each `DeviceConnection` correlates responses to their requests — a background receiver routes each reply to the request that sent it, so requests on one connection never mix: ``
        - the in-example comment becomes
          `    # Requests on the same connection (responses correlated per request)`
        The heading anchor rename is safe: zero inbound links to the old
        anchor exist anywhere in docs/ or mkdocs.yml (verified by grep at
        planning time). The unused asyncio import and the "automatically
        closes" comment in this example are pre-existing cosmetics outside the
        two authorised defect classes — leave them byte-identical.

    Australian English throughout. Commit with `git commit -s` — this task's
    commit is the only one touching src/, so the fence gates below inspect it
    directly.
  </action>
  <verify>
    <automated>! grep -qF '_request_lock' CLAUDE.md && grep -qF '(source, sequence, serial)' CLAUDE.md && grep -qF 'flow.py' CLAUDE.md && grep -qF 'Optimised for high-frequency' CLAUDE.md && ! grep -qF 'Request serialization' src/lifx/network/connection.py && grep -qF 'routes each reply to its request' src/lifx/network/connection.py && test "$(grep -rl 'WARM_WHITE' src/ | wc -l | tr -d ' ')" = "0" && grep -qF 'set_color(Colors.WARM)' src/lifx/api.py && ! grep -qF 'await discover_devices' src/lifx/network/discovery.py && grep -qF 'async for discovered in discover_devices():' src/lifx/network/discovery.py && grep -qF 'if device is None:' src/lifx/network/discovery.py && grep -qF 'unsupported product or transient failure' src/lifx/network/discovery.py && ! grep -qF 'timeout=3.0' docs/api/network.md && ! grep -qF 'device.label' docs/api/network.md && ! grep -qF 'Request Serialization' docs/api/network.md && ! grep -qF 'serialized by internal lock' docs/api/network.md && grep -qF 'async for device in discover_devices():' docs/api/network.md && grep -qF 'device.port' docs/api/network.md && grep -qF 'Requests on a Single Connection' docs/api/network.md && test "$(grep -c '^::: lifx' docs/api/network.md)" = "5" && ! git diff 7baf7dd -- docs/api/network.md | grep -qE '^[+-]:::' && ! grep -q 'initialization' docs/api/animation.md && ! grep -qF 'No ACKs' docs/api/animation.md && ! grep -qF 'Maximum throughput' docs/api/animation.md && grep -qF 'paced against device acknowledgements' docs/api/animation.md && grep -qF 'visual artefacts' docs/api/animation.md && grep -qF '# Normalise to 0-1' docs/user-guide/animation.md && grep -qF 'vectorised RGB' docs/user-guide/animation.md && uv run --frozen pytest -q && OUT=$(uv run zensical build 2>&1) && echo "$OUT" | grep -qF '8 issues found' && ! echo "$OUT" | grep -qE 'user-guide/(troubleshooting|ceiling-lights|advanced-usage|animation)\.md|faq\.md|architecture/overview\.md|api/(animation|network)\.md'</automated>
  </verify>
  <acceptance_criteria>
    - `grep -cF '_request_lock' CLAUDE.md` outputs 0 (was 1); `grep -cF '(source, sequence, serial)' CLAUDE.md` outputs 1; `grep -cF 'flow.py' CLAUDE.md` outputs 1 (was 0); `grep -cF 'Optimised for high-frequency' CLAUDE.md` outputs 1 and `grep -cF 'Optimized for high-frequency'` outputs 0
    - `grep -cF 'Request serialization' src/lifx/network/connection.py` outputs 0 (was 1); `grep -cF 'routes each reply to its request' src/lifx/network/connection.py` outputs 1
    - `grep -rl 'WARM_WHITE' src/ | wc -l` outputs 0 (was 1 file); `grep -cF 'set_color(Colors.WARM)' src/lifx/api.py` outputs 1
    - `grep -cF 'await discover_devices' src/lifx/network/discovery.py` outputs 0 (was 1); `grep -cF 'async for discovered in discover_devices():' src/lifx/network/discovery.py` outputs 1; `grep -cF 'if device is None:' src/lifx/network/discovery.py` outputs 1 (was 0) and `grep -cF 'unsupported product or transient failure'` outputs 1 (was 0) — the D5-21 guard is in place; the Example's create_device call and print line are byte-identical
    - The create_device docstring's Returns: and Raises: sections are byte-identical (routed to API-01(d) per D5-21): `git diff 7baf7dd -- src/lifx/network/discovery.py` contains no `+` or `-` line matching `Returns:`, `Raises:`, `LifxDeviceNotFoundError`, `LifxTimeoutError`, or `LifxProtocolError`
    - `git show --stat HEAD -- src/` after this task's commit shows exactly three files — src/lifx/network/connection.py (1 insertion, 1 deletion), src/lifx/api.py (1 insertion, 1 deletion) and src/lifx/network/discovery.py (3 insertions, 2 deletions — the two-line collapse plus the two guard lines): the D5-11 override budget (D5-18 + D5-20/D5-21) exercised exactly once per site, nothing else in src/ touched
    - `grep -cF 'timeout=3.0' docs/api/network.md` outputs 0 (was 1); `grep -cF 'device.label' docs/api/network.md` outputs 0 (was 1); `grep -cF 'async for device in discover_devices():' docs/api/network.md` outputs 1; `grep -cF 'device.port' docs/api/network.md` outputs 1 — the API-reference discovery example is triply fixed (D5-20)
    - `grep -cF 'Request Serialization' docs/api/network.md` outputs 0 (was 1); `grep -cF 'serialized by internal lock' docs/api/network.md` outputs 0 (was 1); `grep -cF 'Requests on a Single Connection' docs/api/network.md` outputs 1; `grep -cF 'responses correlated per request' docs/api/network.md` outputs 1; the three `await conn.request(` lines are byte-identical
    - The mkdocstrings fence held: `grep -c '^::: lifx' docs/api/network.md` outputs 5 and `git diff 7baf7dd..HEAD -- docs/api/network.md | grep -cE '^[+-]:::'` outputs 0 — no ::: directive line added, removed, or modified
    - `grep -c 'initialization' docs/api/animation.md` outputs 0 (was 1); `grep -cF 'once at initialisation'` outputs 1
    - The Direct UDP Delivery list has exactly three bullets: `grep -cF 'paced against device acknowledgements' docs/api/animation.md` outputs 1; `grep -cF 'never queued (latest-frame-wins)'` outputs 1; `grep -cF 'visual artefacts'` outputs 1 and `grep -cF 'visual artifacts'` outputs 0; the two stale claims (the no-acknowledgements bullet and the unpaced-throughput bullet) each grep to 0
    - `git diff 7baf7dd..HEAD --name-only -- docs/api docs/changelog.md` (7baf7dd = HEAD at second-revision planning time; verified — every commit between e303d5d and 7baf7dd touches only .planning/ files) outputs exactly `docs/api/animation.md` and `docs/api/network.md`; mkdocs.yml is untouched (`git diff 7baf7dd..HEAD --name-only -- mkdocs.yml` is empty)
    - `grep -cF '# Normalise to 0-1' docs/user-guide/animation.md` outputs 1; `grep -cF 'vectorised RGB'` outputs 1; both US-spelled originals grep to 0
    - `uv run --frozen pytest -q` exits 0 (three docstring-only source changes break nothing)
    - `uv run zensical build` exits 0 reporting exactly the pre-existing 8-issue baseline (5× api/effects.md, 3× api/index.md); no issue line names any page edited by this plan — api/network.md is NOT in the baseline and must not join it; if the total differs from 8, every extra issue must be triaged before the task may complete
  </acceptance_criteria>
  <done>The rendered API reference no longer contradicts the prose this phase fixed: the DeviceConnection docstring, the network.md Concurrency section, CLAUDE.md, faq.md and advanced-usage.md all state the same shipped correlation model; the API-reference discovery examples (network.md and the create_device docstring) consume the async generator correctly over real fields with no stale timeout figure; the filter_by_group example runs; the animation API page states the ack-paced latest-frame-wins contract in Australian English; the three fence overrides were exercised exactly once each with the ::: blocks byte-identical; the full test suite and the docs build are green at baseline.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| published docs → reader behaviour | Fixed examples get copy-pasted and run against real devices; a recipe that raises TypeError erodes trust in the reliability guidance itself |
| repo instructions (CLAUDE.md) → agent/contributor behaviour | A false concurrency-model bullet steers future code and docs work toward a lock that does not exist |
| source docstrings → rendered API reference | connection.py:64 and api.py:620 render into the published site; leaving them contradicts the pages this plan fixes |

## STRIDE Threat Register

ASVS L1, block on high. Honest assessment: a documentation phase editing
markdown plus three docstring lines creates no new attack surface — no auth,
session, input-validation, or crypto artefact is touched. The realistic threat
is publishing inaccurate guidance that leads users into broken or wasteful
patterns.

| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-05-09 | Information Disclosure | docs examples (troubleshooting, ceiling-lights, faq, advanced-usage) | low | mitigate | Broken examples replaced with source-verified API usage — every target value re-traced to the shipped signature during planning; no secrets or credentials appear in any example |
| T-05-10 | Tampering | repo instruction accuracy (CLAUDE.md concurrency model) | low | mitigate | The bullet now matches connection.py's shipped receiver/correlation model, verified against _pending_requests at planning time |
| T-05-11 | Denial of Service | reader-side device flooding | low | mitigate | No FPS or polling figure changes anywhere; the api/animation.md rewrite states pacing and drops, lowering (not raising) throughput expectations; the async-for fixes add no polling loops |
| T-05-SC | Tampering | npm/pip/cargo installs | low | accept | No package-manager installs occur in this phase (RESEARCH.md §Package Legitimacy Audit: none) |
</threat_model>

<verification>
- Truth #23 closed: `grep -cF 'await discover_devices(' docs/user-guide/troubleshooting.md` outputs 0 and `grep -cF 'async for device in discover_devices('` outputs 3 — all three sites (27, 70-84, 110) collect into a list; len()/truthiness/iteration all operate on lists
- All D5-17 residuals closed with the exact per-task gate counts (dead matrix APIs 0 / real APIs present with speed=5.0; lock-serialisation claims 0 across faq.md, advanced-usage.md, CLAUDE.md; escalating-schedule wording 1 / backoff wording 0; Colors imports 8; create_task 0 / gather(*tasks) 2; healthy-networks qualifier 1; CeilingLight 3 + ceiling.py 1; tile_orientations 1; every pinned AU spelling corrected)
- D5-18/D5-20/D5-21 fences: `git diff 7baf7dd..HEAD --stat -- src/` (7baf7dd = HEAD at second-revision planning time; every commit between it and execution touches only .planning/, verified) shows exactly three files — src/lifx/network/connection.py (1+/1-), src/lifx/api.py (1+/1-), src/lifx/network/discovery.py (3+/2- — the two-line collapse plus the two guard lines); `git diff 7baf7dd..HEAD --name-only -- docs/api docs/changelog.md` outputs exactly docs/api/animation.md and docs/api/network.md; the five ::: blocks in network.md are byte-identical (`git diff 7baf7dd..HEAD -- docs/api/network.md | grep -cE '^[+-]:::'` outputs 0); the create_device Returns:/Raises: sections are byte-identical (API-01(d)); mkdocs.yml untouched (D5-01)
- D5-19 fence: api.py lines 260-331, 515-557, 974-1016 byte-identical — the plan's only api.py change is the one preset line at 620
- No version attribution introduced anywhere: `grep -rlE 'Since v[0-9]' docs/ CLAUDE.md src/` matches zero files (D5-12 holds)
- Build gate: `uv run zensical build` exits 0 after every task; final state reports exactly the 8-issue pre-existing baseline (5× api/effects.md, 3× api/index.md) with no issue naming any edited page — api/network.md is now an edited page and is not in the baseline
- Full suite: `uv run --frozen pytest` green after the three docstring edits
- The formerly-surfaced None-guard finding is closed under D5-21 (the Example now guards Device | None); the docstring's Returns:/Raises: mismatch and the except-Exception catch-all are routed to API-01(d) in REQUIREMENTS.md and are NOT edited by this plan
</verification>

<success_criteria>
- The phase goal's failed truth #23 now verifies: every discovery example in
  the phase's troubleshooting guidance is runnable — the recommended
  partial-discovery recipe no longer raises TypeError, and the page is
  internally consistent with its own correct example
- The full operator-opted D5-17 residual set, both D5-18 overrides, and the
  D5-20 extension set (network.md's two hand-written sections, the
  create_device docstring Example, the animation.md contract bullets) are
  closed with source-verified target values; the published API reference no
  longer contradicts the prose pages on the concurrency model or discovery
  consumption
- The three narrow fence overrides were exercised exactly once each;
  everything else under src/ docstrings, docs/api/ (including every :::
  mkdocstrings block), and docs/changelog.md is byte-identical
- Docs build clean at the accepted 8-issue baseline; full test suite green;
  DOCS-01 and DOCS-02 remain satisfied with the gap removed
</success_criteria>

## Artifacts this phase produces (this plan's share)

This is a documentation-accuracy gap-closure plan: it creates **no new code
symbols, no new pages, and no new nav entries**. Changed artefacts:

- Renamed anchors (zero inbound links existed, verified by grep at planning
  time): docs/user-guide/advanced-usage.md `#performance-optimization` →
  `#performance-optimisation` (TOC link updated in the same edit) and
  `#synchronized-multi-device-effects` → `#synchronised-multi-device-effects`;
  docs/api/network.md `#request-serialization-on-single-connection` →
  `#requests-on-a-single-connection` (D5-20)
- Modified files: docs/user-guide/troubleshooting.md,
  docs/user-guide/ceiling-lights.md, docs/faq.md,
  docs/user-guide/advanced-usage.md, docs/architecture/overview.md,
  docs/user-guide/animation.md, docs/api/animation.md, docs/api/network.md
  (hand-written prose only — ::: blocks byte-identical), CLAUDE.md,
  src/lifx/network/connection.py (one docstring line),
  src/lifx/network/discovery.py (one docstring Example: async-for collapse +
  None guard per D5-20/D5-21; Returns:/Raises: byte-identical),
  src/lifx/api.py (one docstring line)

<output>
Create `.planning/phases/05-reliability-documentation/05-05-SUMMARY.md` when done.
Record in the summary: (1) confirmation the D5-18/D5-20/D5-21 and widened-D5-14
fences held (the git-diff scope gates vs 7baf7dd: three src files with the
exact insertion/deletion shapes, two docs/api files, zero ::: lines in any
diff, create_device Returns:/Raises: byte-identical), (2) the three flagged
edge-probe assumptions carried forward to verification — especially the live
DOCS-02 residue instruction, (3) that the None-guard finding is closed under
D5-21 and the create_device Returns:/Raises: mismatch plus the
except-Exception catch-all are routed to API-01(d) in REQUIREMENTS.md (not
open findings), plus the low-risk observations (overview.md discovery mermaid;
network.md's unused asyncio import and "automatically closes" comment),
(4) the final build issue count vs the 8-issue baseline and the full-suite
pytest result, and (5) that DOCS-01/DOCS-02 remain marked complete (no
REQUIREMENTS.md change needed — the gap was an accuracy defect inside
already-satisfied requirements).
</output>
