---
phase: 05-reliability-documentation
plan: 06
type: execute
wave: 5
depends_on: ["05-05"]
files_modified:
  - docs/getting-started/quickstart.md
  - docs/architecture/overview.md
  - docs/api/animation.md
  - docs/api/effects.md
  - docs/api/high-level.md
  - docs/api/network.md
  - .github/workflows/docs.yml
  - src/lifx/network/connection.py
  - src/lifx/network/discovery.py
  - src/lifx/animation/animator.py
  - src/lifx/animation/packets.py
  - src/lifx/animation/orientation.py
  - src/lifx/api.py
  - src/lifx/color.py
  - src/lifx/devices/base.py
  - src/lifx/devices/ceiling.py
  - src/lifx/devices/light.py
  - src/lifx/devices/matrix.py
  - src/lifx/devices/multizone.py
  - src/lifx/effects/pulse.py
  - src/lifx/protocol/base.py
  - src/lifx/protocol/generator.py
autonomous: true
gap_closure: true
gap_ids: [G-05-2, G-05-3, G-05-4, G-05-5, G-05-6, G-05-7]
requirements: [DOCS-01, DOCS-02]
must_haves:
  truths:
    - "No published page claims that connecting by IP alone skips discovery: quickstart.md documents the IP-only path as a unicast discovery round-trip and documents the serial+IP form as the genuine zero-discovery path (docs/index.md:41 precedent); overview.md's Direct Connection bullet says the same (G-05-2)"
    - "docs/api/animation.md's Direct UDP Delivery lead-in uses the operator's agreed text verbatim — 'purpose-built network stack with the following characteristics' — and the frame-loss bullet reads 'expected', not an intent-framing the operator rejected (G-05-3)"
    - "Every docstring heading that mkdocstrings publishes is followed by a blank line so its list renders as a list: zero run-on paragraphs (a <p> containing 'heading:' directly followed by a bullet line) remain anywhere under site/api/ after a fresh build (G-05-4 + D5-22 audit findings; was 23 run-on paragraphs at planning time)"
    - "No published API page explains behaviour by reference to another library's internals or design-lineage shorthand: the rendered docstrings of DeviceConnection (incl. __init__ Args), discover_devices, Animator, AnimatorStats, send_frame and both probe_template_index properties speak plain reader vocabulary; the retransmit contract wording matches docs/architecture/overview.md:150 ('escalating schedule') (G-05-5)"
    - "No published API page references internal planning artifacts — requirement IDs, decision IDs, spike numbers or plan numbers resolve to zero occurrences in every public-object docstring of connection.py, discovery.py, animator.py and packets.py; every removed ID survives as a # comment or in an unrendered surface (traceability demoted, never deleted) (G-05-6)"
    - "No internal tuning constant survives in rendered prose (D5-09 as written): the measured spike figure formerly at packets.py:164, the outstanding-ack gate threshold, the ack expiry, the throughput floor, and the retransmit/re-broadcast gap offsets formerly at discovery.py:491 are absent from every public-object docstring; public parameter defaults (timeout 16.0, max_retries 8, DISCOVERY_TIMEOUT 15 s, the ~4 s idle window derived from the public max_response_time × idle_timeout_multiplier parameters) legitimately remain"
    - "The D5-22 audit ran over every :::-reachable docstring for all three defect classes (internal vocabulary, missing blank lines, D5-09 constants) and every finding beyond G-05-4/05/06 was fixed in this plan; residual hits are all on the pinned out-of-scope map (comments, private objects, unrendered modules)"
    - "uv run zensical build --strict exits 0 — ZERO warnings; the former 8-warning baseline is a defect set, not a constant (D5-23): the 5 api/effects.md unresolved link references are closed by backticking the type annotations, and the 3 api/index.md missing anchors are closed by rendering the missing mDNS ::: targets — index.md itself is NOT edited (G-05-7)"
    - "CI gates on strict builds: both zensical invocations in .github/workflows/docs.yml (lines 56 and 114) carry --strict, so the warning class cannot silently drift back (G-05-7/D5-23)"
    - "The three newly published mDNS symbols (lifx.api.discover_mdns, lifx.network.mdns.discover_lifx_services, lifx.network.mdns.LifxServiceRecord) render on their index-linked pages AND passed the D5-22 audit (internal vocabulary, blank lines, D5-09 constants) before going live — planning-time result: all three docstrings clean"
    - "uv run --frozen pytest passes (2618-test baseline), uv run ruff check . and uv run ruff format --check . are clean (88-col E501 respected — the 05-05 deviation precedent), and uv run pyright reports zero errors"
    - statement: "The rewritten prose preserves every behavioural claim it replaces — pacing against acks, latest-frame-wins, never-retried drops, wall-clock-bounded retries, listen-between-sends, correlation-not-serialisation — and introduces no factual claim beyond what the shipped source states"
      verification: backstop
  artifacts:
    - path: "docs/getting-started/quickstart.md"
      provides: "Honest Direct Connection section: IP-only = unicast discovery round-trip; serial+IP = zero discovery (G-05-2)"
      contains: "unicast discovery round-trip"
    - path: "docs/architecture/overview.md"
      provides: "Accurate Direct Connection bullet in the High-Level API layer (G-05-2)"
      contains: "unicast discovery round-trip"
    - path: "docs/api/animation.md"
      provides: "Operator-agreed Direct UDP Delivery lead-in + 'expected' drop framing (G-05-3, verbatim)"
      contains: "purpose-built network stack with the following characteristics"
    - path: "src/lifx/network/connection.py"
      provides: "Features blank line (G-05-4), plain-language retransmit bullet per UAT proposed_text (G-05-5), de-jargoned __init__ Args, de-ID'd engine docstring + traceability comment (G-05-6)"
      contains: "Automatic retransmits on an escalating schedule within each request's"
    - path: "src/lifx/network/discovery.py"
      provides: "discover_devices docstring without design-lineage vocabulary or internal schedule offsets (G-05-5 + D5-09 audit)"
      contains: "re-broadcast several times on an escalating schedule"
    - path: "src/lifx/animation/animator.py"
      provides: "Behavioural-contract Animator/AnimatorStats/send_frame docstrings — no planning IDs, no gate threshold/expiry/floor constants (G-05-6 + D5-09 audit)"
      contains: "there is no flow-control toggle; consumers keep"
    - path: "src/lifx/animation/packets.py"
      provides: "probe_template_index docstrings in reader vocabulary with IDs/spike figures/Glowup demoted to # Traceability comments; five class-docstring blank lines (G-05-6 + G-05-4 audit)"
      contains: "# Traceability:"
    - path: "src/lifx/devices/ceiling.py"
      provides: "Four Args-nested lists rendered as lists (D5-22 audit blank-line findings)"
      contains: "Either:"
    - path: "docs/api/effects.md"
      provides: "Five type annotations backticked so markdown stops parsing [Light] as a link reference (G-05-7a, D5-23)"
      contains: "(`list[Light]`)"
    - path: "docs/api/high-level.md"
      provides: "Rendered mDNS discovery function beside the UDP one (G-05-7b, D5-23)"
      contains: "::: lifx.api.discover_mdns"
    - path: "docs/api/network.md"
      provides: "Rendered low-level mDNS discovery + service record in the Discovery group (G-05-7b, D5-23); all EXISTING ::: blocks byte-identical"
      contains: "::: lifx.network.mdns.discover_lifx_services"
    - path: ".github/workflows/docs.yml"
      provides: "Both CI build invocations gated on --strict so zero-warning holds permanently (G-05-7c)"
      contains: "--strict"
  key_links:
    - from: "src/lifx/network/connection.py"
      to: "docs/architecture/overview.md"
      via: "the rendered DeviceConnection bullet states the same retransmit contract in the same words as the architecture page (overview.md:150)"
      pattern: "escalating schedule"
    - from: "docs/api/animation.md"
      to: "docs/user-guide/animation.md"
      via: "both pages state the same ack-paced latest-frame-wins contract wording"
      pattern: "paced against device acknowledgements"
    - from: "docs/getting-started/quickstart.md"
      to: "src/lifx/devices/base.py"
      via: "the IP-only lead-in describes exactly what from_ip() ships (base.py:454-516: unicast GetService to learn the serial, then construct); the serial+IP example matches docs/index.md:41"
      pattern: "serial=\"d073d5010203\""
    - from: "src/lifx/animation/animator.py"
      to: "src/lifx/animation/flow.py"
      via: "the rewritten Animator docstring states only the behavioural contract flow.py implements; the tuning constants stay in flow.py and its comments"
      pattern: "latest-frame-wins"
    - from: "docs/api/index.md"
      to: "docs/api/high-level.md"
      via: "index.md's untouched discover_mdns link resolves once the ::: target exists (the links were always correct; the destinations never existed)"
      pattern: "lifx.api.discover_mdns"
    - from: "docs/api/index.md"
      to: "docs/api/network.md"
      via: "index.md's untouched mDNS links resolve once discover_lifx_services and LifxServiceRecord render in the Discovery group"
      pattern: "lifx.network.mdns"
  prohibitions:
    - statement: "MUST NOT touch the three deferred api.py behavioural defects or their docstrings (D5-19/API-01): zero edits at api.py lines 260-331, 515-557, 974-1016; the ONLY api.py change in this plan is the single blank-line insertion in the apply_theme docstring (~line 680); create_device()'s Returns:/Raises: sections in discovery.py stay byte-identical, routed to API-01(d) per D5-21 — D5-22 authorises clarity, never paper-over"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT restate internal tuning constants in any rewritten prose (D5-09 as currently written): no retransmit or re-broadcast gap values, no outstanding-ack gate threshold, no ack expiry, no throughput-floor figure, no probe-placement rationale figures, no measured spike percentages in any docstring mkdocstrings publishes — and MUST NOT add the timing documentation, override guidance, or diagram the operator's open D5-09 dispute asks for (that work is BLOCKED on the open decision and spike candidate 006; this plan only removes jargon)"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT attribute any behaviour to any release number — no version attribution of any kind (D5-12)"
      category: values
      requirement_id: DOCS-01
    - statement: "MUST NOT edit any EXISTING ::: mkdocstrings directive block or docs/changelog.md (D5-14 widened): D5-22 covers the docstrings the ::: blocks render, never the directive blocks; the ONLY ::: changes permitted are the three ADDITIVE mDNS blocks D5-23 specifies (high-level.md x1, network.md x2 — diffs on those two pages must be insertion-only). docs/api page edits are confined to: animation.md's hand-written Performance Characteristics prose, effects.md's five annotation sites (D5-23, exactly those defects), and the additive blocks. api/index.md needs NO edit and gets none — its links are correct; their destinations never existed. Every other docs/api page stays fenced — D5-23 is narrow and MUST NOT be widened"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT change mkdocs.yml nav or add new pages (D5-01 — still holds under D5-23: both mDNS destination pages already exist and the index links already name them; if a new page or a nav change ever seems needed to resolve the anchors, STOP and report instead of proceeding)"
      category: values
      requirement_id: DOCS-01
    - statement: "MUST NOT edit any # comment's existing vocabulary or any unrendered docstring: const.py, animation/flow.py (whole module), discovery.py's private _discover_with_packet docstring (:167-186) and its schedule comment (:250), connection.py's in-body comments (:507/:515/:548/:601/:657) and private wrapper docstrings (:668-753), animator.py's module docstring (:5) and comments (:164-167), packets.py's comments (:255/:363), and every sweep site on the pinned out-of-scope map keep their internal IDs and lineage vocabulary as-is — the ONLY comment changes permitted are the three ADDITIVE # Traceability comments this plan specifies"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT edit src/lifx/devices/base.py's from_ip() docstring or any other base.py prose: G-05-2 is a doc-page-only fix (its UAT fence_note: the docstring is not a falsehood and D5-22's grant covers G-05-4/05/06 + audit classes, not G-05-2); base.py's only permitted edits are the two blank-line insertions at the Device class docstring and the capabilities property"
      category: values
      requirement_id: DOCS-01
    - statement: "MUST NOT make prose changes at the whitespace-only blank-line sites: every G-05-4/audit blank-line fix inserts exactly one (or, at the three named ceiling.py sites, two) truly-empty line(s) with zero character changes to any existing line"
      category: values
      requirement_id: DOCS-02
    - statement: "MUST NOT delete traceability outright: every planning ID removed from a rendered docstring must already exist, or be re-recorded, in a # comment or unrendered surface (the demotion map in Task 3 is the checklist)"
      category: values
      requirement_id: DOCS-02
---

<objective>
Close the six operator-diagnosed UAT gaps from 05-UAT.md (G-05-2..G-05-7) under
the D5-22 and D5-23 rulings, plus every additional defect the mandatory D5-22
audit surfaced at planning time. No re-diagnosis is needed or permitted: every
gap carries a verified root cause with file:line evidence, and two carry
operator-agreed literal text that MUST be used verbatim (G-05-3 agreed_text,
G-05-5 proposed_text).

The six gaps:
- **G-05-2 (major):** quickstart.md:95 and overview.md:253 claim IP-only
  connection skips discovery; shipped behaviour (base.py:454-516) is a unicast
  GetService round-trip to learn the serial first. Doc-page fix only.
- **G-05-3 (minor):** docs/api/animation.md:129's bypass-only lead-in replaced
  with the operator's agreed text ('characteristics' per operator ruling,
  drop-framing 'expected').
- **G-05-4 (minor):** rendered docstring headings missing the blank line before
  their lists render as run-on paragraphs. UAT pinned 8 sites; the planning-time
  audit (built-HTML verified) found 23 run-on paragraphs across 13 source files
  — all fixed here, whitespace-only.
- **G-05-5 (major):** "Photons-shaped"/"wall-time budget"/"no blind sleeps"
  design-lineage jargon on rendered pages (connection.py, discovery.py). The
  connection.py:62-63 bullet uses the UAT proposed_text verbatim, matching the
  published overview.md:150 wording.
- **G-05-6 (major):** planning IDs (RETRY-xx, D3-xx, ANIM-xx, D4-xx, spike 003,
  plan 04-07) and a Glowup lineage reference in rendered docstrings of
  connection.py, animator.py and packets.py. IDs are demoted to # comments
  (traceability preserved), and packets.py's published spike measurement is
  removed from rendered prose per D5-09.
- **G-05-7 (major, D5-23 — supersedes this plan's original 8-issue baseline):**
  the "pre-existing 8-warning baseline" every phase plan pinned is 8 real
  defects D5-14 deliberately deferred; the operator reopened them. Two
  unrelated problems: (a) api/effects.md x5 — type annotations outside the
  backticks make markdown parse [Light] as an unresolvable link reference;
  (b) api/index.md x3 — the mDNS links are correct but their ::: render
  targets never existed anywhere; fixed by rendering the three public mDNS
  symbols on the pages the links already name (index.md itself untouched).
  The gate INVERTS: `uv run zensical build --strict` must exit 0 (zero
  warnings, no baseline), and both CI build invocations in docs.yml gain
  --strict so the class cannot regress.

The D5-22 audit is part of the work, not optional: the operator chose full
closure inside Phase 5, accepting that the known leak list was incomplete. The
planning-time audit already ran all three sweeps (internal vocabulary, missing
blank lines, D5-09 constants) over every :::-reachable docstring and pinned the
findings and dispositions in Task 3; execution re-runs the sweeps to confirm
zero residual findings and catch drift.

Every target was re-verified against shipped source and the built site during
planning (2026-07-17, HEAD c139542): from_ip's GetService round-trip
(base.py:454-516), the ::: target list (grep over docs/api/*.md), the rendered
HTML for each jargon/ID/run-on occurrence (site/api/*/index.html), griffe's
handling of blank lines inside Args entries (probed against the installed
griffelib 2.0.2 — entries stay intact, lists render), ruff line-length 88 on all
touched src files, and the residual out-of-scope map (comments/private/module
docstrings that do not render).

Purpose: the phase goal is accurate, readable guidance; these are the defects
the operator found reading the shipped site. Output: 22 files corrected in
place (18 docs/src accuracy targets + effects.md, high-level.md and network.md
under D5-23 + the CI workflow). No new pages, no nav changes, no behavioural
code changes.

Decisions implemented: D5-22 (scope + mandatory audit), D5-23 (8-warning
baseline eliminated; --strict gate; mDNS targets rendered). Boundaries held:
D5-19 (API-01 untouched, incl. create_device Returns:/Raises: per D5-21),
D5-09 as written (no tuning constants; the open dispute is NOT acted on — no
timings, no diagram, no override guidance), D5-12 (no versions), D5-14 widened
(existing ::: blocks + changelog fenced; D5-23's lift is exactly two pages,
exactly these defects), D5-01 (no nav/new pages — the mDNS destinations
already exist), # comments out of scope.
</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-UAT.md
@.planning/phases/05-reliability-documentation/05-CONTEXT.md
</context>

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

| SOURCE   | ID     | Item                                                                  | Task | Status  |
|----------|--------|------------------------------------------------------------------------|------|---------|
| GOAL     | —      | Accurate guidance on guaranteed wire behaviour + quirks                | 1–3  | COVERED |
| REQ      | DOCS-01| Discovery guidance accuracy (quickstart/overview direct-connection claims) | 1 | COVERED |
| REQ      | DOCS-02| Streaming/animation + concurrency guidance accuracy (animation lead-in, rendered docstrings) | 1–3 | COVERED |
| RESEARCH | —      | §Package Legitimacy Audit: no package installs in this phase           | —    | COVERED (n/a) |
| UAT      | G-05-2 | IP-only ≠ no-discovery; serial+IP is the honest zero-discovery path    | 1    | COVERED |
| UAT      | G-05-3 | animation.md lead-in agreed_text verbatim + 'expected'                 | 1    | COVERED |
| UAT      | G-05-4 | Blank lines before lists in rendered docstrings (8 UAT + audit finds)  | 2, 3 | COVERED |
| UAT      | G-05-5 | Plain-language retransmit prose on every rendered surface              | 2    | COVERED |
| UAT      | G-05-6 | Planning IDs demoted from rendered docstrings; spike figure removed    | 2, 3 | COVERED |
| UAT      | G-05-7 | 8-warning baseline eliminated: effects.md backticks, mDNS ::: targets, CI --strict | 1, 3 | COVERED |
| CONTEXT  | D5-22  | Mandatory audit over every :::-reachable docstring (3 defect classes)  | 3    | COVERED |
| CONTEXT  | D5-23  | D5-14 lifted for exactly effects.md/index.md defects; gate inverts to --strict; mDNS docstrings inherit the D5-22 audit before going live | 1, 3 | COVERED |
| CONTEXT  | D5-19/D5-21 | api.py defects + create_device Returns:/Raises: stay untouched    | —    | EXCLUDED (deferred API-01; guarded by prohibition 1) |
| CONTEXT  | D5-09 — OPEN | Timing docs / override guidance / diagram for flow control       | —    | EXCLUDED (blocked on open decision + unscheduled spike 006; explicitly out of scope) |

No MISSING rows. Exclusions are operator-decided (D5-19/D5-21) or blocked-open
(D5-09 dispute). Deferred Ideas (API-01) are not planned.

## Planning-time D5-22 audit — findings and dispositions

The three mandated sweeps ran at planning time over every :::-reachable
docstring. The target list is enumerated live by
`grep -rh '^::: ' docs/api/*.md` — cite the command, not a count, so the scope
cannot go stale; Task 3's vocabulary sweep derives its file set from that same
command at run time. Execution re-runs all sweeps in Task 3.

**Sweep 1 — internal vocabulary.** Repo grep for
`Photons|Glowup|(ANIM|DISC|RETRY|DOCS)-nn|Dn-nn|spike nnn|plan nn-nn` found
rendered-surface hits only in the four files the UAT already names
(connection.py, discovery.py, animator.py, packets.py — fixed in Tasks 2-3).
The target-derived execution-time sweep was also run at planning time over
the full :::-reachable set (51 files resolved from the target list): every
hit fell inside those same four files; the other 47 reachable files are
clean, so the zero-tolerance gate passes post-fix with an empty allowlist.
All other hits are out of scope and MUST remain untouched: const.py:36/48/51
(comments), flow.py (unrendered module, no ::: target), discovery.py:167-186
(private `_discover_with_packet` docstring, incl. its own schedule offsets) and
:250 (comment), connection.py:507/515/548/601/657 (comments) and :668-753
(private wrapper docstrings, incl. D3-06 at :716), animator.py:5 (unrendered
module docstring) and :164-167 (comments), packets.py:255/:363 (comments).

**Sweep 2 — missing blank lines.** AST sweep (heading line ending ':' with a
list item directly beneath, inside a docstring) found 44 sites; 28 are
:::-reachable (fixed: Tasks 2-3), 16 are not and MUST remain untouched:
animation/__init__.py ('Key Components:', 'All color data uses protocol-ready
uint16 values:'), framebuffer.py:13, packets.py:14 ('Supported Devices:') and
:597 (LightPacketGenerator — no ::: target), ceiling.py module (:6/:8/:12) and
private `_determine_*_brightness` (:1076/:1139), detection.py:27, matrix.py:7,
discovery.py:177, quirks.py:23, header.py:3 (module — only the LifxHeader class
is targeted), theme/generators.py:148. Built-HTML verification: 23 run-on
`<p>` paragraphs across site/api/ at planning time; target after fix is 0.
The UAT's 8-site list was a subset — the audit additionally surfaced rendered
run-ons in packets.py (5 class-docstring sites), color.py, light.py (class),
multizone.py, ceiling.py (4 Args sites), effects/pulse.py (2 Args sites) and
protocol/generator.py (5 public-function sites — `::: lifx.protocol.generator`
is a module target that renders its public functions; HTML confirmed).

**Sweep 3 — D5-09 tuning constants in rendered prose.** Found: the spike
measurement at packets.py:163-164 and :284 (already G-05-6 targets), the gate
threshold / ~1 s expiry / 2 frames/s floor in the Animator class docstring and
send_frame (:89-99, :355-364), the outstanding-ack threshold clause in
send_frame, and the re-broadcast cumulative offsets + internal decomposition in
discover_devices (:490-491, :498-506). All fixed in Tasks 2-3. NOT D5-09
violations (kept): public parameter defaults (timeout 16.0, max_retries 8,
DISCOVERY_TIMEOUT 15 s — the D5-13 precedent), the ~4 s idle window (derived
from the public `max_response_time` × `idle_timeout_multiplier` parameters and
already documented in CLAUDE.md), and MatrixPacketGenerator's packet-count
arithmetic (protocol wire facts — Set64's 64-colour ceiling — not tuning
knobs).

**Sweep 4 — the three not-yet-published mDNS docstrings (D5-23 inheritance).**
`lifx.api.discover_mdns` (api.py:796-837), `discover_lifx_services`
(network/mdns/discovery.py:177-205) and `LifxServiceRecord`
(network/mdns/types.py:10-26) have never rendered, so the earlier audits never
covered them. Audited at planning time against all three D5-22 defect classes:
**all three are CLEAN** — no internal vocabulary or planning IDs, no
heading-plus-list sites (the AST sweep confirms), and no D5-09 constants
("default 15.0" is a public parameter default per the D5-13 precedent;
"224.0.0.251:5353" and "typically 56700" are public protocol facts, not
internal tuning). No source edit is expected when they go live; Task 3's
target-derived scan re-verifies at execution time (the mdns modules resolve
into its file set automatically once the ::: blocks exist) and any drift
finding is in scope.

## Flagged planner assumptions (surfaced, not dropped)

1. **Idempotency** — every edit is an exact-string or pinned-line replacement
   whose source text no longer exists post-edit; re-execution is a detectable
   no-op. Flagged for verification.
2. **Concurrency** — this is the only unexecuted plan in the phase (wave 5, all
   dependencies complete); tasks run sequentially with one signed commit per
   task. Task 1 ends with a green non-strict build at the documented interim
   3-warning state (the api/index.md anchors Task 3 closes), Task 2 with the
   network test suite, Task 3 with the full gate set including the --strict
   build — an interruption at any commit boundary leaves a clean, buildable
   state. Flagged for verification.
3. **Live verifier instruction (carried from 05-05)** — the verifier must
   independently probe factual claims in every edited file rather than trust
   these truths; additionally: (a) the UAT located the "Photons-shaped"/ID text
   for G-05-5/G-05-6 partly in `_transmit_and_listen` (connection.py:439-493),
   which the planning-time built-HTML check shows does NOT currently render
   (mkdocstrings' default private filter). This plan fixes those sites anyway —
   they are UAT-listed, D5-22-reachable (DeviceConnection is a ::: target), and
   fixing them is robust to future filter changes — but the verifier should
   judge G-05-5/G-05-6 closure by the RENDERED surfaces (built HTML), not by
   private docstrings. (b) The blank-line fix set is larger than the UAT's
   8-site list because the audit was mandatory and found more; the run-on
   detector on built HTML is the authoritative gate.

<capability_checkpoints>
- **api-coverage**: FALSE POSITIVE — no external API/SDK/service integrated; the
  matches are lifx-async's own API documented in its own docs. No COVERAGE.md.
- **assumption-delta**: no singular→plural, required→optional, or
  derived→chosen transitions in a docs-accuracy gap closure. `no-change`.
- **schema-gate**: NOT APPLICABLE — zero-dependency Python UDP library, no ORM.
</capability_checkpoints>

<tasks>

<task type="auto">
  <name>Task 1: G-05-2 + G-05-3 + G-05-7a — honest direct-connection docs, the agreed animation lead-in, and the effects.md link-reference fixes</name>
  <files>docs/getting-started/quickstart.md, docs/architecture/overview.md, docs/api/animation.md, docs/api/effects.md</files>
  <read_first>
    - .planning/phases/05-reliability-documentation/05-UAT.md (Gaps G-05-2, G-05-3 — agreed_text is canonical and verbatim — and G-05-7 part (a))
    - .planning/phases/05-reliability-documentation/05-CONTEXT.md (D5-23 — the narrow D5-14 lift for exactly these effects.md defects)
    - docs/getting-started/quickstart.md lines 93-115 (the section to rewrite sits between "## Common Patterns" and "### Find Specific Devices")
    - docs/architecture/overview.md lines 247-260 (Layer 8 bullets; line 253 is the target)
    - docs/api/animation.md lines 125-140 (hand-written Performance Characteristics prose; the ::: blocks above line 125 are absolutely fenced)
    - docs/api/effects.md lines 55-62, 80-84, 99-103, 113-117 and 192-196 (the five unresolved-link sites — read EACH individually; :194 is a #### heading, the other four are Parameters list items)
    - docs/index.md lines 35-48 (the accurate serial+IP precedent wording and example)
    - src/lifx/devices/base.py lines 454-516 (from_ip's shipped behaviour — READ ONLY, do not edit: serial=None opens a temp connection with the broadcast serial, sends Device.GetService() to the IP, reads the serial off StateService, closes, then constructs)
  </read_first>
  <action>
    Three doc-page fixes. All three files are hand-written prose outside the
    mkdocstrings fence (G-05-2/G-05-3 fence_notes); the ::: blocks in
    docs/api/animation.md stay byte-identical.

    quickstart.md (G-05-2 — the section at lines 95-110):
    1. The heading at line 95 becomes exactly `### Direct Connection` (the
       parenthetical claim is dropped; zero inbound links to the old anchor
       exist anywhere in docs/ or mkdocs.yml — verified by grep at planning
       time).
    2. The one-line lead-in at line 97 becomes this paragraph (wrapped
       naturally):
       "If you only know the IP address, `from_ip()` first performs a unicast
       discovery round-trip: it sends `GetService` to that address to learn
       the device's serial number, then connects using both:"
    3. The existing fenced python example (lines 99-110: imports, main() with
       `Light.from_ip("192.168.1.100")`, asyncio.run) stays byte-identical.
    4. After that example's closing fence and before the "### Find Specific
       Devices" heading, insert a new paragraph plus a second fenced python
       example:
       - Paragraph: "If you know both the serial and the IP address, no
         discovery of any kind is needed — the library connects straight to
         the device:"
       - Example: byte-identical to the first example except the connection
         line, which becomes
         `async with Light(serial="d073d5010203", ip="192.168.1.100") as light:`
         — the constructor form, NOT awaited (constructors are not awaitable;
         this matches docs/index.md:41 and the WR-03 fix).

    overview.md (G-05-2):
    5. The Direct Connection bullet at line 253 becomes exactly:
       `- **Direct Connection**: Connect by serial and IP with no discovery, or by IP alone via a one-off unicast discovery round-trip`

    docs/api/animation.md (G-05-3 — operator agreed_text, VERBATIM, from
    05-UAT.md; do not paraphrase, do not restore the word the operator
    rejected in the resolved_nit):
    6. The lead-in sentence at line 129 becomes exactly:
       "The animation module uses a purpose-built network stack with the following characteristics:"
    7. Bullets at lines 131-132 are already byte-identical to the agreed text
       — leave them untouched.
    8. The third bullet (line 133) becomes exactly:
       "- A dropped frame is never retried; occasional frame loss under saturation is expected (visual artefacts are brief)"
       (only the intent word changes; everything else in the bullet is
       byte-identical). This wording holds the 05-05 latest-frame-wins
       prohibition: drops are still framed as intended behaviour, and no
       retry advice appears (the UAT confirmed verdict 7 holds under either
       wording).

    docs/api/effects.md (G-05-7a — D5-23 lifts D5-14 for exactly these five
    defects on this page; nothing else on the page may change). The defect: a
    type annotation sits OUTSIDE the backticks, so markdown parses the
    bracketed type as a shortcut link reference it cannot resolve. The fix at
    every site is to backtick the annotation. Verify each site individually
    before editing — they are not identical in shape:
    9. Line 60 (list item): the annotation becomes backticked so the line
       reads exactly (backticks literal):
       - `participants` (`list[Light]`): List of Light instances to apply effect to
       The `effect` (LIFXEffect) item above it has no brackets and stays.
    10. Line 82 (list item), exactly:
        - `lights` (`list[Light]`): List of lights to stop
    11. Line 101 (list item), exactly:
        - `lights` (`list[Light]`): List of lights to add
    12. Line 115 (list item), exactly:
        - `lights` (`list[Light]`): List of lights to remove
        (the neighbouring `restore_state` (bool) item has no brackets and
        stays byte-identical)
    13. Line 194 (a #### HEADING, not a list item), exactly:
        #### `participants` (`list[Light]`)
    api/index.md is NOT touched in this task or any other — its three
    remaining warnings are closed in Task 3 by rendering the missing mDNS
    targets (the links are correct; the destinations never existed).

    Australian English throughout. Commit with `git commit -s` (GPG-signed,
    automatic).
  </action>
  <verify>
    <automated>set -e
test "$(grep -c 'No Discovery' docs/getting-started/quickstart.md)" = "0"
grep -qF '### Direct Connection' docs/getting-started/quickstart.md
test "$(grep -c 'unicast discovery round-trip' docs/getting-started/quickstart.md)" = "1"
grep -qF 'async with Light(serial="d073d5010203", ip="192.168.1.100") as light:' docs/getting-started/quickstart.md
test "$(grep -c 'no discovery of any kind' docs/getting-started/quickstart.md)" = "1"
! grep -qF 'await Light(serial=' docs/getting-started/quickstart.md
! grep -qF 'Connect by IP without discovery' docs/architecture/overview.md
test "$(grep -c 'unicast discovery round-trip' docs/architecture/overview.md)" = "1"
grep -qF 'purpose-built network stack with the following characteristics:' docs/api/animation.md
! grep -qF 'bypasses the connection layer' docs/api/animation.md
grep -qF 'is expected (visual artefacts are brief)' docs/api/animation.md
test "$(grep -c 'is by design' docs/api/animation.md)" = "0"
grep -qF 'paced against device acknowledgements' docs/api/animation.md
git diff c139542 -- docs/api/animation.md | { ! grep -qE '^[+-]:::'; }
test "$(grep -cF '(`list[Light]`)' docs/api/effects.md)" = "5"
! grep -qF '(list[Light])' docs/api/effects.md
grep -qF '#### `participants` (`list[Light]`)' docs/api/effects.md
test -z "$(git diff c139542 -- docs/api/effects.md | grep '^-' | grep -vE '^---|^-.*list\[Light\]')"
OUT=$(uv run zensical build 2>&1)
echo "$OUT" | grep -qF '3 issues found'
! echo "$OUT" | grep -qF 'effects'</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c 'No Discovery' docs/getting-started/quickstart.md` outputs 0 (was 1); the new heading `### Direct Connection` is present
    - quickstart carries exactly one 'unicast discovery round-trip' lead-in, the untouched from_ip example, and one serial+IP constructor example (`async with Light(serial=...`, never awaited)
    - `grep -c 'Connect by IP without discovery' docs/architecture/overview.md` outputs 0 (was 1); the replacement bullet names both paths
    - docs/api/animation.md carries the agreed_text lead-in verbatim and the 'expected' bullet; 'is by design' outputs 0 (was 1); bullets 1-2 byte-identical (still contain 'paced against device acknowledgements')
    - `git diff c139542 -- docs/api/animation.md` contains no `+`/`-` line starting `:::` — the mkdocstrings fence held
    - Backticked-annotation count in docs/api/effects.md outputs 5 (was 0) and the unbackticked form outputs 0 (was 5); the :194 heading form is present; the effects.md diff removes only lines carrying the old annotation shape (D5-23 held to exactly these defects)
    - `uv run zensical build` exits 0 reporting exactly '3 issues found' — effects.md's 5 warnings are gone; the remaining 3 are api/index.md's mDNS anchors, closed in Task 3 — and no issue line names effects
  </acceptance_criteria>
  <done>No published page claims IP-only connection skips discovery; the genuine zero-discovery path (serial+IP) is documented with the index.md-precedent form; the animation lead-in is the operator's agreed text verbatim with 'expected' drop framing; all five effects.md link-reference warnings are closed by backticked annotations with nothing else on the page changed.</done>
</task>

<task type="auto">
  <name>Task 2: G-05-5 + connection/discovery shares of G-05-4/G-05-6 — de-jargon the network-layer rendered docstrings</name>
  <files>src/lifx/network/connection.py, src/lifx/network/discovery.py</files>
  <read_first>
    - .planning/phases/05-reliability-documentation/05-UAT.md (Gap G-05-5 — proposed_text is canonical and verbatim; Gap G-05-4 site list; Gap G-05-6 connection.py sites)
    - src/lifx/network/connection.py lines 52-117 (class docstring Features list + __init__ Args) and 429-520 (the private engine docstring + the comment at :515 which stays untouched)
    - src/lifx/network/discovery.py lines 478-530 (discover_devices docstring: opening paragraph, Note, Args, Yields, Example — only the paragraph and Note change) and 167-186 (the private _discover_with_packet docstring — READ ONLY: it keeps its lineage vocabulary and offsets, out of scope per the UAT)
    - docs/architecture/overview.md line 150 (the published wording anchor: retransmits on an escalating schedule within each request's timeout)
  </read_first>
  <action>
    Six edits across the two network files. Keep every rewritten line within
    ruff's 88-character limit (the 05-05 E501 deviation precedent); you may
    adjust wrap points of the prescribed text so long as each gate substring
    below stays intact on a single line.

    connection.py:
    1. (G-05-4, the operator-reported site) Insert one truly-empty line
       between the `Features:` heading (line 59) and the first bullet, so the
       published list renders as a list.
    2. (G-05-5, UAT proposed_text VERBATIM) The second Features bullet — the
       two lines at 62-63 (the jargon bullet the operator reported) — becomes
       exactly:
       `    - Automatic retransmits on an escalating schedule within each request's` /
       `      timeout, listening for a reply throughout`
       This matches the phrasing already published at overview.md:150, so the
       docstring and the architecture page state the same contract in the
       same words. The sibling bullets (lazy opening; async generator
       streaming; response correlation — the 05-05 rewrite; sequence
       management) stay byte-identical.
    3. (G-05-5) The `__init__` Args descriptions for `max_retries` and
       `timeout` (lines ~106-116) are rewritten to drop the time-budget
       shorthand while preserving every behavioural claim. Replace the two
       entries with exactly:
       `            max_retries: Maximum number of retransmits within the timeout` /
       `                (default: 8). Total transmissions are at most` /
       `                max_retries + 1; after the cap is reached the request keeps` /
       `                listening for a reply until the timeout expires instead of` /
       `                failing early. Whichever of the retransmit cap and the` /
       `                timeout is reached first wins.` /
       `            timeout: Default timeout for requests in seconds (default: 16.0).` /
       `                The timeout is an overall limit on the whole request: all` /
       `                waiting -- transmissions, retransmit gaps, and the final` /
       `                listen window -- counts against it, so a request can never` /
       `                take materially longer than the timeout it was given.`
       The `serial`/`ip`/`port` entries above stay byte-identical. The public
       defaults (8, 16.0) legitimately remain — they are documented API, not
       D5-09 constants.
    4. (G-05-5 + G-05-6) The `_transmit_and_listen` docstring (lines 439-493)
       is rewritten to strip the design-lineage adjective and every
       parenthetical planning-ID citation while keeping all engineering
       content. Concretely, in-place phrase surgery — every other word stays:
       - :443 "the Photons-shaped retransmit schedule" -> "the escalating
         retransmit schedule"
         <!-- planner-discipline-allow: Photons-shaped -->
       - :450 the paragraph opener "Schedule (RETRY-01, D3-01):" -> "Schedule:"
       - :457 "Wall budget (RETRY-03, D3-03): ``timeout`` is a hard wall-time
         deadline" -> "Deadline: ``timeout`` is a hard deadline"
         <!-- planner-discipline-allow: wall-time -->
       - :461 drop the trailing citation so the sentence ends "anywhere in
         this loop."
       - :463 "``max_retries`` interaction rule (D3-05):" ->
         "``max_retries`` interaction rule:"
       - :465-466 "The wall deadline caps *time*." -> "The deadline caps
         *time*."
       - :469 "Correlation contract (RETRY-04, D3-04):" ->
         "Correlation contract:"
       - :479 Args "Overall wall-time budget for the whole request" ->
         "Overall time budget for the whole request"
       - :480 Args "Maximum retransmits within the wall-time budget" ->
         "Maximum retransmits within the timeout"
       Everything else in the docstring (the wrapper description, retransmit
       mechanics, correlation contract prose, Yields, Raises) stays
       byte-identical.
    5. (G-05-6 demotion — traceability preserved, not deleted) Immediately
       after the engine docstring's closing quotes, insert this comment
       block (8-space indent):
       `        # Traceability: schedule RETRY-01/D3-01; listen-while-waiting` /
       `        # RETRY-02/D3-02; wall-clock budget RETRY-03/D3-03; shared-queue` /
       `        # correlation RETRY-04/D3-04; max_retries interaction rule D3-05.`
       Touch nothing else in the file: the comment at :515 keeps its lineage
       vocabulary (comments are out of scope and legitimate traceability),
       as do :507/:548/:601/:657 and the private wrapper docstrings
       (incl. D3-06 at :716).

    discovery.py (only the PUBLIC discover_devices docstring changes; the
    private _discover_with_packet docstring at :167-186 keeps its lineage
    adjective and offsets — it does not render):
    6. (G-05-5 + D5-09 audit) The opening paragraph (lines ~489-496) becomes
       exactly:
       `    Sends a broadcast DeviceGetService packet and yields devices as they respond.` /
       `    The packet is re-broadcast several times on an escalating schedule within` /
       `    the discovery window, so devices behind a lossy access point that miss` /
       `    the first broadcast are still found. Implements DoS protection via` /
       `    timeout, source validation, and serial validation. Serial validation and` /
       `    per-serial deduplication are enforced inside ``_discover_with_packet``,` /
       `    so every caller of that shared generator benefits.`
       (The internal cumulative-offset values are removed from rendered prose
       per D5-09; their traceability lives on in the private docstring at
       :172-173, the :250 comment, and const.py — no demotion needed.)
       And the Note block (lines ~498-506) becomes exactly:
       `    Note:` /
       `        On a populated network, generator *completion* now typically takes` /
       `        longer than a single broadcast alone would: re-broadcasts continue` /
       `        for several seconds into the window, and the generator then waits` /
       `        out the ~4 s idle window (``max_response_time`` ×` /
       `        ``idle_timeout_multiplier``) after the last response -- still well` /
       `        inside ``DISCOVERY_TIMEOUT`` (15 s). Streaming consumers` /
       `        (``async for``) see the first devices at unchanged latency; only` /
       `        overall completion moves later, because later re-broadcasts` /
       `        legitimately keep finding new devices and resetting the idle` /
       `        window.`
       The ~4 s figure derives from the two PUBLIC parameters named beside it
       and stays (CLAUDE.md documents the same derivation); 15 s is the
       documented public default (D5-13 precedent). The Args, Yields and
       Example sections stay byte-identical.

    Commit with `git commit -s`.
  </action>
  <verify>
    <automated>set -e
test "$(grep -c 'wall-time' src/lifx/network/connection.py)" = "0"
test -z "$(grep -A1 '^    Features:$' src/lifx/network/connection.py | tail -n 1)"
grep -qF "Automatic retransmits on an escalating schedule within each request's" src/lifx/network/connection.py
grep -qF 'timeout, listening for a reply throughout' src/lifx/network/connection.py
grep -qF 'an overall limit on the whole request' src/lifx/network/connection.py
! grep -qE '\(RETRY-0[0-9], D3-0[0-9]\)' src/lifx/network/connection.py
! grep -qF '(D3-05)' src/lifx/network/connection.py
grep -qF '# Traceability: schedule RETRY-01/D3-01' src/lifx/network/connection.py
grep -qF 'Photons-shaped retransmit schedule. Read the module attribute' src/lifx/network/connection.py
grep -qF 're-broadcast several times on an escalating schedule' src/lifx/network/discovery.py
test "$(grep -c 'cumulative offsets' src/lifx/network/discovery.py)" = "1"
test "$(grep -c 'Photons-shaped' src/lifx/network/discovery.py)" = "1"
git diff c139542 -- src/lifx/network/discovery.py | { ! grep -qE '^[+-].*(Returns:|Raises:|Example:)'; }
uv run python - << 'PY'
import ast, re, sys
PAT = re.compile(
    r"\b(ANIM|DISC|RETRY|DOCS)-\d{2}\b|\bD\d-\d{2}\b|[Ss]pike \d{3}"
    r"|\bplan \d{2}-\d{2}\b|Photons|Glowup|wall-time|blind sleep"
    r"|cumulative offsets"
)
bad = []
for path in ("src/lifx/network/connection.py", "src/lifx/network/discovery.py"):
    tree = ast.parse(open(path).read())
    for node in ast.walk(tree):
        if not isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
            continue
        if node.name.startswith("_") and node.name != "__init__":
            continue
        doc = ast.get_docstring(node)
        if doc:
            bad += [f"{path}:{node.name}: {m.group(0)}" for m in PAT.finditer(doc)]
print("\n".join(bad))
sys.exit(1 if bad else 0)
PY
uv run ruff check src/lifx/network/
uv run pytest tests/test_network/ -q</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c 'wall-time' src/lifx/network/connection.py` outputs 0 (was 6: the Features bullet, three __init__ Args lines, and two engine-docstring lines); const.py:51's comment keeps its wording untouched (different file, out of scope)
    - The line directly after `    Features:` is empty — the operator-reported G-05-4 site is fixed
    - The proposed_text bullet is present verbatim across its two lines; the 05-05 correlation bullet ('routes each reply to its request') is byte-identical
    - `grep -cE '\(RETRY-0[0-9], D3-0[0-9]\)'` outputs 0 (was 3) and `grep -cF '(D3-05)'` outputs 0 (was 1) in connection.py; the new `# Traceability:` comment carries all five IDs; the :515 comment still opens 'Photons-shaped retransmit schedule. Read the module attribute' (comments untouched)
    - discovery.py: 'cumulative offsets' outputs 1 (was 2 — only the private :172 site remains) and the lineage adjective count outputs 1 (was 2 — only the private :171 site remains); the public paragraph reads 're-broadcast several times on an escalating schedule'
    - `git diff c139542 -- src/lifx/network/discovery.py` touches no Returns:/Raises:/Example: line — the Yields/Args/Example sections and the D5-21-routed create_device docstring are byte-identical
    - The public-docstring scan over both files prints nothing and exits 0 (private docstrings and # comments are excluded by construction — region-scoped per the residual map)
    - `uv run ruff check src/lifx/network/` exits 0 (E501 88-col respected) and `uv run pytest tests/test_network/ -q` exits 0
  </acceptance_criteria>
  <done>The rendered DeviceConnection and discover_devices docstrings speak plain reader vocabulary stating the same contract as the architecture page; the Features list renders as a list; every removed ID survives in the new traceability comment; private docstrings and comments keep their engineering vocabulary untouched.</done>
</task>

<task type="auto">
  <name>Task 3: G-05-6 animation docstrings + full G-05-4/audit blank-line set + G-05-7b/c mDNS render targets and CI --strict + D5-22 audit re-run + full gates</name>
  <files>src/lifx/animation/animator.py, src/lifx/animation/packets.py, src/lifx/animation/orientation.py, src/lifx/api.py, src/lifx/color.py, src/lifx/devices/base.py, src/lifx/devices/ceiling.py, src/lifx/devices/light.py, src/lifx/devices/matrix.py, src/lifx/devices/multizone.py, src/lifx/effects/pulse.py, src/lifx/protocol/base.py, src/lifx/protocol/generator.py, docs/api/high-level.md, docs/api/network.md, .github/workflows/docs.yml</files>
  <read_first>
    - .planning/phases/05-reliability-documentation/05-UAT.md (Gap G-05-6 incl. audit_required; Gap G-05-4 site list; Gap G-05-7 parts (b) and (c))
    - .planning/phases/05-reliability-documentation/05-CONTEXT.md (D5-22 — scope and surviving fences; D5-23 — the mDNS render ruling and the --strict gate inversion; "D5-09 — OPEN" — the timing/diagram/override work that MUST NOT be started here)
    - docs/api/high-level.md lines 1-16 (the ::: lifx.api.discover block at :8 and its options — the pattern the new block must match exactly)
    - docs/api/network.md lines 1-31 (the Discovery group: discover_devices at :9 and DiscoveredDevice at :15, both with show_root_heading/heading_level: 3/members_order: source; the new blocks go after DiscoveredDevice's, before the "### DiscoveryResponse" heading)
    - .github/workflows/docs.yml lines 54-58 and 112-116 (the two `uv run zensical build --clean` invocations)
    - src/lifx/api.py lines 796-840 (discover_mdns docstring — audited CLEAN at planning time, no edit expected), src/lifx/network/mdns/discovery.py lines 177-205 (discover_lifx_services — CLEAN) and src/lifx/network/mdns/types.py lines 1-30 (LifxServiceRecord — CLEAN)
    - src/lifx/animation/animator.py lines 61-110 (AnimatorStats attrs + Animator class docstring) and 351-370 (send_frame)
    - src/lifx/animation/packets.py lines 155-210 (PacketGenerator.probe_template_index + MatrixPacketGenerator class docstring), 250-290 (MatrixPacketGenerator.probe_template_index), 470-480 (MultiZonePacketGenerator docstring)
    - src/lifx/animation/flow.py lines 1-60 (READ ONLY — the behavioural contract source; its constants and IDs stay where they are; do not edit, do not copy figures into any docstring)
    - The whitespace-site files listed in the action (read each target docstring before editing)
  </read_first>
  <action>
    Three groups: the animation-layer prose rewrites (G-05-6 + D5-09 audit),
    the mechanical blank-line set (G-05-4 + audit), and the mandated audit
    re-run. Keep every rewritten line within ruff's 88-character limit; wrap
    points may shift so long as each gate substring stays intact on one line.

    animator.py (G-05-6 + D5-09; the module docstring at :5 and the comments
    at :164-167 are out of scope and stay byte-identical):
    1. AnimatorStats `gated` attribute description: remove the parenthetical
       requirement-ID citation so the first sentence ends "...dropped by
       ack-gated flow control." Every other word of the entry stays,
       rewrapped as needed.
    2. AnimatorStats `acks_outstanding` description: remove the trailing
       parenthetical decision-ID citation so it ends "...consumers cannot
       configure flow control."
    3. The Animator class docstring's first paragraph (lines ~89-99) becomes
       exactly (behavioural contract only — the probe-placement mechanism
       sentence, the outstanding-ack threshold, the expiry figure, the
       throughput-floor figure and both ID citations are removed):
       `    Sends animation frames directly via UDP for maximum throughput. No` /
       `    connection layer overhead -- frames are paced internally by ack-gated` /
       `    flow control rather than fired blind: delivery is paced against device` /
       `    acknowledgements, and when a device falls behind, new frames are` /
       `    dropped, never queued (latest-frame-wins). If a device stops` /
       `    acknowledging entirely, throughput degrades to a slow floor rather` /
       `    than stalling forever. This is entirely internal behaviour -- there is` /
       `    no flow-control toggle; consumers keep calling `send_frame()` exactly` /
       `    as before.`
       The second paragraph ("All packets are prebaked...") and everything
       below stay byte-identical.
    4. send_frame's first docstring paragraph (lines ~355-364) becomes
       exactly:
       `        Applies orientation mapping (for matrix devices), updates colors` /
       `        in prebaked packets, and sends them directly via UDP -- unless` /
       `        ack-gated flow control drops the frame first. Each call first` /
       `        sweeps the animator's own socket for arrived acks; if the device` /
       `        is still behind on acknowledgements after the sweep, the frame is` /
       `        dropped entirely (no framebuffer work, no packets sent, no` /
       `        sequence numbers consumed) and the caller should simply send its` /
       `        next frame rather than retry or queue this one (latest-frame-wins).`
       Demotion map (verify, do not edit): every ID removed from animator.py
       docstrings already survives elsewhere — ANIM-01 (module docstring :5,
       comment :164, flow.py), ANIM-02 (comment :164, flow.py:1), D4-02
       (flow.py:4), D4-03 (comment :164), D4-04 (comment :167, packets.py
       comments). No new comment is needed in this file.

    packets.py (G-05-6 + D5-09 + G-05-4):
    5. PacketGenerator.probe_template_index's docstring becomes exactly:
       `        """Template index that carries the ack-required flow-control probe.` /
       (blank line) /
       `        The Animator bakes the `ack_required` flag into exactly one` /
       `        prebaked template at initialisation time -- this property names` /
       `        which one. Default is the first packet of the frame. Flow control` /
       `        applies uniformly to all generator families with no per-family` /
       `        carve-outs; `MatrixPacketGenerator` overrides this for large-tile` /
       `        mode.` /
       `        """`
    6. Immediately above that property's `@property` line insert (4-space
       indent — the demoted traceability, measured figure included per the
       prefer-demotion rule; comments do not render):
       `    # Traceability: single baked probe D4-03; first-packet arm D4-01 (spike` /
       `    # 003 measured it at 0.0% concurrent-query loss); uniform application` /
       `    # D4-02; large-tile override D4-04.`
    7. MatrixPacketGenerator.probe_template_index's docstring becomes exactly
       (the phase-record sentence, the field-lineage attribution, the UAT/plan
       citation and the measured-arm tail are demoted; all engineering
       rationale stays):
       `        """Template index that carries the ack-required flow-control probe.` /
       (blank line) /
       `        In large-tile mode the probe attaches to the FINAL CopyFrameBuffer` /
       `        -- the frame-commit packet, since nothing is visible until the` /
       `        buffer swap. The CopyFB's ack RTT includes the device's drain of` /
       `        the preceding Set64 burst, a strictly better congestion signal` /
       `        than acking the first Set64 of a multi-packet frame. This property` /
       `        is the one-line fallback seam to index 0 (first Set64) should` /
       `        hardware disagree.` /
       (blank line) /
       `        For standard (≤64px) tiles, the probe sits on the single Set64` /
       `        packet.` /
       `        """`
    8. Immediately above its `@property` line insert:
       `    # Traceability: D4-04 (probe on the frame-commit CopyFB, matching` /
       `    # Glowup's proven field behaviour on >64-zone ceilings);` /
       `    # hardware-validated in the ANIM-04 UAT (plan 04-07); the single-Set64` /
       `    # arm is the one spike 003 measured (0.0% loss).`
    9. Blank-line insertions (whitespace-only) in packets.py class docstrings:
       one empty line after each of 'For standard tiles (≤64 pixels):',
       'widths that do not evenly divide 64:', 'Set64 Payload Layout (522
       bytes):', 'CopyFrameBuffer Payload Layout (15 bytes):'
       (MatrixPacketGenerator) and 'SetExtendedColorZones Payload Layout (664
       bytes):' (MultiZonePacketGenerator). Do NOT touch LightPacketGenerator
       — it has no ::: target and is out of scope.

    Whitespace-only blank-line set (G-05-4 + audit; insert exactly one empty
    line between the quoted heading and its list — zero character changes to
    any existing line):
    10. src/lifx/animation/orientation.py — Orientation class:
        'Physical mounting positions:'
    11. src/lifx/api.py — DeviceGroup.apply_theme: 'Each device applies the
        theme according to its capabilities:'. THE ONLY api.py EDIT in this
        plan (D5-19 fence: lines 260-331, 515-557, 974-1016 byte-identical).
    12. src/lifx/color.py — HSBK.to_protocol: 'LIFX protocol uses uint16
        values for all HSBK components:'
    13. src/lifx/devices/base.py — Device class: 'This class provides common
        functionality for all LIFX devices:'; and the capabilities property:
        'Returns product information including supported features like:'
    14. src/lifx/devices/ceiling.py — four public-method Args entries (griffe
        keeps Args entries intact across these blank lines — probed against
        the installed griffelib at planning time):
        set_downlight_colors 'colors: Either:';
        turn_uplight_on 'color: Optional HSBK color. If provided:' — here
        ALSO insert an empty line between the last bullet and the trailing
        'If None, uses brightness determination logic' line so it renders as
        its own sentence, not part of the final bullet;
        turn_downlight_on 'colors: Optional colors. Can be:' — likewise
        before its trailing 'If provided, updates stored state.' line;
        turn_downlight_off 'colors: Optional colors to store for future
        turn_on. Can be:' — likewise before its trailing 'If provided,
        stores these colors (with brightness=0 on device).' line.
        The private _determine_*_brightness docstrings stay untouched.
    15. src/lifx/devices/light.py — Light class: 'Extends the base Device
        class with light-specific functionality:'; and get_color: 'Returns a
        tuple containing:'
    16. src/lifx/devices/matrix.py — MatrixLight class: 'Zone Addressing:';
        and within the same list, one empty line between the bullet ending
        '(e.g., 16x8 = 128 zones):' and its numbered steps. The module
        docstring ('Terminology:') stays untouched.
    17. src/lifx/devices/multizone.py — MultiZoneLight class: 'Extends the
        Light class with zone-specific functionality:'
    18. src/lifx/effects/pulse.py — EffectPulse.__init__ Args (renders via
        merge_init_into_class): 'period: Effect period in seconds. Defaults
        depend on mode:' and 'cycles: Number of cycles. Defaults:'
    19. src/lifx/protocol/base.py — Packet class: 'Each packet subclass
        defines:'
    20. src/lifx/protocol/generator.py — five public-function sites (the
        module is a ::: target and renders its public functions): 'Quirks
        applied:' in apply_field_name_quirks, in
        apply_extended_multizone_packet_quirks and in
        apply_sensor_packet_quirks; 'This quirk replaces it with the correct
        structure:' in apply_tile_effect_parameter_quirk; 'so they should
        share a single enum. This quirk:' in apply_firmware_effect_enum_quirk.
        (generator.py is the generator itself, not generated output — safe to
        edit; do not touch protocol/packets.py, protocol_types.py or
        registry.py, which ARE generated.)

    G-05-7b — render the three missing mDNS targets (D5-23; the mDNS ruling
    is RENDER, not delete — api/index.md's links are correct and index.md
    gets NO edit; both destination pages already exist so D5-01 holds — if a
    new page or mkdocs.yml nav change ever seems needed here, STOP and
    report):
    22. docs/api/high-level.md — insert after the `::: lifx.api.discover`
        block (options: show_root_heading: true, heading_level: 3) a new
        block matching those options exactly:
        `::: lifx.api.discover_mdns` /
        `    options:` /
        `      show_root_heading: true` /
        `      heading_level: 3`
        with a blank line either side, before the `::: lifx.api.find_by_serial`
        block. Every existing line of the file stays byte-identical.
    23. docs/api/network.md — insert after the
        `::: lifx.network.discovery.DiscoveredDevice` block and before the
        `### DiscoveryResponse` heading, two new blocks matching the
        Discovery group's options exactly (show_root_heading: true,
        heading_level: 3, members_order: source), function first, record
        second:
        `::: lifx.network.mdns.discover_lifx_services` then
        `::: lifx.network.mdns.LifxServiceRecord`, each with the same
        three-line options block and a blank line either side. Every
        existing line of the file — including all five existing ::: blocks —
        stays byte-identical.
        All three symbols are public API: lifx/__init__.py __all__
        (:143/:151/:152) and lifx.network.mdns.__all__. Their docstrings
        were audited at planning time under D5-22 (all three defect classes)
        and are CLEAN — no source edit is expected; if the extended scan in
        this task's verify block disagrees (drift), fix under the standard
        audit rules before this task's build gate.

    G-05-7c — make the zero-warning gate permanent:
    24. .github/workflows/docs.yml — both build invocations (lines ~56 and
        ~114) change from `uv run zensical build --clean` to
        `uv run zensical build --clean --strict`. Nothing else in the
        workflow changes. Without this, the warning counter just drifts
        again — this is the part that makes the fix stick.

    D5-22 audit re-run (mandatory):
    25. Run the three sweeps in the verify block. The blank-line sweep must
        report only the pinned out-of-scope keys; the public-docstring scan
        — whose file set is derived live from the ::: target list via
        importlib resolution, so it covers EVERY :::-reachable module
        (devices/, effects/, color.py, protocol/, theme/, the mdns files and
        the three newly published mDNS docstrings included) — must print
        nothing; the post-build run-on detector must report zero. If any NEW finding appears in a
        :::-reachable docstring (drift since planning), it is IN SCOPE: fix
        it in this task under the same rules — behavioural claims preserved
        verbatim, IDs demoted to # comments, no tuning constants in rendered
        prose, whitespace-only for list rendering — and record the addition
        in the SUMMARY.

    Commit with `git commit -s`.
  </action>
  <verify>
    <automated>set -e
uv run python - << 'PY'
import ast, importlib, pathlib, re, subprocess, sys
# D5-22 mandates this sweep run over EVERY :::-reachable docstring. The file
# set is derived live from the ::: target list (never hard-coded): each
# target is resolved via importlib so re-exports (e.g. lifx.effects.Effect*)
# map to the module that actually holds the docstring.
PAT = re.compile(
    r"\b(ANIM|DISC|RETRY|DOCS)-\d{2}\b|\bD\d-\d{2}\b|[Ss]pike \d{3}"
    r"|\bplan \d{2}-\d{2}\b|Photons|Glowup|wall-time|blind sleep"
    r"|0\.0%|frames/s|roughly one second|two or more probe|cumulative offsets"
)
# Known-good hit allowlist ("path:name: match" strings). EMPTY by design:
# the planning-time run over all reached files (51) found hits only in the
# four files this plan fixes. Any addition must be justified in the SUMMARY.
ALLOWED_HITS: set[str] = set()

def module_to_path(name: str) -> pathlib.Path:
    p = pathlib.Path("src", *name.split("."))
    return p.with_suffix(".py") if p.with_suffix(".py").exists() else p / "__init__.py"

out = subprocess.run(
    ["bash", "-c", "grep -rh '^::: ' docs/api/*.md"], capture_output=True, text=True
).stdout
files = set()
for line in out.splitlines():
    dotted = line.split()[1]
    try:
        importlib.import_module(dotted)
        files.add(module_to_path(dotted))
        continue
    except ImportError:
        pass
    modname, attr = dotted.rsplit(".", 1)
    obj = getattr(importlib.import_module(modname), attr)
    files.add(module_to_path(getattr(obj, "__module__", modname)))
print(f"# :::-reachable files scanned: {len(files)}", file=sys.stderr)
bad = []
for path in sorted(files):
    tree = ast.parse(path.read_text())
    for node in ast.walk(tree):
        if not isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
            continue
        if node.name.startswith("_") and node.name != "__init__":
            continue
        doc = ast.get_docstring(node)
        if doc:
            for m in PAT.finditer(doc):
                hit = f"{path}:{node.name}: {m.group(0)}"
                if hit not in ALLOWED_HITS:
                    bad.append(hit)
print("\n".join(bad))
sys.exit(1 if bad else 0)
PY
uv run python - << 'PY'
import ast, pathlib, re, sys
ALLOWED = {
    ("src/lifx/animation/__init__.py", "Key Components:"),
    ("src/lifx/animation/__init__.py", "All color data uses protocol-ready uint16 values:"),
    ("src/lifx/animation/framebuffer.py", "Example with 5 tiles arranged horizontally:"),
    ("src/lifx/animation/packets.py", "Supported Devices:"),
    ("src/lifx/animation/packets.py", "LightSetColor Payload Layout (13 bytes):"),
    ("src/lifx/devices/ceiling.py", "Terminology:"),
    ("src/lifx/devices/ceiling.py", "- Component: Logical grouping of zones:"),
    ("src/lifx/devices/ceiling.py", "Product IDs:"),
    ("src/lifx/devices/ceiling.py", "Priority order:"),
    ("src/lifx/devices/detection.py", "Detection order (first match wins):"),
    ("src/lifx/devices/matrix.py", "Terminology:"),
    ("src/lifx/network/discovery.py", "This is a powerful protocol trick that allows targeted discovery:"),
    ("src/lifx/products/quirks.py", "Ceiling lights have two logical components:"),
    ("src/lifx/protocol/header.py", "The LIFX header is 36 bytes total, consisting of:"),
    ("src/lifx/theme/generators.py", "Creates natural color splotches by:"),
}
bad = []
for path in sorted(pathlib.Path("src/lifx").rglob("*.py")):
    tree = ast.parse(path.read_text())
    for node in ast.walk(tree):
        if not isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
            continue
        doc = ast.get_docstring(node, clean=False)
        if not doc:
            continue
        lines = doc.split("\n")
        for i in range(len(lines) - 1):
            if re.search(r"[A-Za-z0-9)]\:\s*$", lines[i]) and re.match(r"^\s*(-\s|\d+\.\s)", lines[i + 1]):
                if (str(path), lines[i].strip()) not in ALLOWED:
                    bad.append(f"{path}: {lines[i].strip()!r}")
print("\n".join(bad))
sys.exit(1 if bad else 0)
PY
grep -qF '# Traceability: single baked probe D4-03' src/lifx/animation/packets.py
grep -qF '# Traceability: D4-04 (probe on the frame-commit CopyFB, matching' src/lifx/animation/packets.py
grep -qF '(ANIM-01/ANIM-02, D4-03)' src/lifx/animation/animator.py
grep -qF 'no flow-control toggle; consumers keep' src/lifx/animation/animator.py
grep -qF 'is still behind on acknowledgements after the sweep' src/lifx/animation/animator.py
test "$(git diff --numstat c139542 -- src/lifx/api.py | awk '{print $1"-"$2}')" = "1-0"
test "$(git diff c139542 -- src/lifx/api.py | grep -c '^@@')" = "1"
for f in src/lifx/animation/orientation.py src/lifx/api.py src/lifx/color.py src/lifx/devices/base.py src/lifx/devices/ceiling.py src/lifx/devices/light.py src/lifx/devices/matrix.py src/lifx/devices/multizone.py src/lifx/effects/pulse.py src/lifx/protocol/base.py src/lifx/protocol/generator.py docs/api/high-level.md docs/api/network.md; do test -z "$(git diff c139542 -- "$f" | grep '^-' | grep -v '^---')" || { echo "non-additive diff in $f"; exit 1; }; done
grep -qF '::: lifx.api.discover_mdns' docs/api/high-level.md
grep -qF '::: lifx.network.mdns.discover_lifx_services' docs/api/network.md
grep -qF '::: lifx.network.mdns.LifxServiceRecord' docs/api/network.md
test "$(grep -c '^::: ' docs/api/network.md)" = "7"
test "$(git diff c139542 -- docs/api/index.md | wc -l | tr -d ' ')" = "0"
test "$(grep -c 'zensical build --clean --strict' .github/workflows/docs.yml)" = "2"
! grep -qE 'zensical build --clean\s*$' .github/workflows/docs.yml
uv run --frozen pytest -q
uv run ruff check .
uv run ruff format --check .
uv run pyright
uv run zensical build --strict
uv run python - << 'PY'
import pathlib, re, sys
bad = 0
for page in sorted(pathlib.Path("site/api").rglob("index.html")):
    html = page.read_text(errors="replace")
    for m in re.finditer(r"<p>(.*?)</p>", html, re.S):
        text = re.sub(r"<[^>]+>", "", m.group(1))
        if re.search(r":\s*\n\s*(-\s|\d+\.\s)", text):
            print(f"RUN-ON {page}: {' '.join(text.split())[:100]}")
            bad += 1
print(f"total run-on paragraphs: {bad}")
sys.exit(1 if bad else 0)
PY</automated>
  </verify>
  <acceptance_criteria>
    - The public-docstring vocabulary+constants scan over all four rewritten files prints nothing and exits 0 — no planning ID, no lineage vocabulary, no measured spike figure, no gate-threshold/expiry/floor/offset constant in any rendered docstring
    - The blank-line sweep reports only the pinned out-of-scope keys (16 sites, 15 keys) — every :::-reachable heading-plus-list site carries its blank line
    - The post-build run-on detector reports 0 (was 23 at planning time) — every published docstring list renders as a list, closing the operator-reported DeviceConnection Features symptom and all audit-found siblings end-to-end
    - Both packets.py Traceability comments are present; animator.py's :164 comment is untouched (the demotion map holds — every removed ID survives in a comment or unrendered surface)
    - api.py's diff vs c139542 is exactly one insertion, zero deletions, one hunk (the apply_theme blank line); the D5-19 ranges are byte-identical
    - Every pure-whitespace file's diff vs c139542 contains zero deletion lines — the blank-line fixes changed no existing character; likewise docs/api/high-level.md and docs/api/network.md (the ::: additions are insertion-only, so every EXISTING ::: block is byte-identical — widened D5-14 held)
    - The three mDNS ::: blocks exist with options matching their sibling blocks; network.md's `^::: ` count outputs 7 (was 5); `git diff c139542 -- docs/api/index.md` is EMPTY — index.md untouched, its links resolve because the destinations now exist (G-05-7b)
    - Both docs.yml build invocations read `zensical build --clean --strict` and no un-strict `zensical build --clean` invocation remains — CI now fails on any new docs warning (G-05-7c)
    - The target-derived public-docstring scan prints nothing over every :::-reachable module (file set resolved live from `grep -rh '^::: ' docs/api/*.md`, 51 files at planning time) — the three newly published mDNS docstrings went live audited (D5-23 inheritance; planning-time result CLEAN confirmed), and a drift-introduced leak in ANY reachable module now fails the gate, not just the seven known-finding files
    - `uv run --frozen pytest -q` exits 0 (2618-test baseline — docstring/whitespace edits break nothing); `uv run ruff check .`, `uv run ruff format --check .` and `uv run pyright` all exit 0
    - `uv run zensical build --strict` exits 0 — ZERO warnings; the former 8-warning baseline (5x effects.md closed in Task 1, 3x index.md closed by the mDNS targets here) is eliminated, not re-pinned (G-05-7/D5-23)
  </acceptance_criteria>
  <done>Every mkdocstrings-published docstring speaks reader vocabulary, renders its lists as lists, and carries no planning IDs or internal tuning constants; all demoted traceability survives as comments; the three public mDNS symbols render on the pages index.md always pointed at, audited before going live; the mandated D5-22 audit re-ran clean; the full test suite, lint, format and type-check are green and the docs build is warning-free under --strict, with CI gating on it.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| published docs → reader behaviour | Getting-started guidance shapes how users connect to devices; a false no-discovery claim misleads capacity/latency expectations |
| source docstrings → rendered API reference | Everything a ::: block renders is public; internal process vocabulary and measured figures leak project internals to readers |
| internal traceability → future maintenance | Deleting planning IDs outright would break the phase-record chain; demotion to comments preserves it off the published surface |

## STRIDE Threat Register

ASVS L1, block on high. Honest assessment: a docs/docstring-accuracy plan
touching markdown prose, docstring prose and whitespace creates no new attack
surface — no auth, session, input-validation or crypto artefact is touched.

| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-05-12 | Information Disclosure | rendered API reference (planning IDs, spike figures, lineage refs) | low | mitigate | Internal-process vocabulary removed from every published docstring; traceability retained in non-rendered # comments only |
| T-05-13 | Tampering | published behavioural contract accuracy | low | mitigate | Every rewrite preserves the shipped behavioural claims verbatim-in-substance; gates pin the contract substrings and the overview.md wording anchor |
| T-05-14 | Denial of Service | reader-side expectations (connection/discovery timing) | low | mitigate | No FPS/polling/timing figure is added anywhere (D5-09 held; the open dispute is explicitly not acted on); removed constants lower, not raise, copy-paste risk |
| T-05-15 | Tampering | docs build integrity (silent warning drift) | low | mitigate | Both CI build invocations gain --strict (G-05-7c), so any future unresolved link or missing anchor fails the build instead of accumulating as a new "baseline" |
| T-05-SC | Tampering | npm/pip/cargo installs | low | accept | No package-manager installs occur in this plan (RESEARCH.md §Package Legitimacy Audit: none) |
</threat_model>

<verification>
- All six gap_ids closed against their UAT truths: G-05-2 (both pages fixed,
  doc-page-only, from_ip docstring untouched), G-05-3 (agreed_text verbatim),
  G-05-4 (run-on detector 0 over freshly built site/api/), G-05-5
  (proposed_text verbatim; public-docstring scan 0; overview.md:150 wording
  anchor matched), G-05-6 (public-docstring scan 0 for IDs; demotion map
  verified — nothing deleted outright), G-05-7 (five effects.md annotations
  backticked; three mDNS ::: targets rendered with index.md untouched; both
  docs.yml invocations --strict)
- D5-22 audit re-ran at execution: blank-line sweep clean against the pinned
  out-of-scope keys, vocabulary/constants scan clean over EVERY :::-reachable
  module (file set derived live from the ::: target list via importlib, never
  hard-coded — the newly published mDNS docstrings went live audited, per
  D5-23); any drift findings fixed in-plan and recorded in the SUMMARY
- Fences held: api.py diff = one whitespace hunk (D5-19 ranges byte-identical);
  discovery.py create_device docstring byte-identical (D5-21/API-01(d)); no
  EXISTING ::: block modified anywhere — animation.md's diff has zero :::
  lines and the high-level.md/network.md diffs are insertion-only (the three
  additive mDNS blocks D5-23 specifies); docs/changelog.md untouched (widened
  D5-14); api/index.md diff empty; every other docs/api page untouched —
  D5-23 not widened; mkdocs.yml untouched (D5-01 — no new page was needed);
  no version attribution anywhere (`grep -rlE 'Since v[0-9]' docs/ CLAUDE.md
  src/` matches zero files, D5-12); # comments and unrendered docstrings
  byte-identical except the three additive Traceability comments; no timing
  documentation, override guidance or diagram added (D5-09 — OPEN not acted
  on)
- Build gate inverted per D5-23: `uv run zensical build --strict` exits 0 —
  zero warnings, no baseline to pin (Task 1's interim non-strict build shows
  '3 issues found' mid-plan, all api/index.md, closed by Task 3); CI gates on
  --strict in both docs.yml invocations so the class cannot drift back
- Full suite: `uv run --frozen pytest` green (2618 baseline); `uv run ruff
  check .`, `uv run ruff format --check .`, `uv run pyright` all clean
</verification>

<success_criteria>
- The operator's five UAT issue reports are resolved on the published site:
  the quickstart tells the truth about IP-only connection, the animation
  lead-in names what the stack does, the DeviceConnection Features list
  renders as a list, no published sentence requires knowing Photons, Glowup,
  GSD planning IDs or spike numbers to understand, and the docs build is
  warning-free — the 8-warning "baseline" is gone, not re-pinned
- The mDNS public API (discover_mdns, discover_lifx_services,
  LifxServiceRecord) is rendered in the API reference on the pages index.md
  always linked, with the docstrings audited before first publication
- The D5-22 audit obligation is discharged: all three defect classes swept
  over every :::-reachable docstring at execution time — including the three
  newly reachable mDNS docstrings — with planning-time findings fixed and
  any drift caught, fixed and recorded
- All surviving fences held (D5-19/D5-21, D5-09 as written, D5-12, widened
  D5-14 with D5-23's narrow two-page lift not widened, D5-01,
  comments-out-of-scope); traceability was demoted, never deleted
- `uv run zensical build --strict` exits 0 locally AND both CI build
  invocations gate on --strict; full test suite, lint, format and type-check
  green; DOCS-01 and DOCS-02 remain satisfied with the gaps removed
</success_criteria>

<output>
Create `.planning/phases/05-reliability-documentation/05-06-SUMMARY.md` when done.
Record in the summary: (1) per-gap closure evidence (the gate outputs for each
of G-05-2..G-05-7), (2) the audit re-run results — sweep outputs, any drift
findings fixed beyond the planning-time list, confirmation the residual
out-of-scope map still holds, and confirmation the three mDNS docstrings went
live audited-clean (D5-23 inheritance), (3) fence confirmations (api.py
one-hunk diff, create_device docstring byte-identical, no EXISTING ::: block
modified — insertion-only diffs on high-level.md/network.md, api/index.md diff
empty, every other docs/api page untouched, mkdocs.yml untouched, no version
attribution, comments untouched except the three additive Traceability
blocks), (4) the demotion map final state (where each removed ID now lives),
(5) the `zensical build --strict` exit status (zero warnings — the former
8-warning baseline eliminated per D5-23), the two --strict CI invocations in
docs.yml, and the pytest/ruff/pyright results, and (6) that DOCS-01/DOCS-02
remain complete — these were accuracy/readability defects inside
already-satisfied requirements, so no REQUIREMENTS.md change is needed. Note
for the verifier: judge G-05-5/G-05-6 by the RENDERED surfaces (built HTML),
per flagged assumption 3; judge G-05-7 by the --strict exit code and the CI
diff.
</output>
