# 2026-07-15 -- C2 sub-task (ii): D-120 bands loaded + verified; sandbox loop extended to ip-ranges/ip-addresses

## Context

C2 sub-task (ii), per DOCFIX-195: load the D-120 compose-/24 band ranges + the two
static service IPs into office1-netbox (the VR1 IPAM apex) and VERIFY them. The logged
tooling gap: the sandbox loop (`prod-draft-dump.py` + `sandbox-fidelity-check.py`) did
not cover `ip-ranges`/`ip-addresses` at all, so those objects were invisible to the
fidelity check. This closes that gap and then does the load, verified.

## What changed

**Tooling (the "A" extension):**
- `netbox/prod-draft-dump.py` -- ENDPOINTS += `ipam/ip-ranges` and `ipam/ip-addresses`.
  Ranges get a synthetic `range` key (`start-end`) since the checker keys on one field.
  Verified live: the endpoints dump (0/0 before the load).
- `netbox/sandbox-fidelity-check.py` -- `KEY` += the two endpoints; `EXPECTED_NEW` += the
  3 D-120 ranges (keyed start-end) + the 2 service IPs (keyed address). They are pure
  delta (the frozen v1 draft carries none), so both-bounds already gives endpoint/address
  correctness -- a wrong endpoint is a different key (UNEXPECTED-EXTRA + EXPECTED-BUT-ABSENT).
  Success banner now reads D-115/D-117/D-118/**D-120**.
- `tests/sandbox-fidelity-check/run-tests.sh` -- +T11 (a dropped D-120 range ->
  EXPECTED-BUT-ABSENT) +T12 (a stray ip-address -> UNEXPECTED-EXTRA). 12 -> 14.

**Importer:**
- `netbox/d120-compose-bands.py` -- stdlib, UA-aware, `SANDBOX_HOSTS`-guarded,
  DRY-BY-DEFAULT (`--commit` to write, `--yes-write-upstream` for a non-sandbox target),
  with a WHOLE-PLAN PREFLIGHT (parent `10.10.1.0/24` must exist; every endpoint in-parent;
  start<=end) so a bad plan cannot half-write. Creates the 3 ranges + 2 IPs, idempotent.
  Harness `tests/d120-compose-bands/` (16/16) pins the band values + the guards.

**The load (executed 2026-07-15, operator-approved):**
Dry-run previewed 5 creates / 0 present; `--commit` against office1-netbox created:
- ip-ranges: `10.10.1.2-.49` (static), `.100-.200` (dynamic), `.201-.254` (node)
- ip-addresses: `10.10.1.10` (office1-netbox), `10.10.1.11` (office1-tailscale)
Re-dump -> ip-ranges 3, ip-addresses 2. Fidelity check -> **exit 0**: "upstream draft +
EXACTLY the planned delta (D-115/D-117/D-118/D-120)."

## Result

- **C2 sub-task (ii) CLOSED; C2 fully closed.** office1-netbox now holds the complete
  validated VR1 dataset, verified by a fidelity check that covers every object class in it.
- **D-120 Step 6 CLOSED** -- the band layout is now recorded in NetBox, no apex write.
- Gauntlet ALL GREEN (60 harnesses); repo-lint 0 fail.
- Stage 2 close-out (C1..C5) is COMPLETE -> the branch->main merge point is reached
  (the merge itself remains an operator-gated action).

## Revert

- `git rm netbox/d120-compose-bands.py tests/d120-compose-bands/run-tests.sh docs/changelog-20260715-d120-load-and-iprange-tooling.md`
- `git checkout HEAD -- netbox/prod-draft-dump.py netbox/sandbox-fidelity-check.py tests/sandbox-fidelity-check/run-tests.sh docs/session-ledger.md docs/dc-dc-deployment-workflow.md`
- The 5 NetBox objects (ip-ranges 1-3, ip-addresses 1-2 on office1-netbox) are additive;
  delete them in the NetBox UI if the load must be undone. No existing object was edited.
