carlos / platform Public

edge: wildcard parents — always emit the class mark, catch-all-blind wake, spec amendment (#300 round 2)

An absent platform key now means a pre-mark publisher, never a box
homing nothing platform-class. The wake predicate applies Hosts()'s
own filter so a marked catch-all coming or going wakes nobody. Tests
for a name losing its mark (the rollback case), catch-all churn, the
no-home-map edge and non-ready rows; spec records the wire growth and
the home-boxes-first roll order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 96fbeef74e98b9433a5b153eac673dbd866303f7 parent 26e206b
5 files changed, +63 −12
  • docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md +10 −1
  • internal/edge/homemap.go +12 −5
  • internal/edge/wildcard_parents_test.go +23 −0
  • internal/host/edgesfile.go +5 −3
  • internal/host/edgesfile_test.go +13 −3
diff --git a/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md b/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md
index 530fcf771..8bf68d77c 100644
--- a/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md
+++ b/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md
@@ -396,7 +396,16 @@ timestamp writes nothing. Edge writes do NOT bump `control/serial` —
the serial stays console-written per the writer map, matching Part 2's
certs ruling; edges mirror the `control/edges/` prefix on their own
30-second poll, the same cadence the certs mirror uses. Every edge
-folds these files into a deployment-wide host→home map. The file
+folds these files into a deployment-wide host→home map. **Amendment
+(2026-08-26, #300):** each host object also carries `platform: bool`
+(additive, always written) — the publisher's class mark for
+platform-minted rows (registry Platform or Provisioned). Every edge
+derives its wildcard-parent set from the DEPLOYMENT's marked names
+(its own routes ∪ the home map's marked hosts), not the box's own
+routes, so a region edge serves the account wildcard of an app it only
+backhauls to. Fail closed: a file without the key (pre-mark publisher)
+contributes no parents until its box republishes, so home boxes roll
+before region edges. The file
doubles as the DR runbook's explicit statement of what a dead box was
serving. **Keys are registry row hosts verbatim, including `*.parent`
catch-all rows** — this writer performs no wildcard expansion, so a
diff --git a/internal/edge/homemap.go b/internal/edge/homemap.go
index abebf535c..d8931fcf2 100644
--- a/internal/edge/homemap.go
+++ b/internal/edge/homemap.go
@@ -321,22 +321,29 @@ func (m *homeMap) refreshHosts(ctx context.Context) error {
func samePlatformSet(a, b map[string]homeEntry) bool {
na, nb := 0, 0
for h, e := range a {
- if !e.platform {
+ if !parentInput(h, e) {
continue
}
na++
- if eb, ok := b[h]; !ok || !eb.platform {
+ if eb, ok := b[h]; !ok || !parentInput(h, eb) {
return false
}
}
- for _, e := range b {
- if e.platform {
+ for h, e := range b {
+ if parentInput(h, e) {
nb++
}
}
return na == nb
}
+// parentInput is the one filter Hosts and samePlatformSet share: a marked,
+// exact (non catch-all) name. A catch-all coming or going cannot move a
+// parent, so it must not wake the manager either.
+func parentInput(host string, e homeEntry) bool {
+ return e.platform && !strings.HasPrefix(host, "*.")
+}
+
// Hosts is every exact PLATFORM-class name the deployment's OTHER boxes
// home. Sorted, nil-safe. The wildcard manager unions these with this box's
// own routes so its parent set is the deployment's, not the box's (#300):
@@ -360,7 +367,7 @@ func (m *homeMap) Hosts() []string {
snap := m.snapshot()
out := make([]string, 0, len(snap.hosts))
for h, e := range snap.hosts {
- if !e.platform || strings.HasPrefix(h, "*.") {
+ if !parentInput(h, e) {
continue
}
out = append(out, h)
diff --git a/internal/edge/wildcard_parents_test.go b/internal/edge/wildcard_parents_test.go
index 76bf9e4cb..a97052123 100644
--- a/internal/edge/wildcard_parents_test.go
+++ b/internal/edge/wildcard_parents_test.go
@@ -67,6 +67,25 @@ func TestHomeMapHostsAndWakeOnChange(t *testing.T) {
t.Fatalf("Hosts = %v; want %v", got, want)
}
+ // A name LOSING its mark (a home box rolled back below the mark) is a
+ // change: the parent must leave, and the manager must hear about it.
+ putJSON(t, s, "control/edges/carol.json", `{"v":1,"box":"carol","eip":"4.4.4.4","hosts":{"new.bae.apps.test":{"role":"home"}}}`)
+ refreshHomeMap(t, m)
+ waitWoke(t, &woke, 4)
+ if got, want := m.Hosts(), []string{"other.bad.apps.test"}; !reflect.DeepEqual(got, want) {
+ t.Fatalf("Hosts after unmark = %v; want %v", got, want)
+ }
+ // A marked catch-all coming and going moves nothing and wakes nobody.
+ putJSON(t, s, "control/edges/carol.json", `{"v":1,"box":"carol","eip":"4.4.4.4","hosts":{"new.bae.apps.test":{"role":"home"},"*.claimed.apps.test":{"role":"home","platform":true}}}`)
+ refreshHomeMap(t, m)
+ time.Sleep(20 * time.Millisecond)
+ if woke.Load() != 4 {
+ t.Fatalf("a catch-all woke the manager (%d)", woke.Load())
+ }
+ if got, want := m.Hosts(), []string{"other.bad.apps.test"}; !reflect.DeepEqual(got, want) {
+ t.Fatalf("Hosts after catch-all = %v; want %v", got, want)
+ }
+
var nilMap *homeMap
if nilMap.Hosts() != nil {
t.Fatal("nil map must report no hosts")
@@ -118,7 +137,11 @@ func TestDeploymentHostsUnionsTheHomeMap(t *testing.T) {
if want := []string{"cribado.bad.apps.test", "own.acct.apps.test"}; !reflect.DeepEqual(got, want) {
t.Fatalf("deploymentHosts = %v; want %v", got, want)
}
+ // No deployment store, no home map: a single-box deployment is its own.
lone := testIssuerEdge("own.acct.apps.test")
+ if lone.homes.Load() != nil {
+ t.Fatal("test edge unexpectedly has a home map")
+ }
if got := lone.deploymentHosts(); !reflect.DeepEqual(got, lone.currentHosts()) {
t.Fatalf("no home map: deploymentHosts %v != currentHosts %v", got, lone.currentHosts())
}
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go
index 09991129b..e40f4bea8 100644
--- a/internal/host/edgesfile.go
+++ b/internal/host/edgesfile.go
@@ -38,9 +38,11 @@ type edgeHost struct {
Role string `json:"role"`
// Platform marks a row the platform minted (registry Platform or
// Provisioned) — the class edge.currentHosts admits to wildcard-parent
- // derivation. Additive (omitempty): a reader seeing no flag treats the
- // host as unclassed and derives no parent from it (#300).
- Platform bool `json:"platform,omitempty"`
+ // derivation. Additive; always emitted (never omitempty), so a file with
+ // no "platform" key at all is definitely a pre-mark publisher, not a box
+ // homing nothing platform-class. A reader seeing no flag treats the host
+ // as unclassed and derives no parent from it (#300).
+ Platform bool `json:"platform"`
}
// edgesFile is the wire shape at edgesFileKey(box), schema v1 (spec Part 3):
diff --git a/internal/host/edgesfile_test.go b/internal/host/edgesfile_test.go
index 4ec99aef9..79966559a 100644
--- a/internal/host/edgesfile_test.go
+++ b/internal/host/edgesfile_test.go
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"path/filepath"
+ "strings"
"testing"
"time"
@@ -263,7 +264,7 @@ func TestEdgesFileRawWireBodyMatchesSchema(t *testing.T) {
}
want := `{"v":1,"box":"annie-welding","eip":"99.81.104.219","generation":"` +
- probe.Generation + `","hosts":{"app.example.com":{"role":"home"}}}`
+ probe.Generation + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body =\n%s\nwant\n%s", got, want)
}
@@ -305,7 +306,7 @@ func TestEdgesFileRawWireBodyRegionField(t *testing.T) {
}
gen := genOf(t, got)
want := `{"v":1,"box":"annie-welding","eip":"99.81.104.219","generation":"` +
- gen + `","hosts":{"app.example.com":{"role":"home"}}}`
+ gen + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body (region unset) =\n%s\nwant\n%s", got, want)
}
@@ -324,7 +325,7 @@ func TestEdgesFileRawWireBodyRegionField(t *testing.T) {
}
gen := genOf(t, got)
want := `{"v":1,"box":"annie-welding","eip":"99.81.104.219","region":"eu-west-1","generation":"` +
- gen + `","hosts":{"app.example.com":{"role":"home"}}}`
+ gen + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body (region set) =\n%s\nwant\n%s", got, want)
}
@@ -575,6 +576,7 @@ func TestEdgesFileMarksPlatformRows(t *testing.T) {
{Host: "app.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Platform: true},
{Host: "prov.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3001", Status: "ready", Provisioned: true},
{Host: "stray.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3002", Status: "ready"},
+ {Host: "pending.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3003", Status: "pending", Platform: true},
} {
if err := reg.Upsert(r); err != nil {
t.Fatal(err)
@@ -601,4 +603,12 @@ func TestEdgesFileMarksPlatformRows(t *testing.T) {
t.Errorf("%s platform = %v; want %v", host, got, want)
}
}
+ if _, ok := f.Hosts["pending.acct.apps.test"]; ok {
+ t.Error("a non-ready row is published regardless of class")
+ }
+ // The mark is always written, so a keyless host object can only mean a
+ // pre-mark publisher.
+ if !strings.Contains(string(body), `"stray.acct.apps.test":{"role":"home","platform":false}`) {
+ t.Errorf("mark not emitted for an unclassed row: %s", body)
+ }
}