# M19–M20 execution plan: discovery migration The operational plan for [device-manager.md](device-manager.md)'s increment 8: discovery leaves the kernel — a **pci-bus driver** (M19) and an **acpi service** (M20), with the kernel's device enumeration retired behind them. Same rules as [m17-m18-plan.md](m17-m18-plan.md): one phase at a time, each green before the next; this file is the build order and the checklist. **Definition of green, every phase:** `zig build` clean, `zig build test` clean, `python3 test/qemu_test.py` passes (existing scenarios plus the phase's new one), and the relevant design doc updated. Commit per green phase (no co-author trailers). The full suite is the regression net — the existing `driver-restart` / `usb-report` / `device-list` / `input` scenarios must stay green *through* the migration, which is the whole point: the system must not be able to tell who enumerated it. **Workflow:** dedicated worktree; branches off `main` — `feat/pci-bus` (M19.0–19.3), `feat/acpi-service` (M20.1–20.3); auto-merge to main when a branch is green; keep branches; push everything. ## Settled decisions (2026-07-13 — veto before the loop starts) 1. **What "retiring the kernel scan" means.** The kernel keeps, forever, the parses it needs before user space exists: RSDP/XSDT location, MADT (SMP), the HPET table (the tick), FADT + the AML `\_S5` evaluation (poweroff — the power tests prove it), and MCFG (the host bridge node). What retires is **device enumeration**: the ECAM function walk (M19.3) and the DSDT/SSDT namespace walk that builds device nodes (M20.3). The AML module stays a shared build module compiled into both the kernel (for `\_S5`) and the acpi service (for everything else) — same source, two builds, no fork. 2. **Bridge apertures come from the firmware memory map, not AML.** Registered PCI functions carry BAR resources, and containment demands the bridge own windows that cover them. The apertures are derived kernel-side from the boot memory map's MMIO holes (regions that are neither RAM nor tables) — mechanical, AML-free, and available at boot regardless of what later moved to user space. (The bridge today carries only ECAM + bus range; this is the prerequisite M19.0 exists for.) 3. **`device_register` becomes idempotent on exact match.** A re-registration with identical (parent, class, resources) returns the existing id instead of appending. The kernel table has no unregister, so without this a restarted registering bus would duplicate its children on every respawn — idempotence makes restart-and-re-report safe for every future bus, not just PCI. 4. **The manager matches from reports.** `ChildAdded` gains a `device_id` field (the kernel-registered id, `no_device` for unregistered leaves like USB ports). After the M19.3 flip, PCI driver matching keys off reported identity (the class triple) instead of the manager's boot-time snapshot — the snapshot match remains only for what the kernel still seeds. One flip phase changes both sides at once so no device is ever matched twice. 5. **The acpi service's authority is one node.** The kernel publishes an `acpi-tables` device: memory resources covering the table blobs plus a broad `io_port` resource — the documented trust grant to exactly one process (AML OperationRegions reach EC/PM ports; the claim-gated io_read/io_write calls already exist). The service claims it, maps the tables, and runs the shared AML module in ring 3 behind a `Hal` backed by `mmio_map` + `io_read`/`io_write`. 6. **Both new processes are protocol drivers** under the manager: hello, supervision, restart with backoff — all inherited from M18.1 for free. Registration idempotence (decision 3) is what makes their restarts sound. ## Status - [ ] **M19.0** — prerequisites on `feat/pci-bus`: bridge MMIO apertures from the memory-map holes; `device_register` idempotence (+ kernel unit checks); `ChildAdded.device_id`; archive note on m17-m18-plan.md. - [ ] **M19.1** — pci-bus driver, scan only: claim the host bridge, map the ECAM window, walk bus/device/function headers, log what it finds. Scenario `pci-scan`: the kernel test compares the driver's reported count against the broker table's `pci_device` count — equivalence, per class. - [ ] **M19.2** — register + report: each function registered under the bridge (config-space slice + BARs, `pci_class` in the descriptor), reported with `child_added { device_id, identity = class triple }`. Manager mirrors; spawn-from-reports stays **off**. Scenario extends `pci-scan`: registered ids resolve, no duplicates after a forced driver restart (idempotence proven end to end). - [ ] **M19.3** — the flip: kernel `enumeratePci` call removed (bridge node stays); manager matches PCI drivers from reports. One commit. The existing xHCI scenarios (`driver-restart`, `usb-report`, `device-list`) are the assertion — xhci must come up spawned off a pci-bus report, and the suite must not be able to tell the difference. discovery.md updated. - [ ] **merge** `feat/pci-bus` → main, push. - [ ] **M20.1** — acpi service, parse only: kernel publishes `acpi-tables` (decision 5) — memory over the table blobs, the broad io_port grant, and **the SCI as an irq resource** (from the FADT; unused until M21 but free to record now). The service claims it, maps the blobs, runs the shared AML module in ring 3, logs the namespace device count and `_HID`s. Scenario `acpi-parse`: user-space count equals the kernel walk's count. - [ ] **M20.2** — register + report: namespace devices with `_HID` + `_CRS` resources registered under `acpi-tables` (its io_port + the memory-map holes give containment), reported to the manager. Spawn-from-reports for ACPI matches stays off. Scenario: the reported set includes the PS/2 keyboard and mouse nodes with their IRQ resources. - [ ] **M20.3** — the flip: kernel DSDT device-node building removed (static tables + `\_S5` stay, decision 1); manager matches ACPI-hid drivers (ps2-bus) from reports. The `input` and `device-manager` scenarios are the assertion. discovery.md + acpi.md + device-manager.md updated; device-manager.md increment 8 closed. - [ ] **merge** `feat/acpi-service` → main, push — **loop ends here**. --- ## Phase notes **M19.0 apertures:** the boot memory map already crosses the handoff ([boot-handoff]); the holes computation belongs where the bridge node is built (`parseMcfg`). Sanity-check on QEMU q35: the xHCI BAR (`0xc0000000`-region values seen in the M18 logs) must land inside a derived aperture, asserted in the kernel unit test. **M19.1 scanning without owning config access twice:** the driver reads config space through its ECAM mmio_map grant of the *bridge* window — the same bytes the kernel walk read. Vendor-id `0xFFFF` skip, header-type multifunction rule, no bridge recursion (matches the kernel's current single-segment walk). **M19.2 BAR sizing:** the classic size probe (write all-ones, read mask, restore) is deferred — the BARs' current programmed values and types are enough for containment-checked registration at bring-up; sizing lands with the first driver that needs to *move* a BAR. Log what is registered so the scenario can assert it. **M19.3 what the manager still seeds from the snapshot:** everything the kernel still enumerates (timers, ACPI nodes until M20.3). The PCI arm of `pciDriverFor` switches source; `driverFor` doesn't move until M20.3. **M20.1 Hal in ring 3:** `mapMmio` → `device.mmioMap` over the claimed acpi-tables node (plus a table-offset map for blobs); `pioRead`/`pioWrite` → `device.ioRead`/`ioWrite` against its io_port resource. The interpreter cannot tell it moved — that is the assertion of `acpi-parse`. **M20.2 containment for `_CRS`:** io ports fall inside the node's broad io_port resource; MMIO windows (HPET, LAPIC ranges some firmwares list) fall inside the memory-map holes added to the node in M20.1. Anything that doesn't fit is logged and skipped, loudly — bring-up honesty over silent drops. **M20.3 ps2 ordering:** ps2-bus binds nodes the acpi service now reports, so its spawn moves behind the report (the manager's matching handles this once the source flips); the `input` scenario proves the keyboard still types. **Explicitly out of scope:** PCI bridge recursion (single segment, flat bus walk stays); BAR reprogramming/sizing; disk/PCIe hotplug; interrupt routing changes (`_PRT` stays wherever it is today); the USB descriptor track; multi-segment ECAM; per-device power states (D-states, `_PSx`/`_PRx`, suspend/resume — a future *lifecycle-vocabulary* extension, since "suspend" has the shape of a signal every driver must answer, and it has no consumer until laptop sleep); CPU P/C-states. ## M21 preview — ACPI events + system power (planned next, not in this loop) The acpi service grows the event side (settled direction 2026-07-13; detailed phases when M20 lands): - **21.1 SCI + fixed events**: irq_bind the SCI (the resource M20.1 already records), read/clear PM1 status, publish the power-button event to subscribers (the same pub/sub shape the manager uses). - **21.2 GPE + Notify**: Notify dispatch in the shared AML interpreter, GPE block handling, `Notify(device, code)` published per reported node. The acpi service is a **bus** here: battery (PNP0C0A), AC (ACPI0003), and lid (PNP0C0D) nodes are reported children; small class drivers bind them and speak an evaluate/subscribe protocol to the service — the xHCI split, repeated. The embedded controller (`_Qxx` queries) rides this phase; QEMU emulates no battery/EC, so those paths are interface-complete and validated on real hardware (the laptop is the win condition), while the plumbing is proven by the power button. - **21.3 the capstone**: QEMU `system_powerdown` → acpi service event → init runs the M17 stop sequence over its children → kernel `\_S5` — orderly shutdown as the scenario that proves lifecycle + events compose. (The harness grows a QMP poke to inject the event.)