---
phase: 05-reliability-documentation
plan: 04
type: execute
wave: 3
depends_on: ["05-01", "05-02", "05-03"]
files_modified:
  - docs/user-guide/animation.md
  - docs/user-guide/troubleshooting.md
  - docs/faq.md
  - docs/architecture/overview.md
  - docs/user-guide/advanced-usage.md
  - docs/api/animation.md
  - CLAUDE.md
  - src/lifx/api.py
autonomous: true
gap_closure: true
requirements: [DOCS-01, DOCS-02]
must_haves:
  truths:
    - "No published page or repo instruction attributes the wire-reliability behaviour to a package version: `grep -rl 'Since v1.1' docs/ CLAUDE.md` matches zero files (CR-01 closed per D5-12)"
    - "docs/user-guide/animation.md's Streaming and Flow Control section opens with the version-neutral sentence 'The animation layer paces frame delivery against device acknowledgements internally' and still states latest-frame-wins with no consumer-facing configuration"
    - "docs/user-guide/troubleshooting.md's Connection Drops solution opens 'The library itself retransmits within each request's timeout' and the whole-operation try/except wrapper example is still present (contextualised guidance kept)"
    - "CLAUDE.md's Discovery DoS Protection idle-timeout bullet states ~4 seconds derived from max_response_time × idle_timeout_multiplier, matching MAX_RESPONSE_TIME 1.0 (src/lifx/const.py:31) × IDLE_TIMEOUT_MULTIPLIER 4.0 (src/lifx/const.py:34) — WR-01 per D5-15"
    - "The discover() docstring at src/lifx/api.py:758 states (default 15.0), matching DISCOVERY_TIMEOUT (src/lifx/const.py:28) and the discover_mdns() docstring style at src/lifx/api.py:814 — WR-02 per D5-13"
    - "docs/api/animation.md line 5 claims up to ~20 FPS with Australian spelling; no inflated FPS figure remains on that page — F1 per D5-14"
    - "Every example flagged broken by 05-REVIEW.md now matches the shipped API: no awaited plain constructor (WR-03), protocol packets built with LightHsbk uint16 values and integer millisecond durations (WR-04, WR-05), capability checks via ProductInfo.has_capability() guarded for None (WR-06), no non-existent registry iteration (WR-07), balanced HSBK constructor parentheses (WR-08)"
    - "docs/architecture/overview.md prose layer numbering matches its own mermaid diagram: eight '### Layer' headings numbered 1–8 in order, starting with Layer 1: Utilities (IN-02)"
    - "Australian English holds on edited lines: animation.md line 13 says visualisers (IN-01); faq.md examples carry no double space before ip= (IN-03)"
    - "uv run zensical build exits 0 at the pre-existing 8-issue baseline (5× api/effects.md, 3× api/index.md); no issue names an edited page — docs/api/animation.md is NOT in the baseline and must not join it"
    - statement: "The two reworded wire-behaviour sentences and the new Layer 1: Utilities section read naturally in their host paragraphs, preserve every behavioural claim unchanged, and introduce no factual claims beyond what the mermaid diagram and CLAUDE.md's Utilities entry already state"
      verification: backstop
  artifacts:
    - path: "docs/user-guide/animation.md"
      provides: "Version-neutral streaming behavioural contract (D5-12) + AU spelling fix (IN-01)"
      contains: "The animation layer paces frame delivery"
    - path: "docs/user-guide/troubleshooting.md"
      provides: "Version-neutral retransmit claim (D5-12) + working set_color/registry examples (WR-07, WR-08)"
      contains: "The library itself retransmits"
    - path: "docs/faq.md"
      provides: "Runnable direct-connection and protocol-layer examples (WR-03, WR-04, IN-03)"
      contains: "LightHsbk"
    - path: "docs/architecture/overview.md"
      provides: "Diagram-consistent layer numbering (IN-02) + protocol example in real packet types (WR-05)"
      contains: "### Layer 1: Utilities"
    - path: "docs/user-guide/advanced-usage.md"
      provides: "Capability checks via the real ProductInfo API (WR-06)"
      contains: "has_capability("
    - path: "docs/api/animation.md"
      provides: "Accurate ~20 FPS prose intro, AU spelling (F1 per D5-14)"
      contains: "up to ~20 FPS"
    - path: "CLAUDE.md"
      provides: "Accurate ~4 s discovery idle window (WR-01)"
      contains: "~4 seconds"
    - path: "src/lifx/api.py"
      provides: "discover() docstring matching the real 15.0 s default (WR-02 per D5-13)"
      contains: "(default 15.0)"
  key_links:
    - from: "src/lifx/api.py"
      to: "docs/user-guide/troubleshooting.md"
      via: "docstring and user guide state the same 15.0 s discovery default — the rendered API reference no longer contradicts the user guide"
      pattern: "default 15\\.0"
    - from: "docs/architecture/overview.md"
      to: "docs/architecture/overview.md"
      via: "prose layer headings carry the same numbers as the page's own mermaid subgraph labels, Utilities first"
      pattern: "### Layer 1: Utilities"
    - from: "docs/faq.md"
      to: "src/lifx/protocol/protocol_types.py"
      via: "protocol example imports the real generated class name so the snippet imports cleanly"
      pattern: "LightHsbk"
  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 next version from conventional commits at release time (pyproject.toml:139), so any pin recreates CR-01 (D5-12)"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT edit any source docstring other than the single discover() timeout line at src/lifx/api.py:758 — D5-13 is a one-line override; D5-11 otherwise holds for every other docstring in src/"
      category: values
      requirement_id: DOCS-01
    - statement: "MUST NOT edit docstring-fed docs/api/ content or docs/changelog.md — D5-14 permits exactly one hand-written prose line, docs/api/animation.md:5; everything else under docs/api/ and the changelog stays fenced"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT restate internal flow-control tuning constants (ack-probe placement, outstanding-ack gate threshold, ack expiry) while rewording the streaming paragraph — behavioural contract only (D5-09, carried forward from 05-02)"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT frame latest-frame-wins drops as a defect or advise retrying a dropped frame while rewording the streaming narrative (carried forward from 05-02)"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT delete the legitimate whole-operation retry guidance while removing the version clause from the troubleshooting solution — the contextualised try/except wrapper examples stay (carried forward from 05-03)"
      category: values
      requirement_id: DOCS-01
---

<objective>
Close the 05-VERIFICATION.md blocker (CR-01: false "Since v1.1" package-version
attribution at exactly two sites) with the operator-approved version-neutral
wording (D5-12), and close the full operator-opted residual set (D5-15): WR-01
(CLAUDE.md idle timeout), WR-02 (discover() docstring default — the D5-13 narrow
override of D5-11), WR-03–WR-08 (broken code examples), IN-01–IN-03 (spelling,
layer numbering, double spaces), and F1 (docs/api/animation.md:5 FPS prose — the
D5-14 narrow override of D5-10/prohibition 7).

Every target value in this plan was re-verified against the live files and
shipped source during planning (2026-07-17) — the review is evidence, the source
is truth. The prior cycle's defect was planner-mandated wording; this plan
therefore specifies exact target sentences and exact gate counts.

Purpose: the phase goal is *accurate* guidance. 25/26 must-haves already verify;
this plan removes the one false claim and the accuracy residuals so a user of the
published package (currently v5.5.x) is never told that installs >= 1.1 carry the
new wire behaviour, and never copy-pastes an example that raises TypeError,
ImportError, or AttributeError.

Output: eight files corrected in place. No new pages, no nav changes (D5-01/D5-02
untouched), no behavioural-claim changes — only attribution, accuracy, and
example-correctness fixes.

Decisions implemented: D5-12, D5-13, D5-14, D5-15. Boundaries held: D5-09,
D5-10, D5-11 (outside the two narrow overrides), D5-01, D5-02.
</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 (gap-closure pass)

| SOURCE   | ID     | Item                                                        | Task | Status  |
|----------|--------|-------------------------------------------------------------|------|---------|
| GOAL     | —      | Accurate guidance on guaranteed wire behaviour + quirks      | 1–3  | COVERED |
| REQ      | DOCS-01| Wake-tail/retry guidance accuracy (CR-01 troubleshooting site, WR-01, WR-02, WR-07, WR-08) | 1, 2 | COVERED |
| REQ      | DOCS-02| Streaming guidance accuracy (CR-01 animation site, IN-01, F1) | 1, 3 | COVERED |
| RESEARCH | F1     | docs/api/animation.md:5 stale FPS prose (now opted in via D5-14) | 3  | COVERED |
| CONTEXT  | D5-12  | Version-neutral phrasing at both CR-01 sites                | 1    | COVERED |
| CONTEXT  | D5-13  | discover() docstring default 15.0 (only source edit)        | 1    | COVERED |
| CONTEXT  | D5-14  | docs/api/animation.md:5 one-line prose fix (only docs/api edit) | 3 | COVERED |
| CONTEXT  | D5-15  | WR-01, WR-03–WR-08, IN-01–IN-03 full residual closure       | 1, 2, 3 | COVERED |

No MISSING rows. Exclusions: D5-01–D5-11 were delivered by plans 05-01..03 and
are not re-planned; the `if product:` line at troubleshooting.md:432 is not in
the operator-opted residual list and is harmless (module-level `get_product()`
always returns a ProductInfo), so it stays untouched.

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

No SPEC exists for this phase; the deterministic edge probe over DOCS-01/DOCS-02
returned three `unresolved` rows. Per the no-silent-drop rule each is surfaced
here (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 in this plan is an exact-string
   replacement gated by exact-count greps, so re-executing a task against an
   already-fixed file is a detectable no-op (the negative gates already pass and
   no doubled text can be introduced). Flagged for verification; if the verifier
   finds an edit that would compound on re-run, route it back as a gap.
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 3, all dependencies complete), tasks run sequentially with one
   atomic signed commit per task, so an interruption leaves a clean, buildable
   intermediate state (each task independently ends with a green
   `uv run zensical build`). Flagged for verification.
3. **DOCS-02 / unclassified** ("unclassified — review manually") — The prior
   cycle resolved this row with the assumption that D5-08/D5-09 truths carried
   all DOCS-02 edge risk; the verifier falsified that — the uncovered edge was
   *version attribution* (CR-01). That specific edge is now explicitly covered:
   D5-12 truths plus the zero-occurrence gate in Task 1. What remains is
   genuinely unclassified: the probe cannot enumerate further accuracy edges in
   the streaming guidance (e.g. future API drift, figures invalidated by later
   hardware evidence). Do NOT treat this row as fully covered by the truths
   above; the verifier should independently probe the factual claims in the
   edited sections and route any residue back as a gap rather than pass silently.

Prohibition recall (spec-less §B): six kept prohibitions authored descriptor-less
into `must_haves.prohibitions` above (two new fences from D5-13/D5-14, the D5-12
version-neutrality constraint, three carried forward from 05-02/05-03 because
this plan re-touches the exact paragraphs they guard). The prior cycle's other
prohibitions (wake-tail framing, ESP32/mesh claims, spike leaks) guard sections
this plan never touches and are not carried. Canon-referral: no canon
security/compliance candidates surfaced for a docs-accuracy pass; nothing
referred to /gsd-secure-phase.

<capability_checkpoints>
Dispositions for the three precomputed plan:pre detector results (recorded, not
re-run):

- **api-coverage** (`detected: true` on "the same Animator API"): FALSE POSITIVE.
  The match is lifx-async's own `Animator` API described in its own docs prose —
  not an external API/SDK/service integration. This phase is documentation-only
  (CONTEXT.md domain: no code changes beyond one docstring line) and integrates
  no external service. No COVERAGE.md capability matrix is warranted; fabricating
  one for a non-integration would be noise.
- **assumption-delta** (`detected: true` on "also" in a ROADMAP dependency note):
  FALSE POSITIVE. No second platform, auth method, tenant, region, or source of
  truth is introduced; no identity model exists in a docs phase. Decision:
  `no-change` — no core assumption lost a monopoly.
- **schema-gate**: NOT APPLICABLE. lifx-async is a zero-dependency Python UDP
  library with no database and no ORM schema files anywhere in the repo. No
  schema push task exists to inject.
</capability_checkpoints>

<tasks>

<task type="auto">
  <name>Task 1: Version-neutral wire-behaviour claims + adjacent stale-figure fixes (CR-01/D5-12, WR-01/D5-15, WR-02/D5-13, IN-01)</name>
  <files>docs/user-guide/animation.md, docs/user-guide/troubleshooting.md, CLAUDE.md, src/lifx/api.py</files>
  <read_first>
    - .planning/phases/05-reliability-documentation/05-CONTEXT.md (### Gap closure amendments — D5-12..D5-15 are the operative decisions)
    - docs/user-guide/animation.md lines 10-15 (the bullet with the US-spelled word) and 330-336 (the streaming paragraph to reword)
    - docs/user-guide/troubleshooting.md lines 155-170 (the Connection Drops solution paragraph to reword)
    - CLAUDE.md lines 250-258 (Discovery DoS Protection bullet list; line 256 is the stale idle-timeout bullet)
    - src/lifx/api.py lines 746-760 (discover() signature — timeout defaults to DISCOVERY_TIMEOUT — and its Args docstring; line 758 is the stale line) and lines 805-816 (discover_mdns() Args block — the exact docstring style to match)
    - src/lifx/const.py lines 27-34 (DISCOVERY_TIMEOUT = 15.0; MAX_RESPONSE_TIME = 1.0; IDLE_TIMEOUT_MULTIPLIER = 4.0 — the source-of-truth values)
  </read_first>
  <action>
    Four surgical accuracy edits. The two sentence rewrites implement D5-12
    exactly — no version attribution of any kind may replace the removed clause
    (not the milestone name, not a guessed future release: python-semantic-release
    derives the version from conventional commits at release time, so the
    shipping version is unknowable now).

    1. docs/user-guide/animation.md line 332 (opening of the `## Streaming and
       Flow Control` paragraph): delete the leading version-attribution clause
       (the internal milestone prefix) so the paragraph opens exactly:
       `The animation layer paces frame delivery against device acknowledgements internally. When a device falls behind, new frames are dropped, never queued — latest-frame-wins.`
       The remainder of the paragraph ("There is no consumer-facing
       configuration: you call the same `Animator` API and the library decides
       the delivery strategy.") stays word-for-word; rewrap the paragraph to the
       page's existing line width. Change nothing else in the section (D5-09 and
       the latest-frame-wins framing prohibitions apply).

    2. docs/user-guide/animation.md line 13: the integrations bullet uses the US
       spelling. Change the bullet to `**Integration with music visualisers**`
       (IN-01, Australian English). No other change on that line or page.

    3. docs/user-guide/troubleshooting.md line 162 (Connection Drops solution):
       delete the leading version clause and its comma so the sentence opens
       exactly: `The library itself retransmits within each request's timeout, so transient packet loss is handled for you.`
       The following sentences about the application-level wrapper being for
       whole-operation failures, and the try/except example below them, stay
       untouched (prohibition: do not delete the retry guidance). Rewrap the
       paragraph naturally.

    4. CLAUDE.md line 256 (Discovery DoS Protection): replace the stale
       idle-timeout bullet with exactly:
       `- **Idle timeout** - Discovery stops when no responses are received for ~4 seconds (max_response_time × idle_timeout_multiplier)`
       Rationale to encode nowhere but here: the real window is MAX_RESPONSE_TIME
       (1.0 s, src/lifx/const.py:31) × IDLE_TIMEOUT_MULTIPLIER (4.0,
       src/lifx/const.py:34) ≈ 4 s — the stale two-second figure survived the
       05-03 pass in the exact section it corrected (WR-01, D5-15).

    5. src/lifx/api.py line 758 (the ONLY permitted source edit, D5-13): the
       discover() docstring's timeout line currently claims a 3.0-second default;
       change it to exactly `timeout: Discovery timeout in seconds (default 15.0)`
       — matching both the real default (`timeout: float = DISCOVERY_TIMEOUT`,
       15.0 s) and the discover_mdns() docstring style at line 814. Touch nothing
       else in the file; every other docstring stays out of scope (D5-11 holds).

    Australian English throughout. Commit with `git commit -s` (GPG-signed,
    automatic).
  </action>
  <verify>
    <automated>! grep -rq 'Since v1.1' docs/ CLAUDE.md src/ && grep -q 'The animation layer paces frame delivery against device acknowledgements' docs/user-guide/animation.md && grep -q "The library itself retransmits within each request's timeout" docs/user-guide/troubleshooting.md && ! grep -q 'visualizer' docs/user-guide/animation.md && ! grep -q 'for 2 seconds' CLAUDE.md && grep -qF '~4 seconds' CLAUDE.md && ! grep -qF '(default 3.0)' src/lifx/api.py && test "$(grep -cF '(default 15.0)' src/lifx/api.py)" = "2" && uv run --frozen pytest tests/test_api -q && uv run zensical build</automated>
  </verify>
  <acceptance_criteria>
    - `grep -rl 'Since v1.1' docs/ CLAUDE.md src/ | wc -l` outputs 0 (was 2 files: animation.md:332, troubleshooting.md:162 — CR-01 closed at both sites, no other occurrences existed anywhere)
    - `grep -c 'The animation layer paces frame delivery against device acknowledgements' docs/user-guide/animation.md` outputs 1, and the paragraph still contains `latest-frame-wins` and the unchanged no-consumer-configuration sentence
    - `grep -c "The library itself retransmits within each request's timeout" docs/user-guide/troubleshooting.md` outputs 1, and the try/except wrapper example below it is still present (`grep -c 'except LifxTimeoutError' docs/user-guide/troubleshooting.md` unchanged from pre-edit)
    - No new version attribution introduced: `grep -cE 'Since v[0-9]' docs/user-guide/animation.md docs/user-guide/troubleshooting.md` outputs 0 for both files
    - `grep -c 'visualizer' docs/user-guide/animation.md` outputs 0 and `grep -c 'visualisers' docs/user-guide/animation.md` outputs 1 (IN-01)
    - `grep -c 'for 2 seconds' CLAUDE.md` outputs 0; `grep -cF '~4 seconds' CLAUDE.md` outputs 1; the replacement bullet names both constant roles (max_response_time × idle_timeout_multiplier)
    - `grep -cF '(default 3.0)' src/lifx/api.py` outputs 0; `grep -cF '(default 15.0)' src/lifx/api.py` outputs 2 (discover() joins discover_mdns())
    - `git show --stat HEAD -- src/` shows exactly one file (src/lifx/api.py) with a one-line change — the D5-13 fence held
    - `uv run --frozen pytest tests/test_api -q` exits 0 (docstring-only source change breaks nothing)
    - `uv run zensical build` exits 0
  </acceptance_criteria>
  <done>Both CR-01 sites carry the D5-12 version-neutral sentences verbatim with surrounding paragraphs intact; CLAUDE.md and the discover() docstring match src/lifx/const.py; zero version-attribution clauses remain anywhere in docs/, CLAUDE.md, or src/.</done>
</task>

<task type="auto">
  <name>Task 2: Fix the six broken/misleading code examples (WR-03..WR-08, IN-03 per D5-15)</name>
  <files>docs/faq.md, docs/architecture/overview.md, docs/user-guide/advanced-usage.md, docs/user-guide/troubleshooting.md</files>
  <read_first>
    - docs/faq.md lines 59-66 (direct-connection workaround), 233-237 (context-manager example), 330-350 (low-level protocol example)
    - docs/architecture/overview.md lines 113-127 (protocol-layer example)
    - docs/user-guide/advanced-usage.md lines 365-410 (both capability-check blocks)
    - docs/user-guide/troubleshooting.md lines 280-296 (connection-reuse examples) and 420-436 (product-registry block)
    - src/lifx/protocol/protocol_types.py lines 182-189 (the generated class is `LightHsbk` with int fields hue/saturation/brightness/kelvin — raw uint16)
    - src/lifx/protocol/packets.py lines 617-634 (`Light.SetColor` dataclass fields: `color: LightHsbk`, `duration: int` — protocol field Duration is uint32 milliseconds)
    - src/lifx/products/registry.py lines 39-70 (`ProductInfo.has_capability(capability)` is the supported check; ProductInfo defines no membership/iteration protocol) and lines 1713-1840 (`ProductRegistry` exposes `get_product`, `__len__`, `__contains__` only; module-level `get_product()` always returns a ProductInfo)
    - src/lifx/devices/light.py lines 167-171 (`set_color(color: HSBK, duration: float = 0.0)` — no per-channel keyword arguments)
    - src/lifx/devices/base.py line 454 (`from_ip` is async — awaiting it is correct; the plain constructor is not awaitable) and line 745 (`capabilities` property returns `ProductInfo | None`)
    - src/lifx/network/connection.py lines 88-95 (`DeviceConnection(serial: str, ip: str, ...)` — the surrounding faq example lines are valid and stay untouched)
  </read_first>
  <action>
    Six example fixes, each verified against the shipped source cited in
    read_first. Fix exactly what is flagged; do not restyle surrounding code.

    1. docs/faq.md line 63 (WR-03 + IN-03): the workaround awaits the plain
       constructor (only `Light.from_ip()` is awaitable — awaiting a `Light`
       instance raises TypeError) and has a double space before the ip keyword.
       The line becomes exactly:
       `async with Light(serial="d073d5000000", ip="192.168.1.100") as light:`
       The body line below stays unchanged.

    2. docs/faq.md line 235 (IN-03): same double-space defect in the
       context-manager example; collapse to a single space after the comma so it
       reads `async with Light(serial="d073d5000000", ip="192.168.1.100") as light:`.

    3. docs/faq.md lines 336-347 (WR-04): the low-level protocol example fails at
       import (the generated class is `LightHsbk`, not the float-style name),
       passes user-facing float values where the packet needs raw uint16, and
       passes a float duration where the field is uint32 milliseconds. Rewrite
       only the import line and the packet construction inside the existing
       fenced block:
       - second import line becomes `from lifx.protocol.protocol_types import LightHsbk`
       - packet construction becomes `packet = Light.SetColor(` /
         `    color=LightHsbk(hue=32768, saturation=65535, brightness=52428, kelvin=3500),` /
         `    duration=1000,  # milliseconds (uint32)` / `)`
         — the uint16 values preserve the original intent (32768 ≈ 180°,
         65535 = full saturation, 52428 ≈ 0.8 brightness).
       - keep the `# Create a packet directly` comment; keep the
         `DeviceConnection` lines below unchanged (constructor verified:
         `DeviceConnection(serial, ip)` matches the shipped signature).

    4. docs/architecture/overview.md lines 114-127 (WR-05): the protocol-layer
       example imports the user-facing float class from the top-level package and
       feeds it (plus a float duration) into `Light.SetColor` — exactly the
       format-mixing CLAUDE.md forbids, in the section introducing the protocol
       layer. Apply the same fix shape as WR-04: the second import line becomes
       `from lifx.protocol.protocol_types import LightHsbk`; the construction
       becomes `Light.SetColor(` with `color=LightHsbk(hue=32768, saturation=65535, brightness=52428, kelvin=3500),`
       and `duration=1000,  # milliseconds (uint32)`. Keep the
       `data = packet.pack()` lines unchanged.

    5. docs/user-guide/advanced-usage.md lines 379-385 and 404-409 (WR-06): five
       capability checks use membership-style tests against `.capabilities`,
       which raises TypeError (ProductInfo is not a container) or fails on None.
       Replace each with the supported API, guarded for None, e.g.
       `if light.capabilities and light.capabilities.has_capability(ProductCapability.COLOR):`
       — apply the same shape to all five sites: COLOR/MULTIZONE/INFRARED on
       `light` in the first block, COLOR/MULTIZONE on `device` in the second
       block (use `device.capabilities and device.capabilities.has_capability(...)`
       there). Indentation and the statement bodies stay unchanged.

    6. docs/user-guide/troubleshooting.md lines 425-428 (WR-07): the registry
       example iterates a method that does not exist on ProductRegistry (no
       iteration protocol at all — only get_product, len(), and `in` on product
       IDs). Replace the comment + loop (keeping `registry = get_registry()`)
       with: comment `# Check how many products the registry knows`, then
       `registry = get_registry()`, then
       `print(f"Registry contains {len(registry)} products")`. The
       `# Check specific product` block below (get_product(27) etc.) stays
       untouched.

    7. docs/user-guide/troubleshooting.md lines 287 and 294 (WR-08): the
       HSBK constructor closes after `hue`, spilling saturation/brightness/kelvin
       onto `set_color()` as unexpected keyword arguments (its signature is
       `set_color(color, duration=0.0)`) — TypeError at runtime. Both lines
       become exactly:
       `await light.set_color(HSBK(hue=(360 / 10) * i, saturation=1.0, brightness=1.0, kelvin=3500))`
       (preserving each line's original indentation: 11 spaces at 287, 15 at
       294 relative to the fenced block). The float HSBK class is correct here —
       this is the device layer.

    Australian English in any touched comment. Commit with `git commit -s`.
  </action>
  <verify>
    <automated>! grep -qF 'await Light(' docs/faq.md && ! grep -qF '",  ip=' docs/faq.md && ! grep -qF 'protocol_types import HSBK' docs/faq.md && test "$(grep -cF 'LightHsbk' docs/faq.md)" = "2" && grep -qF 'duration=1000' docs/faq.md && ! grep -qF 'from lifx import HSBK' docs/architecture/overview.md && test "$(grep -cF 'LightHsbk' docs/architecture/overview.md)" = "2" && ! grep -qF 'duration=1.0' docs/faq.md && ! grep -qF 'duration=1.0' docs/architecture/overview.md && ! grep -qE 'in (light|device)\.capabilities' docs/user-guide/advanced-usage.md && test "$(grep -cF 'has_capability(' docs/user-guide/advanced-usage.md)" = "5" && ! grep -qF 'registry.items()' docs/user-guide/troubleshooting.md && grep -qF 'len(registry)' docs/user-guide/troubleshooting.md && ! grep -qF '), saturation=' docs/user-guide/troubleshooting.md && test "$(grep -cF 'kelvin=3500))' docs/user-guide/troubleshooting.md)" = "2" && uv run zensical build</automated>
  </verify>
  <acceptance_criteria>
    - `grep -cF 'await Light(' docs/faq.md` outputs 0 (was 1); `grep -cF '",  ip=' docs/faq.md` outputs 0 (was 2) — WR-03 + IN-03
    - `grep -cF 'protocol_types import HSBK' docs/faq.md` outputs 0 (was 1); `grep -cF 'LightHsbk' docs/faq.md` outputs 2 (import + constructor); `grep -cF 'duration=1000' docs/faq.md` outputs 1 and `grep -cF 'duration=1.0' docs/faq.md` outputs 0 — WR-04
    - `grep -cF 'from lifx import HSBK' docs/architecture/overview.md` outputs 0 (was 1); `grep -cF 'LightHsbk' docs/architecture/overview.md` outputs 2; `grep -cF 'duration=1.0' docs/architecture/overview.md` outputs 0 — WR-05
    - `grep -cE 'in (light|device)\.capabilities' docs/user-guide/advanced-usage.md` outputs 0 (was 5); `grep -cF 'has_capability(' docs/user-guide/advanced-usage.md` outputs 5 — WR-06
    - `grep -cF 'registry.items()' docs/user-guide/troubleshooting.md` outputs 0 (was 1); `grep -cF 'len(registry)' docs/user-guide/troubleshooting.md` outputs 1 — WR-07
    - `grep -cF '), saturation=' docs/user-guide/troubleshooting.md` outputs 0 (was 2); `grep -cF 'kelvin=3500))' docs/user-guide/troubleshooting.md` outputs 2 (both constructors rebalanced) — WR-08
    - All uint16 values in the two protocol examples match the LightHsbk int fields and the SetColor uint32 Duration field (source-verified in read_first) — no float value reaches a protocol packet
    - `uv run zensical build` exits 0
  </acceptance_criteria>
  <done>All six flagged examples import cleanly and call only APIs that exist with argument shapes the shipped source accepts; no HSBK format mixing remains in any protocol-layer example; device-layer examples keep the float HSBK class.</done>
</task>

<task type="auto">
  <name>Task 3: Reconcile overview.md layer numbering (IN-02), fix the D5-14 fenced-override line (F1), and hold the build baseline</name>
  <files>docs/architecture/overview.md, docs/api/animation.md</files>
  <read_first>
    - docs/architecture/overview.md lines 1-97 (the mermaid diagram — its subgraph labels number Utilities=1, Protocol=2, Network=3, Device=4, Animation=5, Effects=6, Theme=7, High-Level API=8) and the prose heading positions: lines 99, 129, 155 (numbered 1-3), 184, 202, 217, 232 (already 5-8)
    - docs/api/animation.md lines 1-10 (line 5 is the hand-written prose intro; everything below the intro is mkdocstrings-directive territory — fenced)
    - CLAUDE.md "### Layered Architecture" section, "Utilities" entry (the content basis for the new Layer 1 section: color.py HSBK class + Colors presets; products/ registry)
  </read_first>
  <action>
    1. docs/architecture/overview.md (IN-02) — decision made at planning time,
       recorded here: **renumber the prose headings to match the page's own
       mermaid diagram** (chosen over dropping the numbers because prose Layers
       5-8 already match the diagram — only headings 1-3 are stale — and no
       inbound link anywhere in docs/ or mkdocs.yml targets a layer anchor, so
       anchor renames are safe; verified by grep during planning). Apply:
       - line 99 heading becomes `### Layer 2: Protocol Layer`
       - line 129 heading becomes `### Layer 3: Network Layer`
       - line 155 heading becomes `### Layer 4: Device Layer`
       - headings for Animation (5), Effects (6), Theme (7), High-Level API (8)
         already match the diagram — leave them byte-identical.
       - Insert a new `### Layer 1: Utilities` section immediately after the
         `## Layer Responsibilities` heading and before the Protocol section,
         mirroring the page's existing per-layer shape (a `**Purpose**:` line,
         3-4 behaviour bullets, a `**Key Files**:` list). Content derives ONLY
         from the diagram's two Utilities boxes and CLAUDE.md's Utilities entry —
         no new claims: Purpose: shared colour and product-capability helpers
         used by every layer above. Bullets: the user-facing `HSBK` class with
         float values and RGB conversion plus `Colors` presets; the auto-generated
         products registry mapping product IDs to capabilities; zero runtime
         dependencies. Key Files: `color.py` - HSBK class and Colors presets;
         `products/registry.py` - Auto-generated product capability database.
         Keep it comparable in length to the other layer sections (roughly
         10-14 lines). No code example needed (Layers 6-8 have none either).

    2. docs/api/animation.md line 5 (F1 per D5-14 — the ONLY permitted docs/api
       edit): the intro sentence carries a US-spelled participle and an inflated
       thirty-plus-FPS figure. The line becomes exactly:
       `The animation module provides efficient high-frequency frame delivery for LIFX devices, optimised for real-time effects at up to ~20 FPS.`
       Nothing else on the page changes — the mkdocstrings-fed content below is
       fenced, as is docs/changelog.md.

    3. Build-baseline gate (SC3): run `uv run zensical build`. Expected post-edit
       state, stated explicitly because docs/api/animation.md is NOT in the
       pre-existing warning baseline: exit 0 with the same 8 issues as before
       (5× api/effects.md, 3× api/index.md) and no output line naming
       api/animation.md or any other edited page — the D5-14 edit is prose-only
       and adds no anchors or links, and the new Utilities heading creates a new
       anchor without any inbound links to break.

    Australian English. Commit with `git commit -s`.
  </action>
  <verify>
    <automated>test "$(grep -oE '^### Layer [0-9]+' docs/architecture/overview.md | awk '{print $3}' | tr '\n' ' ')" = "1 2 3 4 5 6 7 8 " && grep -qF '### Layer 1: Utilities' docs/architecture/overview.md && ! grep -q 'optimized' docs/api/animation.md && ! grep -qF '30+ FPS' docs/api/animation.md && grep -qF 'up to ~20 FPS' docs/api/animation.md && OUT=$(uv run zensical build 2>&1) && echo "$OUT" | grep -qF '8 issues found' && ! echo "$OUT" | grep -qF 'api/animation.md'</automated>
  </verify>
  <acceptance_criteria>
    - `grep -oE '^### Layer [0-9]+' docs/architecture/overview.md | awk '{print $3}' | tr '\n' ' '` outputs `1 2 3 4 5 6 7 8 ` (eight prose headings, diagram-consistent order, Utilities first) — IN-02
    - `grep -cF '### Layer 1: Utilities' docs/architecture/overview.md` outputs 1, and the section names color.py and products/registry.py in Key Files
    - `grep -c 'optimized' docs/api/animation.md` outputs 0; `grep -cF '30+ FPS' docs/api/animation.md` outputs 0; `grep -cF 'up to ~20 FPS' docs/api/animation.md` outputs 1 — F1/D5-14
    - `git show --name-only --format= HEAD | grep -E '^docs/(api|changelog)'` outputs exactly one line, `docs/api/animation.md` — the D5-14 fence held (no other docs/api file, no changelog)
    - `uv run zensical build` exits 0 and reports exactly the pre-existing 8-issue baseline; no issue line names api/animation.md, overview.md, or any other page edited by this plan (if the total differs from 8, every extra issue must be triaged before the task may complete — the baseline is the accepted set, not a target to grow)
  </acceptance_criteria>
  <done>Prose and diagram agree on the eight-layer numbering with a Utilities section a reader can map to the diagram's Layer 1; the api/animation.md intro states the honest ~20 FPS ceiling in Australian English; the docs build holds the exact pre-existing baseline.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| published docs → reader behaviour | Fixed examples get copy-pasted and run against real devices; wrong version claims shape reliability expectations for the published package |
| repo instructions (CLAUDE.md) → agent/contributor behaviour | Stale constants in CLAUDE.md propagate into future code and docs work |
| source docstring → rendered API reference | The discover() docstring feeds the published API pages; a wrong default is a site-wide contradiction |

## STRIDE Threat Register

ASVS L1, block on high. Honest assessment: a documentation + one-docstring-line
phase creates no new attack surface — no auth, session, input-validation, or
crypto artefact is touched (RESEARCH.md §Security Domain: no ASVS category
applies to the changed artefacts).

| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-05-06 | Information Disclosure | docs examples (faq, overview, advanced-usage, troubleshooting) | low | mitigate | Broken examples replaced with source-verified API usage — every fixed snippet was checked against the shipped class/signature during planning; no secrets or credentials appear in any example |
| T-05-07 | Denial of Service | reader-side device flooding | low | mitigate | No FPS or polling figure changes: all edited content keeps the ~20 FPS ceiling / ≤20 msg/s framing; the D5-14 fix *lowers* the documented figure |
| T-05-08 | Spoofing | version-attribution claims | low | mitigate | D5-12 removes the false version claim entirely rather than substituting a guessed one; prohibition 1 blocks reintroduction |
| 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>
- CR-01 closed: `grep -rl 'Since v1.1' docs/ CLAUDE.md src/ | wc -l` outputs 0; both D5-12 replacement sentences present verbatim; no substitute version attribution anywhere (`grep -rcE 'Since v[0-9]' docs/user-guide/animation.md docs/user-guide/troubleshooting.md` all 0)
- All D5-15 residuals closed with the exact gate counts listed per task (WR-01: 0/1 on CLAUDE.md; WR-03/IN-03: 0/0 on faq.md; WR-04/WR-05: LightHsbk ×2 per file, duration=1000, no float durations; WR-06: 0 membership tests / 5 has_capability calls; WR-07: 0 items() / 1 len(registry); WR-08: 0 misplaced parens / 2 rebalanced constructors; IN-01: 0 US spellings on animation.md; IN-02: heading sequence 1-8)
- D5-13 fence: the only src/ change across the whole plan is one line in src/lifx/api.py (`git diff 55545b5..HEAD --stat -- src/` shows exactly src/lifx/api.py, 1 insertion, 1 deletion); `uv run --frozen pytest tests/test_api -q` green
- D5-14 fence: the only docs/api or changelog change is docs/api/animation.md (`git diff 55545b5..HEAD --name-only -- docs/api docs/changelog.md` outputs exactly `docs/api/animation.md`)
- SC3 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 — docs/api/animation.md must not join the baseline
- mkdocs.yml untouched (D5-01); no new pages; no nav changes; no behavioural claims altered — attribution, accuracy, and example correctness only
</verification>

<success_criteria>
- The phase goal's failed truth #23 now verifies: the new guidance contains only
  accurate factual claims — no reader of the published package can misread an
  internal milestone name as a package version (CR-01/D5-12)
- All operator-opted residuals (D5-15: WR-01, WR-03–WR-08, IN-01–IN-03), the
  D5-13 docstring override (WR-02), and the D5-14 prose override (F1) are closed
  with source-verified target values
- The two narrow fence overrides were exercised exactly once each; everything
  else under src/ docstrings, docs/api/, and docs/changelog.md is byte-identical
- Docs build clean at the accepted baseline; 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**. The only new addressable
artefact is one docs heading/anchor:

- New heading/anchor: `docs/architecture/overview.md` → `### Layer 1: Utilities`
  (`#layer-1-utilities`) — no inbound links exist or are added
- Renamed anchors (no inbound links existed, verified): overview.md layer
  headings 1→2 (Protocol), 2→3 (Network), 3→4 (Device)
- Modified files: docs/user-guide/animation.md, docs/user-guide/troubleshooting.md,
  docs/faq.md, docs/architecture/overview.md, docs/user-guide/advanced-usage.md,
  docs/api/animation.md, CLAUDE.md, src/lifx/api.py (one docstring line)

<output>
Create `.planning/phases/05-reliability-documentation/05-04-SUMMARY.md` when done.
Record in the summary: (1) confirmation the D5-13 and D5-14 fences held (the two
git-diff scope gates), (2) the three flagged edge-probe assumptions above carried
forward to verification — especially the honest DOCS-02 residue note, (3) the
final build issue count vs the 8-issue baseline, and (4) that DOCS-01/DOCS-02
remain marked complete (no REQUIREMENTS.md change needed — the gap was an
accuracy defect inside already-satisfied requirements).
</output>
