Skip to content

Commit 4587dce

Browse files
committed
fix(registry): make operation migration authoritative
1 parent 4641117 commit 4587dce

60 files changed

Lines changed: 1916 additions & 850 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/registry/provenance.go

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,26 @@ import (
1515
// through OperationFormatHistory; it is a property of the store, not of one
1616
// row.
1717
const (
18+
OperationRegistryMetadataVersion = 1
19+
20+
OperationModeTransition OperationMode = "transition"
21+
OperationModeReconcile OperationMode = "reconcile"
22+
1823
// OperationFormatLegacy is the shape written before registry-owned
19-
// provenance: operations carry no provenance fields, and an ns.dependency
20-
// entry states its deployment root through Entry.DependencyRoot. Such a
21-
// store also holds updates for entries the current baseline no longer
22-
// declares, which replay applies as upserts.
24+
// provenance. Such a store also holds updates for entries the current
25+
// baseline no longer declares, which replay applies as upserts.
2326
OperationFormatLegacy = 1
2427
// OperationFormatProvenance is the shape written by a runtime that owns
2528
// provenance: every operation carries its own record, so replay holds the
2629
// same total-map invariant a live transition holds.
2730
OperationFormatProvenance = 2
31+
// OperationFormatRegistryMetadata stores registry-owned state in its own
32+
// versioned operation block instead of adding fields to the entry or the
33+
// operation envelope.
34+
OperationFormatRegistryMetadata = 3
2835
// OperationFormatCurrent is the format this runtime writes. A wire-format
2936
// change bumps it together with the migration that re-stamps the store.
30-
OperationFormatCurrent = OperationFormatProvenance
37+
OperationFormatCurrent = OperationFormatRegistryMetadata
3138
)
3239

3340
// OperationFormatSnapshot is the complete, source-verified view a semantic
@@ -74,6 +81,60 @@ var ErrOperationFormatMigrationConflict = errors.New("operation format migration
7481
// until all branches have been rewritten.
7582
var ErrOperationFormatMigrationIncomplete = errors.New("operation format migration does not cover complete history")
7683

84+
// NewOperationRegistryMetadata creates the versioned registry-owned portion
85+
// of an operation. The records are copied so callers retain no mutable alias.
86+
func NewOperationRegistryMetadata(current, previous *EntryProvenance) *OperationRegistryMetadata {
87+
if current == nil && previous == nil {
88+
return nil
89+
}
90+
return NewOperationRegistryMetadataWithMode(OperationModeTransition, current, previous)
91+
}
92+
93+
// NewOperationRegistryMetadataWithMode creates registry-owned operation state
94+
// with explicit application semantics.
95+
func NewOperationRegistryMetadataWithMode(mode OperationMode, current, previous *EntryProvenance) *OperationRegistryMetadata {
96+
metadata := &OperationRegistryMetadata{Version: OperationRegistryMetadataVersion, Mode: mode}
97+
if current != nil {
98+
copy := *current
99+
metadata.Current = &copy
100+
}
101+
if previous != nil {
102+
copy := *previous
103+
metadata.Previous = &copy
104+
}
105+
return metadata
106+
}
107+
108+
// OperationMode returns the registry application semantics carried by the
109+
// operation. An empty value is invalid in the current operation format.
110+
func (o Operation) OperationMode() OperationMode {
111+
if o.Registry == nil {
112+
return ""
113+
}
114+
return o.Registry.Mode
115+
}
116+
117+
// CurrentProvenance returns the registry state produced by the operation.
118+
func (o Operation) CurrentProvenance() *EntryProvenance {
119+
if o.Registry == nil {
120+
return nil
121+
}
122+
return o.Registry.Current
123+
}
124+
125+
// PreviousProvenance returns the registry state replaced by the operation.
126+
func (o Operation) PreviousProvenance() *EntryProvenance {
127+
if o.Registry == nil {
128+
return nil
129+
}
130+
return o.Registry.Previous
131+
}
132+
133+
// SetRegistryMetadata replaces the registry-owned portion of an operation.
134+
func (o *Operation) SetRegistryMetadata(current, previous *EntryProvenance) {
135+
o.Registry = NewOperationRegistryMetadata(current, previous)
136+
}
137+
77138
// ErrMissingProvenance reports a state entry without a provenance record — a
78139
// violation of the ProvenanceMap total-map invariant.
79140
var ErrMissingProvenance = errors.New("entry has no provenance record")

api/registry/registry.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,24 +114,34 @@ type (
114114
Meta attrs.Bag `json:"meta"`
115115
ID ID `json:"id"`
116116
Kind Kind `json:"kind"`
117-
// DependencyRoot identifies an ns.dependency selected as a deployment
118-
// root. It is independent of package ownership, so a published app entry
119-
// can retain meta.module while remaining a root.
120-
DependencyRoot bool `json:"dependency_root,omitempty"`
121117
}
122118

123119
// ChangeSet represents a set of operations to transition the registry from one state to another
124120
ChangeSet []Operation
125121

126122
// Operation represents a single operation within a ChangeSet
127123
Operation struct {
128-
OriginalEntry *Entry `json:"original_entry,omitempty"`
129-
Provenance *EntryProvenance `json:"provenance,omitempty"`
130-
OriginalProvenance *EntryProvenance `json:"original_provenance,omitempty"`
131-
Kind event.Kind `json:"kind"`
132-
Entry Entry `json:"entry"`
124+
OriginalEntry *Entry `json:"original_entry,omitempty"`
125+
Registry *OperationRegistryMetadata `json:"registry,omitempty"`
126+
Kind event.Kind `json:"kind"`
127+
Entry Entry `json:"entry"`
133128
}
134129

130+
// OperationRegistryMetadata is registry-owned transition state. It is kept
131+
// outside the author-owned entry and versioned independently so additional
132+
// registry invariants do not enlarge the entry schema.
133+
OperationRegistryMetadata struct {
134+
Current *EntryProvenance `json:"current,omitempty"`
135+
Previous *EntryProvenance `json:"previous,omitempty"`
136+
Mode OperationMode `json:"mode"`
137+
Version int `json:"version"`
138+
}
139+
140+
// OperationMode defines how persisted registry state is applied. Live
141+
// transitions are exact; migrated history converges the stored declaration
142+
// independently of the deployment baseline present during replay.
143+
OperationMode string
144+
135145
// DependencyPattern defines a pattern for extracting dependencies from entries
136146
DependencyPattern struct {
137147
// Path is the location in entry metadata/data to search (e.g., "meta.server", "data.fs")

api/registry/resolution.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type DependencyRoot struct {
5050
type DependencyResolution struct {
5151
Digest string `json:"digest"`
5252
InputDigest string `json:"input_digest"`
53+
BaselineModel string `json:"baseline_model,omitempty"`
5354
BaselineDigest string `json:"baseline_digest,omitempty"`
5455
Roots []DependencyRoot `json:"roots"`
5556
// References are root-shaped declarations folded into an existing root for
@@ -75,7 +76,8 @@ func CanRebaseDependencyResolution(existing, next *DependencyResolution) bool {
7576
if next.BaselineDigest == "" || existing.Digest == next.Digest {
7677
return false
7778
}
78-
return existing.BaselineDigest == "" || existing.BaselineDigest != next.BaselineDigest
79+
return (existing.BaselineModel == "" && next.BaselineModel != "") ||
80+
existing.BaselineDigest == "" || existing.BaselineDigest != next.BaselineDigest
7981
}
8082

8183
// Canonical returns a detached, deterministically ordered resolution and
@@ -86,6 +88,7 @@ func (r *DependencyResolution) Canonical() *DependencyResolution {
8688
}
8789
out := &DependencyResolution{
8890
InputDigest: r.InputDigest,
91+
BaselineModel: r.BaselineModel,
8992
BaselineDigest: r.BaselineDigest,
9093
Roots: append([]DependencyRoot(nil), r.Roots...),
9194
References: append([]DependencyRoot(nil), r.References...),
@@ -220,6 +223,7 @@ func constraintPermitsSelection(constraint, selected string) bool {
220223
func (r *DependencyResolution) computeDigest() string {
221224
payload := struct {
222225
InputDigest string `json:"input_digest"`
226+
BaselineModel string `json:"baseline_model,omitempty"`
223227
BaselineDigest string `json:"baseline_digest,omitempty"`
224228
Roots []DependencyRoot `json:"roots"`
225229
// omitempty keeps a reference-free digest byte-identical to prior
@@ -229,6 +233,7 @@ func (r *DependencyResolution) computeDigest() string {
229233
Modules []ResolvedModule `json:"modules"`
230234
}{
231235
InputDigest: r.InputDigest,
236+
BaselineModel: r.BaselineModel,
232237
BaselineDigest: r.BaselineDigest,
233238
Roots: r.Roots,
234239
References: r.References,

boot/build/stages/link_fixture_test.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import (
1010
"github.com/wippyai/runtime/api/registry"
1111
)
1212

13-
// Fixtures declare who owns an entry with a "module" meta key and with
14-
// Entry.DependencyRoot. linkFixtureProvenance moves those declarations into the
15-
// provenance the stage reads and removes them from the entries, so a fixture
16-
// can never make the stage read ownership from author payload.
17-
const fixtureModuleKey = "module"
13+
// Fixtures declare registry-owned state in metadata used only while arranging
14+
// a test. linkFixtureProvenance removes it before the stage sees the entries.
15+
const (
16+
fixtureModuleKey = "module"
17+
fixtureRootKey = "dependency_root"
18+
)
1819

1920
func fixtureEntryModule(entry registry.Entry) string {
2021
if entry.Meta == nil {
@@ -27,9 +28,12 @@ func fixtureEntryModule(entry registry.Entry) string {
2728
func linkFixtureProvenance(prov registry.ProvenanceMap, entries *[]registry.Entry) {
2829
for i := range *entries {
2930
entry := &(*entries)[i]
30-
record := registry.EntryProvenance{Module: fixtureEntryModule(*entry), Root: entry.DependencyRoot}
31-
entry.DependencyRoot = false
31+
record := registry.EntryProvenance{Module: fixtureEntryModule(*entry)}
32+
if entry.Meta != nil {
33+
record.Root, _ = entry.Meta[fixtureRootKey].(bool)
34+
}
3235
delete(entry.Meta, fixtureModuleKey)
36+
delete(entry.Meta, fixtureRootKey)
3337
prov[entry.ID] = record
3438
}
3539
}

boot/build/stages/linking_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,10 +2031,9 @@ func TestLink_RootParameterBeatsTransitiveBareAliasSpelling(t *testing.T) {
20312031

20322032
entries := []registry.Entry{
20332033
{
2034-
ID: registry.NewID("app.deps", "telegram_root"),
2035-
Kind: registry.NamespaceDependency,
2036-
Meta: map[string]any{"module": "acme/app"},
2037-
DependencyRoot: true,
2034+
ID: registry.NewID("app.deps", "telegram_root"),
2035+
Kind: registry.NamespaceDependency,
2036+
Meta: map[string]any{"module": "acme/app", fixtureRootKey: true},
20382037
Data: payload.New(map[string]any{
20392038
"component": "butschster/telegram",
20402039
"parameters": []any{

boot/deps/hub/dependency_boot_test.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ modules:
6767
baseline := func(selected string) regapi.ProvenancedState {
6868
root := regapi.Entry{
6969
ID: regapi.NewID("app.deps", "app"), Kind: regapi.NamespaceDependency,
70-
DependencyRoot: true,
71-
Data: payload.New(map[string]any{"component": "acme/app", "version": selected}),
70+
Meta: attrs.NewBagFrom(map[string]any{fixtureRootKey: true}),
71+
Data: payload.New(map[string]any{"component": "acme/app", "version": selected}),
7272
}
7373
service := regapi.Entry{
7474
ID: regapi.NewID("acme.app", "service"), Kind: "service",
@@ -330,11 +330,10 @@ func TestDependencyHandler_ApplyVersionReconciliationSemantics(t *testing.T) {
330330
reg, runner, baselineVersion, hostID := newLegacyArtifactRollbackRegistry(ctx, t)
331331

332332
addonRoot := regapi.Entry{
333-
ID: regapi.NewID("app.deps", "addon"),
334-
Kind: regapi.NamespaceDependency,
335-
DependencyRoot: true,
333+
ID: regapi.NewID("app.deps", "addon"),
334+
Kind: regapi.NamespaceDependency,
336335
Meta: attrs.NewBagFrom(map[string]any{
337-
fixtureModuleKey: "acme/deployment", fixtureModuleVersionKey: "v1.0.0",
336+
fixtureModuleKey: "acme/deployment", fixtureModuleVersionKey: "v1.0.0", fixtureRootKey: true,
338337
}),
339338
Data: payload.New(map[string]any{"component": "acme/addon", "version": "v1.0.0"}),
340339
}
@@ -508,9 +507,8 @@ modules:
508507
)
509508
root := regapi.Entry{
510509
ID: regapi.NewID("app.deps", "app"), Kind: regapi.NamespaceDependency,
511-
DependencyRoot: true,
512510
Meta: attrs.NewBagFrom(map[string]any{
513-
fixtureModuleKey: "acme/deployment", fixtureModuleVersionKey: "v1.0.0",
511+
fixtureModuleKey: "acme/deployment", fixtureModuleVersionKey: "v1.0.0", fixtureRootKey: true,
514512
}),
515513
Data: payload.New(map[string]any{"component": "acme/app", "version": "v1.0.0"}),
516514
}

boot/deps/hub/dependency_control.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ func isRootDependency(entry regapi.Entry, prov regapi.EntryProvenance) bool {
1515
return entry.Kind == regapi.NamespaceDependency && prov.Root
1616
}
1717

18-
// collectControlledModules returns the dependency graph reachable from the
19-
// state's deployment roots. A root's package owner is provenance, not part of
20-
// that graph: the root controls its declared component, while ordinary owned
21-
// dependencies extend the graph from owner to component.
18+
// collectControlledModules returns the deployment-lock cleanup boundary plus
19+
// components reachable through registry dependency declarations. A root's
20+
// package owner is provenance, not part of that graph: the root controls its
21+
// declared component, while ordinary owned dependencies extend the graph from
22+
// owner to component.
2223
func (h *DependencyHandler) collectControlledModules(
2324
ctx context.Context,
2425
snapshot regapi.ProvenancedState,

0 commit comments

Comments
 (0)