carlos / platform Public

domains: the catch-all label counts are judged, so a stale body neither shares a label nor reserves one (final review F3)

InstanceLabelCounts counted instance record keys with no lifetime, so a
late stale body beside a retired lifetime removed a live instance's
derived alias (ReconcileAliases) and reserved an unproven catch-all label
at the console's attach door. domains.InstanceLabelCountsJudged counts
only records the judge vouches for and reports a label with an
unjudgeable record as unknown: the alias pass keeps that label's row and
derives none, and the reservation stays. With no judge it is today's
count.

internal/domains cannot import internal/provision, so domains declares an
InstanceObserver interface and context key; provision.WithJudge records
the same judge there, so the box's fastConvergeLoop and instancesLoop
(provision.WithLifetimes) and the console's recordCtx carry it with no
new wiring. The console passes provision.InstanceObserver(JudgeFrom(ctx)).
internal/domains joins TestNoUnjudgedRecordReads, InstanceLabelCounts is
forbidden there, and only the judged counter's nil branch is allowlisted.

Census: ReconcileAliases and deriveInstanceAliases re-frozen (the seal
moves); instanceHosts replaces InstanceLabelCounts as a key-site; the
console, domains and provision envelopes refreshed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0316ce6a9be48a47dd0ea28e83bdb98024b29c0a parent 2e9efe2
14 files changed, +572 −23
  • docs/superpowers/notes/census-proposal-self-serve-moves-1.md +1 −1
  • internal/console/domaincatchall.go +7 −2
  • internal/console/domaincatchall_test.go +68 −0
  • internal/console/lifetimes_audit_test.go +18 −5
  • internal/domains/aliases.go +25 −4
  • internal/domains/claims.go +99 −5
  • internal/domains/instancecatchall_judged_test.go +139 −0
  • internal/domains/instancecatchall_lifetimes_test.go +109 −0
  • internal/provision/lifetime_judge.go +24 −0
  • internal/provision/lifetime_judge_test.go +55 −0
  • internal/provision/plan14_root_reads_test.go +5 −2
  • internal/provision/provision.go +6 −1
  • internal/provision/self_serve_moves1_census_test.go +8 −0
  • internal/provision/testdata/plan14-root-reads-self-serve-moves-1.tsv +8 −3
diff --git a/docs/superpowers/notes/census-proposal-self-serve-moves-1.md b/docs/superpowers/notes/census-proposal-self-serve-moves-1.md
index a32f2451e..f271cc8ec 100644
--- a/docs/superpowers/notes/census-proposal-self-serve-moves-1.md
+++ b/docs/superpowers/notes/census-proposal-self-serve-moves-1.md
@@ -42,6 +42,6 @@ Task 9 adds the plan's end-to-end witnesses, in `internal/host/movesoffline_life
Task 9 fix round 1 corrects Task 3's create door. When a delayed create's SQLite commit is refused because recovery already committed its allocation, `Server.allocRecordLocked` now asks `Server.committedElsewhere`. That function reads the allocation row and the record through `readRecord` (judged) and writes nothing. When the row is committed and the record is valid at the allocation's incarnation, the door answers success; anything else stays pending. `Server.allocRecordLocked` stays a record-writer through `WriteRecord` and is re-pinned in place in the record-write inventory. The `internal/console` envelope is refreshed in place. No root read, class, legacy row or seal changes, and `TestNoUnjudgedRecordReads` stays green.
-Final review fix round. F1 changes the cutover door: it skips the box floor only once a validated marker body has been read or written by the door, never on the gate's state alone. A failed marker read still closes admission. `Server.handleAPILifetimesCutover` stays nonrecord and is re-pinned in place in the record-write inventory. F2 narrows the create's orphan rule: a live slot of another owner is retired only when no record exists under that owner's key, and a present body of any verdict refuses the create and is left untouched. `Server.planCreateLocked` is not a census unit, so only the envelope moves. The `internal/console` envelope is refreshed in place after each finding. No root read, class, legacy row or seal changes.
+Final review fix round. F1 changes the cutover door: it skips the box floor only once a validated marker body has been read or written by the door, never on the gate's state alone. A failed marker read still closes admission. `Server.handleAPILifetimesCutover` stays nonrecord and is re-pinned in place in the record-write inventory. F2 narrows the create's orphan rule: a live slot of another owner is retired only when no record exists under that owner's key, and a present body of any verdict refuses the create and is left untouched. `Server.planCreateLocked` is not a census unit, so only the envelope moves. F3 judges the catch-all label counts. `internal/domains` cannot import `internal/provision`, so it declares an `InstanceObserver` interface and its own context key, and `provision.WithJudge` records the same judge there (nil records none). `domains.InstanceLabelCountsJudged` counts only what the judge vouches for and reports a label with an unjudgeable record as unknown; with no observer it is `InstanceLabelCounts`. `ReconcileAliases` and `deriveInstanceAliases` (accepted legacy, rev3=J1) are re-frozen with reviewed bases: a stale body no longer makes a label shared, and an unknown label keeps its existing alias row and derives none, which adds four read sites on that row (`registry:Account`, `registry:Alias`, `registry:App` and the whole `RouteAlias`). That moves the seal. In the record-write inventory, `InstanceLabelCounts` leaves and `instanceHosts`, the key listing it now shares with the judged counter, takes its place as a key-site. `Server.catchallInstanceName` counts through the judged counter with the request's judge. The `internal/domains` and `internal/provision` envelopes are refreshed in place. The `internal/console` envelope is refreshed in place after each finding. No root read, class or task is added.
Taken together, the rows marked `PROPOSED-self-serve-moves-1` are the census of one change. Claims-off instance records gain a lifetime: the host incarnation slot, written only through `WriteHostIncarnation` by the console's create, retirement and migration protocols. Every claims-off consumer judges a record against its slot before it acts on it. Every record write stays on `WriteRecord` with the sequence discipline. The per-host `move_seq` and each row's `(incarnation, serving_seq)` pair order the publications an edge folds. Each row is one of five kinds: a write site added or re-pinned in the record-write inventory (each through `WriteRecord`, a `DeleteIfMatch` on an observed ETag, or a Put of a control key that is not a record); a read made judged at its existing class; a console SQLite unit or a legacy fingerprint with a reviewed basis; the `@fields` schema row, which pins the registry fields Task 7 adds (`Route.Incarnation`, `Route.ServingSeq`, and the remote request's `Pair` and `ExpectPair`); or a source envelope refreshed over the new code. No row adds a raw record write, a root-read class or a task. The two registry read sites added in Task 8 are named in its paragraph. These rows are one proposal, for Paul to rule on once, together, after the plan's final review. Until then every row stays PROPOSED, and a green census means only that the rows match the code, not that they are accepted.
diff --git a/internal/console/domaincatchall.go b/internal/console/domaincatchall.go
index 578a96ced..552bfb359 100644
--- a/internal/console/domaincatchall.go
+++ b/internal/console/domaincatchall.go
@@ -192,12 +192,17 @@ func (s *Server) catchallInstanceName(ctx context.Context, host, sqid, app strin
if s.catchallProven(ctx, parent, c.Account) {
return true, nil
}
- counts, err := domains.InstanceLabelCounts(ctx, s.cfg.Releases, c.Account, c.App)
+ // Judged under a unit admitted on (recordCtx): a stale body beside a
+ // retired lifetime is no instance, and a record that cannot be judged
+ // keeps the reservation. Before the cutover, or claims on, the judge is
+ // nil and this is the key count.
+ counts, unknown, err := domains.InstanceLabelCountsJudged(ctx, s.cfg.Releases,
+ provision.InstanceObserver(provision.JudgeFrom(ctx)), c.Account, c.App, 1)
if err != nil {
return false, err
}
label, _, _ := strings.Cut(host, ".")
- return counts[label] > 0, nil
+ return counts[label] > 0 || unknown[label], nil
}
// catchallProven reports whether parent's DNS carries account's catch-all
diff --git a/internal/console/domaincatchall_test.go b/internal/console/domaincatchall_test.go
index 418ecb083..b657a70a5 100644
--- a/internal/console/domaincatchall_test.go
+++ b/internal/console/domaincatchall_test.go
@@ -372,3 +372,71 @@ func TestInstanceCatchallGuardReservations(t *testing.T) {
t.Fatalf("proof naming another account = %d %s, want 200", code, out)
}
}
+
+// An unproved catch-all reserves a label only for an instance its lifetime
+// vouches for (final review F3): a stale body beside a retired lifetime
+// reserves nothing once lifetimes are on, a body that cannot be judged keeps
+// the reservation, and with no judge (before the cutover) it is today's key
+// count.
+func TestInstanceCatchallReservationIsJudged(t *testing.T) {
+ ctx := context.Background()
+ put := func(t *testing.T, l ltOn, key string, v any) {
+ t.Helper()
+ body, ok := v.([]byte)
+ if !ok {
+ var err error
+ if body, err = json.Marshal(v); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if _, err := l.fx.rel.Put(ctx, key, body, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ world := func(t *testing.T) ltOn {
+ t.Helper()
+ l := newLtOn(t)
+ noCatchallProof(l.fx)
+ put(t, l, domains.ClaimKey("shop.example.com"), domains.Claim{Schema: 1, Host: "shop.example.com", Account: "bab", App: "site"})
+ put(t, l, domains.ClaimKey("*.shop.example.com"), domains.Claim{Schema: 1, Host: "*.shop.example.com", Account: "bab", App: ltApp, Instances: true})
+ return l
+ }
+ record := func(host string, inc uint64) provision.Record {
+ return provision.Record{Schema: 1, Host: host, Slug: "team", App: ltApp, Account: "bab", Channel: "stable", Incarnation: inc, CreatedAt: time.Now().UTC()}
+ }
+ reserved := func(t *testing.T, l ltOn, c context.Context) bool {
+ t.Helper()
+ held, err := l.fx.srv.catchallInstanceName(c, "team.shop.example.com", "cdef", "widgets")
+ if err != nil {
+ t.Fatal(err)
+ }
+ return held
+ }
+
+ t.Run("a stale body reserves nothing", func(t *testing.T) {
+ l := world(t)
+ writeSlot(t, l.fx.rel, "team.old.example.net", provision.HostIncarnation{Incarnation: 1, State: provision.SlotRetired, Account: "bab", App: ltApp})
+ put(t, l, recKey(ltApp, "team.old.example.net"), record("team.old.example.net", 1))
+ if reserved(t, l, onCtx(t, l.fx.srv)) {
+ t.Fatal("judged, a stale body reserved the label")
+ }
+ if !reserved(t, l, ctx) {
+ t.Fatal("with no judge the key reserves the label, as today")
+ }
+ })
+ t.Run("a live instance reserves its label", func(t *testing.T) {
+ l := world(t)
+ writeSlot(t, l.fx.rel, "team.new.example.net", provision.HostIncarnation{Incarnation: 1, State: provision.SlotLive, Account: "bab", App: ltApp})
+ put(t, l, recKey(ltApp, "team.new.example.net"), record("team.new.example.net", 1))
+ if !reserved(t, l, onCtx(t, l.fx.srv)) {
+ t.Fatal("judged, a live instance did not reserve its label")
+ }
+ })
+ t.Run("a body that cannot be judged keeps the reservation", func(t *testing.T) {
+ l := world(t)
+ put(t, l, recKey(ltApp, "team.bad.example.net"), []byte("{not a record"))
+ if !reserved(t, l, onCtx(t, l.fx.srv)) {
+ t.Fatal("judged, a corrupt body released the reservation")
+ }
+ })
+}
diff --git a/internal/console/lifetimes_audit_test.go b/internal/console/lifetimes_audit_test.go
index 7548bbb76..bd8bba95a 100644
--- a/internal/console/lifetimes_audit_test.go
+++ b/internal/console/lifetimes_audit_test.go
@@ -16,15 +16,17 @@ import (
)
// TestNoUnjudgedRecordReads is the audit of self-serve moves plan 1, Task 6:
-// no non-test code in internal/console, internal/host or internal/provision
-// reads an instance record raw, except inside the functions listed in
-// unjudgedReadPermitted. Every claims-off consumer goes through a judged
+// no non-test code in internal/console, internal/host, internal/provision or
+// internal/domains reads an instance record raw, except inside the functions
+// listed in unjudgedReadPermitted. Every claims-off consumer goes through a judged
// reader (ReadRecordJudged, Observe, ObserveWith, the *Judged listings,
// FindRecordByHost, (*RecordIndex).Find), which is the unjudged reader under
// a nil judge, so a claims-on caller is unchanged.
//
// Forbidden, resolved by symbol: the raw provision getters and listings,
-// hostops.ReadRecord, a release.Store Get whose key is provision.RecordKey(...)
+// hostops.ReadRecord, domains.InstanceLabelCounts (a count of instance record
+// keys, read without a lifetime: final review F3), a release.Store Get whose
+// key is provision.RecordKey(...)
// (directly or through a local assigned from it), and provision.DecodeRecord
// of bytes read from the store in the same function. A use that is not a call
// (a function value) counts too.
@@ -44,7 +46,7 @@ func TestNoUnjudgedRecordReads(t *testing.T) {
Env: append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=0"),
Overlay: map[string][]byte{fixture: []byte(unjudgedReadsFixture)},
}
- pkgs, err := packages.Load(cfg, "./internal/console", "./internal/host", "./internal/provision")
+ pkgs, err := packages.Load(cfg, "./internal/console", "./internal/host", "./internal/provision", "./internal/domains")
if err != nil {
t.Fatal(err)
}
@@ -88,6 +90,7 @@ func TestNoUnjudgedRecordReads(t *testing.T) {
c + "auditFixtureHostops -> " + pv + "DecodeRecord of bytes read from the store",
c + "auditFixtureKeyVar -> " + pv + "DecodeRecord of bytes read from the store",
c + "auditFixtureKeyVar -> release.Store Get of provision.RecordKey(...)",
+ c + "auditFixtureLabelCounts -> " + pkgDomains + ".InstanceLabelCounts",
c + "auditFixtureListing -> " + pv + "ListAllRecordsTolerant",
}
sort.Strings(fixed)
@@ -139,12 +142,16 @@ var unjudgedReadPermitted = map[string]string{
"(*github.com/carlosframework/platform/internal/console.Server).refreshClaimRetireRecord": "claims-on: refreshes the record a retire claim holds",
"(*github.com/carlosframework/platform/internal/console.Server).claimAliasUnbacked": "claims-on: the alias path's presence check of a record key",
"github.com/carlosframework/platform/internal/host.relocationRecord": "claims-on: region-HA relocation reads the record a claim holder names",
+ // The catch-all label count: under a nil observer (claims on, or before
+ // the cutover) the judged counter is the key count.
+ "github.com/carlosframework/platform/internal/domains.InstanceLabelCountsJudged": "judged counter; a nil observer is InstanceLabelCounts",
}
const (
pkgProvision = "github.com/carlosframework/platform/internal/provision"
pkgHostops = "github.com/carlosframework/platform/internal/hostops"
pkgRelease = "github.com/carlosframework/platform/internal/release"
+ pkgDomains = "github.com/carlosframework/platform/internal/domains"
)
// unjudgedForbidden are the raw readers, by full name.
@@ -159,6 +166,7 @@ var unjudgedForbidden = []string{
pkgProvision + ".ListAllRecordsStrict",
pkgProvision + ".ListAllRecordsTolerant",
pkgHostops + ".ReadRecord",
+ pkgDomains + ".InstanceLabelCounts",
}
type unjudgedFinding struct {
@@ -405,6 +413,7 @@ const unjudgedReadsFixture = `package console
import (
"context"
+ "github.com/carlosframework/platform/internal/domains"
"github.com/carlosframework/platform/internal/hostops"
"github.com/carlosframework/platform/internal/provision"
)
@@ -444,4 +453,8 @@ func (s *Server) auditFixtureFuncValue() any {
func (s *Server) auditFixtureListing(ctx context.Context) {
_, _, _ = provision.ListAllRecordsTolerant(ctx, s.cfg.Releases)
}
+
+func (s *Server) auditFixtureLabelCounts(ctx context.Context) {
+ _, _ = domains.InstanceLabelCounts(ctx, s.cfg.Releases, "bab", "shop")
+}
`
diff --git a/internal/domains/aliases.go b/internal/domains/aliases.go
index 599a705e9..ba8be39d2 100644
--- a/internal/domains/aliases.go
+++ b/internal/domains/aliases.go
@@ -116,7 +116,13 @@ func sayInstanceCatchallOnce(host, reason string) {
// instance it could name. If it fails, the catch-all's existing rows are
// kept as they are for this pass rather than dropped: a listing blip must
// not take every team's name down.
-func deriveInstanceAliases(claims []Claim, listed map[string]bool, routes []registry.Route, byHost map[string]registry.Route, existing []registry.RouteAlias, deploymentRoutes map[string]bool, desired map[string]bool, labelHosts func(account, app string) (map[string]int, error), reg *registry.Registry, now func() time.Time) {
+//
+// With lifetimes on (a judge in the pass's context), labelHosts counts only
+// the records their lifetimes vouch for, so a stale body left beside a
+// retired lifetime does not make a label shared. A label with a record that
+// could not be judged (unknown) keeps its existing row as it is and derives
+// no new one, unless the judged records alone already share it.
+func deriveInstanceAliases(claims []Claim, listed map[string]bool, routes []registry.Route, byHost map[string]registry.Route, existing []registry.RouteAlias, deploymentRoutes map[string]bool, desired map[string]bool, labelHosts func(account, app string) (map[string]int, map[string]bool, error), reg *registry.Registry, now func() time.Time) {
claimed := make(map[string]Claim, len(claims))
for _, c := range claims {
claimed[c.Host] = c
@@ -150,6 +156,7 @@ func deriveInstanceAliases(claims []Claim, listed map[string]bool, routes []regi
continue
}
var hostsOf map[string]int
+ var unknownOf map[string]bool
for _, rt := range routes {
if !rt.Provisioned || rt.IsSidecar() || rt.Replica || rt.App != c.App || release.RouteAccount(rt) != c.Account {
continue
@@ -159,7 +166,7 @@ func deriveInstanceAliases(claims []Claim, listed map[string]bool, routes []regi
continue
}
if hostsOf == nil {
- counts, err := labelHosts(c.Account, c.App)
+ counts, unknown, err := labelHosts(c.Account, c.App)
if err != nil {
log.Printf("domains: instance catch-all %s: list the app's instances: %v — keeping its names as they are this pass", c.Host, err)
for _, a := range existing {
@@ -170,12 +177,22 @@ func deriveInstanceAliases(claims []Claim, listed map[string]bool, routes []regi
}
break
}
- hostsOf = counts
+ hostsOf, unknownOf = counts, unknown
}
if hostsOf[label] > 1 {
sayInstanceCatchallOnce(c.Host, "label "+label+" is shared by "+strconv.Itoa(hostsOf[label])+" instances of "+c.App+" (its instance domain was changed); "+label+"."+parent+" serves none of them until one is deleted")
continue
}
+ if unknownOf[label] {
+ // A record under this label could not be judged: whether
+ // the label is shared is unknown, so its existing row is
+ // kept as it is this pass and no new one is derived.
+ if a, ok := have[label+"."+parent]; ok && a.Account == c.Account && a.App == c.App {
+ desired[a.Alias] = true
+ }
+ sayInstanceCatchallOnce(c.Host, "a record of label "+label+" could not be judged; its name is kept as it is until it can be")
+ continue
+ }
name := label + "." + parent
if n, ok := NormalizeAliasHost(name); !ok || n != name {
continue // too long once joined to parent
@@ -534,7 +551,11 @@ func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Regist
}
}
deriveInstanceAliases(claims, listed, routes, byHost, existing, deploymentRoutes, desired,
- func(account, app string) (map[string]int, error) { return InstanceLabelCounts(ctx, s, account, app) }, reg, now)
+ func(account, app string) (map[string]int, map[string]bool, error) {
+ // The judge rides in the pass's context (provision.WithLifetimes
+ // records it here too); none is today's key count.
+ return InstanceLabelCountsJudged(ctx, s, InstanceObserverFrom(ctx), account, app, 2)
+ }, reg, now)
for _, a := range existing {
if !desired[a.Alias] {
if err := reg.DeleteAlias(a.Alias); err != nil {
diff --git a/internal/domains/claims.go b/internal/domains/claims.go
index 1de2720dc..520382875 100644
--- a/internal/domains/claims.go
+++ b/internal/domains/claims.go
@@ -144,23 +144,117 @@ func instanceRecordsPrefix(account, app string) string {
// deployment-wide view of which labels the app's instances use, whatever
// box serves them and whatever instance domain each was created under.
// Replica records are counted too; they have labels of their own, so they
-// never inflate another instance's count.
+// never inflate another instance's count. It reads no lifetime: every
+// consumer counts through InstanceLabelCountsJudged, which is this count
+// when no judge is armed.
func InstanceLabelCounts(ctx context.Context, s release.Store, account, app string) (map[string]int, error) {
+ hosts, err := instanceHosts(ctx, s, account, app)
+ if err != nil {
+ return nil, err
+ }
+ counts := map[string]int{}
+ for _, host := range hosts {
+ counts[hostLabel(host)]++
+ }
+ return counts, nil
+}
+
+// instanceHosts is the hosts of the app's instance record keys, from one
+// listing.
+func instanceHosts(ctx context.Context, s release.Store, account, app string) ([]string, error) {
prefix := instanceRecordsPrefix(account, app)
keys, err := s.List(ctx, prefix)
if err != nil {
return nil, err
}
- counts := map[string]int{}
+ var hosts []string
for _, key := range keys {
host, ok := strings.CutSuffix(strings.TrimPrefix(key, prefix), ".json")
if !ok || host == "" || strings.Contains(host, "/") {
continue
}
- label, _, _ := strings.Cut(host, ".")
- counts[label]++
+ hosts = append(hosts, host)
}
- return counts, nil
+ return hosts, nil
+}
+
+func hostLabel(host string) string {
+ label, _, _ := strings.Cut(host, ".")
+ return label
+}
+
+// InstanceVerdict is how an InstanceObserver judges one listed instance
+// record key.
+type InstanceVerdict int
+
+const (
+ // InstanceLive is a record its lifetime vouches for: it is counted.
+ InstanceLive InstanceVerdict = iota
+ // InstanceGone is a key absent by the time it is read, a stale body
+ // beside a later lifetime, or a tombstone: it is not counted.
+ InstanceGone
+ // InstanceUnknown is a record that could not be read or judged, or a
+ // corrupt body: its label's count is unknown.
+ InstanceUnknown
+)
+
+// InstanceObserver judges an instance record against its lifetime. It is
+// provision's lifetime judge seen through an interface, because
+// internal/provision imports this package.
+type InstanceObserver interface {
+ ObserveInstance(ctx context.Context, s release.Store, account, app, host string) InstanceVerdict
+}
+
+type instanceObserverKey struct{}
+
+// WithInstanceObserver records o for InstanceObserverFrom. provision.WithJudge
+// calls it with the same judge it records, so a context armed for lifetimes
+// carries both, and a nil judge records none.
+func WithInstanceObserver(ctx context.Context, o InstanceObserver) context.Context {
+ return context.WithValue(ctx, instanceObserverKey{}, o)
+}
+
+// InstanceObserverFrom is the observer WithInstanceObserver recorded in ctx,
+// or nil.
+func InstanceObserverFrom(ctx context.Context) InstanceObserver {
+ o, _ := ctx.Value(instanceObserverKey{}).(InstanceObserver)
+ return o
+}
+
+// InstanceLabelCountsJudged is InstanceLabelCounts with each key's lifetime
+// observed through o: a key o judges gone is not counted, and a label with a
+// key o cannot judge is in unknown (its count is then only a lower bound).
+// A label listed fewer than minListed times is counted from the listing
+// without a read: ReconcileAliases passes 2, since one key can never make a
+// label shared. A nil o is InstanceLabelCounts exactly, with no unknowns.
+func InstanceLabelCountsJudged(ctx context.Context, s release.Store, o InstanceObserver, account, app string, minListed int) (counts map[string]int, unknown map[string]bool, err error) {
+ if o == nil {
+ counts, err = InstanceLabelCounts(ctx, s, account, app)
+ return counts, nil, err
+ }
+ hosts, err := instanceHosts(ctx, s, account, app)
+ if err != nil {
+ return nil, nil, err
+ }
+ listed := map[string]int{}
+ for _, host := range hosts {
+ listed[hostLabel(host)]++
+ }
+ counts, unknown = map[string]int{}, map[string]bool{}
+ for _, host := range hosts {
+ label := hostLabel(host)
+ if listed[label] < minListed {
+ counts[label]++
+ continue
+ }
+ switch o.ObserveInstance(ctx, s, account, app, host) {
+ case InstanceLive:
+ counts[label]++
+ case InstanceUnknown:
+ unknown[label] = true
+ }
+ }
+ return counts, unknown, nil
}
// CatchallProofName and CatchallProofValue are the TXT record that proves an
diff --git a/internal/domains/instancecatchall_judged_test.go b/internal/domains/instancecatchall_judged_test.go
new file mode 100644
index 000000000..bcbe2a8ee
--- /dev/null
+++ b/internal/domains/instancecatchall_judged_test.go
@@ -0,0 +1,139 @@
+package domains
+
+import (
+ "context"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// fakeObserver judges instance keys by host; a host it does not name is
+// live. reads counts what it was asked.
+type fakeObserver struct {
+ verdicts map[string]InstanceVerdict
+ reads []string
+}
+
+func (f *fakeObserver) ObserveInstance(_ context.Context, _ release.Store, _, _, host string) InstanceVerdict {
+ f.reads = append(f.reads, host)
+ return f.verdicts[host]
+}
+
+// shopCatchall is account bab holding shop.example.com and the instance
+// catch-all *.shop.example.com for its app shop.
+func shopCatchall(t *testing.T) release.Store {
+ t.Helper()
+ s := release.NewDirStore(t.TempDir())
+ putClaim(t, s, Claim{Host: "shop.example.com", Account: "bab", App: "site"})
+ putClaim(t, s, Claim{Host: "*.shop.example.com", Account: "bab", App: "shop", Instances: true})
+ return s
+}
+
+func TestInstanceLabelCountsJudged(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ putRecordKey(t, s, "bab", "shop", "team.new.example.com")
+ putRecordKey(t, s, "bab", "shop", "team.old.example.com")
+ putRecordKey(t, s, "bab", "shop", "solo.new.example.com")
+ putRecordKey(t, s, "bab", "shop", "lost.new.example.com")
+ putRecordKey(t, s, "bab", "shop", "lost.old.example.com")
+
+ // A nil observer is InstanceLabelCounts exactly.
+ got, unknown, err := InstanceLabelCountsJudged(ctx, s, nil, "bab", "shop", 2)
+ want, _ := InstanceLabelCounts(ctx, s, "bab", "shop")
+ if err != nil || unknown != nil || len(got) != len(want) || got["team"] != 2 || got["solo"] != 1 || got["lost"] != 2 {
+ t.Fatalf("nil observer = %v %v %v, want %v", got, unknown, err, want)
+ }
+
+ o := &fakeObserver{verdicts: map[string]InstanceVerdict{
+ "team.old.example.com": InstanceGone,
+ "lost.old.example.com": InstanceUnknown,
+ "solo.new.example.com": InstanceGone,
+ }}
+ got, unknown, err = InstanceLabelCountsJudged(ctx, s, o, "bab", "shop", 2)
+ if err != nil || got["team"] != 1 || got["lost"] != 1 || !unknown["lost"] || unknown["team"] {
+ t.Fatalf("judged = %v %v %v, want team 1, lost 1 and unknown", got, unknown, err)
+ }
+ // solo is listed once: under minListed 2 it is counted without a read.
+ if got["solo"] != 1 {
+ t.Fatalf("solo = %d, want 1 from the listing", got["solo"])
+ }
+ for _, h := range o.reads {
+ if h == "solo.new.example.com" {
+ t.Fatal("a label listed once was read under minListed 2")
+ }
+ }
+ got, _, _ = InstanceLabelCountsJudged(ctx, s, o, "bab", "shop", 1)
+ if got["solo"] != 0 {
+ t.Fatalf("solo under minListed 1 = %d, want 0 (judged gone)", got["solo"])
+ }
+}
+
+// A stale body beside a retired lifetime does not make a label shared: an
+// existing derived name stays (final review F3). A record that cannot be
+// judged keeps the label's name as it is. A nil observer is today's count.
+func TestInstanceCatchallJudgesTheLabelCount(t *testing.T) {
+ setup := func(t *testing.T) (release.Store, func(ctx context.Context) map[string]string) {
+ s := shopCatchall(t)
+ putRecordKey(t, s, "bab", "shop", "team.new.example.com")
+ reg := aliasRegWith(t)
+ addInstance(t, reg, "team.new.example.com", "team", "bab", "shop")
+ pass := func(ctx context.Context) map[string]string {
+ t.Helper()
+ if err := ReconcileAliases(ctx, s, reg, aliasNow(), nil, nil); err != nil {
+ t.Fatal(err)
+ }
+ out := map[string]string{}
+ for k, a := range aliasMap(t, reg) {
+ out[k] = a.Host
+ }
+ return out
+ }
+ if got := pass(context.Background()); got["team.shop.example.com"] != "team.new.example.com" {
+ t.Fatalf("precondition: the name derives: %v", got)
+ }
+ putRecordKey(t, s, "bab", "shop", "team.old.example.com")
+ return s, pass
+ }
+ t.Run("valid and stale", func(t *testing.T) {
+ _, pass := setup(t)
+ o := &fakeObserver{verdicts: map[string]InstanceVerdict{"team.old.example.com": InstanceGone}}
+ if got := pass(WithInstanceObserver(context.Background(), o)); got["team.shop.example.com"] != "team.new.example.com" {
+ t.Fatalf("a stale body beside the live instance removed its name: %v", got)
+ }
+ })
+ t.Run("unreadable evidence", func(t *testing.T) {
+ _, pass := setup(t)
+ o := &fakeObserver{verdicts: map[string]InstanceVerdict{"team.old.example.com": InstanceUnknown}}
+ if got := pass(WithInstanceObserver(context.Background(), o)); got["team.shop.example.com"] != "team.new.example.com" {
+ t.Fatalf("an unjudged record removed the label's name: %v", got)
+ }
+ })
+ t.Run("unreadable evidence derives nothing new", func(t *testing.T) {
+ s := shopCatchall(t)
+ putRecordKey(t, s, "bab", "shop", "team.new.example.com")
+ putRecordKey(t, s, "bab", "shop", "team.old.example.com")
+ reg := aliasRegWith(t)
+ addInstance(t, reg, "team.new.example.com", "team", "bab", "shop")
+ o := &fakeObserver{verdicts: map[string]InstanceVerdict{"team.old.example.com": InstanceUnknown}}
+ if err := ReconcileAliases(WithInstanceObserver(context.Background(), o), s, reg, aliasNow(), nil, nil); err != nil {
+ t.Fatal(err)
+ }
+ if got := aliasMap(t, reg); len(got) != 0 {
+ t.Fatalf("an unjudged label derived a new name: %+v", got)
+ }
+ })
+ t.Run("two live records still share the label", func(t *testing.T) {
+ _, pass := setup(t)
+ o := &fakeObserver{verdicts: map[string]InstanceVerdict{}}
+ if got := pass(WithInstanceObserver(context.Background(), o)); len(got) != 0 {
+ t.Fatalf("a label two live instances share kept a name: %v", got)
+ }
+ })
+ t.Run("no observer is today", func(t *testing.T) {
+ _, pass := setup(t)
+ if got := pass(WithInstanceObserver(context.Background(), nil)); len(got) != 0 {
+ t.Fatalf("with no judge the raw count of two removes the name, as today: %v", got)
+ }
+ })
+}
diff --git a/internal/domains/instancecatchall_lifetimes_test.go b/internal/domains/instancecatchall_lifetimes_test.go
new file mode 100644
index 000000000..59e71ebf2
--- /dev/null
+++ b/internal/domains/instancecatchall_lifetimes_test.go
@@ -0,0 +1,109 @@
+package domains_test
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/domains"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// The catch-all alias pass under the box's real wiring: the lifetime judge
+// rides in provision.WithLifetimes, as fastConvergeLoop and instancesLoop
+// arm it, and ReconcileAliases reads it back through
+// domains.InstanceObserverFrom (final review F3).
+func TestInstanceCatchallAliasesUnderTheLifetimeJudge(t *testing.T) {
+ put := func(t *testing.T, s release.Store, key string, v any) {
+ t.Helper()
+ body, err := json.Marshal(v)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, key, body, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ slot := func(t *testing.T, s release.Store, host, state string) {
+ t.Helper()
+ if _, err := provision.WriteHostIncarnation(ctx, s, host, provision.HostIncarnation{Incarnation: 1, State: state, Account: "bab", App: "shop"}, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ record := func(t *testing.T, s release.Store, host string) {
+ t.Helper()
+ put(t, s, provision.RecordKey("bab", "shop", host), provision.Record{Schema: 1, Host: host, Slug: "team",
+ App: "shop", Account: "bab", Channel: "stable", Incarnation: 1, CreatedAt: time.Now().UTC()})
+ }
+ world := func(t *testing.T) (release.Store, *registry.Registry) {
+ t.Helper()
+ s := dirStore(t)
+ put(t, s, domains.ClaimKey("shop.example.com"), domains.Claim{Schema: 1, Host: "shop.example.com", Account: "bab", App: "site"})
+ put(t, s, domains.ClaimKey("*.shop.example.com"), domains.Claim{Schema: 1, Host: "*.shop.example.com", Account: "bab", App: "shop", Instances: true})
+ slot(t, s, "team.new.example.com", provision.SlotLive)
+ record(t, s, "team.new.example.com")
+ reg := regWith(t, registry.Route{Host: "team.new.example.com", Slug: "team", App: "shop", Account: "bab", Kind: "instance", Status: "ready"})
+ if err := reg.SetProvisioned("team.new.example.com", true); err != nil {
+ t.Fatal(err)
+ }
+ return s, reg
+ }
+ alias := func(t *testing.T, reg *registry.Registry) string {
+ t.Helper()
+ as, err := reg.ListAliases()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, a := range as {
+ if a.Alias == "team.shop.example.com" {
+ return a.Host
+ }
+ }
+ return ""
+ }
+ pass := func(t *testing.T, judged bool, s release.Store, reg *registry.Registry) {
+ t.Helper()
+ c := ctx
+ if judged {
+ c = provision.WithLifetimes(ctx, provision.NewLifetimeJudge())
+ }
+ if err := domains.ReconcileAliases(c, s, reg, time.Now, nil, nil); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ t.Run("a stale body keeps the live instance's name", func(t *testing.T) {
+ s, reg := world(t)
+ pass(t, true, s, reg)
+ if got := alias(t, reg); got != "team.new.example.com" {
+ t.Fatalf("precondition: the name derives, got %q", got)
+ }
+ // The old lifetime of team.old.example.com is retired; its body is
+ // the late stale body the plan permits.
+ slot(t, s, "team.old.example.com", provision.SlotRetired)
+ record(t, s, "team.old.example.com")
+ pass(t, true, s, reg)
+ if got := alias(t, reg); got != "team.new.example.com" {
+ t.Fatalf("judged, a stale body removed the name: alias -> %q", got)
+ }
+ // With no judge (claims on, or before the cutover) it is today's
+ // key count: two keys share the label and the name goes.
+ pass(t, false, s, reg)
+ if got := alias(t, reg); got != "" {
+ t.Fatalf("unjudged, the shared label kept its name: alias -> %q", got)
+ }
+ })
+ t.Run("a corrupt body keeps the name as it is", func(t *testing.T) {
+ s, reg := world(t)
+ pass(t, true, s, reg)
+ if _, err := s.Put(ctx, provision.RecordKey("bab", "shop", "team.old.example.com"), []byte("{not a record"), ""); err != nil {
+ t.Fatal(err)
+ }
+ pass(t, true, s, reg)
+ if got := alias(t, reg); got != "team.new.example.com" {
+ t.Fatalf("judged, a corrupt body removed the name: alias -> %q", got)
+ }
+ })
+}
diff --git a/internal/provision/lifetime_judge.go b/internal/provision/lifetime_judge.go
index 49fa96525..6e2c4fc18 100644
--- a/internal/provision/lifetime_judge.go
+++ b/internal/provision/lifetime_judge.go
@@ -10,6 +10,7 @@ import (
"sort"
"sync"
+ "github.com/carlosframework/platform/internal/domains"
"github.com/carlosframework/platform/internal/release"
)
@@ -106,6 +107,29 @@ func (j *LifetimeJudge) ObserveWith(ctx context.Context, s release.Store, acct,
return obs
}
+// ObserveInstance is the judge as a domains.InstanceObserver, for the
+// catch-all label counts: a record the slot vouches for is live; absent,
+// stale or a tombstone is gone; corrupt or unreadable is unknown.
+func (j *LifetimeJudge) ObserveInstance(ctx context.Context, s release.Store, acct, app, host string) domains.InstanceVerdict {
+ obs := j.Observe(ctx, s, acct, app, host)
+ switch {
+ case obs.Verdict == LifetimeUnreadable || obs.Verdict == LifetimeCorrupt:
+ return domains.InstanceUnknown
+ case !obs.Present || obs.Verdict != LifetimeValid || obs.Rec == nil || obs.Rec.Retired:
+ return domains.InstanceGone
+ }
+ return domains.InstanceLive
+}
+
+// InstanceObserver is j as a domains.InstanceObserver: nil for a nil judge,
+// never a non-nil interface holding a nil judge.
+func InstanceObserver(j *LifetimeJudge) domains.InstanceObserver {
+ if j == nil {
+ return nil
+ }
+ return j
+}
+
// observationRecord returns the record an observation vouches for, or the
// error a reader should report: the observation's own error for a corrupt or
// unreadable one, os.ErrNotExist for an absent, stale or retired one.
diff --git a/internal/provision/lifetime_judge_test.go b/internal/provision/lifetime_judge_test.go
index 11121c7aa..ef3a1caa7 100644
--- a/internal/provision/lifetime_judge_test.go
+++ b/internal/provision/lifetime_judge_test.go
@@ -11,6 +11,7 @@ import (
"strings"
"testing"
+ "github.com/carlosframework/platform/internal/domains"
"github.com/carlosframework/platform/internal/release"
)
@@ -925,3 +926,57 @@ func TestListRecordReplicasJudged(t *testing.T) {
t.Fatalf("nil judge %v (%v), unjudged %v", hosts(nilJudged), err, hosts(plain))
}
}
+
+// The catch-all label counts (internal/domains) read the judge through
+// domains.InstanceObserverFrom, which WithJudge records beside JudgeFrom: the
+// two agree, a nil judge overrides an outer one, and no context ever holds a
+// non-nil observer wrapping a nil judge (final review F3).
+func TestInstanceObserverFollowsTheJudge(t *testing.T) {
+ ctx := context.Background()
+ if domains.InstanceObserverFrom(ctx) != nil || InstanceObserver(nil) != nil {
+ t.Fatal("an unarmed context, or a nil judge, has an observer")
+ }
+ j := NewLifetimeJudge()
+ on := WithLifetimes(ctx, j)
+ if o, ok := domains.InstanceObserverFrom(on).(*LifetimeJudge); !ok || o != j || JudgeFrom(on) != j {
+ t.Fatalf("armed: observer %v, judge %v, want both %p", domains.InstanceObserverFrom(on), JudgeFrom(on), j)
+ }
+ off := WithJudge(on, nil)
+ if domains.InstanceObserverFrom(off) != nil || JudgeFrom(off) != nil {
+ t.Fatal("a nil judge did not override the outer one for both readers")
+ }
+
+ s := release.NewDirStore(t.TempDir())
+ verdict := func(host string) domains.InstanceVerdict {
+ return j.ObserveInstance(ctx, s, "bab", "shop", host)
+ }
+ put := func(host string, body []byte) {
+ if _, err := s.Put(ctx, RecordKey("bab", "shop", host), body, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ rec := func(host string, inc uint64) []byte {
+ b, _ := json.Marshal(Record{Schema: 1, Host: host, App: "shop", Account: "bab", Channel: "stable", Incarnation: inc})
+ return b
+ }
+ slot := func(host, state string) {
+ if _, err := WriteHostIncarnation(ctx, s, host, HostIncarnation{Incarnation: 1, State: state, Account: "bab", App: "shop"}, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ slot("live.example.com", SlotLive)
+ put("live.example.com", rec("live.example.com", 1))
+ slot("old.example.com", SlotRetired)
+ put("old.example.com", rec("old.example.com", 1))
+ put("bad.example.com", []byte("{not a record"))
+ for host, want := range map[string]domains.InstanceVerdict{
+ "live.example.com": domains.InstanceLive,
+ "old.example.com": domains.InstanceGone,
+ "gone.example.com": domains.InstanceGone,
+ "bad.example.com": domains.InstanceUnknown,
+ } {
+ if got := verdict(host); got != want {
+ t.Errorf("%s = %v, want %v", host, got, want)
+ }
+ }
+}
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index 4e8964763..ad15ac310 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -1286,8 +1286,11 @@ func TestPlan14RootReadCensus(t *testing.T) {
// re-pins the two offline release units in place (claims on releases
// through today's ReleaseHold), which moves it again. Task 8 (the pair
// and target_host in the edges file) re-freezes edgesFileLoop, which
- // takes the box's lifetimes switch, and moves it once more.
- seal = "49b0bd1e34f165fe37c0bc2953172559c93af366425e83e60f454d237c11b0f5"
+ // takes the box's lifetimes switch, and moves it once more. The final
+ // review's fix round (F3) re-freezes ReconcileAliases and
+ // deriveInstanceAliases, whose catch-all label counts are judged, and
+ // moves it again.
+ seal = "09fc7839a7111d1e945ac07f37a1b28659634149b90537ae4b533af1d98de290"
errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...)
errs = append(errs, checkRootReadDeclarations(got, inv)...)
if len(errs) > 0 {
diff --git a/internal/provision/provision.go b/internal/provision/provision.go
index 5b78f7939..35b0faaf8 100644
--- a/internal/provision/provision.go
+++ b/internal/provision/provision.go
@@ -25,6 +25,7 @@ import (
"github.com/carlosframework/platform/internal/controlname"
"github.com/carlosframework/platform/internal/controlsock"
+ "github.com/carlosframework/platform/internal/domains"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
"golang.org/x/crypto/bcrypt"
@@ -953,8 +954,12 @@ func updateRecord(ctx context.Context, s release.Store, acct, app, host string,
type lifetimeJudgeKey struct{}
// WithJudge returns a context whose record updates act on the judge's
-// observation instead of a bare read. A nil judge is recorded as none.
+// observation instead of a bare read. A nil judge is recorded as none. The
+// same judge is recorded for domains.InstanceObserverFrom, so the catch-all
+// label counts (internal/domains, which cannot import this package) judge
+// exactly when this context's record reads do.
func WithJudge(ctx context.Context, j *LifetimeJudge) context.Context {
+ ctx = domains.WithInstanceObserver(ctx, InstanceObserver(j))
return context.WithValue(ctx, lifetimeJudgeKey{}, j)
}
diff --git a/internal/provision/self_serve_moves1_census_test.go b/internal/provision/self_serve_moves1_census_test.go
index 8033a1476..1db6e0dd8 100644
--- a/internal/provision/self_serve_moves1_census_test.go
+++ b/internal/provision/self_serve_moves1_census_test.go
@@ -232,6 +232,12 @@ package provision
// as before, re-pinned in place. The box floor is skipped only once a
// validated marker body was read (or written by the door), never on the
// gate's state alone; the marker Put is unchanged.
+// - F3: domains/claims.go:InstanceLabelCounts leaves the inventory and
+// instanceHosts takes its place: the listing of the app's instance
+// record keys moved into that helper, which InstanceLabelCounts and
+// InstanceLabelCountsJudged share. key-site as before: it lists keys and
+// writes nothing. The judged counter observes each key through the
+// context's judge (provision's ObserveInstance) and writes nothing.
func plan14SelfServeMoves1WriteCensusInventory() []censusEntry {
removed := []struct{ file, unit string }{
{"internal/provision/provision.go", "GetRecordETag"},
@@ -279,6 +285,7 @@ func plan14SelfServeMoves1WriteCensusInventory() []censusEntry {
{"internal/console/channelmove.go", "Server.handleAPIInstanceChannel"},
{"internal/console/fleets.go", "Server.placeAppTarget"},
{"internal/provision/reconcile.go", "mintHeld"},
+ {"internal/domains/claims.go", "InstanceLabelCounts"},
}
added := []censusEntry{
{"internal/provision/provision.go", "updateRecordSeeding", "record-writer", "key:RecordKey,key:WriteRecord", "0bd38721d7cc97f37dbe8f37803f89d3e6efdaf29f9d6542a1d1b4d8eb370a3e"},
@@ -345,6 +352,7 @@ func plan14SelfServeMoves1WriteCensusInventory() []censusEntry {
{"internal/console/channelmove.go", "Server.handleAPIInstanceChannel", "record-writer", "key:UpdateRecord", "94d0979010cbe5bed720c16d5e96bfa7dc000711c836709fbc44681aab4ce3f7"},
{"internal/console/fleets.go", "Server.placeAppTarget", "record-writer", "key:RecordsPrefix,key:SetRecordRegion", "c7afc71427abb8e831670d7ea9cf0d42f870aca7e132f1655d1ce968254bea19"},
{"internal/provision/reconcile.go", "mintHeld", "key-site", "key:RecordKey", "0c11b996b5e773011047c1bfefb959dd694080abd614b67709290f50f2093e85"},
+ {"internal/domains/claims.go", "instanceHosts", "key-site", "key:instanceRecordsPrefix", "0552090e94107e58bfc06c8f69a827b88d0d063d4ca889252080b32b463e57ff"},
}
entries := plan14ClaimRosterBindingWriteCensusInventory()
for _, r := range removed {
diff --git a/internal/provision/testdata/plan14-root-reads-self-serve-moves-1.tsv b/internal/provision/testdata/plan14-root-reads-self-serve-moves-1.tsv
index 20821a897..2cd3e0f3c 100644
--- a/internal/provision/testdata/plan14-root-reads-self-serve-moves-1.tsv
+++ b/internal/provision/testdata/plan14-root-reads-self-serve-moves-1.tsv
@@ -1,5 +1,5 @@
# Census overlay self-serve-moves-1, marked rev3=PROPOSED-self-serve-moves-1 (not an accepted ruling; docs/superpowers/notes/census-proposal-self-serve-moves-1.md). Task 1 adds the lifetime fields to provision.Record (incarnation, incarnation_migrating, move_seq, retired) and internal/provision/lifetime.go (the host incarnation slot, the lifetime verdict, the tombstone body, GetRecordRaw); GetRecordETag now hides a tombstone. Envelope only: no new root read, class or task, and the registry schema (@fields) is unchanged because Record is not a registry struct. internal/provision is re-pinned over the new code. The seal stands.
-@coverage internal/provision 68e9c5f7b5f6e9f994007e3f7de9149ddbced3155512583215ec444fd708ee18
+@coverage internal/provision d3de4ce79f1ca7ea8af3e59de0769de53427f0392821252b695ae347971559d1
# Task 2 and fix round 1: rev3=PROPOSED-self-serve-moves-1 throughout; all dispositions below await the human census ruling. Console SQL and randomness run as carlos-console under console.service; no root decision is added. Existing legacy rows are re-frozen at this boundary, and the two constructors lose their own read sites to insertAppRow.
# The H3 removal syntax requires the census-ruling- prefix; PROPOSED in those tokens is deliberate and does not claim approval.
@legacy 296610cb0201ccb67b21152a50191b8b819f41e1d39fd43ca8b8d35cd75a2601 internal/console/lifetimes_store.go Store.BeginAllocation legacy accepted-legacy accepted-legacy;rev3=PROPOSED-self-serve-moves-1;basis=console.service user carlos-console, J4 root-input: inserts one pending allocation in the console SQLite store, carrying the account, app and app lifetime; no bucket or root action file:Exec@132 7c7cb202595de014e2a549ebb001ae2c0afd800fc7959a6b5e145f5c1bc9c8ef
@@ -45,7 +45,7 @@ internal/console/store_gate.go claimSQLCensus legacy accepted-legacy accepted-le
@remove internal/console/tempstore.go Store.CreateTemporaryAppInRegion
@removed internal/console/tempstore.go Store.CreateTemporaryAppInRegion H3 census-ruling-PROPOSED-self-serve-moves-1;rev3=PROPOSED-self-serve-moves-1;basis=constructor remains declared but has no direct read; app insertion and lifetime minting moved to insertAppRow, classified above; proposed removal only
# Task 2 envelopes, rev3=PROPOSED-self-serve-moves-1: internal/console covers the lifetime store, app constructors, lifetime projection and table-scoped gate labels; internal/provision (above) adds only ErrLifetimeExhausted beyond Task 1. Registry fields are unchanged.
-@coverage internal/console 3b006c37c57938e97b34a1407fea006eb83d273b963f7e8b0fb747ab9c8ac12e
+@coverage internal/console 6a7239960c7ef46a0d16e3a0c01df6c74d68ea9f5c93a49bce7dcf45e9ec0b77
# Task 6a: rev3=PROPOSED-self-serve-moves-1; reviewed provision envelope now includes bounded record-then-slot observations, judged read/list/update gates and signed-counter exhaustion at slot read. No removal-loop, host or console change; registry fields and the legacy seal stand.
internal/provision/rootrecord.go IndexRecords authority B3,C rev3=PROPOSED-self-serve-moves-1;basis=retain every discovered candidate owner key per host for fresh lifetime judgement at Find; partial discovery still retries and no verdict or body is cached e44ac431865bf38baa4105f66c9cb23014cc3c7ffcd41d5385244cc76fc4c0b9
internal/provision/rootrecord.go RecordIndex.Find authority B3,C rev3=PROPOSED-self-serve-moves-1;basis=observe each candidate through the context judge, refuse corruption and read failures, require one valid owner and validate its schema and shape; nil-judge duplicates retain permission refusal 760eed59cbd6a8ddab1098ce3a5f7a4272ecda81c17cc079dc135aba3a85a8f8
@@ -73,7 +73,7 @@ internal/console/lifetimes.go newLifetimeOpID legacy accepted-legacy accepted-le
internal/console/lifetimes_store.go Store.ConvertRetirementKind legacy accepted-legacy accepted-legacy;rev3=PROPOSED-self-serve-moves-1;basis=console.service user carlos-console, J4 root-input: converts one pending normal SQLite retirement intent to kind cleanup with the corrupt body's ETag and refuses any other row file:Exec@215 400ffadd8d2210de9016fd288e8350307c7ecf7ba3f40dc08b9dacf63a6234b1
@legacy 1fd9608bc83e53fa767adb28b116414434d5bd87a480b15181a12d3c763f95ec internal/domains/reconcile.go Reconcile legacy accepted-legacy accepted-legacy;rev3=PROPOSED-self-serve-moves-1;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos), from its domains loop: the seeded-instances body (a trashed app's route kept while an import on its host is journaled, deleted only if the row is still the one listed); self-serve moves 1 Task 4 removes the claims-off 1d auto-purge, so a claims-off box destroys no app objects and a host-claim box only requests the claim's purge as before; the registry reads are unchanged registry:Account@5579,registry:Account@5775,registry:App@10116,registry:App@11452,registry:App@13691,registry:App@5380,registry:App@5528,registry:App@7191,registry:App@7248,registry:App@7449,registry:App@7662,registry:App@7900,registry:App@8090,registry:Canary@10060,registry:Canary@12738,registry:Canary@13582,registry:Channel@11507,registry:Channel@12715,registry:Channel@13613,registry:Channel@13700,registry:Channel@14003,registry:Host@10023,registry:Host@10702,registry:Host@13738,registry:Host@13823,registry:Host@13994,registry:Host@5650,registry:Host@5736,registry:Host@7298,registry:Host@7440,registry:Host@7653,registry:Host@7891,registry:Host@8081,registry:Kind@12381,registry:Platform@10044,registry:Platform@13567,whole:Route@10005,whole:Route@10111,whole:Route@10172,whole:Route@10378,whole:Route@10394,whole:Route@10520,whole:Route@10548,whole:Route@10714,whole:Route@10730,whole:Route@10738,whole:Route@11437,whole:Route@11490,whole:Route@11647,whole:Route@11663,whole:Route@12708,whole:Route@12778,whole:Route@12947,whole:Route@12963,whole:Route@12971,whole:Route@13522,whole:Route@13553,whole:Route@13686,whole:Route@13841,whole:Route@13855,whole:Route@13861,whole:Route@14033,whole:Route@14047,whole:Route@14053,whole:Route@14061,whole:Route@14070,whole:Route@5366,whole:Route@5380,whole:Route@5510,whole:Route@5528,whole:Route@5579,whole:Route@5650,whole:Route@5736,whole:Route@5775,whole:Route@7144,whole:Route@7176,whole:Route@7243,whole:Route@7469,whole:Route@7487,whole:Route@7497,whole:Route@7563,whole:Route@7680,whole:Route@7698,whole:Route@7708,whole:Route@7913,whole:Route@7931,whole:Route@7941,whole:Route@8119,whole:Route@8137,whole:Route@8147,whole:Route@8157,whole:Route@8166 9ee4958e2acd7313769f764102349ba0e6435c5dd895e9ac8e6c0f794734a7a4
internal/domains/reconcile.go Reconcile legacy accepted-legacy accepted-legacy;rev3=PROPOSED-self-serve-moves-1;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos), from its domains loop: the seeded-instances body (a trashed app's route kept while an import on its host is journaled, deleted only if the row is still the one listed); self-serve moves 1 Task 4 removes the claims-off 1d auto-purge, so a claims-off box destroys no app objects and a host-claim box only requests the claim's purge as before; the registry reads are unchanged registry:Account@5579,registry:Account@5775,registry:App@10116,registry:App@11452,registry:App@13691,registry:App@5380,registry:App@5528,registry:App@7191,registry:App@7248,registry:App@7449,registry:App@7662,registry:App@7900,registry:App@8090,registry:Canary@10060,registry:Canary@12738,registry:Canary@13582,registry:Channel@11507,registry:Channel@12715,registry:Channel@13613,registry:Channel@13700,registry:Channel@14003,registry:Host@10023,registry:Host@10702,registry:Host@13738,registry:Host@13823,registry:Host@13994,registry:Host@5650,registry:Host@5736,registry:Host@7298,registry:Host@7440,registry:Host@7653,registry:Host@7891,registry:Host@8081,registry:Kind@12381,registry:Platform@10044,registry:Platform@13567,whole:Route@10005,whole:Route@10111,whole:Route@10172,whole:Route@10378,whole:Route@10394,whole:Route@10520,whole:Route@10548,whole:Route@10714,whole:Route@10730,whole:Route@10738,whole:Route@11437,whole:Route@11490,whole:Route@11647,whole:Route@11663,whole:Route@12708,whole:Route@12778,whole:Route@12947,whole:Route@12963,whole:Route@12971,whole:Route@13522,whole:Route@13553,whole:Route@13686,whole:Route@13841,whole:Route@13855,whole:Route@13861,whole:Route@14033,whole:Route@14047,whole:Route@14053,whole:Route@14061,whole:Route@14070,whole:Route@5366,whole:Route@5380,whole:Route@5510,whole:Route@5528,whole:Route@5579,whole:Route@5650,whole:Route@5736,whole:Route@5775,whole:Route@7144,whole:Route@7176,whole:Route@7243,whole:Route@7469,whole:Route@7487,whole:Route@7497,whole:Route@7563,whole:Route@7680,whole:Route@7698,whole:Route@7708,whole:Route@7913,whole:Route@7931,whole:Route@7941,whole:Route@8119,whole:Route@8137,whole:Route@8147,whole:Route@8157,whole:Route@8166 9ee4958e2acd7313769f764102349ba0e6435c5dd895e9ac8e6c0f794734a7a4
-@coverage internal/domains f0f68ddee74e459f4416359b4b5d8c911d6c0850d70032bd7346c6b82f1a0885
+@coverage internal/domains 78f7cb6c185da532240d402d3444b9750b9a81f86f1d204d26851309299e3e41
@coverage internal/host c520fb14e1584aebc16285652da59f50b945dc2fc9159919d4c5b3d7ff628169
@coverage internal/release 99ef7ef9b58b82e6fb4b22a6e378a04d8fd9cccafe1f8726710b8181058ec065
# Task 6b (consumers observe the lifetime: the provision reconcile and import journal, the host wiring and readers), rev3=PROPOSED-self-serve-moves-1: every changed unit re-pinned at its class and task with a reviewed basis; legacy units keep accepted-legacy and take a new frozen fingerprint (the seal moves); the four units with no read left re-pinned at their declarations; seedrollback.go journalRecordJudged added as accepted-legacy beside journalRecordByKey. All rows await the human census ruling.
@@ -194,3 +194,8 @@ internal/host/edgesfile.go edgesFileWriteClaims authority F,J1 rev3=PROPOSED-sel
internal/host/edgesfile.go edgesFileLoop legacy accepted-legacy accepted-legacy;rev3=PROPOSED-self-serve-moves-1;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): census-ruling-moves-offline's body, which also takes the box's lifetimes switch (runAgent's boxJudge != nil) and hands it to every edgesFileWriteClaims pass; read site unchanged, offset moved; a report, not a decision; self-serve moves 1 Task 8 file:Read@2757 65c1d42c39f7f69035ccc6c2a6ce92f155f26d7c7af4b82b4633e215557615e7
# Task 9 fix round 1 (a delayed create whose replay already committed answers success), rev3=PROPOSED-self-serve-moves-1: allocRecordLocked asks committedElsewhere when its SQLite commit is refused (one SQLite read of its own allocation row, one judged record read through readRecord; no write). No root read, class, legacy row or seal changes; the internal/console envelope is refreshed in place above.
# Final review fix round, rev3=PROPOSED-self-serve-moves-1: F1 makes the cutover door run the box floor unless a validated marker was read (no new read site; the floor reads are the door's as before). F2 makes a create refuse another owner's live slot whenever any record is present under that owner's key (one fewer path to a retirement; no read site added). No root read, class, legacy row or seal changes; the internal/console envelope is refreshed in place above.
+# Final review fix round F3 (catch-all label counts judged), rev3=PROPOSED-self-serve-moves-1: ReconcileAliases (accepted legacy) now hands deriveInstanceAliases a judged counter (domains.InstanceLabelCountsJudged with the context's observer, which provision.WithJudge records beside the judge; nil is today's key count); its read sites are unchanged, offsets moved. deriveInstanceAliases (accepted legacy) skips stale bodies in the shared-label count and, for a label with a record that cannot be judged, keeps its existing alias row (four new sites on that row: registry:Account, registry:Alias, registry:App and the whole RouteAlias) and derives none. Both are re-frozen with reviewed bases, which moves the seal. The internal/console (catchallInstanceName judged), internal/domains and internal/provision (ObserveInstance, InstanceObserver, WithJudge) envelopes are refreshed in place. No class or task is added; registry fields stand.
+@legacy bfee05191142ea2c7985d11be04b944c38dccb6e6eed9f5eb14e3fd5f788047f internal/domains/aliases.go ReconcileAliases legacy accepted-legacy accepted-legacy;rev3=PROPOSED-self-serve-moves-1;basis=root-route; the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos), from its fast-converge and instances loops: rev3=J1's body, which now counts the catch-all's labels through domains.InstanceLabelCountsJudged with the pass context's observer (provision.WithLifetimes on a claims-off box; none on a claims-on box, which is today's key count); registry read sites unchanged, offsets moved registry:Account@15249,registry:Account@15262,registry:Alias@14864,registry:Alias@14904,registry:Alias@14974,registry:App@11952,registry:App@12152,registry:App@12308,registry:App@12509,registry:App@15275,registry:App@15284,registry:App@2026,registry:App@2222,registry:App@5823,registry:App@6022,registry:Host@15234,registry:Host@15523,registry:Host@15576,registry:Host@15648,registry:Host@16160,registry:Host@16252,registry:Host@16311,registry:Host@16392,registry:Host@406,registry:Prefix@16168,registry:Prefix@16266,registry:Prefix@16319,registry:Prefix@16400,registry:Status@8686,whole:Route@11921,whole:Route@11996,whole:Route@12143,whole:Route@12205,whole:Route@12348,whole:Route@12504,whole:Route@13400,whole:Route@1369,whole:Route@14469,whole:Route@14477,whole:Route@2066,whole:Route@2217,whole:Route@2273,whole:Route@359,whole:Route@388,whole:Route@399,whole:Route@417,whole:Route@5169,whole:Route@5867,whole:Route@6013,whole:Route@8016,whole:Route@8468,whole:Route@8630,whole:RouteAlias@14485,whole:RouteAlias@14839,whole:RoutePath@10213,whole:RoutePath@16053,whole:RoutePath@16095,whole:RoutePath@16209,whole:RoutePath@16239,whole:WildcardDomain@12720,whole:WildcardDomain@15164,whole:WildcardDomain@15213,whole:WildcardDomain@15376,whole:WildcardDomain@15482,whole:WildcardDomain@15511 f0cb08a90be8c8d6c97c24be3cf73720abb54cb8444793fa25ba982e9525e9f9
+internal/domains/aliases.go ReconcileAliases legacy accepted-legacy accepted-legacy;rev3=PROPOSED-self-serve-moves-1;basis=root-route; the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos), from its fast-converge and instances loops: rev3=J1's body, which now counts the catch-all's labels through domains.InstanceLabelCountsJudged with the pass context's observer (provision.WithLifetimes on a claims-off box; none on a claims-on box, which is today's key count); registry read sites unchanged, offsets moved registry:Account@15249,registry:Account@15262,registry:Alias@14864,registry:Alias@14904,registry:Alias@14974,registry:App@11952,registry:App@12152,registry:App@12308,registry:App@12509,registry:App@15275,registry:App@15284,registry:App@2026,registry:App@2222,registry:App@5823,registry:App@6022,registry:Host@15234,registry:Host@15523,registry:Host@15576,registry:Host@15648,registry:Host@16160,registry:Host@16252,registry:Host@16311,registry:Host@16392,registry:Host@406,registry:Prefix@16168,registry:Prefix@16266,registry:Prefix@16319,registry:Prefix@16400,registry:Status@8686,whole:Route@11921,whole:Route@11996,whole:Route@12143,whole:Route@12205,whole:Route@12348,whole:Route@12504,whole:Route@13400,whole:Route@1369,whole:Route@14469,whole:Route@14477,whole:Route@2066,whole:Route@2217,whole:Route@2273,whole:Route@359,whole:Route@388,whole:Route@399,whole:Route@417,whole:Route@5169,whole:Route@5867,whole:Route@6013,whole:Route@8016,whole:Route@8468,whole:Route@8630,whole:RouteAlias@14485,whole:RouteAlias@14839,whole:RoutePath@10213,whole:RoutePath@16053,whole:RoutePath@16095,whole:RoutePath@16209,whole:RoutePath@16239,whole:WildcardDomain@12720,whole:WildcardDomain@15164,whole:WildcardDomain@15213,whole:WildcardDomain@15376,whole:WildcardDomain@15482,whole:WildcardDomain@15511 f0cb08a90be8c8d6c97c24be3cf73720abb54cb8444793fa25ba982e9525e9f9
+@legacy a37ea03438815852c187e4b5e8d83641253f23e8cbd48898abd4e2c72820c65e internal/domains/aliases.go deriveInstanceAliases legacy accepted-legacy accepted-legacy;rev3=PROPOSED-self-serve-moves-1;basis=root-route; the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): rev3=J1's body; a stale body under a retired lifetime no longer makes a label shared, and a label with a record that cannot be judged keeps its existing alias row of the catch-all's own account and app (reading that row's Account, App and Alias) and derives no new one; a report-only conservative keep, never a new route registry:Account@2163,registry:Account@2950,registry:Account@3913,registry:Alias@2225,registry:Alias@2293,registry:Alias@2337,registry:Alias@3006,registry:Alias@557,registry:App@1686,registry:App@2189,registry:App@2976,registry:App@3939,registry:Host@1843,registry:Host@3892,registry:Host@3902,registry:Host@4098,registry:Provisioned@1636,registry:Replica@1672,registry:Slug@1775,whole:Route@1620,whole:Route@1726,whole:Route@3502,whole:RouteAlias@2143,whole:RouteAlias@2920,whole:RouteAlias@3874,whole:RouteAlias@509,whole:RouteAlias@539,whole:RouteAlias@552,whole:RouteAlias@568 888a5a32dbbcd9619b0bf513c0dc746686ad7c4e5681bf82248022e8f13b2512
+internal/domains/aliases.go deriveInstanceAliases legacy accepted-legacy accepted-legacy;rev3=PROPOSED-self-serve-moves-1;basis=root-route; the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): rev3=J1's body; a stale body under a retired lifetime no longer makes a label shared, and a label with a record that cannot be judged keeps its existing alias row of the catch-all's own account and app (reading that row's Account, App and Alias) and derives no new one; a report-only conservative keep, never a new route registry:Account@2163,registry:Account@2950,registry:Account@3913,registry:Alias@2225,registry:Alias@2293,registry:Alias@2337,registry:Alias@3006,registry:Alias@557,registry:App@1686,registry:App@2189,registry:App@2976,registry:App@3939,registry:Host@1843,registry:Host@3892,registry:Host@3902,registry:Host@4098,registry:Provisioned@1636,registry:Replica@1672,registry:Slug@1775,whole:Route@1620,whole:Route@1726,whole:Route@3502,whole:RouteAlias@2143,whole:RouteAlias@2920,whole:RouteAlias@3874,whole:RouteAlias@509,whole:RouteAlias@539,whole:RouteAlias@552,whole:RouteAlias@568 888a5a32dbbcd9619b0bf513c0dc746686ad7c4e5681bf82248022e8f13b2512