diff --git a/contracts/go/seats/meshartifactsweep/contract.go b/contracts/go/seats/meshartifactsweep/contract.go new file mode 100644 index 00000000..8aa87d21 --- /dev/null +++ b/contracts/go/seats/meshartifactsweep/contract.go @@ -0,0 +1,129 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshartifactsweep is the contract of the seat "mesh-artifact-sweep", defined whole (seats/mesh-artifact-sweep/seat.json): no description. +package meshartifactsweep + +import "time" + +// Seat is the seat's name. +const Seat = "mesh-artifact-sweep" + +const ( + VerbArtifacts = "artifacts" + VerbHeld = "held" + VerbCollect = "collect" + VerbMirrors = "mirrors" + VerbImages = "images" +) + +const ( + EventArtifactsGone = "artifacts-gone" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbArtifacts, VerbHeld, VerbCollect, VerbMirrors, VerbImages} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventArtifactsGone} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-artifact-sweep.events" + FamilyMaxAgeDays = 7 +) + +// SubjectArtifactsGone is where artifacts-gone is published. +const SubjectArtifactsGone = "mesh.seat.mesh-artifact-sweep.event.artifacts-gone" + +// ArtifactsGone is the body of artifacts-gone. +type ArtifactsGone struct { + // when the sweep found it + At *time.Time `json:"at,omitempty"` + // artifacts-gone + Event string `json:"event"` + // the key's fingerprint + Fingerprint string `json:"fingerprint"` + // every reference of the key the store does not hold, sorted + Gone []string `json:"gone"` + // the builder's done key + Key string `json:"key,omitempty"` + // the module whose builder's key names a gone artifact + Module string `json:"module"` +} + +// ArtifactsInput is what artifacts takes. +type ArtifactsInput struct { + // "true": list the collected references too (counted always) + Collected string `json:"collected,omitempty"` + // only this repository's references, as / (the counts stay whole) + Repository string `json:"repository,omitempty"` +} + +// The values of ArtifactsInput's collected. +const ( + ArtifactsInputCollectedTrue = "true" + ArtifactsInputCollectedFalse = "false" +) + +// ArtifactsAnswer is what artifacts answers. +type ArtifactsAnswer = map[string]any + +// HeldInput is what held takes. +type HeldInput struct { + // the artifacts' references, artifact-store:////blobs/sha256:: a list, or one text separated by commas + References []string `json:"references"` +} + +// HeldAnswer is what held answers. +type HeldAnswer struct { + // the references the store answered 404 for + Gone []string `json:"gone"` + // the references the store holds + Held []string `json:"held"` + // the store asked, host and port + Store string `json:"store,omitempty"` + // a reference the store could not be asked about, to why + Unknown map[string]string `json:"unknown,omitempty"` +} + +// CollectInput is what collect takes. +type CollectInput struct { + // "true": let go of what is eligible (needs why); without it nothing is done + Confirm string `json:"confirm,omitempty"` + // let go of at most this many (500 by default, at most 5000) + Most string `json:"most,omitempty"` + // with confirm: why, required and recorded + Why string `json:"why,omitempty"` +} + +// The values of CollectInput's confirm. +const ( + CollectInputConfirmTrue = "true" + CollectInputConfirmFalse = "false" +) + +// CollectAnswer is what collect answers. +type CollectAnswer = map[string]any + +// MirrorsInput is what mirrors takes. +type MirrorsInput struct { +} + +// MirrorsAnswer is what mirrors answers. +type MirrorsAnswer = map[string]any + +// ImagesInput is what images takes. +type ImagesInput struct { + // the machine's name + Node string `json:"node"` +} + +// ImagesAnswer is what images answers. +type ImagesAnswer = map[string]any + +// ArtifactsGoneEvent is the body of the event artifacts-gone. +type ArtifactsGoneEvent = ArtifactsGone diff --git a/contracts/go/seats/meshartifactsweep/contract_test.go b/contracts/go/seats/meshartifactsweep/contract_test.go new file mode 100644 index 00000000..ea7a5f1f --- /dev/null +++ b/contracts/go/seats/meshartifactsweep/contract_test.go @@ -0,0 +1,54 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshartifactsweep + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-artifact-sweep/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 ยง6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "artifacts-gone on the seat's subject", subject: "mesh.seat.mesh-artifact-sweep.event.artifacts-gone", given: "{\"at\":\"2026-11-05T15:04:05Z\",\"event\":\"artifacts-gone\",\"fingerprint\":\"f1\",\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"key\":\"ca-trust.archive.f1\",\"module\":\"ca-trust\"}", wire: "{\"at\":\"2026-11-05T15:04:05Z\",\"event\":\"artifacts-gone\",\"fingerprint\":\"f1\",\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"key\":\"ca-trust.archive.f1\",\"module\":\"ca-trust\"}", + decode: func(b []byte) (any, error) { var v ArtifactsGoneEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-artifact-sweep\\.event\\.artifacts-gone$")}, + {name: "held asks the references", subject: "", given: "{\"references\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"]}", wire: "{\"references\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"]}", + decode: func(b []byte) (any, error) { var v HeldInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "held answers held, gone and unknown", subject: "", given: "{\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"held\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"],\"store\":\"store.test:5100\",\"unknown\":{\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\":\"the store answered 503\"}}", wire: "{\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"held\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"],\"store\":\"store.test:5100\",\"unknown\":{\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\":\"the store answered 503\"}}", + decode: func(b []byte) (any, error) { var v HeldAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "held answers with nothing unknown", subject: "", given: "{\"gone\":[],\"held\":[],\"store\":\"store.test:5100\"}", wire: "{\"gone\":[],\"held\":[],\"store\":\"store.test:5100\"}", + decode: func(b []byte) (any, error) { var v HeldAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/meshartifactsweep/go.mod b/contracts/go/seats/meshartifactsweep/go.mod new file mode 100644 index 00000000..77f0649b --- /dev/null +++ b/contracts/go/seats/meshartifactsweep/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep + +go 1.26.0 diff --git a/contracts/module.json b/contracts/module.json index d80c383c..09be597e 100644 --- a/contracts/module.json +++ b/contracts/module.json @@ -23,6 +23,16 @@ "series": "0.1", "generated-from": "." }, + { + "name": "go-meshartifactsweep", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshartifactsweep" + ], + "series": "0.1", + "generated-from": "." + }, { "name": "go-meshcatalog", "kind": "package", @@ -186,6 +196,16 @@ "series": "0.1", "generated-from": "." }, + { + "name": "ts-meshartifactsweep", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshartifactsweep" + ], + "series": "0.1", + "generated-from": "." + }, { "name": "ts-meshcatalog", "kind": "package", diff --git a/contracts/seats/mesh-artifact-sweep/fixtures/events.json b/contracts/seats/mesh-artifact-sweep/fixtures/events.json new file mode 100644 index 00000000..3e39336a --- /dev/null +++ b/contracts/seats/mesh-artifact-sweep/fixtures/events.json @@ -0,0 +1,15 @@ +[ + { + "name": "artifacts-gone on the seat's subject", + "of": "event:artifacts-gone", + "subject": "mesh.seat.mesh-artifact-sweep.event.artifacts-gone", + "given": { + "event": "artifacts-gone", + "module": "ca-trust", + "key": "ca-trust.archive.f1", + "fingerprint": "f1", + "gone": ["artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"], + "at": "2026-11-05T15:04:05Z" + } + } +] diff --git a/contracts/seats/mesh-artifact-sweep/fixtures/states.json b/contracts/seats/mesh-artifact-sweep/fixtures/states.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/contracts/seats/mesh-artifact-sweep/fixtures/states.json @@ -0,0 +1 @@ +[] diff --git a/contracts/seats/mesh-artifact-sweep/fixtures/verbs.json b/contracts/seats/mesh-artifact-sweep/fixtures/verbs.json new file mode 100644 index 00000000..b70970ee --- /dev/null +++ b/contracts/seats/mesh-artifact-sweep/fixtures/verbs.json @@ -0,0 +1,24 @@ +[ + { + "name": "held asks the references", + "of": "verb:held.input", + "why": "the references are the holder's own test values (the tool refuses a call without them through the console)", + "given": {"references": ["artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"]} + }, + { + "name": "held answers held, gone and unknown", + "of": "verb:held.answer", + "why": "the answer of the holder's test (cmd/artifact-store-tools): every reference in one of the three", + "given": { + "store": "store.test:5100", + "held": ["artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], + "gone": ["artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"], + "unknown": {"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc": "the store answered 503"} + } + }, + { + "name": "held answers with nothing unknown", + "of": "verb:held.answer", + "given": {"store": "store.test:5100", "held": [], "gone": []} + } +] diff --git a/contracts/seats/mesh-artifact-sweep/seat.json b/contracts/seats/mesh-artifact-sweep/seat.json index 2e5b673d..4763ce8d 100644 --- a/contracts/seats/mesh-artifact-sweep/seat.json +++ b/contracts/seats/mesh-artifact-sweep/seat.json @@ -2,7 +2,7 @@ "format": 1, "seat": "mesh-artifact-sweep", "scope": "mesh", - "grain": "seat-list", + "grain": "whole", "domain": "Delivery", "decision": "novox/hq ADR 0571", "verbs": [ @@ -25,6 +25,10 @@ } }, "type": "object" + }, + "answer": { + "type": "object", + "description": "what artifacts answers, as its description says" } }, { @@ -44,6 +48,41 @@ "references" ], "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "store": { + "type": "string", + "description": "the store asked, host and port" + }, + "held": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the references the store holds" + }, + "gone": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the references the store answered 404 for" + }, + "unknown": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "a reference the store could not be asked about, to why" + } + }, + "required": [ + "held", + "gone" + ], + "description": "what held answers: a reference is in held, gone or unknown, never none of them" } }, { @@ -69,6 +108,10 @@ } }, "type": "object" + }, + "answer": { + "type": "object", + "description": "what collect answers, as its description says" } }, { @@ -77,6 +120,10 @@ "input": { "properties": {}, "type": "object" + }, + "answer": { + "type": "object", + "description": "what mirrors answers, as its description says" } }, { @@ -93,12 +140,65 @@ "node" ], "type": "object" + }, + "answer": { + "type": "object", + "description": "what images answers, as its description says" } } ], + "family": { + "name": "mesh-artifact-sweep.events", + "max-age-days": 7 + }, "events": [ { - "name": "artifacts-gone" + "name": "artifacts-gone", + "body": { + "$ref": "#/defs/ArtifactsGone" + } } - ] + ], + "defs": { + "ArtifactsGone": { + "type": "object", + "properties": { + "event": { + "type": "string", + "description": "artifacts-gone" + }, + "module": { + "type": "string", + "description": "the module whose builder's key names a gone artifact" + }, + "key": { + "type": "string", + "description": "the builder's done key" + }, + "fingerprint": { + "type": "string", + "description": "the key's fingerprint" + }, + "gone": { + "type": "array", + "items": { + "type": "string" + }, + "description": "every reference of the key the store does not hold, sorted" + }, + "at": { + "type": "string", + "format": "date-time", + "description": "when the sweep found it" + } + }, + "required": [ + "event", + "module", + "fingerprint", + "gone" + ], + "description": "the body of artifacts-gone" + } + } } diff --git a/contracts/ts/seats/meshartifactsweep/package-lock.json b/contracts/ts/seats/meshartifactsweep/package-lock.json new file mode 100644 index 00000000..2d3064d4 --- /dev/null +++ b/contracts/ts/seats/meshartifactsweep/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-mesh-artifact-sweep", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-mesh-artifact-sweep", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/meshartifactsweep/package.json b/contracts/ts/seats/meshartifactsweep/package.json new file mode 100644 index 00000000..14c46319 --- /dev/null +++ b/contracts/ts/seats/meshartifactsweep/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-artifact-sweep, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-mesh-artifact-sweep", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/meshartifactsweep/src/index.ts b/contracts/ts/seats/meshartifactsweep/src/index.ts new file mode 100644 index 00000000..ad4d48a4 --- /dev/null +++ b/contracts/ts/seats/meshartifactsweep/src/index.ts @@ -0,0 +1,99 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-artifact-sweep", defined whole (seats/mesh-artifact-sweep/seat.json): no description. + +export const seat = "mesh-artifact-sweep"; + +export const verbs = { + artifacts: "artifacts", + held: "held", + collect: "collect", + mirrors: "mirrors", + images: "images", +} as const; + +export const events = { + artifactsGone: "artifacts-gone", +} as const; + +export const family = { name: "mesh-artifact-sweep.events", maxAgeDays: 7 } as const; + +export const artifactsGoneSubject = "mesh.seat.mesh-artifact-sweep.event.artifacts-gone"; + +/** the body of artifacts-gone */ +export interface ArtifactsGone { + /** when the sweep found it */ + "at"?: string; + /** artifacts-gone */ + "event": string; + /** the key's fingerprint */ + "fingerprint": string; + /** every reference of the key the store does not hold, sorted */ + "gone": string[]; + /** the builder's done key */ + "key"?: string; + /** the module whose builder's key names a gone artifact */ + "module": string; +} + +/** is what artifacts takes. */ +export interface ArtifactsInput { + /** "true": list the collected references too (counted always) */ + "collected"?: "true" | "false"; + /** only this repository's references, as / (the counts stay whole) */ + "repository"?: string; +} + +/** is what artifacts answers. */ +export type ArtifactsAnswer = Record; + +/** is what held takes. */ +export interface HeldInput { + /** the artifacts' references, artifact-store:////blobs/sha256:: a list, or one text separated by commas */ + "references": string[]; +} + +/** is what held answers. */ +export interface HeldAnswer { + /** the references the store answered 404 for */ + "gone": string[]; + /** the references the store holds */ + "held": string[]; + /** the store asked, host and port */ + "store"?: string; + /** a reference the store could not be asked about, to why */ + "unknown"?: Record; +} + +/** is what collect takes. */ +export interface CollectInput { + /** "true": let go of what is eligible (needs why); without it nothing is done */ + "confirm"?: "true" | "false"; + /** let go of at most this many (500 by default, at most 5000) */ + "most"?: string; + /** with confirm: why, required and recorded */ + "why"?: string; +} + +/** is what collect answers. */ +export type CollectAnswer = Record; + +/** is what mirrors takes. */ +export interface MirrorsInput { +} + +/** is what mirrors answers. */ +export type MirrorsAnswer = Record; + +/** is what images takes. */ +export interface ImagesInput { + /** the machine's name */ + "node": string; +} + +/** is what images answers. */ +export type ImagesAnswer = Record; + +/** is the body of the event artifacts-gone. */ +export type ArtifactsGoneEvent = ArtifactsGone; + diff --git a/contracts/ts/seats/meshartifactsweep/tsconfig.json b/contracts/ts/seats/meshartifactsweep/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/meshartifactsweep/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +}