ONE changelog per session (GA-R2/D1). Under blanket approval this is the review surface: every item states WHAT, WHY (evidence), and HOW TO REVERT.
No live mutation in this session. Everything below is a repo change; the only live access was READ-ONLY (a NetBox apex poll and a pre-flight-checks run). No logged window was opened because nothing was mutated.
Status authority for everything here is docs/CURRENT-STATE.md (GA-R1). This file is session-scoped scratch and is NOT citable as status or decision authority.
provider-bundle-check.py: VIP arity, family coupling, and the apex-derived v6 bandsWhat. The vip validator now accepts a v4 TRIPLE (3 addresses) or a dual-family SEXTET (6), validates the three v6 legs against the per-DC v6 /64s, requires the v6 host part to MIRROR the v4 octet textually, and couples prefer-ipv6 to the arity in both directions. The v6 bands are READ FROM THE NETBOX APEX RECORD, not hardcoded. The standalone octet-uniqueness block (old item 6) folded into the same loop.
Why. Measured at :137: if len(parts) != 3 fails EVERY application once R2's ruled dual-stack lands, and :149's p.split(".")[-1] returns the whole address on a v6 literal, so every v6 leg was trivially "unique" and unchecked. The apex now holds the dual-family set a renderer will emit (verified live this session: 160 ip-addresses, 78 IPv6, 156 VIP objects), so the only gate that validates overlays could not validate what is about to be produced.
The prefer-ipv6 coupling is the substance, not a bonus. prefer-ipv6 makes HAProxy bind :::port in ADDITION to *:port; the measured L3-9 finding is that the overlay merge order which keeps prefer-ipv6 while dropping the v6 VIP legs is the one that EXITS 0 -- the dangerous order is the green one. Both directions now fail.
Authority. The arity gap was carried in docs/CURRENT-STATE.md as an open question, explicitly NOT covered by R11. Operator direction 2026-07-28, exact utterance: "Fix the arity gap first, then start the renderer", with the scope fork (arity alone vs arity + R11's three ruled changes) put separately and answered "Arity gap + R11's three ruled changes (Recommended)". OPS under GA-R3 -- a script change making an already-RULED end state expressible; doubt resolves DOWN, no D-number assigned.
Prior art followed, not reinvented. The (role, kind) apex keying and the "provider leg lives in the DEDICATED GUA VIP /64, admin/internal in their plane /64s" rule are copied from scripts/dc-plane-ipam.sh:102-131 and netbox/dc-plane-apex-import.py. Both live-API and repo-dump shapes are handled (nested scope.slug vs flat scope_site) -- writing against one and matching zero objects on the other is a trap this repo already hit once.
Refuses rather than passes. A dual-family vip with an unreadable or absent apex record exits 2, never 0: "could not look" is never "nothing there". A v4-only bundle needs no apex at all, so this introduces no new dependency for the current tree (harness T25/T26 lock both halves).
Revert. git revert this commit. The checker returns to triple-only; nothing live depends on it.
What.
provider-bundle-check.py: OCTET_LO, OCTET_HI 50-60 -> 50-99.scripts/lib-net.sh: VIP_OCTET_MAX 60 -> 99, VIP_COUNT_EXPECT 11 -> 13.provider-bundle-check.py: NEW invariant 8 -- an hacluster principal with no vip is a FAIL.EXPECT_PUBLIC_VIP deliberately STAYS 11.Why. R11 ruled vault .61 and designate .62 as full dual-family triples and ruled the gate hardening IN SCOPE, not deferred. cluster_count is asserted NOWHERE in scripts/ or tests/, so a 3->1 rewrite of all 20 values produces a byte-identical PASS -- decorative HA was found by audit rather than by gate.
EXPECT_PUBLIC_VIP stays 11 because -- measured -- NEITHER vault nor designate carries a public binding, so they do not join that count. Bumping both constants together breaks the gate. The two band constants are SEPARATELY NAMED in two files; this is a two-file change by construction, and both sites carry a comment saying so.
MEASURED CONSEQUENCE, stated precisely. Two sub-checks flip PASS -> FAIL:
provider-bundle-check on the base bundle now FAILS with hacluster relation but no vip: designate.pre-flight-checks CHECK 1 now reports VIP validation OK=11 bad=0 (want OK=13).preflight.sh was ALREADY exit 1 before this change (octavia-pki overlay absent, MAAS unreachable from the jumphost, P5's 7 credential findings). Measured after: still exit 1, now 3 fatal, 2 warning. This did NOT flip preflight from pass to fail and no deploy path that was open is closed by it -- the same precedent this document records for P5. The reds are the ruled gate reporting real work owed: R11's .61/.62 VIPs are RULED-BUT-NOT-BUILT, and per R6 they land BEFORE the HA overlay.
Revert. Revert this commit; the constants and the invariant-8 block are contiguous and independent of item 1.
What. tests/provider-bundle-check/run-tests.sh gains 15 cases. The fixture base splits: pristine.yaml is the repo bundle untouched; good.yaml is that bundle PLUS designate's ruled .62 VIP.
Why the split. Invariant 8 legitimately fails the real repo bundle. Left alone, every rc=0 case (T1/T9/T14) would have been asserting "fails for the reason we already know" instead of its own invariant. Per the standing rule -- when remediating a finding turns the harness red, REPLACE the assertion with the new invariant and say so, never delete it to go green -- the real tree's state is kept honest by a NEW case (T16) that asserts pristine.yaml DOES trip it. When .62 lands in bundle.yaml, T16 must be re-pointed, not deleted.
Proven able to fail AND to pass -- the complement this project recorded on 2026-07-27 after building three gates whose every live run failed:
dc-ha-scaleup.yaml -- R6's "VIPs before the overlay" ordering, made executable.prefer-ipv6 coupling FAIL; T22 a v6 leg in the node /64 instead of the VIP /64 FAILS; T23 the ::32 hex trap FAILS; T24 a 2-address vip FAILS; T25 an unreadable apex REFUSES at rc=2; T26 a v4-only bundle needs no apex; T27/T28 the band is widened to 99 but still bounded.T29/T30 exercise the dc1 arm of the apex lookup, which T19-T23 never reached (they all run at the default --dc vr1-dc0): T29 resolves dc1's f03:11::/64 / :320::/64 / :321::/64 and passes, T30 proves a dc0 v6 leg under --dc vr1-dc1 FAILS, so a cross-DC copy-paste of a rendered overlay cannot pass. The renderer's first real output is per-DC overlays, and a gate observed in only one DC mode is the same untrustworthiness T16/T18 exist to rule out.
T8's assertion string moved outside 50-60 -> outside 50-99 (the same re-pointing rule; the case still guards the band, at its ruled bounds).
Evidence. Harness 30/30 (was 15). Gauntlet ALL GREEN (84 harnesses) on vcloud -- the host is named because the gauntlet is measured HOST-DEPENDENT (2/81 fail on voffice1 on an identical commit). repo-lint 0 fail / 1 standing warn / 605 files scanned. No harness was added or renamed, so tests/HARNESS-MANIFEST is unchanged and needed no re-record.
Revert. Revert this commit; the harness returns to 15 cases.
pre-flight-checks.sh CHECK 1 is triple-shaped. Its awk parse assumes three columns, so it will mis-read a dual-family sextet when those artifacts land. Same class as the arity gap just fixed, one gate over. Not touched -- it interlocks with BLOCKER-1 (the ruling-3 VIP extraction makes CHECK 1 see 0 VIPs).cluster_count coherence is still unasserted. Invariant 8 catches an hacluster principal with NO vip, which is what R11 ruled. It does NOT check that cluster_count matches the principal's num_units -- a 3->1 rewrite of all 20 values still produces a PASS. Out of ruled scope; worth its own exchange.netbox/*.py tools carry in-file usage examples pointing at https://netbox.baldurkeep.com -- the FROZEN v1 reference. Two of them (ipv6-mark-reserved.py, ipv4-prefixes-import.py) have NO SANDBOX_HOSTS write guard at all while carrying write paths (update_status, create_or_report_prefix, create_or_report_iprange). The guarded tools also expose a deliberate --yes-write-upstream escape hatch, so "writes only reach 10.10.1.10" is a convention with an override, not a hard invariant.~/vr1-office1-creds/vr1-netbox.env points at netbox.baldurkeep.com (the read-only v1 reference) while vr1-netbox-sandbox.env points at the LIVE VR1 apex http://10.10.1.10:8000. The filename says the opposite of what each is.?site= API filter silently does not filter. Measured this session: /api/ipam/ip-addresses/?site=vr1-dc0 and ?site=vr1-dc1 each returned the full 160. A per-site apex count taken that way would be wrong and look right.Same branch, continued. Operator direction: "You can order the steps in whatever order you want. Deploy agents to help if needed."
I first proposed doing ruling-3's VIP extraction before the renderer, then reversed it once three read-only agents reported. Recording the reversal rather than the conclusion, because the reason is the reusable part:
overlays/vr1-dc1-vips.yaml is still byte-identical to tests/render-baseline/fixtures/vr1-dc1-vips.v4-11app.yaml (3f93ecb3...) -- verified this session. The reconciliation (11->13 apps, dual-family) destroys it.runbooks/phase-01-bundle-deploy.md:170-176, where juju deploy runs only on 11/11/0), the Octavia SAN derivation (:371), preflight in two places, and 8 of 30 harness cases.scripts/render-dc-overlays.py -- two stages, split where F2 froze the interfaceWhat. derive (apex + lib-net.sh -> a per-DC VALUES file) and render (VALUES -> overlay TEXT). Plus check, which renders and byte-compares.
Why split. Forward item F2 froze the interface: "the renderer is a deterministic command-line tool whose inputs are the per-DC values file and whose outputs are committed, reviewable artifacts. Nothing about automating it should require changing the renderer." render is therefore PURE -- no apex, no network, no clock -- so it is offline-testable and a future CI job only re-runs derive.
Why a text emitter, not yaml.dump. The target is byte-compared. A round-tripper destroys the comment header, normalises vip: "a b c" to a plain scalar, and imposes its own key order -- all three are load-bearing. PyYAML reads the values file and never writes an overlay.
Two things that are INPUT, not output, and would otherwise be invented:
derive --from-overlay lifts it VERBATIM from the artifact being reproduced, so a reproduction run is honest about which bytes the tool actually generates.The ruled app->octet map is READ, not restated -- parsed with ast out of netbox/dc-plane-apex-import.py's APP_OCTET, the tool that wrote those values into the apex. A second hand-maintained copy is a second thing to drift.
Revert. Delete scripts/render-dc-overlays.py, tests/render-dc-overlays/, render/values/, and revert the render-baseline T6 hunk + the manifest line. Nothing else consumes any of it; no committed artifact was regenerated by it yet.
render output hashes to 3f93ecb3205cea8b303f5b2cbbbde343a2762156575277ca84684369da8bf896 -- identical to both the committed overlay (1634 bytes) and the frozen fixture.
A byte-compare only ever observed PASSING is as untrustworthy as a gate only ever observed failing, so three faults were seeded and all three fail it: a wrong octet, a dropped header, and dc0 prefixes against the dc1 artifact. render-baseline's new T6 was separately proven by making the renderer sort alphabetically -- hash changed, case went red.
The renderer's FIRST dual-family output was malformed. The values file stores each v6 /64 base with trailing colons stripped (2602:f3e2:f03:11), and the emitter joined with a SINGLE colon -- yielding 2602:f3e2:f03:11:50. That is not a valid address, and it still looks like one.
provider-bundle-check.py -- fixed yesterday for exactly this class -- rejected all 13 applications with bad vip ip. Fixed to ::%d and locked by harness cases T13/T13b, which assert both that the correct form is present AND that no single-colon leg is emitted. This is the acceptance criterion the whole arity fix existed to provide, working on its first real use.
apex -> values -> renderer -> overlay -> gate. The dual-family dc1 render with R11's ruled-but-unbuilt apps included produces 13 clustered VIPs, all dual-family, and provider-bundle-check --dc vr1-dc1 reports PASS including the new hacluster invariant (12 principals, all carrying a VIP).
Evidence. New harness tests/render-dc-overlays 17/17; render-baseline 10/10 (was 9, T6 added -- ADDED per its README, existing assertions NOT repointed). Gauntlet ALL GREEN (85 harnesses) on vcloud (was 84); tests/HARNESS-MANIFEST re-recorded deliberately after the drift gate caught the addition -- which is the gate working. repo-lint 0 fail / 608 files scanned.
docs/dc-dc-deployment-workflow.md:361 says ovn-chassis MACs "stay sourced from scripts/lib-hosts.sh". Measured: lib-hosts carries only HOST_BOOT_MAC (the PXE NIC, ...:NN:01); ovn-chassis needs the provider-public NIC (...:NN:02), which is not in lib-hosts at all. The real source is opentofu/vr1-dcN-substrate/main.tf, macs[1] of each compute node -- and it exists for BOTH DCs. This also narrows CURRENT-STATE's claim that dc0's MACs "cannot be derived": not from a PATTERN, but they are pinned literals in tofu and are perfectly sourceable. F3's conclusion (MACs are renderer output, not apex input) still stands; only its sourcing sentence is wrong.scripts/dc-plane-ipam.sh:120 has a latent scope-matcher bug. It matches scope.name or scope_site but NOT scope.slug, so it would match ZERO prefixes against a live-shaped record. Latent only because it defaults to a repo dump. The other two tools use the three-way matcher; this one is the odd one out.pre-flight-checks.sh CHECK 1 is triple-only AND dc0-only. It hardcodes if(n!=3) (:86), reads bundle.yaml as RAW TEXT (never a merged input), and never calls lib_net_select_dc -- and lib-net.sh deliberately UNSETS VIP_PREFIX_* for dc1, so CHECK 1 cannot evaluate dc1 at all. The renderer's dual-family output will pass provider-bundle-check and FAIL preflight P4.lib-net.sh carries the values. Same in-document supersession class this doc keeps finding.render-baseline T3 compares COUNTS, not SETS. A deleted pinned fixture plus an added unpinned one keeps the count equal; a # comment line in SHA256SUMS inflates the pin count and masks one unpinned file; and -maxdepth 1 cannot see a subdirectory. It catches the fault it was seeded against; a set-difference would close all three.bundle.yaml's own header (:11-13, :19-24). Ten of the eleven inline VIP comments are the bare token # B1. Migrating them into an overlay STRANDS them from their definitions -- appendix-A:652-661 records the repo's rule for exactly this identifier class. This must be handled when the extraction lands, not discovered then.Operator: "Approved, continue". This is commit 1 of the two ruling 3 (2026-07-25) prescribes. The dual-stack ADD is commit 2 and is deliberately NOT in here.
bundle.yaml is now VIP-FREE; overlays/vr1-dc0-vips.yaml is GENERATEDWhat. All 11 inline vip: lines removed from bundle.yaml; placement's then-empty options: block dropped rather than left parsing as null. The values moved to overlays/vr1-dc0-vips.yaml, rendered by scripts/render-dc-overlays.py from render/values/vr1-dc0-vips.yaml -- the renderer's first real output.
The 11 inline comments were carried, not dropped. Ten are the bare token # B1 and three carry real operational content (keystone's catalog-endpoint note, ceph-radosgw's un-defer note, openstack-dashboard's ALLOWED_HOSTS warning). They were extracted VERBATIM from bundle.yaml by a parser, not retyped, and the renderer grew optional per-app comment support to carry them. An app with no comment emits none, so the dc1 artifact still reproduces byte-for-byte (re-verified: 1634 bytes, unchanged).
The B1/B5 tokens stay DEFINED in bundle.yaml's header, where they always were; the overlay header names that explicitly so the references do not dangle (appendix-A:652-661 is the repo's rule for this identifier class).
PROVEN NEUTRAL by the ruling's own named check. provider-bundle-check on the pre-extraction bundle and on bundle.yaml --overlay overlays/vr1-dc0-vips.yaml --dc vr1-dc0 produce line-for-line identical output, including the same known hacluster relation but no vip: designate failure. Separately, a structural diff confirmed only vip changed: applications 56 -> 56, relations 108 -> 108, every non-VIP option and every non-option key byte-identical.
Revert. git revert; the values file and overlay are additive and the bundle edit is a pure line removal.
Every item below would have been BROKEN by the move. All were found by the read-only agent sweep BEFORE the edit, not after.
scripts/preflight.sh P2 validated the BARE base, which post-move reports 11 phantom "no vip" failures. Now validates the MERGED dc0 input; the overlay is passed only when present so a tree without it still runs.scripts/pre-flight-checks.sh CHECK 1 read bundle.yaml as raw text and would have seen ZERO VIPs -> hard-fail. Now reads the merged source. This is BLOCKER-1, fixed the ruled way (point it at the merged bundle) and explicitly NOT by bumping the expected count. A measured trap on the way: the file sets IFS=$'\n\t', so a space-joined "$VIP_SRC" reached grep as ONE filename, grep exited 2, pipefail propagated and the gate died silently mid-check. Fixed with a bash ARRAY.runbooks/phase-01-bundle-deploy.md -- the real hazard. Its RUN block gates juju deploy on TOT/HI/LO == 11/11/0; post-move that reads 0/0/0 and aborts the deploy. Both guard blocks now span base + overlay. A second bug had to be fixed in the same edit: grep -c over TWO files prints one count PER FILE, so the bare $( ) would have captured a two-line string and every numeric test would have failed anyway -- the counts are now awk-summed. Verified end to end: the gate reads 11/11/0 -> DEPLOY. The deploy line itself now carries --overlay overlays/vr1-dc0-vips.yaml.phase-01's Octavia SAN derivation (DOCFIX-067) read bundle['applications']['octavia']['options']['vip'], which post-move raises KeyError -- swallowed by || true into an EMPTY VIP, then a "implausible VIP" abort. Now reads the overlay. Verified: derives 10.12.4.57.runbooks/phase-00-teardown-maas-reset.md:233 bare checker call -> merged input.runbooks/dc-dc-phase4-juju-bundle-per-dc.md said "DC1 needs NO VIP/CIDR changes at all" and "deploy the EXISTING bundle.yaml with NO VIP edits". Followed verbatim post-move that deploys a VIP-free DC1. Superseded in place (the D-101 plane reasoning is kept, because it is still correct); the DC2 instruction to "edit bundle.yaml's VIP lines" now points at rendering a new overlay instead.runbooks/appendix-A-troubleshooting.md L3 told an operator to grep the bundle for the metal-side VIP token -- which now returns zero, and the block's || true makes zero look like a clean result. Note added.tests/render-baseline/README.md records a capture-time bundle.yaml sha256 that no longer re-verifies. Marked HISTORY with what changed and why, rather than re-pinned -- the fixture hashes themselves are untouched.tests/provider-bundle-check now builds its fixture base by MERGING overlays/vr1-dc0-vips.yaml into bundle.yaml, because that pair is the deploy input. T16's label changed from "pristine repo bundle" to "the real dc0 deploy input" -- same assertion, honest name. NEW T31 asserts ruling 3's new invariant directly: the BARE base fails for every clustered principal, so "deployed without its overlay" is a tested state rather than an assumption. 31/31.
Evidence. Gauntlet ALL GREEN (85) on vcloud; repo-lint 0 fail / 610 files; render-dc-overlays 17/17; render-baseline 10/10 (dc1 reproduction still byte-exact after the comment-support change); preflight 16/16.
Still red, unchanged and expected: pre-flight-checks CHECK 1 reports OK=11 want 13 and provider-bundle-check reports designate -- both are R11's .61/.62 VIPs being RULED-BUT-NOT-BUILT, which is commit 2's job, not this one's.