diff --git a/go/seats/containerimageregistry/contract.go b/go/seats/containerimageregistry/contract.go index ef6d3f2..ac06122 100644 --- a/go/seats/containerimageregistry/contract.go +++ b/go/seats/containerimageregistry/contract.go @@ -21,6 +21,7 @@ const ( EventPublished = "published" EventRefused = "refused" EventRemoved = "removed" + EventKept = "kept" EventHeld = "held" EventReleased = "released" ) @@ -32,7 +33,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved, EventHeld, EventReleased} + return []string{EventPublished, EventRefused, EventRemoved, EventKept, EventHeld, EventReleased} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -50,6 +51,9 @@ const SubjectRefused = "mesh.seat.container-image-registry.event.refused" // SubjectRemoved is where removed is published. const SubjectRemoved = "mesh.seat.container-image-registry.event.removed" +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.container-image-registry.event.kept" + // SubjectHeld is where held is published. const SubjectHeld = "mesh.seat.container-image-registry.event.held" @@ -115,12 +119,14 @@ type Outcome struct { Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` - // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) Reason string `json:"reason,omitempty"` // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) Reference string `json:"reference,omitempty"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` // the transfer it ends, when it ended one Transfer string `json:"transfer,omitempty"` // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone @@ -250,10 +256,21 @@ const ( // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` // it is gone Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` } +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + // MirrorInput is what mirror takes. type MirrorInput struct { // its pinned digest, sha256: @@ -337,6 +354,9 @@ type RefusedEvent = Outcome // RemovedEvent is the body of the event removed. type RemovedEvent = Outcome +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + // HeldEvent is the body of the event held. type HeldEvent = Hold diff --git a/go/seats/containerimageregistry/contract_test.go b/go/seats/containerimageregistry/contract_test.go index 6b5e2c0..3ad30c7 100644 --- a/go/seats/containerimageregistry/contract_test.go +++ b/go/seats/containerimageregistry/contract_test.go @@ -23,6 +23,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.refused$")}, {name: "removed on the seat's subject", subject: "mesh.seat.container-image-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.container-image-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.kept$")}, {name: "held on the seat's subject", subject: "mesh.seat.container-image-registry.event.held", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", decode: func(b []byte) (any, error) { var v HeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.held$")}, {name: "released on the seat's subject", subject: "mesh.seat.container-image-registry.event.released", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", @@ -39,6 +41,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a release of what is not held answers released false", subject: "", given: "{\"released\":false}", wire: "{\"released\":false}", decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/gopackageregistry/contract.go b/go/seats/gopackageregistry/contract.go index 54ea7a4..eb53461 100644 --- a/go/seats/gopackageregistry/contract.go +++ b/go/seats/gopackageregistry/contract.go @@ -17,6 +17,7 @@ const ( EventPublished = "published" EventRefused = "refused" EventRemoved = "removed" + EventKept = "kept" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -26,7 +27,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved} + return []string{EventPublished, EventRefused, EventRemoved, EventKept} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -44,6 +45,9 @@ const SubjectRefused = "mesh.seat.go-package-registry.event.refused" // SubjectRemoved is where removed is published. const SubjectRemoved = "mesh.seat.go-package-registry.event.removed" +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.go-package-registry.event.kept" + // Artifact is one published thing, as every registry event and answer names it. type Artifact struct { // the source commit it was built from @@ -87,12 +91,14 @@ type Outcome struct { Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` - // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) Reason string `json:"reason,omitempty"` // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) Reference string `json:"reference,omitempty"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` // the transfer it ends, when it ended one Transfer string `json:"transfer,omitempty"` // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone @@ -222,10 +228,21 @@ const ( // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` // it is gone Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` } +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + // PublishedEvent is the body of the event published. type PublishedEvent = Outcome @@ -235,6 +252,9 @@ type RefusedEvent = Outcome // RemovedEvent is the body of the event removed. type RemovedEvent = Outcome +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + // GoPackageRegistryForm is the form of the provision go-package-registry. type GoPackageRegistryForm struct { // the read side's address diff --git a/go/seats/gopackageregistry/contract_test.go b/go/seats/gopackageregistry/contract_test.go index 8f127f0..e5f69ec 100644 --- a/go/seats/gopackageregistry/contract_test.go +++ b/go/seats/gopackageregistry/contract_test.go @@ -23,6 +23,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.refused$")}, {name: "removed on the seat's subject", subject: "mesh.seat.go-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.go-package-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"shared-digest\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"shared-digest\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.kept$")}, {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", @@ -33,6 +35,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "the versions held", subject: "", given: "{\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"version\":\"0.1.19\"}]}", decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/npmpackageregistry/contract.go b/go/seats/npmpackageregistry/contract.go index 1237a9e..5f39cb8 100644 --- a/go/seats/npmpackageregistry/contract.go +++ b/go/seats/npmpackageregistry/contract.go @@ -17,6 +17,7 @@ const ( EventPublished = "published" EventRefused = "refused" EventRemoved = "removed" + EventKept = "kept" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -26,7 +27,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved} + return []string{EventPublished, EventRefused, EventRemoved, EventKept} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -44,6 +45,9 @@ const SubjectRefused = "mesh.seat.npm-package-registry.event.refused" // SubjectRemoved is where removed is published. const SubjectRemoved = "mesh.seat.npm-package-registry.event.removed" +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.npm-package-registry.event.kept" + // Artifact is one published thing, as every registry event and answer names it. type Artifact struct { // the source commit it was built from @@ -87,12 +91,14 @@ type Outcome struct { Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` - // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) Reason string `json:"reason,omitempty"` // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) Reference string `json:"reference,omitempty"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` // the transfer it ends, when it ended one Transfer string `json:"transfer,omitempty"` // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone @@ -222,10 +228,21 @@ const ( // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` // it is gone Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` } +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + // PublishedEvent is the body of the event published. type PublishedEvent = Outcome @@ -235,6 +252,9 @@ type RefusedEvent = Outcome // RemovedEvent is the body of the event removed. type RemovedEvent = Outcome +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + // NpmPackageRegistryForm is the form of the provision npm-package-registry. type NpmPackageRegistryForm struct { // the read side's address diff --git a/go/seats/npmpackageregistry/contract_test.go b/go/seats/npmpackageregistry/contract_test.go index ed57d7f..3092b37 100644 --- a/go/seats/npmpackageregistry/contract_test.go +++ b/go/seats/npmpackageregistry/contract_test.go @@ -23,6 +23,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.refused$")}, {name: "removed on the seat's subject", subject: "mesh.seat.npm-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.npm-package-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.kept$")}, {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", @@ -33,6 +35,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "the versions held", subject: "", given: "{\"name\":\"@novox/mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"@novox/mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.19\"}]}", decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/ociartifactregistry/contract.go b/go/seats/ociartifactregistry/contract.go index a02d700..39bba8b 100644 --- a/go/seats/ociartifactregistry/contract.go +++ b/go/seats/ociartifactregistry/contract.go @@ -19,6 +19,7 @@ const ( EventPublished = "published" EventRefused = "refused" EventRemoved = "removed" + EventKept = "kept" EventHeld = "held" EventReleased = "released" ) @@ -30,7 +31,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved, EventHeld, EventReleased} + return []string{EventPublished, EventRefused, EventRemoved, EventKept, EventHeld, EventReleased} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -48,6 +49,9 @@ const SubjectRefused = "mesh.seat.oci-artifact-registry.event.refused" // SubjectRemoved is where removed is published. const SubjectRemoved = "mesh.seat.oci-artifact-registry.event.removed" +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.oci-artifact-registry.event.kept" + // SubjectHeld is where held is published. const SubjectHeld = "mesh.seat.oci-artifact-registry.event.held" @@ -113,12 +117,14 @@ type Outcome struct { Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` - // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) Reason string `json:"reason,omitempty"` // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) Reference string `json:"reference,omitempty"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` // the transfer it ends, when it ended one Transfer string `json:"transfer,omitempty"` // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone @@ -248,10 +254,21 @@ const ( // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` // it is gone Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` } +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + // HoldInput is what hold takes. type HoldInput struct { // the digest @@ -295,6 +312,9 @@ type RefusedEvent = Outcome // RemovedEvent is the body of the event removed. type RemovedEvent = Outcome +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + // HeldEvent is the body of the event held. type HeldEvent = Hold diff --git a/go/seats/ociartifactregistry/contract_test.go b/go/seats/ociartifactregistry/contract_test.go index 8c8cdd6..9a2f3a2 100644 --- a/go/seats/ociartifactregistry/contract_test.go +++ b/go/seats/ociartifactregistry/contract_test.go @@ -23,6 +23,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.refused$")}, {name: "removed on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.oci-artifact-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.kept$")}, {name: "held on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.held", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", decode: func(b []byte) (any, error) { var v HeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.held$")}, {name: "released on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.released", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", @@ -39,6 +41,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a release of what is not held answers released false", subject: "", given: "{\"released\":false}", wire: "{\"released\":false}", decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/seats.go b/go/seats/seats.go index 3464d0d..125f4fe 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -381,7 +381,7 @@ func List() []Seat { {Name: "images", Description: "Every container image one machine's want and have name, each with the resources naming it: what the machine keeps when it prunes its images. Reads only.", Input: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the machine's name", "type": "string"}}, "required": []string{"node"}, "type": "object"}}, }}, - {Name: NpmPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0109", Delivers: "npm-package-registry", Emits: []string{"published", "refused", "removed"}, + {Name: NpmPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0109", Delivers: "npm-package-registry", Emits: []string{"published", "refused", "removed", "kept"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, @@ -392,11 +392,11 @@ func List() []Seat { {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, }}, - {Name: GoPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "go-package-registry", Emits: []string{"published", "refused", "removed"}, + {Name: GoPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "go-package-registry", Emits: []string{"published", "refused", "removed", "kept"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, @@ -407,11 +407,11 @@ func List() []Seat { {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, }}, - {Name: ContainerImageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "held", "released"}, + {Name: ContainerImageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "kept", "held", "released"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, @@ -422,9 +422,9 @@ func List() []Seat { {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, {Name: "mirror", Description: "Copy an outside image into the store by its pinned digest; emits published or refused.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "its pinned digest, sha256:", "type": "string"}, "name": map[string]any{"description": "the name it is kept under", "type": "string"}, "source": map[string]any{"description": "the outside image's reference", "type": "string"}}, "required": []string{"source", "digest", "name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest stored", "type": "string"}, "reference": map[string]any{"description": "the reference the seat answers", "type": "string"}}, "required": []string{"reference", "digest"}, "type": "object"}}, @@ -438,7 +438,7 @@ func List() []Seat { Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "a hold was let go; false when there was none", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, }}, - {Name: OciArtifactRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "held", "released"}, + {Name: OciArtifactRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "kept", "held", "released"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, @@ -449,9 +449,9 @@ func List() []Seat { {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "which of the sweep's rules keeps it", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "it is held", "type": "boolean"}, "since": map[string]any{"description": "when the hold began, RFC 3339", "type": "string"}}, "required": []string{"held"}, "type": "object"}}, diff --git a/parts/registry.json b/parts/registry.json index af1249c..ca0022f 100644 --- a/parts/registry.json +++ b/parts/registry.json @@ -196,7 +196,7 @@ }, { "name": "remove", - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", "callers": "the builder, for a tagged version; the artifact sweep's holder, for retention", "input": { "type": "object", @@ -238,6 +238,24 @@ "type": "boolean", "description": "it is gone" }, + "kept": { + "type": "boolean", + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)" + }, + "reason": { + "type": "string", + "enum": [ + "shared-digest" + ], + "description": "why it was kept: shared-digest, another tag names its digest" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the other tags still naming its digest, when kept" + }, "artifact": { "$ref": "#/defs/Artifact" } @@ -267,6 +285,12 @@ "body": { "$ref": "#/defs/Outcome" } + }, + { + "name": "kept", + "body": { + "$ref": "#/defs/Outcome" + } } ], "defs": { @@ -390,7 +414,14 @@ }, "reason": { "type": "string", - "description": "why: a refusal's cause, or a removal's (retention, tag-gone, by-hand)" + "description": "why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest)" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "for kept: the other tags still naming its digest" } }, "required": [ diff --git a/seats/container-image-registry/fixtures/events.json b/seats/container-image-registry/fixtures/events.json index 8bd60c9..65e98e5 100644 --- a/seats/container-image-registry/fixtures/events.json +++ b/seats/container-image-registry/fixtures/events.json @@ -49,6 +49,26 @@ "reason": "retention" } }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.container-image-registry.event.kept", + "given": { + "name": "mesh-sdk", + "version": "0.1.19-feat-x.2", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } + }, { "name": "held on the seat's subject", "of": "event:held", diff --git a/seats/container-image-registry/fixtures/verbs.json b/seats/container-image-registry/fixtures/verbs.json index 2d94ee7..6e50d36 100644 --- a/seats/container-image-registry/fixtures/verbs.json +++ b/seats/container-image-registry/fixtures/verbs.json @@ -101,5 +101,18 @@ "given": { "released": false } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/seats/go-package-registry/fixtures/events.json b/seats/go-package-registry/fixtures/events.json index 37579ae..c3a8b16 100644 --- a/seats/go-package-registry/fixtures/events.json +++ b/seats/go-package-registry/fixtures/events.json @@ -49,5 +49,25 @@ "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", "reason": "retention" } + }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.go-package-registry.event.kept", + "given": { + "name": "git.novox.be/novox/mesh-sdk/go", + "version": "0.1.19-feat-x.2", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/seats/go-package-registry/fixtures/verbs.json b/seats/go-package-registry/fixtures/verbs.json index 716fe9f..be6a53a 100644 --- a/seats/go-package-registry/fixtures/verbs.json +++ b/seats/go-package-registry/fixtures/verbs.json @@ -93,5 +93,18 @@ } ] } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/seats/npm-package-registry/fixtures/events.json b/seats/npm-package-registry/fixtures/events.json index 0fe1c76..4cadc30 100644 --- a/seats/npm-package-registry/fixtures/events.json +++ b/seats/npm-package-registry/fixtures/events.json @@ -48,5 +48,25 @@ "reference": "@novox/mesh-sdk@0.1.19", "reason": "retention" } + }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.npm-package-registry.event.kept", + "given": { + "name": "@novox/mesh-sdk", + "version": "0.1.19-feat-x.2", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "reference": "@novox/mesh-sdk@0.1.19", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/seats/npm-package-registry/fixtures/verbs.json b/seats/npm-package-registry/fixtures/verbs.json index 529d486..0b88f08 100644 --- a/seats/npm-package-registry/fixtures/verbs.json +++ b/seats/npm-package-registry/fixtures/verbs.json @@ -93,5 +93,18 @@ } ] } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/seats/oci-artifact-registry/fixtures/events.json b/seats/oci-artifact-registry/fixtures/events.json index 0928933..e0a8cde 100644 --- a/seats/oci-artifact-registry/fixtures/events.json +++ b/seats/oci-artifact-registry/fixtures/events.json @@ -49,6 +49,26 @@ "reason": "retention" } }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.oci-artifact-registry.event.kept", + "given": { + "name": "mesh-sdk", + "version": "0.1.19-feat-x.2", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } + }, { "name": "held on the seat's subject", "of": "event:held", diff --git a/seats/oci-artifact-registry/fixtures/verbs.json b/seats/oci-artifact-registry/fixtures/verbs.json index b571a88..e0121b7 100644 --- a/seats/oci-artifact-registry/fixtures/verbs.json +++ b/seats/oci-artifact-registry/fixtures/verbs.json @@ -101,5 +101,18 @@ "given": { "released": false } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/ts/seats/containerimageregistry/src/index.ts b/ts/seats/containerimageregistry/src/index.ts index b20e37e..9baf704 100644 --- a/ts/seats/containerimageregistry/src/index.ts +++ b/ts/seats/containerimageregistry/src/index.ts @@ -21,6 +21,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", held: "held", released: "released", } as const; @@ -33,11 +34,14 @@ export const refusedSubject = "mesh.seat.container-image-registry.event.refused" export const removedSubject = "mesh.seat.container-image-registry.event.removed"; +export const keptSubject = "mesh.seat.container-image-registry.event.kept"; + export const heldSubject = "mesh.seat.container-image-registry.event.held"; export const releasedSubject = "mesh.seat.container-image-registry.event.released"; export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; export type Outcome = registry.Outcome; export type Part = registry.Part; export type PublishAnswer = registry.PublishAnswer; diff --git a/ts/seats/containerimageregistry/src/parts/registry.ts b/ts/seats/containerimageregistry/src/parts/registry.ts index 544dc55..17a8de4 100644 --- a/ts/seats/containerimageregistry/src/parts/registry.ts +++ b/ts/seats/containerimageregistry/src/parts/registry.ts @@ -13,6 +13,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; /** one published thing, as every registry event and answer names it */ @@ -49,12 +50,14 @@ export interface Outcome { "module"?: string; /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; - /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ "reason"?: string; /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ "reference"?: string; /** the tag it carries, for a tagged version */ "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; /** the transfer it ends, when it ended one */ "transfer"?: string; /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ @@ -160,8 +163,14 @@ export interface RemoveInput { /** is what remove answers. */ export interface RemoveAnswer { "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; /** it is gone */ "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; } /** is the body of the event published. */ @@ -173,3 +182,6 @@ export type RefusedEvent = Outcome; /** is the body of the event removed. */ export type RemovedEvent = Outcome; +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/ts/seats/gopackageregistry/src/index.ts b/ts/seats/gopackageregistry/src/index.ts index 834ecb3..f072090 100644 --- a/ts/seats/gopackageregistry/src/index.ts +++ b/ts/seats/gopackageregistry/src/index.ts @@ -17,6 +17,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; export const family = { name: "go-package-registry.events", maxAgeDays: 7 } as const; @@ -27,7 +28,10 @@ export const refusedSubject = "mesh.seat.go-package-registry.event.refused"; export const removedSubject = "mesh.seat.go-package-registry.event.removed"; +export const keptSubject = "mesh.seat.go-package-registry.event.kept"; + export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; export type Outcome = registry.Outcome; export type Part = registry.Part; export type PublishAnswer = registry.PublishAnswer; diff --git a/ts/seats/gopackageregistry/src/parts/registry.ts b/ts/seats/gopackageregistry/src/parts/registry.ts index 544dc55..17a8de4 100644 --- a/ts/seats/gopackageregistry/src/parts/registry.ts +++ b/ts/seats/gopackageregistry/src/parts/registry.ts @@ -13,6 +13,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; /** one published thing, as every registry event and answer names it */ @@ -49,12 +50,14 @@ export interface Outcome { "module"?: string; /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; - /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ "reason"?: string; /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ "reference"?: string; /** the tag it carries, for a tagged version */ "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; /** the transfer it ends, when it ended one */ "transfer"?: string; /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ @@ -160,8 +163,14 @@ export interface RemoveInput { /** is what remove answers. */ export interface RemoveAnswer { "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; /** it is gone */ "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; } /** is the body of the event published. */ @@ -173,3 +182,6 @@ export type RefusedEvent = Outcome; /** is the body of the event removed. */ export type RemovedEvent = Outcome; +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/ts/seats/npmpackageregistry/src/index.ts b/ts/seats/npmpackageregistry/src/index.ts index 0834e45..a704e8b 100644 --- a/ts/seats/npmpackageregistry/src/index.ts +++ b/ts/seats/npmpackageregistry/src/index.ts @@ -17,6 +17,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; export const family = { name: "npm-package-registry.events", maxAgeDays: 7 } as const; @@ -27,7 +28,10 @@ export const refusedSubject = "mesh.seat.npm-package-registry.event.refused"; export const removedSubject = "mesh.seat.npm-package-registry.event.removed"; +export const keptSubject = "mesh.seat.npm-package-registry.event.kept"; + export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; export type Outcome = registry.Outcome; export type Part = registry.Part; export type PublishAnswer = registry.PublishAnswer; diff --git a/ts/seats/npmpackageregistry/src/parts/registry.ts b/ts/seats/npmpackageregistry/src/parts/registry.ts index 544dc55..17a8de4 100644 --- a/ts/seats/npmpackageregistry/src/parts/registry.ts +++ b/ts/seats/npmpackageregistry/src/parts/registry.ts @@ -13,6 +13,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; /** one published thing, as every registry event and answer names it */ @@ -49,12 +50,14 @@ export interface Outcome { "module"?: string; /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; - /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ "reason"?: string; /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ "reference"?: string; /** the tag it carries, for a tagged version */ "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; /** the transfer it ends, when it ended one */ "transfer"?: string; /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ @@ -160,8 +163,14 @@ export interface RemoveInput { /** is what remove answers. */ export interface RemoveAnswer { "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; /** it is gone */ "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; } /** is the body of the event published. */ @@ -173,3 +182,6 @@ export type RefusedEvent = Outcome; /** is the body of the event removed. */ export type RemovedEvent = Outcome; +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/ts/seats/ociartifactregistry/src/index.ts b/ts/seats/ociartifactregistry/src/index.ts index 1467090..c1fa55e 100644 --- a/ts/seats/ociartifactregistry/src/index.ts +++ b/ts/seats/ociartifactregistry/src/index.ts @@ -19,6 +19,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", held: "held", released: "released", } as const; @@ -31,11 +32,14 @@ export const refusedSubject = "mesh.seat.oci-artifact-registry.event.refused"; export const removedSubject = "mesh.seat.oci-artifact-registry.event.removed"; +export const keptSubject = "mesh.seat.oci-artifact-registry.event.kept"; + export const heldSubject = "mesh.seat.oci-artifact-registry.event.held"; export const releasedSubject = "mesh.seat.oci-artifact-registry.event.released"; export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; export type Outcome = registry.Outcome; export type Part = registry.Part; export type PublishAnswer = registry.PublishAnswer; diff --git a/ts/seats/ociartifactregistry/src/parts/registry.ts b/ts/seats/ociartifactregistry/src/parts/registry.ts index 544dc55..17a8de4 100644 --- a/ts/seats/ociartifactregistry/src/parts/registry.ts +++ b/ts/seats/ociartifactregistry/src/parts/registry.ts @@ -13,6 +13,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; /** one published thing, as every registry event and answer names it */ @@ -49,12 +50,14 @@ export interface Outcome { "module"?: string; /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; - /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ "reason"?: string; /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ "reference"?: string; /** the tag it carries, for a tagged version */ "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; /** the transfer it ends, when it ended one */ "transfer"?: string; /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ @@ -160,8 +163,14 @@ export interface RemoveInput { /** is what remove answers. */ export interface RemoveAnswer { "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; /** it is gone */ "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; } /** is the body of the event published. */ @@ -173,3 +182,6 @@ export type RefusedEvent = Outcome; /** is the body of the event removed. */ export type RemovedEvent = Outcome; +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/ts/src/seats.ts b/ts/src/seats.ts index c4f7292..efe9c39 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -3063,7 +3063,8 @@ export const list: readonly Seat[] = [ "emits": [ "published", "refused", - "removed" + "removed", + "kept" ], "grain": "whole", "name": "npm-package-registry", @@ -3316,7 +3317,7 @@ export const list: readonly Seat[] = [ } }, { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", "input": { "properties": { "digest": { @@ -3400,9 +3401,27 @@ export const list: readonly Seat[] = [ ], "type": "object" }, + "kept": { + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", + "type": "boolean" + }, + "reason": { + "description": "why it was kept: shared-digest, another tag names its digest", + "enum": [ + "shared-digest" + ], + "type": "string" + }, "removed": { "description": "it is gone", "type": "boolean" + }, + "tags": { + "description": "the other tags still naming its digest, when kept", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ @@ -3419,7 +3438,8 @@ export const list: readonly Seat[] = [ "emits": [ "published", "refused", - "removed" + "removed", + "kept" ], "grain": "whole", "name": "go-package-registry", @@ -3672,7 +3692,7 @@ export const list: readonly Seat[] = [ } }, { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", "input": { "properties": { "digest": { @@ -3756,9 +3776,27 @@ export const list: readonly Seat[] = [ ], "type": "object" }, + "kept": { + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", + "type": "boolean" + }, + "reason": { + "description": "why it was kept: shared-digest, another tag names its digest", + "enum": [ + "shared-digest" + ], + "type": "string" + }, "removed": { "description": "it is gone", "type": "boolean" + }, + "tags": { + "description": "the other tags still naming its digest, when kept", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ @@ -3776,6 +3814,7 @@ export const list: readonly Seat[] = [ "published", "refused", "removed", + "kept", "held", "released" ], @@ -4030,7 +4069,7 @@ export const list: readonly Seat[] = [ } }, { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", "input": { "properties": { "digest": { @@ -4114,9 +4153,27 @@ export const list: readonly Seat[] = [ ], "type": "object" }, + "kept": { + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", + "type": "boolean" + }, + "reason": { + "description": "why it was kept: shared-digest, another tag names its digest", + "enum": [ + "shared-digest" + ], + "type": "string" + }, "removed": { "description": "it is gone", "type": "boolean" + }, + "tags": { + "description": "the other tags still naming its digest, when kept", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ @@ -4311,6 +4368,7 @@ export const list: readonly Seat[] = [ "published", "refused", "removed", + "kept", "held", "released" ], @@ -4565,7 +4623,7 @@ export const list: readonly Seat[] = [ } }, { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", "input": { "properties": { "digest": { @@ -4649,9 +4707,27 @@ export const list: readonly Seat[] = [ ], "type": "object" }, + "kept": { + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", + "type": "boolean" + }, + "reason": { + "description": "why it was kept: shared-digest, another tag names its digest", + "enum": [ + "shared-digest" + ], + "type": "string" + }, "removed": { "description": "it is gone", "type": "boolean" + }, + "tags": { + "description": "the other tags still naming its digest, when kept", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [