Compare commits

...
Author SHA1 Message Date
jschoubben 432df75997 gofmt 2026-11-05 20:50:19 +01:00
jschoubben e98f6f30b1 The checker's event layouts are its own published package, the try group key and wording are the mesh-catalog client's; one limit for try (ADR 0634 sections 1 and 2, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 20:50:14 +01:00
jschoubben 03809d8d83 Merge origin/main into b7/registry: the group try and the successor validation take the group's contracts, clients and seat list (issue 1966, issue 1921)
The seat verb try gains members (ADR 0637 section 6); the try's group name and the missing-preview reason are one definition of mesh-delivery-sdk (ADR 0637 section 8).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 20:41:41 +01:00
jschoubben b638303104 No client names the holder of the seat mesh-provisioner: each reader keeps its own constant, held to its manifest (ADR 0625 section 4, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 19:36:21 +01:00
jschoubben 535f475d6e The assigner's texts say its seat mesh-module-assigner (ADR 0634 section 9, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 19:24:07 +01:00
jschoubben a8e78eae19 The node-engine's account hears what its manifest consumes: the view's settled is a consumes line, not a special case in the grants part (ADR 0625 section 4, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 19:23:39 +01:00
jschoubben f6f18d04e1 The delivery SDK's enginereads, findings and heard leave for the clients of their seats (ADR 0634 sections 1 and 2, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 19:22:05 +01:00
jschoubben ddaff38e75 RemovedDeliveryVerbs leaves mesh-sdk for the seat mesh-delivery's client (ADR 0634 section 2, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:45:50 +01:00
jschoubben f0895a212e The wanted accounts' layout leaves mesh-sdk for the grants part's package grantsstate (ADR 0634 section 1, ADR 0635 section 4, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:43:21 +01:00
jschoubben c8241d0be6 No client names a holder: each reader keeps its own constant, held to its manifest (ADR 0625 section 4, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:37:28 +01:00
jschoubben 2dbb0da8a5 Require every mesh package of the group at the floor version (ADR 0573 sections 2 and 4, issue 1966)
A member's go.mod names the oldest version a module accepts; the check builds the group's own heads whatever it names. go.sum lines of mesh packages are left out.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:56:39 +00:00
jschoubben 4f647cbc80 Merge branch 'b9i/registry' into b7/registry 2026-11-05 17:39:18 +01:00
jschoubben 8f6c90adff The reference forms move beside the build key, in builds/forms, and read the registry seat's form through the registry clients' reference package; the builder names the sweep seat from its contract (ADR 0634 §4, S-8; issue 1966)
sdk/forms could not import the reference package: the registry client's module requires the delivery sdk (parts).
builds already requires the client and both seats, and both users of forms (builder, assigner) require builds.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:34:56 +01:00
jschoubben 3e7281cb14 assigner: the seat mesh-module-assigner's four events are said on the seat's subject, not on a stream of the module's own; its placement stream and the judge's exception for it are gone (V1; ruling 18:4x, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:32:54 +01:00
jschoubben 21110940b0 checker: its asks stream keeps its history, the bus's predefined form of an event family (V1; ADR 0592 §6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:32:54 +01:00
jschoubben 947d0b82eb mesh-delivery-sdk: its package artifact leaves paths out for the module's whole directory, as the manifest validator requires (V1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:32:54 +01:00
jschoubben 436347d712 The builder takes the Delivery SDK's buildstep for ids and fetch requests and the conditions client for its findings and the cleared condition (ADR 0635 §4, issue 1966)
mesh-sdk go/buildstep and go/health's condition and finding names are no longer imported; the seat's condition-cleared is heard under the seat events pattern already held.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:15:36 +01:00
jschoubben dba15eb4b1 The judge's rule tests are grouped by kind into subtests, each by its former name: unit count 900 to 671 (D-26, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:11:58 +01:00
jschoubben e085c0ce22 mesh-catalog seat regenerated with the nullable bounds: a config declaration's minimum and maximum are *float64, so a bound of 0 is kept (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:11:58 +01:00
jschoubben c9279b0795 Merge b7l/registry into the registry milestone's mesh-delivery: the builder and the checker onto the Delivery SDK (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:03:28 +01:00
jschoubben 2d987c5632 Merge b7q/registry into the registry milestone's mesh-delivery: provisions, overlay and findings onto their clients (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:02:08 +01:00
jschoubben 84e617d3a9 Checker: off mesh-sdk go/delivery onto the seats' types and the clients (verdict, kept, preview, owed, candidates, modules, manifests, packages, source, turns, haves), its holders and buckets its own constants checked against its manifest (D-11), the check ask's fetch typed with the group package and the check answer's fetch read as the Delivery SDK's FetchAnswer, a merge with no program told by merge (B2g; ADR 0634 §1, §2, §6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:00:25 +01:00
jschoubben ffafa90ec5 mesh-grants: the unused aliases of membership's CredentialIn and BareAddress go (ADR 0635 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:00:13 +01:00
jschoubben 8f1ffcd44d The grants part publishes busgrants (the removals' grants, the ended controller's leftovers, what the watchman hears); the watchman takes them and the self-check's checked from the seat, provisions from meshprovisionerclient (ADR 0634 §4, ADR 0635 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:00:01 +01:00
jschoubben 5c73853653 mesh-grants: no go/delivery, health or provisions import left: the user list and grants are its own grantsstate, the other parts' states are named in its buscompose names.go (checked against its manifest), their values are the seats' types (ADR 0634 §1, §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:54:28 +01:00
jschoubben 444fa48fbf watchman: its findings are written and read in the seat mesh-conditions' layout through meshconditionsclient (ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:49:27 +01:00
jschoubben 7c61a7ad37 indexer: its findings are written and read in the seat mesh-conditions' layout through meshconditionsclient (ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:48:57 +01:00
jschoubben 6b32e8257d Fetch part: the TypeScript module's group package ({name, version} or {repository, commit, ref}), which the checker hands the Node fetch and the part lacked; typed in the three seats and the sdk, a node check ask fixture (B2g gap; ADR 0607 §3, ADR 0500 §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:48:37 +01:00
jschoubben 436ab59214 view status: the conditions and the healers' attempts are read as the seats' types (mesh-conditions, mesh-healers) and ordered by meshconditionsclient (ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:48:32 +01:00
jschoubben a6c8db74f4 assigner: its findings are the seat mesh-conditions' layout, written through meshconditionsclient's Observation, Key, EncodeFinding and DecodeFinding; no mesh-sdk health finding left (ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:47:47 +01:00
jschoubben 5aa0c02b00 indexer judge test: the private network's manifest comes from mesh-wireguard's overlay package (ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:45:44 +01:00
jschoubben 738c8ca036 assigner, view, watchman: provisions come from meshprovisionerclient, not mesh-sdk go/provisions (ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:45:25 +01:00
jschoubben b79b94b19d Clients: the kept and preview keys of the checker's states and the candidates key of the catalog's (ADR 0634 §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:42:19 +01:00
jschoubben dd6f899b12 Checker client: the codecs of the verdicts, kept and previews states the checker writes and others read (ruling 17:4x; ADR 0634 §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:39:50 +01:00
jschoubben 508eff8e1b Builder declares its read of the assigner's placement state, which its toolchain gate follows through meshmoduleassignerclient (ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:37:26 +01:00
jschoubben e52d9d7791 Assigner: the node's presence is read through the watchdogs client's key and codec, no seat codec of its own (ADR 0634 §4; issue 1966) 2026-11-05 16:36:50 +01:00
jschoubben 4cd6934842 mesh-catalog seat: a config declaration's minimum and maximum are nullable, so a declared 0 survives the typed state; a fixture row with 0 (third of a kind: a zero or empty optional value dropped; ADR 0634 §2; issue 1966) 2026-11-05 16:36:06 +01:00
jschoubben bcd0c8bdac Builder manifest test names its log and history from the builds package (ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:33:57 +01:00
jschoubben 9ffdaca898 Join: one buildstep Toolchain (the seats' value) under the agreement rule; the packages key is the seat's key form (ADR 0634 §2; issue 1966) 2026-11-05 16:33:43 +01:00
jschoubben 1af40279b1 Builder: its candidates, modules, manifests, sources, steps and toolchains are the catalog's, the gate's and the build seats' types and the clients' codecs, its build ask the Delivery SDK's, its artifacts the forms' type; the holders and buckets it names are its own constants checked against its manifest; no go/delivery left (D-11; ADR 0634 §1, §2, §6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:33:29 +01:00
jschoubben 4bbb809ef1 Merge branch 'b7n/registry' into b7/registry 2026-11-05 16:31:05 +01:00
jschoubben c71a812e0d Merge branch 'b7m/registry' into b7/registry 2026-11-05 16:30:38 +01:00
jschoubben 2f04e9f400 Merge branch 'b7k/registry' into b7/registry 2026-11-05 16:30:31 +01:00
jschoubben 52c880201c Merge branch 'b7i/registry' into b7/registry 2026-11-05 16:30:27 +01:00
jschoubben 520e56d53d Merge branch 'b7f/registry' into b7/registry 2026-11-05 16:30:21 +01:00
jschoubben 0bcc046c3d Merge branch 'b2/registry' into b7/registry 2026-11-05 16:30:17 +01:00
jschoubben c85b150782 View read.go: the reads' names from the clients and the view's own constants (kept apart from the unit commit for the buildlog change of read.go)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:28:52 +01:00
jschoubben 3456f34fa8 View: no go/delivery left; wants, haves, presence and stuck through their clients, the gate's, the catalog's and the assigner's seats, the builder's builds, the build step, the viewstate; the holders and buckets it names are its own constants checked against its manifest (D-11; ADR 0634 §1, §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:28:52 +01:00
jschoubben 88d100b9d5 Watchman: the node-engine's, wants', build steps', builder's and catalog's names come from their clients and seats, the watchdogs' codec from meshwatchdogsclient; the holders and buckets it names are its own constants checked against its manifest (D-11; ADR 0634 §1, §2, §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:28:51 +01:00
jschoubben 9869e34b8b Indexer: the catalog's values are the seat's types and the catalog client's, the gate's, checker's and builder's states are read by their own names, the go.mod leaves the old seat paths; no go/delivery name is left (D-11: its holders and buckets are its own constants checked against its manifest; ADR 0634 §1, §2, §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:25:35 +01:00
jschoubben 5125365189 Forge message package: B7m's forgemsg as committed there (b7m 95fabbfc), the indexer needs it to build; identical bytes, so the join takes it once (ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:25:35 +01:00
jschoubben 4dcb3a44ab grants part, indexer and checker read the seat list again through the sdk's ReloadIfUnread; the checker's hub binds a late consumer (B9e; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:20:53 +01:00
jschoubben 0f5f8bf4c8 Builder runner reads a node's toolchains through the Delivery SDK's neutral toolchain value and key (B2g), which gains the key's parse (ADR 0634 §1, §6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:19:55 +01:00
jschoubben 989d41f3cc Delivery SDK: the agreement over toolchain entries uses the neutral Toolchain value of the build seats' state (B2g's), not a second type (ADR 0634 §1, §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:19:41 +01:00
jschoubben dfffaf729e Merge branch 'b2/registry' into b7l/registry 2026-11-05 16:19:05 +01:00
jschoubben 6d46b8a59e mesh-delivery-sdk buildstep: Toolchain, ToolchainKey and the codec of a node's toolchains value, shared by the three build modules, move here from mesh-build-go's own package; a test reads each seat's fixture strictly into it (B2g; ADR 0634 section 6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:18:04 +01:00
jschoubben d1d70c0750 Merge branch 'b7k/registry' into b7l/registry 2026-11-05 16:17:33 +01:00
jschoubben baa04d68ce mesh-delivery-contracts, sdk buildstep: the check answer's fetch keeps the fetch tool's whole answer (workspace, commit, modules, merge, conflicts, changed, programs, trees, beside, packages, installed, refused), typed in the seats' StepResult and the sdk's CheckAnswer and StepResult; fixtures from the build modules' captured answers (B2g; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:17:33 +01:00
jschoubben 4382ac1fcb Enrolment: the enrolment's names come from the node-enroller's and the vault's clients and seats, the assigner's seat and client, and the node-engine's client; the state's rules and the expired event (the seat's enrolment) are the holder's own; it invokes the seat mesh-module-assigner; the names it holds beside its own are checked against its manifest (D-11; ADR 0634 §1, §2, §9; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:17:25 +01:00
jschoubben 37ff2ed1d1 mesh-delivery-contracts, sdk buildstep: the check job carries the toolchain the checker agreed (ADR 0584 clause 2) and the node names, so a node on another toolchain declines the check; the part had neither, the sdk's CheckJob lacked the toolchain (B2g; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:16:16 +01:00
jschoubben a13f11074e Grants part: the live placement row carries the node-engine's four declared states (the findings state does not bring the user list's refusal back; issue 774) 2026-11-05 16:15:57 +01:00
jschoubben 0aad2a45b2 Grants part: a node's engine may write and clear its own node's findings (state findings), so the finding that the node waits for the seat list is not refused on the bus (ADR 0564 §2, ADR 0636 §1; issue 1966) 2026-11-05 16:15:22 +01:00
jschoubben 6b610e1657 Builder runner: the steps, asks and toolchain agreement are the Delivery SDK's, the toolchains the build seat's and the presence the watchdogs seat's types, the haves and placements their clients'; the buckets it reads are its own constants (D-11; ADR 0634 §1, §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:14:18 +01:00
jschoubben 9350c3807d Delivery SDK: the toolchain a language's builders agree on (entries, majority rule) over a seat-neutral form, so the builder, the checker and the indexer read a build seat's toolchains state with one rule (ADR 0634 §1, §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:13:27 +01:00
jschoubben 95fabbfcde Forge bridge: the gate's merges and owed acts are the seat's types and the delivery client's, the version and the repository check the keys' and the trunk's; the holders and buckets it names are its own constants checked against its manifest (D-11; ADR 0634 §1, §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:13:00 +01:00
jschoubben cde060d523 Checker: reads the seat list when its hub is heard while it is unread, since the seat's event before its consumer is lost to it (ADR 0636 §1; issue 1966) 2026-11-05 16:12:49 +01:00
jschoubben 8c685160ea Indexer: its reader of its own seat list reads again as the states it answers from are bound, with no definition moving (ADR 0636 §1; issue 1966) 2026-11-05 16:12:05 +01:00
jschoubben 8448315b72 Delivery SDK: the toolchain a language's builders agree on (entries, majority rule) over a seat-neutral form, so the builder, the checker and the indexer read a build seat's toolchains state with one rule (ADR 0634 §1, §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:11:41 +01:00
jschoubben d38a0c8607 Delete sdk/buildlog: the indexer and the view ask the builder's own builds.IsBuildEntry (the view's read.go is the only file of the view touched; ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:11:11 +01:00
jschoubben 0f9dd8a8e3 Delivery sdk: graph, forms and packagename name the catalog client's and trunk's names, not mesh-sdk's go/delivery; forms holds BuiltArtifact (ADR 0634 §1, §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:11:04 +01:00
jschoubben ead87c4c90 Grants part: the unread seat list never holds the part's own module or the holder of the seat it is read through (ADR 0636 §1; issue 1966) 2026-11-05 16:10:48 +01:00
jschoubben e2deb78caf Grants part: reads the seat list again when its own consumer is heard, since the seat's event before it is lost to it (whole case for the unread phase; ADR 0636 §1; issue 1966) 2026-11-05 16:10:11 +01:00
jschoubben 72e1676d61 Assigner: the module graph is served from the node-engine's haves, the adapter and the last go/delivery import are gone (ADR 0634 §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:08:35 +01:00
jschoubben b0d5b8d1af Merge branch 'b7/registry' into b7f/registry 2026-11-05 16:07:56 +01:00
jschoubben 788ee56791 Assigner: its own and the indexer's module names from their seats' clients, the teller's name beside the buckets, the delivery bound from motion, the provided modules from the overlay package, the build queue from buildstep (ADR 0634 §1, §4, §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:06:38 +01:00
jschoubben e000ed0f66 Teller: the gate's asks and answers are the delivery client's and the seat's types, the assigner's asks its asks package's, the view's answers the view's; the holders and buckets it names are its own constants checked against its manifest (D-11, D-17; ADR 0634 §1, §2, §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:06:00 +01:00
jschoubben 463fa66aa1 meshcatalogclient: the holder's module name and the addresses of the states others read (modules, manifests), so a reader names them from the client (lead's request; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:04:21 +01:00
jschoubben 05b7dbce48 Assigner: the gate's answer, source and merge are the delivery client's and the seat's, the verdict words the checker client's (ADR 0634 §2, §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:04:06 +01:00
jschoubben bd6855e1b7 Merge branch 'b7f/registry' into b7/registry 2026-11-05 16:03:33 +01:00
jschoubben 8c2d61a2cf Merge b2/registry (B2f: the builder on the generated mesh-artifact-sweep seat, the kept state's client) into b7/registry; the builder keeps the hub for the seats' events (B7j)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:03:24 +01:00
jschoubben 70865842f2 Assigner: the published compositions are read with the node-engine client's Composition (ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:01:58 +01:00
jschoubben 5fe482edec graph.Served takes the node-engine contract's haves (running, healthy), so a caller needs no adapter; the checker reads them through nodeengineclient (B7j; ADR 0634 §1, §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:01:48 +01:00
jschoubben 3fd09d50e9 Assigner: a want, a node's own want and a hold are the wants client's, a put is the assigner's own puts package (ADR 0634 §4, §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:01:10 +01:00
jschoubben 1286a24926 mesh-delivery-builder: reads held's answer and the artifacts-gone body in the seat mesh-artifact-sweep's generated types, not go/delivery's (B2f; ADR 0571 §2, ADR 0634 §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:59:45 +01:00
jschoubben 7fcd2d0b64 mesh-delivery-contracts: the seat mesh-artifact-sweep is a whole seat: held's answer (HeldAnswer) and the artifacts-gone body (ArtifactsGone) typed, with fixtures from the holder's test values, generated Go and TypeScript (B2f; ADR 0571 §2, ADR 0634 §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:58:29 +01:00
jschoubben f938a53e09 Merge b2/registry (B2e: window, buildstep ask, buildrun, gate codecs) into b7/registry
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:58:00 +01:00
jschoubben 6f7a358049 Grants part: composes from the seat list it reads (reader over its connection and the event hub, catalogue and the pass follow it, a module whose seats the unread list does not name is held as a manifest waiting for a judge is, the two runtime tools, the manifest invokes and consumes) (D-30; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:57:55 +01:00
jschoubben 660fa44b56 Merge branch 'b2/registry' into b7f/registry 2026-11-05 15:55:42 +01:00
jschoubben 44397a53fc Assigner: its asks and their keys are its asks package's, an asking's id the delivery client's, the view's answers the view's (ADR 0634 §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:54:19 +01:00
jschoubben 54f91d5ad9 Assigner: a node's presence is read through the watchdogs seat's keys and values (ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:53:27 +01:00
jschoubben d0f1afbede mesh-delivery-contracts: the gate's values in the seat's types with their rules (source, merge, ask, owed, turn, answer: encode, decode, check, same), the turns and approved keys, the merge states, and the checker's verdict key, goldens from go/delivery's output; the turns' times are time.Time (B5e ruling 3; ADR 0625 §4, ADR 0634 §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:53:23 +01:00
jschoubben 7ff785978e Assigner: a build record is the builder's builds package's and a grant the grants part's grantsstate package's (ADR 0634 §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:52:46 +01:00
jschoubben a3e53e6f78 Assigner: a module's source and its manifest are the catalogue seat's types, its keys and codecs the catalogue client's (ADR 0634 §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:51:52 +01:00
jschoubben 840a7c5b07 mesh-delivery-sdk: the runner's stale-step part moves to package buildrun, so buildstep (layouts, keys, codecs) and the builder's published builds package reach no bus; a test each refuses it (B6e finding 5; ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:50:31 +01:00
jschoubben 1ca0af7a2d Assigner: who may add a node is read from the holders of the seat mesh-node-enroller, not from a module's name (B7f finding 5; ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:50:29 +01:00
jschoubben eaa84387f7 mesh-delivery-sdk buildstep: the build ask once, as one neutral form for the three build seats (the part build-step's StepAsk, Fetch, jobs, answers, kinds, languages, needs, subjects, pauses, capacity, composer key), one conversion AskOf for any seat's type; seatgen writes a package per seat and shares no part, so the form lives here (B5e ruling 1; ADR 0634 §6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:48:52 +01:00
jschoubben b58e2616c7 Checker: the manifests of a change are judged against the seat list its reader holds (the request's Seats; a list not read fails the check), the module graph follows it, the manifest invokes seat-definitions and consumes its two events; composer from the delivery sdk (D-29; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:47:35 +01:00
jschoubben 3036130de2 mesh-delivery-sdk window: StepWindow and WindowHeld, the one rule distribution and the builder step a window by, moved with their test (B5e ruling 2; ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:44:40 +01:00
jschoubben 891d292dfd Assigner: the operations of a change and the states of a proposal are the module assigner client's, which the enrolment builds with (B7f finding 4; ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:44:06 +01:00
jschoubben cd94b19384 Assigner: the node-engine's have, phases, healths and candidate answers are the node-engine contract's and client's, the buckets it reads are its own package's, held equal to the manifest (B7f finding 3; ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:43:00 +01:00
jschoubben 839e27a064 meshcatalogclient: the holder's module name and the addresses of the states others read (modules, manifests), so a reader names them from the client (lead's request; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:41:04 +01:00
jschoubben cba659a615 Merge b7f/registry (the assigner's placement-check package) into b7/registry
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:35:26 +01:00
jschoubben 376119d61e Forge bridge: its one pull of its durable on the shared events stream is the module's event hub, for consistency with the modules that have a second part (B7c finding 1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:34:08 +01:00
jschoubben be93d5f71b Assigner judge: its comment names the pulse key's new home (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:34:04 +01:00
jschoubben 8c80e1b097 Teller: the router's warrants are a handler of the module's event hub beside the views reader, on the seat's own subject (B7c finding 1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:33:37 +01:00
jschoubben a3cb3e4a9b Assigner: the seat's state names, events, buckets and history are the contract's and the placement package's (D-12; ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:33:01 +01:00
jschoubben 25a704a1f5 Grants part: its one pull of its durable on the shared events stream is the module's event hub: the owners' views, the seat mesh-catalog's verdicts and the other seats' events are handlers of it (B7c finding 1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:32:27 +01:00
jschoubben 1266c1ada0 Assigner: placements, proposals, candidates and seat holders are the contract's types and the assigner's own placement package, no longer go/delivery's (D-12; ADR 0634 §1; issue 1966)
The judge keeps the node's answer as the verdict's body; pointer times of the contract are guarded. One ask test folded into a table to hold the unit count.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:31:50 +01:00
jschoubben 2e66238a01 Watchman: its one pull of its durable on the shared events stream is the module's event hub: the take-in events, the seats' events and the views reader are three handlers of it (B7c finding 1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:30:33 +01:00
jschoubben 4a0618ea11 Checker: its one pull of its durable on the shared events stream is the module's event hub: the registry seats' published and the views reader are two handlers of it (B7c finding 1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:29:41 +01:00
jschoubben 9d360e701a Builder: its one pull of its durable on the shared events stream is the module's event hub: the seats' events and the condition keeper's are one handler, the views reader another, the hub started last (B7c finding 1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:27:41 +01:00
jschoubben e75cb6e02e Assigner: its own placement-check package over the contract's types: the check, the changes laid over the keys, the proposal's refusals and digests, who holds which seat (ADR 0634 §1; issue 1966)
Digests are held to the earlier holder's values in the field order it wrote them.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:25:39 +01:00
jschoubben 986ab0396d The verdict events are the seat mesh-catalog's: the indexer says manifest-accepted and manifest-refused on the seat's subject and no longer lists them in emits; builder, watchman and the grants part consume mesh-catalog.<event>, the bus module's keeper hears a watched seat's events (B7d finding 1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:24:21 +01:00
jschoubben ab168212c8 Indexer: a contracts module whose seats cannot be listed is named by its seats directory, not by an index file (D-I5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:16:23 +01:00
jschoubben ebc9501fc9 Assigner: its one pull of its durable on the shared events stream is the module's event hub: the engines' reads changed and the indexer's views are handlers of it (B7c finding 1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:15:19 +01:00
jschoubben e80d8d05c5 Indexer: the tests follow the seat mesh-module-assigner and the seat list reader's bus (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:14:35 +01:00
jschoubben e482ebc667 Indexer: its one pull of its durable on the shared events stream is the module's event hub: the node-engine's reads changed, the registries' published, the builder's numbered and the assigner's views are handlers of it (B7c finding 1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:14:17 +01:00
jschoubben 07a75a768a Indexer: it reads its own seat list as every part does (the seat list reader over its own state, its own moves heard at once) and gives catalogue what it holds, so the judge judges against the seats the mesh defines (D-29 host wiring; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:13:12 +01:00
jschoubben c16b38bd83 mesh-delivery-contracts: the seat mesh-catalog's events manifest-accepted and manifest-refused, said on the verdicts of a manifest (ADR 0604, ADR 0578; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:10:03 +01:00
jschoubben cad6dc40b5 Merge b2/registry (b5a75693) into b7/registry 2026-11-05 15:08:25 +01:00
jschoubben 06d4e19631 Indexer: its views family is its ordinary events (views.<state> among its emits, no stream of its own), the assigner's placement followed on the shared events stream, its findings through the views' writer; the seat mesh-catalog's verbs from the contracts, described by the tool runner; a seat list in its tests (D-25 owner half, D-29 tools; issue 1966)
The indexer's consumers of its durable on the shared stream still compete; they join the module's one hub.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:08:25 +01:00
jschoubben 2e8b4dcdcd Assigner: its views family is its ordinary events (views.<state> among its emits, no stream of its own), the indexer's followed on the shared events stream, its findings through the views' writer; the seat mesh-module-assigner and mesh-wants served by the contracts' verb names, described by the tool runner from the seat list; operator-channel, placement and hand-act names from the contracts and clients (D-25 owner half, D-12, D-24; issue 1966)
The assigner's two consumers of its durable on the shared stream still compete; they join the module's one hub.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:08:25 +01:00
jschoubben a1b22505e1 Findings: a part whose findings also say its conditions gives the views' writer its two functions (D-25; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:08:25 +01:00
jschoubben e2f8ed5de0 mesh-delivery-contracts: the build seats' times are time.Time (seatgen writes date-time so; B5d; ADR 0634; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:07:57 +01:00
jschoubben b5a75693f5 mesh-delivery-sdk graph: a reader of the seat list waits for its holder by the verb edge and a foundation part on a cycle with it does not (D-31; ADR 0614 §2, ADR 0636 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:06:29 +01:00
jschoubben 6f22665c98 mesh-catalog's client: the manifest digest and key, the modules value's rules, the verdict's rules and the candidate's fingerprint set, over the seat's typed states (ADR 0625 §4, ADR 0634 §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:04:06 +01:00
jschoubben 6fb4236eb4 mesh-delivery-contracts: mesh-catalog's modules and module answers typed, a module's toolchains typed, fixtures captured from the seat's own read tools (ADR 0634 §6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:01:35 +01:00
jschoubben 84df3db4ad Merge b2/registry (f2c61815) into b7/registry 2026-11-05 14:57:36 +01:00
jschoubben d62922602e Forge bridge: its tools listed once; builder and checker require the build step module once (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:55:58 +01:00
jschoubben f2c61815f8 mesh-delivery-contracts: the build seats' part types Fetch, the toolchain, the artifacts, the check's levels and a step's result, and marks the times date-time; fixtures for a check ask and the three kinds of result (ADR 0634 §6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:55:35 +01:00
jschoubben 1468743b73 Grants part: the claims' scopes and verbs come from the seat list catalogue holds (the host's), the seat names from the contracts, the enrolment seat is mesh-node-enroller in the part and its tests (D-30 first part, D-23; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:55:31 +01:00
jschoubben c183f170cc mesh-delivery-assigner: the asks package (the keys of its asks and their value) is the module's own, a package artifact; the view's answer key and fit in viewstate (ADR 0634 §1, §4, ADR 0635 §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:52:26 +01:00
jschoubben 5472e633dd Enrolment: the hand acts through the seat's client, its findings through the conditions client's writer, the owners' states followed from the shared events stream, seat names from the contracts and its claim and end events name the seat mesh-node-enroller (D-23); contract import paths follow the domain folders (D-12..D-22 Enrolment part; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:51:55 +01:00
jschoubben 404083d25f Checker: the hand acts through the seat's client, its findings through the conditions client's writer, the owners' states followed from the shared events stream, seat names from the contracts; contract import paths follow the domain folders (D-12..D-22 Checker part; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:51:55 +01:00
jschoubben 19b6b18076 Builder: the hand acts through the seat's client, its findings through the conditions client's writer, the owners' states followed from the shared events stream, seat names from the contracts; contract import paths follow the domain folders (D-12..D-22 Builder part; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:51:55 +01:00
jschoubben 1477497ef2 Watchman: the hand acts through the seat's client, its findings through the conditions client's writer, the owners' states followed from the shared events stream, seat names from the contracts; contract import paths follow the domain folders (D-12..D-22 Watchman part; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:51:55 +01:00
jschoubben b148b1b6ea View: the hand acts through the seat's client, its findings through the conditions client's writer, the owners' states followed from the shared events stream; contract import paths follow the domain folders (D-12..D-22 view part; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:50:14 +01:00
jschoubben b9b589a02c Teller: the hand acts through the seat's client, its findings through the conditions client's writer, the assigner's asks followed from the shared events stream, the warrants handed over by the views reader's one consumer; contract import paths follow the domain folders (D-12..D-22 teller part; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:47:28 +01:00
jschoubben ccef33ef31 mesh-delivery-contracts: the client of mesh-delivery (heads hash, merge ask key, ask id, owed acts and keys, owed values) and the checker client's preview rules (D-27, ADR 0625 §4, ADR 0635 §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:47:16 +01:00
jschoubben dcd1f3c22b mesh-delivery-contracts: the artifact reference forms move to the registry seats' client and the fingerprint key part to a new client of mesh-catalog; mesh-delivery-sdk serves Delivery modules only (ruling 3, ADR 0634 §1, §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:44:23 +01:00
jschoubben 145cc70ceb mesh-delivery-sdk: a part's findings state is written through the seat mesh-conditions' client (findings.Writer over its bucket), and the family-not-heard finding is encoded by it (ADR 0564 §2, ADR 0635 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:42:38 +01:00
jschoubben acd8e985d1 mesh-delivery-sdk kept: the version check, CheckName and IsHex64 are mesh-sdk keys' now; kept keeps Decode and CheckDigest (ruling 2, ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:41:02 +01:00
jschoubben da2f8cfc10 Judge: no seat list compiled in; it judges against the seat list its host reads (the request's seats, UseSeatList), the seat names from the contracts (D-29, ADR 0634 §4, ADR 0636 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:39:04 +01:00
jschoubben 7c42361fbc Merge branch 'b2/registry' into b7/registry 2026-11-05 14:38:13 +01:00
jschoubben 4215eac993 Revert "mesh-delivery-contracts: mesh-catalog serves the verb seat-definitions, every definition names its domain Delivery, regenerated (ADR 0636 §1, §5; issue 1966)"
This reverts commit d641313280.
2026-11-05 14:38:13 +01:00
jschoubben 719ddabb14 mesh-delivery-contracts: the client of mesh-delivery-checker holds the rules of a verdict and a kept verdict that its holder and readers share (D-10, ADR 0634 §2, ADR 0625 §4; issue 1966) 2026-11-05 14:37:13 +01:00
jschoubben 530a4ddfde Manifests of the builder, the view and the forge bridge: the package artifacts are read by their tests; the merge's duplicate keys (tools, invokes) joined into one (D-10, D-0; issue 1966) 2026-11-05 14:35:45 +01:00
jschoubben d641313280 mesh-delivery-contracts: mesh-catalog serves the verb seat-definitions, every definition names its domain Delivery, regenerated (ADR 0636 §1, §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:34:09 +01:00
jschoubben 830e265bd0 mesh-delivery-contracts: mesh-catalog serves the verb seat-definitions; every definition names its glossary domain, Delivery (ADR 0636 §1, §5; issue 1966) 2026-11-05 14:34:05 +01:00
jschoubben cf9c736061 mesh-delivery-view: the viewstate package (reach and the view's answer layout) is the module's own, a package artifact; the indexer's taken value joins its take-in package; trunk keys a repository (D-10, ADR 0634 §1; issue 1966) 2026-11-05 14:33:04 +01:00
jschoubben 4a1f716634 Merge branch 'b2/registry' into b7/registry 2026-11-05 14:32:56 +01:00
jschoubben 4edc0ce90b Indexer: the seat mesh-catalog serves seat-definitions and says seat-definition-changed and -removed for trunks; a definition names its domain and a seat of the mesh's own contracts is taken only from its domain's module, a move there a handover in either order; seats listed from the module's seats/ directory, no index (D-29 verb, D-I3, D-I5, ADR 0636 §4, §5; issue 1966) 2026-11-05 14:31:11 +01:00
jschoubben ef2a2e99c9 mesh-grants: the grantsstate package (its states grants and user-list, the sealed list's rules) is the module's own, a package artifact (D-10, ADR 0634 §1; issue 1966) 2026-11-05 14:31:00 +01:00
jschoubben e824dd5439 mesh-delivery-builder: the builds package (its state builds, the versions question, the event version.numbered, the build log) is the module's own, a package artifact; mesh-delivery-sdk gains kept, CheckImports and CheckAskID (D-10, ADR 0634 §1; issue 1966) 2026-11-05 14:30:07 +01:00
jschoubben df6cc0992b mesh-delivery-sdk: the fingerprint's key part and the registry reference forms, seat-neutral over the registry seats' names the caller gives (D-10 base, ADR 0634 §1, ADR 0635 §4; issue 1966) 2026-11-05 14:27:03 +01:00
jschoubben a638776248 Merge b2/registry into b7/registry: the Delivery contracts, sdk and forgemsg homes (D-12..D-22 base; issue 1966) 2026-11-05 14:22:14 +01:00
jschoubben 50471d6774 Forge bridge: forgemsg declared as a package artifact (series 0.1), so the gate can require a published version (D-10 part, F3; issue 1966) 2026-11-05 14:20:18 +01:00
jschoubben 8668e3e538 Merge mesh-delivery main 2fcd61b into the registry join: take-in size fix; the forge message keeps the join's needs field (D-0; issue 1966) 2026-11-05 14:19:49 +01:00
jschoubben 9126b6c7f6 mesh-delivery-sdk: buildstep and composer move in from mesh-sdk, seat-neutral: the step layout of the part build-step, the case's verdicts and placements as raw JSON (D-8, D-9, ADR 0635 §4; issue 1966) 2026-11-05 14:18:28 +01:00
jschoubben 88d721ccf6 mesh-delivery-sdk: the file-in-parts transfer moves in from mesh-sdk; the four registry clients import it (D-7, ADR 0634 §3; issue 1966) 2026-11-05 14:17:18 +01:00
jschoubben 5651c611f7 mesh-delivery-sdk: module.json with its one package artifact; trunk, buildrecord and forgekey (the builder kind, the clean runner, Said) move in from mesh-sdk (D-6, D-9, ADR 0634 §3, ADR 0635 §4; issue 1966) 2026-11-05 14:16:52 +01:00
jschoubben 1cda01b869 Merge main (take-in #153, #154) into the registry milestone's join (D-0; issue 1966) 2026-11-05 14:16:17 +01:00
jschoubben 69edf1e99e mesh-delivery-contracts: the registry image seats gain holds and windows; git, mesh-artifact-sweep and mesh-artifact-store join as seat-list files (D-5, D-28, ADR 0635 §2; issue 1966) 2026-11-05 14:11:55 +01:00
jschoubben 7b4a469a3f mesh-delivery-contracts: the part build-step and the three build seats defined whole with their states (D-4, ADR 0634 §6; issue 1966) 2026-11-05 14:11:31 +01:00
jschoubben 07bd0472b9 mesh-delivery-contracts: fixtures for the new states (issue 1966) 2026-11-05 14:10:43 +01:00
jschoubben d44c905e37 mesh-delivery-contracts: mesh-delivery-checker, mesh-delivery and mesh-catalog defined whole with their states; mesh-catalog serves seat-definitions and its two events (D-2, D-3, D-27, ADR 0634 §6, ADR 0635 §5, ADR 0636 §1; issue 1966) 2026-11-05 14:10:22 +01:00
jschoubben a6f29d42be mesh-delivery-contracts: the four registry seats, their parts and clients move in from mesh-contracts (D-1, ADR 0634 §7; issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-05 14:07:09 +01:00
jschoubben b201ccfa72 Indexer: hold a module whose templates need a reading, and raise a hold past its bound (ADR 0632 §2; issue 1966)
docker's daemon file names the image seat's reach while its manifest stays, so
the hold read from the manifest alone let an engine before the reading lose the
file. The forge bridge reads each manifest's templates at the merge and carries
the module's needs whole; the indexer takes them, keeps them past the judge, and
holds any changed entry, not only a new manifest digest.

A hold standing past 30 minutes is the finding manifest-held, its bound a key
of the new state hold-bounds that expires; its expiry and the start weigh the
holds again, so a restart loses no bound. A hold moved by hand names the nodes
it strands, in its answer and its hand act. The hold test is a case of the
manifests test, not a function of its own.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 21:39:03 +01:00
jschoubben bc09e29fb6 Indexer: hold the store holder's new manifest from the mesh until every counted node-engine reads it (ADR 0632 §2; issue 1966)
Every node-engine reads the store holder's manifest to find the store, and an
engine before form:seat-reach finds it only through the retired provision, so
writing the new entry while one counted engine lacks the reading would leave
that node without the store. The entry keeps its old manifest, fingerprint and
inputs; the new one waits in the state holds and moves once every counted
node-engine (ADR 0624 §1, read as the builder reads them) states it.
indexer_holds reads, asks again or moves a hold by hand; indexer_engine_reads
reads the engines' statements.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 21:15:37 +01:00
jschoubben ccb0f4503c Judge: the store's seat delivers nothing, as the provision artifact-store retired (ADR 0632 §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 20:58:24 +01:00
jschoubben 8e3c9645fd Judge: a store holder that builds is no longer refused by its manifest (issue 1966)
The operator's decision of 2026-11-04: the fresh-mesh circle, a holder whose build publishes through itself, is caught
where a fresh mesh starts (issue 1152), not by a rule that refuses the registry seats' holder.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 20:28:52 +01:00
jschoubben 83b962cc6d Pin mesh-sdk 05b3e2e1b94f, mesh-contracts ae5266e77639 and mesh-catalog's buildstep 125929dbc459, the group's final heads (issue 1966)
mesh/delivery ready: it is merged on the Approve
mesh/check checking 83b962cc6d6d on fab533dfd0cd | novox/mesh-delivery#151 in group feat/registry-milestone
mesh/delivery-group group feat/registry-milestone checking: 8 of 8 member(s) ready
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:47:39 +01:00
jschoubben b3b6456ab8 Pin mesh-sdk e5d89c5eedb5, mesh-contracts 347a3053580a and mesh-catalog's buildstep fdc3331465dc, the group's heads after review round 2 (issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:26:33 +01:00
jschoubben 4ba4cacc66 G-delivery 7: the indexer's and the assigner's views emitters write view-unsaid
mesh-sdk's views.Emitter writes the owner's finding of a change not said past its bound only when Findings is
set; both owners of views in mesh-delivery now set it to their findings state.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:20:53 +01:00
jschoubben 24f9b6f602 G-delivery 6: gofmt
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:18:39 +01:00
jschoubben 3a3611aa41 G-delivery 6: the builder ends a remove on kept and records the version kept
A seat that cannot remove a tag alone (another tag names its digest) now answers and says kept. The builder ends
that remove there, with no retry, and marks the version kept on its store with the reason and the other tags;
builder_versions shows it, and a later branch end asks it no more. The four seats' kept are consumed.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:18:33 +01:00
jschoubben d0983a8b97 G-delivery 5: the judge refuses a read-only state no process names; grants read it from the manifest
mesh-sdk's state read-only (d420491c) is mapped into the grants' buckets, so the module's account and the tool
runner read it and never write it; the judge refuses a read-only state no process's bus state names, since
nothing would write it.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:16:38 +01:00
jschoubben dafb8e1ce6 G-delivery 5: gofmt
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:13:48 +01:00
jschoubben 276886d8f6 G-delivery 5 (grants half): a read-only state is written by the process naming it alone
The bus module's spend record must not be writable by the tool runner. A state declared read-only gets read on
the module's own account and the tool runner carrying it (direct get, a watch's consumer) and no publish on its
$KV subjects; the process whose bus state names it writes it. The judge rule and the manifest field follow
with mesh-sdk's read-only.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:13:41 +01:00
jschoubben 441b6d2922 G-delivery 3, 4: family-not-heard says it clears at the part's next start; views.Bound is two minutes
Bind runs once at a part's start, so the finding's words no longer promise it clears when the grants part makes
the consumer. The assigner's comment on firstViewBound named five seconds for views.Bound, which is two minutes.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:10:51 +01:00
jschoubben 121c6d9c48 G-delivery 2: sdk/heard's cases are rows of the watchman's findings table
mesh-delivery is over its unit budget, so the new function goes; its three cases run as rows of an existing
table of a user of the package.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:10:16 +01:00
jschoubben e5011e1f83 G-delivery 1: the builder's bounds not watched, or a bound not written, is a finding
A watch of bounds that cannot start lost every bound's end with a log line only: it is now family-not-heard
naming the state. A publish bound that cannot be written would never end: the version is publish-unanswered
at once, saying why.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:09:48 +01:00
jschoubben 4be1d7bae1 Pin mesh-sdk c54648e74346, mesh-contracts ec9529022c44 and mesh-catalog's buildstep ccb7eaff1382, the group's heads (issue 1966)
The placeholder versions of the contracts' seats and clients are gone; the repository's own sdk and buildrun keep their
relative replaces.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:44:36 +01:00
jschoubben 3e96e977bc mesh-grants: the bus module's keeper reads its warrant on the account composed from nats' manifest
Review round 1, running F4 (design 25 § 12.7), moved here from the nats test by the lead: in the composed-grants case
of the whole test, the part composes nats and messenger from their manifests (testdata copies of the catalogue's at
the join); the keeper connects on its own process account to a server loading the composed list, reads the router's
record of its ask, and is refused a write to the router's asks and the part's state and another asker's record.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:28:21 +01:00
jschoubben bf8f5c5c25 mesh-grants: the bus module's process consumer hears the seat events said to it by name
Its durable consumer filtered only its consumes, composed as module subjects, so the operator channel's
decided for its own asks could not be heard there (design 25 § 12.3: it wakes on the warrant).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:11:53 +01:00
jschoubben 4f7ceb61ea mesh-grants: a first view's condition the keeper did not take is said again at the next pass
The raise and clear of first-view-unanswered dropped a failed publish (_ = say). The binding comment
named the bus module's own stream and 30 days; the part hears the seat's events on the shared stream, 7 days.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:08:23 +01:00
jschoubben 4dbb6b2aec F-delivery: builder_bounds reads the builder's state bounds
Every state a module keeps has a read tool (the lead's ruling). Each key of bounds now holds what it bounds
and when it ends, and builder_bounds lists them with the time left.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:46:32 +01:00
jschoubben f9adc9d44c F-delivery 8: two claims never one number, on the bus, in the builder's whole-module test
The case ran only on the in-memory store. Eight claims of one name at once on the builder's state versions,
through its own bus adapter, get eight versions; with Create turned into Put they share one.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:43:06 +01:00
jschoubben 71b9737fc7 F-delivery 6: old-form-held is raised at a node's seventh day by an expiring key
The finding was written only at the window's next change. Each node holding the window has a key of the
builder's state bounds whose time to live ends at its seven days; the bus's marker of its end steps the
window, which raises the finding then; a node that no longer holds it loses its key.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:41:35 +01:00
jschoubben 00903e9149 F-delivery 7: the builder's old-form window steps by mesh-sdk's delivery.StepWindow
A read naming no node (an empty node_haves, the placement not read yet) closed the window for good (security
review F3, second of a kind). The window embeds delivery.WindowHeld and steps by the shared rule, named being
the placement's nodes before any is dropped. The builder's expiring keys share one state, bounds (the publish
bound of F-delivery 5 renamed), for the old-form bound next.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:40:44 +01:00
jschoubben 78fc3ffa35 F-delivery 5: the builder raises publish-refused, publish-unanswered and seat-outcome-unread
A holder's refused, and a version no holder answered, raised no finding outside the floor, so a build said done
while its file was not in the registry; a seat's outcome that does not read was only logged (design 51 Health
and conditions). The bound is a key of the new state publish-bounds with its own time to live, armed when a
built version is still numbered; a later published of the name clears both findings.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:38:17 +01:00
jschoubben 0d39254ebf F-delivery 4: gofmt sdk/heard
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:34:06 +01:00
jschoubben d4598f5772 F-delivery 4: a consumer that does not hear its family is a finding until it binds
The indexer's, the assigner's, the builder's and the watchman's consumers that were not there or could not be
heard only said so in the log, so the part stayed healthy while it heard nothing (ADR 0615 §5). sdk/heard is
the one shared helper: the finding family-not-heard names the observer and the family, written when the
consumer does not bind and deleted when it does.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:34:02 +01:00
jschoubben 395a1a0f40 F-delivery 3: the watchman waits for created only for entries not yet said created
The bus module says created once per entry, so every later accepted manifest of the module raised a false
bus-state-overdue five minutes on. The watchman keeps a mark per entry said created, on its pulse state with no
time to live and read back at a restart; a refused or an entry no longer asked drops it.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:29:21 +01:00
jschoubben 2a32721b80 F-delivery 2: the assigner takes the module graph by the indexer's first view (ADR 0631)
The assigner read the indexer's graph state itself, the one reader left after ADR 0615 §1. The indexer serves
graph as a seventh first-view tool to the assigner alone; the assigner asks it, follows views.graph, raises the
first-view condition past the bound while the indexer is down, and its reads of graph goes.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:27:07 +01:00
jschoubben 9674710503 F-delivery 1: the checker judges names against an answered empty packages state; judgeOver judges one digest
An answered packages state with no name handed the judge nil, which judges no name, so a manifest naming an
unknown package passed (DLV-I14, design 32). PackageNames is nil only until the state's first reading ends.
judgeOver takes one module and digest, so dropping the known packages for a digest the trunk does not hold
cannot drop them for another (J7's note).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:22:59 +01:00
jschoubben 0e44467b70 J8b: sdk/forms reads the seat form through catalogue, upstream naming included
catalogue.InRegistrySeat now reads the holder's upstream naming and InArtifactStore derives its store path, so forms'
own reader goes, and the upstream rows run the encode check again.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:58:59 +01:00
jschoubben bf92a37730 J8: sdk/forms names an image and an upstream copy in the seat reference form, each deriving its artifact-store form
ADR 0624: container-image-registry/<module>/<artifact>@<digest> and the holder's container-image-registry/upstream/<host>/<path>@<digest> are form:seat-reference; Older derives artifact-store://... so a key keeps both. The registry form now precedes the artifact-store form, so a node stating nothing keeps the recorded store form of an image.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:55:45 +01:00
jschoubben c47f0af0ab J7 (3): the indexer's verdict on a pull request's candidate does not refuse on a package's name
The checker, which knows the delivery group, judges a change's names; the indexer's state knows only the trunk's packages
and refused a package new in the group. The trunk's manifests are still judged on them, and a merge's manifest in full,
though its candidate was accepted without them (UnknownPackageNames).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:44:36 +01:00
jschoubben 2bb0ccd3c6 J7 (2): the checker hands the judge the known packages in composer.Request.Packages, not in Input
Input is the composition's input; the judge's names now have their own field (mesh-sdk), so packagename.Input and the
judge's inputOf are gone and mesh-judge reads Packages.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:42:03 +01:00
jschoubben e91a3923d0 J7 (1): the assigner, the teller and the view read a kept ask through the operator-channel client
Their three copies of the id and option checks (askjson.go checkedAsk, the teller's routerAskOf, the view's routerAsk)
are deleted; each calls occ.Carried with the id the key and the asking give.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:39:47 +01:00
jschoubben a09f5bde38 Join: gofmt sdk/enginereads (L3 left it unformatted)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:35:30 +01:00
jschoubben fa27137036 Join: a look at a done build key logs no "kept" narration
After the join the builder looks at a done key again whenever a first view is answered late or a package's name is
heard (lead's i), and each look's "version kept" narration was written to the build log after the build's end, with no
start or end around it. A look at a done key builds nothing: a kept version says nothing new there and stays in the
process log only. TestBuilderMakesADoneKeyWithTheDigests failed at about one run in three (kept narrations of
tools-go and tools-typescript after the end); 8 of 8 pass.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:35:22 +01:00
jschoubben 71f4e8d128 Join (lead's i): the builder follows the indexer's packages and numbers a package once its name is known
The indexer's packages state is read through its read tool's first view and its views family (ADR 0615 §1), as the
other four states the builder follows, not by its bucket: the manifest drops the read, invokes indexer_read_packages and
consumes views.packages.

A package built unnumbered because its name was not known at its claim was numbered only at its module's next claim.
Now a series key heard naming a name looks at its module again (Builder.PackageNamed), and a look at a done trunk build
numbers what it left unnumbered with the done key's digests (BuildClaim.Done), so version.numbered carries the digest
for the build module's late publish. Woken by the views family, never a timer.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:32:57 +01:00
jschoubben 5c4016c960 Join item 5: the judge's packages rule takes the known packages by their ecosystem names
Design 32 names a package in a manifest's packages by its name in its own
language, so the judge (DLV-I14) now refuses a name no known package carries
instead of any name that is not a declared <module>/<artifact>. The indexer
hands it its packages state's names, kept before the merge's judge pass; the
checker hands it its followed packages picture plus, for a package new in the
group, the name read at the member's head. Both read a name with one reader,
sdk/packagename.Of, and the judge request carries the names in its Input
(packagename.Input). The series-form check stays.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:30:11 +01:00
jschoubben 12f5e24e7f Join item 7: a key's history through the owners' read tools; the verdict and its event name each series change
History (A2j finding 1, J4): the indexer's and the assigner's read tools are views.ReadTool over views.KeptSource, which
answers `{key, history: true}` now; modules (20), heard (64) and wants (20) keep more than one revision. Rows in the
view's "each tool answers through the owners' read tools as before": indexer_module_history and indexer_heard answer
the owner's kept history, and a want's history (view_tiers' Since) is read through assigner_read_wants (the row it
replaces held that the owner answered no history).
Series (A2j finding 2, J4): the checker keeps each series change as a delivery.SeriesChange {module, artifact, from,
to} and renders the status line from it; delivery.Verdict.Series holds the case's changes (each module's artifact once),
and the verdict event's series the pull request's (meshdeliverychecker.VerdictHolderEventSeriesItem). An artifact new
on the head is no series change (the lead's ruling). Rows: TestTheIndexersJudgeJudgesTheChangesManifests (the verdict's
series; failed first, empty) and TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure (the event's; failed
with the fill cut).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:17:50 +01:00
jschoubben c8eccf299a Join (lead's iii): a series key's value is mesh-sdk's delivery.SeriesPackage; its readers decode it by the contract
The indexer's SeriesPackage and SeriesVersion are aliases of delivery.SeriesPackage and SeriesVersion (J4), SeriesKey and
IsSeriesKey are delivery.SeriesPackageKey and IsSeriesPackageKey; the indexer's readers (the series read for a user's
fingerprint, indexer_package_users) and the builder's NamesIn decode with delivery.DecodeSeriesPackage (the builder no
longer reads the indexer's value by field names). Found by the contract: a published newest's Contents named no time,
which DecodeSeriesPackage refuses, so the accepted series was left out and its users kept the commit form; the
indexer now stamps the time the contents were read (TestImportsReportedMoveTheFingerprintOnlyWithAnImportedPackage,
"an accepted series' newest ... pins it", failed with the decode before the stamp).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:14:41 +01:00
jschoubben 7ae4f2bb7f Join item 6: the builder says version.numbered as delivery.VersionNumbered and reads out-of-step by BuildAnswer's field
versions.NumberedBody and NumberedEvent are mesh-sdk's delivery.VersionNumbered and VersionNumberedEvent now (J3), the
contract a build module consumes for BG2's late publish; the body's JSON is unchanged. The lane reads a step's
out-of-step files through delivery.BuildAnswer.OutOfStep, a failed step's result too (as before, A2k's d15d66fc: read
before the step's state; its three rows hold it), in place of a local decode by name. The integration test hears
version.numbered on delivery.VersionNumberedSubject and reads it with DecodeVersionNumbered, as a build module does.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:12:13 +01:00
jschoubben 91b24a5426 Join item 8: versions.WaitsOn dropped (A3i ruling: dead code)
One modules value per module means a trunk claim has no earlier merge of the same name to wait on, so nothing called
it; its subtest "merge order" goes with it. Design 51's merge-order line holds by that (a review check).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:10:58 +01:00
jschoubben 952dedd0ed Join item 2: the readers follow through mesh-sdk's views.ProcessReader; sdk/firstview is gone
mesh-delivery's sdk/firstview moved whole into mesh-sdk go/views (J3): the builder, checker, watchman, teller and view
follow each owner's state with views.ProcessReader.Follow(owner, state, tool, stream), naming the owner's read tool as
their invokes name it and its views family as the owner's manifest's `streams` names it ("indexer-views",
"assigner-views"); a refused Follow stops the start. Their beds serve the owners with viewstest.Serve and grant with
viewstest.Grants by the same names. The view's tools ask the owners' read tools by their literal names. The assigner
names its own views family and the indexer's itself (views.AssignerStream and IndexerStream are gone).
Every views.StatusTools call takes handacts.Recorder{Ask: stdio.Ask}: a first view asked again by hand is a hand act,
recorded through seat:mesh-hand-acts.hand-act, which the builder's, checker's, watchman's, teller's and view's
manifests now invoke (a row in each manifest test, each failed first).
Row (A2j's written-time switch): TestRunNeedsItsCredential "a want stood since the assigner's bus wrote it": the
watchman's want time (createdOr) is the time the owner's bus wrote the key, through ProcessReader's first view (failed
with createdOr ignoring the entry's time). sdk/go.mod: nats.go is indirect now.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:10:36 +01:00
jschoubben 2fe8d01e86 Join item 1: the rewrite map, asks/asker, the vault's types and compose-candidate to mesh-contracts
mesh-sdk holds no go/asks, go/asker, vault answers or compose-candidate types (A1d, J1): each module now names the
operator-channel contract and client, the channel benches (mesh-sdk/go/channel), mesh-vault's contract, and node-engine's
contract and client (a1-progress.md, the A1d and J1 rewrite maps).
- assigner, teller, view: delivery.Ask.Ask and AssignerAsk.Ask are the router's ask as JSON; the refusals mesh-sdk
  dropped (an id other than AskingID(key, asking), no option) are each reader's and writer's: assign.EncodeAsk/DecodeAsk
  (askjson.go), the teller's routerAskOf (teller.go) and the view's routerAsk (view.go); the teller refuses an option
  the asking does not offer and one whose level is not acknowledge (viewanswers.go), the two refusals ViewAnswerFits
  gave. Row: subtest of TestTheHoldAskIsValid (another id, no option, no ask; each written before).
- enrolment, grants: the vault's verbs and answers from meshvault; asks' conditions from the contract and client.
- checker, assigner's judge: compose-candidate from nodeengine/nodeengineclient (nec.Own for "node").
- mesh-grants' user-lists golden: the node-engine's users now subscribe to mesh-delivery-view's settled (mesh-sdk's
  NodeAgentHears, A7's ENG-8 retry).
go.mod: each module requires the mesh-contracts modules it imports (and nodeengineclient, which mesh-sdk imports) at the
zero version, for the group pin.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:05:12 +01:00
jschoubben 5385d6844d Join item 4: the indexer serves indexer_read_seat_definitions, the checker's seventh first view
The checker invokes it (A2j 5700edf) and the indexer said the state on its views family without a read tool for it.
Row in TestManifestDeclarations: every indexer tool a module of this repository invokes is one the indexer serves
(failed first, naming the checker's invoke).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:04:34 +01:00
jschoubben e648955a94 Join item 3: the indexer's empty manifest-accepted takes its id from the event (EventID names module and revision)
delivery.ManifestAccepted.EventID now names, for an event with no digest, the module and the event's revision (J3,
ruling A5(d)), so the indexer drops its own left id. The revision is the modules state's revision of the value that left
(write hands the revision it read), so two leaves of one module are two events; asked again by hand for a module no
longer held it is 0, and the again suffix keeps it unique. Row in TestAMergeThatRemovesAModuleDeletesItsKey (failed
first: Revision 0, id naming the last digest).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:03:35 +01:00
jschoubben 5c4486d512 Join item 2, indexer: its views family and the assigner's by their manifests' names; its ask-again records a hand act
views.IndexerStream and views.AssignerStream are gone from mesh-sdk (J3 lead's (a)): the indexer names its own views
family "indexer-views" and the assigner's "assigner-views", as each manifest's `streams` names them. Its first-view
ask-again records the hand act through seat:mesh-hand-acts.hand-act (views.StatusTools takes a handacts.Recorder;
the manifest already invokes the verb). judge_test.go gofmt'd (c6c6687 left it unformatted).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:01:42 +01:00
jschoubben 1cd0d83f41 Join: merge A3's builder and L3 (wip/registry-a3 88a9d19)
L3 (710c4032) arrives with it. Conflicts, each kept from both sides:
- assigner module.json, manifest_test.go, main.go, tools.go, integration_test.go: A2's first-view tools,
  read tools and states beside L3's assigner_engine_reads and engine-reads; the two "consumes" lists
  joined into one (the indexer's views and node-engine.manifest-reads-changed).
- builder module.json and manifest_test.go: A3's tools, states, consumes and invokes beside A2g's first
  views (DLV-M1); one findings state (A3's, history 5) carries both; reads keep A3o's
  mesh-delivery-indexer.packages until the builder follows it (join item).
- builder cmd main.go: rebuilt from A3's with A2g's changes applied (the reader follows modules,
  manifests, candidates and placement; one calls connection; one findings bucket, A3's
  health.FindingsBucket; FollowPlacement on a goroutine once placement's first view is answered;
  buildFromModules in place of the start's settle). Tools main.go: A3's tools then the first-view tools.
- builder run.go: A2k's out-of-step on the end entry, then A3's numbering of a done build.
- view module.json and manifest_test.go: node_manifest_reads and seat:node-engine.manifest-reads beside
  A2j's first views and read tools; one invokes list.
- indexer judge rules_test.go: L3's manifest-reads verb and event, without pull-waits (J1 dropped it).
- sdk/go.mod and builder go.mod: both requires kept.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:58:45 +01:00
jschoubben 84a5094684 Join: merge A5's grants part, keeper and watchman consumers (wip/registry-a5 d53986f)
Conflict: modules/mesh-delivery-watchman/manifest_test.go, both sides asserting m.Consumes,
and both adding a Consumes field to the test's manifest struct (kept once).
Git also auto-merged module.json into two "consumes" keys (A2g's first views and A5's
manifest-accepted and mesh-broker events): joined into one list, and the test asserts all five.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:51:50 +01:00
jschoubben 88a9d19b4f DLV-B2: a package is numbered under its name in its ecosystem, read from the indexer's packages state
The builder numbers, asks the floor of, records and tags a package under the name the indexer's series key names for
its <module>/<artifact> (a Go module path, an npm package name; design 32), so builder_versions answers the holder's
question by the name a registry seat publishes it under. A package the state names nothing for, or a state not readable,
is built unnumbered and said; the rest of the claim is numbered.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:51:27 +01:00
jschoubben d266a4c839 Join: merge A2g's checker, view, watchman, builder, teller and enrolment readers (wip/registry-a2g aafd90b)
No conflicts.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:48:00 +01:00
jschoubben 3caa8291b0 DLV-B9: a bundle answered in the registry seat's form keeps the artifact-store form it derives for each counted engine that does not read it
The builder's forms gain seat-reference for bundles and archives (oci-artifact-registry/<module>/<artifact>@<digest>);
the old-form window writes the older artifact-store reference beside it (catalogue.InArtifactStore), the oldest kept
in the single reference (ADR 0624 §1, §3), so mesh-build-go's seat-form answer no longer strands an engine stating nothing.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:45:00 +01:00
jschoubben 8d7e8d3cb9 DLV-I6 ruling: a package is named in its own ecosystem, and its series key with it
At each trunk merge the indexer reads every package artifact's name from the merge commit (a Go package's go.mod module
path, a TypeScript package's package.json name) and writes it into the series key as name, beside the module and artifact
it is built from, so the builder numbers it under that name before its first version. Series keys, accepted series and
indexer_package_users go by the name (the lead's design limit for design 32, replacing the builder-name fallback).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:44:38 +01:00
jschoubben aafd90b8cf DLV-M5r1: the watchman's want time is the picture's written time once the join's views name it
The lead's ruling on A2h's finding 1: J3 builds each first-view key's written time into mesh-sdk views
(Answer.Written, a Picture entry's Created) and J2 the tool runner's state reads' at. The watchman and
the builder's toolchain gate already read the entry's Created(), so neither changes; this says so where
the fallback stands.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:42:26 +01:00
jschoubben 5700edf955 DLV-M2r1: the checker fails a pull request whose head's seat definition does not parse, naming the seat and where
The lead's ruling on A2i's finding 1 (design 26): the indexer keeps an unparsed definition of a pull
request's head as its entry of the state seat-definitions with `unparsed`, and cannot refuse the manifest.
The checker follows that state as its seventh first view (invokes indexer_read_seat_definitions, consumes
views.seat-definitions; checks start once it is answered too, as M2's six) and fails each pull request
with an entry holding `unparsed`, naming the seat, its module, its directory and the indexer's words; a
state that cannot be read fails it too. Rows: TestTheIndexersJudgeJudgesTheChangesManifests (fails
first without the refusal), the adapter's key match in TestThePublishedVersionsAreTheIndexersPackagesState
(fails first matching every key), the manifest's invokes and consumes; the integration bed serves the
state (without it every check waits and five tests fail).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:40:02 +01:00
jschoubben f7a6118e61 DLV-I19, I20 [i]: the indexer's whole-module test keeps a verdict's and a trunk build's out-of-step files and reads a pull request again by hand
Cases in TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage: the checker's verdict event keeps a pull request's
files, a trunk build end raises generated-types-out-of-step and a clean one clears it, as indexer_out_of_step reads them;
indexer_seat_definitions_again, asked through takeins, reads a pull request's head and indexer_seat_definition answers it.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:39:32 +01:00
jschoubben d15d66fc47 DLV-I20 (builder): a build's end names the generated files its build module found out of step
The builder reads out-of-step from each lane's step result, a failed step's too, and writes it on the build's end
entry (BuildLogEntry.OutOfStep, J3's mesh-sdk field), where the indexer raises generated-types-out-of-step for a trunk
(the lead's ruling on A2i's finding 2). BuildAnswer names no such field yet, so it is read by name and shape.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:35:59 +01:00
jschoubben 20269818c9 DLV-C5: gofmt of the checker's examiner test
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:32:46 +01:00
jschoubben 30ee47caa6 DLV-C5: the checker asks the seat mesh-config's verb renames for every change that brings or drops a renamed-from
Design 32 (A config value renamed): the checker invokes seat:mesh-config.renames, never the module. A
change bringing renamed-from to a holder that serves no renames (nothing answers the optional verb) is
refused naming it; dropping a renamed-from while its window is open is refused naming the nodes holding it
open; a check that cannot ask fails, saying so. The pull request's trunk and head manifests are read once
for this and for the series changes (manifestPair). Rows in TestTheIndexersJudgeJudgesTheChangesManifests
with the verb faked from the seat's definition (its fixture's answer): not served, served, open on g14,
closed, an ask that fails, no seat to ask; the four refusals failed first. Manifest row for the invokes.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:32:41 +01:00
jschoubben 1d31ef1e85 DLV-I17, I19 rulings: a seat has one trunk definition, and a pull request is read again with its head's manifest
A second repository's trunk defining a seat another trunk holds is refused and kept nowhere, with the finding
seat-defined-twice naming both repositories; indexer_seat_definitions_again for a pull request reads the head's own
generated-from, so a pull request changing that field is read right by hand too (the lead's rulings on A2i 3 and 4).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:31:39 +01:00
jschoubben f9e0e6544c DLV-F5r: the assigner answers its readers' first views before any new-form state, with no binding of the bus module
The case in the assigner's whole-module test serves the tools bundle's first-view tools as the runtime of its own does,
with the callers its manifest writes, while the assigner still waits on the indexer: the grants part, the tool runner and
the indexer each get theirs, every other account (the assigner's own included) is refused (ADR 0621 §1-§3).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:29:10 +01:00
jschoubben 890b8f36e9 DLV-C4: the checker names each change of a package series a pull request makes in its status line
Design 32 (A package, its series): the review of a change sees every series it raises. For each module a
pull request touches, every artifact its trunk manifest and its head's manifest both declare whose series
differs is named as "<module>/<artifact> series <trunk> → <head>" in the pull request's mesh/check line,
never dropped when the line is cut; a manifest that cannot be read is said in the same place. Rows in
TestTheIndexersJudgeJudgesTheChangesManifests (raised: named; kept: not named); the raised row failed
first. gofmt of the integration test's imports (M2).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:28:18 +01:00
jschoubben 8afd34525d DLV-M4, DLV-V1: the view reads the indexer's and the assigner's twelve states through their read tools, never their buckets
ADR 0615 §1, design 25 § 12.2: the manifest drops its twelve reads of the two owners' states and invokes
their twelve read tools. The process follows the six it watches (placement, wants, asks, candidates,
modules, taken) by the first view and then the views families (consumes), through firstview pictures, so
its watch code is unchanged; an owner that is down holds no other watch, and the unanswered first view is
the view's finding (states first-views and findings; tools view_first_views, view_first_view_again).
The tools keep their names, arguments and answers and ask the owners' read tools on each call
(view.OwnerRead; a read of many keys is one paged values read). DLV-V1: a table subtest answers each
tool through the owner's own Read over a fake state and matches what reading the state answered.

The owners' read tools answer no history of a key: indexer_module_history, indexer_heard and view_tiers'
times say so (an error, never none) until the read tool takes a history argument (a request to the join).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:24:17 +01:00
jschoubben 24dd2ec29a DLV-F5r (assigner's case): the indexer's runtime answering after the assigner asked is followed within the bound
A case of the assigner's existing whole-module test: the indexer's first views are held until the
assigner has started, and the wants are written from what they answer (ADR 0621 §3).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:22:31 +01:00
jschoubben b84db41198 DLV-F5r (indexer's case): the assigner's runtime answering after the indexer asked is followed within the bound
A case of the indexer's existing whole-module test: the assigner's first view is held until the
indexer has started, and only the placed node's toolchain counts once it answers (ADR 0621 §3).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:21:13 +01:00
jschoubben 598204b4c4 DLV-I17, I18: the indexer's whole-module test keeps a contracts module's definitions, a pull request's unparsed one, and drops it on close
Research 081 test 21 as a case of the indexer's existing whole-module test (no new test function:
mesh-delivery is over its test budget).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:20:01 +01:00
jschoubben aa647d0eda DLV-I17, I18, I19, I20: the indexer keeps every contracts module's seat definitions on the bus
Design 26 and ADR 0608 §2-§4: the definitions are read at each trunk merge and pull request head
through the forge's get-file tool, parsed by mesh-sdk seatdef and kept in the state seat-definitions
by seat and source; seat-definition-changed is said, kept seven days, only when a digest moved. A
trunk definition that does not parse leaves the trunk's entry and writes seat-definition-unparsed;
a pull request's entries go when it closes or merges. A trunk build end naming out-of-step files
writes generated-types-out-of-step, cleared only by a clean build of the same module; the checker's
verdict keeps a pull request's. Tools indexer_seat_definition, indexer_seat_definitions_again (a hand
act) and indexer_out_of_step.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:18:17 +01:00
jschoubben c6c66873f4 DLV-M6r5: the judge takes a claimed seat's module with tools of its own beside its seat's verbs
The lead's ruling 5: one row in the judge's table, the enrolment's shape (a claim serving the seat's
verbs and the first-view tools in its own tools list); the judge accepts it as it stands.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:14:37 +01:00
jschoubben a6fdb121fe DLV-M2: the checker follows the indexer's candidates, modules, manifests, packages and predefined keys and the assigner's placement by first views
ADR 0615 §1 and ADR 0596: the checker invokes the six read tools, consumes their views families and
reads none of the six buckets; this ends its direct read of the packages state (the lead's ruling 2).
It keeps first-views and findings and announces both as its own. The lead's ruling: an owner that is
down never holds the start; the checks, the tries and the hand asks start once every first view is
answered, the placement is followed into the toolchain gate once its first view is. firstview.Reader
opens a followed state's picture as a process.BucketOpener (the builder uses it too). Tools
checker_first_views and checker_first_view_again.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:13:28 +01:00
jschoubben 28dd86b451 DLV-M1: the builder follows the indexer's modules, manifests and candidates and the assigner's placement by their read tools' first views
ADR 0615 §1: the builder invokes indexer_read_modules, _manifests, _candidates and
assigner_read_placement, consumes their views families and reads none of the four buckets; it keeps
first-views and findings. The lead's ruling: an owner that is down never holds the start; the modules
are built once the modules', manifests' and candidates' first views are answered, the placement is
followed into the toolchain gate once its first view is, and until then the gate passes every report on
as before the placement settled. Tools builder_first_views and builder_first_view_again.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:09:22 +01:00
jschoubben 72d8e2155f DLV-M5: the watchman follows the assigner's wants and the indexer's modules by their read tools' first views, read once answered
ADR 0615 §1: the watchman invokes assigner_read_wants and indexer_read_modules, consumes their views
families and never reads either bucket; it keeps first-views and writes an unanswered first view as a
finding of its own. The lead's ruling: an owner that is down never holds the start; each state is read
on a goroutine once its first view is answered, and what it holds (seeded pulses, stuck keys, the
findings it is the source of) is reconciled then (watch.Awaits, ReadLate), never dropped at the start's
end. Tools watchman_first_views and watchman_first_view_again.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:06:04 +01:00
jschoubben ea6fbd31db DLV-I16: the judge refuses a contracts module claiming a seat, naming the seat
A module whose package carries generated-from holds contracts only (design 26).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:06:03 +01:00
jschoubben 2e5917535c DLV-I15: the judge refuses a change's renamed-from that the trunk's manifest does not allow
A rename moves a module's own value with its trust (design 32): the old name is a value the trunk's manifest of the
same module declares and the change does not, no secret and no key of the predefined list, and the old and new value
have the same trust, declared or by the predefined key they take. Judged for a change over the trunk; each refusal
names the module and the key.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:05:09 +01:00
jschoubben c22f9d25dc DLV-I14: the judge refuses a packages entry naming no declared package or an ill-formed series
Between the manifests (design 32): each name in a module's packages is a package some manifest declares, by the
builder's name <module>/<artifact>, and each accepted series is <major>.<minor>.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:02:57 +01:00
jschoubben 20a9322209 DLV-I13: the judge reads a package series, a package's language and the contracts marker
Design 32's words: a series is <major>.<minor> and only on the four kinds the builder numbers, a package is go or
typescript, generated-from only on a package. A package is published as its source, so it names no system: today's
judge refused mesh-sdk's Go package (no system) as a binary.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:01:38 +01:00
jschoubben 87ba69ed69 DLV-I12: the judge refuses a ledger type whose operations or indexes name what its schema lacks
Left by the bus-state review (ADR 0595 §8): a set-entry or remove names its into and key, an operation's fields and an
index's on name schema fields, and a digest-of names a text field; each refusal names module, entry, type, operation
or index, and the field.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:59:37 +01:00
jschoubben 7372447c68 DLV-I9, I9b, I9c: manifest-accepted for every accepted manifest and a module leaving the trunk, never lost silently
The bus module's orphans need to hear a module that asks nothing now (design 25 § 12.2): every newly accepted manifest
is said, its entries an empty list when it asks for none, and a module whose directory leaves the trunk gets an empty
one; a manifest set aside to wait gets none. A failed publish is published once more; one failing twice writes the
finding accepted-unsaid naming the module, said again at the next judging pass and cleared once said.
indexer_say_accepted says it again by hand from the held verdict, a hand act through mesh-hand-acts; it refuses a
module with no accepted verdict.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:58:02 +01:00
jschoubben 1ce394db97 DLV-M6: the enrolment follows the assigner's placement by its read tool's first view and its views family, never its state
ADR 0615 §1: views.Reader asks mesh-delivery-assigner.assigner_read_placement and hears views.placement;
the holder reads placement from the picture and every other state through the runtime. The manifest drops
the read, invokes the tool, consumes the event, keeps findings and serves enrolment_first_views and
enrolment_first_view_again.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:56:31 +01:00
jschoubben 28c3b27fec DLV-M3: the teller reads the assigner's asks by its read tool's first view and its views family, never its bucket
ADR 0615 §1: sdk/firstview is a process's picture of an owner's state, read as the bucket it
replaces (Get, Keys, Watch; every write refused), asked through <owner>.<short>_read_<state> with
values and kept from the owner's views family; firstviewtest serves an owner in a whole-module test.
The teller declares invokes assigner_read_asks, consumes views.asks, states first-views and findings,
and the tools teller_first_views and teller_first_view_again.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:54:50 +01:00
jschoubben 51829a6b14 DLV-I8: indexer_package_users lists each package's users and the series each accepts
So a user left on an older series is seen (design 32): per package its series with each one's newest published
version, and its trunk users with the series they accept, that series' newest and the version they are pinned to.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:51:43 +01:00
jschoubben eedd3271ce DLV-I6: a user accepting a package's series is fingerprinted against its newest published version, by that version's contents
A series' newest published Go package keeps its module's digests read at the version's commit; each module of another
repository whose series the user's manifest accepts enters the import form and the version line by them, pinned to the
version (Go's form). A series with no published version keeps the commit form; a group member keeps its head.
A publish that moves a series' newest recomputes at once.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:49:27 +01:00
jschoubben 1592abbf4f DLV-I5: the indexer keeps each package's series, its newest numbered and newest published trunk version
A user takes the newest of the series it accepts (ADR 0609 §1), so the
indexer learns versions from the builder's numbered and the registry seats'
published, one key <package>.<series> beside the per-module digests.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:42:33 +01:00
jschoubben ab502a7174 DLV-C3r1: a group's Go package version is keyed by the module path go-package-registry's published names
The seat's contract now carries the Go module path, so the checker no longer
works it out from the artifact's paths, which could disagree with it.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:39:08 +01:00
jschoubben 753ea6a067 DLV-C3r3: name the packed group package the old form, kept while the running checker hands it
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:36:14 +01:00
jschoubben 2a3ce39590 DLV-C2, DLV-C6 (verdict): the checker says each pull request's verdict on its stream asks, with the out-of-step files its steps named
Design 26 has the indexer learn out-of-step generated files from the check's
verdict event, so it runs no generator; the test step's answer carries them.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:35:51 +01:00
jschoubben 8c3916bd7b DLV-C3, DLV-C6 (tag-wanted): the checker's whole-module test hears tag-wanted at the definition's subject and builds on the published
A case of the existing group test (no new integration test): the bed's streams are the manifest's (checks with
preview.written, asks with verdict and tag-wanted at the mesh-delivery-checker definition's subjects) and the shared
events stream with the registry seats' published. A group whose mesh-sdk member has no preview yet is read at its head,
says tag-wanted whose body decodes strictly into the definition's type, is not judged before the Go seat's published,
and then builds mesh-delivery against v0.1.15-feat-x.1. The verdict half of DLV-C6 follows DLV-C2.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:28:44 +01:00
jschoubben 3a64fde7d9 DLV-B10: the builder's three narrations are build.said entries of its build log
Design 51 says the build log tells a version kept and why, a tag ask unanswered after five minutes,
and the old form's window closing; the process log alone was read by nobody. Each numbering decision
is now a build.said of its module and fingerprint, its why one of numbered, kept, waiting, unnumbered;
the tag ask and the window, no one build's, are said under the builder's own key, the ask's or the
window's key standing for the fingerprint. Needs mesh-sdk 7efb173 (build.said). Each failed first.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:26:15 +01:00
jschoubben 28ef5af2df DLV-B9 readers: the indexer and the view read the builder's log kinds through one helper of mesh-delivery's sdk
The two copies of IsBuildEntry move into sdk/buildlog, which both modules already require from the repository's own
sdk module (the lead's ruling 5 on A2c's finding 5). The existing rows in the indexer's registered test and the view's
history test cover it unchanged.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:24:17 +01:00
jschoubben c04fa5ff62 DLV-C3r3, C3r4: a group's check builds its members against the group's tagged versions
Design 51 *Tagged versions* and ADR 0607 §3, §5: once each package's published is heard, a Go member's ask names the
group's tagged version of each package another member publishes (Go's form, v0.1.15-feat-x.2), a package new in the
group included, and a TypeScript member's Node fetch takes {name, version} from the registry instead of a packed head.
The published is matched by seat, head and name, so one head with several Go packages (mesh-contracts) gets each its
own version. With no preview of the merge, a member's manifest is read at its head on the forge, so a package new in
the group is asked for on the first check too (the lead's ruling 4 on A2c's finding 4).

Needs mesh-sdk's PackageVersions.Check to take a Go semantic version (a2d-sdk-tagged-version.patch, for A1).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:23:10 +01:00
jschoubben 321c4e4122 DLV-B4, DLV-B6: the seats' outcomes and a branch's end are cases of the builder's whole-module test
On the real bus the artifact seat's removed (retention) and refused (another digest) move a numbered
version, and the forge bridge's merged asks the Go seat to remove the version tagged for the branch,
reason tag-gone, whose removed turns it removed. Each failed first with its hearing off. No new test
function: the builder holds 16 integration tests.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:23:02 +01:00
jschoubben fb4fc8a602 DLV-B10: design 51's round-2 builder cases are cases of the builder's whole-module test
The bed holds the builder's versions, findings, floor-waits and reasks, EVENTS with the artifact
seat's outcomes, and the floor's three sources served on the bus. Two merges close together number
the second at once; builder_publish_again after published takes a new number; a step bound running
out raises build-failed, and the fingerprint built again keeps its number, said with why, and clears
it. Each case failed first with its behaviour off. The kept version is said in the process log until
the build log takes build.said.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:20:49 +01:00
jschoubben d53986f1f6 DLV-F5r: the grants part started before its owners answer waits, says so, and composes nothing from them
A case of the grants part's whole-module test (ADR 0615 §6, ADR 0621 §3): each first view refused once, the part's log
names the indexer's and the assigner's tools it waits on, no membership is written; once the owners answer it composes
as before.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:12:29 +01:00
jschoubben 6c94ff75e2 DLV-B8: the tag ask's narration is a case of the builder's whole-module test, and two instances together joins the archive table
The builder's integration tests fall from 17 to 16: the two-instance test is a row of the archive table; the bed holds the
tag asks and their key-ttl bounds and the builder's process log.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:11:57 +01:00
jschoubben d2e7754816 DLV-B6: a tag ask is marked numbered with its versions, and one unanswered is narrated when its bound ends on the bus
A check's build of the ask's head writes the ask numbered in tag-asks by compare-and-set. An unanswered ask has a key in the
new key-ttl state tag-ask-bounds (five minutes); the bus's marker of its end, heard on the watch, narrates the ask with what
it waits on (the check's build not claimed, names waiting on their floor) and writes the bound again; no timer (design 51
*Tagged versions*). A restart arms each unanswered ask whose bound ended meanwhile.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:11:57 +01:00
jschoubben 8e60a3e078 DLV-C3 follow: the builder hears tag-wanted on the checker's own stream, mesh.stream.<checker>.event.tag-wanted
The checker's family asks is its own stream, whose subjects are mesh.stream.<module>.event.* (ruled on A2c's finding 1);
its verdicts on the same stream are acknowledged and not taken as tag asks.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:11:57 +01:00
jschoubben e20eb05a9b DLV-J4: the judge refuses keeps on a tools bundle's bus, the bus module's included (ADR 0626 §1, §3)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:08:15 +01:00
jschoubben 273da7535b DLV-R1: a verdict below the picture's revision asks the verdicts' first view again, unless the picture holds it
ADR 0615 §5: a gap in revisions asks the first view once more. The indexer's verdict events carry no mark of when its
verdicts were made, so verdicts made again show only as revisions starting low, which the grants part passed over as
already held. A redelivery of a verdict the picture holds for its manifest is still passed over, so a consumer's backlog
after a restart asks nothing.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:07:57 +01:00
jschoubben b3552eafd6 DLV-C3: tag-wanted is the mesh-delivery-checker definition's holder event, branch included
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:07:35 +01:00
jschoubben 606e2b9212 DLV-B9 readers: the indexer and the view pass over build-log entries that are no build's start or end
The builder writes design 51's old-form close into its build log; the indexer's Builds.Put refused it
(no repository) and the view's history stopped on it. Both read the entry's event first and skip any other
kind, whatever its form.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:06:57 +01:00
jschoubben 8a78e00bb0 DLV-G1, DLV-W1: the grants part and the watchman hear mesh-broker's events on the seat's subject; the keeper is granted them
The lead's ruling of 13:0x (registry milestone): mesh-broker's events are mesh.seat.mesh-broker.event.<event> on the
shared events stream like every seat's. The grants part and the watchman consume mesh-broker.<event> and no longer hear
the bus module's own stream, which is its history for its tool. The keeper's process account is composed the seat's
event subjects, and the acknowledgement of a snapshot's chunks of each entry it keeps, which ADR 0626 §1 grants and the
composition lacked; a row holds it to no subscription of a restricted stream's subjects and no consumer of one.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:05:27 +01:00
jschoubben 5865d3c632 DLV-C3: a group's check says tag-wanted for each package its members declare and builds no member before its published
The checker's family asks (stream asks, seven days, in the old streams section) carries tag-wanted
{repository, branch, head, module, package, pull}; the tag is left to the builder, which makes it from the
branch. Each package's published is heard from the two package registry seats on the checker's durable
consumer of the shared events stream; none within the ten-minute bound fails the check naming the package,
before any member is built. A lone pull request and a try ask nothing (design 51 *Tagged versions*).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:03:42 +01:00
jschoubben 5fbd84091e DLV-B6: a check's build of a head a kept tag ask names numbers its packages tagged at its claim
The builder reads tag-asks for the candidate's repository and head (versions.TagFor), numbers only its packages under
the tag below the next version, keeps each with its repository for the branch's end, and says numbered with the head;
the same fingerprint claimed again keeps its number (design 51 *Tagged versions*).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:00:17 +01:00
jschoubben 0dc0cf9c9a DLV-B9: the old reference form's window over sdk/forms, kept in old-form, read and dropped by builder_old_form
The window opens while a counted node-engine reads, of some kind, none of the newest form sdk/forms knows, and closes
when none does: said once as version.old-form-closed, and a node holding it seven days is old-form-held about it. It
steps on a statement's change, the placement, a hand drop and each build key written, never on a timer. A retired node
dropped by hand leaves the count of the forms a build key keeps (design 51). internal/versions' own Forms goes: which
forms a key keeps is sdk/forms'.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:55:45 +01:00
jschoubben 20a4a79993 DLV-G2: a row that no tool runner, no tools bundle of the bus module and no account invoking every tool holds the bus's create, update, snapshot or delete subjects of a module's bus state (ADR 0626 §4, How it is checked)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:50:18 +01:00
jschoubben 5c8d59b6c7 Merge lead-47's L3 mesh-delivery head 710c4032 into the registry builder
The builder keeps both: the registry's versions, floor, tag asks and findings, and ADR 0618's engine-reads state,
its tool builder_engine_reads, the node-engine seat's manifest-reads verb and manifest-reads-changed event.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:48:42 +01:00
jschoubben cb1cf7fd73 DLV-R1: the grants part follows the verdicts on the indexer's manifest-accepted and manifest-refused at the revision each carries, not on its views family (ADR 0622 §2)
The next revision changes the verdicts' picture with no ask; one already held is passed over; an
event carrying no revision, past the next, or naming no manifest above the picture asks the first
view once more, said in the log. The manifest consumes the two events in place of views.verdicts.
Needs mesh-sdk 3ff694e (the events' Revision) and the indexer setting it (A2b).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:48:30 +01:00
jschoubben 0811861ede DLV-I7: the requirements state and indexer_requirements, what consumers' go.sum files require of each mesh Go module
Filled at each merge from the go.sum the indexer reads (the lead's ruling on I7), read once per consumer per process
even where its hash is unchanged; the builder's floor asks {package} and reads {requirements: [...]}, highest last.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:48:02 +01:00
jschoubben 27e3bb266f DLV-I11: the manifest test counts indexer_bus_state
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:45:04 +01:00
jschoubben b5a88fae9d DLV-I11: indexer_bus_state, the bus module's keeper's first view of every trunk module's accepted bus state
Answers {revision, modules:[{module, digest, revision, bus-state}]} from the indexer's own states, at the verdicts
state's mark, as A5's keeper reads it.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:44:46 +01:00
jschoubben e612fbebe7 DLV-B3: a name waiting on its floor is the builder's finding floor-silent, cleared once all three sources answer
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:44:18 +01:00
jschoubben 627cb0c0c9 DLV-G2: the bus module's keeper is the process whose bus declares keeps, and only it is composed the create, update, snapshot and delete of every module's bus state (ADR 0626 §1, §3)
A process of the bus module without keeps, or keeps on any other module, is composed nothing
of it. mesh-sdk's ProcessBus.Keeps is asked of A1 (a5-sdk-keeps.patch).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:44:17 +01:00
jschoubben 2178db5a1d DLV-B7: a failed trunk build is the builder's finding build-failed, cleared by a later done build
Read from the builder's own build log entries at their end; a candidate's
build is the check's to say.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:43:51 +01:00
jschoubben 710c4032d2 The checker hands the composer the node-engine's input encoded again from the seat contract's object
mesh-sdk's compose-candidate answer is now the node-engine seat's generated type, whose input is a JSON object; the
composer's request takes bytes, so the checker encodes it again, its numbers kept as written (json.Number).

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 12:43:11 +01:00
jschoubben bbe6ac6ef5 DLV-B6: a branch's end removes its tagged versions, acknowledged once every seat said removed; the checker's tag-wanted is kept in tag-asks
The builder hears the forge bridge's stream on a durable consumer: merged, a
pull's closed and branch-deleted ask remove of each tagged version still
held, and the bus gives the message again each minute until all are removed;
past 15 minutes its finding tag-removal-not-done names the seats. The builder
gains the state findings (ADR 0564 §2). Every forge message also wakes a floor
whose bridge_tags was silent.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:42:57 +01:00
jschoubben f271a70659 DLV-F4r: the grants part takes the views follower from mesh-sdk (go/views); the held-seat rows use a seat whose verbs its holder declares
mesh-broker's verbs are all optional now and the composition grants its reading verbs to its
holder, so taking one from a membership loses no holder: the rows that prove a lost last holder
is held back name a seat the catalogue defines without verbs, claimed by the bus module beside
mesh-broker.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:41:36 +01:00
jschoubben 7fd07d0236 DLV-R1: the judge's seat-set test reads mesh-contracts' seat list: fifty-nine seats, the registry seats beside the artifact store, pull-waits and renames
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:41:30 +01:00
jschoubben a7d490b0e5 DLV-J4: the judge allows the bus module's keeper its own process account, and keeps on it alone (ADR 0626 §1, §3)
The new nats manifest has a keeper process with a bus account of its own; the judge refused it under ADR 0235 §8.
Needs mesh-sdk's ProcessBus to carry keeps (requested of A1); until then the strict decoder refuses the key.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:40:49 +01:00
jschoubben a70b9a0951 DLV-B4: the builder hears the four registry seats' published, refused and removed, and keeps its floor waits in the state floor-waits
Each outcome is a seat event <seat>.<event> on the shared events stream (the
lead's ruling of 12:4x), heard on the builder's events consumer through its
consumes; the subjects are mesh-contracts'. A restarted builder takes its
floor waits in again, so its silent sources' events still wake them;
builder_floor_waits reads them.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:38:15 +01:00
jschoubben 3a789b7a6b DLV-O4: manifest-accepted and manifest-refused carry the verdicts state's revision of the verdict they say (ADR 0622 §2)
The grants part follows verdicts by these events after its first view, so each must carry the revision it is at.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:37:33 +01:00
jschoubben 72da51a10a The node-engine seat's manifest-reads answer and event come from mesh-contracts' nodeengine contract, judged by their reader (ADR 0618 §3, §4, issue 774)
The contract carries no checks, so sdk/enginereads judges the layout's version, the node, each capability, the reason and the times. The local replaces of mesh-sdk are gone: the lead pins mesh-sdk and mesh-contracts at the join.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 12:36:48 +01:00
jschoubben b109d09b84 DLV-F3r: the indexer and the assigner take the views follower from mesh-sdk go/views, and mesh-delivery's sdk/views goes
One copy of the reader's rule for every reader, kept where shared code lives (mesh-sdk), as the lead ruled.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:36:00 +01:00
jschoubben d78161f579 DLV-T1: the watchman's three integration tests are one whole-module test with two subtests, so mesh-delivery's integration count falls by two (test budget)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:50 +01:00
jschoubben 2cb19c6415 DLV-F4r, DLV-R1: the grants part reads placement, wants, manifests, modules and verdicts by their owners' first views, then their views families; an answer empty at revision 0 drops no login (ADR 0615 §4 to §6, ADR 0621, ADR 0622)
Each first view is asked at the start until answered, past two minutes raising
first-view-unanswered naming the owner and the tool; an empty answer at revision 0 reads as
a failed read, so the user list stays in force, and first-view-empty names the operator's
step until an answer above 0. Each owner's views event applies one change with no ask; a gap
asks once more. The reads of the five buckets leave the manifest, and the tools ask the
first views too. Tools grants_first_views and grants_first_view_again (a hand act).

The verdicts are followed on the indexer's views family (views.verdicts), not on
manifest-accepted and manifest-refused as ADR 0622 §2 words it: those carry no revision.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:50 +01:00
jschoubben 56891abe20 TLS-5 grants: the tool runner's consumes of the owners' views get their durable consumers and its findings state its grant, as any manifest's (A7's request; ADR 0615 §5)
Nothing in the composition changes for it: the consumers on module streams are made since
DLV-G1. The row proves it on the tool runner's own manifest.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:50 +01:00
jschoubben 5b68106662 DLV-G2: the bus module's process serves nothing, as every process account (ADR 0436 §3): it hears its module's consumes on a consumer of its own, says its stream's events and raises its conditions; the seat's reading verbs stay its module's tools'
The keeper is asked by a person through its own state requests (CAT-N8); the acting verbs
are served by nobody on the bus and denied to every principal whose grant reaches them. The
bus module's tools bundle may read its module's own stream back (nats_history).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:50 +01:00
jschoubben 745c30e068 DLV-G1, DLV-W1: read the bus module's events by the mesh-broker contract's field names, on its own stream or the seat's subject
The seat's definition (mesh-contracts meshbroker) names local-name and storage-type and puts
the events on mesh.seat.mesh-broker.event.<event>; design 25 § 12.3 keeps them on the bus
module's own stream. Both are read until that is ruled.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:50 +01:00
jschoubben fc150986b7 DLV-W1: the watchman says an accepted entry of bus state the bus module said nothing of in five minutes, and a refused one, until created or no longer asked (ADR 0595 §4, design 25 § 12.2)
It consumes the indexer's manifest-accepted and the bus module's created and refused on its
durable consumers. The five-minute bound is a row of its signals table (a pulse key whose
expiry is the finding, read back at a restart), not a new state: the prepare's state
overdue would have kept the same keys with no expiry to act on.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:50 +01:00
jschoubben 8f6562368c DLV-G2: the bus module's process gets its own account: it defines every module's bus state by its names and an orphan's it created, serves mesh-broker's eight verbs, and no other account holds the acting four (ADR 0595 §4, §5)
The grants are composed for the process of the module holding mesh-broker, from every
manifest's bus-state entries and the streams of the bindings kept; the writers table names
it beside the grants part as a definer of streams; the acting verbs are denied to every
principal whose grant reaches them, and neither the holder's own account nor the tool runner
serves the seat's verbs.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:50 +01:00
jschoubben 8975c37a0a ENG-11 grants: a node's engine hears the status view's settled, on which it retries a failed pull from a store's holder (design 51)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:50 +01:00
jschoubben 7cd5bfb31b DLV-G1: the grants part writes the bus module's binding into the module's memberships on created, rewrites it on recreated, removes it on dropped, kept in memberships (ADR 0595 §1, §7)
The part hears the bus module's own stream on its durable consumer there. That consumer did
not exist: since the controller ended (ADR 0590) nobody created a module's consumer on a
module stream it hears (AccountConsumers left them to the controller), so it is created now
with every account consumer, and a pass that created consumers wakes one more pass so the
part's own hearing opens at once. Bindings are kept per module under bound.<module>.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:50 +01:00
jschoubben e33cf8459b DLV-G3: the grants part raises no history-full; fill is the bus module's, and the old key is cleared once at start (ADR 0595 §6, design 25 § 12.3)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:50 +01:00
jschoubben 6b3a679151 DLV-B5: builder_publish_again, builder_floor_again, builder_tag, builder_tag_asks and builder_untag, with the state tag-asks
A holder that failed leaves a version to publish again by hand: said numbered again while the build is held, built
again under the same number when its file is gone, numbered anew after a published; refused for other bytes. The
floor is asked again by hand; a branch's tagged versions are removed through each seat's remove, per repository.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:30:20 +01:00
jschoubben e41666d846 mesh-delivery-view: node_manifest_reads lists what each node's engine reads by asking the node-engine seat's verb of every node at once, each bounded, naming a node not served or away with its record's reads (ADR 0618 §3, issue 774)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 12:26:48 +01:00
jschoubben ad77815d88 mesh-delivery-builder: a done key keeps each reference form a counted node's engine reads, the oldest in its single reference, and names its needs; what each counted node reads is asked through the node-engine seat's verb and kept in the state engine-reads (ADR 0618 §1, §2, §4, ADR 0624 §1, §3, issue 774)
A counted node is one the placement names; a node with no statement reads as the baseline's first forms, and no node record is read in its place. builder_engine_reads lists, asks again and drops.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 12:24:21 +01:00
jschoubben b873b9af4b DLV-I10: indexer_config_readers; and indexer_read_findings with its views event
indexer_config_readers {module} answers {readers: [{module, key, kind,
readers}]} for every value key and own secret any kept manifest of the
module ever declared: the module where its trunk's manifest declares it,
each open pull request whose head declares it, none for a key no longer
read (design 32 *A value no longer read*). It reads one watched copy of
the modules, manifests and config-candidates states. Nodes is left out:
the indexer cannot say which nodes' builds read a key. The indexer's
findings are said on its views family and read by indexer_read_findings,
for the condition keeper (the lead's ruling).

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00
jschoubben d662c63db5 DLV-J1, DLV-J2, DLV-J3: the foundation is the nine parts, and indexer_old_sections counts the reads left on the owners' state
The judge's foundation holds exactly the nine parts of ADR 0619 §1 and §3,
left out of the old sections' count, and refuses a foundation manifest
asking bus state in the new form. The indexer also counts, across the
accepted manifests, the reads entries still declared on the indexer's and
the assigner's state, by owner and by entry; indexer_old_sections answers
both, so design 25 § 12.7's live row (0 on each owner) has its tool.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00
jschoubben 3e241b3034 gofmt the assigner
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00
jschoubben 51eedd93a1 DLV-F2r, DLV-F3r (assigner): the assigner reads the indexer's manifests and modules through its first views
The assigner no longer reads the indexer's manifests and modules buckets
(ADR 0615 §1): it asks mesh-delivery-indexer.manifests and .modules at its
start, its first pass waiting for both answers as it waits for every watch,
then follows indexer-views on its durable consumer, asking once more on a
gap; past the bound its finding names the indexer and the tool. Its status
is in its state first-views, read by assigner_first_views and asked again
by hand with assigner_first_view_again. The whole-module tests run against
an indexer owner built from the views emitter and first-view answer.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00
jschoubben 7e77074fec sdk/views: gofmt
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00
jschoubben ccfca79c90 DLV-F1r, DLV-F3r (indexer): the indexer reads the placement through the assigner's first view
The indexer no longer reads the assigner's placement bucket (ADR 0615 §1):
it asks mesh-delivery-assigner.placement at its start, waiting at most the
two-minute bound so a normal start computes the fingerprints once, then
follows assigner-views on its durable consumer, asking once more on a gap.
Past the bound it raises its finding naming the assigner and placement,
cleared by the answer. Its status is kept in its state first-views, read
by indexer_first_views; indexer_first_view_again asks again by hand.
sdk/views gains the event's write time, the status hook, and the status
state and tools a reader's bundle serves.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00
jschoubben 1d7124e388 DLV-FB1 (indexer's side): a FORGE message of a kind the indexer does not take is acknowledged and passed over
The forge bridge now says a branch's end as branch-deleted (design 51); the
indexer terminated it as unreadable. It acknowledges it, writes it to heard
as ignored, and the two handle tests are one table (mesh-delivery is over
its unit budget).

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00
jschoubben 9d79379c5f DLV-O5: the assigner announces its marks state; its tests' placement stream takes only the seat's four events
Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00
jschoubben 249a9333aa DLV-O2, DLV-O3, DLV-O5: the assigner answers its first views and says its views family
The assigner's bundle runs in a runtime of its own on its own account and
serves the first-view tools placement, seat-holders and wants, each answering
only the callers its manifest writes (ADR 0621 §1-§2), and the six read tools
of design 25 § 12.2. The process says each change of those six states on the
family assigner-views beside placement in its old streams section (ADR 0619
§2, ADR 0615 §5); a waiter on put never wakes on it.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00
jschoubben 5d7fff91ce DLV-O1, DLV-O3, DLV-O4: the indexer answers its first views and says its views family
The indexer's tools bundle runs in a runtime of its own on its own account
(runs-as) and serves the first-view tools manifests, modules and verdicts,
each answering only the callers its manifest writes (ADR 0621 §1-§2, ADR
0622 §1), and the nine read tools of design 25 § 12.2. The process says each
change of its ten read states on the family indexer-views in its old streams
section (7 days, 256 MiB), with a mark per state the first view answers its
revision from, so a reader that takes the answer misses no event after it
(ADR 0615 §4-§5). The shared shape lives in sdk/views: event, answer, mark,
the emitter, the tools and the reader's follower.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00
jschoubben 5aa7bc7d49 mesh-delivery-assigner: what each node's engine reads comes from the node-engine seat's verb, asked at its start, on manifest-reads-changed and when a node is back online; a build's reference forms hold a want only when the node reads none (ADR 0618 §1, §3, §4, ADR 0624 §2, issue 774)
Where the engine does not answer the verb, the node's record is read once in its place, said in the journal (the fallback until every counted engine serves the verb). Kept in the state engine-reads, which assigner_engine_reads lists and asks again.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 12:19:34 +01:00
jschoubben e68a47886f sdk: the reference forms a build keeps and holds by, and the keeper of what each node's engine reads, shared by the assigner, the builder and the view (ADR 0618, ADR 0624, issue 774)
Logic only mesh-delivery's parts share, so it is mesh-delivery's and not mesh-sdk's (ADR 0593 §1, ADR 0608 §5). Tested through the assigner's and the builder's rows (the test budget). mesh-sdk is pinned by a local replace to the group's head while developing.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 12:19:34 +01:00
jschoubben a8bcd2abb9 DLV-B3b, DLV-B4 logic: a floor wakes on manifest-accepted, condition-cleared and the next source revision; the seats' outcomes move a version
The builder's one consumer of EVENTS now consumes the indexer's manifest-accepted and
the condition keeper's condition-cleared; a heard source revision stands for the
forge's merge. Numberer.Heard marks a version published, refused or removed and wakes
a floor whose seat was silent; its consumer of each <seat>.events waits on the seat
family's stream form (asked of A1).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:18:00 +01:00
jschoubben 688f6c21fa DLV-B2, DLV-B3 wiring: number each trunk build before its claim, record its digest, ask the floor as the builder's own account
A package whose floor is not known waits and nothing is claimed; an image, bundle or
archive is built unnumbered. The state versions is bound when the bus holds it, every
new number is said as version.numbered, and the manifest invokes the floor's three
sources (design 51 The versions, The floor; ADR 0606, ADR 0617 §1).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:11:53 +01:00
jschoubben ea193186c5 mesh-delivery-indexer: its judge knows the node-engine's manifest-reads and manifest-reads-changed (ADR 0618 §3, §4, issue 774)
mesh-sdk is pinned by a local replace to the group's head while developing.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 12:06:58 +01:00
jschoubben 4d94b0b9e4 mesh-grants: a node seat's event that names its holder's machine is heard from every machine, by the one rule its holder is granted it by (ADR 0618 §4, issue 774)
A watch of the node-engine's manifest-reads-changed subscribed the flat subject, which no engine says, so its consumer would hear nothing. mesh-sdk is pinned by a local replace to the group's head while developing.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 12:05:58 +01:00
jschoubben bd13302ecf DLV-FB1: the forge bridge says a branch's end as branch-deleted and answers bridge_tags
The bridge consumes the forge module's forge.delete and writes a branch-deleted message on FORGE for a deleted
branch (never the trunk, never a tag), so the builder removes the branch's tagged versions (design 51 *The forge
bridge says a branch's end*). Only the forge module's account may say forge.delete: the bus refuses it from
another (a case of the bridge's whole-module test). bridge_tags, served by a new tools bundle, answers a
package's git tags by prefix through the forge module's gitea_api, for the builder's floor.

One unit test merged into another, so the tags test adds no function (test budget).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:44:53 +01:00
jschoubben 2274155766 DLV-B3, DLV-B3b, DLV-B9: the floor's three sources and what wakes a silent one; the old reference form's window
The floor asks the seat's versions, bridge_tags and indexer_requirements; a silent source is named in the
floor finding and woken by an event of that source (the holder's published, the forge bridge's next message, the
indexer's next manifest-accepted) or its module's condition clearing, or by hand (builder_floor_again), never by
a timer (design 51 *The floor*, ADR 0617 §1). The old form's window (ADR 0624 §1-§3, ADR 0618 §2): each counted
engine gets the newest form it reads among those written, the new form only once every present engine states
it, the oldest kept in the single reference; a node not serving manifest-reads counts as the baseline; a node
dropped by hand counts no more; old-form-closed is said once, and a node holding it seven days is a finding.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:41:05 +01:00
jschoubben 64b918c64c DLV-B1, DLV-B2: the builder's state versions, numbering under compare-and-set per fingerprint or source revision, and builder_versions
The builder keeps one key per name, tag and version (design 51 *The versions*, ADR 0606), in its old state
section (ADR 0619), history one, 64 MiB with the 80 % mark. A package is numbered per source revision and keeps a
version held published, publishing nothing; one numbered but never stored is published again under it. An image,
bundle or archive keeps its fingerprint's number. An unknown floor builds an image, bundle or archive unnumbered and
makes a package wait (ADR 0617 §1). Merge order and tagged versions below the next version are the package's
helpers. builder_versions answers a name's versions and the holder's question (ADR 0605 §4).

Four claim tests merged into two, so the new table test adds no function (test budget).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:39:17 +01:00
726 changed files with 63842 additions and 11167 deletions
@@ -0,0 +1,63 @@
// Package containerimageregistryclient is the container-image-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51
// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part,
// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a
// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is
// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the
// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport.
package containerimageregistryclient
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry"
"git.novox.be/novox/mesh-delivery/sdk/parts"
)
// Call asks one verb of the seat, `seat:<seat>.<verb>`, with a body, and answers its JSON.
type Call func(key string, body any) (json.RawMessage, error)
// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were
// sent, and the chunks they took.
type Sent struct {
Transfer string
parts.Sent
}
// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in
// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes
// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read.
// Its outcome, published or refused, is the holder's event, which the caller waits for.
func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) {
open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) {
in.Parts = make([]seat.Part, 0, len(ps))
for _, p := range ps {
in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size})
}
raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in)
if err != nil {
return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err)
}
var a seat.PublishAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err)
}
return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil
}
chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error {
raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part,
Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)})
if err != nil {
return fmt.Errorf("%s's publish-part: %w", seat.Seat, err)
}
var a seat.PublishPartAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err)
}
return nil
}
sent, err := parts.Send(ctx, open, chunk, ps)
return Sent{Transfer: in.Transfer, Sent: sent}, err
}
@@ -0,0 +1,57 @@
package containerimageregistryclient
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry"
"git.novox.be/novox/mesh-delivery/sdk/parts"
)
// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the
// holder lacks travels by publish-part, its bytes whole, the last chunk marked.
func TestPublishTalksToItsOwnSeat(t *testing.T) {
held, lacked := []byte("held"), []byte("lacked")
part := func(b []byte) parts.Part {
sum := sha256.Sum256(b)
return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)),
Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }}
}
ps := []parts.Part{part(held), part(lacked)}
var asked []string
var got []byte
call := func(key string, body any) (json.RawMessage, error) {
asked = append(asked, key)
raw, _ := json.Marshal(body)
switch key {
case "seat:container-image-registry.publish":
var in seat.PublishInput
_ = json.Unmarshal(raw, &in)
if len(in.Parts) != 2 {
t.Errorf("publish names %d parts, want 2", len(in.Parts))
}
return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}})
case "seat:container-image-registry.publish-part":
var in seat.PublishPartInput
_ = json.Unmarshal(raw, &in)
data, _ := base64.StdEncoding.DecodeString(in.Data)
if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" {
t.Errorf("publish-part %+v", in)
}
got = append(got, data...)
return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part})
}
t.Fatalf("asked %s", key)
return nil, nil
}
sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps)
if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 {
t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err)
}
}
@@ -0,0 +1,18 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
)
require (
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
@@ -0,0 +1,12 @@
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -0,0 +1,116 @@
// Package reference holds the form of an artifact's reference once a registry seat published it: `<seat>/<module>/<artifact>@<digest>`
// (novox/hq ADR 0605 §9), and the older `artifact-store://<path>` form read beside it. It is the rule the holders and callers of the two
// registry seats that front the artifact store share (the seats' client, novox/hq ADR 0625 §4, ADR 0634 §2); it names no seat itself (ADR 0634 §4, ADR 0635 §4): the
// caller gives the seats' names from their contracts (Seats), so a package here cannot drift from a contract.
package reference
import "strings"
// ArtifactStoreScheme marks a reference to something in the mesh's artifact store, kept without the store's address.
const ArtifactStoreScheme = "artifact-store://"
// UpstreamPrefix opens the holder's own name for an upstream copy it keeps: `upstream/<host>/<path>@<digest>`, after
// `<image seat>/` in the seat's form and after `artifact-store://` in the older one.
const UpstreamPrefix = "upstream/"
// Seats are the names of the two registry seats a reference of the registry form names first: Image the seat that fronts
// images and upstream copies (container-image-registry), Object the seat that fronts bundles and archives
// (oci-artifact-registry). The caller reads them from the seats' contracts.
type Seats struct{ Image, Object string }
// All is the two names.
func (s Seats) All() []string { return []string{s.Image, s.Object} }
// Recorded is a reference as the mesh records it: the artifact store's address, if the builder wrote one, taken off.
func (s Seats) Recorded(reference string) string {
if _, _, seat := s.InRegistrySeat(reference); seat || strings.HasPrefix(reference, ArtifactStoreScheme) {
return reference
}
if rest, isURL := strings.CutPrefix(reference, "http://"); isURL {
if _, path, ok := strings.Cut(rest, "/v2/"); ok && strings.Contains(path, "/blobs/") {
return ArtifactStoreScheme + path
}
return reference
}
host, path, ok := strings.Cut(reference, "/")
if !ok || !isRegistryHost(host) || !strings.Contains(path, "@sha256:") {
return reference
}
return ArtifactStoreScheme + path
}
// isRegistryHost is the runtime's own rule for reading the first component of a reference as a registry rather than a
// namespace: it has a dot or a port in it, or it is localhost.
func isRegistryHost(component string) bool {
return component == "localhost" || strings.ContainsAny(component, ".:")
}
// InArtifactStore reports whether a reference is a kept one, and what it names there: `<module>/<artifact>@<digest>` for an
// image, `<module>/<artifact>/blobs/<digest>` for a bundle or an archive. It reads both forms (ADR 0605 §9).
func (s Seats) InArtifactStore(reference string) (path string, kept bool) {
if seat, rest, ok := s.InRegistrySeat(reference); ok {
if seat == s.Object {
name, digest, _ := strings.Cut(rest, "@")
return name + "/blobs/" + digest, true
}
return rest, true
}
return strings.CutPrefix(reference, ArtifactStoreScheme)
}
// SeatReference is the reference a build names an image, bundle, archive or upstream copy by once it published it through
// a registry seat: `<seat>/<module>/<artifact>@sha256:<digest>`, as InRegistrySeat reads it back.
func SeatReference(seat, module, artifact, digest string) string {
return seat + "/" + module + "/" + artifact + "@" + digest
}
// InRegistrySeat reads a reference in the form a registry seat answers: the seat it came through and the rest. A seat's
// name holds no dot or port, so no image of an outside registry reads as one, and a kept `artifact-store://` reference is
// not one. It also reads the holder's own naming of an upstream copy through the image seat, whose rest InUpstream reads.
func (s Seats) InRegistrySeat(reference string) (seat, path string, ok bool) {
first, rest, found := strings.Cut(reference, "/")
if !found || !strings.Contains(rest, "@sha256:") {
return "", "", false
}
if _, _, upstream := InUpstream(rest); upstream && first == s.Image {
return first, rest, true
}
if strings.Count(rest, "/") != 1 {
return "", "", false
}
for _, name := range s.All() {
if first == name {
return name, rest, true
}
}
return "", "", false
}
// InUpstream reads a path in the holder's naming of an upstream copy, `upstream/<host>/<path>@sha256:<digest>`: the
// upstream registry host and the image's path there, with its digest.
func InUpstream(path string) (host, image string, ok bool) {
rest, found := strings.CutPrefix(path, UpstreamPrefix)
if !found {
return "", "", false
}
host, image, found = strings.Cut(rest, "/")
name, digest, pinned := strings.Cut(image, "@")
if !found || !isRegistryHost(host) || name == "" || strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") ||
strings.Contains(name, "//") || !pinned || !strings.HasPrefix(digest, "sha256:") || strings.Contains(digest, "/") {
return "", "", false
}
return host, image, true
}
// Routed is a kept reference as a machine fetches it, through the artifact store at `address` (host:port). A reference
// that is not a kept one is what it was.
func (s Seats) Routed(reference, address string) string {
path, kept := s.InArtifactStore(reference)
if !kept {
return reference
}
if strings.Contains(path, "/blobs/") {
return "http://" + address + "/v2/" + path
}
return address + "/" + path
}
@@ -0,0 +1,69 @@
package reference
import (
"strings"
"testing"
)
var digest = "sha256:" + strings.Repeat("d", 64)
var seats = Seats{Image: "container-image-registry", Object: "object-registry"}
func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) {
cases := map[string]string{
"anchor.internal:5100/gitea/server@" + digest: ArtifactStoreScheme + "gitea/server@" + digest,
"localhost:5000/gitea/server@" + digest: ArtifactStoreScheme + "gitea/server@" + digest,
"http://anchor.internal:5100/v2/gitea/config/blobs/" + digest: ArtifactStoreScheme + "gitea/config/blobs/" + digest,
ArtifactStoreScheme + "gitea/server@" + digest: ArtifactStoreScheme + "gitea/server@" + digest,
digest: digest,
"@novox/sdk@1.2.3": "@novox/sdk@1.2.3",
"gitea/gitea@" + digest: "gitea/gitea@" + digest,
"https://registry.example/v2/gitea/config/blobs/" + digest: "https://registry.example/v2/gitea/config/blobs/" + digest,
// The form a registry seat answers is recorded as it is (ADR 0605 §9).
"container-image-registry/gitea/server@" + digest: "container-image-registry/gitea/server@" + digest,
"object-registry/gitea/config@" + digest: "object-registry/gitea/config@" + digest,
}
for announced, want := range cases {
if got := seats.Recorded(announced); got != want {
t.Errorf("Recorded(%q) = %q, want %q", announced, got, want)
}
}
// And every reader reads both forms: a kept reference and the seat's are fetched from the same place.
for reference, want := range map[string]string{
ArtifactStoreScheme + "gitea/server@" + digest: "10.0.0.1:5000/gitea/server@" + digest,
"container-image-registry/gitea/server@" + digest: "10.0.0.1:5000/gitea/server@" + digest,
ArtifactStoreScheme + "gitea/config/blobs/" + digest: "http://10.0.0.1:5000/v2/gitea/config/blobs/" + digest,
"object-registry/gitea/config@" + digest: "http://10.0.0.1:5000/v2/gitea/config/blobs/" + digest,
"registry.example/gitea/server@" + digest: "registry.example/gitea/server@" + digest,
"mesh-artifact-store/gitea/server@" + digest: "mesh-artifact-store/gitea/server@" + digest,
// An upstream copy under the holder's own naming, in both forms.
"container-image-registry/upstream/quay.io:443/org/app@" + digest: "10.0.0.1:5000/upstream/quay.io:443/org/app@" + digest,
ArtifactStoreScheme + "upstream/quay.io:443/org/app@" + digest: "10.0.0.1:5000/upstream/quay.io:443/org/app@" + digest,
} {
if got := seats.Routed(reference, "10.0.0.1:5000"); got != want {
t.Errorf("Routed(%q) = %q, want %q", reference, got, want)
}
}
// A build module names what it published in the seat's form, and reads it back as the seat it came through.
for _, seat := range seats.All() {
ref := SeatReference(seat, "gitea", "config", digest)
if got, path, ok := seats.InRegistrySeat(ref); !ok || got != seat || path != "gitea/config@"+digest {
t.Errorf("SeatReference(%s) = %q, read back as %q %q %v", seat, ref, got, path, ok)
}
}
// The holder's naming of an upstream copy reads through the image seat only, and InUpstream reads its host and path.
for reference, want := range map[string]bool{
"container-image-registry/upstream/quay.io:443/org/app@" + digest: true,
"object-registry/upstream/quay.io:443/org/app@" + digest: false,
"container-image-registry/upstream/org/app@" + digest: false,
"container-image-registry/upstream/quay.io/app": false,
} {
_, path, ok := seats.InRegistrySeat(reference)
if ok != want {
t.Errorf("InRegistrySeat(%q) read %v, want %v", reference, ok, want)
}
if host, image, up := InUpstream(path); ok && (!up || host != "quay.io:443" || image != "org/app@"+digest) {
t.Errorf("InUpstream(%q) = %q %q %v", path, host, image, up)
}
}
}
@@ -0,0 +1,63 @@
// Package gopackageregistryclient is the go-package-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51
// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part,
// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a
// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is
// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the
// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport.
package gopackageregistryclient
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry"
"git.novox.be/novox/mesh-delivery/sdk/parts"
)
// Call asks one verb of the seat, `seat:<seat>.<verb>`, with a body, and answers its JSON.
type Call func(key string, body any) (json.RawMessage, error)
// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were
// sent, and the chunks they took.
type Sent struct {
Transfer string
parts.Sent
}
// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in
// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes
// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read.
// Its outcome, published or refused, is the holder's event, which the caller waits for.
func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) {
open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) {
in.Parts = make([]seat.Part, 0, len(ps))
for _, p := range ps {
in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size})
}
raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in)
if err != nil {
return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err)
}
var a seat.PublishAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err)
}
return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil
}
chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error {
raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part,
Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)})
if err != nil {
return fmt.Errorf("%s's publish-part: %w", seat.Seat, err)
}
var a seat.PublishPartAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err)
}
return nil
}
sent, err := parts.Send(ctx, open, chunk, ps)
return Sent{Transfer: in.Transfer, Sent: sent}, err
}
@@ -0,0 +1,57 @@
package gopackageregistryclient
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry"
"git.novox.be/novox/mesh-delivery/sdk/parts"
)
// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the
// holder lacks travels by publish-part, its bytes whole, the last chunk marked.
func TestPublishTalksToItsOwnSeat(t *testing.T) {
held, lacked := []byte("held"), []byte("lacked")
part := func(b []byte) parts.Part {
sum := sha256.Sum256(b)
return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)),
Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }}
}
ps := []parts.Part{part(held), part(lacked)}
var asked []string
var got []byte
call := func(key string, body any) (json.RawMessage, error) {
asked = append(asked, key)
raw, _ := json.Marshal(body)
switch key {
case "seat:go-package-registry.publish":
var in seat.PublishInput
_ = json.Unmarshal(raw, &in)
if len(in.Parts) != 2 {
t.Errorf("publish names %d parts, want 2", len(in.Parts))
}
return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}})
case "seat:go-package-registry.publish-part":
var in seat.PublishPartInput
_ = json.Unmarshal(raw, &in)
data, _ := base64.StdEncoding.DecodeString(in.Data)
if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" {
t.Errorf("publish-part %+v", in)
}
got = append(got, data...)
return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part})
}
t.Fatalf("asked %s", key)
return nil, nil
}
sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps)
if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 {
t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err)
}
}
@@ -0,0 +1,18 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
)
require (
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
@@ -0,0 +1,12 @@
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -0,0 +1,74 @@
package meshcatalogclient
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"regexp"
"sort"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// FingerprintSet is the `fpset` of a candidate's pull requests: the first 12 hex digits of the sha256 over the lines
// `<module>=<fp2>` or `<module>=none:<reason>` of every module with an entry in a pull's fps, sorted and joined by a newline,
// so it is the same for every order and changes with any fingerprint or reason. A manifest digest does not enter: the
// fingerprint already covers the manifest. It is the empty string when no pull request carries a fingerprint. The checker
// keys its verdict by it.
func FingerprintSet(pulls []seat.CandidatesByGroupValuePullsItem) string {
lines := map[string]bool{}
for _, p := range pulls {
for module, m := range p.Fps {
if m.Fp2 != "" {
lines[module+"="+m.Fp2] = true
} else {
lines[module+"=none:"+m.None2] = true
}
}
}
if len(lines) == 0 {
return ""
}
all := make([]string, 0, len(lines))
for l := range lines {
all = append(all, l)
}
sort.Strings(all)
sum := sha256.Sum256([]byte(strings.Join(all, "\n")))
return hex.EncodeToString(sum[:])[:12]
}
// CandidatesKey is the key, in the state candidates, of a group: `<group>` escaped (keys.EscapeName), the branch name the
// group's pull requests share (a slash is allowed); a lone pull request is a group of one under its branch name.
func CandidatesKey(group string) (string, error) {
if group == "" {
return "", fmt.Errorf("meshcatalogclient: the group name is empty")
}
return seat.CandidatesByGroupKey(keys.EscapeName(group)), nil
}
// ParseCandidatesKey is the inverse of CandidatesKey.
func ParseCandidatesKey(key string) (string, error) {
part, ok := seat.ParseCandidatesByGroupKey(key)
if !ok {
return "", fmt.Errorf("meshcatalogclient: %q is not a candidates key", key)
}
group, err := keys.UnescapeName(part)
if err != nil {
return "", fmt.Errorf("meshcatalogclient: %q is not a candidates key: %w", key, err)
}
return group, nil
}
var tryGroupOK = regexp.MustCompile(`^try-[0-9]{1,20}$`)
// IsTryGroup says a candidates key is the group of a whole try, try-<digits>, which is also the try's id: the indexer makes the
// candidate of a try's group from its previews, and the checker judges it by its try, never as a pull request's check.
func IsTryGroup(key string) bool { return tryGroupOK.MatchString(key) }
// NoneNoPreview is why a candidate's touched module has no fingerprint of the new form yet: its merge preview is not
// computed. The indexer writes it; a reader that waits for a try's candidate takes one that still says it as not yet the
// candidate of the heads previewed.
const NoneNoPreview = "no merge preview is computed"
@@ -0,0 +1,167 @@
package meshcatalogclient
import (
"errors"
"strings"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
func hex64(c string) string { return strings.Repeat(c, 64) }
// The digest and the candidate's fingerprint set equal what the trunk's go/delivery computed for the same input (the
// expected strings are its output), so a key or a verdict written before is read after.
func TestDigestAndFingerprintSetKeepTheirForms(t *testing.T) {
if got := ManifestDigest([]byte(`{"module":"nats"}`)); got != "19a7450f557153c318558acac91cd6972321a192f6f95c3698a0e4c57edd4359" {
t.Errorf("ManifestDigest = %s", got)
}
pull := func(fps map[string]seat.CandidatesByGroupValuePullsItemFpsValue) []seat.CandidatesByGroupValuePullsItem {
return []seat.CandidatesByGroupValuePullsItem{{Fps: fps}}
}
fps := map[string]seat.CandidatesByGroupValuePullsItemFpsValue{"nats": {Fp2: SourcePrefix2 + hex64("a")}, "zsh": {None2: "no manifest"}}
set := FingerprintSet(pull(fps))
if set != "50a3f57cc4e2" {
t.Errorf("FingerprintSet = %s", set)
}
fps["zsh"] = seat.CandidatesByGroupValuePullsItemFpsValue{None2: "another reason"}
if FingerprintSet(pull(fps)) == set {
t.Error("a changed reason leaves the set as it was")
}
if FingerprintSet(pull(nil)) != "" {
t.Error("no fingerprint gives a set")
}
}
// A manifest and a module are read back as written, and each rule of the old values is kept: a refusal names its reason.
func TestManifestAndModuleValuesKeepTheirRules(t *testing.T) {
if _, err := ManifestsKey("abc"); err == nil || !strings.Contains(err.Error(), `"abc"`) {
t.Errorf("ManifestsKey(abc) = %v", err)
}
if k, err := ParseManifestsKey(hex64("b")); err != nil || k != hex64("b") {
t.Errorf("ParseManifestsKey = %q, %v", k, err)
}
raw, err := EncodeManifest(seat.ManifestsByDigestValue{Module: "nats", Manifest: `{"module":"nats"}`})
if err != nil {
t.Fatal(err)
}
if m, err := DecodeManifest(raw); err != nil || m.Module != "nats" || m.V != keys.Version {
t.Errorf("DecodeManifest = %+v, %v", m, err)
}
for _, bad := range []seat.ManifestsByDigestValue{{Module: "a.b", Manifest: "{}"}, {Module: "nats"}, {Module: "nats", Manifest: "not json"}, {Module: "nats", Manifest: "{\"a\":\"\xff\"}"}} {
if _, err := EncodeManifest(bad); err == nil {
t.Errorf("EncodeManifest(%+v) is accepted", bad)
}
}
if _, err := DecodeManifest([]byte(`{"module":"nats","manifest":"{}"}`)); !errors.Is(err, keys.ErrUnknownVersion) {
t.Errorf("a manifest with no version: %v", err)
}
good := seat.ModulesByModuleValue{Fp2: SourcePrefix2 + hex64("a"), Repository: "novox/mesh-catalog", Commit: "c", Manifest: hex64("b"), Path: RootPath}
raw, err = EncodeModule(good)
if err != nil {
t.Fatal(err)
}
if m, err := DecodeModule(raw); err != nil || m.Fp2 != good.Fp2 || m.Path != RootPath {
t.Errorf("DecodeModule = %+v, %v", m, err)
}
refused := good
refused.Verdict, refused.Problems = VerdictRefused, []string{"p"}
if _, err := EncodeModule(refused); err != nil {
t.Errorf("a refused module: %v", err)
}
for name, mutate := range map[string]func(m *seat.ModulesByModuleValue){
"neither a fingerprint nor a reason": func(m *seat.ModulesByModuleValue) { m.Fp2 = "" },
"both fp2 and none2": func(m *seat.ModulesByModuleValue) { m.None2 = "x" },
"an old fingerprint of another form": func(m *seat.ModulesByModuleValue) { m.Fp = "src2:" + hex64("a") },
"a fingerprint of the old key form": func(m *seat.ModulesByModuleValue) { m.Fp2 = "src1:" + hex64("a") },
"inputs and no manifest": func(m *seat.ModulesByModuleValue) {
m.Manifest = ""
m.Inputs2 = &seat.ModulesByModuleValueInputs2{Tree: "t"}
},
"a refusal with no problem": func(m *seat.ModulesByModuleValue) { m.Verdict = VerdictRefused },
"problems with no refusal": func(m *seat.ModulesByModuleValue) { m.Problems = []string{"p"} },
"unknown fields with no refusal": func(m *seat.ModulesByModuleValue) { m.Unknown = []string{"series"} },
"a path outside the directory": func(m *seat.ModulesByModuleValue) { m.Paths = map[string][]string{"tools": {"../x"}} },
"a toolchain with no recipe": func(m *seat.ModulesByModuleValue) {
m.Toolchains = map[string]seat.ToolchainUsed{"go": {Language: "go", Version: "1.26"}}
},
"an import form with no imports": func(m *seat.ModulesByModuleValue) {
m.Inputs2 = &seat.ModulesByModuleValueInputs2{Tree: "t", ImportForm: SourcePrefix2 + hex64("a")}
},
} {
m := good
mutate(&m)
if _, err := EncodeModule(m); err == nil {
t.Errorf("%s: EncodeModule accepts %+v", name, m)
}
}
}
// A verdict is taken as the readers have always taken it: a missing one accepted, a refusal held with its problems, a wait
// held with the fields it waits on, and a module's own verdict field gives the same.
func TestVerdictsAreTakenAsTheReadersTookThem(t *testing.T) {
if refused, problems := TakeVerdict(nil); refused || problems != nil {
t.Errorf("no verdict: %v %v", refused, problems)
}
waits := seat.VerdictsByDigestValue{Module: "nats", State: VerdictManifestWaits, Unknown: []string{"series"}, Judge: "j"}
refused, problems := TakeVerdict(&waits)
if !refused || len(problems) != 1 || problems[0] != "it carries series, which the indexer's judge does not know yet" {
t.Errorf("a wait: %v %v", refused, problems)
}
if got := VerdictWhy(waits); got != "the indexer's judge (j) holds the manifest of nats: "+problems[0] {
t.Errorf("VerdictWhy(wait) = %q", got)
}
if as := VerdictAsRefused(waits); as.State != VerdictRefused || len(as.Unknown) != 0 || as.Problems[0] != problems[0] {
t.Errorf("VerdictAsRefused = %+v", as)
}
no := seat.VerdictsByDigestValue{Module: "nats", State: VerdictRefused, Problems: []string{"a", "b"}, Judge: "j"}
if got := VerdictWhy(no); got != "the indexer's judge (j) refused the manifest of nats: a; b" {
t.Errorf("VerdictWhy(refused) = %q", got)
}
if !SameVerdict(no, seat.VerdictsByDigestValue{Module: "nats", State: VerdictRefused, Problems: []string{"a", "b"}, Judge: "k"}) {
t.Error("another judge is another verdict")
}
fromModule := ModuleVerdict("nats", seat.ModulesByModuleValue{Verdict: VerdictRefused, Unknown: []string{"series"}, Problems: []string{"x"}})
if fromModule == nil || fromModule.State != VerdictManifestWaits || fromModule.Unknown[0] != "series" {
t.Errorf("a refusal naming unknown fields is the wait it was written as: %+v", fromModule)
}
if ModuleVerdict("nats", seat.ModulesByModuleValue{}) != nil {
t.Error("a module with no verdict carries one")
}
for _, bad := range []seat.VerdictsByDigestValue{{Module: "nats", State: "accepted", Problems: []string{"p"}, Judge: "j"}, {Module: "nats", State: "refused", Judge: "j"}, {Module: "nats", State: "accepted"}, {Module: "nats", State: "odd", Judge: "j"}} {
if _, err := EncodeVerdict(bad); err == nil {
t.Errorf("EncodeVerdict(%+v) is accepted", bad)
}
}
raw, err := EncodeVerdict(no)
if err != nil {
t.Fatal(err)
}
if v, err := DecodeVerdict(raw); err != nil || !SameVerdict(v, no) {
t.Errorf("DecodeVerdict = %+v, %v", v, err)
}
if k, err := VerdictsKey(hex64("c")); err != nil || k != hex64("c") {
t.Errorf("VerdictsKey = %q %v", k, err)
}
if _, err := ModulesKey("a.b"); err == nil {
t.Error("ModulesKey accepts a dotted name")
}
}
func TestCandidatesKeyEscapesTheGroupAndReadsBack(t *testing.T) {
key, err := CandidatesKey("fix/1.2")
if err != nil {
t.Fatal(err)
}
if key == "fix/1.2" {
t.Errorf("the group is not escaped: %q", key)
}
if group, err := ParseCandidatesKey(key); err != nil || group != "fix/1.2" {
t.Errorf("read back = %q, %v", group, err)
}
if _, err := CandidatesKey(""); err == nil {
t.Error("an empty group keys")
}
}
@@ -0,0 +1,41 @@
// Package meshcatalogclient holds the logic the holders and callers of the seat mesh-catalog share over its contract (novox/hq
// ADR 0625 §4, ADR 0634 §2): the form of a module's source fingerprint, `src2:` and 64 lowercase hex digits (ADR 0593 §2),
// over the `fp2` field of the seat's `modules` value. The indexer computes a fingerprint alone, in its own package; every part
// that keys a state by it, the node-engine among them, reads this form here.
package meshcatalogclient
import (
"fmt"
"strings"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// SourcePrefix2 opens every source fingerprint of the form.
const SourcePrefix2 = "src2:"
// Toolchain2 is what an artifact's toolchain enters the fingerprint as (novox/hq ADR 0405 clause 7): the artifact's
// language, the installed toolchain's version and the build module's recipe version.
type Toolchain2 struct {
Language string
Version string
Recipe string
}
// KeyPart is a fingerprint as a key part: its 64 hex digits without the prefix, since a bus key may not hold ':'. It
// refuses a value that is not `src2:` and 64 lowercase hex digits, a src1 fingerprint included, naming it.
func KeyPart(fp string) (string, error) {
part, ok := strings.CutPrefix(fp, SourcePrefix2)
if !ok || !keys.IsHex64(part) {
return "", fmt.Errorf("fingerprint: %q is not a fingerprint of the form, %s and 64 lowercase hex digits", fp, SourcePrefix2)
}
return part, nil
}
// ParseKeyPart is the inverse of KeyPart. It refuses a part that is not 64 lowercase hex digits, naming it.
func ParseKeyPart(part string) (string, error) {
if !keys.IsHex64(part) {
return "", fmt.Errorf("fingerprint: %q is not the key part of a fingerprint, 64 lowercase hex digits", part)
}
return SourcePrefix2 + part, nil
}
@@ -0,0 +1,34 @@
package meshcatalogclient
import (
"strings"
"testing"
)
// The key part holds only bus key characters and parsing gives the fingerprint back; a src1 or malformed value is refused,
// naming it.
func TestFingerprintKeyPart(t *testing.T) {
fp := SourcePrefix2 + strings.Repeat("ab", 32)
part, err := KeyPart(fp)
if err != nil {
t.Fatal(err)
}
if len(part) != 64 || strings.Trim(part, "0123456789abcdef") != "" {
t.Errorf("key part %q is not 64 lowercase hex characters", part)
}
back, err := ParseKeyPart(part)
if err != nil || back != fp {
t.Errorf("parse(%q) = %q, %v; want %q", part, back, err, fp)
}
src1 := "src1:" + strings.Repeat("c", 64)
for _, bad := range []string{src1, "src2:", "src2:xyz", "", "src2:" + strings.Repeat("A", 64), "src2:" + strings.Repeat("a", 63)} {
if _, err := KeyPart(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
t.Errorf("KeyPart(%q) = %v, want a refusal naming it", bad, err)
}
}
for _, bad := range []string{src1, "src2:" + strings.Repeat("a", 64), strings.Repeat("a", 63), strings.Repeat("A", 64), "", "zz"} {
if _, err := ParseKeyPart(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
t.Errorf("ParseKeyPart(%q) = %v, want a refusal naming it", bad, err)
}
}
}
@@ -0,0 +1,17 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
)
require (
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
@@ -0,0 +1,12 @@
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -0,0 +1,79 @@
package meshcatalogclient
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"unicode/utf8"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// ManifestDigest is the hex sha256 of a manifest's bytes exactly as read at the commit: the key of a `manifests` value and
// the digest a `modules` value and a candidate's module carry (novox/hq ADR 0429 clause 16).
func ManifestDigest(raw []byte) string {
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:])
}
// ManifestsKey is the key of a `manifests` value: the manifest's digest, 64 lowercase hex digits. The indexer writes it
// once, by a create that fails when the key exists.
func ManifestsKey(digest string) (string, error) {
if !keys.IsHex64(digest) {
return "", fmt.Errorf("meshcatalogclient: %q is not a manifest digest, 64 lowercase hex digits", digest)
}
return seat.ManifestsByDigestKey(digest), nil
}
// ParseManifestsKey is the inverse of ManifestsKey.
func ParseManifestsKey(key string) (string, error) {
digest, err := ManifestsKey(key)
if err != nil {
return "", fmt.Errorf("meshcatalogclient: %q is not a manifests key: %w", key, err)
}
return digest, nil
}
func checkManifest(m seat.ManifestsByDigestValue) error {
if err := keys.CheckName("module", m.Module); err != nil {
return err
}
if m.Manifest == "" {
return fmt.Errorf("meshcatalogclient: the manifest of %s is empty", m.Module)
}
if !utf8.ValidString(m.Manifest) {
return fmt.Errorf("meshcatalogclient: the manifest of %s is not UTF-8 text, which a value cannot hold byte for byte", m.Module)
}
if !json.Valid([]byte(m.Manifest)) {
return fmt.Errorf("meshcatalogclient: the manifest of %s is not JSON", m.Module)
}
return nil
}
// EncodeManifest is m as JSON at the kept values' version; it refuses a module that is not a name and a manifest that is
// empty, not UTF-8 or not JSON.
func EncodeManifest(m seat.ManifestsByDigestValue) ([]byte, error) {
if err := checkManifest(m); err != nil {
return nil, err
}
m.V = keys.Version
return json.Marshal(m)
}
// DecodeManifest reads a `manifests` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeManifest
// refuses.
func DecodeManifest(data []byte) (seat.ManifestsByDigestValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.ManifestsByDigestValue{}, err
}
var m seat.ManifestsByDigestValue
if err := json.Unmarshal(data, &m); err != nil {
return seat.ManifestsByDigestValue{}, fmt.Errorf("meshcatalogclient: value does not fit its shape: %w", err)
}
if err := checkManifest(m); err != nil {
return seat.ManifestsByDigestValue{}, err
}
return m, nil
}
@@ -0,0 +1,133 @@
package meshcatalogclient
import (
"encoding/json"
"fmt"
"path"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// RootPath is a `modules` value's path when the module's manifest is its repository's root.
const RootPath = "."
// SourcePrefix1 opens the fingerprint of the old form a `modules` value may still carry in `fp`.
const SourcePrefix1 = "src1:"
// ModulesKey is the key of a `modules` value: the module's name.
func ModulesKey(module string) (string, error) {
if err := keys.CheckName("module", module); err != nil {
return "", err
}
return seat.ModulesByModuleKey(module), nil
}
// ParseModulesKey is the inverse of ModulesKey.
func ParseModulesKey(key string) (string, error) {
module, err := ModulesKey(key)
if err != nil {
return "", fmt.Errorf("meshcatalogclient: %q is not a modules key: %w", key, err)
}
return module, nil
}
// CheckModule refuses a `modules` value that contradicts itself: both or neither of a fingerprint and the reason for none,
// of either form, an old fingerprint not of the form `src1:`, kept inputs without a manifest digest, a verdict the indexer
// does not write, problems without a refusal (or a refusal without problems), a path that is not clean inside the module's
// directory, and a fingerprint or a digest not of its form.
func CheckModule(m seat.ModulesByModuleValue) error {
switch {
case m.Fp != "" && m.None != "":
return fmt.Errorf("meshcatalogclient: a module has both a fingerprint and a reason for none")
case m.Fp == "" && m.None == "" && m.Fp2 == "" && m.None2 == "":
return fmt.Errorf("meshcatalogclient: a module has neither a fingerprint nor a reason for none, of either form")
case m.Fp != "" && !strings.HasPrefix(m.Fp, SourcePrefix1):
return fmt.Errorf("meshcatalogclient: the module's fingerprint %q does not start with %s", m.Fp, SourcePrefix1)
case m.Fp2 != "" && m.None2 != "":
return fmt.Errorf("meshcatalogclient: a module has both fp2 and none2")
}
for language, t := range m.Toolchains {
if t.Version == "" || t.Recipe == "" {
return fmt.Errorf("meshcatalogclient: the module's toolchain of %q needs its version and its recipe version", language)
}
}
if m.Inputs2 != nil && m.Manifest == "" {
return fmt.Errorf("meshcatalogclient: a module keeps new-form inputs but has no manifest digest")
}
for artifact, paths := range m.Paths {
if artifact == "" || len(paths) == 0 {
return fmt.Errorf("meshcatalogclient: the module's named paths of the artifact %q are empty or name no artifact", artifact)
}
for _, p := range paths {
if !cleanInside(p) {
return fmt.Errorf("meshcatalogclient: the artifact %q names the path %q, not a clean path inside the module's directory", artifact, p)
}
}
}
if in := m.Inputs2; in != nil {
if f := in.ImportForm; f != "" && (len(in.Imports) == 0 || !strings.HasPrefix(f, SourcePrefix2) || !keys.IsHex64(f[len(SourcePrefix2):])) {
return fmt.Errorf("meshcatalogclient: the kept import form %q is set only with imports, and is %s and 64 hex digits", f, SourcePrefix2)
}
for p, object := range in.Paths {
if !cleanInside(p) {
return fmt.Errorf("meshcatalogclient: the named path %q is not a clean path inside the module's directory", p)
}
if object == "" {
return fmt.Errorf("meshcatalogclient: the named path %q names no git object", p)
}
}
}
switch {
case m.Verdict != "" && m.Verdict != VerdictAccepted && m.Verdict != VerdictRefused:
return fmt.Errorf("meshcatalogclient: the module's verdict %q is neither %s nor %s", m.Verdict, VerdictAccepted, VerdictRefused)
case len(m.Unknown) > 0 && m.Verdict != VerdictRefused:
return fmt.Errorf("meshcatalogclient: a module's unknown fields are named only with the refusal a wait is written as")
case m.Verdict != "" && m.Manifest == "":
return fmt.Errorf("meshcatalogclient: a module carries a verdict and no manifest digest")
case (m.Verdict == VerdictRefused) != (len(m.Problems) > 0):
return fmt.Errorf("meshcatalogclient: a module's problems are named only with a refusal, and a refusal names them")
case m.Verdict == VerdictRefused && len(m.Warnings) > 0:
return fmt.Errorf("meshcatalogclient: a refused module warns: a refusal names problems")
}
if m.Fp2 != "" {
if _, err := KeyPart(m.Fp2); err != nil {
return err
}
}
if m.Manifest != "" {
if _, err := ManifestsKey(m.Manifest); err != nil {
return err
}
}
return nil
}
func cleanInside(p string) bool {
return p != "" && p == path.Clean(p) && !strings.HasPrefix(p, "/") && p != ".." && !strings.HasPrefix(p, "../")
}
// EncodeModule is m as JSON at the kept values' version; it refuses what CheckModule refuses.
func EncodeModule(m seat.ModulesByModuleValue) ([]byte, error) {
if err := CheckModule(m); err != nil {
return nil, err
}
m.V = keys.Version
return json.Marshal(m)
}
// DecodeModule reads a `modules` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeModule refuses.
func DecodeModule(data []byte) (seat.ModulesByModuleValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.ModulesByModuleValue{}, err
}
var m seat.ModulesByModuleValue
if err := json.Unmarshal(data, &m); err != nil {
return seat.ModulesByModuleValue{}, fmt.Errorf("meshcatalogclient: value does not fit its shape: %w", err)
}
if err := CheckModule(m); err != nil {
return seat.ModulesByModuleValue{}, err
}
return m, nil
}
@@ -0,0 +1,168 @@
package meshcatalogclient
import (
"encoding/json"
"fmt"
"slices"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The states of a verdict on a manifest (novox/hq ADR 0578 §1): the indexer judges every manifest it takes in once and
// every other part takes the verdict from the seat's state `verdicts`.
const (
// VerdictAccepted is the state of a manifest the judge found no problem in.
VerdictAccepted = "accepted"
// VerdictRefused is the state of a manifest the judge found a problem in; the verdict's problems say each.
VerdictRefused = "refused"
// VerdictManifestWaits is the state of a manifest that carries a field the judge does not know: it is not refused,
// because a newer judge may know the field; the verdict's unknown names each. A reader holds the module as a refused
// one and keeps what runs. In a `modules` value it is written as a refusal naming the fields.
VerdictManifestWaits = "waits"
)
// VerdictsKey is the key of a `verdicts` value: the digest of the manifest it judges.
func VerdictsKey(digest string) (string, error) {
if !keys.IsHex64(digest) {
return "", fmt.Errorf("meshcatalogclient: %q is not a manifest digest, 64 lowercase hex digits", digest)
}
return seat.VerdictsByDigestKey(digest), nil
}
// VerdictRefuses says whether v refuses its manifest.
func VerdictRefuses(v seat.VerdictsByDigestValue) bool { return v.State == VerdictRefused }
// VerdictWaits says whether v says the manifest waits for a judge that knows its fields.
func VerdictWaits(v seat.VerdictsByDigestValue) bool { return v.State == VerdictManifestWaits }
// VerdictHeld says whether a reader holds the module as refused: the manifest is refused or waits.
func VerdictHeld(v seat.VerdictsByDigestValue) bool { return VerdictRefuses(v) || VerdictWaits(v) }
// WaitsOn says, in words, what a waiting manifest waits on.
func WaitsOn(v seat.VerdictsByDigestValue) string {
return "it carries " + strings.Join(v.Unknown, ", ") + ", which the indexer's judge does not know yet"
}
// TakeVerdict is how a reader takes the verdict on a manifest (ADR 0578 §3): held, with its problems, when the indexer
// wrote a refusal, or, for a manifest that waits, with the words that say which fields it waits on. A missing verdict (nil)
// is taken as accepted: every manifest the indexer holds before it has judged it was taken in under the judges that ran
// until then. A reader holds a refused module and names it with VerdictWhy, and reads the rest.
func TakeVerdict(v *seat.VerdictsByDigestValue) (refused bool, problems []string) {
if v == nil || !VerdictHeld(*v) {
return false, nil
}
if VerdictWaits(*v) {
return true, []string{WaitsOn(*v)}
}
return true, v.Problems
}
// VerdictWhy is a refusal in a reader's words: the module, that the indexer's judge refused its manifest, and each problem.
func VerdictWhy(v seat.VerdictsByDigestValue) string {
if !VerdictHeld(v) {
return ""
}
judge := "the indexer's judge"
if v.Judge != "" {
judge += " (" + v.Judge + ")"
}
if VerdictWaits(v) {
return fmt.Sprintf("%s holds the manifest of %s: %s", judge, v.Module, WaitsOn(v))
}
why := fmt.Sprintf("%s refused the manifest of %s:", judge, v.Module)
for _, p := range v.Problems {
why += " " + p + ";"
}
return why[:len(why)-1]
}
// VerdictAsRefused is v as a reader that does not know the waiting state takes it: a waiting verdict is a refusal naming what
// it waits on, any other verdict is v.
func VerdictAsRefused(v seat.VerdictsByDigestValue) seat.VerdictsByDigestValue {
if !VerdictWaits(v) {
return v
}
v.State, v.Problems, v.Unknown = VerdictRefused, []string{WaitsOn(v)}, nil
return v
}
// SameVerdict says whether v and o say the same of a manifest: the module, the state, the problems, the unknown fields and
// the warnings, whoever judged and when. The indexer writes a verdict only when it is not the same as the one it holds.
func SameVerdict(v, o seat.VerdictsByDigestValue) bool {
return v.Module == o.Module && v.State == o.State && slices.Equal(v.Problems, o.Problems) && slices.Equal(v.Unknown, o.Unknown) && slices.Equal(v.Warnings, o.Warnings)
}
func checkVerdict(v seat.VerdictsByDigestValue) error {
if err := keys.CheckName("module", v.Module); err != nil {
return err
}
if v.State != VerdictManifestWaits && len(v.Unknown) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s names unknown fields and does not wait", v.Module)
}
switch v.State {
case VerdictAccepted:
if len(v.Problems) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s accepts its manifest and names problems", v.Module)
}
case VerdictRefused:
if len(v.Problems) == 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s refuses its manifest and names no problem", v.Module)
}
if len(v.Warnings) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s refuses its manifest and warns: a refusal names problems", v.Module)
}
case VerdictManifestWaits:
if len(v.Unknown) == 0 || len(v.Problems) > 0 || len(v.Warnings) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s waits and names no unknown field, or names problems or warnings: a wait names the fields", v.Module)
}
default:
return fmt.Errorf("meshcatalogclient: the verdict on %s is %q, none of %s, %s and %s", v.Module, v.State, VerdictAccepted, VerdictRefused, VerdictManifestWaits)
}
if v.Judge == "" {
return fmt.Errorf("meshcatalogclient: the verdict on %s names no judge", v.Module)
}
return nil
}
// EncodeVerdict is v as JSON at the kept values' version; it refuses a verdict without a module or a judge, a state other
// than accepted, refused or waits, a refusal naming no problem and an acceptance naming one.
func EncodeVerdict(v seat.VerdictsByDigestValue) ([]byte, error) {
if err := checkVerdict(v); err != nil {
return nil, err
}
v.V = keys.Version
return json.Marshal(v)
}
// DecodeVerdict reads a `verdicts` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeVerdict
// refuses.
func DecodeVerdict(data []byte) (seat.VerdictsByDigestValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.VerdictsByDigestValue{}, err
}
var v seat.VerdictsByDigestValue
if err := json.Unmarshal(data, &v); err != nil {
return seat.VerdictsByDigestValue{}, fmt.Errorf("meshcatalogclient: value does not fit its shape: %w", err)
}
if err := checkVerdict(v); err != nil {
return seat.VerdictsByDigestValue{}, err
}
return v, nil
}
// ModuleVerdict is the verdict a `modules` value carries, or nil when it carries none (taken as accepted, TakeVerdict). A
// refusal naming unknown fields is the wait it was written as.
func ModuleVerdict(module string, m seat.ModulesByModuleValue) *seat.VerdictsByDigestValue {
if m.Verdict == "" && len(m.Warnings) == 0 {
return nil
}
if m.Verdict == "" {
return &seat.VerdictsByDigestValue{Module: module, State: VerdictAccepted, Warnings: m.Warnings}
}
if m.Verdict == VerdictRefused && len(m.Unknown) > 0 {
return &seat.VerdictsByDigestValue{Module: module, State: VerdictManifestWaits, Unknown: m.Unknown}
}
return &seat.VerdictsByDigestValue{Module: module, State: m.Verdict, Problems: m.Problems}
}
@@ -0,0 +1,312 @@
// Package meshdeliverycheckerclient holds the logic the holders and callers of the seat mesh-delivery-checker share over its
// contract (novox/hq ADR 0625 §4, ADR 0634 §2): the rules a verdict and a kept verdict must keep to be written or read. The
// checker writes them, and the view, the gate and the indexer read them; the contract's decoding is strict by schema only, so
// these rules are written once here and no caller copies them.
package meshdeliverycheckerclient
import (
"fmt"
pathpkg "path"
"strings"
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The states of a verdict, the levels of a check and the compositions' states.
const (
VerdictRunning = "running"
VerdictPass = "pass"
VerdictFail = "fail"
LevelUnit = "unit"
LevelIntegration = "integration"
CompositionNotRun = "not-run"
CompositionPass = "pass"
CompositionFail = "fail"
)
// MaxFailures is the most failures a verdict names; it names the count of the rest beside them.
const MaxFailures = 50
func checkLevel(level string) error {
if level != LevelUnit && level != LevelIntegration {
return fmt.Errorf("meshdeliverycheckerclient: the level %q is not %s or %s", level, LevelUnit, LevelIntegration)
}
return nil
}
func isHex12(s string) bool { return len(s) == 12 && strings.Trim(s, "0123456789abcdef") == "" }
// CheckRepository refuses a repository that is not <owner>/<name> with exactly one slash, each part a key part.
func CheckRepository(repository string) error {
owner, name, ok := strings.Cut(repository, "/")
if !ok || strings.Contains(name, "/") {
return fmt.Errorf("meshdeliverycheckerclient: the repository %q is not <owner>/<name>, with exactly one slash", repository)
}
if err := keys.CheckName("repository owner", owner); err != nil {
return fmt.Errorf("meshdeliverycheckerclient: the repository %q: %w", repository, err)
}
if err := keys.CheckName("repository name", name); err != nil {
return fmt.Errorf("meshdeliverycheckerclient: the repository %q: %w", repository, err)
}
return nil
}
func checkComposition(c *checker.VerdictsByGroupHeadsValueComposition) error {
if c == nil {
return nil
}
switch c.State {
case CompositionNotRun:
if c.Sdk != "" || len(c.Machines) > 0 {
return fmt.Errorf("meshdeliverycheckerclient: the verdict's composition is not run and names no mesh-sdk commit or machines")
}
case CompositionPass:
if c.Sdk == "" {
return fmt.Errorf("meshdeliverycheckerclient: the verdict's composition passed and names no mesh-sdk commit")
}
case CompositionFail:
if c.Words == "" {
return fmt.Errorf("meshdeliverycheckerclient: the verdict's composition failed and gives no words")
}
default:
return fmt.Errorf("meshdeliverycheckerclient: the verdict's composition state %q is not %s, %s or %s", c.State, CompositionNotRun, CompositionPass, CompositionFail)
}
return nil
}
// CheckVerdict refuses a verdict a reader could not place: no group, heads that are not 12 lowercase hex digits, a repository
// both merged and read beside, a carried-from that carries nothing, a series change from a series to itself or named twice, a
// level that is neither unit nor integration or counts a negative number, a time below zero, a pass that says nothing of a
// level, a fail that names neither a test nor a machine, and failures on a verdict that did not fail.
func CheckVerdict(v checker.VerdictsByGroupHeadsValue) error {
const who = "meshdeliverycheckerclient: "
if v.Group == "" {
return fmt.Errorf(who + "a verdict names no group")
}
if !isHex12(v.Heads) {
return fmt.Errorf(who+"the verdict's heads hash %q is not 12 lowercase hex digits", v.Heads)
}
for repository, head := range v.Trunk {
if head == "" {
return fmt.Errorf(who+"the verdict's trunk head of %s is empty", repository)
}
}
for repository, commit := range v.Beside {
if commit == "" {
return fmt.Errorf(who+"the verdict's sibling commit of %s is empty", repository)
}
if _, ok := v.Trunk[repository]; ok {
return fmt.Errorf(who+"the verdict names %s as a trunk it merged and as a sibling it read", repository)
}
}
if len(v.CarriedFrom) > 0 {
if v.State == VerdictRunning {
return fmt.Errorf(who + "a running verdict is carried from nothing: carried-from is written with its end")
}
moved := false
for repository, commit := range v.CarriedFrom {
now, inTrunk := v.Trunk[repository]
if !inTrunk {
now = v.Beside[repository]
}
if commit == "" || now == "" {
return fmt.Errorf(who+"the verdict's carried-from names %s, which it judged on no commit of", repository)
}
moved = moved || commit != now
}
if !moved {
return fmt.Errorf(who + "the verdict's carried-from names the commits it is judged on: nothing moved, so nothing was carried")
}
}
changed := map[string]bool{}
for _, c := range v.Series {
if c.Module == "" || c.Artifact == "" || c.From == "" || c.To == "" {
return fmt.Errorf(who+"the verdict's series change %+v names no module, artifact, from or to", c)
}
if c.From == c.To {
return fmt.Errorf(who+"the verdict's series change of %s/%s is from %s to the same series", c.Module, c.Artifact, c.From)
}
if changed[c.Module+"/"+c.Artifact] {
return fmt.Errorf(who+"the verdict names the series change of %s/%s twice", c.Module, c.Artifact)
}
changed[c.Module+"/"+c.Artifact] = true
}
for level, r := range v.Levels {
if err := checkLevel(level); err != nil {
return err
}
if r.Run < 0 || r.Kept < 0 || r.Skipped < 0 {
return fmt.Errorf(who+"the verdict's %s level counts a negative number of tests", level)
}
}
if err := checkComposition(v.Composition); err != nil {
return err
}
seen := map[string]bool{}
for _, level := range v.OverBound {
if err := checkLevel(level); err != nil {
return fmt.Errorf(who+"the verdict's over-bound names: %w", err)
}
if seen[level] {
return fmt.Errorf(who+"the verdict's over-bound names the level %s twice", level)
}
seen[level] = true
}
if t := v.Took; t != nil {
for part, ms := range map[string]int64{"queue": t.Queue, "clone": t.Clone, "unit": t.Unit, "lint": t.Lint,
"integration-setup": t.IntegrationSetup, "integration-run": t.IntegrationRun, "composition": t.Composition} {
if ms < 0 {
return fmt.Errorf(who+"the verdict's time of the part %s is negative", part)
}
}
}
named := v.Failed != nil && (v.Failed.Test != "" || v.Failed.Machine != "")
if len(v.Failures) > 0 && v.State != VerdictFail {
return fmt.Errorf(who+"a %s verdict names no failures", v.State)
}
if len(v.Failures) > MaxFailures+1 {
return fmt.Errorf(who+"the verdict names %d failures, more than %d and the count of the rest", len(v.Failures), MaxFailures)
}
switch v.State {
case VerdictRunning:
if v.Failed != nil && *v.Failed != (checker.VerdictsByGroupHeadsValueFailed{}) {
return fmt.Errorf(who + "a running verdict names no failure")
}
case VerdictPass:
if v.Failed != nil && *v.Failed != (checker.VerdictsByGroupHeadsValueFailed{}) {
return fmt.Errorf(who + "a pass names no failure")
}
for _, level := range []string{LevelUnit, LevelIntegration} {
if _, ok := v.Levels[level]; !ok {
return fmt.Errorf(who+"a pass says nothing of the %s level", level)
}
}
case VerdictFail:
if !named {
return fmt.Errorf(who + "a fail names neither a test nor a machine")
}
default:
return fmt.Errorf(who+"the verdict state %q is not running, pass or fail", v.State)
}
return nil
}
// CheckKept refuses a kept verdict that is not a pass of a repository's package at a level, with its toolchain and its time:
// a failure is never kept.
func CheckKept(k checker.KeptByLevelDigestValue) error {
const who = "meshdeliverycheckerclient: "
if k.State != VerdictPass {
return fmt.Errorf(who+"a kept verdict is a pass, never %q: a failure is never kept", k.State)
}
if err := CheckRepository(k.Repository); err != nil {
return err
}
if k.Package == "" {
return fmt.Errorf(who + "a kept verdict names no package")
}
if err := checkLevel(k.Level); err != nil {
return err
}
if k.Tests < 0 || k.Took < 0 {
return fmt.Errorf(who + "a kept verdict counts a negative number of tests or a negative time")
}
if k.Toolchain == "" {
return fmt.Errorf(who + "a kept verdict names no toolchain version")
}
if k.At == "" {
return fmt.Errorf(who + "a kept verdict says no time")
}
return nil
}
// CheckPreview refuses a preview a reader could not place (the checker writes it, the indexer reads it for the candidates'
// fingerprints): a repository, head or trunk commit that does not key, a head in conflict that names no file or has a merge
// commit or modules, a head that merged and names no merge commit, and a module with no directory, no tree, a manifest
// digest that is not 64 lowercase hex digits, a lock without path or hash, or a named path that is not clean and inside the
// module's directory or names no git object.
func CheckPreview(p checker.PreviewsByRepositoryHeadTrunkValue) error {
const who = "meshdeliverycheckerclient: "
if err := CheckRepository(p.Repository); err != nil {
return err
}
if err := keys.CheckName("head", p.Head); err != nil {
return fmt.Errorf(who+"the preview: %w", err)
}
if err := keys.CheckName("trunk commit", p.Trunk); err != nil {
return fmt.Errorf(who+"the preview: %w", err)
}
for _, f := range p.Conflicts {
if f == "" {
return fmt.Errorf(who + "a preview names a conflicting file with an empty path")
}
}
if len(p.Conflicts) > 0 {
if p.Merge != "" || len(p.Modules) > 0 {
return fmt.Errorf(who + "a preview of a head in conflict has no merge commit and no modules")
}
return nil
}
if p.Merge == "" {
return fmt.Errorf(who + "a preview names no merge commit")
}
for dir, m := range p.Modules {
if dir == "" {
return fmt.Errorf(who + "a preview names a module with an empty directory")
}
if m.Tree == "" {
return fmt.Errorf(who+"the preview's module %q has no tree", dir)
}
if !keys.IsHex64(m.Manifest) {
return fmt.Errorf(who+"the preview's module %q: %q is not a manifest digest, 64 lowercase hex digits", dir, m.Manifest)
}
for path, hash := range m.Locks {
if path == "" || hash == "" {
return fmt.Errorf(who+"the lock %q of the preview's module %q has an empty path or hash", path, dir)
}
}
for p, object := range m.Paths {
if p == "" || p != pathpkg.Clean(p) || strings.HasPrefix(p, "/") || p == ".." || strings.HasPrefix(p, "../") {
return fmt.Errorf(who+"the preview's module %q: the named path %q is not a clean path inside the module's directory", dir, p)
}
if object == "" {
return fmt.Errorf(who+"the preview's module %q: the named path %q names no git object", dir, p)
}
}
}
return nil
}
// VerdictKey is the key, in the state verdicts, of a group's check at its heads: `<escaped group>.<heads hash>`, the
// group escaped so a branch such as `fix/1.2` keys, the heads hash as the mesh-delivery client's HeadsHash gives it
// (12 lowercase hex digits).
func VerdictKey(group, headsHash string) (string, error) {
if group == "" {
return "", fmt.Errorf("meshdeliverycheckerclient: the group name is empty")
}
if !isHex12(headsHash) {
return "", fmt.Errorf("meshdeliverycheckerclient: %q is not a heads hash, 12 lowercase hex digits", headsHash)
}
return keys.EscapeName(group) + "." + headsHash, nil
}
// ParseVerdictKey is the inverse of VerdictKey.
func ParseVerdictKey(key string) (group, headsHash string, err error) {
i := strings.LastIndex(key, ".")
if i < 0 {
return "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a verdict key, <group>.<heads hash>", key)
}
part, headsHash := key[:i], key[i+1:]
if !isHex12(headsHash) {
return "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a verdict key: %q is not a heads hash, 12 lowercase hex digits", key, headsHash)
}
if part == "" || strings.Contains(part, ".") {
return "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a verdict key: the group part is empty or holds a dot", key)
}
if group, err = keys.UnescapeName(part); err != nil {
return "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a verdict key: %w", key, err)
}
return group, headsHash, nil
}
@@ -0,0 +1,223 @@
package meshdeliverycheckerclient
import (
"strings"
"testing"
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
type verdict = checker.VerdictsByGroupHeadsValue
func passing() verdict {
return verdict{Group: "g", Heads: "0123456789ab", State: VerdictPass, Trunk: map[string]string{"novox/a": "c1"},
Levels: map[string]checker.VerdictsByGroupHeadsValueLevelsValue{LevelUnit: {Run: 3}, LevelIntegration: {Run: 1}}}
}
// The rules a verdict keeps to be written or read: each case edits a passing verdict and names what the refusal says.
func TestAVerdictIsRefusedForWhatAReaderCouldNotPlace(t *testing.T) {
if err := CheckVerdict(passing()); err != nil {
t.Fatalf("a passing verdict is refused: %v", err)
}
for name, c := range map[string]struct {
edit func(*verdict)
says string
}{
"no group": {func(v *verdict) { v.Group = "" }, "no group"},
"heads of 11 digits": {func(v *verdict) { v.Heads = "0123456789a" }, "12 lowercase hex"},
"an empty trunk head": {func(v *verdict) { v.Trunk["novox/a"] = "" }, "trunk head"},
"merged and read": {func(v *verdict) { v.Beside = map[string]string{"novox/a": "c2"} }, "trunk it merged and as a sibling"},
"nothing carried": {func(v *verdict) { v.CarriedFrom = map[string]string{"novox/a": "c1"} }, "nothing moved"},
"a running carry": {func(v *verdict) { v.State, v.CarriedFrom = VerdictRunning, map[string]string{"novox/a": "c0"} }, "carried from nothing"},
"a series to itself": {func(v *verdict) {
v.Series = []checker.VerdictsByGroupHeadsValueSeriesItem{{Module: "m", Artifact: "a", From: "0.1", To: "0.1"}}
}, "same series"},
"an unknown level": {func(v *verdict) { v.Levels["smoke"] = checker.VerdictsByGroupHeadsValueLevelsValue{} }, "smoke"},
"a negative count": {func(v *verdict) { v.Levels[LevelUnit] = checker.VerdictsByGroupHeadsValueLevelsValue{Run: -1} }, "negative"},
"a negative time": {func(v *verdict) { v.Took = &checker.VerdictsByGroupHeadsValueTook{Unit: -1} }, "negative"},
"a pass without a level": {func(v *verdict) { delete(v.Levels, LevelIntegration) }, "integration level"},
"a pass that failed": {func(v *verdict) { v.Failed = &checker.VerdictsByGroupHeadsValueFailed{Test: "T"} }, "names no failure"},
"failures of a pass": {func(v *verdict) { v.Failures = []checker.VerdictsByGroupHeadsValueFailuresItem{{Test: "T"}} }, "names no failures"},
"a fail naming nothing": {func(v *verdict) { v.State = VerdictFail }, "neither a test nor a machine"},
"an unknown state": {func(v *verdict) { v.State = "maybe" }, "maybe"},
"a composition passed without a commit": {func(v *verdict) {
v.Composition = &checker.VerdictsByGroupHeadsValueComposition{State: CompositionPass}
}, "mesh-sdk commit"},
"the same level over bound twice": {func(v *verdict) { v.OverBound = []string{LevelUnit, LevelUnit} }, "twice"},
} {
v := passing()
c.edit(&v)
if err := CheckVerdict(v); err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v, want a refusal saying %q", name, err, c.says)
}
}
failed := passing()
failed.State, failed.Failed = VerdictFail, &checker.VerdictsByGroupHeadsValueFailed{Machine: "ace"}
if err := CheckVerdict(failed); err != nil {
t.Errorf("a fail naming a machine is refused: %v", err)
}
}
func TestAKeptVerdictIsAPassOfAPackageAtALevel(t *testing.T) {
t.Run("a preview is refused for what a reader of candidates could not place", previewIsRefused)
ok := checker.KeptByLevelDigestValue{State: VerdictPass, Repository: "novox/mesh-sdk", Package: "./go/keys", Level: LevelUnit, Toolchain: "1.26.0", At: "2026-11-05T10:00:00Z"}
if err := CheckKept(ok); err != nil {
t.Fatalf("a kept pass is refused: %v", err)
}
for name, edit := range map[string]func(*checker.KeptByLevelDigestValue){
"a fail": func(k *checker.KeptByLevelDigestValue) { k.State = VerdictFail },
"no owner": func(k *checker.KeptByLevelDigestValue) { k.Repository = "mesh-sdk" },
"no package": func(k *checker.KeptByLevelDigestValue) { k.Package = "" },
"an unknown level": func(k *checker.KeptByLevelDigestValue) { k.Level = "smoke" },
"negative tests": func(k *checker.KeptByLevelDigestValue) { k.Tests = -1 },
"no toolchain": func(k *checker.KeptByLevelDigestValue) { k.Toolchain = "" },
"no time": func(k *checker.KeptByLevelDigestValue) { k.At = "" },
} {
k := ok
edit(&k)
if CheckKept(k) == nil {
t.Errorf("%s was taken", name)
}
}
}
// A preview is refused for what a reader of candidates could not place.
func previewIsRefused(t *testing.T) {
good := func() checker.PreviewsByRepositoryHeadTrunkValue {
return checker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/a", Head: "h1", Trunk: "t1", Merge: "m1",
Modules: map[string]checker.PreviewsByRepositoryHeadTrunkValueModulesValue{"mod": {Tree: "tr", Manifest: strings.Repeat("a", 64),
Locks: map[string]string{"go.sum": "x"}, Paths: map[string]string{"src": "o1"}}}}
}
if err := CheckPreview(good()); err != nil {
t.Fatalf("a good preview is refused: %v", err)
}
for name, c := range map[string]struct {
edit func(*checker.PreviewsByRepositoryHeadTrunkValue)
says string
}{
"a repository with no owner": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Repository = "a" }, "<owner>/<name>"},
"a head with a dot": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Head = "h.1" }, "head"},
"a conflict with a merge": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Conflicts = []string{"f"} }, "in conflict"},
"no merge commit": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Merge = "" }, "no merge commit"},
"a module with no tree": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) {
m := p.Modules["mod"]
m.Tree = ""
p.Modules["mod"] = m
}, "no tree"},
"a short manifest digest": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) {
m := p.Modules["mod"]
m.Manifest = "abc"
p.Modules["mod"] = m
}, "manifest digest"},
"a path outside the module": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) {
m := p.Modules["mod"]
m.Paths = map[string]string{"../x": "o"}
p.Modules["mod"] = m
}, "clean path"},
} {
p := good()
c.edit(&p)
if err := CheckPreview(p); err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: CheckPreview = %v, want it to say %q", name, err, c.says)
}
}
if err := CheckPreview(checker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/a", Head: "h", Trunk: "t", Conflicts: []string{"f"}}); err != nil {
t.Errorf("a head in conflict is refused: %v", err)
}
}
// A verdict's key as go/delivery's CheckerVerdictKey wrote it (golden): the group escaped, the heads hash after the
// last dot.
func TestVerdictKeyAsGoDeliveryWroteIt(t *testing.T) {
key, err := VerdictKey("fix/1.2", "0123456789ab")
if err != nil || key != "fix/1=2e2.0123456789ab" {
t.Fatalf("%q %v", key, err)
}
group, heads, err := ParseVerdictKey(key)
if err != nil || group != "fix/1.2" || heads != "0123456789ab" {
t.Fatalf("%q %q %v", group, heads, err)
}
for _, bad := range []string{"nodot", "g.XYZ", ".0123456789ab", "a.b.0123456789ab", "g=zz.0123456789ab"} {
if _, _, err := ParseVerdictKey(bad); err == nil {
t.Errorf("%q parses", bad)
}
}
if _, err := VerdictKey("", "0123456789ab"); err == nil {
t.Error("an empty group keys")
}
if _, err := VerdictKey("g", "0123"); err == nil {
t.Error("a short hash keys")
}
}
// The codecs write a value at the kept values' version after its rules and read it back through them.
func TestTheCodecsWriteAtTheVersionAndReadBackThroughTheRules(t *testing.T) {
raw, err := EncodeVerdict(passing())
if err != nil {
t.Fatal(err)
}
if got, err := DecodeVerdict(raw); err != nil || got.V == 0 || got.Group != "g" {
t.Errorf("verdict read back = %+v, %v", got, err)
}
bad := passing()
bad.Heads = "x"
if _, err := EncodeVerdict(bad); err == nil {
t.Error("a verdict whose heads are no hash is written")
}
if _, err := DecodeVerdict([]byte(`{"v":99,"group":"g","heads":"0123456789ab","state":"running"}`)); err == nil {
t.Error("a verdict of an unknown version is read")
}
k := checker.KeptByLevelDigestValue{State: VerdictPass, Repository: "novox/mesh-sdk", Package: "./go/keys", Level: LevelUnit, Toolchain: "1.26.0", At: "2026-11-05T10:00:00Z"}
raw, err = EncodeKept(k)
if err != nil {
t.Fatal(err)
}
if got, err := DecodeKept(raw); err != nil || got.Package != k.Package {
t.Errorf("kept read back = %+v, %v", got, err)
}
k.State = VerdictFail
if _, err := EncodeKept(k); err == nil {
t.Error("a failure is kept")
}
p := checker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/a", Head: "h1", Trunk: "t1", Conflicts: []string{"f.go"}}
raw, err = EncodePreview(p)
if err != nil {
t.Fatal(err)
}
if got, err := DecodePreview(raw); err != nil || got.Head != "h1" {
t.Errorf("preview read back = %+v, %v", got, err)
}
p.Conflicts = nil
if _, err := EncodePreview(p); err == nil {
t.Error("a preview with no merge commit and no conflict is written")
}
}
// The kept and preview keys: a key reads back to its parts, and a part that does not key is refused.
func TestKeptAndPreviewKeysReadBack(t *testing.T) {
digest := strings.Repeat("ab", 32)
key, err := KeptKey(LevelUnit, digest)
if err != nil || key != "unit."+digest {
t.Fatalf("kept key = %q, %v", key, err)
}
if level, d, err := ParseKeptKey(key); err != nil || level != LevelUnit || d != digest {
t.Errorf("kept key read back = %q %q, %v", level, d, err)
}
for _, bad := range []string{"fast." + digest, "unit.XY", "unit"} {
if _, _, err := ParseKeptKey(bad); err == nil {
t.Errorf("kept key %q is read", bad)
}
}
key, err = PreviewKey("novox/mesh-sdk", "h1", "t1")
if err != nil {
t.Fatal(err)
}
if repo, h, tr, err := ParsePreviewKey(key); err != nil || repo != "novox/mesh-sdk" || h != "h1" || tr != "t1" {
t.Errorf("preview key %q read back = %q %q %q, %v", key, repo, h, tr, err)
}
for _, bad := range []string{"novox.h1.t1", "a/b.h1", "a/b.h1.t1.x"} {
if _, _, _, err := ParsePreviewKey(bad); err == nil {
t.Errorf("preview key %q is read", bad)
}
}
}
@@ -0,0 +1,150 @@
package meshdeliverycheckerclient
import (
"encoding/json"
"fmt"
"strings"
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The codecs of the three states the checker writes and others read (the view and the gate read verdicts, the indexer reads
// previews, the checker and the tools read kept verdicts): a value is written at the version of the kept values after its rules
// (CheckVerdict, CheckKept, CheckPreview) and read back through the same rules. The checker's owed state is read by no other
// part and keeps its own codec.
// EncodeVerdict is v as JSON at the kept values' version, refused by CheckVerdict's rules.
func EncodeVerdict(v checker.VerdictsByGroupHeadsValue) ([]byte, error) {
if err := CheckVerdict(v); err != nil {
return nil, err
}
v.V = keys.Version
return json.Marshal(v)
}
// DecodeVerdict reads a `verdicts` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeVerdict refuses.
func DecodeVerdict(data []byte) (checker.VerdictsByGroupHeadsValue, error) {
var v checker.VerdictsByGroupHeadsValue
if err := decode(data, &v); err != nil {
return v, err
}
if err := CheckVerdict(v); err != nil {
return checker.VerdictsByGroupHeadsValue{}, err
}
return v, nil
}
// EncodeKept is k as JSON at the kept values' version, refused by CheckKept's rules.
func EncodeKept(k checker.KeptByLevelDigestValue) ([]byte, error) {
if err := CheckKept(k); err != nil {
return nil, err
}
k.V = keys.Version
return json.Marshal(k)
}
// DecodeKept reads a `kept` value; it refuses an unknown version and what EncodeKept refuses.
func DecodeKept(data []byte) (checker.KeptByLevelDigestValue, error) {
var k checker.KeptByLevelDigestValue
if err := decode(data, &k); err != nil {
return k, err
}
if err := CheckKept(k); err != nil {
return checker.KeptByLevelDigestValue{}, err
}
return k, nil
}
// EncodePreview is p as JSON at the kept values' version, refused by CheckPreview's rules.
func EncodePreview(p checker.PreviewsByRepositoryHeadTrunkValue) ([]byte, error) {
if err := CheckPreview(p); err != nil {
return nil, err
}
p.V = keys.Version
return json.Marshal(p)
}
// DecodePreview reads a `previews` value; it refuses an unknown version and what EncodePreview refuses.
func DecodePreview(data []byte) (checker.PreviewsByRepositoryHeadTrunkValue, error) {
var p checker.PreviewsByRepositoryHeadTrunkValue
if err := decode(data, &p); err != nil {
return p, err
}
if err := CheckPreview(p); err != nil {
return checker.PreviewsByRepositoryHeadTrunkValue{}, err
}
return p, nil
}
func decode(data []byte, into any) error {
if err := keys.CheckVersion(data); err != nil {
return err
}
if err := json.Unmarshal(data, into); err != nil {
return fmt.Errorf("meshdeliverycheckerclient: value does not fit its shape: %w", err)
}
return nil
}
// KeptKey is the key, in the state kept, of a kept verdict: `<level>.<digest>`, the level unit or integration and the digest,
// lowercase hex, of everything that package's tests read at that level as the language's build module names it.
func KeptKey(level, digest string) (string, error) {
if err := checkLevel(level); err != nil {
return "", err
}
if digest == "" || strings.Trim(digest, "0123456789abcdef") != "" {
return "", fmt.Errorf("meshdeliverycheckerclient: %q is not a digest, lowercase hex", digest)
}
return checker.KeptByLevelDigestKey(level, digest), nil
}
// ParseKeptKey is the inverse of KeptKey.
func ParseKeptKey(key string) (level, digest string, err error) {
level, digest, ok := checker.ParseKeptByLevelDigestKey(key)
if !ok {
return "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a kept key, <level>.<digest>", key)
}
if _, err = KeptKey(level, digest); err != nil {
return "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a kept key: %w", key, err)
}
return level, digest, nil
}
// PreviewKey is the key, in the state previews, of a head merged onto a trunk commit: `<repository>.<head>.<trunk>`, the
// repository's owner and name each escaped (keys.EscapeName).
func PreviewKey(repository, head, trunk string) (string, error) {
if err := CheckRepository(repository); err != nil {
return "", err
}
if err := keys.CheckName("head", head); err != nil {
return "", err
}
if err := keys.CheckName("trunk commit", trunk); err != nil {
return "", err
}
owner, name, _ := strings.Cut(repository, "/")
return checker.PreviewsByRepositoryHeadTrunkKey(keys.EscapeName(owner)+"/"+keys.EscapeName(name), head, trunk), nil
}
// ParsePreviewKey is the inverse of PreviewKey.
func ParsePreviewKey(key string) (repository, head, trunk string, err error) {
repo, head, trunk, ok := checker.ParsePreviewsByRepositoryHeadTrunkKey(key)
if !ok || strings.Contains(trunk, ".") {
return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key, <repository>.<head>.<trunk>", key)
}
owner, name, found := strings.Cut(repo, "/")
if !found {
return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key: the repository has no slash", key)
}
owner, err1 := keys.UnescapeName(owner)
name, err2 := keys.UnescapeName(name)
if err1 != nil || err2 != nil {
return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key: the repository does not unescape", key)
}
repository = owner + "/" + name
if _, err := PreviewKey(repository, head, trunk); err != nil {
return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key: %w", key, err)
}
return repository, head, trunk, nil
}
@@ -0,0 +1,9 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
)
@@ -0,0 +1,323 @@
// Package meshdeliveryclient holds the logic the holders and callers of the seat mesh-delivery share over its contract
// (novox/hq ADR 0625 §4, ADR 0634 §2, ADR 0635 §5): the keys of its states `merges`, `asks` and `owed` and the rules their
// values keep, the id the teller gives the router for an ask, and the act an ask's options bind. The gate writes them, the
// forge bridge, the teller and the assigner read them; the contract's decoding is strict by schema only, so these rules are
// written once here and no caller copies them.
package meshdeliveryclient
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// AskKindMerge is the kind of the gate's ask for a group's merge, the first part of its key.
const AskKindMerge = "merge"
// VerbMerge is the act an ask for a group's merge binds in its options, `mesh-delivery.merge` (Approve).
const VerbMerge = "merge"
// Head is one pull request at one commit: the repository as `<owner>/<name>` and the full sha.
type Head struct {
Repository string `json:"repository"`
Number int `json:"number"`
Head string `json:"head"`
}
// HeadsHash is the first 12 hex digits of the sha256 over the lines `<repository>#<number>=<head>`, sorted and joined by a
// newline: the same for every order of heads, different when any head changes. An empty list is refused.
func HeadsHash(heads []Head) (string, error) {
if len(heads) == 0 {
return "", fmt.Errorf("meshdeliveryclient: a heads hash needs at least one pull request")
}
lines := make([]string, len(heads))
for i, h := range heads {
lines[i] = fmt.Sprintf("%s#%d=%s", h.Repository, h.Number, h.Head)
}
sort.Strings(lines)
sum := sha256.Sum256([]byte(strings.Join(lines, "\n")))
return hex.EncodeToString(sum[:])[:12], nil
}
var hashOK = regexp.MustCompile(`^[0-9a-f]{12}$`)
func groupKey(group string) (string, error) {
if group == "" {
return "", fmt.Errorf("meshdeliveryclient: the group name is empty")
}
return keys.EscapeName(group), nil
}
// MergesKey is the key, in the state merges, of a group: `<group>`, escaped so a branch such as `fix/1.2` keys.
func MergesKey(group string) (string, error) { return groupKey(group) }
// MergeAskKey is the key, in the state asks, of the ask for merging a group at these heads: `merge.<group>.<heads hash>`.
// The teller's answer has the same key.
func MergeAskKey(group string, heads []Head) (string, error) {
part, err := groupKey(group)
if err != nil {
return "", err
}
hash, err := HeadsHash(heads)
if err != nil {
return "", err
}
return AskKindMerge + "." + part + "." + hash, nil
}
// ParseAskKey is the inverse of MergeAskKey: the kind, the group and the heads hash.
func ParseAskKey(key string) (kind, group, hash string, err error) {
parts := strings.Split(key, ".")
if len(parts) != 3 || parts[0] != AskKindMerge {
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key, merge.<group>.<heads hash>", key)
}
if parts[1] == "" {
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: the group name is empty", key)
}
if group, err = keys.UnescapeName(parts[1]); err != nil {
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: %w", key, err)
}
if !hashOK.MatchString(parts[2]) {
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: %q is not 12 hex digits", key, parts[2])
}
return parts[0], group, parts[2], nil
}
// SettleGroupPrefix begins the group of a settle ask (novox/hq ADR 0556 clause 5): a person's ask to settle deliveries held
// because their merge reached the trunk without a passing check. It is kept in the asks under MergeAskKey like a merge ask,
// so the teller carries it unchanged. Git forbids a colon in a branch name, so no pull request's group ever begins with it.
const SettleGroupPrefix = "settle:"
// SettleGroup is the group of the settle ask of these delivery ids: the prefix and twelve hex digits of the sha256 over the
// ids, sorted and joined by a newline, so the same deliveries asked again keep one key.
func SettleGroup(ids []string) string {
sorted := append([]string(nil), ids...)
sort.Strings(sorted)
sum := sha256.Sum256([]byte(strings.Join(sorted, "\n")))
return SettleGroupPrefix + hex.EncodeToString(sum[:])[:12]
}
// AskVerb is the act the options of an ask bind, by its group: a settle ask's `mesh-delivery.settle`, any other
// `mesh-delivery.merge`. The gate composes the act with it and the teller checks a warrant against it.
func AskVerb(group string) string {
if strings.HasPrefix(group, SettleGroupPrefix) {
return seat.Seat + "." + seat.VerbSettle
}
return seat.Seat + "." + VerbMerge
}
// AskID is the id the teller gives the router for the ask at askKey. The router's ids refuse dots, so in the readable part a
// dot becomes `-` and any other byte outside letters, digits, `_` and `-` becomes `_`; that part is cut to fit, and every id
// ends in `-` and the first 12 hex digits of the sha256 of the whole key, so two keys never give one id. It cannot be read
// back: the teller keeps the key beside it.
func AskID(askKey string) string {
const most = 64 // the router's bound on an id
const tail = 13 // "-" and 12 hex digits
readable := []byte(askKey)
for i, c := range readable {
switch {
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9':
case c == '.':
readable[i] = '-'
default: // a slash, an equals sign or any other byte the router's id refuses
readable[i] = '_'
}
}
if len(readable) > 0 && (readable[0] == '-' || readable[0] == '_') {
readable[0] = 'k' // an id starts with a letter or digit
}
if len(readable) > most-tail {
readable = readable[:most-tail]
}
sum := sha256.Sum256([]byte(askKey))
return string(readable) + "-" + hex.EncodeToString(sum[:])[:12]
}
// AskingID is the id of the asking-th writing of the ask at askKey. Asking 0 and 1 give AskID(askKey), so the first asking's
// id is unchanged; asking n of 2 or more gives the AskID form of askKey + "." + n, so each asking has an id of its own and the
// router, which drops an ask under an id it keeps as a redelivery, takes each as new and an answer to an earlier asking is
// never taken for a later one.
func AskingID(askKey string, asking int) string {
if asking < 2 {
return AskID(askKey)
}
return AskID(askKey + "." + strconv.Itoa(asking))
}
// The acts the gate owes the forge, the last part of an owed key.
const (
OwedStatus = "status"
OwedView = "view"
OwedNote = "note"
OwedComment = "comment"
)
// The status contexts the mesh owes the forge.
const (
StatusContextMergeGate = "mesh/merge-gate"
StatusContextDelivery = "mesh/delivery"
StatusContextDeliveryGroup = "mesh/delivery-group"
StatusContextRepoCheck = "mesh/repo-check"
StatusContextCheck = "mesh/check"
)
var statusContexts = []string{StatusContextMergeGate, StatusContextDelivery, StatusContextDeliveryGroup, StatusContextRepoCheck, StatusContextCheck}
// StatusContexts is the contexts, in a new slice.
func StatusContexts() []string { return append([]string(nil), statusContexts...) }
// The states a status the gate owes the forge may carry.
var statusStates = []string{"pending", "success", "failure", "error", "warning"}
func checkStatusContext(context string) error {
for _, c := range statusContexts {
if context == c {
return nil
}
}
return fmt.Errorf("meshdeliveryclient: the status context %q is not one of %s", context, strings.Join(statusContexts, ", "))
}
// ParseOwedAct splits an owed act into its kind and, for a status, its context, for a comment its sequence as digits. The act
// is `status=<context>`, `view`, `note` or `comment.<seq>`.
func ParseOwedAct(act string) (kind, context string, err error) {
kind, context, hasContext := strings.Cut(act, "=")
switch kind {
case OwedStatus:
if !hasContext {
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is a status without its context, status=<context>", act)
}
if err := checkStatusContext(context); err != nil {
return "", "", err
}
return kind, context, nil
case OwedView, OwedNote:
if hasContext {
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q takes no context", act)
}
return kind, "", nil
}
if kind, seq, ok := strings.Cut(act, "."); ok && kind == OwedComment {
if n, err := strconv.Atoi(seq); err != nil || n < 1 || strconv.Itoa(n) != seq {
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is a comment without its sequence, comment.<seq> with seq of 1 or more", act)
}
return OwedComment, seq, nil
}
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is not status=<context>, view, note or comment.<seq>", act)
}
// OwedCommentAct is the act of the seq-th comment on a head: `comment.<seq>`, seq of 1 or more, so a second comment has a key
// of its own and does not overwrite the first.
func OwedCommentAct(seq int) (string, error) {
if seq < 1 {
return "", fmt.Errorf("meshdeliveryclient: a comment's sequence is 1 or more, not %d", seq)
}
return OwedComment + "." + strconv.Itoa(seq), nil
}
func escapeRepository(repository string) (string, error) {
owner, name, ok := strings.Cut(repository, "/")
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
return "", fmt.Errorf("meshdeliveryclient: the repository %q is not <owner>/<name>, with exactly one slash", repository)
}
return keys.EscapeName(owner) + "/" + keys.EscapeName(name), nil
}
func unescapeRepository(part string) (string, error) {
owner, name, ok := strings.Cut(part, "/")
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
return "", fmt.Errorf("meshdeliveryclient: the repository %q is not <owner>/<name>, with exactly one slash", part)
}
o, err := keys.UnescapeName(owner)
if err != nil {
return "", err
}
n, err := keys.UnescapeName(name)
if err != nil {
return "", err
}
return o + "/" + n, nil
}
// OwedKey is the key, in the state owed, of an act the gate owes the forge on a pull request at a head:
// `<repository>.<number>.<head>.<act>`. The number is 0 for a commit no pull request names, and never negative.
func OwedKey(repository string, number int, head, act string) (string, error) {
repo, err := escapeRepository(repository)
if err != nil {
return "", err
}
if number < 0 {
return "", fmt.Errorf("meshdeliveryclient: an owed key's pull request number is 0 (none) or more, not %d", number)
}
if err := keys.CheckName("head", head); err != nil {
return "", err
}
if _, _, err := ParseOwedAct(act); err != nil {
return "", err
}
return repo + "." + strconv.Itoa(number) + "." + head + "." + act, nil
}
// ParseOwedKey is the inverse of OwedKey; act is whole, as `status=<context>` for a status.
func ParseOwedKey(key string) (repository string, number int, head, act string, err error) {
parts := strings.SplitN(key, ".", 4) // a comment's act holds a dot
if len(parts) != 4 {
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key, <repository>.<number>.<head>.<act>", key)
}
number, convErr := strconv.Atoi(parts[1])
if convErr != nil {
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %q is not a pull request number", key, parts[1])
}
if repository, err = unescapeRepository(parts[0]); err != nil {
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %w", key, err)
}
if _, err := OwedKey(repository, number, parts[2], parts[3]); err != nil {
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %w", key, err)
}
return repository, number, parts[2], parts[3], nil
}
// CheckOwed refuses an owed value the forge bridge could not perform: a status needs its context (one the mesh owes), a
// state of the list and a target; a view and a comment need text; a note needs text and a ref.
func CheckOwed(o seat.OwedByRepositoryNumberHeadActValue) error {
const who = "meshdeliveryclient: "
switch o.Act {
case OwedStatus:
if o.Context == "" {
return fmt.Errorf(who + "an owed status has no context")
}
if err := checkStatusContext(o.Context); err != nil {
return err
}
if o.Target == "" {
return fmt.Errorf(who + "an owed status has no target")
}
for _, s := range statusStates {
if o.State == s {
return nil
}
}
return fmt.Errorf(who+"the owed status state %q is not %s", o.State, strings.Join(statusStates, ", "))
case OwedView, OwedComment:
if o.Text == "" {
return fmt.Errorf(who+"an owed %s has no text", o.Act)
}
case OwedNote:
if o.Text == "" {
return fmt.Errorf(who + "an owed note has no text")
}
if o.Ref == "" {
return fmt.Errorf(who + "an owed note has no ref")
}
default:
return fmt.Errorf(who+"the owed act %q is not status, view, note or comment", o.Act)
}
return nil
}
@@ -0,0 +1,129 @@
package meshdeliveryclient
import (
"encoding/json"
"os"
"strings"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
// The keys, ids and acts equal what the gate and the teller have always written (the expected strings are the trunk's own
// output for the same input), so a value written before is read after.
func TestTheAsksAndOwedActsKeepTheirFormsAndRules(t *testing.T) {
heads := []Head{{Repository: "novox/a", Number: 3, Head: "abc"}, {Repository: "novox/b", Number: 1, Head: "def"}}
key, err := MergeAskKey("fix/1.2", heads)
if err != nil || key != "merge.fix/1=2e2.d106c7950d28" {
t.Fatalf("MergeAskKey = %q, %v", key, err)
}
if rev, _ := MergeAskKey("fix/1.2", []Head{heads[1], heads[0]}); rev != key {
t.Errorf("the heads' order changes the key: %q", rev)
}
if kind, group, hash, err := ParseAskKey(key); err != nil || kind != "merge" || group != "fix/1.2" || hash != "d106c7950d28" {
t.Errorf("ParseAskKey = %q %q %q %v", kind, group, hash, err)
}
for _, bad := range []string{"merge.g", "settle.g.d106c7950d28", "merge..d106c7950d28", "merge.g.D106C7950D28", "merge.g.d106"} {
if _, _, _, err := ParseAskKey(bad); err == nil {
t.Errorf("ParseAskKey(%q) reads a wrong key", bad)
}
}
if _, err := MergeAskKey("", heads); err == nil {
t.Error("an empty group keys")
}
if _, err := MergeAskKey("g", nil); err == nil {
t.Error("no heads keys")
}
if id := AskID(key); id != "merge-fix_1_2e2-d106c7950d28-400030402c88" || AskingID(key, 1) != id || AskingID(key, 0) != id {
t.Errorf("AskID = %q", id)
}
if id := AskingID(key, 3); id != "merge-fix_1_2e2-d106c7950d28-3-ea06879e48d8" || len(id) > 64 {
t.Errorf("AskingID(3) = %q", id)
}
if g := SettleGroup([]string{"b", "a"}); g != "settle:7e18f737311b" || g != SettleGroup([]string{"a", "b"}) {
t.Errorf("SettleGroup = %q", g)
}
if AskVerb(SettleGroup([]string{"a"})) != "mesh-delivery.settle" || AskVerb("fix/1") != "mesh-delivery.merge" {
t.Error("AskVerb binds the wrong act")
}
for act, want := range map[string]string{"status=mesh/check": "novox/a.3.abc.status=mesh/check", "view": "novox/a.3.abc.view", "comment.2": "novox/a.3.abc.comment.2"} {
k, err := OwedKey("novox/a", 3, "abc", act)
if err != nil || k != want {
t.Errorf("OwedKey(%q) = %q, %v", act, k, err)
}
if repo, n, head, a, err := ParseOwedKey(k); err != nil || repo != "novox/a" || n != 3 || head != "abc" || a != act {
t.Errorf("ParseOwedKey(%q) = %q %d %q %q %v", k, repo, n, head, a, err)
}
}
for _, act := range []string{"status", "status=mesh/other", "view=x", "comment", "comment.0", "comment.01", "other"} {
if _, err := OwedKey("novox/a", 3, "abc", act); err == nil {
t.Errorf("OwedKey takes the act %q", act)
}
}
if _, err := OwedKey("novox", 0, "abc", "view"); err == nil {
t.Error("a repository with no slash keys")
}
if _, err := OwedKey("novox/a", -1, "abc", "view"); err == nil {
t.Error("a negative number keys")
}
if act, err := OwedCommentAct(2); err != nil || act != "comment.2" {
t.Errorf("OwedCommentAct = %q, %v", act, err)
}
type owed = seat.OwedByRepositoryNumberHeadActValue
for name, c := range map[string]struct {
v owed
says string
}{
"a good status": {owed{Act: "status", Context: StatusContextCheck, Target: "http://x", State: "success"}, ""},
"a good note": {owed{Act: "note", Text: "t", Ref: "mesh-plan"}, ""},
"a status no target": {owed{Act: "status", Context: StatusContextCheck, State: "success"}, "no target"},
"a status wrong state": {owed{Act: "status", Context: StatusContextCheck, Target: "x", State: "done"}, "state"},
"a status no context": {owed{Act: "status", Target: "x", State: "success"}, "no context"},
"a view no text": {owed{Act: "view"}, "no text"},
"a note no ref": {owed{Act: "note", Text: "t"}, "no ref"},
"an unknown act": {owed{Act: "merge"}, "not status"},
} {
err := CheckOwed(c.v)
if (c.says == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.says)) {
t.Errorf("%s: CheckOwed = %v, want %q", name, err, c.says)
}
}
}
// The removed verbs are the optional rows of the seat's definition that say they were removed, each with the replacement
// the definition gives: the list and the definition cannot drift apart.
func TestTheRemovedVerbsAreTheDefinitionsRemovedRows(t *testing.T) {
raw, err := os.ReadFile("../../../seats/mesh-delivery/seat.json")
if err != nil {
t.Fatal(err)
}
var def struct {
Verbs []struct {
Name string `json:"name"`
Description string `json:"description"`
Optional bool `json:"optional"`
} `json:"verbs"`
}
if err := json.Unmarshal(raw, &def); err != nil {
t.Fatal(err)
}
removed := RemovedVerbs()
rows := 0
for _, v := range def.Verbs {
if !strings.HasPrefix(v.Description, RemovedPrefix) {
if _, in := removed[v.Name]; in {
t.Errorf("%s is in the removed list, the definition keeps it", v.Name)
}
continue
}
rows++
if want, in := removed[v.Name]; !in || !v.Optional || v.Description != RemovedPrefix+want {
t.Errorf("%s: in the list %v, optional %v, described %q, want %q", v.Name, in, v.Optional, v.Description, RemovedPrefix+want)
}
}
if rows != len(removed) || rows == 0 {
t.Errorf("the definition removes %d verbs, the list names %d", rows, len(removed))
}
}
@@ -0,0 +1,407 @@
package meshdeliveryclient
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The values of the seat's states `source`, `merges`, `asks`, `owed` and `turns`, in the seat's types: each is stamped
// with Version when encoded, read strictly (an unknown field refused) and its version checked when decoded, and its rules
// held on both sides, so the gate that writes them and the readers (the forge bridge, the teller, the assigner, the view)
// agree. What the schema cannot say is written here once (ADR 0625 §4).
// Version is the layout version of the seat's values.
const Version = 1
// MergeState is the state of a pull request in a merge.
type MergeState string
// The states a merge's pull request is in.
const (
MergeMerging MergeState = "merging"
MergeMerged MergeState = "merged"
MergeRefused MergeState = "refused"
)
// SourceMainKey is the one key of the state source.
const SourceMainKey = "main"
func encodeValue(v any, check error, stamp func()) ([]byte, error) {
if check != nil {
return nil, check
}
stamp()
return json.Marshal(v)
}
// ApprovedKey is the key, in a merge's approved heads, of a pull request: `<repository>@<number>`.
func ApprovedKey(repository string, number int) string {
return repository + "@" + strconv.Itoa(number)
}
// TurnsKey is the key, in the state turns, of a repository: `<owner>.<name>`, lower-case and escaped.
func TurnsKey(repository string) (string, error) {
repository = strings.ToLower(repository)
part, err := escapeRepository(repository)
if err != nil {
return "", err
}
return strings.Replace(part, "/", ".", 1), nil
}
// ParseTurnsKey is the inverse of TurnsKey; a key that does not name a lower-case <owner>/<name> is refused.
func ParseTurnsKey(key string) (string, error) {
owner, name, ok := strings.Cut(key, ".")
if !ok || owner == "" || name == "" || strings.Contains(name, ".") {
return "", fmt.Errorf("meshdeliveryclient: %q is not a turns key, <owner>.<name>", key)
}
repository, err := unescapeRepository(owner + "/" + name)
if err != nil {
return "", fmt.Errorf("meshdeliveryclient: %q is not a turns key: %w", key, err)
}
if again, err := TurnsKey(repository); err != nil || again != key {
return "", fmt.Errorf("meshdeliveryclient: %q is not a turns key: it does not name a lowercase <owner>/<name>", key)
}
return repository, nil
}
// ----- source
// CheckSource refuses a source naming no repository, a repository with no name or an empty head.
func CheckSource(s seat.SourceByMainValue) error {
if len(s.Heads) == 0 {
return fmt.Errorf("meshdeliveryclient: a source names no repository")
}
for repository, head := range s.Heads {
if repository == "" {
return fmt.Errorf("meshdeliveryclient: a source names a repository with no name")
}
if head == "" {
return fmt.Errorf("meshdeliveryclient: the source's head of %s is empty", repository)
}
}
return nil
}
// EncodeSource is s as the value of the key main.
func EncodeSource(s seat.SourceByMainValue) ([]byte, error) {
return encodeValue(&s, CheckSource(s), func() { s.V = Version })
}
// DecodeSource reads the value of the key main.
func DecodeSource(data []byte) (seat.SourceByMainValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.SourceByMainValue{}, err
}
s, err := seat.DecodeSourceByMainValue(data)
if err != nil {
return s, fmt.Errorf("meshdeliveryclient: a source does not fit its shape: %w", err)
}
return s, CheckSource(s)
}
// SameSource says whether two sources are the same.
func SameSource(a, b seat.SourceByMainValue) bool { return reflect.DeepEqual(a, b) }
// ----- merge
// CheckMerge refuses a merge naming no group, ask or pull request, a pull request outside the approved heads or at
// another head than the Approve was for, and a state that lacks what it needs: merged its commit, refused the forge's words.
func CheckMerge(m seat.MergesByGroupValue) error {
if m.Group == "" {
return fmt.Errorf("meshdeliveryclient: a merge names no group")
}
if m.Ask == "" {
return fmt.Errorf("meshdeliveryclient: a merge names no ask")
}
if len(m.Pulls) == 0 {
return fmt.Errorf("meshdeliveryclient: a merge names no pull request")
}
for key, sha := range m.Approved {
if sha == "" {
return fmt.Errorf("meshdeliveryclient: the approved head of %s is empty", key)
}
}
for _, p := range m.Pulls {
who := fmt.Sprintf("%s#%d", p.Repository, p.Number)
if len(m.Approved) > 0 {
sha, ok := m.Approved[ApprovedKey(p.Repository, int(p.Number))]
if !ok {
return fmt.Errorf("meshdeliveryclient: the pull request %s is handed to the merge but is not among the approved heads", who)
}
if sha != p.Head {
return fmt.Errorf("meshdeliveryclient: the pull request %s is handed at head %q but the Approve was for %q", who, p.Head, sha)
}
}
switch MergeState(p.State) {
case MergeMerging:
case MergeMerged:
if p.Commit == "" {
return fmt.Errorf("meshdeliveryclient: the merged pull request %s has no merge commit", who)
}
case MergeRefused:
if p.Words == "" {
return fmt.Errorf("meshdeliveryclient: the refused pull request %s has no words of the forge", who)
}
default:
return fmt.Errorf("meshdeliveryclient: the state %q of %s is not merging, merged or refused", p.State, who)
}
}
return nil
}
// EncodeMerge is m as the value of a group's key.
func EncodeMerge(m seat.MergesByGroupValue) ([]byte, error) {
return encodeValue(&m, CheckMerge(m), func() { m.V = Version })
}
// DecodeMerge reads the value of a group's key.
func DecodeMerge(data []byte) (seat.MergesByGroupValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.MergesByGroupValue{}, err
}
m, err := seat.DecodeMergesByGroupValue(data)
if err != nil {
return m, fmt.Errorf("meshdeliveryclient: a merge does not fit its shape: %w", err)
}
return m, CheckMerge(m)
}
// SameMerge says whether two merges are the same.
func SameMerge(a, b seat.MergesByGroupValue) bool { return reflect.DeepEqual(a, b) }
// ----- ask
// HeadsOf are an ask's heads as Heads (for HeadsHash and MergeAskKey).
func HeadsOf(items []seat.AsksByMergeGroupHeadsValueHeadsItem) []Head {
out := make([]Head, len(items))
for i, h := range items {
out[i] = Head{Repository: h.Repository, Number: int(h.Number), Head: h.Head}
}
return out
}
// CheckAsk refuses an ask with no group or heads, a negative count, or an ask that is no object.
func CheckAsk(a seat.AsksByMergeGroupHeadsValue) error {
if a.Asking < 0 {
return fmt.Errorf("meshdeliveryclient: the ask's asking %d is negative", a.Asking)
}
if _, err := MergeAskKey(a.Group, HeadsOf(a.Heads)); err != nil {
return fmt.Errorf("meshdeliveryclient: an ask needs its group and at least one pull request's head: %w", err)
}
raw, err := json.Marshal(a.Ask)
var fields map[string]json.RawMessage
if err != nil || len(bytes.TrimSpace(raw)) == 0 || json.Unmarshal(raw, &fields) != nil || fields == nil {
return fmt.Errorf("meshdeliveryclient: the value carries no router's ask")
}
return nil
}
// EncodeAsk is a as the value of a merge ask's key; a.Ask may be a json.RawMessage.
func EncodeAsk(a seat.AsksByMergeGroupHeadsValue) ([]byte, error) {
return encodeValue(&a, CheckAsk(a), func() { a.V = Version })
}
// DecodeAsk reads the value of a merge ask's key; its Ask is the router's ask as a decoded object.
func DecodeAsk(data []byte) (seat.AsksByMergeGroupHeadsValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.AsksByMergeGroupHeadsValue{}, err
}
a, err := seat.DecodeAsksByMergeGroupHeadsValue(data)
if err != nil {
return a, fmt.Errorf("meshdeliveryclient: an ask does not fit its shape: %w", err)
}
return a, CheckAsk(a)
}
// SameAsk says whether two asks are the same (the router's ask by its content).
func SameAsk(a, b seat.AsksByMergeGroupHeadsValue) bool { return reflect.DeepEqual(a, b) }
// ----- owed
// EncodeOwed is o as the value of an owed key, after CheckOwed.
func EncodeOwed(o seat.OwedByRepositoryNumberHeadActValue) ([]byte, error) {
return encodeValue(&o, CheckOwed(o), func() { o.V = Version })
}
// DecodeOwed reads the value of an owed key.
func DecodeOwed(data []byte) (seat.OwedByRepositoryNumberHeadActValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.OwedByRepositoryNumberHeadActValue{}, err
}
o, err := seat.DecodeOwedByRepositoryNumberHeadActValue(data)
if err != nil {
return o, fmt.Errorf("meshdeliveryclient: an owed value does not fit its shape: %w", err)
}
return o, CheckOwed(o)
}
// SameOwed says whether two owed values are the same.
func SameOwed(a, b seat.OwedByRepositoryNumberHeadActValue) bool { return a == b }
// ----- turns
// CheckTurn refuses a turn whose repository is not lower-case, that names no delivery, or whose deliveries repeat a heads
// hash, lack their times, are out of the order of the Approve, or name a wait without a verdict key.
func CheckTurn(t seat.TurnsByRepositoryValue) error {
repository := strings.ToLower(t.Repository)
if repository != t.Repository {
return fmt.Errorf("meshdeliveryclient: the turn's repository %q is not lowercase", t.Repository)
}
if _, err := escapeRepository(repository); err != nil {
return err
}
if len(t.Waiting) == 0 {
return fmt.Errorf("meshdeliveryclient: the turn of %s names no delivery: its key is deleted instead", t.Repository)
}
seen := map[string]bool{}
for i, w := range t.Waiting {
if w.Group == "" {
return fmt.Errorf("meshdeliveryclient: a delivery in the turn of %s names no group", t.Repository)
}
if !hashOK.MatchString(w.Heads) {
return fmt.Errorf("meshdeliveryclient: the heads hash %q in the turn of %s is not 12 lowercase hex digits", w.Heads, t.Repository)
}
if seen[w.Heads] {
return fmt.Errorf("meshdeliveryclient: the turn of %s names the heads %s twice", t.Repository, w.Heads)
}
seen[w.Heads] = true
if w.Approved.IsZero() || w.Since.IsZero() {
return fmt.Errorf("meshdeliveryclient: the delivery %s in the turn of %s says no time of its Approve or of its wait", w.Group, t.Repository)
}
if i > 0 && w.Approved.Before(t.Waiting[i-1].Approved) {
return fmt.Errorf("meshdeliveryclient: the turn of %s is not in the order of the Approve: %s was approved before %s", t.Repository, w.Group, t.Waiting[i-1].Group)
}
if w.WaitsFor != "" {
if _, _, err := parseVerdictKey(w.WaitsFor); err != nil {
return fmt.Errorf("meshdeliveryclient: the delivery %s in the turn of %s waits for what is not a verdict key: %w", w.Group, t.Repository, err)
}
} else if (w.WaitsSince != nil && !w.WaitsSince.IsZero()) || w.Step != "" {
return fmt.Errorf("meshdeliveryclient: the delivery %s in the turn of %s says when and where a check runs, and names no check", w.Group, t.Repository)
}
}
return nil
}
// parseVerdictKey is the checker's verdict key `<group>.<heads hash>` (the checker's client has the same rule over its
// own seat; a client uses its own seat's contract alone).
func parseVerdictKey(key string) (group, heads string, err error) {
i := strings.LastIndex(key, ".")
if i < 0 || !hashOK.MatchString(key[i+1:]) || i == 0 || strings.Contains(key[:i], ".") {
return "", "", fmt.Errorf("%q is not a verdict key, <group>.<heads hash>", key)
}
if group, err = keys.UnescapeName(key[:i]); err != nil {
return "", "", err
}
return group, key[i+1:], nil
}
// TurnHolds says whether a delivery at these heads is not in the turn while the turn names others: the delivery's
// check or merge holds back for the one before it.
func TurnHolds(t seat.TurnsByRepositoryValue, heads string) bool {
if len(t.Waiting) == 0 {
return false
}
for _, w := range t.Waiting {
if w.Heads == heads {
return false
}
}
return true
}
// EncodeTurn is t as the value of a repository's key.
func EncodeTurn(t seat.TurnsByRepositoryValue) ([]byte, error) {
if err := CheckTurn(t); err != nil {
return nil, err
}
t.V = Version
return json.Marshal(t)
}
// DecodeTurn reads the value of a repository's key.
func DecodeTurn(data []byte) (seat.TurnsByRepositoryValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.TurnsByRepositoryValue{}, err
}
t, err := seat.DecodeTurnsByRepositoryValue(data)
if err != nil {
return t, fmt.Errorf("meshdeliveryclient: a turn does not fit its shape: %w", err)
}
return t, CheckTurn(t)
}
// SameTurn says whether two turns are the same, a time by the instant it names.
func SameTurn(a, b seat.TurnsByRepositoryValue) bool {
if len(a.Waiting) != len(b.Waiting) || a.Repository != b.Repository || a.V != b.V {
return false
}
for i := range a.Waiting {
x, y := a.Waiting[i], b.Waiting[i]
if !x.Approved.Equal(y.Approved) || !x.Since.Equal(y.Since) || !sameTime(x.WaitsSince, y.WaitsSince) {
return false
}
x.Approved, y.Approved, x.Since, y.Since, x.WaitsSince, y.WaitsSince = time.Time{}, time.Time{}, time.Time{}, time.Time{}, nil, nil
if !reflect.DeepEqual(x, y) {
return false
}
}
return true
}
func sameTime(a, b *time.Time) bool {
if a == nil || a.IsZero() {
return b == nil || b.IsZero()
}
return b != nil && a.Equal(*b)
}
// Answer is the operator's answer to a merge ask as the gate acts on it: the option chosen, the warrant that proves who
// answered, and the words said. It is the teller's state `answers` under the ask's key (the teller's own record; this is
// its layout for the gate that reads it).
type Answer struct {
V int `json:"v"`
Option string `json:"option"`
Warrant string `json:"warrant"`
Words string `json:"words"`
}
// CheckAnswer refuses an answer lacking its option, warrant or words.
func CheckAnswer(a Answer) error {
for _, f := range []struct{ name, value string }{{"option", a.Option}, {"warrant", a.Warrant}, {"words", a.Words}} {
if f.value == "" {
return fmt.Errorf("meshdeliveryclient: an answer has no %s", f.name)
}
}
return nil
}
// EncodeAnswer is a as the value of an answer's key.
func EncodeAnswer(a Answer) ([]byte, error) {
return encodeValue(&a, CheckAnswer(a), func() { a.V = Version })
}
// DecodeAnswer reads the value of an answer's key.
func DecodeAnswer(data []byte) (Answer, error) {
if err := keys.CheckVersion(data); err != nil {
return Answer{}, err
}
var a Answer
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&a); err != nil {
return Answer{}, fmt.Errorf("meshdeliveryclient: an answer does not fit its shape: %w", err)
}
return a, CheckAnswer(a)
}
// SameAnswer says whether two answers are the same.
func SameAnswer(a, b Answer) bool { return a == b }
@@ -0,0 +1,186 @@
package meshdeliveryclient
import (
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// sameJSON says whether two documents hold the same values (the generated types write their fields in alphabetical
// order, the old package's in declaration order: the bytes differ, the documents do not).
func sameJSON(t *testing.T, got []byte, want string) {
t.Helper()
var g, w any
if err := json.Unmarshal(got, &g); err != nil {
t.Fatalf("%s: %v", got, err)
}
if err := json.Unmarshal([]byte(want), &w); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(g, w) {
t.Errorf("got %s\nwant %s", got, want)
}
}
// The goldens are the output of go/delivery's Encode* before it was deleted; each value must encode to the same
// document and decode back to what it was.
func TestGateValuesEncodeAsGoDeliveryDid(t *testing.T) {
ap := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)
wait := ap.Add(2 * time.Minute)
for _, c := range []struct {
name string
encode func() ([]byte, error)
decode func([]byte) (any, error)
value any
golden string
}{
{"source", func() ([]byte, error) {
return EncodeSource(seat.SourceByMainValue{Group: "fix/1.2", Heads: map[string]string{"novox/a": "aaaa", "novox/b": "bbbb"}})
}, func(b []byte) (any, error) { return DecodeSource(b) },
seat.SourceByMainValue{V: 1, Group: "fix/1.2", Heads: map[string]string{"novox/a": "aaaa", "novox/b": "bbbb"}},
`{"v":1,"group":"fix/1.2","heads":{"novox/a":"aaaa","novox/b":"bbbb"}}`},
{"merge", func() ([]byte, error) {
return EncodeMerge(seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: map[string]string{"novox/a@7": "h1"},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 7, Head: "h1", State: "merged", Commit: "c1"}}})
}, func(b []byte) (any, error) { return DecodeMerge(b) },
seat.MergesByGroupValue{V: 1, Group: "g", Ask: "merge.g.0123456789ab", Approved: map[string]string{"novox/a@7": "h1"},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 7, Head: "h1", State: "merged", Commit: "c1"}}},
`{"v":1,"group":"g","ask":"merge.g.0123456789ab","approved":{"novox/a@7":"h1"},"pulls":[{"repository":"novox/a","number":7,"head":"h1","state":"merged","commit":"c1"}]}`},
{"ask", func() ([]byte, error) {
return EncodeAsk(seat.AsksByMergeGroupHeadsValue{Group: "g", Asking: 2, Ask: json.RawMessage(`{"headline":"x"}`),
Heads: []seat.AsksByMergeGroupHeadsValueHeadsItem{{Repository: "novox/a", Number: 7, Head: "h1"}}})
}, func(b []byte) (any, error) { return DecodeAsk(b) },
seat.AsksByMergeGroupHeadsValue{V: 1, Group: "g", Asking: 2, Ask: map[string]any{"headline": "x"},
Heads: []seat.AsksByMergeGroupHeadsValueHeadsItem{{Repository: "novox/a", Number: 7, Head: "h1"}}},
`{"v":1,"group":"g","heads":[{"repository":"novox/a","number":7,"head":"h1"}],"ask":{"headline":"x"},"asking":2}`},
{"owed", func() ([]byte, error) {
return EncodeOwed(seat.OwedByRepositoryNumberHeadActValue{Act: OwedStatus, Context: StatusContextCheck, Target: "t", State: "pending"})
}, func(b []byte) (any, error) { return DecodeOwed(b) },
seat.OwedByRepositoryNumberHeadActValue{V: 1, Act: OwedStatus, Context: StatusContextCheck, Target: "t", State: "pending"},
`{"v":1,"act":"status","context":"mesh/check","target":"t","state":"pending"}`},
{"turn", func() ([]byte, error) {
return EncodeTurn(seat.TurnsByRepositoryValue{Repository: "novox/a", Waiting: []seat.TurnsByRepositoryValueWaitingItem{{
Group: "g", Heads: "0123456789ab", Ask: "k", Approved: ap, Since: ap.Add(time.Minute), WaitsFor: "g.0123456789ab", WaitsSince: &wait, Step: "s"}}})
}, func(b []byte) (any, error) { return DecodeTurn(b) },
seat.TurnsByRepositoryValue{V: 1, Repository: "novox/a", Waiting: []seat.TurnsByRepositoryValueWaitingItem{{
Group: "g", Heads: "0123456789ab", Ask: "k", Approved: ap, Since: ap.Add(time.Minute), WaitsFor: "g.0123456789ab", WaitsSince: &wait, Step: "s"}}},
`{"v":1,"repository":"novox/a","waiting":[{"group":"g","heads":"0123456789ab","ask":"k","approved":"2026-11-05T10:00:00Z","since":"2026-11-05T10:01:00Z","waits-for":"g.0123456789ab","waits-since":"2026-11-05T10:02:00Z","step":"s"}]}`},
{"answer", func() ([]byte, error) { return EncodeAnswer(Answer{Option: "merge", Warrant: "w", Words: "ok"}) },
func(b []byte) (any, error) { return DecodeAnswer(b) }, Answer{V: 1, Option: "merge", Warrant: "w", Words: "ok"},
`{"v":1,"option":"merge","warrant":"w","words":"ok"}`},
} {
raw, err := c.encode()
if err != nil {
t.Fatalf("%s: %v", c.name, err)
}
sameJSON(t, raw, c.golden)
back, err := c.decode([]byte(c.golden))
if err != nil || !reflect.DeepEqual(back, c.value) {
t.Errorf("%s decoded %+v, %v; want %+v", c.name, back, err, c.value)
}
}
}
func TestGateKeysAsGoDeliveryHadThem(t *testing.T) {
for in, want := range map[string]string{"novox/Mesh-sdk": "novox.mesh-sdk", "novox/mesh-sdk": "novox.mesh-sdk", "novox/mesh.x": "novox.mesh=2ex"} {
got, err := TurnsKey(in)
if err != nil || got != want {
t.Errorf("TurnsKey(%q) = %q, %v; want %q", in, got, err, want)
}
}
if r, err := ParseTurnsKey("novox.mesh=2ex"); err != nil || r != "novox/mesh.x" {
t.Errorf("%q %v", r, err)
}
for _, bad := range []string{"novox", "novox.Mesh", "a.b.c", ".x", "novox.mesh-sdk."} {
if _, err := ParseTurnsKey(bad); err == nil {
t.Errorf("%q parses as a turns key", bad)
}
}
if _, err := TurnsKey("novox"); err == nil {
t.Error("a repository with no slash keys")
}
if k := ApprovedKey("novox/a", 7); k != "novox/a@7" {
t.Error(k)
}
if SourceMainKey != seat.SourceByMainKey() {
t.Error("the source's key is the seat's")
}
}
func TestGateValuesRefuseWhatTheirRulesRefuse(t *testing.T) {
ap := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)
pull := seat.MergesByGroupValuePullsItem{Repository: "novox/a", Number: 7, Head: "h1", State: "merging"}
merge := func(edit func(*seat.MergesByGroupValue)) error {
m := seat.MergesByGroupValue{Group: "g", Ask: "k", Pulls: []seat.MergesByGroupValuePullsItem{pull}}
edit(&m)
_, err := EncodeMerge(m)
return err
}
turn := func(edit func(*seat.TurnsByRepositoryValue)) error {
tv := seat.TurnsByRepositoryValue{Repository: "novox/a", Waiting: []seat.TurnsByRepositoryValueWaitingItem{{Group: "g", Heads: "0123456789ab", Approved: ap, Since: ap}}}
edit(&tv)
_, err := EncodeTurn(tv)
return err
}
for name, c := range map[string]struct {
err error
want string
}{
"merge: no group": {merge(func(m *seat.MergesByGroupValue) { m.Group = "" }), "no group"},
"merge: no pull": {merge(func(m *seat.MergesByGroupValue) { m.Pulls = nil }), "no pull request"},
"merge: not approved": {merge(func(m *seat.MergesByGroupValue) { m.Approved = map[string]string{"novox/b@1": "x"} }), "not among the approved"},
"merge: other head": {merge(func(m *seat.MergesByGroupValue) { m.Approved = map[string]string{"novox/a@7": "x"} }), "Approve was for"},
"merge: merged no commit": {merge(func(m *seat.MergesByGroupValue) { m.Pulls[0].State = "merged" }), "no merge commit"},
"merge: refused no words": {merge(func(m *seat.MergesByGroupValue) { m.Pulls[0].State = "refused" }), "no words"},
"merge: unknown state": {merge(func(m *seat.MergesByGroupValue) { m.Pulls[0].State = "lost" }), "not merging, merged or refused"},
"turn: upper case": {turn(func(v *seat.TurnsByRepositoryValue) { v.Repository = "Novox/a" }), "not lowercase"},
"turn: empty": {turn(func(v *seat.TurnsByRepositoryValue) { v.Waiting = nil }), "names no delivery"},
"turn: bad hash": {turn(func(v *seat.TurnsByRepositoryValue) { v.Waiting[0].Heads = "xyz" }), "12 lowercase hex"},
"turn: no time": {turn(func(v *seat.TurnsByRepositoryValue) { v.Waiting[0].Since = time.Time{} }), "no time"},
"turn: step with no wait": {turn(func(v *seat.TurnsByRepositoryValue) { v.Waiting[0].Step = "s" }), "names no check"},
"turn: not a verdict key": {turn(func(v *seat.TurnsByRepositoryValue) { v.Waiting[0].WaitsFor = "nope" }), "not a verdict key"},
} {
if c.err == nil || !strings.Contains(c.err.Error(), c.want) {
t.Errorf("%s: %v, want %q", name, c.err, c.want)
}
}
if _, err := EncodeSource(seat.SourceByMainValue{Group: "g"}); err == nil {
t.Error("an empty source encodes")
}
if _, err := EncodeAsk(seat.AsksByMergeGroupHeadsValue{Group: "g", Ask: "text", Heads: []seat.AsksByMergeGroupHeadsValueHeadsItem{{Repository: "novox/a", Number: 1, Head: "h"}}}); err == nil {
t.Error("an ask that is no object encodes")
}
if _, err := EncodeAnswer(Answer{Option: "merge"}); err == nil {
t.Error("an answer with no warrant encodes")
}
if _, err := DecodeSource([]byte(`{"v":2,"group":"g","heads":{"a":"b"}}`)); !errors.Is(err, keys.ErrUnknownVersion) {
t.Errorf("another version: %v", err)
}
if _, err := DecodeMerge([]byte(`{"v":1,"group":"g","ask":"k","pulls":[],"extra":1}`)); err == nil {
t.Error("an unknown field decodes")
}
}
func TestTurnHoldsAndSameTurn(t *testing.T) {
ap := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)
tv := seat.TurnsByRepositoryValue{V: 1, Repository: "novox/a", Waiting: []seat.TurnsByRepositoryValueWaitingItem{{Group: "g", Heads: "0123456789ab", Approved: ap, Since: ap}}}
if TurnHolds(tv, "0123456789ab") || !TurnHolds(tv, "ba9876543210") || TurnHolds(seat.TurnsByRepositoryValue{}, "x") {
t.Error("a turn holds a delivery that is not in it while others wait")
}
other := tv
other.Waiting = []seat.TurnsByRepositoryValueWaitingItem{tv.Waiting[0]}
other.Waiting[0].Approved = ap.In(time.FixedZone("x", 3600))
if !SameTurn(tv, other) {
t.Error("the same instant in another zone is another turn")
}
other.Waiting[0].Since = ap.Add(time.Second)
if SameTurn(tv, other) {
t.Error("another instant is the same turn")
}
}
@@ -0,0 +1,17 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
)
require (
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
@@ -0,0 +1,12 @@
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -0,0 +1,18 @@
package meshdeliveryclient
// RemovedPrefix opens the description of a verb release 6 removed from the seat mesh-delivery (novox/hq ADR 0539 clause 17):
// the definition keeps it as an optional row with no input, so the holder of before and the holder of release 6 both hold
// the seat, and a caller is answered by name with its replacement.
const RemovedPrefix = "Removed in R6 (ADR 0539): refused by name, naming its replacement: "
// RemovedVerbs are the seat's verbs release 6 removed, each with its replacement in words: with no plan the gate keeps the
// merge's states and derives nothing after the merge. A new map each call.
func RemovedVerbs() map[string]string {
return map[string]string{
"times": "the delivery view (nox-delivery): each machine's want and have, and since when",
"what-if": "the merge check's composition, which composes every machine a change touches",
"release": "the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view",
"stop": "a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)",
"retire-history": "nothing to retire: the gate keeps no delivery after its merge",
}
}
@@ -0,0 +1,63 @@
// Package npmpackageregistryclient is the npm-package-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51
// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part,
// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a
// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is
// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the
// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport.
package npmpackageregistryclient
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry"
"git.novox.be/novox/mesh-delivery/sdk/parts"
)
// Call asks one verb of the seat, `seat:<seat>.<verb>`, with a body, and answers its JSON.
type Call func(key string, body any) (json.RawMessage, error)
// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were
// sent, and the chunks they took.
type Sent struct {
Transfer string
parts.Sent
}
// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in
// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes
// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read.
// Its outcome, published or refused, is the holder's event, which the caller waits for.
func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) {
open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) {
in.Parts = make([]seat.Part, 0, len(ps))
for _, p := range ps {
in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size})
}
raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in)
if err != nil {
return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err)
}
var a seat.PublishAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err)
}
return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil
}
chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error {
raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part,
Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)})
if err != nil {
return fmt.Errorf("%s's publish-part: %w", seat.Seat, err)
}
var a seat.PublishPartAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err)
}
return nil
}
sent, err := parts.Send(ctx, open, chunk, ps)
return Sent{Transfer: in.Transfer, Sent: sent}, err
}
@@ -0,0 +1,57 @@
package npmpackageregistryclient
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry"
"git.novox.be/novox/mesh-delivery/sdk/parts"
)
// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the
// holder lacks travels by publish-part, its bytes whole, the last chunk marked.
func TestPublishTalksToItsOwnSeat(t *testing.T) {
held, lacked := []byte("held"), []byte("lacked")
part := func(b []byte) parts.Part {
sum := sha256.Sum256(b)
return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)),
Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }}
}
ps := []parts.Part{part(held), part(lacked)}
var asked []string
var got []byte
call := func(key string, body any) (json.RawMessage, error) {
asked = append(asked, key)
raw, _ := json.Marshal(body)
switch key {
case "seat:npm-package-registry.publish":
var in seat.PublishInput
_ = json.Unmarshal(raw, &in)
if len(in.Parts) != 2 {
t.Errorf("publish names %d parts, want 2", len(in.Parts))
}
return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}})
case "seat:npm-package-registry.publish-part":
var in seat.PublishPartInput
_ = json.Unmarshal(raw, &in)
data, _ := base64.StdEncoding.DecodeString(in.Data)
if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" {
t.Errorf("publish-part %+v", in)
}
got = append(got, data...)
return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part})
}
t.Fatalf("asked %s", key)
return nil, nil
}
sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps)
if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 {
t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err)
}
}
@@ -0,0 +1,18 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
)
require (
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
@@ -0,0 +1,12 @@
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -0,0 +1,63 @@
// Package ociartifactregistryclient is the oci-artifact-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51
// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part,
// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a
// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is
// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the
// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport.
package ociartifactregistryclient
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry"
"git.novox.be/novox/mesh-delivery/sdk/parts"
)
// Call asks one verb of the seat, `seat:<seat>.<verb>`, with a body, and answers its JSON.
type Call func(key string, body any) (json.RawMessage, error)
// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were
// sent, and the chunks they took.
type Sent struct {
Transfer string
parts.Sent
}
// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in
// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes
// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read.
// Its outcome, published or refused, is the holder's event, which the caller waits for.
func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) {
open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) {
in.Parts = make([]seat.Part, 0, len(ps))
for _, p := range ps {
in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size})
}
raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in)
if err != nil {
return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err)
}
var a seat.PublishAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err)
}
return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil
}
chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error {
raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part,
Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)})
if err != nil {
return fmt.Errorf("%s's publish-part: %w", seat.Seat, err)
}
var a seat.PublishPartAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err)
}
return nil
}
sent, err := parts.Send(ctx, open, chunk, ps)
return Sent{Transfer: in.Transfer, Sent: sent}, err
}
@@ -0,0 +1,57 @@
package ociartifactregistryclient
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry"
"git.novox.be/novox/mesh-delivery/sdk/parts"
)
// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the
// holder lacks travels by publish-part, its bytes whole, the last chunk marked.
func TestPublishTalksToItsOwnSeat(t *testing.T) {
held, lacked := []byte("held"), []byte("lacked")
part := func(b []byte) parts.Part {
sum := sha256.Sum256(b)
return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)),
Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }}
}
ps := []parts.Part{part(held), part(lacked)}
var asked []string
var got []byte
call := func(key string, body any) (json.RawMessage, error) {
asked = append(asked, key)
raw, _ := json.Marshal(body)
switch key {
case "seat:oci-artifact-registry.publish":
var in seat.PublishInput
_ = json.Unmarshal(raw, &in)
if len(in.Parts) != 2 {
t.Errorf("publish names %d parts, want 2", len(in.Parts))
}
return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}})
case "seat:oci-artifact-registry.publish-part":
var in seat.PublishPartInput
_ = json.Unmarshal(raw, &in)
data, _ := base64.StdEncoding.DecodeString(in.Data)
if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" {
t.Errorf("publish-part %+v", in)
}
got = append(got, data...)
return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part})
}
t.Fatalf("asked %s", key)
return nil, nil
}
sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps)
if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 {
t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err)
}
}
@@ -0,0 +1,18 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/ociartifactregistryclient
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
)
require (
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
@@ -0,0 +1,12 @@
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -0,0 +1,528 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
// Package containerimageregistry is the contract of the seat "container-image-registry", defined whole (seats/container-image-registry/seat.json): The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image.
package containerimageregistry
import (
"bytes"
"encoding/json"
"errors"
"strings"
)
// Seat is the seat's name.
const Seat = "container-image-registry"
const (
VerbPublish = "publish"
VerbPublishPart = "publish-part"
VerbVersions = "versions"
VerbRemove = "remove"
VerbMirror = "mirror"
VerbTags = "tags"
VerbHold = "hold"
VerbRelease = "release"
VerbList = "list"
)
const (
EventPublished = "published"
EventRefused = "refused"
EventRemoved = "removed"
EventKept = "kept"
EventHeld = "held"
EventReleased = "released"
)
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
func Verbs() []string {
return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbMirror, VerbTags, VerbHold, VerbRelease, VerbList}
}
// Events is every event published on the seat's subject: a new copy each call.
func Events() []string {
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.
const (
Family = "container-image-registry.events"
FamilyMaxAgeDays = 7
)
// SubjectPublished is where published is published.
const SubjectPublished = "mesh.seat.container-image-registry.event.published"
// SubjectRefused is where refused is published.
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"
// SubjectReleased is where released is published.
const SubjectReleased = "mesh.seat.container-image-registry.event.released"
const (
StateHolds = "holds"
StateWindows = "windows"
)
// HoldsBySeatNameDigestKey is the key of one value of the state holds under the form "<seat>.<name>.<digest>".
func HoldsBySeatNameDigestKey(seat, name, digest string) string {
return seat + "." + name + "." + digest
}
// ParseHoldsBySeatNameDigestKey reads the parts of a key of the form "<seat>.<name>.<digest>", in the order of the form; the last result is false for a key of another form.
func ParseHoldsBySeatNameDigestKey(key string) (string, string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", "", false
}
rest = after0
v2, after2, found2 := strings.Cut(rest, ".")
if !found2 || v2 == "" {
return "", "", "", false
}
rest = after2
if rest == "" {
return "", "", "", false
}
v4 := rest
rest = ""
if rest != "" {
return "", "", "", false
}
return v0, v2, v4, true
}
// DecodeHoldsBySeatNameDigestValue reads one value of the state holds under the form "<seat>.<name>.<digest>", refusing a field HoldsBySeatNameDigestValue does not know and any text after the value.
func DecodeHoldsBySeatNameDigestValue(raw []byte) (HoldsBySeatNameDigestValue, error) {
var v HoldsBySeatNameDigestValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// WindowsByCallerKey is the key of one value of the state windows under the form "<caller>".
func WindowsByCallerKey(caller string) string { return caller }
// ParseWindowsByCallerKey reads the parts of a key of the form "<caller>", in the order of the form; the last result is false for a key of another form.
func ParseWindowsByCallerKey(key string) (string, bool) {
rest := key
if rest == "" {
return "", false
}
v0 := rest
rest = ""
if rest != "" {
return "", false
}
return v0, true
}
// DecodeWindowsByCallerValue reads one value of the state windows under the form "<caller>", refusing a field WindowsByCallerValue does not know and any text after the value.
func DecodeWindowsByCallerValue(raw []byte) (WindowsByCallerValue, error) {
var v WindowsByCallerValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// Artifact is one published thing, as every registry event and answer names it.
type Artifact struct {
// the source commit it was built from
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// what it is
Kind string `json:"kind"`
// 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"`
// the tag it carries, for a tagged version
Tag string `json:"tag,omitempty"`
// the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone
Version string `json:"version,omitempty"`
}
// The values of Artifact's kind.
const (
ArtifactKindPackage = "package"
ArtifactKindImage = "image"
ArtifactKindBundle = "bundle"
ArtifactKindArchive = "archive"
ArtifactKindUpstream = "upstream"
)
// Hold is the body of held and released.
type Hold struct {
// when, RFC 3339
At string `json:"at,omitempty"`
// the caller the holder proved
By string `json:"by,omitempty"`
// the digest
Digest string `json:"digest"`
// the module holding the seat
Holder string `json:"holder"`
// the name
Name string `json:"name"`
// why
Reason string `json:"reason,omitempty"`
}
// Listed is a type of the contract.
type Listed struct {
// for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by
Artifact string `json:"artifact,omitempty"`
// the manifest's digest, sha256:<hex>
Digest string `json:"digest"`
// a hold keeps it from retention
Held bool `json:"held"`
// it is an index, naming a manifest per platform
Index bool `json:"index"`
// for an index, the digests of the manifests it names
Platforms []string `json:"platforms,omitempty"`
// its bytes: its own content and every blob it names
Size int64 `json:"size"`
// the tags naming it, none for an untagged one
Tags []string `json:"tags,omitempty"`
}
// Outcome is the body of published, refused and removed.
type Outcome struct {
// the source commit it was built from
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// the module holding the seat that said it
Holder string `json:"holder"`
// what it is
Kind string `json:"kind"`
// the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact
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, 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: <registry>/<module>/<artifact>@sha256:<digest>, or <name>@<version> 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
Version string `json:"version,omitempty"`
}
// The values of Outcome's kind.
const (
OutcomeKindPackage = "package"
OutcomeKindImage = "image"
OutcomeKindBundle = "bundle"
OutcomeKindArchive = "archive"
OutcomeKindUpstream = "upstream"
)
// Part is one part of a transfer.
type Part struct {
// the part's digest, sha256:<hex>: an image's layer or config, a package's file, a bundle's blob
Digest string `json:"digest"`
// its size in bytes
Size int64 `json:"size"`
}
// Repository is a type of the contract.
type Repository struct {
// each manifest it holds, by digest
Manifests []Listed `json:"manifests"`
// the repository
Name string `json:"name"`
}
// PublishInput is what publish takes.
type PublishInput struct {
// the source commit it was built from
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// what it is
Kind string `json:"kind"`
// 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"`
// every part's digest and size; none when the digest is already held
Parts []Part `json:"parts,omitempty"`
// the tag it carries, for a tagged version
Tag string `json:"tag,omitempty"`
// the transfer's name, the caller's own; the same name opens the same transfer again
Transfer string `json:"transfer"`
// the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone
Version string `json:"version,omitempty"`
}
// The values of PublishInput's kind.
const (
PublishInputKindPackage = "package"
PublishInputKindImage = "image"
PublishInputKindBundle = "bundle"
PublishInputKindArchive = "archive"
PublishInputKindUpstream = "upstream"
)
// PublishAnswer is what publish answers.
type PublishAnswer struct {
// the digest is held already: nothing to send; the outcome follows as an event
Held bool `json:"held,omitempty"`
// the digests of the parts the holder lacks, to send by publish-part
Lacks []string `json:"lacks,omitempty"`
// the transfer
Transfer string `json:"transfer"`
}
// PublishPartInput is what publish-part takes.
type PublishPartInput struct {
// the chunk's bytes, base64 (at most 760 KiB before encoding)
Data string `json:"data"`
// this chunk is the part's last
Last bool `json:"last"`
// the part's digest
Part string `json:"part"`
// the chunk's place in its part, from 0
Sequence int64 `json:"sequence"`
// the transfer
Transfer string `json:"transfer"`
}
// PublishPartAnswer is what publish-part answers.
type PublishPartAnswer struct {
// the part is whole and its bytes match its digest
Complete bool `json:"complete,omitempty"`
// the part
Part string `json:"part"`
// the chunks of the part held so far
Received int64 `json:"received,omitempty"`
// the transfer
Transfer string `json:"transfer"`
}
// VersionsInput is what versions takes.
type VersionsInput struct {
// narrowed to one kind (optional)
Kind string `json:"kind,omitempty"`
// the name
Name string `json:"name"`
}
// VersionsAnswer is what versions answers.
type VersionsAnswer struct {
// the name
Name string `json:"name"`
// newest first
Versions []Artifact `json:"versions"`
}
// RemoveInput is what remove takes.
type RemoveInput struct {
// the digest (for a bundle, archive or copied base)
Digest string `json:"digest,omitempty"`
// the name
Name string `json:"name"`
// why it goes
Reason string `json:"reason"`
// the tag (for a tagged version)
Tag string `json:"tag,omitempty"`
// the version
Version string `json:"version,omitempty"`
}
// The values of RemoveInput's reason.
const (
RemoveInputReasonTagGone = "tag-gone"
RemoveInputReasonRetention = "retention"
)
// 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:<hex>
Digest string `json:"digest"`
// the name it is kept under
Name string `json:"name"`
// the outside image's reference
Source string `json:"source"`
}
// MirrorAnswer is what mirror answers.
type MirrorAnswer struct {
// the digest stored
Digest string `json:"digest"`
// the reference the seat answers
Reference string `json:"reference"`
}
// TagsInput is what tags takes.
type TagsInput struct {
// the image
Name string `json:"name"`
}
// TagsAnswerTagsItem is a type of the contract.
type TagsAnswerTagsItem struct {
// its digest
Digest string `json:"digest"`
// the tag
Tag string `json:"tag"`
}
// TagsAnswer is what tags answers.
type TagsAnswer struct {
// the image
Name string `json:"name"`
// every tag
Tags []TagsAnswerTagsItem `json:"tags"`
}
// HoldInput is what hold takes.
type HoldInput struct {
// the digest
Digest string `json:"digest"`
// the name
Name string `json:"name"`
// which of the sweep's rules keeps it
Reason string `json:"reason"`
}
// HoldAnswer is what hold answers.
type HoldAnswer struct {
// it is held
Held bool `json:"held"`
// when the hold began, RFC 3339
Since string `json:"since,omitempty"`
}
// ReleaseInput is what release takes.
type ReleaseInput struct {
// the digest
Digest string `json:"digest"`
// the name
Name string `json:"name"`
// why it is let go
Reason string `json:"reason"`
}
// ReleaseAnswer is what release answers.
type ReleaseAnswer struct {
// a hold was let go; false when there was none
Released bool `json:"released"`
}
// ListInput is what list takes.
type ListInput struct {
// the repository, <module>/<artifact>; every repository of the seat when not given
Name string `json:"name,omitempty"`
}
// ListAnswer is what list answers.
type ListAnswer struct {
// each repository named, in order of its name
Repositories []Repository `json:"repositories"`
}
// PublishedEvent is the body of the event published.
type PublishedEvent = Outcome
// RefusedEvent is the body of the event refused.
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
// ReleasedEvent is the body of the event released.
type ReleasedEvent = Hold
// HoldsBySeatNameDigestValue is one value of the state holds under the key <seat>.<name>.<digest>.
type HoldsBySeatNameDigestValue struct {
By string `json:"by"`
Digest string `json:"digest"`
Kind string `json:"kind"`
Name string `json:"name"`
Seat string `json:"seat"`
// when (RFC 3339 time)
Since string `json:"since"`
Why string `json:"why,omitempty"`
}
// WindowsByCallerValueDroppedItem is a type of the contract.
type WindowsByCallerValueDroppedItem struct {
// when (RFC 3339 time)
At string `json:"at,omitempty"`
By string `json:"by,omitempty"`
Node string `json:"node"`
Why string `json:"why,omitempty"`
}
// WindowsByCallerValue is one value of the state windows under the key <caller>.
type WindowsByCallerValue struct {
// when it last closed (RFC 3339 time)
Closed string `json:"closed,omitempty"`
// retired nodes dropped by hand
Dropped []WindowsByCallerValueDroppedItem `json:"dropped,omitempty"`
// the nodes holding it open
Holding []string `json:"holding,omitempty"`
LastHolding []string `json:"last_holding,omitempty"`
// the caller whose window it is
Name string `json:"name"`
Open bool `json:"open,omitempty"`
// node to when it began to hold
Since map[string]string `json:"since,omitempty"`
// of each node holding the window, why it holds it
Why map[string]string `json:"why,omitempty"`
}
@@ -0,0 +1,82 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
package containerimageregistry
import (
"encoding/json"
"reflect"
"regexp"
"testing"
)
// TestFixtures reads every fixture of seats/container-image-registry/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: "published on the seat's subject", subject: "mesh.seat.container-image-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"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\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}",
decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.published$")},
{name: "refused on the seat's subject", subject: "mesh.seat.container-image-registry.event.refused", 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 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\"}",
decode: func(b []byte) (any, error) { var v ReleasedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.released$")},
{name: "a hold", subject: "", given: "{\"by\":\"operator@shanks\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"kind\":\"base\",\"name\":\"base\",\"seat\":\"container-image-registry\",\"since\":\"2026-11-05T10:00:00Z\"}", wire: "{\"by\":\"operator@shanks\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"kind\":\"base\",\"name\":\"base\",\"seat\":\"container-image-registry\",\"since\":\"2026-11-05T10:00:00Z\"}",
decode: func(b []byte) (any, error) {
var v HoldsBySeatNameDigestValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "an open window", subject: "", given: "{\"holding\":[\"ace\"],\"name\":\"mesh-delivery-builder\",\"open\":true,\"since\":{\"ace\":\"2026-11-05T10:00:00Z\"}}", wire: "{\"holding\":[\"ace\"],\"name\":\"mesh-delivery-builder\",\"open\":true,\"since\":{\"ace\":\"2026-11-05T10:00:00Z\"}}",
decode: func(b []byte) (any, error) { var v WindowsByCallerValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"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\":\"image\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"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: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}",
decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}",
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\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"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 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},
{name: "list answers an index with its platforms, its tag and its hold", subject: "", given: "{\"repositories\":[{\"manifests\":[{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":false,\"index\":true,\"platforms\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"size\":2048,\"tags\":[\"3\"]},{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"held\":false,\"index\":false,\"size\":1024}],\"name\":\"mail/server\"}]}", wire: "{\"repositories\":[{\"manifests\":[{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":false,\"index\":true,\"platforms\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"size\":2048,\"tags\":[\"3\"]},{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"held\":false,\"index\":false,\"size\":1024}],\"name\":\"mail/server\"}]}",
decode: func(b []byte) (any, error) { var v ListAnswer; 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)
}
}
}
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry
go 1.26.0
@@ -0,0 +1,262 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
// Package gopackageregistry is the contract of the seat "go-package-registry", defined whole (seats/go-package-registry/seat.json): The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus.
package gopackageregistry
// Seat is the seat's name.
const Seat = "go-package-registry"
const (
VerbPublish = "publish"
VerbPublishPart = "publish-part"
VerbVersions = "versions"
VerbRemove = "remove"
)
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.
func Verbs() []string {
return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove}
}
// Events is every event published on the seat's subject: a new copy each call.
func Events() []string {
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.
const (
Family = "go-package-registry.events"
FamilyMaxAgeDays = 7
)
// SubjectPublished is where published is published.
const SubjectPublished = "mesh.seat.go-package-registry.event.published"
// SubjectRefused is where refused is published.
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
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// what it is
Kind string `json:"kind"`
// 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"`
// the tag it carries, for a tagged version
Tag string `json:"tag,omitempty"`
// the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone
Version string `json:"version,omitempty"`
}
// The values of Artifact's kind.
const (
ArtifactKindPackage = "package"
ArtifactKindImage = "image"
ArtifactKindBundle = "bundle"
ArtifactKindArchive = "archive"
ArtifactKindUpstream = "upstream"
)
// Outcome is the body of published, refused and removed.
type Outcome struct {
// the source commit it was built from
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// the module holding the seat that said it
Holder string `json:"holder"`
// what it is
Kind string `json:"kind"`
// the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact
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, 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: <registry>/<module>/<artifact>@sha256:<digest>, or <name>@<version> 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
Version string `json:"version,omitempty"`
}
// The values of Outcome's kind.
const (
OutcomeKindPackage = "package"
OutcomeKindImage = "image"
OutcomeKindBundle = "bundle"
OutcomeKindArchive = "archive"
OutcomeKindUpstream = "upstream"
)
// Part is one part of a transfer.
type Part struct {
// the part's digest, sha256:<hex>: an image's layer or config, a package's file, a bundle's blob
Digest string `json:"digest"`
// its size in bytes
Size int64 `json:"size"`
}
// PublishInput is what publish takes.
type PublishInput struct {
// the source commit it was built from
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// what it is
Kind string `json:"kind"`
// 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"`
// every part's digest and size; none when the digest is already held
Parts []Part `json:"parts,omitempty"`
// the tag it carries, for a tagged version
Tag string `json:"tag,omitempty"`
// the transfer's name, the caller's own; the same name opens the same transfer again
Transfer string `json:"transfer"`
// the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone
Version string `json:"version,omitempty"`
}
// The values of PublishInput's kind.
const (
PublishInputKindPackage = "package"
PublishInputKindImage = "image"
PublishInputKindBundle = "bundle"
PublishInputKindArchive = "archive"
PublishInputKindUpstream = "upstream"
)
// PublishAnswer is what publish answers.
type PublishAnswer struct {
// the digest is held already: nothing to send; the outcome follows as an event
Held bool `json:"held,omitempty"`
// the digests of the parts the holder lacks, to send by publish-part
Lacks []string `json:"lacks,omitempty"`
// the transfer
Transfer string `json:"transfer"`
}
// PublishPartInput is what publish-part takes.
type PublishPartInput struct {
// the chunk's bytes, base64 (at most 760 KiB before encoding)
Data string `json:"data"`
// this chunk is the part's last
Last bool `json:"last"`
// the part's digest
Part string `json:"part"`
// the chunk's place in its part, from 0
Sequence int64 `json:"sequence"`
// the transfer
Transfer string `json:"transfer"`
}
// PublishPartAnswer is what publish-part answers.
type PublishPartAnswer struct {
// the part is whole and its bytes match its digest
Complete bool `json:"complete,omitempty"`
// the part
Part string `json:"part"`
// the chunks of the part held so far
Received int64 `json:"received,omitempty"`
// the transfer
Transfer string `json:"transfer"`
}
// VersionsInput is what versions takes.
type VersionsInput struct {
// narrowed to one kind (optional)
Kind string `json:"kind,omitempty"`
// the name
Name string `json:"name"`
}
// VersionsAnswer is what versions answers.
type VersionsAnswer struct {
// the name
Name string `json:"name"`
// newest first
Versions []Artifact `json:"versions"`
}
// RemoveInput is what remove takes.
type RemoveInput struct {
// the digest (for a bundle, archive or copied base)
Digest string `json:"digest,omitempty"`
// the name
Name string `json:"name"`
// why it goes
Reason string `json:"reason"`
// the tag (for a tagged version)
Tag string `json:"tag,omitempty"`
// the version
Version string `json:"version,omitempty"`
}
// The values of RemoveInput's reason.
const (
RemoveInputReasonTagGone = "tag-gone"
RemoveInputReasonRetention = "retention"
)
// 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
// RefusedEvent is the body of the event refused.
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
URL string `json:"url"`
}
@@ -0,0 +1,66 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
package gopackageregistry
import (
"encoding/json"
"reflect"
"regexp"
"testing"
)
// TestFixtures reads every fixture of seats/go-package-registry/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: "published on the seat's subject", subject: "mesh.seat.go-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"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\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"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 PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.published$")},
{name: "refused on the seat's subject", subject: "mesh.seat.go-package-registry.event.refused", 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 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\"}",
decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}",
decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}",
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))
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)
}
}
}
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry
go 1.26.0
@@ -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 <module>/<artifact> (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://<module>/<artifact>/blobs/sha256:<hex>: 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
@@ -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)
}
}
}
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep
go 1.26.0
+717
View File
@@ -0,0 +1,717 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
// Package meshcatalog is the contract of the seat "mesh-catalog", defined whole (seats/mesh-catalog/seat.json): The catalog of the mesh: every module the trunk holds with its manifest, fingerprint and verdict, the packages published, and the seat list (ADR 0636 §1, §4).
package meshcatalog
import (
"bytes"
"encoding/json"
"errors"
"strings"
"time"
)
// Seat is the seat's name.
const Seat = "mesh-catalog"
const (
VerbModules = "modules"
VerbModule = "module"
VerbProvides = "provides"
VerbDependents = "dependents"
VerbSeatDefinitions = "seat-definitions"
)
const (
EventSeatDefinitionChanged = "seat-definition-changed"
EventSeatDefinitionRemoved = "seat-definition-removed"
EventManifestAccepted = "manifest-accepted"
EventManifestRefused = "manifest-refused"
)
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
func Verbs() []string {
return []string{VerbModules, VerbModule, VerbProvides, VerbDependents, VerbSeatDefinitions}
}
// Events is every event published on the seat's subject: a new copy each call.
func Events() []string {
return []string{EventSeatDefinitionChanged, EventSeatDefinitionRemoved, EventManifestAccepted, EventManifestRefused}
}
// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects.
const (
Family = "mesh-catalog.events"
FamilyMaxAgeDays = 7
)
// SubjectSeatDefinitionChanged is where seat-definition-changed is published.
const SubjectSeatDefinitionChanged = "mesh.seat.mesh-catalog.event.seat-definition-changed"
// SubjectSeatDefinitionRemoved is where seat-definition-removed is published.
const SubjectSeatDefinitionRemoved = "mesh.seat.mesh-catalog.event.seat-definition-removed"
// SubjectManifestAccepted is where manifest-accepted is published.
const SubjectManifestAccepted = "mesh.seat.mesh-catalog.event.manifest-accepted"
// SubjectManifestRefused is where manifest-refused is published.
const SubjectManifestRefused = "mesh.seat.mesh-catalog.event.manifest-refused"
const (
StateManifests = "manifests"
StateModules = "modules"
StateCandidates = "candidates"
StateVerdicts = "verdicts"
StatePredefined = "predefined"
StateConfigCandidates = "config-candidates"
StatePackages = "packages"
)
// ManifestsByDigestKey is the key of one value of the state manifests under the form "<digest>".
func ManifestsByDigestKey(digest string) string { return digest }
// ParseManifestsByDigestKey reads the parts of a key of the form "<digest>", in the order of the form; the last result is false for a key of another form.
func ParseManifestsByDigestKey(key string) (string, bool) {
rest := key
if rest == "" {
return "", false
}
v0 := rest
rest = ""
if rest != "" {
return "", false
}
return v0, true
}
// DecodeManifestsByDigestValue reads one value of the state manifests under the form "<digest>", refusing a field ManifestsByDigestValue does not know and any text after the value.
func DecodeManifestsByDigestValue(raw []byte) (ManifestsByDigestValue, error) {
var v ManifestsByDigestValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// ModulesByModuleKey is the key of one value of the state modules under the form "<module>".
func ModulesByModuleKey(module string) string { return module }
// ParseModulesByModuleKey reads the parts of a key of the form "<module>", in the order of the form; the last result is false for a key of another form.
func ParseModulesByModuleKey(key string) (string, bool) {
rest := key
if rest == "" {
return "", false
}
v0 := rest
rest = ""
if rest != "" {
return "", false
}
return v0, true
}
// DecodeModulesByModuleValue reads one value of the state modules under the form "<module>", refusing a field ModulesByModuleValue does not know and any text after the value.
func DecodeModulesByModuleValue(raw []byte) (ModulesByModuleValue, error) {
var v ModulesByModuleValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// CandidatesByGroupKey is the key of one value of the state candidates under the form "<group>".
func CandidatesByGroupKey(group string) string { return group }
// ParseCandidatesByGroupKey reads the parts of a key of the form "<group>", in the order of the form; the last result is false for a key of another form.
func ParseCandidatesByGroupKey(key string) (string, bool) {
rest := key
if rest == "" {
return "", false
}
v0 := rest
rest = ""
if rest != "" {
return "", false
}
return v0, true
}
// DecodeCandidatesByGroupValue reads one value of the state candidates under the form "<group>", refusing a field CandidatesByGroupValue does not know and any text after the value.
func DecodeCandidatesByGroupValue(raw []byte) (CandidatesByGroupValue, error) {
var v CandidatesByGroupValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// VerdictsByDigestKey is the key of one value of the state verdicts under the form "<digest>".
func VerdictsByDigestKey(digest string) string { return digest }
// ParseVerdictsByDigestKey reads the parts of a key of the form "<digest>", in the order of the form; the last result is false for a key of another form.
func ParseVerdictsByDigestKey(key string) (string, bool) {
rest := key
if rest == "" {
return "", false
}
v0 := rest
rest = ""
if rest != "" {
return "", false
}
return v0, true
}
// DecodeVerdictsByDigestValue reads one value of the state verdicts under the form "<digest>", refusing a field VerdictsByDigestValue does not know and any text after the value.
func DecodeVerdictsByDigestValue(raw []byte) (VerdictsByDigestValue, error) {
var v VerdictsByDigestValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// PredefinedByListKey is the key of one value of the state predefined under the form "list".
func PredefinedByListKey() string { return "list" }
// ParsePredefinedByListKey reads the parts of a key of the form "list", in the order of the form; the last result is false for a key of another form.
func ParsePredefinedByListKey(key string) bool {
rest := key
if !strings.HasPrefix(rest, "list") {
return false
}
rest = rest[4:]
if rest != "" {
return false
}
return true
}
// DecodePredefinedByListValue reads one value of the state predefined under the form "list", refusing a field PredefinedByListValue does not know and any text after the value.
func DecodePredefinedByListValue(raw []byte) (PredefinedByListValue, error) {
var v PredefinedByListValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// ConfigCandidatesByPullKey is the key of one value of the state config-candidates under the form "<pull>".
func ConfigCandidatesByPullKey(pull string) string { return pull }
// ParseConfigCandidatesByPullKey reads the parts of a key of the form "<pull>", in the order of the form; the last result is false for a key of another form.
func ParseConfigCandidatesByPullKey(key string) (string, bool) {
rest := key
if rest == "" {
return "", false
}
v0 := rest
rest = ""
if rest != "" {
return "", false
}
return v0, true
}
// DecodeConfigCandidatesByPullValue reads one value of the state config-candidates under the form "<pull>", refusing a field ConfigCandidatesByPullValue does not know and any text after the value.
func DecodeConfigCandidatesByPullValue(raw []byte) (ConfigCandidatesByPullValue, error) {
var v ConfigCandidatesByPullValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// PackagesByModuleKey is the key of one value of the state packages under the form "<module>".
func PackagesByModuleKey(module string) string { return module }
// ParsePackagesByModuleKey reads the parts of a key of the form "<module>", in the order of the form; the last result is false for a key of another form.
func ParsePackagesByModuleKey(key string) (string, bool) {
rest := key
if rest == "" {
return "", false
}
v0 := rest
rest = ""
if rest != "" {
return "", false
}
return v0, true
}
// DecodePackagesByModuleValue reads one value of the state packages under the form "<module>", refusing a field PackagesByModuleValue does not know and any text after the value.
func DecodePackagesByModuleValue(raw []byte) (PackagesByModuleValue, error) {
var v PackagesByModuleValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// PackagesByPackageSeriesKey is the key of one value of the state packages under the form "<package>.<series>".
func PackagesByPackageSeriesKey(packagePart, series string) string { return packagePart + "." + series }
// ParsePackagesByPackageSeriesKey reads the parts of a key of the form "<package>.<series>", in the order of the form; the last result is false for a key of another form.
func ParsePackagesByPackageSeriesKey(key string) (string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", false
}
rest = after0
if rest == "" {
return "", "", false
}
v2 := rest
rest = ""
if rest != "" {
return "", "", false
}
return v0, v2, true
}
// DecodePackagesByPackageSeriesValue reads one value of the state packages under the form "<package>.<series>", refusing a field PackagesByPackageSeriesValue does not know and any text after the value.
func DecodePackagesByPackageSeriesValue(raw []byte) (PackagesByPackageSeriesValue, error) {
var v PackagesByPackageSeriesValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// ToolchainUsed is a toolchain a fingerprint was computed with (ADR 0405 clause 7).
type ToolchainUsed struct {
Language string `json:"language"`
// the build module's recipe version
Recipe string `json:"recipe"`
// the installed toolchain's version
Version string `json:"version"`
}
// ModulesInput is what modules takes.
type ModulesInput struct {
}
// ModulesAnswerModulesItem is a type of the contract.
type ModulesAnswerModulesItem struct {
// the merge commit it was last changed at
Commit string `json:"commit"`
// its fingerprint (src2: and 64 hex digits); absent when it has none
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
// why it has none
None string `json:"none,omitempty"`
// the module's directory in its repository, '.' for a root
Path string `json:"path,omitempty"`
Repository string `json:"repository"`
}
// ModulesAnswer is what modules answers.
type ModulesAnswer struct {
Modules []ModulesAnswerModulesItem `json:"modules"`
}
// ModuleInput is what module takes.
type ModuleInput struct {
// the module's name
Module string `json:"module"`
}
// ModuleAnswerModule is a type of the contract.
type ModuleAnswerModule struct {
// the merge commit it was last changed at
Commit string `json:"commit"`
// its fingerprint (src2: and 64 hex digits); absent when it has none
Fp string `json:"fp,omitempty"`
// the module's manifest as the trunk holds it
Manifest map[string]any `json:"manifest,omitempty"`
Module string `json:"module"`
// why it has none
None string `json:"none,omitempty"`
// the module's directory in its repository, '.' for a root
Path string `json:"path,omitempty"`
Repository string `json:"repository"`
}
// ModuleAnswer is what module answers.
type ModuleAnswer struct {
Module ModuleAnswerModule `json:"module"`
}
// ProvidesInput is what provides takes.
type ProvidesInput struct {
// the provision, such as postgres-database
Provision string `json:"provision"`
}
// ProvidesAnswer is what provides answers.
type ProvidesAnswer = map[string]any
// DependentsInput is what dependents takes.
type DependentsInput struct {
// the module's name
Module string `json:"module"`
}
// DependentsAnswer is what dependents answers.
type DependentsAnswer = map[string]any
// SeatDefinitionsInput is what seat-definitions takes.
type SeatDefinitionsInput struct {
// the seat's name; none lists every trunk seat
Seat string `json:"seat,omitempty"`
}
// SeatDefinitionsAnswerEntry is a type of the contract.
type SeatDefinitionsAnswerEntry struct {
// the definition, resolved with the parts it includes
Definition map[string]any `json:"definition,omitempty"`
Digest string `json:"digest"`
Seat string `json:"seat"`
// trunk
Source string `json:"source"`
// why the definition could not be read, when it could not
Unparsed string `json:"unparsed,omitempty"`
}
// SeatDefinitionsAnswerSeatsItem is a type of the contract.
type SeatDefinitionsAnswerSeatsItem struct {
Digest string `json:"digest"`
Seat string `json:"seat"`
// trunk
Source string `json:"source"`
}
// SeatDefinitionsAnswer is what seat-definitions answers.
type SeatDefinitionsAnswer struct {
Entry *SeatDefinitionsAnswerEntry `json:"entry,omitempty"`
Found bool `json:"found,omitempty"`
Revision int64 `json:"revision,omitempty"`
Seat string `json:"seat,omitempty"`
Seats []SeatDefinitionsAnswerSeatsItem `json:"seats,omitempty"`
}
// SeatDefinitionChangedEvent is the body of the event seat-definition-changed.
type SeatDefinitionChangedEvent struct {
Digest string `json:"digest"`
Seat string `json:"seat"`
// trunk
Source string `json:"source"`
}
// SeatDefinitionRemovedEvent is the body of the event seat-definition-removed.
type SeatDefinitionRemovedEvent struct {
Seat string `json:"seat"`
}
// ManifestAcceptedEventBusStateItem is a type of the contract.
type ManifestAcceptedEventBusStateItem struct {
Configuration map[string]any `json:"configuration,omitempty"`
Name string `json:"name"`
Options map[string]any `json:"options,omitempty"`
Storage string `json:"storage"`
Template string `json:"template,omitempty"`
Types []map[string]any `json:"types,omitempty"`
}
// ManifestAcceptedEvent is the body of the event manifest-accepted.
type ManifestAcceptedEvent struct {
BusState []ManifestAcceptedEventBusStateItem `json:"bus-state"`
Digest string `json:"digest"`
Judge string `json:"judge"`
Module string `json:"module"`
// the indexer's revision of the verdict it says; a reader asks its first view again on a gap in it; zero from an indexer that does not say it
Revision int64 `json:"revision,omitempty"`
V int64 `json:"v"`
}
// ManifestRefusedEvent is the body of the event manifest-refused.
type ManifestRefusedEvent struct {
Digest string `json:"digest"`
Judge string `json:"judge"`
Module string `json:"module"`
Problems []string `json:"problems"`
// the indexer's revision of the verdict it says; a reader asks its first view again on a gap in it; zero from an indexer that does not say it
Revision int64 `json:"revision,omitempty"`
V int64 `json:"v"`
}
// ManifestsByDigestValue is one value of the state manifests under the key <digest>.
type ManifestsByDigestValue struct {
// the manifest, UTF-8 JSON text
Manifest string `json:"manifest"`
Module string `json:"module"`
// the layout version
V int64 `json:"v,omitempty"`
}
// ModulesByModuleValueInputs2 is a type of the contract.
type ModulesByModuleValueInputs2 struct {
Bases []string `json:"bases,omitempty"`
Builder string `json:"builder,omitempty"`
Contexts map[string]string `json:"contexts,omitempty"`
ImportForm string `json:"import-form,omitempty"`
Imports []string `json:"imports,omitempty"`
Locks map[string]string `json:"locks,omitempty"`
Packages []string `json:"packages,omitempty"`
Paths map[string]string `json:"paths,omitempty"`
Tree string `json:"tree"`
}
// ModulesByModuleValue is one value of the state modules under the key <module>.
type ModulesByModuleValue struct {
Commit string `json:"commit"`
// the old-form fingerprint
Fp string `json:"fp,omitempty"`
FpSrc int64 `json:"fp-src,omitempty"`
// src2: fingerprint
Fp2 string `json:"fp2,omitempty"`
Inputs2 *ModulesByModuleValueInputs2 `json:"inputs2,omitempty"`
// manifest digest
Manifest string `json:"manifest,omitempty"`
Needs []string `json:"needs,omitempty"`
None string `json:"none,omitempty"`
// why it has none
None2 string `json:"none2,omitempty"`
Packages map[string]string `json:"packages,omitempty"`
Path string `json:"path,omitempty"`
Paths map[string][]string `json:"paths,omitempty"`
Problems []string `json:"problems,omitempty"`
Repository string `json:"repository"`
// the source revision
Src int64 `json:"src,omitempty"`
// by language: the toolchain FP2 was computed with
Toolchains map[string]ToolchainUsed `json:"toolchains,omitempty"`
Unknown []string `json:"unknown,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
Verdict string `json:"verdict,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}
// CandidatesByGroupValuePullsItemFpsValue is a type of the contract.
type CandidatesByGroupValuePullsItemFpsValue struct {
Fp2 string `json:"fp2,omitempty"`
Manifest string `json:"manifest,omitempty"`
None2 string `json:"none2,omitempty"`
Packages map[string]string `json:"packages,omitempty"`
Paths map[string]string `json:"paths,omitempty"`
}
// CandidatesByGroupValuePullsItem is a type of the contract.
type CandidatesByGroupValuePullsItem struct {
Fps map[string]CandidatesByGroupValuePullsItemFpsValue `json:"fps,omitempty"`
Head string `json:"head"`
Modules []string `json:"modules"`
Number int64 `json:"number"`
Repository string `json:"repository"`
}
// CandidatesByGroupValue is one value of the state candidates under the key <group>.
type CandidatesByGroupValue struct {
// the source revision it is read on
Base int64 `json:"base"`
Fpset string `json:"fpset,omitempty"`
Group string `json:"group"`
Heads string `json:"heads"`
Pulls []CandidatesByGroupValuePullsItem `json:"pulls"`
// the layout version
V int64 `json:"v,omitempty"`
}
// VerdictsByDigestValue is one value of the state verdicts under the key <digest>.
type VerdictsByDigestValue struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
Judge string `json:"judge,omitempty"`
Module string `json:"module"`
Problems []string `json:"problems,omitempty"`
State string `json:"state"`
Unknown []string `json:"unknown,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}
// PredefinedByListValue is one value of the state predefined under the key list.
type PredefinedByListValue struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
Commit string `json:"commit"`
// the foundation's predefined list as kept
List any `json:"list"`
// the layout version
V int64 `json:"v,omitempty"`
}
// ConfigCandidatesByPullValueDeclaresValueValue is a type of the contract.
type ConfigCandidatesByPullValueDeclaresValueValue struct {
// the default
Default any `json:"default"`
Description string `json:"description,omitempty"`
// the allowed values
Enum any `json:"enum,omitempty"`
Kind string `json:"kind"`
// a declared bound; 0 is a bound, absent is none
Maximum *float64 `json:"maximum,omitempty"`
// a declared bound; 0 is a bound, absent is none
Minimum *float64 `json:"minimum,omitempty"`
Pattern string `json:"pattern,omitempty"`
Predefined string `json:"predefined,omitempty"`
Required bool `json:"required,omitempty"`
Runtime bool `json:"runtime,omitempty"`
Trusted bool `json:"trusted,omitempty"`
Type string `json:"type,omitempty"`
Why string `json:"why"`
}
// ConfigCandidatesByPullValue is one value of the state config-candidates under the key <pull>.
type ConfigCandidatesByPullValue struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
// module to key to declaration
Declares map[string]map[string]ConfigCandidatesByPullValueDeclaresValueValue `json:"declares"`
Head string `json:"head"`
Pull string `json:"pull"`
// the layout version
V int64 `json:"v,omitempty"`
}
// PackagesByModuleValuePackagesItem is a type of the contract.
type PackagesByModuleValuePackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// PackagesByModuleValue is one value of the state packages under the key <module>.
type PackagesByModuleValue struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
Commit string `json:"commit"`
Dir string `json:"dir,omitempty"`
// the Go module path
Module string `json:"module"`
Packages []PackagesByModuleValuePackagesItem `json:"packages"`
Repository string `json:"repository"`
// digest of go.mod and go.sum
Requires string `json:"requires,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
}
// PackagesByPackageSeriesValueNewestContentsPackagesItem is a type of the contract.
type PackagesByPackageSeriesValueNewestContentsPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// PackagesByPackageSeriesValueNewestContents is a type of the contract.
type PackagesByPackageSeriesValueNewestContents struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
Commit string `json:"commit"`
Dir string `json:"dir,omitempty"`
// the Go module path
Module string `json:"module"`
Packages []PackagesByPackageSeriesValueNewestContentsPackagesItem `json:"packages"`
Repository string `json:"repository"`
// digest of go.mod and go.sum
Requires string `json:"requires,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
}
// PackagesByPackageSeriesValueNewest is a type of the contract.
type PackagesByPackageSeriesValueNewest struct {
Commit string `json:"commit,omitempty"`
Contents *PackagesByPackageSeriesValueNewestContents `json:"contents,omitempty"`
Digest string `json:"digest,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Version string `json:"version"`
}
// PackagesByPackageSeriesValueNumberedContentsPackagesItem is a type of the contract.
type PackagesByPackageSeriesValueNumberedContentsPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// PackagesByPackageSeriesValueNumberedContents is a type of the contract.
type PackagesByPackageSeriesValueNumberedContents struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
Commit string `json:"commit"`
Dir string `json:"dir,omitempty"`
// the Go module path
Module string `json:"module"`
Packages []PackagesByPackageSeriesValueNumberedContentsPackagesItem `json:"packages"`
Repository string `json:"repository"`
// digest of go.mod and go.sum
Requires string `json:"requires,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
}
// PackagesByPackageSeriesValueNumbered is a type of the contract.
type PackagesByPackageSeriesValueNumbered struct {
Commit string `json:"commit,omitempty"`
Contents *PackagesByPackageSeriesValueNumberedContents `json:"contents,omitempty"`
Digest string `json:"digest,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Version string `json:"version"`
}
// PackagesByPackageSeriesValue is one value of the state packages under the key <package>.<series>.
type PackagesByPackageSeriesValue struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
GoModule string `json:"go-module,omitempty"`
Name string `json:"name"`
Newest *PackagesByPackageSeriesValueNewest `json:"newest,omitempty"`
Numbered *PackagesByPackageSeriesValueNumbered `json:"numbered,omitempty"`
Package string `json:"package,omitempty"`
Seat string `json:"seat,omitempty"`
Series string `json:"series"`
// the layout version
V int64 `json:"v,omitempty"`
}
@@ -0,0 +1,82 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
package meshcatalog
import (
"encoding/json"
"reflect"
"regexp"
"testing"
)
// TestFixtures reads every fixture of seats/mesh-catalog/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: "a manifest (xclip, read through mesh-catalog.module)", subject: "", given: "{\"manifest\":\"{\\\"build\\\":{\\\"artifacts\\\":[{\\\"binary\\\":\\\"xclip-tools\\\",\\\"from\\\":\\\"cmd/xclip-tools\\\",\\\"kind\\\":\\\"bundle\\\",\\\"language\\\":\\\"go\\\",\\\"loads\\\":[\\\"xclip-tools\\\"],\\\"name\\\":\\\"tools\\\",\\\"system\\\":\\\"arch\\\"}]},\\\"capabilities\\\":[\\\"package-manager\\\"],\\\"module\\\":\\\"xclip\\\",\\\"resources\\\":[{\\\"id\\\":\\\"package\\\",\\\"package\\\":\\\"xclip\\\",\\\"type\\\":\\\"package\\\"}],\\\"tools\\\":[\\\"xclip_copy\\\",\\\"xclip_paste\\\",\\\"xclip_targets\\\",\\\"xclip_session\\\"],\\\"version\\\":\\\"1\\\"}\",\"module\":\"xclip\",\"v\":1}", wire: "{\"manifest\":\"{\\\"build\\\":{\\\"artifacts\\\":[{\\\"binary\\\":\\\"xclip-tools\\\",\\\"from\\\":\\\"cmd/xclip-tools\\\",\\\"kind\\\":\\\"bundle\\\",\\\"language\\\":\\\"go\\\",\\\"loads\\\":[\\\"xclip-tools\\\"],\\\"name\\\":\\\"tools\\\",\\\"system\\\":\\\"arch\\\"}]},\\\"capabilities\\\":[\\\"package-manager\\\"],\\\"module\\\":\\\"xclip\\\",\\\"resources\\\":[{\\\"id\\\":\\\"package\\\",\\\"package\\\":\\\"xclip\\\",\\\"type\\\":\\\"package\\\"}],\\\"tools\\\":[\\\"xclip_copy\\\",\\\"xclip_paste\\\",\\\"xclip_targets\\\",\\\"xclip_session\\\"],\\\"version\\\":\\\"1\\\"}\",\"module\":\"xclip\",\"v\":1}",
decode: func(b []byte) (any, error) { var v ManifestsByDigestValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a module (xclip, read through mesh-catalog.modules; the inputs and toolchains no tool reads are not here)", subject: "", given: "{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp2\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\",\"v\":1}", wire: "{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp2\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\",\"v\":1}",
decode: func(b []byte) (any, error) { var v ModulesByModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a candidate", subject: "", given: "{\"base\":3,\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"pulls\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"modules\":[\"nats\"],\"number\":1,\"repository\":\"novox/mesh-sdk\"}],\"v\":1}", wire: "{\"base\":3,\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"pulls\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"modules\":[\"nats\"],\"number\":1,\"repository\":\"novox/mesh-sdk\"}],\"v\":1}",
decode: func(b []byte) (any, error) { var v CandidatesByGroupValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a refused verdict (nats, read through the indexer's verdicts tool)", subject: "", given: "{\"at\":\"2026-11-04T18:51:12.614583047+01:00\",\"judge\":\"exe:661e6fb424d7ffea\",\"module\":\"nats\",\"problems\":[\"this is not a module manifest: a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl}: json: unknown field \\\"read-only\\\" (read without state[2].read-only)\"],\"state\":\"refused\"}", wire: "{\"at\":\"2026-11-04T18:51:12.614583047+01:00\",\"judge\":\"exe:661e6fb424d7ffea\",\"module\":\"nats\",\"problems\":[\"this is not a module manifest: a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl}: json: unknown field \\\"read-only\\\" (read without state[2].read-only)\"],\"state\":\"refused\"}",
decode: func(b []byte) (any, error) { var v VerdictsByDigestValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "the predefined list", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"list\":{\"streams\":[]},\"v\":1}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"list\":{\"streams\":[]},\"v\":1}",
decode: func(b []byte) (any, error) { var v PredefinedByListValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a pull request's configuration", subject: "", given: "{\"declares\":{\"nats\":{\"limit\":{\"default\":1,\"kind\":\"setting\",\"why\":\"a bound\"}}},\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"pull\":\"novox/mesh-catalog#1\",\"v\":1}", wire: "{\"declares\":{\"nats\":{\"limit\":{\"default\":1,\"kind\":\"setting\",\"why\":\"a bound\"}}},\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"pull\":\"novox/mesh-catalog#1\",\"v\":1}",
decode: func(b []byte) (any, error) {
var v ConfigCandidatesByPullValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a pull request's configuration state whose bounds are declared 0 (a zero is a bound, not none)", subject: "", given: "{\"declares\":{\"nats\":{\"limit\":{\"default\":0,\"kind\":\"setting\",\"maximum\":0,\"minimum\":0,\"why\":\"a bound\"}}},\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"pull\":\"novox/mesh-catalog#1\",\"v\":1}", wire: "{\"declares\":{\"nats\":{\"limit\":{\"default\":0,\"kind\":\"setting\",\"maximum\":0,\"minimum\":0,\"why\":\"a bound\"}}},\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"pull\":\"novox/mesh-catalog#1\",\"v\":1}",
decode: func(b []byte) (any, error) {
var v ConfigCandidatesByPullValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a published Go module", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"packages\":[{\"digest\":\"ab\",\"package\":\"git.novox.be/novox/mesh-sdk/go/keys\"}],\"repository\":\"novox/mesh-sdk\",\"v\":1}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"packages\":[{\"digest\":\"ab\",\"package\":\"git.novox.be/novox/mesh-sdk/go/keys\"}],\"repository\":\"novox/mesh-sdk\",\"v\":1}",
decode: func(b []byte) (any, error) { var v PackagesByModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a package series", subject: "", given: "{\"name\":\"go-keys\",\"series\":\"0.1\",\"v\":1}", wire: "{\"name\":\"go-keys\",\"series\":\"0.1\",\"v\":1}",
decode: func(b []byte) (any, error) {
var v PackagesByPackageSeriesValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a module with its toolchain", subject: "", given: "{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp2\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"repository\":\"novox/mesh-catalog\",\"toolchains\":{\"go\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"v\":1}", wire: "{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp2\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"repository\":\"novox/mesh-catalog\",\"toolchains\":{\"go\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"v\":1}",
decode: func(b []byte) (any, error) { var v ModulesByModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "the modules verb (captured: the first three modules)", subject: "", given: "{\"modules\":[{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:5e4a1b4ad4ea6175c05e4655ce58aeb957547149d4c9805eb27636efd50c1c87\",\"module\":\"adwaita\",\"path\":\"modules/adwaita\",\"repository\":\"novox/mesh-catalog\"},{\"commit\":\"b112adcecc41d18f54190cbd2fc864199b70c518\",\"fp\":\"src2:7dd0db8e98224f46121a5ae05c0b9c5a430eff531a5120aa40b1a59237853a3f\",\"module\":\"amqp-email-forwarder\",\"path\":\".\",\"repository\":\"jschoubben/amqp-email-forwarder\"},{\"commit\":\"dd458cb155f5f5fa3ed824a23728e444be5b156a\",\"fp\":\"src2:a98f8152f0a837dd5ba883a15efaf110ba88100193157e4bdb1a93ba55209371\",\"module\":\"artifact-store-tools\",\"path\":\"modules/artifact-store-tools\",\"repository\":\"novox/mesh-catalog\"}]}", wire: "{\"modules\":[{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:5e4a1b4ad4ea6175c05e4655ce58aeb957547149d4c9805eb27636efd50c1c87\",\"module\":\"adwaita\",\"path\":\"modules/adwaita\",\"repository\":\"novox/mesh-catalog\"},{\"commit\":\"b112adcecc41d18f54190cbd2fc864199b70c518\",\"fp\":\"src2:7dd0db8e98224f46121a5ae05c0b9c5a430eff531a5120aa40b1a59237853a3f\",\"module\":\"amqp-email-forwarder\",\"path\":\".\",\"repository\":\"jschoubben/amqp-email-forwarder\"},{\"commit\":\"dd458cb155f5f5fa3ed824a23728e444be5b156a\",\"fp\":\"src2:a98f8152f0a837dd5ba883a15efaf110ba88100193157e4bdb1a93ba55209371\",\"module\":\"artifact-store-tools\",\"path\":\"modules/artifact-store-tools\",\"repository\":\"novox/mesh-catalog\"}]}",
decode: func(b []byte) (any, error) { var v ModulesAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "the module verb (captured: xclip)", subject: "", given: "{\"module\":{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"manifest\":{\"build\":{\"artifacts\":[{\"binary\":\"xclip-tools\",\"from\":\"cmd/xclip-tools\",\"kind\":\"bundle\",\"language\":\"go\",\"loads\":[\"xclip-tools\"],\"name\":\"tools\",\"system\":\"arch\"}]},\"capabilities\":[\"package-manager\"],\"module\":\"xclip\",\"resources\":[{\"id\":\"package\",\"package\":\"xclip\",\"type\":\"package\"}],\"tools\":[\"xclip_copy\",\"xclip_paste\",\"xclip_targets\",\"xclip_session\"],\"version\":\"1\"},\"module\":\"xclip\",\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\"}}", wire: "{\"module\":{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"manifest\":{\"build\":{\"artifacts\":[{\"binary\":\"xclip-tools\",\"from\":\"cmd/xclip-tools\",\"kind\":\"bundle\",\"language\":\"go\",\"loads\":[\"xclip-tools\"],\"name\":\"tools\",\"system\":\"arch\"}]},\"capabilities\":[\"package-manager\"],\"module\":\"xclip\",\"resources\":[{\"id\":\"package\",\"package\":\"xclip\",\"type\":\"package\"}],\"tools\":[\"xclip_copy\",\"xclip_paste\",\"xclip_targets\",\"xclip_session\"],\"version\":\"1\"},\"module\":\"xclip\",\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\"}}",
decode: func(b []byte) (any, error) { var v ModuleAnswer; 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)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog
go 1.26.0
+455
View File
@@ -0,0 +1,455 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
// Package meshdelivery is the contract of the seat "mesh-delivery", defined whole (seats/mesh-delivery/seat.json): The delivery gate: reads and steers deliveries, merges and the checks that decide them; the merge is made on the operator's Approve once both checks passed.
package meshdelivery
import (
"bytes"
"encoding/json"
"errors"
"strings"
"time"
)
// Seat is the seat's name.
const Seat = "mesh-delivery"
const (
VerbDeliveries = "deliveries"
VerbTimes = "times"
VerbShow = "show"
VerbGroups = "groups"
VerbWhatIf = "what-if"
VerbChecks = "checks"
VerbTable = "table"
VerbStalled = "stalled"
VerbRecheck = "recheck"
VerbRelease = "release"
VerbStop = "stop"
VerbClose = "close"
VerbRetireHistory = "retire-history"
VerbSettle = "settle"
)
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
func Verbs() []string {
return []string{VerbDeliveries, VerbTimes, VerbShow, VerbGroups, VerbWhatIf, VerbChecks, VerbTable, VerbStalled, VerbRecheck, VerbRelease, VerbStop, VerbClose, VerbRetireHistory, VerbSettle}
}
const (
StateSource = "source"
StateMerges = "merges"
StateAsks = "asks"
StateOwed = "owed"
StateTurns = "turns"
)
// SourceByMainKey is the key of one value of the state source under the form "main".
func SourceByMainKey() string { return "main" }
// ParseSourceByMainKey reads the parts of a key of the form "main", in the order of the form; the last result is false for a key of another form.
func ParseSourceByMainKey(key string) bool {
rest := key
if !strings.HasPrefix(rest, "main") {
return false
}
rest = rest[4:]
if rest != "" {
return false
}
return true
}
// DecodeSourceByMainValue reads one value of the state source under the form "main", refusing a field SourceByMainValue does not know and any text after the value.
func DecodeSourceByMainValue(raw []byte) (SourceByMainValue, error) {
var v SourceByMainValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// MergesByGroupKey is the key of one value of the state merges under the form "<group>".
func MergesByGroupKey(group string) string { return group }
// ParseMergesByGroupKey reads the parts of a key of the form "<group>", in the order of the form; the last result is false for a key of another form.
func ParseMergesByGroupKey(key string) (string, bool) {
rest := key
if rest == "" {
return "", false
}
v0 := rest
rest = ""
if rest != "" {
return "", false
}
return v0, true
}
// DecodeMergesByGroupValue reads one value of the state merges under the form "<group>", refusing a field MergesByGroupValue does not know and any text after the value.
func DecodeMergesByGroupValue(raw []byte) (MergesByGroupValue, error) {
var v MergesByGroupValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// AsksByMergeGroupHeadsKey is the key of one value of the state asks under the form "merge.<group>.<heads>".
func AsksByMergeGroupHeadsKey(group, heads string) string { return "merge." + group + "." + heads }
// ParseAsksByMergeGroupHeadsKey reads the parts of a key of the form "merge.<group>.<heads>", in the order of the form; the last result is false for a key of another form.
func ParseAsksByMergeGroupHeadsKey(key string) (string, string, bool) {
rest := key
if !strings.HasPrefix(rest, "merge.") {
return "", "", false
}
rest = rest[6:]
v1, after1, found1 := strings.Cut(rest, ".")
if !found1 || v1 == "" {
return "", "", false
}
rest = after1
if rest == "" {
return "", "", false
}
v3 := rest
rest = ""
if rest != "" {
return "", "", false
}
return v1, v3, true
}
// DecodeAsksByMergeGroupHeadsValue reads one value of the state asks under the form "merge.<group>.<heads>", refusing a field AsksByMergeGroupHeadsValue does not know and any text after the value.
func DecodeAsksByMergeGroupHeadsValue(raw []byte) (AsksByMergeGroupHeadsValue, error) {
var v AsksByMergeGroupHeadsValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// OwedByRepositoryNumberHeadActKey is the key of one value of the state owed under the form "<repository>.<number>.<head>.<act>".
func OwedByRepositoryNumberHeadActKey(repository, number, head, act string) string {
return repository + "." + number + "." + head + "." + act
}
// ParseOwedByRepositoryNumberHeadActKey reads the parts of a key of the form "<repository>.<number>.<head>.<act>", in the order of the form; the last result is false for a key of another form.
func ParseOwedByRepositoryNumberHeadActKey(key string) (string, string, string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", "", "", false
}
rest = after0
v2, after2, found2 := strings.Cut(rest, ".")
if !found2 || v2 == "" {
return "", "", "", "", false
}
rest = after2
v4, after4, found4 := strings.Cut(rest, ".")
if !found4 || v4 == "" {
return "", "", "", "", false
}
rest = after4
if rest == "" {
return "", "", "", "", false
}
v6 := rest
rest = ""
if rest != "" {
return "", "", "", "", false
}
return v0, v2, v4, v6, true
}
// DecodeOwedByRepositoryNumberHeadActValue reads one value of the state owed under the form "<repository>.<number>.<head>.<act>", refusing a field OwedByRepositoryNumberHeadActValue does not know and any text after the value.
func DecodeOwedByRepositoryNumberHeadActValue(raw []byte) (OwedByRepositoryNumberHeadActValue, error) {
var v OwedByRepositoryNumberHeadActValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// TurnsByRepositoryKey is the key of one value of the state turns under the form "<repository>".
func TurnsByRepositoryKey(repository string) string { return repository }
// ParseTurnsByRepositoryKey reads the parts of a key of the form "<repository>", in the order of the form; the last result is false for a key of another form.
func ParseTurnsByRepositoryKey(key string) (string, bool) {
rest := key
if rest == "" {
return "", false
}
v0 := rest
rest = ""
if rest != "" {
return "", false
}
return v0, true
}
// DecodeTurnsByRepositoryValue reads one value of the state turns under the form "<repository>", refusing a field TurnsByRepositoryValue does not know and any text after the value.
func DecodeTurnsByRepositoryValue(raw []byte) (TurnsByRepositoryValue, error) {
var v TurnsByRepositoryValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// DeliveriesInput is what deliveries takes.
type DeliveriesInput struct {
// "true": the final ones of the last thirty days too
All string `json:"all,omitempty"`
// a group's id (its branch name)
Group string `json:"group,omitempty"`
// owner/repository
Repository string `json:"repository,omitempty"`
// one state, e.g. delivering or held
State string `json:"state,omitempty"`
}
// The values of DeliveriesInput's all.
const (
DeliveriesInputAllTrue = "true"
DeliveriesInputAllFalse = "false"
)
// DeliveriesAnswer is what deliveries answers.
type DeliveriesAnswer = map[string]any
// TimesInput is what times takes.
type TimesInput struct {
}
// TimesAnswer is what times answers.
type TimesAnswer = map[string]any
// ShowInput is what show takes.
type ShowInput struct {
// a delivery's id, or a group's
ID string `json:"id"`
}
// ShowAnswer is what show answers.
type ShowAnswer = map[string]any
// GroupsInput is what groups takes.
type GroupsInput struct {
// "true": the groups that ended too
All string `json:"all,omitempty"`
}
// The values of GroupsInput's all.
const (
GroupsInputAllTrue = "true"
GroupsInputAllFalse = "false"
)
// GroupsAnswer is what groups answers.
type GroupsAnswer = map[string]any
// WhatIfInput is what what-if takes.
type WhatIfInput struct {
}
// WhatIfAnswer is what what-if answers.
type WhatIfAnswer = map[string]any
// ChecksInput is what checks takes.
type ChecksInput struct {
// a commit's sha, or the start of one, instead of a pull request
Commit string `json:"commit,omitempty"`
// a pull request's number: its head is read
Number string `json:"number,omitempty"`
// owner/repository
Repository string `json:"repository"`
}
// ChecksAnswer is what checks answers.
type ChecksAnswer = map[string]any
// TableInput is what table takes.
type TableInput struct {
}
// TableAnswer is what table answers.
type TableAnswer = map[string]any
// StalledInput is what stalled takes.
type StalledInput struct {
}
// StalledAnswer is what stalled answers.
type StalledAnswer = map[string]any
// RecheckInput is what recheck takes.
type RecheckInput struct {
// the delivery's id
ID string `json:"id"`
// why, kept with the transition
Why string `json:"why"`
}
// RecheckAnswer is what recheck answers.
type RecheckAnswer = map[string]any
// ReleaseInput is what release takes.
type ReleaseInput struct {
}
// ReleaseAnswer is what release answers.
type ReleaseAnswer = map[string]any
// StopInput is what stop takes.
type StopInput struct {
}
// StopAnswer is what stop answers.
type StopAnswer = map[string]any
// CloseInput is what close takes.
type CloseInput struct {
// the delivery's id
ID string `json:"id"`
// the condition it answers
Why string `json:"why"`
}
// CloseAnswer is what close answers.
type CloseAnswer = map[string]any
// RetireHistoryInput is what retire-history takes.
type RetireHistoryInput struct {
}
// RetireHistoryAnswer is what retire-history answers.
type RetireHistoryAnswer = map[string]any
// SettleInput is what settle takes.
type SettleInput struct {
// the held deliveries' ids (owner/repository@commit)
Ids []string `json:"ids"`
// why they may be settled, shown to the operator
Why string `json:"why"`
}
// SettleAnswer is what settle answers.
type SettleAnswer = map[string]any
// SourceByMainValue is one value of the state source under the key main.
type SourceByMainValue struct {
// the group that moved it
Group string `json:"group"`
// repository to the merged head, a full commit
Heads map[string]string `json:"heads"`
// the layout version
V int64 `json:"v,omitempty"`
}
// MergesByGroupValuePullsItem is a type of the contract.
type MergesByGroupValuePullsItem struct {
Commit string `json:"commit,omitempty"`
Head string `json:"head"`
Number int64 `json:"number"`
Repository string `json:"repository"`
// merging, merged or refused
State string `json:"state"`
Words string `json:"words,omitempty"`
}
// MergesByGroupValue is one value of the state merges under the key <group>.
type MergesByGroupValue struct {
// <repository>@<number> to the approved head
Approved map[string]string `json:"approved,omitempty"`
// the ask key
Ask string `json:"ask"`
Group string `json:"group"`
Pulls []MergesByGroupValuePullsItem `json:"pulls"`
// the layout version
V int64 `json:"v,omitempty"`
}
// AsksByMergeGroupHeadsValueHeadsItem is a type of the contract.
type AsksByMergeGroupHeadsValueHeadsItem struct {
// the full commit
Head string `json:"head"`
Number int64 `json:"number"`
// <owner>/<name>
Repository string `json:"repository"`
}
// AsksByMergeGroupHeadsValue is one value of the state asks under the key merge.<group>.<heads>.
type AsksByMergeGroupHeadsValue struct {
// the operator-channel ask as raised
Ask any `json:"ask"`
// the number of times it was asked
Asking int64 `json:"asking,omitempty"`
Group string `json:"group"`
Heads []AsksByMergeGroupHeadsValueHeadsItem `json:"heads"`
// the layout version
V int64 `json:"v,omitempty"`
}
// OwedByRepositoryNumberHeadActValue is one value of the state owed under the key <repository>.<number>.<head>.<act>.
type OwedByRepositoryNumberHeadActValue struct {
// status=<context>, view, note or comment.<seq>
Act string `json:"act"`
Context string `json:"context,omitempty"`
Ref string `json:"ref,omitempty"`
State string `json:"state,omitempty"`
Target string `json:"target,omitempty"`
Text string `json:"text,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
}
// TurnsByRepositoryValueWaitingItem is a type of the contract.
type TurnsByRepositoryValueWaitingItem struct {
// when (RFC 3339 time)
Approved time.Time `json:"approved"`
Ask string `json:"ask,omitempty"`
Group string `json:"group"`
Heads string `json:"heads"`
// when (RFC 3339 time)
Since time.Time `json:"since"`
Step string `json:"step,omitempty"`
WaitsFor string `json:"waits-for,omitempty"`
// when (RFC 3339 time)
WaitsSince *time.Time `json:"waits-since,omitempty"`
}
// TurnsByRepositoryValue is one value of the state turns under the key <repository>.
type TurnsByRepositoryValue struct {
Repository string `json:"repository"`
// the layout version
V int64 `json:"v,omitempty"`
Waiting []TurnsByRepositoryValueWaitingItem `json:"waiting"`
}
@@ -0,0 +1,64 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
package meshdelivery
import (
"encoding/json"
"reflect"
"regexp"
"testing"
)
// TestFixtures reads every fixture of seats/mesh-delivery/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: "the source revision", subject: "", given: "{\"group\":\"feat/registry\",\"heads\":{\"novox/mesh-sdk\":\"cccccccccccccccccccccccccccccccccccccccc\"},\"v\":1}", wire: "{\"group\":\"feat/registry\",\"heads\":{\"novox/mesh-sdk\":\"cccccccccccccccccccccccccccccccccccccccc\"},\"v\":1}",
decode: func(b []byte) (any, error) { var v SourceByMainValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a merge", subject: "", given: "{\"ask\":\"merge.feat=registry.0123456789ab\",\"group\":\"feat/registry\",\"pulls\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"number\":1,\"repository\":\"novox/mesh-sdk\",\"state\":\"merging\"}],\"v\":1}", wire: "{\"ask\":\"merge.feat=registry.0123456789ab\",\"group\":\"feat/registry\",\"pulls\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"number\":1,\"repository\":\"novox/mesh-sdk\",\"state\":\"merging\"}],\"v\":1}",
decode: func(b []byte) (any, error) { var v MergesByGroupValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "an ask", subject: "", given: "{\"ask\":{\"headline\":\"merge\"},\"group\":\"feat/registry\",\"heads\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"number\":1,\"repository\":\"novox/mesh-sdk\"}],\"v\":1}", wire: "{\"ask\":{\"headline\":\"merge\"},\"group\":\"feat/registry\",\"heads\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"number\":1,\"repository\":\"novox/mesh-sdk\"}],\"v\":1}",
decode: func(b []byte) (any, error) {
var v AsksByMergeGroupHeadsValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "an act owed", subject: "", given: "{\"act\":\"view\",\"v\":1}", wire: "{\"act\":\"view\",\"v\":1}",
decode: func(b []byte) (any, error) {
var v OwedByRepositoryNumberHeadActValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a turn", subject: "", given: "{\"repository\":\"novox/mesh-sdk\",\"v\":1,\"waiting\":[{\"approved\":\"2026-11-05T10:00:00Z\",\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"since\":\"2026-11-05T10:01:00Z\"}]}", wire: "{\"repository\":\"novox/mesh-sdk\",\"v\":1,\"waiting\":[{\"approved\":\"2026-11-05T10:00:00Z\",\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"since\":\"2026-11-05T10:01:00Z\"}]}",
decode: func(b []byte) (any, error) { var v TurnsByRepositoryValue; 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)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery
go 1.26.0
@@ -0,0 +1,450 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
// Package meshdeliverychecker is the contract of the seat "mesh-delivery-checker", defined whole (seats/mesh-delivery-checker/seat.json): The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2).
package meshdeliverychecker
import (
"bytes"
"encoding/json"
"errors"
"strings"
)
// Seat is the seat's name.
const Seat = "mesh-delivery-checker"
const (
VerbTry = "try"
VerbTryRead = "try-read"
VerbTryEnd = "try-end"
)
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
func Verbs() []string {
return []string{VerbTry, VerbTryRead, VerbTryEnd}
}
// The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2).
const (
HolderEventVerdict = "verdict"
HolderEventTagWanted = "tag-wanted"
HolderEventPreviewWritten = "preview.written"
)
// HolderSubjectVerdict is where the holder publishes verdict, kept in checker.asks.
func HolderSubjectVerdict(holder string) string { return "mesh.stream." + holder + ".event.verdict" }
// HolderSubjectTagWanted is where the holder publishes tag-wanted, kept in checker.asks.
func HolderSubjectTagWanted(holder string) string {
return "mesh.stream." + holder + ".event.tag-wanted"
}
// HolderSubjectPreviewWritten is where the holder publishes preview.written, kept in checks.
func HolderSubjectPreviewWritten(holder string) string {
return "mesh.stream." + holder + ".event.preview.written"
}
const (
StateVerdicts = "verdicts"
StateKept = "kept"
StateOwed = "owed"
StatePreviews = "previews"
)
// VerdictsByGroupHeadsKey is the key of one value of the state verdicts under the form "<group>.<heads>".
func VerdictsByGroupHeadsKey(group, heads string) string { return group + "." + heads }
// ParseVerdictsByGroupHeadsKey reads the parts of a key of the form "<group>.<heads>", in the order of the form; the last result is false for a key of another form.
func ParseVerdictsByGroupHeadsKey(key string) (string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", false
}
rest = after0
if rest == "" {
return "", "", false
}
v2 := rest
rest = ""
if rest != "" {
return "", "", false
}
return v0, v2, true
}
// DecodeVerdictsByGroupHeadsValue reads one value of the state verdicts under the form "<group>.<heads>", refusing a field VerdictsByGroupHeadsValue does not know and any text after the value.
func DecodeVerdictsByGroupHeadsValue(raw []byte) (VerdictsByGroupHeadsValue, error) {
var v VerdictsByGroupHeadsValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// KeptByLevelDigestKey is the key of one value of the state kept under the form "<level>.<digest>".
func KeptByLevelDigestKey(level, digest string) string { return level + "." + digest }
// ParseKeptByLevelDigestKey reads the parts of a key of the form "<level>.<digest>", in the order of the form; the last result is false for a key of another form.
func ParseKeptByLevelDigestKey(key string) (string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", false
}
rest = after0
if rest == "" {
return "", "", false
}
v2 := rest
rest = ""
if rest != "" {
return "", "", false
}
return v0, v2, true
}
// DecodeKeptByLevelDigestValue reads one value of the state kept under the form "<level>.<digest>", refusing a field KeptByLevelDigestValue does not know and any text after the value.
func DecodeKeptByLevelDigestValue(raw []byte) (KeptByLevelDigestValue, error) {
var v KeptByLevelDigestValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// OwedByRepositoryNumberHeadActKey is the key of one value of the state owed under the form "<repository>.<number>.<head>.<act>".
func OwedByRepositoryNumberHeadActKey(repository, number, head, act string) string {
return repository + "." + number + "." + head + "." + act
}
// ParseOwedByRepositoryNumberHeadActKey reads the parts of a key of the form "<repository>.<number>.<head>.<act>", in the order of the form; the last result is false for a key of another form.
func ParseOwedByRepositoryNumberHeadActKey(key string) (string, string, string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", "", "", false
}
rest = after0
v2, after2, found2 := strings.Cut(rest, ".")
if !found2 || v2 == "" {
return "", "", "", "", false
}
rest = after2
v4, after4, found4 := strings.Cut(rest, ".")
if !found4 || v4 == "" {
return "", "", "", "", false
}
rest = after4
if rest == "" {
return "", "", "", "", false
}
v6 := rest
rest = ""
if rest != "" {
return "", "", "", "", false
}
return v0, v2, v4, v6, true
}
// DecodeOwedByRepositoryNumberHeadActValue reads one value of the state owed under the form "<repository>.<number>.<head>.<act>", refusing a field OwedByRepositoryNumberHeadActValue does not know and any text after the value.
func DecodeOwedByRepositoryNumberHeadActValue(raw []byte) (OwedByRepositoryNumberHeadActValue, error) {
var v OwedByRepositoryNumberHeadActValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// PreviewsByRepositoryHeadTrunkKey is the key of one value of the state previews under the form "<repository>.<head>.<trunk>".
func PreviewsByRepositoryHeadTrunkKey(repository, head, trunk string) string {
return repository + "." + head + "." + trunk
}
// ParsePreviewsByRepositoryHeadTrunkKey reads the parts of a key of the form "<repository>.<head>.<trunk>", in the order of the form; the last result is false for a key of another form.
func ParsePreviewsByRepositoryHeadTrunkKey(key string) (string, string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", "", false
}
rest = after0
v2, after2, found2 := strings.Cut(rest, ".")
if !found2 || v2 == "" {
return "", "", "", false
}
rest = after2
if rest == "" {
return "", "", "", false
}
v4 := rest
rest = ""
if rest != "" {
return "", "", "", false
}
return v0, v2, v4, true
}
// DecodePreviewsByRepositoryHeadTrunkValue reads one value of the state previews under the form "<repository>.<head>.<trunk>", refusing a field PreviewsByRepositoryHeadTrunkValue does not know and any text after the value.
func DecodePreviewsByRepositoryHeadTrunkValue(raw []byte) (PreviewsByRepositoryHeadTrunkValue, error) {
var v PreviewsByRepositoryHeadTrunkValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// TryInputMembersItem is a type of the contract.
type TryInputMembersItem struct {
// the full commit to try, 40 or 64 hex digits
Head string `json:"head"`
// <owner>/<name>
Repository string `json:"repository"`
// the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent
Trunk string `json:"trunk,omitempty"`
}
// TryInput is what try takes.
type TryInput struct {
// the full commit to try, 40 or 64 hex digits, for a try of one member (instead of members)
Head string `json:"head,omitempty"`
// unit or integration: only this level; both when absent
Level string `json:"level,omitempty"`
// the group: one repository at most once, each at the exact commit asked and the trunk commit it is merged onto; instead of repository and head
Members []TryInputMembersItem `json:"members,omitempty"`
// <owner>/<name>, for a try of one member (instead of members)
Repository string `json:"repository,omitempty"`
// a go test -run pattern: only those tests run, and nothing is kept
Tests string `json:"tests,omitempty"`
// the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent
Trunk string `json:"trunk,omitempty"`
}
// TryAnswer is what try answers.
type TryAnswer struct {
// the try's id
ID string `json:"id"`
// its state
State string `json:"state,omitempty"`
}
// TryReadInput is what try-read takes.
type TryReadInput struct {
// the try's id, try-<digits>
ID string `json:"id"`
}
// TryReadAnswer is what try-read answers.
type TryReadAnswer = map[string]any
// TryEndInput is what try-end takes.
type TryEndInput struct {
// the try's id, try-<digits>
ID string `json:"id"`
// why it is ended, in words
Why string `json:"why"`
}
// TryEndAnswer is what try-end answers.
type TryEndAnswer struct {
// it is ended
Ended bool `json:"ended"`
// the try
ID string `json:"id"`
}
// VerdictHolderEventSeriesItem is a type of the contract.
type VerdictHolderEventSeriesItem struct {
// its artifact, or the package's ecosystem name as the module's packages name it
Artifact string `json:"artifact"`
// the series on trunk
From string `json:"from"`
// the module whose artifact's series changes
Module string `json:"module"`
// the series the checked head gives it
To string `json:"to"`
}
// VerdictHolderEvent is the body of the holder's event verdict.
type VerdictHolderEvent struct {
// the commit checked
Head string `json:"head"`
// the generated files out of step with their seat definitions, each path (design 26)
OutOfStep []string `json:"out-of-step,omitempty"`
// the pull request's number, for a pull request's check
Pull int64 `json:"pull,omitempty"`
// owner/repo
Repository string `json:"repository"`
// each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none
Series []VerdictHolderEventSeriesItem `json:"series,omitempty"`
// pass or fail
Verdict string `json:"verdict"`
}
// TagWantedHolderEvent is the body of the holder's event tag-wanted.
type TagWantedHolderEvent struct {
// the pull request's branch: its tag is removed when the branch ends
Branch string `json:"branch"`
// its head
Head string `json:"head"`
// the group member
Module string `json:"module"`
// the package it declares
Package string `json:"package"`
// the pull request
Pull int64 `json:"pull,omitempty"`
// owner/repo
Repository string `json:"repository"`
// the tag wanted
Tag string `json:"tag,omitempty"`
}
// VerdictsByGroupHeadsValueComposition is a type of the contract.
type VerdictsByGroupHeadsValueComposition struct {
Fpset string `json:"fpset,omitempty"`
Machines []string `json:"machines,omitempty"`
Sdk string `json:"sdk,omitempty"`
// not-run, pass or fail
State string `json:"state"`
Words string `json:"words,omitempty"`
}
// VerdictsByGroupHeadsValueFailed is a type of the contract.
type VerdictsByGroupHeadsValueFailed struct {
Machine string `json:"machine,omitempty"`
Test string `json:"test,omitempty"`
Words string `json:"words,omitempty"`
}
// VerdictsByGroupHeadsValueFailuresItem is a type of the contract.
type VerdictsByGroupHeadsValueFailuresItem struct {
Machine string `json:"machine,omitempty"`
Test string `json:"test,omitempty"`
Words string `json:"words,omitempty"`
}
// VerdictsByGroupHeadsValueLevelsValue is a type of the contract.
type VerdictsByGroupHeadsValueLevelsValue struct {
Kept int64 `json:"kept"`
Run int64 `json:"run"`
Skipped int64 `json:"skipped,omitempty"`
}
// VerdictsByGroupHeadsValueSeriesItem is a type of the contract.
type VerdictsByGroupHeadsValueSeriesItem struct {
Artifact string `json:"artifact"`
From string `json:"from"`
Module string `json:"module"`
To string `json:"to"`
}
// VerdictsByGroupHeadsValueTook is milliseconds.
type VerdictsByGroupHeadsValueTook struct {
Clone int64 `json:"clone,omitempty"`
Composition int64 `json:"composition,omitempty"`
IntegrationRun int64 `json:"integration-run,omitempty"`
IntegrationSetup int64 `json:"integration-setup,omitempty"`
Lint int64 `json:"lint,omitempty"`
Queue int64 `json:"queue,omitempty"`
Unit int64 `json:"unit,omitempty"`
}
// VerdictsByGroupHeadsValue is one value of the state verdicts under the key <group>.<heads>.
type VerdictsByGroupHeadsValue struct {
Beside map[string]string `json:"beside,omitempty"`
CarriedFrom map[string]string `json:"carried-from,omitempty"`
Composition *VerdictsByGroupHeadsValueComposition `json:"composition,omitempty"`
Failed *VerdictsByGroupHeadsValueFailed `json:"failed,omitempty"`
Failures []VerdictsByGroupHeadsValueFailuresItem `json:"failures,omitempty"`
Group string `json:"group"`
// 12 hex digits
Heads string `json:"heads"`
// by level, unit or integration
Levels map[string]VerdictsByGroupHeadsValueLevelsValue `json:"levels,omitempty"`
Machines []string `json:"machines,omitempty"`
OverBound []string `json:"over-bound,omitempty"`
Sdk string `json:"sdk,omitempty"`
Series []VerdictsByGroupHeadsValueSeriesItem `json:"series,omitempty"`
// running, pass or fail
State string `json:"state"`
// milliseconds
Took *VerdictsByGroupHeadsValueTook `json:"took,omitempty"`
// repository to the trunk head merged onto
Trunk map[string]string `json:"trunk,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
}
// KeptByLevelDigestValue is one value of the state kept under the key <level>.<digest>.
type KeptByLevelDigestValue struct {
// when (RFC 3339 time)
At string `json:"at,omitempty"`
Level string `json:"level"`
Package string `json:"package"`
Repository string `json:"repository"`
// pass: a failure is never kept
State string `json:"state"`
Tests int64 `json:"tests,omitempty"`
Took int64 `json:"took,omitempty"`
Toolchain string `json:"toolchain,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
}
// OwedByRepositoryNumberHeadActValue is one value of the state owed under the key <repository>.<number>.<head>.<act>.
type OwedByRepositoryNumberHeadActValue struct {
// status=<context>, view, note or comment.<seq>
Act string `json:"act"`
Context string `json:"context,omitempty"`
Ref string `json:"ref,omitempty"`
State string `json:"state,omitempty"`
Target string `json:"target,omitempty"`
Text string `json:"text,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
}
// PreviewsByRepositoryHeadTrunkValueModulesValue is a type of the contract.
type PreviewsByRepositoryHeadTrunkValueModulesValue struct {
Locks map[string]string `json:"locks,omitempty"`
// manifest digest
Manifest string `json:"manifest"`
Packages []string `json:"packages,omitempty"`
Paths map[string]string `json:"paths,omitempty"`
Tree string `json:"tree"`
}
// PreviewsByRepositoryHeadTrunkValue is one value of the state previews under the key <repository>.<head>.<trunk>.
type PreviewsByRepositoryHeadTrunkValue struct {
// files in conflict
Conflicts []string `json:"conflicts,omitempty"`
Head string `json:"head"`
// the merge commit
Merge string `json:"merge,omitempty"`
// by directory
Modules map[string]PreviewsByRepositoryHeadTrunkValueModulesValue `json:"modules,omitempty"`
Repository string `json:"repository"`
Trunk string `json:"trunk"`
// the layout version
V int64 `json:"v,omitempty"`
}
@@ -0,0 +1,72 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
package meshdeliverychecker
import (
"encoding/json"
"reflect"
"regexp"
"testing"
)
// TestFixtures reads every fixture of seats/mesh-delivery-checker/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: "verdict on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.verdict", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"series\":[{\"artifact\":\"go\",\"from\":\"0.1\",\"module\":\"mesh-contracts\",\"to\":\"0.2\"}],\"verdict\":\"fail\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"series\":[{\"artifact\":\"go\",\"from\":\"0.1\",\"module\":\"mesh-contracts\",\"to\":\"0.2\"}],\"verdict\":\"fail\"}",
decode: func(b []byte) (any, error) { var v VerdictHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.verdict$")},
{name: "tag-wanted on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.tag-wanted", given: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", wire: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}",
decode: func(b []byte) (any, error) { var v TagWantedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.tag-wanted$")},
{name: "preview.written on the checker's stream", subject: "mesh.stream.mesh-delivery-checker.event.preview.written", given: "{}", wire: "{}",
decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.preview\\.written$")},
{name: "a running verdict", subject: "", given: "{\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"state\":\"running\",\"v\":1}", wire: "{\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"state\":\"running\",\"v\":1}",
decode: func(b []byte) (any, error) {
var v VerdictsByGroupHeadsValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a kept pass", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"level\":\"unit\",\"package\":\"go/keys\",\"repository\":\"novox/mesh-sdk\",\"state\":\"pass\",\"tests\":4,\"took\":120,\"toolchain\":\"go1.26\",\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"level\":\"unit\",\"package\":\"go/keys\",\"repository\":\"novox/mesh-sdk\",\"state\":\"pass\",\"tests\":4,\"took\":120,\"toolchain\":\"go1.26\",\"v\":1}",
decode: func(b []byte) (any, error) { var v KeptByLevelDigestValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "the status owed for a head", subject: "", given: "{\"act\":\"status=mesh/check\",\"context\":\"mesh/check\",\"state\":\"success\",\"v\":1}", wire: "{\"act\":\"status=mesh/check\",\"context\":\"mesh/check\",\"state\":\"success\",\"v\":1}",
decode: func(b []byte) (any, error) {
var v OwedByRepositoryNumberHeadActValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a preview", subject: "", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"merge\":\"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\",\"repository\":\"novox/mesh-sdk\",\"trunk\":\"dddddddddddddddddddddddddddddddddddddddd\",\"v\":1}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"merge\":\"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\",\"repository\":\"novox/mesh-sdk\",\"trunk\":\"dddddddddddddddddddddddddddddddddddddddd\",\"v\":1}",
decode: func(b []byte) (any, error) {
var v PreviewsByRepositoryHeadTrunkValue
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)
}
}
}
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker
go 1.26.0
+557
View File
@@ -0,0 +1,557 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
// Package nodebuildgo is the contract of the seat "node-build-go", defined whole (seats/node-build-go/seat.json): The Go build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers.
package nodebuildgo
import (
"bytes"
"encoding/json"
"errors"
"strings"
"time"
)
// Seat is the seat's name.
const Seat = "node-build-go"
const (
VerbCurrent = "current"
VerbPause = "pause"
VerbResume = "resume"
)
const (
AcceptDelivery = "delivery"
AcceptCheck = "check"
AcceptRecheck = "recheck"
)
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
func Verbs() []string {
return []string{VerbCurrent, VerbPause, VerbResume}
}
// AcceptSubjectDelivery is where delivery is sent.
const AcceptSubjectDelivery = "mesh.seat.node-build-go.accept.delivery"
// AcceptSubjectCheck is where check is sent.
const AcceptSubjectCheck = "mesh.seat.node-build-go.accept.check"
// AcceptSubjectRecheck is where recheck is sent.
const AcceptSubjectRecheck = "mesh.seat.node-build-go.accept.recheck"
const (
StateSteps = "steps"
StatePauses = "pauses"
StateToolchains = "toolchains"
)
// StepsByNodeIDKey is the key of one value of the state steps under the form "<node>.<id>".
func StepsByNodeIDKey(node, id string) string { return node + "." + id }
// ParseStepsByNodeIDKey reads the parts of a key of the form "<node>.<id>", in the order of the form; the last result is false for a key of another form.
func ParseStepsByNodeIDKey(key string) (string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", false
}
rest = after0
if rest == "" {
return "", "", false
}
v2 := rest
rest = ""
if rest != "" {
return "", "", false
}
return v0, v2, true
}
// DecodeStepsByNodeIDValue reads one value of the state steps under the form "<node>.<id>", refusing a field StepsByNodeIDValue does not know and any text after the value.
func DecodeStepsByNodeIDValue(raw []byte) (StepsByNodeIDValue, error) {
var v StepsByNodeIDValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// PausesByPausedNodeKey is the key of one value of the state pauses under the form "paused.<node>".
func PausesByPausedNodeKey(node string) string { return "paused." + node }
// ParsePausesByPausedNodeKey reads the parts of a key of the form "paused.<node>", in the order of the form; the last result is false for a key of another form.
func ParsePausesByPausedNodeKey(key string) (string, bool) {
rest := key
if !strings.HasPrefix(rest, "paused.") {
return "", false
}
rest = rest[7:]
if rest == "" {
return "", false
}
v1 := rest
rest = ""
if rest != "" {
return "", false
}
return v1, true
}
// DecodePausesByPausedNodeValue reads one value of the state pauses under the form "paused.<node>", refusing a field PausesByPausedNodeValue does not know and any text after the value.
func DecodePausesByPausedNodeValue(raw []byte) (PausesByPausedNodeValue, error) {
var v PausesByPausedNodeValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// PausesByCapacityNodeKey is the key of one value of the state pauses under the form "capacity.<node>".
func PausesByCapacityNodeKey(node string) string { return "capacity." + node }
// ParsePausesByCapacityNodeKey reads the parts of a key of the form "capacity.<node>", in the order of the form; the last result is false for a key of another form.
func ParsePausesByCapacityNodeKey(key string) (string, bool) {
rest := key
if !strings.HasPrefix(rest, "capacity.") {
return "", false
}
rest = rest[9:]
if rest == "" {
return "", false
}
v1 := rest
rest = ""
if rest != "" {
return "", false
}
return v1, true
}
// DecodePausesByCapacityNodeValue reads one value of the state pauses under the form "capacity.<node>", refusing a field PausesByCapacityNodeValue does not know and any text after the value.
func DecodePausesByCapacityNodeValue(raw []byte) (PausesByCapacityNodeValue, error) {
var v PausesByCapacityNodeValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// ToolchainsByNodeLanguageKey is the key of one value of the state toolchains under the form "<node>.<language>".
func ToolchainsByNodeLanguageKey(node, language string) string { return node + "." + language }
// ParseToolchainsByNodeLanguageKey reads the parts of a key of the form "<node>.<language>", in the order of the form; the last result is false for a key of another form.
func ParseToolchainsByNodeLanguageKey(key string) (string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", false
}
rest = after0
if rest == "" {
return "", "", false
}
v2 := rest
rest = ""
if rest != "" {
return "", "", false
}
return v0, v2, true
}
// DecodeToolchainsByNodeLanguageValue reads one value of the state toolchains under the form "<node>.<language>", refusing a field ToolchainsByNodeLanguageValue does not know and any text after the value.
func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageValue, error) {
var v ToolchainsByNodeLanguageValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// AskArtifact is one artifact of a build ask: built from the fetched tree, or copied from an upstream pin.
type AskArtifact struct {
Artifact string `json:"artifact"`
From string `json:"from,omitempty"`
Seat string `json:"seat,omitempty"`
Version string `json:"version,omitempty"`
}
// BuildAnswer is a type of the contract.
type BuildAnswer struct {
Artifacts []BuiltArtifact `json:"artifacts"`
Imports []string `json:"imports,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// BuiltArtifact is one artifact a build made: its digest and the reference it was published under.
type BuiltArtifact struct {
Artifact string `json:"artifact"`
Digest string `json:"digest"`
Reference string `json:"reference,omitempty"`
}
// Capacity is how many steps the node runs at once.
type Capacity struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
BuildHours string `json:"build-hours,omitempty"`
BuildHoursFault string `json:"build-hours-fault,omitempty"`
ByCpu int64 `json:"by-cpu,omitempty"`
Configured int64 `json:"configured"`
ConfiguredFrom string `json:"configured-from,omitempty"`
CoresPerStep float64 `json:"cores-per-step,omitempty"`
Effective int64 `json:"effective"`
Ineligible string `json:"ineligible,omitempty"`
OutsideHours bool `json:"outside-hours,omitempty"`
QuotaCores float64 `json:"quota-cores,omitempty"`
QuotaFrom string `json:"quota-from,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
Why string `json:"why"`
}
// CheckAnswer is a type of the contract.
type CheckAnswer struct {
Fetch FetchAnswer `json:"fetch"`
Levels []LevelAnswer `json:"levels,omitempty"`
}
// ComposeAnswer is a type of the contract.
type ComposeAnswer struct {
Built bool `json:"built"`
Key string `json:"key,omitempty"`
// the compose-candidate verdict
Verdict any `json:"verdict,omitempty"`
}
// FetchBesideItem is a type of the contract.
type FetchBesideItem struct {
Commit string `json:"commit"`
Repository string `json:"repository"`
}
// FetchPackage is the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's.
type FetchPackage struct {
Commit string `json:"commit,omitempty"`
Name string `json:"name,omitempty"`
Ref string `json:"ref,omitempty"`
Repository string `json:"repository,omitempty"`
Version string `json:"version,omitempty"`
}
// Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside.
type Fetch struct {
// the trunk commit a merge preview merges onto
Base string `json:"base,omitempty"`
// the sibling repositories placed beside the fetched tree
Beside []FetchBesideItem `json:"beside,omitempty"`
// the full commit
Commit string `json:"commit"`
// the module's directory in the repository ("." for its root), with paths
Dir string `json:"dir,omitempty"`
// the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's
Package *FetchPackage `json:"package,omitempty"`
// the version (a full commit) of each mesh package to build against, by its module path
Packages map[string]string `json:"packages,omitempty"`
// the paths the module names, relative to dir
Paths []string `json:"paths,omitempty"`
// the pull request's ref, when only its head carries the commit
Ref string `json:"ref,omitempty"`
// <owner>/<name>
Repository string `json:"repository"`
}
// FetchAnswer is what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package.
type FetchAnswer struct {
Base string `json:"base,omitempty"`
Beside []PlacedBeside `json:"beside,omitempty"`
Changed []string `json:"changed,omitempty"`
Commit string `json:"commit"`
Conflicts []string `json:"conflicts,omitempty"`
Group string `json:"group,omitempty"`
Installed []string `json:"installed,omitempty"`
Merge string `json:"merge,omitempty"`
Modules []string `json:"modules"`
Packages []TakenVersion `json:"packages,omitempty"`
Programs []Program `json:"programs,omitempty"`
Refused []FetchRefusal `json:"refused,omitempty"`
Trees map[string]ModuleTree `json:"trees,omitempty"`
Workspace string `json:"workspace"`
}
// FetchRefusal is a TypeScript module whose packages the fetch did not install, with why.
type FetchRefusal struct {
Module string `json:"module"`
Reason string `json:"reason"`
}
// LevelAnswerDigestNeedsItem is a type of the contract.
type LevelAnswerDigestNeedsItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswerDigestPackagesItem is a type of the contract.
type LevelAnswerDigestPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// LevelAnswerDigest is a type of the contract.
type LevelAnswerDigest struct {
Beside []string `json:"beside,omitempty"`
JudgedBy string `json:"judged_by,omitempty"`
Level string `json:"level,omitempty"`
Needs []LevelAnswerDigestNeedsItem `json:"needs,omitempty"`
Packages []LevelAnswerDigestPackagesItem `json:"packages,omitempty"`
WithoutTests []string `json:"without_tests,omitempty"`
}
// LevelAnswerKeptItem is a type of the contract.
type LevelAnswerKeptItem struct {
Digest string `json:"digest,omitempty"`
Package string `json:"package,omitempty"`
Tests int64 `json:"tests,omitempty"`
}
// LevelAnswerUnmetItem is a type of the contract.
type LevelAnswerUnmetItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswer is one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test.
type LevelAnswer struct {
Digest *LevelAnswerDigest `json:"digest,omitempty"`
Judged string `json:"judged,omitempty"`
Kept []LevelAnswerKeptItem `json:"kept,omitempty"`
Level string `json:"level"`
Tests []any `json:"tests,omitempty"`
Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"`
}
// ModuleTree is the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths.
type ModuleTree struct {
Locks map[string]string `json:"locks,omitempty"`
Manifest string `json:"manifest"`
// no longer filled (ADR 0593 section 2); kept so a preview written before still reads
Packages []string `json:"packages,omitempty"`
Paths map[string]string `json:"paths,omitempty"`
Tree string `json:"tree"`
}
// Pause is whether the node takes no new ask.
type Pause struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
By string `json:"by,omitempty"`
Paused bool `json:"paused"`
// the layout version
V int64 `json:"v,omitempty"`
}
// PlacedBeside is a sibling repository placed as a plain directory under the workspace's beside directory.
type PlacedBeside struct {
Commit string `json:"commit"`
Dir string `json:"dir"`
Repository string `json:"repository"`
}
// Program is a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it.
type Program struct {
Commit string `json:"commit,omitempty"`
// ELF, Mach-O or PE
Kind string `json:"kind"`
Path string `json:"path"`
}
// Step is one step a build module runs on a node.
type Step struct {
// when (RFC 3339 time)
Ended *time.Time `json:"ended,omitempty"`
ID string `json:"id"`
Node string `json:"node"`
Owner string `json:"owner,omitempty"`
Phase string `json:"phase,omitempty"`
// when (RFC 3339 time)
PhaseAt *time.Time `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
Result *StepResult `json:"result,omitempty"`
// when (RFC 3339 time)
Started time.Time `json:"started"`
// running, passed, failed or refused
State string `json:"state"`
Tool string `json:"tool"`
// the layout version
V int64 `json:"v,omitempty"`
What string `json:"what,omitempty"`
}
// StepAskBuild is a type of the contract.
type StepAskBuild struct {
Artifacts []AskArtifact `json:"artifacts"`
Fetch *Fetch `json:"fetch,omitempty"`
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
Path string `json:"path,omitempty"`
Publish bool `json:"publish,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// StepAskCheck is a type of the contract.
type StepAskCheck struct {
Fetch Fetch `json:"fetch"`
Kept bool `json:"kept,omitempty"`
Levels []string `json:"levels"`
// the node names the check's tests run against
NodeNames []string `json:"node-names,omitempty"`
Packages []string `json:"packages,omitempty"`
Run string `json:"run,omitempty"`
Runner string `json:"runner,omitempty"`
RunnerWhen string `json:"runner_when,omitempty"`
// the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// StepAskCompose is a type of the contract.
type StepAskCompose struct {
Dir string `json:"dir"`
Fetch Fetch `json:"fetch"`
Package string `json:"package"`
Reader string `json:"reader,omitempty"`
// the compose-candidate request
Request any `json:"request"`
}
// StepAsk is one ask of a build module.
type StepAsk struct {
Asker string `json:"asker"`
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
Build *StepAskBuild `json:"build,omitempty"`
Check *StepAskCheck `json:"check,omitempty"`
Compose *StepAskCompose `json:"compose,omitempty"`
ID string `json:"id"`
// build, check or compose
Kind string `json:"kind"`
// go, typescript or image
Language string `json:"language"`
Needs []string `json:"needs,omitempty"`
// package to the tagged version a build pins
Packages map[string]string `json:"packages,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
}
// StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four.
type StepResult struct {
Artifacts []BuiltArtifact `json:"artifacts,omitempty"`
Built bool `json:"built,omitempty"`
// the holder declined the ask: its node lacks what the ask needs
Declined bool `json:"declined,omitempty"`
Fetch *FetchAnswer `json:"fetch,omitempty"`
Imports []string `json:"imports,omitempty"`
Key string `json:"key,omitempty"`
// what the holder's node lacks of it
Lacks []string `json:"lacks,omitempty"`
Levels []LevelAnswer `json:"levels,omitempty"`
// what the declined ask needs
Needs []string `json:"needs,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
// the compose-candidate verdict (the composer's own layout)
Verdict any `json:"verdict,omitempty"`
}
// TakenVersion is of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said.
type TakenVersion struct {
Asked string `json:"asked"`
Built string `json:"built"`
Dir string `json:"dir"`
Named string `json:"named"`
Package string `json:"package"`
Said string `json:"said,omitempty"`
Sum string `json:"sum,omitempty"`
}
// Toolchain is the toolchain a node reports.
type Toolchain struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
// go or typescript
Language string `json:"language"`
Recipe string `json:"recipe"`
// the layout version
V int64 `json:"v,omitempty"`
Version string `json:"version"`
}
// ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe.
type ToolchainUsed struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
}
// CurrentInput is what current takes.
type CurrentInput struct {
}
// CurrentAnswer is what current answers.
type CurrentAnswer = map[string]any
// PauseInput is what pause takes.
type PauseInput struct {
}
// PauseAnswer is what pause answers.
type PauseAnswer = map[string]any
// ResumeInput is what resume takes.
type ResumeInput struct {
}
// ResumeAnswer is what resume answers.
type ResumeAnswer = map[string]any
// DeliveryAccept is the body of delivery.
type DeliveryAccept = StepAsk
// CheckAccept is the body of check.
type CheckAccept = StepAsk
// RecheckAccept is the body of recheck.
type RecheckAccept = StepAsk
// StepsByNodeIDValue is one value of the state steps under the key <node>.<id>.
type StepsByNodeIDValue = Step
// PausesByPausedNodeValue is one value of the state pauses under the key paused.<node>.
type PausesByPausedNodeValue = Pause
// PausesByCapacityNodeValue is one value of the state pauses under the key capacity.<node>.
type PausesByCapacityNodeValue = Capacity
// ToolchainsByNodeLanguageValue is one value of the state toolchains under the key <node>.<language>.
type ToolchainsByNodeLanguageValue = Toolchain
@@ -0,0 +1,78 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
package nodebuildgo
import (
"encoding/json"
"reflect"
"regexp"
"testing"
)
// TestFixtures reads every fixture of seats/node-build-go/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: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}",
decode: func(b []byte) (any, error) {
var v PausesByPausedNodeValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a node's capacity", subject: "", given: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}", wire: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}",
decode: func(b []byte) (any, error) {
var v PausesByCapacityNodeValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a node's toolchain", subject: "", given: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", wire: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}",
decode: func(b []byte) (any, error) {
var v ToolchainsByNodeLanguageValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a build ask", subject: "mesh.seat.node-build-go.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}",
decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.delivery$")},
{name: "a check ask with its fetch, siblings and levels", subject: "mesh.seat.node-build-go.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"2\",\"version\":\"1.26.0\"}},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"2\",\"version\":\"1.26.0\"}},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}",
decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.check$")},
{name: "a finished build step with its artifacts", subject: "", given: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a finished check step with its levels", subject: "", given: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"a.go\",\"modules/m/main.go\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"fedcba9876543210fedcba9876543210fedcba98\",\"modules\":[\".\"],\"trees\":{\"modules/m\":{\"locks\":{\"go.sum\":\"c5fc83c01e92404452b986527d239140ccf9a48b88e0c268fbf38c2e1429e9c9\"},\"manifest\":\"ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356\",\"tree\":\"1111111111111111111111111111111111111111\"}},\"workspace\":\"mvii1iwu5414e4\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"a.go\",\"modules/m/main.go\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"fedcba9876543210fedcba9876543210fedcba98\",\"modules\":[\".\"],\"trees\":{\"modules/m\":{\"locks\":{\"go.sum\":\"c5fc83c01e92404452b986527d239140ccf9a48b88e0c268fbf38c2e1429e9c9\"},\"manifest\":\"ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356\",\"tree\":\"1111111111111111111111111111111111111111\"}},\"workspace\":\"mvii1iwu5414e4\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a declined step", subject: "", given: "{\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"declined\":true,\"lacks\":[\"docker\"],\"needs\":[\"docker\"]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"refused\",\"tool\":\"check\",\"v\":1}", wire: "{\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"declined\":true,\"lacks\":[\"docker\"],\"needs\":[\"docker\"]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"refused\",\"tool\":\"check\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a finished check step whose head does not merge", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"conflicts\":[\"a.go\",\"sub/b.go\"],\"modules\":[\".\"],\"workspace\":\"mvii1iwz5050eb\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"conflicts\":[\"a.go\",\"sub/b.go\"],\"modules\":[\".\"],\"workspace\":\"mvii1iwz5050eb\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; 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)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo
go 1.26.0
@@ -0,0 +1,557 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
// Package nodebuildimage is the contract of the seat "node-build-image", defined whole (seats/node-build-image/seat.json): The image build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers.
package nodebuildimage
import (
"bytes"
"encoding/json"
"errors"
"strings"
"time"
)
// Seat is the seat's name.
const Seat = "node-build-image"
const (
VerbCurrent = "current"
VerbPause = "pause"
VerbResume = "resume"
)
const (
AcceptDelivery = "delivery"
AcceptCheck = "check"
AcceptRecheck = "recheck"
)
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
func Verbs() []string {
return []string{VerbCurrent, VerbPause, VerbResume}
}
// AcceptSubjectDelivery is where delivery is sent.
const AcceptSubjectDelivery = "mesh.seat.node-build-image.accept.delivery"
// AcceptSubjectCheck is where check is sent.
const AcceptSubjectCheck = "mesh.seat.node-build-image.accept.check"
// AcceptSubjectRecheck is where recheck is sent.
const AcceptSubjectRecheck = "mesh.seat.node-build-image.accept.recheck"
const (
StateSteps = "steps"
StatePauses = "pauses"
StateToolchains = "toolchains"
)
// StepsByNodeIDKey is the key of one value of the state steps under the form "<node>.<id>".
func StepsByNodeIDKey(node, id string) string { return node + "." + id }
// ParseStepsByNodeIDKey reads the parts of a key of the form "<node>.<id>", in the order of the form; the last result is false for a key of another form.
func ParseStepsByNodeIDKey(key string) (string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", false
}
rest = after0
if rest == "" {
return "", "", false
}
v2 := rest
rest = ""
if rest != "" {
return "", "", false
}
return v0, v2, true
}
// DecodeStepsByNodeIDValue reads one value of the state steps under the form "<node>.<id>", refusing a field StepsByNodeIDValue does not know and any text after the value.
func DecodeStepsByNodeIDValue(raw []byte) (StepsByNodeIDValue, error) {
var v StepsByNodeIDValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// PausesByPausedNodeKey is the key of one value of the state pauses under the form "paused.<node>".
func PausesByPausedNodeKey(node string) string { return "paused." + node }
// ParsePausesByPausedNodeKey reads the parts of a key of the form "paused.<node>", in the order of the form; the last result is false for a key of another form.
func ParsePausesByPausedNodeKey(key string) (string, bool) {
rest := key
if !strings.HasPrefix(rest, "paused.") {
return "", false
}
rest = rest[7:]
if rest == "" {
return "", false
}
v1 := rest
rest = ""
if rest != "" {
return "", false
}
return v1, true
}
// DecodePausesByPausedNodeValue reads one value of the state pauses under the form "paused.<node>", refusing a field PausesByPausedNodeValue does not know and any text after the value.
func DecodePausesByPausedNodeValue(raw []byte) (PausesByPausedNodeValue, error) {
var v PausesByPausedNodeValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// PausesByCapacityNodeKey is the key of one value of the state pauses under the form "capacity.<node>".
func PausesByCapacityNodeKey(node string) string { return "capacity." + node }
// ParsePausesByCapacityNodeKey reads the parts of a key of the form "capacity.<node>", in the order of the form; the last result is false for a key of another form.
func ParsePausesByCapacityNodeKey(key string) (string, bool) {
rest := key
if !strings.HasPrefix(rest, "capacity.") {
return "", false
}
rest = rest[9:]
if rest == "" {
return "", false
}
v1 := rest
rest = ""
if rest != "" {
return "", false
}
return v1, true
}
// DecodePausesByCapacityNodeValue reads one value of the state pauses under the form "capacity.<node>", refusing a field PausesByCapacityNodeValue does not know and any text after the value.
func DecodePausesByCapacityNodeValue(raw []byte) (PausesByCapacityNodeValue, error) {
var v PausesByCapacityNodeValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// ToolchainsByNodeLanguageKey is the key of one value of the state toolchains under the form "<node>.<language>".
func ToolchainsByNodeLanguageKey(node, language string) string { return node + "." + language }
// ParseToolchainsByNodeLanguageKey reads the parts of a key of the form "<node>.<language>", in the order of the form; the last result is false for a key of another form.
func ParseToolchainsByNodeLanguageKey(key string) (string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", false
}
rest = after0
if rest == "" {
return "", "", false
}
v2 := rest
rest = ""
if rest != "" {
return "", "", false
}
return v0, v2, true
}
// DecodeToolchainsByNodeLanguageValue reads one value of the state toolchains under the form "<node>.<language>", refusing a field ToolchainsByNodeLanguageValue does not know and any text after the value.
func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageValue, error) {
var v ToolchainsByNodeLanguageValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// AskArtifact is one artifact of a build ask: built from the fetched tree, or copied from an upstream pin.
type AskArtifact struct {
Artifact string `json:"artifact"`
From string `json:"from,omitempty"`
Seat string `json:"seat,omitempty"`
Version string `json:"version,omitempty"`
}
// BuildAnswer is a type of the contract.
type BuildAnswer struct {
Artifacts []BuiltArtifact `json:"artifacts"`
Imports []string `json:"imports,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// BuiltArtifact is one artifact a build made: its digest and the reference it was published under.
type BuiltArtifact struct {
Artifact string `json:"artifact"`
Digest string `json:"digest"`
Reference string `json:"reference,omitempty"`
}
// Capacity is how many steps the node runs at once.
type Capacity struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
BuildHours string `json:"build-hours,omitempty"`
BuildHoursFault string `json:"build-hours-fault,omitempty"`
ByCpu int64 `json:"by-cpu,omitempty"`
Configured int64 `json:"configured"`
ConfiguredFrom string `json:"configured-from,omitempty"`
CoresPerStep float64 `json:"cores-per-step,omitempty"`
Effective int64 `json:"effective"`
Ineligible string `json:"ineligible,omitempty"`
OutsideHours bool `json:"outside-hours,omitempty"`
QuotaCores float64 `json:"quota-cores,omitempty"`
QuotaFrom string `json:"quota-from,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
Why string `json:"why"`
}
// CheckAnswer is a type of the contract.
type CheckAnswer struct {
Fetch FetchAnswer `json:"fetch"`
Levels []LevelAnswer `json:"levels,omitempty"`
}
// ComposeAnswer is a type of the contract.
type ComposeAnswer struct {
Built bool `json:"built"`
Key string `json:"key,omitempty"`
// the compose-candidate verdict
Verdict any `json:"verdict,omitempty"`
}
// FetchBesideItem is a type of the contract.
type FetchBesideItem struct {
Commit string `json:"commit"`
Repository string `json:"repository"`
}
// FetchPackage is the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's.
type FetchPackage struct {
Commit string `json:"commit,omitempty"`
Name string `json:"name,omitempty"`
Ref string `json:"ref,omitempty"`
Repository string `json:"repository,omitempty"`
Version string `json:"version,omitempty"`
}
// Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside.
type Fetch struct {
// the trunk commit a merge preview merges onto
Base string `json:"base,omitempty"`
// the sibling repositories placed beside the fetched tree
Beside []FetchBesideItem `json:"beside,omitempty"`
// the full commit
Commit string `json:"commit"`
// the module's directory in the repository ("." for its root), with paths
Dir string `json:"dir,omitempty"`
// the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's
Package *FetchPackage `json:"package,omitempty"`
// the version (a full commit) of each mesh package to build against, by its module path
Packages map[string]string `json:"packages,omitempty"`
// the paths the module names, relative to dir
Paths []string `json:"paths,omitempty"`
// the pull request's ref, when only its head carries the commit
Ref string `json:"ref,omitempty"`
// <owner>/<name>
Repository string `json:"repository"`
}
// FetchAnswer is what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package.
type FetchAnswer struct {
Base string `json:"base,omitempty"`
Beside []PlacedBeside `json:"beside,omitempty"`
Changed []string `json:"changed,omitempty"`
Commit string `json:"commit"`
Conflicts []string `json:"conflicts,omitempty"`
Group string `json:"group,omitempty"`
Installed []string `json:"installed,omitempty"`
Merge string `json:"merge,omitempty"`
Modules []string `json:"modules"`
Packages []TakenVersion `json:"packages,omitempty"`
Programs []Program `json:"programs,omitempty"`
Refused []FetchRefusal `json:"refused,omitempty"`
Trees map[string]ModuleTree `json:"trees,omitempty"`
Workspace string `json:"workspace"`
}
// FetchRefusal is a TypeScript module whose packages the fetch did not install, with why.
type FetchRefusal struct {
Module string `json:"module"`
Reason string `json:"reason"`
}
// LevelAnswerDigestNeedsItem is a type of the contract.
type LevelAnswerDigestNeedsItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswerDigestPackagesItem is a type of the contract.
type LevelAnswerDigestPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// LevelAnswerDigest is a type of the contract.
type LevelAnswerDigest struct {
Beside []string `json:"beside,omitempty"`
JudgedBy string `json:"judged_by,omitempty"`
Level string `json:"level,omitempty"`
Needs []LevelAnswerDigestNeedsItem `json:"needs,omitempty"`
Packages []LevelAnswerDigestPackagesItem `json:"packages,omitempty"`
WithoutTests []string `json:"without_tests,omitempty"`
}
// LevelAnswerKeptItem is a type of the contract.
type LevelAnswerKeptItem struct {
Digest string `json:"digest,omitempty"`
Package string `json:"package,omitempty"`
Tests int64 `json:"tests,omitempty"`
}
// LevelAnswerUnmetItem is a type of the contract.
type LevelAnswerUnmetItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswer is one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test.
type LevelAnswer struct {
Digest *LevelAnswerDigest `json:"digest,omitempty"`
Judged string `json:"judged,omitempty"`
Kept []LevelAnswerKeptItem `json:"kept,omitempty"`
Level string `json:"level"`
Tests []any `json:"tests,omitempty"`
Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"`
}
// ModuleTree is the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths.
type ModuleTree struct {
Locks map[string]string `json:"locks,omitempty"`
Manifest string `json:"manifest"`
// no longer filled (ADR 0593 section 2); kept so a preview written before still reads
Packages []string `json:"packages,omitempty"`
Paths map[string]string `json:"paths,omitempty"`
Tree string `json:"tree"`
}
// Pause is whether the node takes no new ask.
type Pause struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
By string `json:"by,omitempty"`
Paused bool `json:"paused"`
// the layout version
V int64 `json:"v,omitempty"`
}
// PlacedBeside is a sibling repository placed as a plain directory under the workspace's beside directory.
type PlacedBeside struct {
Commit string `json:"commit"`
Dir string `json:"dir"`
Repository string `json:"repository"`
}
// Program is a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it.
type Program struct {
Commit string `json:"commit,omitempty"`
// ELF, Mach-O or PE
Kind string `json:"kind"`
Path string `json:"path"`
}
// Step is one step a build module runs on a node.
type Step struct {
// when (RFC 3339 time)
Ended *time.Time `json:"ended,omitempty"`
ID string `json:"id"`
Node string `json:"node"`
Owner string `json:"owner,omitempty"`
Phase string `json:"phase,omitempty"`
// when (RFC 3339 time)
PhaseAt *time.Time `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
Result *StepResult `json:"result,omitempty"`
// when (RFC 3339 time)
Started time.Time `json:"started"`
// running, passed, failed or refused
State string `json:"state"`
Tool string `json:"tool"`
// the layout version
V int64 `json:"v,omitempty"`
What string `json:"what,omitempty"`
}
// StepAskBuild is a type of the contract.
type StepAskBuild struct {
Artifacts []AskArtifact `json:"artifacts"`
Fetch *Fetch `json:"fetch,omitempty"`
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
Path string `json:"path,omitempty"`
Publish bool `json:"publish,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// StepAskCheck is a type of the contract.
type StepAskCheck struct {
Fetch Fetch `json:"fetch"`
Kept bool `json:"kept,omitempty"`
Levels []string `json:"levels"`
// the node names the check's tests run against
NodeNames []string `json:"node-names,omitempty"`
Packages []string `json:"packages,omitempty"`
Run string `json:"run,omitempty"`
Runner string `json:"runner,omitempty"`
RunnerWhen string `json:"runner_when,omitempty"`
// the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// StepAskCompose is a type of the contract.
type StepAskCompose struct {
Dir string `json:"dir"`
Fetch Fetch `json:"fetch"`
Package string `json:"package"`
Reader string `json:"reader,omitempty"`
// the compose-candidate request
Request any `json:"request"`
}
// StepAsk is one ask of a build module.
type StepAsk struct {
Asker string `json:"asker"`
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
Build *StepAskBuild `json:"build,omitempty"`
Check *StepAskCheck `json:"check,omitempty"`
Compose *StepAskCompose `json:"compose,omitempty"`
ID string `json:"id"`
// build, check or compose
Kind string `json:"kind"`
// go, typescript or image
Language string `json:"language"`
Needs []string `json:"needs,omitempty"`
// package to the tagged version a build pins
Packages map[string]string `json:"packages,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
}
// StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four.
type StepResult struct {
Artifacts []BuiltArtifact `json:"artifacts,omitempty"`
Built bool `json:"built,omitempty"`
// the holder declined the ask: its node lacks what the ask needs
Declined bool `json:"declined,omitempty"`
Fetch *FetchAnswer `json:"fetch,omitempty"`
Imports []string `json:"imports,omitempty"`
Key string `json:"key,omitempty"`
// what the holder's node lacks of it
Lacks []string `json:"lacks,omitempty"`
Levels []LevelAnswer `json:"levels,omitempty"`
// what the declined ask needs
Needs []string `json:"needs,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
// the compose-candidate verdict (the composer's own layout)
Verdict any `json:"verdict,omitempty"`
}
// TakenVersion is of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said.
type TakenVersion struct {
Asked string `json:"asked"`
Built string `json:"built"`
Dir string `json:"dir"`
Named string `json:"named"`
Package string `json:"package"`
Said string `json:"said,omitempty"`
Sum string `json:"sum,omitempty"`
}
// Toolchain is the toolchain a node reports.
type Toolchain struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
// go or typescript
Language string `json:"language"`
Recipe string `json:"recipe"`
// the layout version
V int64 `json:"v,omitempty"`
Version string `json:"version"`
}
// ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe.
type ToolchainUsed struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
}
// CurrentInput is what current takes.
type CurrentInput struct {
}
// CurrentAnswer is what current answers.
type CurrentAnswer = map[string]any
// PauseInput is what pause takes.
type PauseInput struct {
}
// PauseAnswer is what pause answers.
type PauseAnswer = map[string]any
// ResumeInput is what resume takes.
type ResumeInput struct {
}
// ResumeAnswer is what resume answers.
type ResumeAnswer = map[string]any
// DeliveryAccept is the body of delivery.
type DeliveryAccept = StepAsk
// CheckAccept is the body of check.
type CheckAccept = StepAsk
// RecheckAccept is the body of recheck.
type RecheckAccept = StepAsk
// StepsByNodeIDValue is one value of the state steps under the key <node>.<id>.
type StepsByNodeIDValue = Step
// PausesByPausedNodeValue is one value of the state pauses under the key paused.<node>.
type PausesByPausedNodeValue = Pause
// PausesByCapacityNodeValue is one value of the state pauses under the key capacity.<node>.
type PausesByCapacityNodeValue = Capacity
// ToolchainsByNodeLanguageValue is one value of the state toolchains under the key <node>.<language>.
type ToolchainsByNodeLanguageValue = Toolchain
@@ -0,0 +1,68 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
package nodebuildimage
import (
"encoding/json"
"reflect"
"regexp"
"testing"
)
// TestFixtures reads every fixture of seats/node-build-image/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: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}",
decode: func(b []byte) (any, error) {
var v PausesByPausedNodeValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a node's capacity", subject: "", given: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}", wire: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}",
decode: func(b []byte) (any, error) {
var v PausesByCapacityNodeValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a node's toolchain", subject: "", given: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", wire: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}",
decode: func(b []byte) (any, error) {
var v ToolchainsByNodeLanguageValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a build ask", subject: "mesh.seat.node-build-image.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}",
decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.accept\\.delivery$")},
}
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)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildimage
go 1.26.0
@@ -0,0 +1,557 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
// Package nodebuildnode is the contract of the seat "node-build-node", defined whole (seats/node-build-node/seat.json): The Node build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers.
package nodebuildnode
import (
"bytes"
"encoding/json"
"errors"
"strings"
"time"
)
// Seat is the seat's name.
const Seat = "node-build-node"
const (
VerbCurrent = "current"
VerbPause = "pause"
VerbResume = "resume"
)
const (
AcceptDelivery = "delivery"
AcceptCheck = "check"
AcceptRecheck = "recheck"
)
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
func Verbs() []string {
return []string{VerbCurrent, VerbPause, VerbResume}
}
// AcceptSubjectDelivery is where delivery is sent.
const AcceptSubjectDelivery = "mesh.seat.node-build-node.accept.delivery"
// AcceptSubjectCheck is where check is sent.
const AcceptSubjectCheck = "mesh.seat.node-build-node.accept.check"
// AcceptSubjectRecheck is where recheck is sent.
const AcceptSubjectRecheck = "mesh.seat.node-build-node.accept.recheck"
const (
StateSteps = "steps"
StatePauses = "pauses"
StateToolchains = "toolchains"
)
// StepsByNodeIDKey is the key of one value of the state steps under the form "<node>.<id>".
func StepsByNodeIDKey(node, id string) string { return node + "." + id }
// ParseStepsByNodeIDKey reads the parts of a key of the form "<node>.<id>", in the order of the form; the last result is false for a key of another form.
func ParseStepsByNodeIDKey(key string) (string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", false
}
rest = after0
if rest == "" {
return "", "", false
}
v2 := rest
rest = ""
if rest != "" {
return "", "", false
}
return v0, v2, true
}
// DecodeStepsByNodeIDValue reads one value of the state steps under the form "<node>.<id>", refusing a field StepsByNodeIDValue does not know and any text after the value.
func DecodeStepsByNodeIDValue(raw []byte) (StepsByNodeIDValue, error) {
var v StepsByNodeIDValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// PausesByPausedNodeKey is the key of one value of the state pauses under the form "paused.<node>".
func PausesByPausedNodeKey(node string) string { return "paused." + node }
// ParsePausesByPausedNodeKey reads the parts of a key of the form "paused.<node>", in the order of the form; the last result is false for a key of another form.
func ParsePausesByPausedNodeKey(key string) (string, bool) {
rest := key
if !strings.HasPrefix(rest, "paused.") {
return "", false
}
rest = rest[7:]
if rest == "" {
return "", false
}
v1 := rest
rest = ""
if rest != "" {
return "", false
}
return v1, true
}
// DecodePausesByPausedNodeValue reads one value of the state pauses under the form "paused.<node>", refusing a field PausesByPausedNodeValue does not know and any text after the value.
func DecodePausesByPausedNodeValue(raw []byte) (PausesByPausedNodeValue, error) {
var v PausesByPausedNodeValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// PausesByCapacityNodeKey is the key of one value of the state pauses under the form "capacity.<node>".
func PausesByCapacityNodeKey(node string) string { return "capacity." + node }
// ParsePausesByCapacityNodeKey reads the parts of a key of the form "capacity.<node>", in the order of the form; the last result is false for a key of another form.
func ParsePausesByCapacityNodeKey(key string) (string, bool) {
rest := key
if !strings.HasPrefix(rest, "capacity.") {
return "", false
}
rest = rest[9:]
if rest == "" {
return "", false
}
v1 := rest
rest = ""
if rest != "" {
return "", false
}
return v1, true
}
// DecodePausesByCapacityNodeValue reads one value of the state pauses under the form "capacity.<node>", refusing a field PausesByCapacityNodeValue does not know and any text after the value.
func DecodePausesByCapacityNodeValue(raw []byte) (PausesByCapacityNodeValue, error) {
var v PausesByCapacityNodeValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// ToolchainsByNodeLanguageKey is the key of one value of the state toolchains under the form "<node>.<language>".
func ToolchainsByNodeLanguageKey(node, language string) string { return node + "." + language }
// ParseToolchainsByNodeLanguageKey reads the parts of a key of the form "<node>.<language>", in the order of the form; the last result is false for a key of another form.
func ParseToolchainsByNodeLanguageKey(key string) (string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", false
}
rest = after0
if rest == "" {
return "", "", false
}
v2 := rest
rest = ""
if rest != "" {
return "", "", false
}
return v0, v2, true
}
// DecodeToolchainsByNodeLanguageValue reads one value of the state toolchains under the form "<node>.<language>", refusing a field ToolchainsByNodeLanguageValue does not know and any text after the value.
func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageValue, error) {
var v ToolchainsByNodeLanguageValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// AskArtifact is one artifact of a build ask: built from the fetched tree, or copied from an upstream pin.
type AskArtifact struct {
Artifact string `json:"artifact"`
From string `json:"from,omitempty"`
Seat string `json:"seat,omitempty"`
Version string `json:"version,omitempty"`
}
// BuildAnswer is a type of the contract.
type BuildAnswer struct {
Artifacts []BuiltArtifact `json:"artifacts"`
Imports []string `json:"imports,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// BuiltArtifact is one artifact a build made: its digest and the reference it was published under.
type BuiltArtifact struct {
Artifact string `json:"artifact"`
Digest string `json:"digest"`
Reference string `json:"reference,omitempty"`
}
// Capacity is how many steps the node runs at once.
type Capacity struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
BuildHours string `json:"build-hours,omitempty"`
BuildHoursFault string `json:"build-hours-fault,omitempty"`
ByCpu int64 `json:"by-cpu,omitempty"`
Configured int64 `json:"configured"`
ConfiguredFrom string `json:"configured-from,omitempty"`
CoresPerStep float64 `json:"cores-per-step,omitempty"`
Effective int64 `json:"effective"`
Ineligible string `json:"ineligible,omitempty"`
OutsideHours bool `json:"outside-hours,omitempty"`
QuotaCores float64 `json:"quota-cores,omitempty"`
QuotaFrom string `json:"quota-from,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
Why string `json:"why"`
}
// CheckAnswer is a type of the contract.
type CheckAnswer struct {
Fetch FetchAnswer `json:"fetch"`
Levels []LevelAnswer `json:"levels,omitempty"`
}
// ComposeAnswer is a type of the contract.
type ComposeAnswer struct {
Built bool `json:"built"`
Key string `json:"key,omitempty"`
// the compose-candidate verdict
Verdict any `json:"verdict,omitempty"`
}
// FetchBesideItem is a type of the contract.
type FetchBesideItem struct {
Commit string `json:"commit"`
Repository string `json:"repository"`
}
// FetchPackage is the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's.
type FetchPackage struct {
Commit string `json:"commit,omitempty"`
Name string `json:"name,omitempty"`
Ref string `json:"ref,omitempty"`
Repository string `json:"repository,omitempty"`
Version string `json:"version,omitempty"`
}
// Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside.
type Fetch struct {
// the trunk commit a merge preview merges onto
Base string `json:"base,omitempty"`
// the sibling repositories placed beside the fetched tree
Beside []FetchBesideItem `json:"beside,omitempty"`
// the full commit
Commit string `json:"commit"`
// the module's directory in the repository ("." for its root), with paths
Dir string `json:"dir,omitempty"`
// the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's
Package *FetchPackage `json:"package,omitempty"`
// the version (a full commit) of each mesh package to build against, by its module path
Packages map[string]string `json:"packages,omitempty"`
// the paths the module names, relative to dir
Paths []string `json:"paths,omitempty"`
// the pull request's ref, when only its head carries the commit
Ref string `json:"ref,omitempty"`
// <owner>/<name>
Repository string `json:"repository"`
}
// FetchAnswer is what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package.
type FetchAnswer struct {
Base string `json:"base,omitempty"`
Beside []PlacedBeside `json:"beside,omitempty"`
Changed []string `json:"changed,omitempty"`
Commit string `json:"commit"`
Conflicts []string `json:"conflicts,omitempty"`
Group string `json:"group,omitempty"`
Installed []string `json:"installed,omitempty"`
Merge string `json:"merge,omitempty"`
Modules []string `json:"modules"`
Packages []TakenVersion `json:"packages,omitempty"`
Programs []Program `json:"programs,omitempty"`
Refused []FetchRefusal `json:"refused,omitempty"`
Trees map[string]ModuleTree `json:"trees,omitempty"`
Workspace string `json:"workspace"`
}
// FetchRefusal is a TypeScript module whose packages the fetch did not install, with why.
type FetchRefusal struct {
Module string `json:"module"`
Reason string `json:"reason"`
}
// LevelAnswerDigestNeedsItem is a type of the contract.
type LevelAnswerDigestNeedsItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswerDigestPackagesItem is a type of the contract.
type LevelAnswerDigestPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// LevelAnswerDigest is a type of the contract.
type LevelAnswerDigest struct {
Beside []string `json:"beside,omitempty"`
JudgedBy string `json:"judged_by,omitempty"`
Level string `json:"level,omitempty"`
Needs []LevelAnswerDigestNeedsItem `json:"needs,omitempty"`
Packages []LevelAnswerDigestPackagesItem `json:"packages,omitempty"`
WithoutTests []string `json:"without_tests,omitempty"`
}
// LevelAnswerKeptItem is a type of the contract.
type LevelAnswerKeptItem struct {
Digest string `json:"digest,omitempty"`
Package string `json:"package,omitempty"`
Tests int64 `json:"tests,omitempty"`
}
// LevelAnswerUnmetItem is a type of the contract.
type LevelAnswerUnmetItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswer is one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test.
type LevelAnswer struct {
Digest *LevelAnswerDigest `json:"digest,omitempty"`
Judged string `json:"judged,omitempty"`
Kept []LevelAnswerKeptItem `json:"kept,omitempty"`
Level string `json:"level"`
Tests []any `json:"tests,omitempty"`
Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"`
}
// ModuleTree is the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths.
type ModuleTree struct {
Locks map[string]string `json:"locks,omitempty"`
Manifest string `json:"manifest"`
// no longer filled (ADR 0593 section 2); kept so a preview written before still reads
Packages []string `json:"packages,omitempty"`
Paths map[string]string `json:"paths,omitempty"`
Tree string `json:"tree"`
}
// Pause is whether the node takes no new ask.
type Pause struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
By string `json:"by,omitempty"`
Paused bool `json:"paused"`
// the layout version
V int64 `json:"v,omitempty"`
}
// PlacedBeside is a sibling repository placed as a plain directory under the workspace's beside directory.
type PlacedBeside struct {
Commit string `json:"commit"`
Dir string `json:"dir"`
Repository string `json:"repository"`
}
// Program is a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it.
type Program struct {
Commit string `json:"commit,omitempty"`
// ELF, Mach-O or PE
Kind string `json:"kind"`
Path string `json:"path"`
}
// Step is one step a build module runs on a node.
type Step struct {
// when (RFC 3339 time)
Ended *time.Time `json:"ended,omitempty"`
ID string `json:"id"`
Node string `json:"node"`
Owner string `json:"owner,omitempty"`
Phase string `json:"phase,omitempty"`
// when (RFC 3339 time)
PhaseAt *time.Time `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
Result *StepResult `json:"result,omitempty"`
// when (RFC 3339 time)
Started time.Time `json:"started"`
// running, passed, failed or refused
State string `json:"state"`
Tool string `json:"tool"`
// the layout version
V int64 `json:"v,omitempty"`
What string `json:"what,omitempty"`
}
// StepAskBuild is a type of the contract.
type StepAskBuild struct {
Artifacts []AskArtifact `json:"artifacts"`
Fetch *Fetch `json:"fetch,omitempty"`
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
Path string `json:"path,omitempty"`
Publish bool `json:"publish,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// StepAskCheck is a type of the contract.
type StepAskCheck struct {
Fetch Fetch `json:"fetch"`
Kept bool `json:"kept,omitempty"`
Levels []string `json:"levels"`
// the node names the check's tests run against
NodeNames []string `json:"node-names,omitempty"`
Packages []string `json:"packages,omitempty"`
Run string `json:"run,omitempty"`
Runner string `json:"runner,omitempty"`
RunnerWhen string `json:"runner_when,omitempty"`
// the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// StepAskCompose is a type of the contract.
type StepAskCompose struct {
Dir string `json:"dir"`
Fetch Fetch `json:"fetch"`
Package string `json:"package"`
Reader string `json:"reader,omitempty"`
// the compose-candidate request
Request any `json:"request"`
}
// StepAsk is one ask of a build module.
type StepAsk struct {
Asker string `json:"asker"`
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
Build *StepAskBuild `json:"build,omitempty"`
Check *StepAskCheck `json:"check,omitempty"`
Compose *StepAskCompose `json:"compose,omitempty"`
ID string `json:"id"`
// build, check or compose
Kind string `json:"kind"`
// go, typescript or image
Language string `json:"language"`
Needs []string `json:"needs,omitempty"`
// package to the tagged version a build pins
Packages map[string]string `json:"packages,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
}
// StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four.
type StepResult struct {
Artifacts []BuiltArtifact `json:"artifacts,omitempty"`
Built bool `json:"built,omitempty"`
// the holder declined the ask: its node lacks what the ask needs
Declined bool `json:"declined,omitempty"`
Fetch *FetchAnswer `json:"fetch,omitempty"`
Imports []string `json:"imports,omitempty"`
Key string `json:"key,omitempty"`
// what the holder's node lacks of it
Lacks []string `json:"lacks,omitempty"`
Levels []LevelAnswer `json:"levels,omitempty"`
// what the declined ask needs
Needs []string `json:"needs,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
// the compose-candidate verdict (the composer's own layout)
Verdict any `json:"verdict,omitempty"`
}
// TakenVersion is of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said.
type TakenVersion struct {
Asked string `json:"asked"`
Built string `json:"built"`
Dir string `json:"dir"`
Named string `json:"named"`
Package string `json:"package"`
Said string `json:"said,omitempty"`
Sum string `json:"sum,omitempty"`
}
// Toolchain is the toolchain a node reports.
type Toolchain struct {
// when (RFC 3339 time)
At *time.Time `json:"at,omitempty"`
// go or typescript
Language string `json:"language"`
Recipe string `json:"recipe"`
// the layout version
V int64 `json:"v,omitempty"`
Version string `json:"version"`
}
// ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe.
type ToolchainUsed struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
}
// CurrentInput is what current takes.
type CurrentInput struct {
}
// CurrentAnswer is what current answers.
type CurrentAnswer = map[string]any
// PauseInput is what pause takes.
type PauseInput struct {
}
// PauseAnswer is what pause answers.
type PauseAnswer = map[string]any
// ResumeInput is what resume takes.
type ResumeInput struct {
}
// ResumeAnswer is what resume answers.
type ResumeAnswer = map[string]any
// DeliveryAccept is the body of delivery.
type DeliveryAccept = StepAsk
// CheckAccept is the body of check.
type CheckAccept = StepAsk
// RecheckAccept is the body of recheck.
type RecheckAccept = StepAsk
// StepsByNodeIDValue is one value of the state steps under the key <node>.<id>.
type StepsByNodeIDValue = Step
// PausesByPausedNodeValue is one value of the state pauses under the key paused.<node>.
type PausesByPausedNodeValue = Pause
// PausesByCapacityNodeValue is one value of the state pauses under the key capacity.<node>.
type PausesByCapacityNodeValue = Capacity
// ToolchainsByNodeLanguageValue is one value of the state toolchains under the key <node>.<language>.
type ToolchainsByNodeLanguageValue = Toolchain
@@ -0,0 +1,72 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
package nodebuildnode
import (
"encoding/json"
"reflect"
"regexp"
"testing"
)
// TestFixtures reads every fixture of seats/node-build-node/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: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}",
decode: func(b []byte) (any, error) {
var v PausesByPausedNodeValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a node's capacity", subject: "", given: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}", wire: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}",
decode: func(b []byte) (any, error) {
var v PausesByCapacityNodeValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a node's toolchain", subject: "", given: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", wire: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}",
decode: func(b []byte) (any, error) {
var v ToolchainsByNodeLanguageValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a build ask", subject: "mesh.seat.node-build-node.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}",
decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.accept\\.delivery$")},
{name: "a check ask with the group's tagged package", subject: "mesh.seat.node-build-node.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"package\":{\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.15-feat-x.1\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-delivery\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"typescript\",\"recipe\":\"1\",\"version\":\"26.12.0\"}},\"id\":\"c3d5\",\"kind\":\"check\",\"language\":\"typescript\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"package\":{\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.15-feat-x.1\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-delivery\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"typescript\",\"recipe\":\"1\",\"version\":\"26.12.0\"}},\"id\":\"c3d5\",\"kind\":\"check\",\"language\":\"typescript\",\"v\":1}",
decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.accept\\.check$")},
{name: "a finished check step of a TypeScript merge preview", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; 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)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildnode
go 1.26.0
@@ -0,0 +1,262 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
// Package npmpackageregistry is the contract of the seat "npm-package-registry", defined whole (seats/npm-package-registry/seat.json): The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus.
package npmpackageregistry
// Seat is the seat's name.
const Seat = "npm-package-registry"
const (
VerbPublish = "publish"
VerbPublishPart = "publish-part"
VerbVersions = "versions"
VerbRemove = "remove"
)
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.
func Verbs() []string {
return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove}
}
// Events is every event published on the seat's subject: a new copy each call.
func Events() []string {
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.
const (
Family = "npm-package-registry.events"
FamilyMaxAgeDays = 7
)
// SubjectPublished is where published is published.
const SubjectPublished = "mesh.seat.npm-package-registry.event.published"
// SubjectRefused is where refused is published.
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
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// what it is
Kind string `json:"kind"`
// 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"`
// the tag it carries, for a tagged version
Tag string `json:"tag,omitempty"`
// the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone
Version string `json:"version,omitempty"`
}
// The values of Artifact's kind.
const (
ArtifactKindPackage = "package"
ArtifactKindImage = "image"
ArtifactKindBundle = "bundle"
ArtifactKindArchive = "archive"
ArtifactKindUpstream = "upstream"
)
// Outcome is the body of published, refused and removed.
type Outcome struct {
// the source commit it was built from
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// the module holding the seat that said it
Holder string `json:"holder"`
// what it is
Kind string `json:"kind"`
// the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact
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, 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: <registry>/<module>/<artifact>@sha256:<digest>, or <name>@<version> 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
Version string `json:"version,omitempty"`
}
// The values of Outcome's kind.
const (
OutcomeKindPackage = "package"
OutcomeKindImage = "image"
OutcomeKindBundle = "bundle"
OutcomeKindArchive = "archive"
OutcomeKindUpstream = "upstream"
)
// Part is one part of a transfer.
type Part struct {
// the part's digest, sha256:<hex>: an image's layer or config, a package's file, a bundle's blob
Digest string `json:"digest"`
// its size in bytes
Size int64 `json:"size"`
}
// PublishInput is what publish takes.
type PublishInput struct {
// the source commit it was built from
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// what it is
Kind string `json:"kind"`
// 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"`
// every part's digest and size; none when the digest is already held
Parts []Part `json:"parts,omitempty"`
// the tag it carries, for a tagged version
Tag string `json:"tag,omitempty"`
// the transfer's name, the caller's own; the same name opens the same transfer again
Transfer string `json:"transfer"`
// the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone
Version string `json:"version,omitempty"`
}
// The values of PublishInput's kind.
const (
PublishInputKindPackage = "package"
PublishInputKindImage = "image"
PublishInputKindBundle = "bundle"
PublishInputKindArchive = "archive"
PublishInputKindUpstream = "upstream"
)
// PublishAnswer is what publish answers.
type PublishAnswer struct {
// the digest is held already: nothing to send; the outcome follows as an event
Held bool `json:"held,omitempty"`
// the digests of the parts the holder lacks, to send by publish-part
Lacks []string `json:"lacks,omitempty"`
// the transfer
Transfer string `json:"transfer"`
}
// PublishPartInput is what publish-part takes.
type PublishPartInput struct {
// the chunk's bytes, base64 (at most 760 KiB before encoding)
Data string `json:"data"`
// this chunk is the part's last
Last bool `json:"last"`
// the part's digest
Part string `json:"part"`
// the chunk's place in its part, from 0
Sequence int64 `json:"sequence"`
// the transfer
Transfer string `json:"transfer"`
}
// PublishPartAnswer is what publish-part answers.
type PublishPartAnswer struct {
// the part is whole and its bytes match its digest
Complete bool `json:"complete,omitempty"`
// the part
Part string `json:"part"`
// the chunks of the part held so far
Received int64 `json:"received,omitempty"`
// the transfer
Transfer string `json:"transfer"`
}
// VersionsInput is what versions takes.
type VersionsInput struct {
// narrowed to one kind (optional)
Kind string `json:"kind,omitempty"`
// the name
Name string `json:"name"`
}
// VersionsAnswer is what versions answers.
type VersionsAnswer struct {
// the name
Name string `json:"name"`
// newest first
Versions []Artifact `json:"versions"`
}
// RemoveInput is what remove takes.
type RemoveInput struct {
// the digest (for a bundle, archive or copied base)
Digest string `json:"digest,omitempty"`
// the name
Name string `json:"name"`
// why it goes
Reason string `json:"reason"`
// the tag (for a tagged version)
Tag string `json:"tag,omitempty"`
// the version
Version string `json:"version,omitempty"`
}
// The values of RemoveInput's reason.
const (
RemoveInputReasonTagGone = "tag-gone"
RemoveInputReasonRetention = "retention"
)
// 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
// RefusedEvent is the body of the event refused.
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
URL string `json:"url"`
}
@@ -0,0 +1,66 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
package npmpackageregistry
import (
"encoding/json"
"reflect"
"regexp"
"testing"
)
// TestFixtures reads every fixture of seats/npm-package-registry/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: "published on the seat's subject", subject: "mesh.seat.npm-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"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\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}",
decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.published$")},
{name: "refused on the seat's subject", subject: "mesh.seat.npm-package-registry.event.refused", 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 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\"}",
decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}",
decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}",
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))
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)
}
}
}
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry
go 1.26.0
@@ -0,0 +1,486 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
// Package ociartifactregistry is the contract of the seat "oci-artifact-registry", defined whole (seats/oci-artifact-registry/seat.json): The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side.
package ociartifactregistry
import (
"bytes"
"encoding/json"
"errors"
"strings"
)
// Seat is the seat's name.
const Seat = "oci-artifact-registry"
const (
VerbPublish = "publish"
VerbPublishPart = "publish-part"
VerbVersions = "versions"
VerbRemove = "remove"
VerbHold = "hold"
VerbRelease = "release"
VerbList = "list"
)
const (
EventPublished = "published"
EventRefused = "refused"
EventRemoved = "removed"
EventKept = "kept"
EventHeld = "held"
EventReleased = "released"
)
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
func Verbs() []string {
return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbHold, VerbRelease, VerbList}
}
// Events is every event published on the seat's subject: a new copy each call.
func Events() []string {
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.
const (
Family = "oci-artifact-registry.events"
FamilyMaxAgeDays = 7
)
// SubjectPublished is where published is published.
const SubjectPublished = "mesh.seat.oci-artifact-registry.event.published"
// SubjectRefused is where refused is published.
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"
// SubjectReleased is where released is published.
const SubjectReleased = "mesh.seat.oci-artifact-registry.event.released"
const (
StateHolds = "holds"
StateWindows = "windows"
)
// HoldsBySeatNameDigestKey is the key of one value of the state holds under the form "<seat>.<name>.<digest>".
func HoldsBySeatNameDigestKey(seat, name, digest string) string {
return seat + "." + name + "." + digest
}
// ParseHoldsBySeatNameDigestKey reads the parts of a key of the form "<seat>.<name>.<digest>", in the order of the form; the last result is false for a key of another form.
func ParseHoldsBySeatNameDigestKey(key string) (string, string, string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".")
if !found0 || v0 == "" {
return "", "", "", false
}
rest = after0
v2, after2, found2 := strings.Cut(rest, ".")
if !found2 || v2 == "" {
return "", "", "", false
}
rest = after2
if rest == "" {
return "", "", "", false
}
v4 := rest
rest = ""
if rest != "" {
return "", "", "", false
}
return v0, v2, v4, true
}
// DecodeHoldsBySeatNameDigestValue reads one value of the state holds under the form "<seat>.<name>.<digest>", refusing a field HoldsBySeatNameDigestValue does not know and any text after the value.
func DecodeHoldsBySeatNameDigestValue(raw []byte) (HoldsBySeatNameDigestValue, error) {
var v HoldsBySeatNameDigestValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// WindowsByCallerKey is the key of one value of the state windows under the form "<caller>".
func WindowsByCallerKey(caller string) string { return caller }
// ParseWindowsByCallerKey reads the parts of a key of the form "<caller>", in the order of the form; the last result is false for a key of another form.
func ParseWindowsByCallerKey(key string) (string, bool) {
rest := key
if rest == "" {
return "", false
}
v0 := rest
rest = ""
if rest != "" {
return "", false
}
return v0, true
}
// DecodeWindowsByCallerValue reads one value of the state windows under the form "<caller>", refusing a field WindowsByCallerValue does not know and any text after the value.
func DecodeWindowsByCallerValue(raw []byte) (WindowsByCallerValue, error) {
var v WindowsByCallerValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// Artifact is one published thing, as every registry event and answer names it.
type Artifact struct {
// the source commit it was built from
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// what it is
Kind string `json:"kind"`
// 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"`
// the tag it carries, for a tagged version
Tag string `json:"tag,omitempty"`
// the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone
Version string `json:"version,omitempty"`
}
// The values of Artifact's kind.
const (
ArtifactKindPackage = "package"
ArtifactKindImage = "image"
ArtifactKindBundle = "bundle"
ArtifactKindArchive = "archive"
ArtifactKindUpstream = "upstream"
)
// Hold is the body of held and released.
type Hold struct {
// when, RFC 3339
At string `json:"at,omitempty"`
// the caller the holder proved
By string `json:"by,omitempty"`
// the digest
Digest string `json:"digest"`
// the module holding the seat
Holder string `json:"holder"`
// the name
Name string `json:"name"`
// why
Reason string `json:"reason,omitempty"`
}
// Listed is a type of the contract.
type Listed struct {
// for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by
Artifact string `json:"artifact,omitempty"`
// the manifest's digest, sha256:<hex>
Digest string `json:"digest"`
// a hold keeps it from retention
Held bool `json:"held"`
// it is an index, naming a manifest per platform
Index bool `json:"index"`
// for an index, the digests of the manifests it names
Platforms []string `json:"platforms,omitempty"`
// its bytes: its own content and every blob it names
Size int64 `json:"size"`
// the tags naming it, none for an untagged one
Tags []string `json:"tags,omitempty"`
}
// Outcome is the body of published, refused and removed.
type Outcome struct {
// the source commit it was built from
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// the module holding the seat that said it
Holder string `json:"holder"`
// what it is
Kind string `json:"kind"`
// the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact
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, 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: <registry>/<module>/<artifact>@sha256:<digest>, or <name>@<version> 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
Version string `json:"version,omitempty"`
}
// The values of Outcome's kind.
const (
OutcomeKindPackage = "package"
OutcomeKindImage = "image"
OutcomeKindBundle = "bundle"
OutcomeKindArchive = "archive"
OutcomeKindUpstream = "upstream"
)
// Part is one part of a transfer.
type Part struct {
// the part's digest, sha256:<hex>: an image's layer or config, a package's file, a bundle's blob
Digest string `json:"digest"`
// its size in bytes
Size int64 `json:"size"`
}
// Repository is a type of the contract.
type Repository struct {
// each manifest it holds, by digest
Manifests []Listed `json:"manifests"`
// the repository
Name string `json:"name"`
}
// PublishInput is what publish takes.
type PublishInput struct {
// the source commit it was built from
Commit string `json:"commit,omitempty"`
// its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)
Digest string `json:"digest"`
// the build's fingerprint
Fingerprint string `json:"fingerprint,omitempty"`
// what it is
Kind string `json:"kind"`
// 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"`
// every part's digest and size; none when the digest is already held
Parts []Part `json:"parts,omitempty"`
// the tag it carries, for a tagged version
Tag string `json:"tag,omitempty"`
// the transfer's name, the caller's own; the same name opens the same transfer again
Transfer string `json:"transfer"`
// the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone
Version string `json:"version,omitempty"`
}
// The values of PublishInput's kind.
const (
PublishInputKindPackage = "package"
PublishInputKindImage = "image"
PublishInputKindBundle = "bundle"
PublishInputKindArchive = "archive"
PublishInputKindUpstream = "upstream"
)
// PublishAnswer is what publish answers.
type PublishAnswer struct {
// the digest is held already: nothing to send; the outcome follows as an event
Held bool `json:"held,omitempty"`
// the digests of the parts the holder lacks, to send by publish-part
Lacks []string `json:"lacks,omitempty"`
// the transfer
Transfer string `json:"transfer"`
}
// PublishPartInput is what publish-part takes.
type PublishPartInput struct {
// the chunk's bytes, base64 (at most 760 KiB before encoding)
Data string `json:"data"`
// this chunk is the part's last
Last bool `json:"last"`
// the part's digest
Part string `json:"part"`
// the chunk's place in its part, from 0
Sequence int64 `json:"sequence"`
// the transfer
Transfer string `json:"transfer"`
}
// PublishPartAnswer is what publish-part answers.
type PublishPartAnswer struct {
// the part is whole and its bytes match its digest
Complete bool `json:"complete,omitempty"`
// the part
Part string `json:"part"`
// the chunks of the part held so far
Received int64 `json:"received,omitempty"`
// the transfer
Transfer string `json:"transfer"`
}
// VersionsInput is what versions takes.
type VersionsInput struct {
// narrowed to one kind (optional)
Kind string `json:"kind,omitempty"`
// the name
Name string `json:"name"`
}
// VersionsAnswer is what versions answers.
type VersionsAnswer struct {
// the name
Name string `json:"name"`
// newest first
Versions []Artifact `json:"versions"`
}
// RemoveInput is what remove takes.
type RemoveInput struct {
// the digest (for a bundle, archive or copied base)
Digest string `json:"digest,omitempty"`
// the name
Name string `json:"name"`
// why it goes
Reason string `json:"reason"`
// the tag (for a tagged version)
Tag string `json:"tag,omitempty"`
// the version
Version string `json:"version,omitempty"`
}
// The values of RemoveInput's reason.
const (
RemoveInputReasonTagGone = "tag-gone"
RemoveInputReasonRetention = "retention"
)
// 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
Digest string `json:"digest"`
// the name
Name string `json:"name"`
// which of the sweep's rules keeps it
Reason string `json:"reason"`
}
// HoldAnswer is what hold answers.
type HoldAnswer struct {
// it is held
Held bool `json:"held"`
// when the hold began, RFC 3339
Since string `json:"since,omitempty"`
}
// ReleaseInput is what release takes.
type ReleaseInput struct {
// the digest
Digest string `json:"digest"`
// the name
Name string `json:"name"`
// why it is let go
Reason string `json:"reason"`
}
// ReleaseAnswer is what release answers.
type ReleaseAnswer struct {
// a hold was let go; false when there was none
Released bool `json:"released"`
}
// ListInput is what list takes.
type ListInput struct {
// the repository, <module>/<artifact>; every repository of the seat when not given
Name string `json:"name,omitempty"`
}
// ListAnswer is what list answers.
type ListAnswer struct {
// each repository named, in order of its name
Repositories []Repository `json:"repositories"`
}
// PublishedEvent is the body of the event published.
type PublishedEvent = Outcome
// RefusedEvent is the body of the event refused.
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
// ReleasedEvent is the body of the event released.
type ReleasedEvent = Hold
// HoldsBySeatNameDigestValue is one value of the state holds under the key <seat>.<name>.<digest>.
type HoldsBySeatNameDigestValue struct {
By string `json:"by"`
Digest string `json:"digest"`
Kind string `json:"kind"`
Name string `json:"name"`
Seat string `json:"seat"`
// when (RFC 3339 time)
Since string `json:"since"`
Why string `json:"why,omitempty"`
}
// WindowsByCallerValueDroppedItem is a type of the contract.
type WindowsByCallerValueDroppedItem struct {
// when (RFC 3339 time)
At string `json:"at,omitempty"`
By string `json:"by,omitempty"`
Node string `json:"node"`
Why string `json:"why,omitempty"`
}
// WindowsByCallerValue is one value of the state windows under the key <caller>.
type WindowsByCallerValue struct {
// when it last closed (RFC 3339 time)
Closed string `json:"closed,omitempty"`
// retired nodes dropped by hand
Dropped []WindowsByCallerValueDroppedItem `json:"dropped,omitempty"`
// the nodes holding it open
Holding []string `json:"holding,omitempty"`
LastHolding []string `json:"last_holding,omitempty"`
// the caller whose window it is
Name string `json:"name"`
Open bool `json:"open,omitempty"`
// node to when it began to hold
Since map[string]string `json:"since,omitempty"`
// of each node holding the window, why it holds it
Why map[string]string `json:"why,omitempty"`
}
@@ -0,0 +1,82 @@
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
package ociartifactregistry
import (
"encoding/json"
"reflect"
"regexp"
"testing"
)
// TestFixtures reads every fixture of seats/oci-artifact-registry/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: "published on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"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\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}",
decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.published$")},
{name: "refused on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.refused", 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 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\"}",
decode: func(b []byte) (any, error) { var v ReleasedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.released$")},
{name: "a hold", subject: "", given: "{\"by\":\"operator@shanks\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"kind\":\"base\",\"name\":\"base\",\"seat\":\"oci-artifact-registry\",\"since\":\"2026-11-05T10:00:00Z\"}", wire: "{\"by\":\"operator@shanks\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"kind\":\"base\",\"name\":\"base\",\"seat\":\"oci-artifact-registry\",\"since\":\"2026-11-05T10:00:00Z\"}",
decode: func(b []byte) (any, error) {
var v HoldsBySeatNameDigestValue
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "an open window", subject: "", given: "{\"holding\":[\"ace\"],\"name\":\"mesh-delivery-builder\",\"open\":true,\"since\":{\"ace\":\"2026-11-05T10:00:00Z\"}}", wire: "{\"holding\":[\"ace\"],\"name\":\"mesh-delivery-builder\",\"open\":true,\"since\":{\"ace\":\"2026-11-05T10:00:00Z\"}}",
decode: func(b []byte) (any, error) { var v WindowsByCallerValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"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\":\"bundle\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"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: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}",
decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}",
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\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"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 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},
{name: "list answers an untagged archive by the blob it keeps", subject: "", given: "{\"repositories\":[{\"manifests\":[{\"artifact\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":true,\"index\":false,\"size\":4096}],\"name\":\"mail/files\"}]}", wire: "{\"repositories\":[{\"manifests\":[{\"artifact\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":true,\"index\":false,\"size\":4096}],\"name\":\"mail/files\"}]}",
decode: func(b []byte) (any, error) { var v ListAnswer; 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)
}
}
}
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry
go 1.26.0
+292
View File
@@ -0,0 +1,292 @@
{
"module": "mesh-delivery-contracts",
"version": "1",
"build": {
"artifacts": [
{
"name": "go-containerimageregistry",
"kind": "package",
"language": "go",
"paths": [
"go/seats/containerimageregistry"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "go-gopackageregistry",
"kind": "package",
"language": "go",
"paths": [
"go/seats/gopackageregistry"
],
"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",
"language": "go",
"paths": [
"go/seats/meshcatalog"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "go-meshdelivery",
"kind": "package",
"language": "go",
"paths": [
"go/seats/meshdelivery"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "go-meshdeliverychecker",
"kind": "package",
"language": "go",
"paths": [
"go/seats/meshdeliverychecker"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "go-nodebuildgo",
"kind": "package",
"language": "go",
"paths": [
"go/seats/nodebuildgo"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "go-nodebuildimage",
"kind": "package",
"language": "go",
"paths": [
"go/seats/nodebuildimage"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "go-nodebuildnode",
"kind": "package",
"language": "go",
"paths": [
"go/seats/nodebuildnode"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "go-npmpackageregistry",
"kind": "package",
"language": "go",
"paths": [
"go/seats/npmpackageregistry"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "go-ociartifactregistry",
"kind": "package",
"language": "go",
"paths": [
"go/seats/ociartifactregistry"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "go-containerimageregistryclient",
"kind": "package",
"language": "go",
"paths": [
"go/clients/containerimageregistryclient"
],
"series": "0.1"
},
{
"name": "go-meshcatalogclient",
"kind": "package",
"language": "go",
"paths": [
"go/clients/meshcatalogclient"
],
"series": "0.1"
},
{
"name": "go-meshdeliveryclient",
"kind": "package",
"language": "go",
"paths": [
"go/clients/meshdeliveryclient"
],
"series": "0.1"
},
{
"name": "go-meshdeliverycheckerclient",
"kind": "package",
"language": "go",
"paths": [
"go/clients/meshdeliverycheckerclient"
],
"series": "0.1"
},
{
"name": "go-gopackageregistryclient",
"kind": "package",
"language": "go",
"paths": [
"go/clients/gopackageregistryclient"
],
"series": "0.1"
},
{
"name": "go-npmpackageregistryclient",
"kind": "package",
"language": "go",
"paths": [
"go/clients/npmpackageregistryclient"
],
"series": "0.1"
},
{
"name": "go-ociartifactregistryclient",
"kind": "package",
"language": "go",
"paths": [
"go/clients/ociartifactregistryclient"
],
"series": "0.1"
},
{
"name": "ts-containerimageregistry",
"kind": "package",
"language": "typescript",
"paths": [
"ts/seats/containerimageregistry"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "ts-gopackageregistry",
"kind": "package",
"language": "typescript",
"paths": [
"ts/seats/gopackageregistry"
],
"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",
"language": "typescript",
"paths": [
"ts/seats/meshcatalog"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "ts-meshdelivery",
"kind": "package",
"language": "typescript",
"paths": [
"ts/seats/meshdelivery"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "ts-meshdeliverychecker",
"kind": "package",
"language": "typescript",
"paths": [
"ts/seats/meshdeliverychecker"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "ts-nodebuildgo",
"kind": "package",
"language": "typescript",
"paths": [
"ts/seats/nodebuildgo"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "ts-nodebuildimage",
"kind": "package",
"language": "typescript",
"paths": [
"ts/seats/nodebuildimage"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "ts-nodebuildnode",
"kind": "package",
"language": "typescript",
"paths": [
"ts/seats/nodebuildnode"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "ts-npmpackageregistry",
"kind": "package",
"language": "typescript",
"paths": [
"ts/seats/npmpackageregistry"
],
"series": "0.1",
"generated-from": "."
},
{
"name": "ts-ociartifactregistry",
"kind": "package",
"language": "typescript",
"paths": [
"ts/seats/ociartifactregistry"
],
"series": "0.1",
"generated-from": "."
}
]
},
"resources": []
}
+948
View File
@@ -0,0 +1,948 @@
{
"format": 1,
"part": "build-step",
"description": "What the three build seats share: the verbs that read and steer a node's builds, and the layouts of the asks they accept and the steps they keep. A seat lists its states against these (a part carries no states).",
"verbs": [
{
"name": "current",
"description": "The asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.",
"input": {
"type": "object",
"properties": {}
},
"answer": {
"type": "object",
"description": "the steps running, the capacity and whether paused"
}
},
{
"name": "pause",
"description": "Take no new ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module. A capacity of 0 is the lasting way to keep a node from building.",
"input": {
"type": "object",
"properties": {}
},
"answer": {
"type": "object",
"description": "whether the node is paused"
}
},
{
"name": "resume",
"description": "Take asks again on this node.",
"input": {
"type": "object",
"properties": {}
},
"answer": {
"type": "object",
"description": "whether the node is paused"
}
}
],
"defs": {
"StepAsk": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"id": {
"type": "string"
},
"kind": {
"type": "string",
"description": "build, check or compose"
},
"language": {
"type": "string",
"description": "go, typescript or image"
},
"asker": {
"type": "string"
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)",
"format": "date-time"
},
"build": {
"type": "object",
"properties": {
"module": {
"type": "string"
},
"path": {
"type": "string"
},
"fp": {
"type": "string"
},
"fetch": {
"$ref": "#/defs/Fetch"
},
"artifacts": {
"type": "array",
"items": {
"$ref": "#/defs/AskArtifact"
}
},
"publish": {
"type": "boolean"
},
"toolchain": {
"$ref": "#/defs/ToolchainUsed"
}
},
"required": [
"module",
"artifacts"
]
},
"check": {
"type": "object",
"properties": {
"fetch": {
"$ref": "#/defs/Fetch"
},
"levels": {
"type": "array",
"items": {
"type": "string",
"description": "unit or integration"
}
},
"kept": {
"type": "boolean"
},
"runner": {
"type": "string"
},
"runner_when": {
"type": "string"
},
"run": {
"type": "string"
},
"packages": {
"type": "array",
"items": {
"type": "string"
}
},
"toolchain": {
"$ref": "#/defs/ToolchainUsed",
"description": "the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check"
},
"node-names": {
"type": "array",
"items": {
"type": "string"
},
"description": "the node names the check's tests run against"
}
},
"required": [
"fetch",
"levels"
]
},
"compose": {
"type": "object",
"properties": {
"fetch": {
"$ref": "#/defs/Fetch"
},
"dir": {
"type": "string"
},
"package": {
"type": "string"
},
"request": {
"description": "the compose-candidate request"
},
"reader": {
"type": "string"
}
},
"required": [
"fetch",
"dir",
"package",
"request"
]
},
"needs": {
"type": "array",
"items": {
"type": "string"
}
},
"packages": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "package to the tagged version a build pins"
}
},
"required": [
"id",
"kind",
"language",
"asker"
],
"description": "one ask of a build module"
},
"BuildAnswer": {
"type": "object",
"properties": {
"artifacts": {
"type": "array",
"items": {
"$ref": "#/defs/BuiltArtifact"
}
},
"toolchain": {
"$ref": "#/defs/ToolchainUsed"
},
"imports": {
"type": "array",
"items": {
"type": "string"
}
},
"out-of-step": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"artifacts"
]
},
"CheckAnswer": {
"type": "object",
"properties": {
"fetch": {
"$ref": "#/defs/FetchAnswer"
},
"levels": {
"type": "array",
"items": {
"$ref": "#/defs/LevelAnswer"
}
}
},
"required": [
"fetch"
]
},
"ComposeAnswer": {
"type": "object",
"properties": {
"built": {
"type": "boolean"
},
"key": {
"type": "string"
},
"verdict": {
"description": "the compose-candidate verdict"
}
},
"required": [
"built"
]
},
"Step": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"id": {
"type": "string"
},
"tool": {
"type": "string"
},
"node": {
"type": "string"
},
"state": {
"type": "string",
"description": "running, passed, failed or refused"
},
"started": {
"type": "string",
"description": "when (RFC 3339 time)",
"format": "date-time"
},
"ended": {
"type": "string",
"description": "when (RFC 3339 time)",
"format": "date-time"
},
"reason": {
"type": "string"
},
"result": {
"$ref": "#/defs/StepResult"
},
"what": {
"type": "string"
},
"phase": {
"type": "string"
},
"phase-at": {
"type": "string",
"description": "when (RFC 3339 time)",
"format": "date-time"
},
"owner": {
"type": "string"
}
},
"required": [
"id",
"tool",
"node",
"state",
"started"
],
"description": "one step a build module runs on a node"
},
"Toolchain": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"language": {
"type": "string",
"description": "go or typescript"
},
"version": {
"type": "string"
},
"recipe": {
"type": "string"
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
"language",
"version",
"recipe"
],
"description": "the toolchain a node reports"
},
"Pause": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"paused": {
"type": "boolean"
},
"by": {
"type": "string"
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
"paused"
],
"description": "whether the node takes no new ask"
},
"Capacity": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"effective": {
"type": "integer"
},
"configured": {
"type": "integer"
},
"configured-from": {
"type": "string"
},
"quota-cores": {
"type": "number"
},
"quota-from": {
"type": "string"
},
"cores-per-step": {
"type": "number"
},
"by-cpu": {
"type": "integer"
},
"build-hours": {
"type": "string"
},
"outside-hours": {
"type": "boolean"
},
"build-hours-fault": {
"type": "string"
},
"ineligible": {
"type": "string"
},
"why": {
"type": "string"
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
"effective",
"configured",
"why"
],
"description": "how many steps the node runs at once"
},
"Fetch": {
"type": "object",
"description": "what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside",
"properties": {
"repository": {
"type": "string",
"description": "<owner>/<name>"
},
"commit": {
"type": "string",
"description": "the full commit"
},
"ref": {
"type": "string",
"description": "the pull request's ref, when only its head carries the commit"
},
"base": {
"type": "string",
"description": "the trunk commit a merge preview merges onto"
},
"dir": {
"type": "string",
"description": "the module's directory in the repository (\".\" for its root), with paths"
},
"paths": {
"type": "array",
"items": {
"type": "string"
},
"description": "the paths the module names, relative to dir"
},
"packages": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "the version (a full commit) of each mesh package to build against, by its module path"
},
"package": {
"type": "object",
"description": "the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's",
"properties": {
"name": { "type": "string" },
"version": { "type": "string" },
"repository": { "type": "string" },
"commit": { "type": "string" },
"ref": { "type": "string" }
}
},
"beside": {
"type": "array",
"items": {
"type": "object",
"properties": {
"repository": {
"type": "string"
},
"commit": {
"type": "string"
}
},
"required": [
"repository",
"commit"
]
},
"description": "the sibling repositories placed beside the fetched tree"
}
},
"required": [
"repository",
"commit"
]
},
"ToolchainUsed": {
"type": "object",
"properties": {
"language": {
"type": "string"
},
"version": {
"type": "string"
},
"recipe": {
"type": "string"
}
},
"required": [
"language",
"version",
"recipe"
],
"description": "the toolchain a build was made with: the language, the installed version and the build module's recipe"
},
"AskArtifact": {
"type": "object",
"properties": {
"artifact": {
"type": "string"
},
"from": {
"type": "string"
},
"seat": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"artifact"
],
"description": "one artifact of a build ask: built from the fetched tree, or copied from an upstream pin"
},
"BuiltArtifact": {
"type": "object",
"properties": {
"artifact": {
"type": "string"
},
"digest": {
"type": "string"
},
"reference": {
"type": "string"
}
},
"required": [
"artifact",
"digest"
],
"description": "one artifact a build made: its digest and the reference it was published under"
},
"LevelAnswer": {
"type": "object",
"properties": {
"level": {
"type": "string"
},
"judged": {
"type": "string"
},
"digest": {
"type": "object",
"properties": {
"level": {
"type": "string"
},
"packages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"digest": {
"type": "string"
}
},
"required": [
"package",
"digest"
]
}
},
"beside": {
"type": "array",
"items": {
"type": "string"
}
},
"without_tests": {
"type": "array",
"items": {
"type": "string"
}
},
"judged_by": {
"type": "string"
},
"needs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"needs": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
},
"kept": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"digest": {
"type": "string"
},
"tests": {
"type": "integer"
}
}
}
},
"tests": {
"type": "array",
"items": {
"description": "one test's result"
}
},
"unmet": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"needs": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"required": [
"level"
],
"description": "one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test"
},
"StepResult": {
"type": "object",
"description": "what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four",
"properties": {
"artifacts": {
"type": "array",
"items": {
"$ref": "#/defs/BuiltArtifact"
}
},
"toolchain": {
"$ref": "#/defs/ToolchainUsed"
},
"imports": {
"type": "array",
"items": {
"type": "string"
}
},
"out-of-step": {
"type": "array",
"items": {
"type": "string"
}
},
"fetch": {
"$ref": "#/defs/FetchAnswer"
},
"levels": {
"type": "array",
"items": {
"$ref": "#/defs/LevelAnswer"
}
},
"built": {
"type": "boolean"
},
"key": {
"type": "string"
},
"verdict": {
"description": "the compose-candidate verdict (the composer's own layout)"
},
"declined": {
"type": "boolean",
"description": "the holder declined the ask: its node lacks what the ask needs"
},
"needs": {
"type": "array",
"items": {
"type": "string"
},
"description": "what the declined ask needs"
},
"lacks": {
"type": "array",
"items": {
"type": "string"
},
"description": "what the holder's node lacks of it"
}
}
},
"ModuleTree": {
"type": "object",
"description": "the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths",
"properties": {
"tree": {
"type": "string"
},
"manifest": {
"type": "string"
},
"locks": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"paths": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"packages": {
"type": "array",
"items": {
"type": "string"
},
"description": "no longer filled (ADR 0593 section 2); kept so a preview written before still reads"
}
},
"required": [
"tree",
"manifest"
]
},
"Program": {
"type": "object",
"description": "a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it",
"properties": {
"path": {
"type": "string"
},
"kind": {
"type": "string",
"description": "ELF, Mach-O or PE"
},
"commit": {
"type": "string"
}
},
"required": [
"path",
"kind"
]
},
"PlacedBeside": {
"type": "object",
"description": "a sibling repository placed as a plain directory under the workspace's beside directory",
"properties": {
"repository": {
"type": "string"
},
"commit": {
"type": "string"
},
"dir": {
"type": "string"
}
},
"required": [
"repository",
"commit",
"dir"
]
},
"TakenVersion": {
"type": "object",
"description": "of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said",
"properties": {
"dir": {
"type": "string"
},
"package": {
"type": "string"
},
"asked": {
"type": "string"
},
"named": {
"type": "string"
},
"built": {
"type": "string"
},
"sum": {
"type": "string"
},
"said": {
"type": "string"
}
},
"required": [
"dir",
"package",
"asked",
"named",
"built"
]
},
"FetchRefusal": {
"type": "object",
"description": "a TypeScript module whose packages the fetch did not install, with why",
"properties": {
"module": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"required": [
"module",
"reason"
]
},
"FetchAnswer": {
"type": "object",
"description": "what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package",
"properties": {
"workspace": {
"type": "string"
},
"commit": {
"type": "string"
},
"modules": {
"type": "array",
"items": {
"type": "string"
}
},
"base": {
"type": "string"
},
"merge": {
"type": "string"
},
"conflicts": {
"type": "array",
"items": {
"type": "string"
}
},
"changed": {
"type": "array",
"items": {
"type": "string"
}
},
"programs": {
"type": "array",
"items": {
"$ref": "#/defs/Program"
}
},
"trees": {
"type": "object",
"additionalProperties": {
"$ref": "#/defs/ModuleTree"
}
},
"beside": {
"type": "array",
"items": {
"$ref": "#/defs/PlacedBeside"
}
},
"packages": {
"type": "array",
"items": {
"$ref": "#/defs/TakenVersion"
}
},
"installed": {
"type": "array",
"items": {
"type": "string"
}
},
"refused": {
"type": "array",
"items": {
"$ref": "#/defs/FetchRefusal"
}
},
"group": {
"type": "string"
}
},
"required": [
"workspace",
"commit",
"modules"
]
}
}
}
+436
View File
@@ -0,0 +1,436 @@
{
"format": 1,
"part": "registry",
"description": "The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved.",
"verbs": [
{
"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).",
"callers": "the build modules",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"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)"
},
"version": {
"type": "string",
"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone"
},
"kind": {
"type": "string",
"enum": [
"package",
"image",
"bundle",
"archive",
"upstream"
],
"description": "what it is"
},
"tag": {
"type": "string",
"description": "the tag it carries, for a tagged version"
},
"digest": {
"type": "string",
"description": "its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)"
},
"fingerprint": {
"type": "string",
"description": "the build's fingerprint"
},
"commit": {
"type": "string",
"description": "the source commit it was built from"
},
"transfer": {
"type": "string",
"description": "the transfer's name, the caller's own; the same name opens the same transfer again"
},
"parts": {
"type": "array",
"items": {
"$ref": "#/defs/Part"
},
"description": "every part's digest and size; none when the digest is already held"
}
},
"required": [
"transfer",
"name",
"kind",
"digest"
]
},
"answer": {
"type": "object",
"properties": {
"transfer": {
"type": "string",
"description": "the transfer"
},
"lacks": {
"type": "array",
"items": {
"type": "string"
},
"description": "the digests of the parts the holder lacks, to send by publish-part"
},
"held": {
"type": "boolean",
"description": "the digest is held already: nothing to send; the outcome follows as an event"
}
},
"required": [
"transfer"
]
},
"optional": true
},
{
"name": "publish-part",
"description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.",
"callers": "the build modules, and only the proved caller that opened the transfer",
"input": {
"type": "object",
"properties": {
"transfer": {
"type": "string",
"description": "the transfer"
},
"part": {
"type": "string",
"description": "the part's digest"
},
"sequence": {
"type": "integer",
"description": "the chunk's place in its part, from 0"
},
"last": {
"type": "boolean",
"description": "this chunk is the part's last"
},
"data": {
"type": "string",
"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)"
}
},
"required": [
"transfer",
"part",
"sequence",
"last",
"data"
]
},
"answer": {
"type": "object",
"properties": {
"transfer": {
"type": "string",
"description": "the transfer"
},
"part": {
"type": "string",
"description": "the part"
},
"received": {
"type": "integer",
"description": "the chunks of the part held so far"
},
"complete": {
"type": "boolean",
"description": "the part is whole and its bytes match its digest"
}
},
"required": [
"transfer",
"part"
]
},
"optional": true
},
{
"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.",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"kind": {
"type": "string",
"description": "narrowed to one kind (optional)"
}
},
"required": [
"name"
]
},
"answer": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"versions": {
"type": "array",
"items": {
"$ref": "#/defs/Artifact"
},
"description": "newest first"
}
},
"required": [
"name",
"versions"
]
},
"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.",
"callers": "the builder, for a tagged version; the artifact sweep's holder, for retention",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"version": {
"type": "string",
"description": "the version"
},
"tag": {
"type": "string",
"description": "the tag (for a tagged version)"
},
"digest": {
"type": "string",
"description": "the digest (for a bundle, archive or copied base)"
},
"reason": {
"type": "string",
"enum": [
"tag-gone",
"retention"
],
"description": "why it goes"
}
},
"required": [
"name",
"reason"
]
},
"answer": {
"type": "object",
"properties": {
"removed": {
"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"
}
},
"required": [
"removed"
]
},
"optional": true
}
],
"events": [
{
"name": "published",
"body": {
"$ref": "#/defs/Outcome"
}
},
{
"name": "refused",
"body": {
"$ref": "#/defs/Outcome"
}
},
{
"name": "removed",
"body": {
"$ref": "#/defs/Outcome"
}
},
{
"name": "kept",
"body": {
"$ref": "#/defs/Outcome"
}
}
],
"defs": {
"Artifact": {
"type": "object",
"properties": {
"name": {
"type": "string",
"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)"
},
"version": {
"type": "string",
"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone"
},
"kind": {
"type": "string",
"enum": [
"package",
"image",
"bundle",
"archive",
"upstream"
],
"description": "what it is"
},
"tag": {
"type": "string",
"description": "the tag it carries, for a tagged version"
},
"digest": {
"type": "string",
"description": "its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)"
},
"fingerprint": {
"type": "string",
"description": "the build's fingerprint"
},
"commit": {
"type": "string",
"description": "the source commit it was built from"
}
},
"required": [
"name",
"kind",
"digest"
],
"description": "one published thing, as every registry event and answer names it"
},
"Part": {
"type": "object",
"properties": {
"digest": {
"type": "string",
"description": "the part's digest, sha256:<hex>: an image's layer or config, a package's file, a bundle's blob"
},
"size": {
"type": "integer",
"description": "its size in bytes"
}
},
"required": [
"digest",
"size"
],
"description": "one part of a transfer"
},
"Outcome": {
"type": "object",
"properties": {
"name": {
"type": "string",
"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)"
},
"module": {
"type": "string",
"description": "the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact"
},
"version": {
"type": "string",
"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone"
},
"kind": {
"type": "string",
"enum": [
"package",
"image",
"bundle",
"archive",
"upstream"
],
"description": "what it is"
},
"tag": {
"type": "string",
"description": "the tag it carries, for a tagged version"
},
"digest": {
"type": "string",
"description": "its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)"
},
"fingerprint": {
"type": "string",
"description": "the build's fingerprint"
},
"commit": {
"type": "string",
"description": "the source commit it was built from"
},
"holder": {
"type": "string",
"description": "the module holding the seat that said it"
},
"transfer": {
"type": "string",
"description": "the transfer it ends, when it ended one"
},
"reference": {
"type": "string",
"description": "the reference the seat answers: <registry>/<module>/<artifact>@sha256:<digest>, or <name>@<version> for a package (ADR 0605 §9)"
},
"reason": {
"type": "string",
"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": [
"name",
"kind",
"digest",
"holder"
],
"description": "the body of published, refused and removed"
}
}
}
@@ -0,0 +1,98 @@
[
{
"name": "published on the seat's subject",
"of": "event:published",
"subject": "mesh.seat.container-image-registry.event.published",
"given": {
"name": "mesh-sdk",
"version": "0.1.19",
"kind": "image",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"holder": "distribution",
"transfer": "t1",
"reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
},
{
"name": "refused on the seat's subject",
"of": "event:refused",
"subject": "mesh.seat.container-image-registry.event.refused",
"given": {
"name": "mesh-sdk",
"version": "0.1.19",
"kind": "image",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"holder": "distribution",
"transfer": "t1",
"reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"reason": "retention"
}
},
{
"name": "removed on the seat's subject",
"of": "event:removed",
"subject": "mesh.seat.container-image-registry.event.removed",
"given": {
"name": "mesh-sdk",
"version": "0.1.19",
"kind": "image",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"holder": "distribution",
"transfer": "t1",
"reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"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",
"subject": "mesh.seat.container-image-registry.event.held",
"given": {
"name": "mesh-sdk",
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"reason": "kept by the sweep's rule",
"holder": "distribution",
"by": "mesh-artifact-sweep",
"at": "2026-11-04T12:00:00Z"
}
},
{
"name": "released on the seat's subject",
"of": "event:released",
"subject": "mesh.seat.container-image-registry.event.released",
"given": {
"name": "mesh-sdk",
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"reason": "kept by the sweep's rule",
"holder": "distribution",
"by": "mesh-artifact-sweep",
"at": "2026-11-04T12:00:00Z"
}
}
]
@@ -0,0 +1,28 @@
[
{
"name": "a hold",
"of": "state:holds/<seat>.<name>.<digest>",
"given": {
"seat": "container-image-registry",
"name": "base",
"kind": "base",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"by": "operator@shanks",
"since": "2026-11-05T10:00:00Z"
}
},
{
"name": "an open window",
"of": "state:windows/<caller>",
"given": {
"name": "mesh-delivery-builder",
"open": true,
"holding": [
"ace"
],
"since": {
"ace": "2026-11-05T10:00:00Z"
}
}
}
]
@@ -0,0 +1,149 @@
[
{
"name": "publish opens a transfer",
"of": "verb:publish.input",
"given": {
"name": "mesh-sdk",
"version": "0.1.19",
"kind": "image",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"transfer": "t1",
"parts": [
{
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"size": 1048576
}
]
}
},
{
"name": "a field the reader does not know is dropped",
"of": "verb:publish.input",
"why": "ADR 0608 §6: a reader ignores a field it does not know",
"given": {
"name": "mesh-sdk",
"version": "0.1.19",
"kind": "image",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"transfer": "t1",
"parts": [
{
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"size": 1048576
}
],
"later": {
"x": 1
}
},
"wire": {
"name": "mesh-sdk",
"version": "0.1.19",
"kind": "image",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"transfer": "t1",
"parts": [
{
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"size": 1048576
}
]
}
},
{
"name": "publish answers what it lacks",
"of": "verb:publish.answer",
"given": {
"transfer": "t1",
"lacks": [
"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
]
}
},
{
"name": "a chunk of a part",
"of": "verb:publish-part.input",
"given": {
"transfer": "t1",
"part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"sequence": 0,
"last": true,
"data": "aGVsbG8="
}
},
{
"name": "the versions held",
"of": "verb:versions.answer",
"given": {
"name": "mesh-sdk",
"versions": [
{
"name": "mesh-sdk",
"version": "0.1.19",
"kind": "package",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc"
}
]
}
},
{
"name": "a release of what is not held answers released false",
"of": "verb:release.answer",
"why": "A3t: the outcome asked holds already, so it is no error",
"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"
]
}
},
{
"name": "list answers an index with its platforms, its tag and its hold",
"of": "verb:list.answer",
"given": {
"repositories": [
{
"name": "mail/server",
"manifests": [
{
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"index": true,
"platforms": [
"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
],
"tags": [
"3"
],
"size": 2048,
"held": false
},
{
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"index": false,
"size": 1024,
"held": false
}
]
}
]
}
}
]
@@ -0,0 +1,480 @@
{
"format": 1,
"seat": "container-image-registry",
"scope": "mesh",
"grain": "whole",
"domain": "Delivery",
"decision": "novox/hq ADR 0605",
"description": "The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image.",
"includes": [
"registry"
],
"verbs": [
{
"name": "mirror",
"description": "Copy an outside image into the store by its pinned digest; emits published or refused.",
"callers": "mesh-build-image's holder of the image build seat",
"input": {
"type": "object",
"properties": {
"source": {
"type": "string",
"description": "the outside image's reference"
},
"digest": {
"type": "string",
"description": "its pinned digest, sha256:<hex>"
},
"name": {
"type": "string",
"description": "the name it is kept under"
}
},
"required": [
"source",
"digest",
"name"
]
},
"answer": {
"type": "object",
"properties": {
"reference": {
"type": "string",
"description": "the reference the seat answers"
},
"digest": {
"type": "string",
"description": "the digest stored"
}
},
"required": [
"reference",
"digest"
]
},
"optional": true
},
{
"name": "tags",
"description": "The tags of one image and the digest each names.",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the image"
}
},
"required": [
"name"
]
},
"answer": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the image"
},
"tags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"tag": {
"type": "string",
"description": "the tag"
},
"digest": {
"type": "string",
"description": "its digest"
}
},
"required": [
"tag",
"digest"
]
},
"description": "every tag"
}
},
"required": [
"name",
"tags"
]
},
"optional": true
},
{
"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.",
"callers": "the module that runs the artifact sweep alone",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"digest": {
"type": "string",
"description": "the digest"
},
"reason": {
"type": "string",
"description": "which of the sweep's rules keeps it"
}
},
"required": [
"name",
"digest",
"reason"
]
},
"answer": {
"type": "object",
"properties": {
"held": {
"type": "boolean",
"description": "it is held"
},
"since": {
"type": "string",
"description": "when the hold began, RFC 3339"
}
},
"required": [
"held"
]
},
"optional": true
},
{
"name": "release",
"description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.",
"callers": "the module that runs the artifact sweep; the operator's session through the holder's tool",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"digest": {
"type": "string",
"description": "the digest"
},
"reason": {
"type": "string",
"description": "why it is let go"
}
},
"required": [
"name",
"digest",
"reason"
]
},
"answer": {
"type": "object",
"properties": {
"released": {
"type": "boolean",
"description": "a hold was let go; false when there was none"
}
},
"required": [
"released"
]
},
"optional": true
},
{
"name": "list",
"description": "What the store holds of one repository, or of every repository the seat holds when none is named: every image manifest and index, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the repository, <module>/<artifact>; every repository of the seat when not given"
}
}
},
"answer": {
"type": "object",
"properties": {
"repositories": {
"type": "array",
"items": {
"$ref": "#/defs/Repository"
},
"description": "each repository named, in order of its name"
}
},
"required": [
"repositories"
]
},
"optional": true
}
],
"events": [
{
"name": "held",
"body": {
"$ref": "#/defs/Hold"
}
},
{
"name": "released",
"body": {
"$ref": "#/defs/Hold"
}
}
],
"family": {
"name": "container-image-registry.events",
"max-age-days": 7
},
"defs": {
"Hold": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"digest": {
"type": "string",
"description": "the digest"
},
"reason": {
"type": "string",
"description": "why"
},
"holder": {
"type": "string",
"description": "the module holding the seat"
},
"by": {
"type": "string",
"description": "the caller the holder proved"
},
"at": {
"type": "string",
"description": "when, RFC 3339"
}
},
"required": [
"name",
"digest",
"holder"
],
"description": "the body of held and released"
},
"Repository": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the repository"
},
"manifests": {
"type": "array",
"items": {
"$ref": "#/defs/Listed"
},
"description": "each manifest it holds, by digest"
}
},
"required": [
"name",
"manifests"
]
},
"Listed": {
"type": "object",
"properties": {
"digest": {
"type": "string",
"description": "the manifest's digest, sha256:<hex>"
},
"index": {
"type": "boolean",
"description": "it is an index, naming a manifest per platform"
},
"platforms": {
"type": "array",
"items": {
"type": "string"
},
"description": "for an index, the digests of the manifests it names"
},
"artifact": {
"type": "string",
"description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "the tags naming it, none for an untagged one"
},
"size": {
"type": "integer",
"description": "its bytes: its own content and every blob it names"
},
"held": {
"type": "boolean",
"description": "a hold keeps it from retention"
}
},
"required": [
"digest",
"index",
"size",
"held"
]
}
},
"states": [
{
"name": "holds",
"template": "state",
"history": 1,
"description": "the bundles, archives and copied bases kept from retention's removal",
"key-forms": [
{
"key": "<seat>.<name>.<digest>",
"value": {
"type": "object",
"description": "one hold: an untagged bundle, archive or copied base kept from retention's removal",
"properties": {
"seat": {
"type": "string"
},
"name": {
"type": "string"
},
"kind": {
"type": "string"
},
"digest": {
"type": "string"
},
"by": {
"type": "string"
},
"why": {
"type": "string"
},
"since": {
"type": "string",
"description": "when (RFC 3339 time)"
}
},
"required": [
"seat",
"name",
"kind",
"digest",
"by",
"since"
]
},
"description": "one hold"
}
]
},
{
"name": "windows",
"template": "state",
"history": 1,
"description": "the direct-write windows, one per caller",
"key-forms": [
{
"key": "<caller>",
"value": {
"type": "object",
"description": "the direct-write window of one caller: open while a node holds it open",
"properties": {
"name": {
"type": "string",
"description": "the caller whose window it is"
},
"open": {
"type": "boolean"
},
"holding": {
"type": "array",
"items": {
"type": "string"
},
"description": "the nodes holding it open"
},
"since": {
"type": "object",
"additionalProperties": {
"type": "string",
"description": "when (RFC 3339 time)"
},
"description": "node to when it began to hold"
},
"closed": {
"type": "string",
"description": "when it last closed (RFC 3339 time)"
},
"last_holding": {
"type": "array",
"items": {
"type": "string"
}
},
"dropped": {
"type": "array",
"items": {
"type": "object",
"properties": {
"node": {
"type": "string"
},
"by": {
"type": "string"
},
"why": {
"type": "string"
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
}
},
"required": [
"node"
]
},
"description": "retired nodes dropped by hand"
},
"why": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "of each node holding the window, why it holds it"
}
},
"required": [
"name"
]
},
"description": "one caller's window"
}
]
}
]
}
+9
View File
@@ -0,0 +1,9 @@
{
"format": 1,
"seat": "git",
"scope": "mesh",
"grain": "seat-list",
"domain": "Delivery",
"decision": "novox/hq ADR 0111",
"delivers": "git"
}
@@ -0,0 +1,73 @@
[
{
"name": "published on the seat's subject",
"of": "event:published",
"subject": "mesh.seat.go-package-registry.event.published",
"given": {
"name": "git.novox.be/novox/mesh-sdk/go",
"module": "git.novox.be/novox/mesh-sdk/go",
"version": "0.1.19",
"kind": "package",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"holder": "gitea",
"transfer": "t1",
"reference": "git.novox.be/novox/mesh-sdk/go@0.1.19"
}
},
{
"name": "refused on the seat's subject",
"of": "event:refused",
"subject": "mesh.seat.go-package-registry.event.refused",
"given": {
"name": "git.novox.be/novox/mesh-sdk/go",
"version": "0.1.19",
"kind": "package",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"holder": "gitea",
"transfer": "t1",
"reference": "git.novox.be/novox/mesh-sdk/go@0.1.19",
"reason": "retention"
}
},
{
"name": "removed on the seat's subject",
"of": "event:removed",
"subject": "mesh.seat.go-package-registry.event.removed",
"given": {
"name": "git.novox.be/novox/mesh-sdk/go",
"version": "0.1.19",
"kind": "package",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"holder": "gitea",
"transfer": "t1",
"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"
]
}
}
]
@@ -0,0 +1,110 @@
[
{
"name": "publish opens a transfer",
"of": "verb:publish.input",
"given": {
"name": "git.novox.be/novox/mesh-sdk/go",
"version": "0.1.19",
"kind": "package",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"transfer": "t1",
"parts": [
{
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"size": 1048576
}
]
}
},
{
"name": "a field the reader does not know is dropped",
"of": "verb:publish.input",
"why": "ADR 0608 §6: a reader ignores a field it does not know",
"given": {
"name": "git.novox.be/novox/mesh-sdk/go",
"version": "0.1.19",
"kind": "package",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"transfer": "t1",
"parts": [
{
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"size": 1048576
}
],
"later": {
"x": 1
}
},
"wire": {
"name": "git.novox.be/novox/mesh-sdk/go",
"version": "0.1.19",
"kind": "package",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"transfer": "t1",
"parts": [
{
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"size": 1048576
}
]
}
},
{
"name": "publish answers what it lacks",
"of": "verb:publish.answer",
"given": {
"transfer": "t1",
"lacks": [
"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
]
}
},
{
"name": "a chunk of a part",
"of": "verb:publish-part.input",
"given": {
"transfer": "t1",
"part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"sequence": 0,
"last": true,
"data": "aGVsbG8="
}
},
{
"name": "the versions held",
"of": "verb:versions.answer",
"given": {
"name": "git.novox.be/novox/mesh-sdk/go",
"versions": [
{
"name": "git.novox.be/novox/mesh-sdk/go",
"version": "0.1.19",
"kind": "package",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "f1",
"commit": "cccccccccccccccccccccccccccccccccccccccc"
}
]
}
},
{
"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"
]
}
}
]
@@ -0,0 +1,35 @@
{
"format": 1,
"seat": "go-package-registry",
"scope": "mesh",
"grain": "whole",
"domain": "Delivery",
"decision": "novox/hq ADR 0605",
"description": "The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus.",
"delivers": "go-package-registry",
"includes": [
"registry"
],
"family": {
"name": "go-package-registry.events",
"max-age-days": 7
},
"provisions": [
{
"name": "go-package-registry",
"form": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "the read side's address"
}
},
"required": [
"url"
],
"description": "form none: the Go module proxy's read side, answered anonymously, with no credential (design 51)"
}
}
]
}
@@ -0,0 +1,8 @@
{
"format": 1,
"seat": "mesh-artifact-store",
"scope": "mesh",
"grain": "seat-list",
"domain": "Delivery",
"decision": "novox/hq ADR 0075"
}
@@ -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"
}
}
]
@@ -0,0 +1 @@
[]
@@ -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": []}
}
]
@@ -0,0 +1,204 @@
{
"format": 1,
"seat": "mesh-artifact-sweep",
"scope": "mesh",
"grain": "whole",
"domain": "Delivery",
"decision": "novox/hq ADR 0571",
"verbs": [
{
"name": "artifacts",
"description": "Every artifact the mesh made that lives in its artifact store, each with its repository, digest, kind (image or archive) and state: kept, and why (a want names it, or one of the five newest builds of its module in the builder's log), eligible (made, kept for no reason, not yet let go), or unrecorded (in the store and named by nothing the mesh made: never removed). Reads only.",
"input": {
"properties": {
"collected": {
"description": "\"true\": list the collected references too (counted always)",
"enum": [
"true",
"false"
],
"type": "string"
},
"repository": {
"description": "only this repository's references, as \u003cmodule\u003e/\u003cartifact\u003e (the counts stay whole)",
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "what artifacts answers, as its description says"
}
},
{
"name": "held",
"description": "Whether the artifact store holds each reference given, asked with one HEAD each: held, gone (the store answered 404) or unknown with why. The builder asks it before it reuses a done key. Reads only.",
"input": {
"properties": {
"references": {
"description": "the artifacts' references, artifact-store://\u003cmodule\u003e/\u003cartifact\u003e/blobs/sha256:\u003chex\u003e: a list, or one text separated by commas",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"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"
}
},
{
"name": "collect",
"description": "The artifact store's sweep, now: what is eligible is let go of. Without confirm a dry run: what would be let go, nothing held or deleted. With confirm and why: every kept archive held first, then each eligible artifact let go of, oldest first, an index with the platform manifests no kept index names, each deletion recorded in the hand-act log. Bounded by most (500 by default, at most 5000) and 45 seconds; what is left is said. Never touches a digest the mesh did not make.",
"input": {
"properties": {
"confirm": {
"description": "\"true\": let go of what is eligible (needs why); without it nothing is done",
"enum": [
"true",
"false"
],
"type": "string"
},
"most": {
"description": "let go of at most this many (500 by default, at most 5000)",
"type": "string"
},
"why": {
"description": "with confirm: why, required and recorded",
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "what collect answers, as its description says"
}
},
{
"name": "mirrors",
"description": "The bases the mesh copied into its artifact store, each kept (a kept build stood on it), eligible (only builds no longer kept stood on it) or unrecorded (no build the builder's log holds stood on it: never removed), by what the builder's log says each build stood on. Records nothing by hand.",
"input": {
"properties": {},
"type": "object"
},
"answer": {
"type": "object",
"description": "what mirrors answers, as its description says"
}
},
{
"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": {
"properties": {
"node": {
"description": "the machine's name",
"type": "string"
}
},
"required": [
"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",
"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"
}
}
}
@@ -0,0 +1,223 @@
[
{
"name": "a manifest (xclip, read through mesh-catalog.module)",
"of": "state:manifests/<digest>",
"given": {
"v": 1,
"module": "xclip",
"manifest": "{\"build\":{\"artifacts\":[{\"binary\":\"xclip-tools\",\"from\":\"cmd/xclip-tools\",\"kind\":\"bundle\",\"language\":\"go\",\"loads\":[\"xclip-tools\"],\"name\":\"tools\",\"system\":\"arch\"}]},\"capabilities\":[\"package-manager\"],\"module\":\"xclip\",\"resources\":[{\"id\":\"package\",\"package\":\"xclip\",\"type\":\"package\"}],\"tools\":[\"xclip_copy\",\"xclip_paste\",\"xclip_targets\",\"xclip_session\"],\"version\":\"1\"}"
}
},
{
"name": "a module (xclip, read through mesh-catalog.modules; the inputs and toolchains no tool reads are not here)",
"of": "state:modules/<module>",
"given": {
"v": 1,
"repository": "novox/mesh-catalog",
"commit": "f469eb387a3015569d9b16c66ed418e9dd098f64",
"path": "modules/xclip",
"fp2": "src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56"
}
},
{
"name": "a candidate",
"of": "state:candidates/<group>",
"given": {
"v": 1,
"group": "feat/registry",
"base": 3,
"heads": "0123456789ab",
"pulls": [
{
"repository": "novox/mesh-sdk",
"number": 1,
"head": "cccccccccccccccccccccccccccccccccccccccc",
"modules": [
"nats"
]
}
]
}
},
{
"name": "a refused verdict (nats, read through the indexer's verdicts tool)",
"of": "state:verdicts/<digest>",
"given": {
"module": "nats",
"state": "refused",
"problems": [
"this is not a module manifest: a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl}: json: unknown field \"read-only\" (read without state[2].read-only)"
],
"judge": "exe:661e6fb424d7ffea",
"at": "2026-11-04T18:51:12.614583047+01:00"
}
},
{
"name": "the predefined list",
"of": "state:predefined/list",
"given": {
"v": 1,
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"list": {
"streams": []
}
}
},
{
"name": "a pull request's configuration",
"of": "state:config-candidates/<pull>",
"given": {
"v": 1,
"pull": "novox/mesh-catalog#1",
"head": "cccccccccccccccccccccccccccccccccccccccc",
"declares": {
"nats": {
"limit": {
"kind": "setting",
"default": 1,
"why": "a bound"
}
}
}
}
},
{
"name": "a pull request's configuration state whose bounds are declared 0 (a zero is a bound, not none)",
"of": "state:config-candidates/<pull>",
"given": {
"v": 1,
"pull": "novox/mesh-catalog#1",
"head": "cccccccccccccccccccccccccccccccccccccccc",
"declares": {
"nats": {
"limit": {
"kind": "setting",
"default": 0,
"why": "a bound",
"minimum": 0,
"maximum": 0
}
}
}
}
},
{
"name": "a published Go module",
"of": "state:packages/<module>",
"given": {
"v": 1,
"module": "git.novox.be/novox/mesh-sdk/go",
"repository": "novox/mesh-sdk",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"packages": [
{
"package": "git.novox.be/novox/mesh-sdk/go/keys",
"digest": "ab"
}
]
}
},
{
"name": "a package series",
"of": "state:packages/<package>.<series>",
"given": {
"v": 1,
"name": "go-keys",
"series": "0.1"
}
},
{
"name": "a module with its toolchain",
"of": "state:modules/<module>",
"given": {
"v": 1,
"repository": "novox/mesh-catalog",
"commit": "f469eb387a3015569d9b16c66ed418e9dd098f64",
"fp2": "src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56",
"toolchains": {
"go": {
"language": "go",
"version": "1.26.0",
"recipe": "1"
}
}
}
},
{
"name": "the modules verb (captured: the first three modules)",
"of": "verb:modules.answer",
"given": {
"modules": [
{
"commit": "f469eb387a3015569d9b16c66ed418e9dd098f64",
"fp": "src2:5e4a1b4ad4ea6175c05e4655ce58aeb957547149d4c9805eb27636efd50c1c87",
"module": "adwaita",
"path": "modules/adwaita",
"repository": "novox/mesh-catalog"
},
{
"commit": "b112adcecc41d18f54190cbd2fc864199b70c518",
"fp": "src2:7dd0db8e98224f46121a5ae05c0b9c5a430eff531a5120aa40b1a59237853a3f",
"module": "amqp-email-forwarder",
"path": ".",
"repository": "jschoubben/amqp-email-forwarder"
},
{
"commit": "dd458cb155f5f5fa3ed824a23728e444be5b156a",
"fp": "src2:a98f8152f0a837dd5ba883a15efaf110ba88100193157e4bdb1a93ba55209371",
"module": "artifact-store-tools",
"path": "modules/artifact-store-tools",
"repository": "novox/mesh-catalog"
}
]
}
},
{
"name": "the module verb (captured: xclip)",
"of": "verb:module.answer",
"given": {
"module": {
"commit": "f469eb387a3015569d9b16c66ed418e9dd098f64",
"fp": "src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56",
"manifest": {
"build": {
"artifacts": [
{
"binary": "xclip-tools",
"from": "cmd/xclip-tools",
"kind": "bundle",
"language": "go",
"loads": [
"xclip-tools"
],
"name": "tools",
"system": "arch"
}
]
},
"capabilities": [
"package-manager"
],
"module": "xclip",
"resources": [
{
"id": "package",
"package": "xclip",
"type": "package"
}
],
"tools": [
"xclip_copy",
"xclip_paste",
"xclip_targets",
"xclip_session"
],
"version": "1"
},
"module": "xclip",
"path": "modules/xclip",
"repository": "novox/mesh-catalog"
}
}
}
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
[
{
"name": "verdict on the checker's own family",
"of": "holder-event:verdict",
"subject": "mesh.stream.mesh-delivery-checker.event.verdict",
"given": {
"repository": "novox/mesh-contracts",
"pull": 3,
"head": "cccccccccccccccccccccccccccccccccccccccc",
"verdict": "fail",
"out-of-step": [
"go/seats/meshbroker/contract.go"
],
"series": [
{
"module": "mesh-contracts",
"artifact": "go",
"from": "0.1",
"to": "0.2"
}
]
}
},
{
"name": "tag-wanted on the checker's own family",
"of": "holder-event:tag-wanted",
"subject": "mesh.stream.mesh-delivery-checker.event.tag-wanted",
"given": {
"repository": "novox/mesh-sdk",
"pull": 170,
"head": "cccccccccccccccccccccccccccccccccccccccc",
"branch": "feat/registry",
"module": "mesh-sdk",
"package": "go",
"tag": "registry-1966"
}
},
{
"name": "preview.written on the checker's stream",
"of": "holder-event:preview.written",
"subject": "mesh.stream.mesh-delivery-checker.event.preview.written",
"given": {}
}
]
@@ -0,0 +1,48 @@
[
{
"name": "a running verdict",
"of": "state:verdicts/<group>.<heads>",
"given": {
"v": 1,
"group": "feat/registry",
"heads": "0123456789ab",
"state": "running"
}
},
{
"name": "a kept pass",
"of": "state:kept/<level>.<digest>",
"given": {
"v": 1,
"state": "pass",
"repository": "novox/mesh-sdk",
"package": "go/keys",
"level": "unit",
"tests": 4,
"took": 120,
"toolchain": "go1.26",
"at": "2026-11-05T10:00:00Z"
}
},
{
"name": "the status owed for a head",
"of": "state:owed/<repository>.<number>.<head>.<act>",
"given": {
"v": 1,
"act": "status=mesh/check",
"context": "mesh/check",
"state": "success"
}
},
{
"name": "a preview",
"of": "state:previews/<repository>.<head>.<trunk>",
"given": {
"v": 1,
"repository": "novox/mesh-sdk",
"head": "cccccccccccccccccccccccccccccccccccccccc",
"trunk": "dddddddddddddddddddddddddddddddddddddddd",
"merge": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
}
}
]
@@ -0,0 +1,657 @@
{
"format": 1,
"seat": "mesh-delivery-checker",
"scope": "mesh",
"grain": "whole",
"domain": "Delivery",
"decision": "novox/hq ADR 0547",
"verbs": [
{
"name": "try",
"description": "Try pushed heads before their pull requests are opened: the checker judges them as it judges a delivery group's pull requests, each member merged onto its trunk, its unit and integration levels through the build modules, then the members' manifests, the module graph and the composition on every placed node, and owes the forge nothing. Name the group's members (each a repository and the exact commit), or one repository and its head for a try of one member. Answers the try's id at once; try-read reads where it stands. The same members at the same commits, in any order, are the answer to a second ask while that try waits or runs; at most 4 run on a build node, and 8 wait for one, mesh-wide.",
"optional": true,
"replaces": [
"sh merge-check.sh",
"docker run postgres"
],
"input": {
"properties": {
"head": {
"description": "the full commit to try, 40 or 64 hex digits, for a try of one member (instead of members)",
"type": "string"
},
"level": {
"description": "unit or integration: only this level; both when absent",
"type": "string"
},
"repository": {
"description": "<owner>/<name>, for a try of one member (instead of members)",
"type": "string"
},
"tests": {
"description": "a go test -run pattern: only those tests run, and nothing is kept",
"type": "string"
},
"trunk": {
"description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent",
"type": "string"
},
"members": {
"description": "the group: one repository at most once, each at the exact commit asked and the trunk commit it is merged onto; instead of repository and head",
"type": "array",
"items": {
"type": "object",
"properties": {
"repository": {
"description": "<owner>/<name>",
"type": "string"
},
"head": {
"description": "the full commit to try, 40 or 64 hex digits",
"type": "string"
},
"trunk": {
"description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent",
"type": "string"
}
},
"required": [
"repository",
"head"
]
}
}
},
"type": "object"
},
"answer": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "the try's id"
},
"state": {
"type": "string",
"description": "its state"
}
},
"required": [
"id"
]
}
},
{
"name": "try-read",
"description": "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.",
"optional": true,
"input": {
"properties": {
"id": {
"description": "the try's id, try-<digits>",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "the try: its state, its steps and its verdict when it has one"
}
},
{
"name": "try-end",
"description": "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.",
"optional": true,
"input": {
"properties": {
"id": {
"description": "the try's id, try-<digits>",
"type": "string"
},
"why": {
"description": "why it is ended, in words",
"type": "string"
}
},
"required": [
"id",
"why"
],
"type": "object"
},
"answer": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "the try"
},
"ended": {
"type": "boolean",
"description": "it is ended"
}
},
"required": [
"id",
"ended"
]
}
}
],
"description": "The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2).",
"holder-events": [
{
"name": "verdict",
"subject": "mesh.stream.<holder>.event.verdict",
"family": "checker.asks",
"body": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"description": "owner/repo"
},
"pull": {
"type": "integer",
"description": "the pull request's number, for a pull request's check"
},
"head": {
"type": "string",
"description": "the commit checked"
},
"verdict": {
"type": "string",
"description": "pass or fail"
},
"out-of-step": {
"type": "array",
"items": {
"type": "string"
},
"description": "the generated files out of step with their seat definitions, each path (design 26)"
},
"series": {
"type": "array",
"items": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module whose artifact's series changes"
},
"artifact": {
"type": "string",
"description": "its artifact, or the package's ecosystem name as the module's packages name it"
},
"from": {
"type": "string",
"description": "the series on trunk"
},
"to": {
"type": "string",
"description": "the series the checked head gives it"
}
},
"required": [
"module",
"artifact",
"from",
"to"
]
},
"description": "each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none"
}
},
"required": [
"repository",
"head",
"verdict"
]
}
},
{
"name": "tag-wanted",
"subject": "mesh.stream.<holder>.event.tag-wanted",
"family": "checker.asks",
"body": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"description": "owner/repo"
},
"pull": {
"type": "integer",
"description": "the pull request"
},
"head": {
"type": "string",
"description": "its head"
},
"branch": {
"type": "string",
"description": "the pull request's branch: its tag is removed when the branch ends"
},
"module": {
"type": "string",
"description": "the group member"
},
"package": {
"type": "string",
"description": "the package it declares"
},
"tag": {
"type": "string",
"description": "the tag wanted"
}
},
"required": [
"repository",
"head",
"branch",
"module",
"package"
]
}
},
{
"name": "preview.written",
"subject": "mesh.stream.<holder>.event.preview.written",
"family": "checks"
}
],
"states": [
{
"name": "verdicts",
"template": "state",
"key-forms": [
{
"key": "<group>.<heads>",
"value": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"group": {
"type": "string"
},
"heads": {
"type": "string",
"description": "12 hex digits"
},
"trunk": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "repository to the trunk head merged onto"
},
"state": {
"type": "string",
"description": "running, pass or fail"
},
"failed": {
"type": "object",
"properties": {
"test": {
"type": "string"
},
"machine": {
"type": "string"
},
"words": {
"type": "string"
}
}
},
"failures": {
"type": "array",
"items": {
"type": "object",
"properties": {
"test": {
"type": "string"
},
"machine": {
"type": "string"
},
"words": {
"type": "string"
}
}
}
},
"levels": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"run": {
"type": "integer"
},
"kept": {
"type": "integer"
},
"skipped": {
"type": "integer"
}
},
"required": [
"run",
"kept"
]
},
"description": "by level, unit or integration"
},
"took": {
"type": "object",
"properties": {
"queue": {
"type": "integer"
},
"clone": {
"type": "integer"
},
"unit": {
"type": "integer"
},
"lint": {
"type": "integer"
},
"integration-setup": {
"type": "integer"
},
"integration-run": {
"type": "integer"
},
"composition": {
"type": "integer"
}
},
"description": "milliseconds"
},
"machines": {
"type": "array",
"items": {
"type": "string"
}
},
"sdk": {
"type": "string"
},
"composition": {
"type": "object",
"properties": {
"state": {
"type": "string",
"description": "not-run, pass or fail"
},
"words": {
"type": "string"
},
"sdk": {
"type": "string"
},
"machines": {
"type": "array",
"items": {
"type": "string"
}
},
"fpset": {
"type": "string"
}
},
"required": [
"state"
]
},
"over-bound": {
"type": "array",
"items": {
"type": "string"
}
},
"beside": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"carried-from": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"series": {
"type": "array",
"items": {
"type": "object",
"properties": {
"module": {
"type": "string"
},
"artifact": {
"type": "string"
},
"from": {
"type": "string"
},
"to": {
"type": "string"
}
},
"required": [
"module",
"artifact",
"from",
"to"
]
}
}
},
"required": [
"group",
"heads",
"state"
],
"description": "one check of a group at one set of heads"
},
"description": "one check of a group; the group is an escaped branch name, heads 12 hex digits"
}
],
"description": "the checker's verdict of each group at each set of heads",
"history": 5
},
{
"name": "kept",
"template": "state",
"key-forms": [
{
"key": "<level>.<digest>",
"value": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"state": {
"type": "string",
"description": "pass: a failure is never kept"
},
"repository": {
"type": "string"
},
"package": {
"type": "string"
},
"level": {
"type": "string"
},
"tests": {
"type": "integer"
},
"took": {
"type": "integer"
},
"toolchain": {
"type": "string"
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
}
},
"required": [
"state",
"repository",
"package",
"level"
],
"description": "one passing level of one package, kept so the next check does not run it again"
},
"description": "a passing level of one package by its content digest"
}
],
"description": "the kept passing levels",
"history": 1
},
{
"name": "owed",
"template": "state",
"key-forms": [
{
"key": "<repository>.<number>.<head>.<act>",
"value": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"act": {
"type": "string",
"description": "status=<context>, view, note or comment.<seq>"
},
"context": {
"type": "string"
},
"target": {
"type": "string"
},
"state": {
"type": "string"
},
"text": {
"type": "string"
},
"ref": {
"type": "string"
}
},
"required": [
"act"
],
"description": "one act owed to the forge, written by its owner and done by the forge bridge"
},
"description": "the status mesh/check the checker owes the forge for a head (its act is status=mesh/check)"
}
],
"description": "the status the checker owes the forge",
"history": 5
},
{
"name": "previews",
"template": "state",
"key-forms": [
{
"key": "<repository>.<head>.<trunk>",
"value": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"repository": {
"type": "string"
},
"head": {
"type": "string"
},
"trunk": {
"type": "string"
},
"merge": {
"type": "string",
"description": "the merge commit"
},
"conflicts": {
"type": "array",
"items": {
"type": "string"
},
"description": "files in conflict"
},
"modules": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"tree": {
"type": "string"
},
"manifest": {
"type": "string",
"description": "manifest digest"
},
"locks": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"paths": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"packages": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"tree",
"manifest"
]
},
"description": "by directory"
}
},
"required": [
"repository",
"head",
"trunk"
],
"description": "the merge of a head onto a trunk commit, read before a pull request is checked"
},
"description": "the merge of a head onto a trunk commit"
}
],
"description": "the previews of a head merged onto a trunk",
"history": 5
}
]
}
@@ -0,0 +1,72 @@
[
{
"name": "the source revision",
"of": "state:source/main",
"given": {
"v": 1,
"group": "feat/registry",
"heads": {
"novox/mesh-sdk": "cccccccccccccccccccccccccccccccccccccccc"
}
}
},
{
"name": "a merge",
"of": "state:merges/<group>",
"given": {
"v": 1,
"group": "feat/registry",
"ask": "merge.feat=registry.0123456789ab",
"pulls": [
{
"repository": "novox/mesh-sdk",
"number": 1,
"head": "cccccccccccccccccccccccccccccccccccccccc",
"state": "merging"
}
]
}
},
{
"name": "an ask",
"of": "state:asks/merge.<group>.<heads>",
"given": {
"v": 1,
"group": "feat/registry",
"heads": [
{
"repository": "novox/mesh-sdk",
"number": 1,
"head": "cccccccccccccccccccccccccccccccccccccccc"
}
],
"ask": {
"headline": "merge"
}
}
},
{
"name": "an act owed",
"of": "state:owed/<repository>.<number>.<head>.<act>",
"given": {
"v": 1,
"act": "view"
}
},
{
"name": "a turn",
"of": "state:turns/<repository>",
"given": {
"v": 1,
"repository": "novox/mesh-sdk",
"waiting": [
{
"group": "feat/registry",
"heads": "0123456789ab",
"approved": "2026-11-05T10:00:00Z",
"since": "2026-11-05T10:01:00Z"
}
]
}
}
]
+574
View File
@@ -0,0 +1,574 @@
{
"format": 1,
"seat": "mesh-delivery",
"scope": "mesh",
"grain": "whole",
"domain": "Delivery",
"decision": "novox/hq ADR 0239",
"verbs": [
{
"name": "deliveries",
"description": "Every delivery not final, and those that ended in the last day, one line each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed by state, repository or group.",
"input": {
"properties": {
"all": {
"description": "\"true\": the final ones of the last thirty days too",
"enum": [
"true",
"false"
],
"type": "string"
},
"group": {
"description": "a group's id (its branch name)",
"type": "string"
},
"repository": {
"description": "owner/repository",
"type": "string"
},
"state": {
"description": "one state, e.g. delivering or held",
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "what deliveries answers, as its description says"
}
},
{
"name": "times",
"description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the delivery view (nox-delivery): each machine's want and have, and since when",
"optional": true,
"input": {
"properties": {},
"type": "object"
},
"answer": {
"type": "object",
"description": "what times answers, as its description says"
}
},
{
"name": "show",
"description": "One delivery or group whole: its delivery plan (what it builds, what each machine receives, what is not an ordinary send), every transition with when and why, the machine steps of its walk, its group and its order.",
"input": {
"properties": {
"id": {
"description": "a delivery's id, or a group's",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "what show answers, as its description says"
}
},
{
"name": "groups",
"description": "Every delivery group: its members in order, why each pair is ordered (declared, built by, version skew, engine before controller, by name), its composed check and its state, derived from its members.",
"input": {
"properties": {
"all": {
"description": "\"true\": the groups that ended too",
"enum": [
"true",
"false"
],
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "what groups answers, as its description says"
}
},
{
"name": "what-if",
"description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the merge check's composition, which composes every machine a change touches",
"optional": true,
"input": {
"properties": {},
"type": "object"
},
"answer": {
"type": "object",
"description": "what what-if answers, as its description says"
}
},
{
"name": "checks",
"description": "What the mesh's checks said of a pull request's head or of one commit: each of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, mesh/delivery, …) with its state, description and when it was set; the merge check's full verdict as the controller said it — each layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's protection would let it merge, every required status being success.",
"optional": true,
"input": {
"properties": {
"commit": {
"description": "a commit's sha, or the start of one, instead of a pull request",
"type": "string"
},
"number": {
"description": "a pull request's number: its head is read",
"type": "string"
},
"repository": {
"description": "owner/repository",
"type": "string"
}
},
"required": [
"repository"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "what checks answers, as its description says"
}
},
{
"name": "table",
"description": "The state table every delivery runs by: each transition with its guard, each state's bound and what healer H2 may do once it has passed; and the machine steps' table.",
"input": {
"properties": {},
"type": "object"
},
"answer": {
"type": "object",
"description": "what table answers, as its description says"
}
},
{
"name": "stalled",
"description": "Every delivery held past its state's bound, with the transition the table lets healer H2 take for it, or none.",
"input": {
"properties": {},
"type": "object"
},
"answer": {
"type": "object",
"description": "what stalled answers, as its description says"
}
},
{
"name": "recheck",
"description": "Check a rejected or ready delivery again: it goes back to proposed and the controller is asked for its check. With why.",
"input": {
"properties": {
"id": {
"description": "the delivery's id",
"type": "string"
},
"why": {
"description": "why, kept with the transition",
"type": "string"
}
},
"required": [
"id",
"why"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "what recheck answers, as its description says"
}
},
{
"name": "release",
"description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view",
"optional": true,
"input": {
"properties": {},
"type": "object"
},
"answer": {
"type": "object",
"description": "what release answers, as its description says"
}
},
{
"name": "stop",
"description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)",
"optional": true,
"input": {
"properties": {},
"type": "object"
},
"answer": {
"type": "object",
"description": "what stop answers, as its description says"
}
},
{
"name": "close",
"description": "Healer H2's verb: take the one transition the table names for a delivery held past its bound. Refused for any other.",
"input": {
"properties": {
"id": {
"description": "the delivery's id",
"type": "string"
},
"why": {
"description": "the condition it answers",
"type": "string"
}
},
"required": [
"id",
"why"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "what close answers, as its description says"
}
},
{
"name": "retire-history",
"description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: nothing to retire: the gate keeps no delivery after its merge",
"optional": true,
"input": {
"properties": {},
"type": "object"
},
"answer": {
"type": "object",
"description": "what retire-history answers, as its description says"
}
},
{
"name": "settle",
"description": "Ask the operator to settle deliveries held because their merge reached the trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another reason, naming why.",
"optional": true,
"input": {
"additionalProperties": false,
"properties": {
"ids": {
"description": "the held deliveries' ids (owner/repository@commit)",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"why": {
"description": "why they may be settled, shown to the operator",
"type": "string"
}
},
"required": [
"ids",
"why"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "what settle answers, as its description says"
}
}
],
"description": "The delivery gate: reads and steers deliveries, merges and the checks that decide them; the merge is made on the operator's Approve once both checks passed.",
"states": [
{
"name": "source",
"template": "state",
"key-forms": [
{
"key": "main",
"value": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"group": {
"type": "string",
"description": "the group that moved it"
},
"heads": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "repository to the merged head, a full commit"
}
},
"required": [
"group",
"heads"
],
"description": "every repository's merged head; its revision is the mesh's source revision"
},
"description": "the one key, written by the holder alone by compare-and-set"
}
],
"description": "the mesh's source revision",
"history": 64
},
{
"name": "merges",
"template": "state",
"key-forms": [
{
"key": "<group>",
"value": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"group": {
"type": "string"
},
"ask": {
"type": "string",
"description": "the ask key"
},
"approved": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "<repository>@<number> to the approved head"
},
"pulls": {
"type": "array",
"items": {
"type": "object",
"properties": {
"repository": {
"type": "string"
},
"number": {
"type": "integer"
},
"head": {
"type": "string"
},
"state": {
"type": "string",
"description": "merging, merged or refused"
},
"commit": {
"type": "string"
},
"words": {
"type": "string"
}
},
"required": [
"repository",
"number",
"head",
"state"
]
}
}
},
"required": [
"group",
"ask",
"pulls"
],
"description": "one group's merge and how far each pull request is"
},
"description": "a group's merge"
}
],
"description": "the merges",
"history": 10
},
{
"name": "asks",
"template": "state",
"key-forms": [
{
"key": "merge.<group>.<heads>",
"value": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"group": {
"type": "string"
},
"heads": {
"type": "array",
"items": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"description": "<owner>/<name>"
},
"number": {
"type": "integer"
},
"head": {
"type": "string",
"description": "the full commit"
}
},
"required": [
"repository",
"number",
"head"
]
}
},
"ask": {
"description": "the operator-channel ask as raised"
},
"asking": {
"type": "integer",
"description": "the number of times it was asked"
}
},
"required": [
"group",
"heads",
"ask"
]
},
"description": "the ask of the operator for one group at one set of heads"
}
],
"description": "the asks of the operator for a merge",
"history": 5
},
{
"name": "owed",
"template": "state",
"key-forms": [
{
"key": "<repository>.<number>.<head>.<act>",
"value": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"act": {
"type": "string",
"description": "status=<context>, view, note or comment.<seq>"
},
"context": {
"type": "string"
},
"target": {
"type": "string"
},
"state": {
"type": "string"
},
"text": {
"type": "string"
},
"ref": {
"type": "string"
}
},
"required": [
"act"
],
"description": "one act owed to the forge, written by its owner and done by the forge bridge"
},
"description": "one act owed to the forge"
}
],
"description": "the acts the gate owes the forge",
"history": 5
},
{
"name": "turns",
"template": "state",
"key-forms": [
{
"key": "<repository>",
"value": {
"type": "object",
"properties": {
"v": {
"type": "integer",
"description": "the layout version"
},
"repository": {
"type": "string"
},
"waiting": {
"type": "array",
"items": {
"type": "object",
"properties": {
"group": {
"type": "string"
},
"heads": {
"type": "string"
},
"ask": {
"type": "string"
},
"approved": {
"type": "string",
"format": "date-time",
"description": "when (RFC 3339 time)"
},
"since": {
"type": "string",
"format": "date-time",
"description": "when (RFC 3339 time)"
},
"waits-for": {
"type": "string"
},
"waits-since": {
"type": "string",
"format": "date-time",
"description": "when (RFC 3339 time)"
},
"step": {
"type": "string"
}
},
"required": [
"group",
"heads",
"approved",
"since"
]
}
}
},
"required": [
"repository",
"waiting"
],
"description": "the merges of a repository in the order they wait for their turn"
},
"description": "the merges waiting on a repository"
}
],
"description": "which merge waits for which",
"history": 5
}
]
}
@@ -0,0 +1,257 @@
[
{
"name": "a running step",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "a1b2",
"tool": "build",
"node": "ace",
"state": "running",
"started": "2026-11-05T10:00:00Z"
}
},
{
"name": "a paused node",
"of": "state:pauses/paused.<node>",
"given": {
"v": 1,
"paused": true,
"by": "operator@shanks",
"at": "2026-11-05T10:00:00Z"
}
},
{
"name": "a node's capacity",
"of": "state:pauses/capacity.<node>",
"given": {
"v": 1,
"effective": 2,
"configured": 2,
"why": "the configured steps fit the cores"
}
},
{
"name": "a node's toolchain",
"of": "state:toolchains/<node>.<language>",
"given": {
"v": 1,
"language": "go",
"version": "1.26.0",
"recipe": "1"
}
},
{
"name": "a build ask",
"of": "accept:delivery",
"subject": "mesh.seat.node-build-go.accept.delivery",
"given": {
"id": "a1b2",
"kind": "build",
"language": "go",
"asker": "mesh-delivery-builder",
"build": {
"module": "nats",
"artifacts": [
{
"artifact": "go"
}
]
}
}
},
{
"name": "a check ask with its fetch, siblings and levels",
"of": "accept:check",
"subject": "mesh.seat.node-build-go.accept.check",
"given": {
"v": 1,
"id": "c3d4",
"kind": "check",
"language": "go",
"asker": "mesh-delivery-checker",
"at": "2026-11-05T10:00:00Z",
"check": {
"fetch": {
"repository": "novox/mesh-catalog",
"commit": "0123456789abcdef0123456789abcdef01234567",
"ref": "refs/pull/7/head",
"base": "89abcdef0123456789abcdef0123456789abcdef",
"beside": [
{
"repository": "novox/mesh-sdk",
"commit": "fedcba9876543210fedcba9876543210fedcba98"
}
],
"packages": {
"git.novox.be/novox/mesh-sdk/go": "fedcba9876543210fedcba9876543210fedcba98"
}
},
"levels": [
"unit",
"integration"
],
"kept": true,
"toolchain": {
"language": "go",
"version": "1.26.0",
"recipe": "2"
},
"node-names": [
"ace",
"g14"
]
}
}
},
{
"name": "a finished build step with its artifacts",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "a1b2",
"tool": "build",
"node": "ace",
"state": "passed",
"started": "2026-11-05T10:00:00Z",
"ended": "2026-11-05T10:03:00Z",
"result": {
"artifacts": [
{
"artifact": "go",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"reference": "container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"toolchain": {
"language": "go",
"version": "1.26.0",
"recipe": "1"
},
"imports": [
"git.novox.be/novox/mesh-sdk/go"
]
}
}
},
{
"name": "a finished check step with its levels",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "c3d4",
"tool": "check",
"node": "ace",
"state": "passed",
"started": "2026-11-05T10:00:00Z",
"ended": "2026-11-05T10:06:00Z",
"result": {
"fetch": {
"workspace": "mvii1iwu5414e4",
"commit": "0123456789abcdef0123456789abcdef01234567",
"modules": [
"."
],
"base": "abcdef0123456789abcdef0123456789abcdef01",
"merge": "fedcba9876543210fedcba9876543210fedcba98",
"changed": [
"a.go",
"modules/m/main.go"
],
"trees": {
"modules/m": {
"tree": "1111111111111111111111111111111111111111",
"manifest": "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356",
"locks": {
"go.sum": "c5fc83c01e92404452b986527d239140ccf9a48b88e0c268fbf38c2e1429e9c9"
}
}
}
},
"levels": [
{
"level": "unit",
"digest": {
"level": "unit",
"packages": [
{
"package": "./modules/a",
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
]
},
"kept": [
{
"package": "./modules/a",
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"tests": 12
}
],
"tests": [
{
"package": "./modules/b",
"pass": true
}
],
"unmet": [
{
"package": "./modules/c",
"needs": [
"docker"
]
}
]
}
]
}
}
},
{
"name": "a declined step",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "c3d5",
"tool": "check",
"node": "ace",
"state": "refused",
"started": "2026-11-05T10:00:00Z",
"result": {
"declined": true,
"needs": [
"docker"
],
"lacks": [
"docker"
]
}
}
},
{
"name": "a finished check step whose head does not merge",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "c3d5",
"tool": "check",
"node": "ace",
"state": "passed",
"started": "2026-11-05T10:00:00Z",
"ended": "2026-11-05T10:01:00Z",
"result": {
"fetch": {
"workspace": "mvii1iwz5050eb",
"commit": "0123456789abcdef0123456789abcdef01234567",
"modules": [
"."
],
"base": "abcdef0123456789abcdef0123456789abcdef01",
"conflicts": [
"a.go",
"sub/b.go"
]
}
}
}
}
]
+88
View File
@@ -0,0 +1,88 @@
{
"format": 1,
"seat": "node-build-go",
"scope": "node",
"grain": "whole",
"domain": "Delivery",
"decision": "novox/hq ADR 0525, ADR 0536, ADR 0553",
"pulled-by-kind": true,
"yields": true,
"accepts": [
{
"name": "delivery",
"body": {
"$ref": "#/defs/StepAsk"
}
},
{
"name": "check",
"body": {
"$ref": "#/defs/StepAsk"
}
},
{
"name": "recheck",
"body": {
"$ref": "#/defs/StepAsk"
}
}
],
"description": "The Go build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers.",
"includes": [
"build-step"
],
"states": [
{
"name": "steps",
"template": "state",
"key-forms": [
{
"key": "<node>.<id>",
"value": {
"$ref": "#/defs/Step"
},
"description": "a step on a node"
}
],
"description": "the steps this module runs",
"history": 5
},
{
"name": "pauses",
"template": "state",
"key-forms": [
{
"key": "paused.<node>",
"value": {
"$ref": "#/defs/Pause"
},
"description": "whether a node is paused"
},
{
"key": "capacity.<node>",
"value": {
"$ref": "#/defs/Capacity"
},
"description": "a node's capacity"
}
],
"description": "the pause and the capacity of each node",
"history": 1
},
{
"name": "toolchains",
"template": "state",
"key-forms": [
{
"key": "<node>.<language>",
"value": {
"$ref": "#/defs/Toolchain"
},
"description": "the toolchain a node reports"
}
],
"description": "the toolchains the nodes report",
"history": 1
}
]
}
@@ -0,0 +1,63 @@
[
{
"name": "a running step",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "a1b2",
"tool": "build",
"node": "ace",
"state": "running",
"started": "2026-11-05T10:00:00Z"
}
},
{
"name": "a paused node",
"of": "state:pauses/paused.<node>",
"given": {
"v": 1,
"paused": true,
"by": "operator@shanks",
"at": "2026-11-05T10:00:00Z"
}
},
{
"name": "a node's capacity",
"of": "state:pauses/capacity.<node>",
"given": {
"v": 1,
"effective": 2,
"configured": 2,
"why": "the configured steps fit the cores"
}
},
{
"name": "a node's toolchain",
"of": "state:toolchains/<node>.<language>",
"given": {
"v": 1,
"language": "go",
"version": "1.26.0",
"recipe": "1"
}
},
{
"name": "a build ask",
"of": "accept:delivery",
"subject": "mesh.seat.node-build-image.accept.delivery",
"given": {
"id": "a1b2",
"kind": "build",
"language": "go",
"asker": "mesh-delivery-builder",
"build": {
"module": "nats",
"artifacts": [
{
"artifact": "go"
}
]
}
}
}
]
@@ -0,0 +1,88 @@
{
"format": 1,
"seat": "node-build-image",
"scope": "node",
"grain": "whole",
"domain": "Delivery",
"decision": "novox/hq ADR 0525, ADR 0536, ADR 0553",
"pulled-by-kind": true,
"yields": true,
"accepts": [
{
"name": "delivery",
"body": {
"$ref": "#/defs/StepAsk"
}
},
{
"name": "check",
"body": {
"$ref": "#/defs/StepAsk"
}
},
{
"name": "recheck",
"body": {
"$ref": "#/defs/StepAsk"
}
}
],
"description": "The image build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers.",
"includes": [
"build-step"
],
"states": [
{
"name": "steps",
"template": "state",
"key-forms": [
{
"key": "<node>.<id>",
"value": {
"$ref": "#/defs/Step"
},
"description": "a step on a node"
}
],
"description": "the steps this module runs",
"history": 5
},
{
"name": "pauses",
"template": "state",
"key-forms": [
{
"key": "paused.<node>",
"value": {
"$ref": "#/defs/Pause"
},
"description": "whether a node is paused"
},
{
"key": "capacity.<node>",
"value": {
"$ref": "#/defs/Capacity"
},
"description": "a node's capacity"
}
],
"description": "the pause and the capacity of each node",
"history": 1
},
{
"name": "toolchains",
"template": "state",
"key-forms": [
{
"key": "<node>.<language>",
"value": {
"$ref": "#/defs/Toolchain"
},
"description": "the toolchain a node reports"
}
],
"description": "the toolchains the nodes report",
"history": 1
}
]
}
@@ -0,0 +1,135 @@
[
{
"name": "a running step",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "a1b2",
"tool": "build",
"node": "ace",
"state": "running",
"started": "2026-11-05T10:00:00Z"
}
},
{
"name": "a paused node",
"of": "state:pauses/paused.<node>",
"given": {
"v": 1,
"paused": true,
"by": "operator@shanks",
"at": "2026-11-05T10:00:00Z"
}
},
{
"name": "a node's capacity",
"of": "state:pauses/capacity.<node>",
"given": {
"v": 1,
"effective": 2,
"configured": 2,
"why": "the configured steps fit the cores"
}
},
{
"name": "a node's toolchain",
"of": "state:toolchains/<node>.<language>",
"given": {
"v": 1,
"language": "go",
"version": "1.26.0",
"recipe": "1"
}
},
{
"name": "a build ask",
"of": "accept:delivery",
"subject": "mesh.seat.node-build-node.accept.delivery",
"given": {
"id": "a1b2",
"kind": "build",
"language": "go",
"asker": "mesh-delivery-builder",
"build": {
"module": "nats",
"artifacts": [
{
"artifact": "go"
}
]
}
}
},
{
"name": "a check ask with the group's tagged package",
"of": "accept:check",
"subject": "mesh.seat.node-build-node.accept.check",
"given": {
"v": 1,
"id": "c3d5",
"kind": "check",
"language": "typescript",
"asker": "mesh-delivery-checker",
"at": "2026-11-05T10:00:00Z",
"check": {
"fetch": {
"repository": "novox/mesh-delivery",
"commit": "0123456789abcdef0123456789abcdef01234567",
"ref": "refs/pull/7/head",
"base": "89abcdef0123456789abcdef0123456789abcdef",
"package": {
"name": "@novox/mesh-sdk",
"version": "0.1.15-feat-x.1"
}
},
"levels": [
"unit",
"integration"
],
"kept": true,
"toolchain": {
"language": "typescript",
"version": "26.12.0",
"recipe": "1"
},
"node-names": [
"ace",
"g14"
]
}
}
},
{
"name": "a finished check step of a TypeScript merge preview",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "c3d6",
"tool": "check",
"node": "ace",
"state": "passed",
"started": "2026-11-05T10:00:00Z",
"ended": "2026-11-05T10:01:00Z",
"result": {
"fetch": {
"workspace": "mvii1sxt979f67",
"commit": "0123456789abcdef0123456789abcdef01234567",
"modules": [
"modules/m"
],
"base": "abcdef0123456789abcdef0123456789abcdef01",
"merge": "0123456789abcdef0123456789abcdef01234567",
"changed": [
"modules/m/package.json"
],
"trees": {
"modules/m": {
"tree": "0123456789abcdef0123456789abcdef01234567",
"manifest": "0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef"
}
}
}
}
}
}
]
+88
View File
@@ -0,0 +1,88 @@
{
"format": 1,
"seat": "node-build-node",
"scope": "node",
"grain": "whole",
"domain": "Delivery",
"decision": "novox/hq ADR 0525, ADR 0536, ADR 0553",
"pulled-by-kind": true,
"yields": true,
"accepts": [
{
"name": "delivery",
"body": {
"$ref": "#/defs/StepAsk"
}
},
{
"name": "check",
"body": {
"$ref": "#/defs/StepAsk"
}
},
{
"name": "recheck",
"body": {
"$ref": "#/defs/StepAsk"
}
}
],
"description": "The Node build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers.",
"includes": [
"build-step"
],
"states": [
{
"name": "steps",
"template": "state",
"key-forms": [
{
"key": "<node>.<id>",
"value": {
"$ref": "#/defs/Step"
},
"description": "a step on a node"
}
],
"description": "the steps this module runs",
"history": 5
},
{
"name": "pauses",
"template": "state",
"key-forms": [
{
"key": "paused.<node>",
"value": {
"$ref": "#/defs/Pause"
},
"description": "whether a node is paused"
},
{
"key": "capacity.<node>",
"value": {
"$ref": "#/defs/Capacity"
},
"description": "a node's capacity"
}
],
"description": "the pause and the capacity of each node",
"history": 1
},
{
"name": "toolchains",
"template": "state",
"key-forms": [
{
"key": "<node>.<language>",
"value": {
"$ref": "#/defs/Toolchain"
},
"description": "the toolchain a node reports"
}
],
"description": "the toolchains the nodes report",
"history": 1
}
]
}

Some files were not shown because too many files have changed in this diff Show More