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
mesh-tools 8c67adaf91 Merge pull request 'The indexer learns its own build right at its start, and says when a start takes nothing in (issue 1921)' (#156) from fix/1921-own-build-at-start into main 2026-11-05 19:22:04 +00:00
jschoubben e3735da928 The indexer takes the engine's want as its build only while the apply is in progress (issue 1921)
mesh/check being carried to e3735da928f4 on 8c67adaf9136 | from abe9ea3796ea
mesh/delivery merged: source.main moved to novox/mesh-delivery@8c67adaf9136
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 19:53:05 +01:00
jschoubben 78d57e1cc6 A transient forge failure is asked again; only a directory missing from the tree is cached per head (issue 1921)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 19:49:27 +01:00
jschoubben 9f9e743b2f The indexer started during its replacement fixes the successor's build; a start that decides nothing says so; a failing head is said once (issue 1921)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 19:47:42 +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
mesh-tools abe9ea3796 Merge pull request 'The running indexer accepts its successor, and a delivery group can be tried whole (issue 1966, issue 1921)' (#155) from fix/1921-group-try into main 2026-11-05 18:34:52 +00:00
jschoubben f9578cb7b4 A try's end is said again until the stream acknowledges it; a waiting try says why; a hand recovery never overlaps a try event (issue 1921, ADR 0639)
mesh/delivery merged: source.main moved to novox/mesh-delivery@abe9ea3796ea
mesh/check being carried to f9578cb7b418 on 8c67adaf9136 | from abe9ea3796ea
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 19:24:45 +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 69a2790099 The indexer fixes its own build once at its start and never learns it later; a merge held for an unknown own build is said pending with why and since (issue 1921, ADR 0633 §1)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:47:31 +01:00
jschoubben 88a47be251 A try says its end with the state it ended in, kept in the try's own state, so an end after a checker restart is still said; only the checker publishes try events (issue 1921)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:47:31 +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 bd5eb33e9e A failed recovery of the open tries is said and retried; every open try is listed; a silent node ends a try as error (issue 1921)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:38:11 +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 c2b8ac3370 The indexer hears the checker's events after it lists the open tries; rows for the start order and the config skip (issue 1921)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:35:22 +01:00
jschoubben 92c38981ea mesh-sdk's tests read mesh-contracts and mesh-delivery beside it (issue 1921)
mesh-sdk compares its bootstrapped seat definitions with their contracts files, read beside it; the checker
places both, at the group's head for a member, else at the trunk, for a check and for a group try alike.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:30:35 +01:00
jschoubben 45386212da A group try's candidate follows the checker's try events, judged in one locked pass (issue 1921)
The checker says try.started (members at their commits) and try.ended for a whole try; the indexer writes and
deletes the candidate from them and finds the open tries again at its start through checker_tries, never reading
the checker's state. The try's digests are judged inside the one pass that takes the seats; the definitions of every
contracts module of each member are read; the config declarations leave a try's candidate alone.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:28:52 +01:00
jschoubben b97e277d2d The indexer writes a group try's candidate from the checker's try and its previews (issue 1921)
A whole try has no pull request, yet compose-candidate and the manifest validator read the indexer's
candidate of a group. The indexer reads the checker's tries, writes candidates.<try id> (number 0 per member) from the
previews, judges its manifests with the seat definitions the members carry, leaves try-* alone in the open message,
deletes it when the try ends, and has a tool that reads and one that drops it.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:13:29 +01:00
jschoubben 8823cc29d6 Merge branch 'fix/validate-successor-with-its-definitions' into fix/1921-group-try 2026-11-05 18:06:35 +01:00
jschoubben 55936c1cd7 Try a delivery group: members at exact commits, judged by the code of a group's check, every failure named (issue 1921)
A try names several repositories, each the exact commit asked. A whole try (not narrowed to a level or
tests) runs the group check's own path: the manifest validator, the module graph and the composition on every
placed node, reading the indexer's candidate of the try's group. A try with no such candidate is an error that
says so, never a pass. A try's candidate is no check: the checker leaves it alone and owes the forge nothing.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:05:24 +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 31ff708f88 Judge a successor indexer's manifest with the seat definitions its revision carries, not the built-in list (issue 1966, ADR 0633 §1, §6)
The take-in's dry run reads the contracts modules' definitions of the revision's merges and puts them over the seats while it judges; cmd/mesh-judge puts the request's seats over its own. A refusal of the named indexer's manifest is said on take-in.pending and in indexer_pending.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:52:53 +01: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
mesh-tools 2fcd61baa6 Merge pull request 'Take-in holds a merge by reference, so a mesh-catalog merge over the state value cap is taken in (issue 1585, ADR 0633)' (#154) from fix/1585-pending-by-reference into main 2026-11-05 10:48:57 +00:00
jschoubben 655791b8a3 Bridge tool test: the merged-message rows are a subtest of the existing tools test, within the test budget (issue 1585)
mesh/check being carried to 655791b8a351 on 2fcd61baa6eb | from 4596fbe1b7ac
mesh/delivery merged: source.main moved to novox/mesh-delivery@2fcd61baa6eb
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 11:42:10 +01:00
jschoubben 3c832d1a2d Indexer: a held merge's message is asked of the forge bridge, not read from its stream; covered merges leave their candidate; indexer_take_in_merge (issue 1585)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 11:37:13 +01:00
jschoubben 0605962960 Forge bridge: bridge_merged_message answers a merge's merged message again, built as the bridge builds the one it publishes (issue 1585)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 11:34:04 +01:00
jschoubben 343321f11e Indexer: Arrive keeps its message in memory, only the newest merge of a repository is read, an unreadable one is said and covered by a newer one (issue 1585)
The take-in unblocks merges without the read grant on the forge bridge's stream: the message Arrive was handed is used,
the stream is read only for a pending merge not held in memory, and older merges of a repository in a revision are
covered by its newest and never read.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 11:24:18 +01:00
jschoubben 4b7a31b2c7 Indexer: a pending merge keeps a reference to its FORGE message, not a copy, so a merge over the state value cap is held (issue 1585)
The pending value holds the stream sequence and the small fields the waits read; the take-in reads the message from the
forge bridge's stream by sequence. A message the stream no longer holds is said (take-in.pending lost, indexer_pending)
and never taken in silently. indexer_take_in_forge takes one forge message in by its sequence.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 11:21:42 +01:00
mesh-tools 4596fbe1b7 Merge pull request 'Take-in by the indexer a revision names; a group's merges taken in together (issue 1585, ADR 0633)' (#153) from feat/take-in into main 2026-11-05 00:33:44 +00:00
jschoubben c78941e80b Take-in events are the indexer's ordinary events: no stream of its own, which the manifest validator refuses (issue 1585, ADR 0633 §5)
mesh/delivery merged: source.main moved to novox/mesh-delivery@4596fbe1b7ac
mesh/delivery-group group feat/take-in merged: every member is merged
mesh/check being carried to c78941e80b59 on 4596fbe1b7ac | from ae31b66a9e33, mesh-catalog fd4931a75261 | novox/mesh-delivery#153 in group feat/take-in
The group's check failed at 7b7096f3: the group's own manifest validator refuses the indexer's stream "take-in", outside
the bus's predefined forms. The lead's ruling: the indexer says take-in.pending and take-in.taken as its ordinary events
(its emits, mesh.mod.<module>.event.take-in.<kind>), as manifest-accepted is said, and keeps no stream. The wait's key is
in the body only: an emitted event names one event (no wildcard), and a key such as g.<group> is no event name. The
watchman consumes the two events by name on the shared events stream; it needs no replay, since it seeds its waits from
indexer_pending at its start. indexer_pending says the last event of each wait as take-in.<kind>.

Run locally, as the check runs it: cmd/mesh-judge over every manifest of mesh-catalog, mesh-delivery, mesh-host,
mesh-tools, mesh-networking and mesh-media-catalog, with this branch's and mesh-catalog's feat/take-in manifests in the
change case and origin/main's in the trunk case: 171 of 171 accepted in both. The same run with the indexer's manifest at
7b7096f3 refuses it with the check's words.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 00:59:27 +01:00
jschoubben 7b7096f37e Take-in, review round 1: no claim left behind, no sleep at the start, waits seeded at a restart, ends asked readable (issue 1585, ADR 0633)
mesh/delivery-group group feat/take-in rejected: composed together: the checker's verdict at the members' heads (38623984d6ad): mesh-build-go
mesh/delivery closed: a newer head of the same pull request
mesh/check failed (2): mesh-build-go: the indexer's judge at the group's mesh-delivery head 7b7096f37ea76e1…; and 1 more | 7b7096f37ea7 on ae31b66a9e33
- A take-in releases its claims (compare-and-set, only while the claim is still its own) on a lost claim, on a claim it
  cannot read back, and on a merge it fails to take in: a claim left behind refused every later take-in until a
  restart. T5 row with a store that fails once, both ways.
- The start step waits for nothing: with the build it runs unknown it says so and the forge consumer starts; a merged
  message left unacknowledged asks the build again (clause 1). T10 row: the start answers at once and takes nothing in.
- A toolchain report evaluates pending again (ToolchainReported), since the build a revision names moves with the
  toolchain. T7 row: the revision is taken in once the report makes it name the running build.
- The group's end is told by its whole subject (process.ModuleStreamSubject), so a repository named forge/group is
  never taken for one; row in the indexer's handle test.
- The watchman asks the indexer's waits through indexer_pending at its start (invokes), before its durable consumer,
  which does not hear again what it acknowledged before a restart; the comment that said the whole stream is heard is
  corrected. W2 row: a wait seeded within its bound is raised past it.
- bridge_groups names each group's last end ask, its who, why and when, and the bridge's answer (state end-asks).
- The check's words for the indexer's rules: the trunk's manifest validator.
- The bridge's end drops the merged-state test its merge commit already implies.
- Test budget: groupask_test folded into groups_test, and the K1 mesh/check test into the owed status one: 804 unit
  tests (was 806).

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 00:28:56 +01:00
jschoubben 4e4601a1eb Take-in and forge contracts live with their owners: the indexer's takein package, the forge bridge's forgemsg package; mesh-sdk pinned at main (issue 1585, ADR 0633)
The operator, 2026-11-04: logic one module owns lives inside that module as a package of its own, so the take-in step
changes no mesh-sdk. TakeIn, its keys and event names, PendingWait and PendingAnswer move to
modules/mesh-delivery-indexer/takein (its merges a takein.Merge of its own); Forge whole, with its group fields,
ForgeGroupEnd and GroupMerge, moves to modules/mesh-delivery-forge-bridge/forgemsg. Both are nested Go modules that their
readers take by replace, as buildrun is. Every importer moves in this commit: the indexer, the bridge, the watchman and
the view. Forge's private checks (the repository name, the version) are copied small; mesh-sdk main still holds an unused
Forge until its own fold removes it.

The indexer, bridge, watchman, view and checker pin mesh-sdk 5a38a12 (origin/main), no feat/take-in commit.

The bridge's integration bus now grants the state end-asks that bridge_group_end_again added: without it every test of
the package failed on a permissions violation. Test budget: two new test functions (TestForge, TestTakeInEvents) and the
two the join added are paid by merging four pairs in internal/bridge; the repository stays at 806 unit tests.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 00:05:01 +01:00
jschoubben a395b49be2 WIP (ADR 0633 declined): watchman pinned to the group's mesh-sdk head; the other pins, the gate's const swap and the full checks not done
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:24:26 +01:00
jschoubben 1040c8ec46 Forge bridge: the process takes the keys of performed from internal/groups, one copy (issue 1585)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:24:05 +01:00
jschoubben 92dc5f03d0 Merge commit 'ef13e63' into feat/take-in 2026-11-04 23:23:38 +01:00
jschoubben ef13e63268 indexer: TestIndexerWhole runs the take-in step whole, a restart and a group (ADR 0633, issue 1585 I8a, I8b)
A revision changing the indexer writes only what the named build needs and is
taken in when that build starts; a gathered group stays across the restart and
its end makes one take-in.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:22:35 +01:00
jschoubben 9c6e70e555 indexer: before the pending state is on the bus, take a merge in at once and start without asking the own build (ADR 0633, issue 1585)
TestIndexerWhole found the start waiting for ever on a build no node-engine
answered, so the forge consumer never started, though nothing can be pending.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:19:07 +01:00
jschoubben ad9a353484 Forge bridge: bridge_groups and bridge_group_end_again, the end said again through the state end-asks (issue 1585, ADR 0633 §4)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:17:27 +01:00
jschoubben a762701e3d forge bridge: a member's merged message names its group, and the group's end is said once (ADR 0633 §4, issue 1585)
The group is read from the gate's merges state the bridge already reads;
the end comes only once every approved member is merged and its merged
message written, never on a refusal, with a message id the stream's
duplicate window holds to one.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:14:44 +01:00
jschoubben d6b6f24e98 forge bridge: a member's merged message names its group, and the group's end is said once (ADR 0633 §4, issue 1585)
The group is read from the gate's merges state the bridge already reads;
the end comes only once every approved member is merged and its merged
message written, never on a refusal, with a message id the stream's
duplicate window holds to one.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:11:53 +01:00
jschoubben ed4f07b0d8 Watchman: a take-in wait past its bound heard in the replay is said once the consumer has caught up, not after a time (issue 1585, ADR 0633 §5)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:10:23 +01:00
jschoubben 7609f2d9a2 Watchman: a replayed pending that its taken ends is never raised; a drop then a pending again is covered (issue 1585, ADR 0633 §5)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:09:20 +01:00
jschoubben d11a5af113 View: view_tiers names a module the indexer holds back, read through indexer_pending (issue 1585, ADR 0633 §5)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:09:20 +01:00
jschoubben 531d05ab4b indexer: hear a group's end, keep merges pending, take in at start (ADR 0633 §2, §4, issue 1585)
The consumer tells a group's end apart by its name before the forge decode,
a merged message goes through the pending state, and the pending revisions
naming this build are taken in before the consumer starts; take-in events go
on the indexer's own stream. Until the pending state is on the bus every
merge is taken in at once, as before.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:08:37 +01:00
jschoubben 071a28f218 indexer: a source revision is taken in only by the indexer it names (ADR 0633 §1-§5, issue 1585)
A merged message is kept in the indexer's own state pending and the merges
there are computed together on overlays of the indexer's states: taken in
whole when the indexer's fingerprint is the running build, otherwise only
the indexer's own entry, graph key and verdict are written and the rest
waits for the named indexer. A group's members are gathered until its end.
Waits are said on the stream take-in, read by indexer_pending and steered
by indexer_take_in_pending and indexer_drop_pending.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 23:07:02 +01:00
jschoubben 72633722c5 Checker test: the accepted-indexer row asserts the trunk's judge said so (issue 1585, T22)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 22:55:27 +01:00
jschoubben 0579e4e74d Watchman: hear the indexer's take-in stream and raise take-in-waits past 60 minutes (revision) or 30 (group) (issue 1585, ADR 0633 §5)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 22:54:21 +01:00
jschoubben 2cb3198049 Checker: judge the new indexer's manifest with the trunk's judge, and fail a held verdict (issue 1585, ADR 0633 clause 6)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 22:47:19 +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 failed (7): ace: novox/mesh-catalog#612 does not merge into the trunk: files in conflict: module…; and 6 more | 83b962cc6d6d on c0e5bc2834fe
mesh/delivery-group group feat/registry-milestone rejected: composed together: the checker's verdict at the members' heads (3a038b9f8b4d): ace
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
mesh-tools ae31b66a9e Merge pull request 'mesh-grants and the teller: the removed controller's leftover consumers are removable, and the teller pulls its durable consumer (issue 1126)' (#150) from fix/consumer-remove-any into main 2026-11-04 10:36:44 +00:00
jschoubben fc300216ec mesh-delivery-teller: pull the durable consumer for the router's warrants, so an answer sent while it is down is not lost (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@ae31b66a9e33
mesh/delivery-group group fix/consumer-remove-any merged: every member is merged
mesh/check being carried to fc300216ecf7 on ae31b66a9e33 | from dd90a26251b9, mesh-catalog 7a7db0166ad2
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 10:57:33 +01:00
jschoubben c4bba04e8f mesh-grants: the forge's ended take-in consumer is a removable leftover; the retired build-agent seat's subject is no writer (issue 1126)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 10:57:33 +01:00
jschoubben 1c6ac37fc1 mesh-grants: the controller's take-in and config-follow consumers on three buckets are removable leftovers (issue 1126)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 10:53:44 +01:00
mesh-tools dd90a26251 Merge pull request 'L3 final: verb, reader-capability and bus-state edges; one unreadable manifest holds only its logins; the indexer holds mesh-catalog (ADR 0614, ADR 0604, issue 774)' (#149) from feat/l3-final into main 2026-11-04 00:23:49 +00:00
jschoubben 70ade15c36 A refusal an older judge made never stands: the indexer judges again a pull's manifests and, at its start, every other judge's held verdict; indexer_judge takes a digest (issue 774)
mesh/delivery-group group feat/l3-final merged: every member is merged
mesh/delivery merged: source.main moved to novox/mesh-delivery@dd90a26251b9
mesh/check checking 70ade15c36eb on dd90a26251b9 | novox/mesh-delivery#149 in group feat/l3-final
The indexer judged only the manifests its modules state holds, so a digest refused by an older judge (node-tools
48c729f6 and mesh-provisioner-client a382b9df, refused for words then unknown) kept that refusal once it was back on
the trunk or in a candidate. Each such digest is now judged alone, in the catalogue the trunk holds, writing its
verdict and nothing else; an accepted verdict stays accepted under any later judge (ADR 0579 §1), so only a refused or
waiting verdict by another judge, or none, is judged again.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 00:45:59 +01:00
jschoubben cbbc10871f Pin mesh-sdk 85ba0a9a70d8, the group's head of feat/l3-final (issue 774)
mesh/delivery closed: a newer head of the same pull request
mesh/delivery-group group feat/l3-final rejected: composed together: the checker's verdict at the members' heads (278aa5f0ccbe): unit: mesh-provisioner-client/…
mesh/check failed (2): unit: mesh-provisioner-client/cmd/mesh-provisioner-client.TestNatsANodeComingBackFil…; and 1 more | cbbc10871f2e on 007c982ae670
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:45:00 +01:00
jschoubben 084bfb9f3e Merge branch 'feat/l3-final-c' into feat/l3-final 2026-11-03 23:43:51 +01:00
jschoubben 9d0b3cafc5 A modules value with no verdict yet holds only its module's users, and one module's value that does not read is a gap of that module, never of the whole list (ADR 0614 §8, issue 774)
The indexer writes a changed manifest's value before it judges it; the part now reads the indexer's verdicts.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:43:26 +01:00
jschoubben 62f7375522 Merge branch 'feat/l3-final-b' into feat/l3-final 2026-11-03 23:43:11 +01:00
jschoubben 2d49c660c0 view_tiers names bus state by the assigner's held want alone, never by a provisioner key no one makes (ADR 0614 §5; issue 774)
bus-state is offered with no credential of a consumer's own, so the provisioner keys nothing for it; the view showed
every module asking bus state as waiting for that key for good.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:42:37 +01:00
jschoubben 9d2816d001 The module offering bus-state never waits on bus state itself, and an offline node's build offers nothing (ADR 0614 §5; issue 774)
A bus module that both offers and asks bus state waited on itself for good at its first such build; it now waits on
nobody for it, as the graph and catalogue.Awaited leave a module's own provision out.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:42:18 +01:00
jschoubben 7006380599 Merge branch 'feat/l3-final-c' into feat/l3-final 2026-11-03 23:30:41 +01:00
jschoubben 46c735473f A want asking bus state waits until a build offering bus-state runs healthy on a node present (ADR 0614 §5; issue 774)
bus-state is answered with no credential of a consumer's own, so ADR 0603 §5 counts the provider's own have: the
assigner reads, per pass, what the running healthy builds on online nodes offer (their manifests through the builder's
builds) and holds a consumer with a bus-state entry under the provisions' rule until one offers it, handed or not and
with no provisioner key. The words name bus state, and the delivery view names the edge from them.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:28:41 +01:00
jschoubben 702b19fdb1 view_tiers names each wait by its edge, a verb, a reader capability or bus state, and every verb edge left out (ADR 0614 §2-§5; issue 774)
The words of a want held on a verb or a reader capability are written and read in one place, the module graph's
package: the assigner writes them, the view names the edge from them. A bus-state wait is named bus state, and each
module's verb edges the graph leaves out on a cycle are listed beside its waits.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:26:25 +01:00
jschoubben 0279c5c288 The check's cycle refusal meets no verb edge on a cycle and no package edge, and still refuses provisions and bases (ADR 0614 §3, §6; issue 774)
Rows in the judge's table: two modules invoking each other, or a verb closing a cycle with a provision, are not
refused; a cycle of provisions and one of bases are. The refusal's words name what a part waits on now.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:26:08 +01:00
jschoubben 1d9f9cfa2c The check names a changed build module that invokes a verb no running build serves as one it cannot run (ADR 0614 §7; issue 774)
What the running builds serve is read when a changed build module is met: the node-engine's haves, the builder's
build of each, its manifest. The modules it builds are composed from their manifests, as for any changed build
module, and the composition's warnings name the verb.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:26:08 +01:00
jschoubben c45bbfc391 A want waits until each verb it invokes is served by a running build, read from the module graph (ADR 0614 §2, §3; issue 774)
The assigner reads the indexer's module graph (a new reads line): of what a module invokes it waits only on the edges
the graph keeps and another module serves, so a left-out cycle holds nothing; a verb counts as served once a running
healthy build's manifest serves it. The hold is under the provisions' rule, its words naming the verb, since no reader
knows a rule number of its own yet. Without the graph no want waits on a verb.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:26:08 +01:00
jschoubben b4bfc14556 The indexer writes each module's verb edges into the module graph, the ones on a cycle left out over the whole graph (ADR 0614 §2, §3; issue 774)
The assigner reads which edges are left out from the graph, so one place decides the cycle; the whole graph is
judged, a module past the graph's cap included, before the first GraphMost are written.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:25:35 +01:00
jschoubben df05b1a5f7 The module graph carries verb and bus-state edges, and leaves out every verb edge on a cycle (ADR 0614 §2, §3, §5, §6; issue 774)
A graph node now names what its manifest invokes and serves, and the verb edges the graph leaves out; a bus-state
entry is a requirement of bus-state. Before and Cycle count the kept verb edges beside bases and awaited provisions,
never a package or toolchain edge. LeaveOut leaves out every verb edge whose ends reach each other, so the builder's
remove and a holder's builder_versions hold nothing and the check never refuses them.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:25:35 +01:00
jschoubben 509d277b32 The assigner holds a node's want while that node's engine does not state a reader capability the manifest needs, and moves it once it does (ADR 0614 §4, issue 774)
The rules read Module.Needs from the indexer's modules value and each node's stated reads from its record in
`<node>.node` of the haves; an engine stating none is read as the baseline, so the node-engine's own new build, whose
manifest uses only the baseline's words, is not held. The hold is rule 7 (no rule of its own is numbered in this
release, ADR 0614 F2) with words naming the node's engine and each capability, which the want tools and view_tiers
already show.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:24:39 +01:00
jschoubben 477b3bfea6 The indexer's judge names, with each verdict, the reader capabilities a manifest's own words, forms and seat names need, in its module value (ADR 0614 §4, issue 774)
Module.Needs is catalogue.ManifestNeeds of the manifest held: written by the merge that takes a changed manifest in, so
no reader takes it as needing nothing before the judge's pass, named again by the judge with the verdict, and carried
over by a write that leaves the digest, as the verdict is. The module value is the one the assigner and the checker
already read.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:22:08 +01:00
jschoubben ffdeb9771a The indexer claims the seat mesh-catalog and its tools bundle serves the four verbs from its states (ADR 0604 §2, issue 774)
CatalogTools moves into the index package so its row sits in the existing catalog test.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:11:21 +01:00
jschoubben b83085adac An unreadable manifest holds only its module's users: the list advances for every other principal, and the condition and grants_user_list name the module unreadable (ADR 0614 §8, issue 774)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:10:13 +01:00
jschoubben 390e50d7d1 Words back: the tool runner is what says tool-runner again; sdk/toolrunner goes (issue 774, issue 1585)
Reverts e3ce4b0: node-tools' manifest says tool-runner again, so the judge and
mesh-grants ask catalogue.IsToolRunner and the kept RuntimeModule is not counted.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:09:20 +01:00
mesh-tools 007c982ae6 Merge pull request 'L3 recovery: count mesh-sdk's kept RuntimeModule as the tool runner while node-tools' trunk manifest leaves tool-runner out (issue 774, issue 1585)' (#148) from fix/l3-words-after-judge into main 2026-11-03 21:09:09 +00:00
jschoubben e3ce4b095c Count mesh-sdk's kept RuntimeModule as the tool runner while node-tools' trunk manifest leaves tool-runner out
mesh/delivery merged: source.main moved to novox/mesh-delivery@007c982ae670
mesh/delivery-group group fix/l3-words-after-judge merged: every member is merged
mesh/check being carried to e3ce4b095ce9 on 007c982ae670 | from 4154c3040cee, mesh-catalog e6de42630355, mesh-tools 01ee2ef39488
The running indexer refuses the tool-runner word, so node-tools' trunk manifest takes it out again (mesh-tools
fix/l3-words-after-judge); trunk's judge then refuses node-tools twice (its waits, its runtime bundle) and mesh-grants
would compose its node users as plain modules. One helper, sdk/toolrunner.Is, counts a manifest as the tool runner
when it says tool-runner or its module is the SDK's deprecated catalogue.RuntimeModule; the judge's waits and
undelivered-bundle rules and mesh-grants' declared records ask it. It goes once tool-runner is back on node-tools'
trunk manifest (issue 774, issue 1585 note 42).

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 21:36:26 +01:00
mesh-tools 4154c3040c Merge pull request 'L3: a source revision ordered by the module graph, the check waits on its candidate's builds, and an emptied repository never drops a module another trunk holds (ADR 0603, ADR 0604, issues 1367, 774)' (#147) from feat/l3b-bus into main 2026-11-03 20:12:55 +00:00
jschoubben 945906ac97 The assigner waits for a requirement nothing offers, and for a provision the provisioner keys whoever is placed yet; a requirement something offers (a module's own name, a module the mesh provides itself) is left to Awaited as before (ADR 0603 §5)
mesh/delivery merged: source.main moved to novox/mesh-delivery@4154c3040cee
mesh/delivery-group group feat/l3b-bus merged: every member is merged
mesh/check checking 945906ac976c on 4154c3040cee | novox/mesh-delivery#147 in group feat/l3b-bus
2026-11-03 19:45:15 +01:00
jschoubben 1c66d58bce The assigner waits for a provision whose provider is not known yet: an unknown provider has provided nothing, so a consumer never moves ahead of a provider the indexer has not taken in (ADR 0603 §5); the integration test's write order is unordered again 2026-11-03 19:36:18 +01:00
jschoubben 34ef185ba5 The assigner's integration test puts the provider's module before the consumer's, so the consumer's requirement always has a provider to be judged against (the group check's flaky failure, 5 of 40 runs) 2026-11-03 19:33:43 +01:00
jschoubben f12823681c Merge the indexer's integration test: a field its judge does not know waits while the running build is unknown (ADR 0601 §3; the group check's failure)
mesh/delivery-group group feat/l3b-bus rejected: composed together: the checker's verdict at the members' heads (62ca67fc6b58): integration: mesh-delivery-assi…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-delivery-assigner/cmd/mesh-delivery-assigner.TestWantsAreWrittenAndNoDifferenceIs:… | f12823681c06 on c4470f42dba9
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 18:51:14 +01:00
jschoubben b96a4f1d99 Pin mesh-sdk b7abe203a929, the group's head of feat/l3b-bus: it keeps, deprecated, the exported names the running parts still use
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 18:50:12 +01:00
jschoubben 70ac4ff79a The indexer's integration test expects a field from later to wait, not be refused, while no engine says which build runs (ADR 0601 §3)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 18:41:10 +01:00
jschoubben 51deed51e3 Merge ADR 0603 §10: the check waits on the builder's builds of its candidate before it composes, asks itself again when a failed build is done again, and names what it composes from manifests (the operator, 2026-11-03: build §10 in L3)
mesh/delivery-group group feat/l3b-bus checking: 4 of 4 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 51deed51e3b4 on c4470f42dba9 | novox/mesh-delivery#147 in group feat/l3b-bus
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:56:03 +01:00
jschoubben 3842a464ed A check failed on a build asks itself again once the build is done again, and a failed check never waits on builds (ADR 0603 §10)
A failed candidate build no longer needs mesh-delivery.recheck, an exception verb (ADR 0421 §12): the checker hears the
builder's keys the failed verdict names and, once each is written done (builder_build_again), retakes the check of the
same heads as it does after a failure of the nodes, at its start too. When the tests have already failed the check
composes at once and names the builds it did not wait on, so its red status is never held back by a build.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:55:41 +01:00
jschoubben 877b09e92b checker_status names the builds a running check waits on (ADR 0603 §10)
Worked out from the builder's builds state at each read, stored nowhere: of each running check, the candidate's builds
that are neither done nor failed.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:51:09 +01:00
jschoubben 453cdfa607 The check waits on the builder's builds of its candidate before it composes, so it composes with them (ADR 0603 §10)
The builder already builds every candidate fingerprint into its own key. The check now watches exactly those keys on the
bus and composes once each is done or failed, within the new setting build-wait-bound (1800 s). A failed build fails the
check, naming it; a build not ended within the bound is named and the check composes without it. A module built by a
build module the change alters (mesh-build-go or mesh-build-node), one standing on a base the change alters, and one the
trunk holds no record of are not waited on, and the composition's warnings name each with why.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:51:09 +01:00
jschoubben 8f7945a109 Merge round 2 finding C and the check's true words: the new rows fold into existing tests, the check names what it composes from manifests (ADR 0603 §10; test budget)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:36:49 +01:00
jschoubben 2773885600 Merge round 2 findings A and B: only the newest manifest's provisions hold a want, and a wait keeps its first start while it stands (ADR 0603 §8, §9)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:36:49 +01:00
jschoubben 45bd63427f A wait the watchman mirrors keeps its earliest start while the same mark stands, so a key written again with new words does not start the bound again (ADR 0603 §8) 2026-11-03 17:26:45 +01:00
jschoubben effa12c4b4 Fold the three new tests into existing ones (test budget); the base note says what the check does
The check builds no artifacts and no changed base before its dependents; build_change.go now says only that they are composed from their manifests.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:26:31 +01:00
jschoubben 9da14440e8 The assigner waits only on the keys of provisions the newest manifest still requires: an older edge's asked or retiring key holds nothing (ADR 0603 §9) 2026-11-03 17:26:30 +01:00
jschoubben fd638b99d5 Merge D9, D13b, D15 and review round 1 findings 3 and 4: the indexer runs without its graph state, a package inside a module's directory has its publisher (ADR 0603 §3, §4, §10; ADR 0604 §1)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:21:28 +01:00
jschoubben 0d1edef846 Merge D11, D12, D14: delivered over the nodes present, the watchman's waits with a bound, the newest provider edge (ADR 0603 §7, §8, §9; L3 review round 1, finding 2)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:21:28 +01:00
jschoubben 5c4d770aa1 The check names the modules built with a changed build module or base; rows for D9 and the assigner's integration test (ADR 0603 §3, §4, §10)
D13b: the composition's warnings, and so the check's line, name the changed modules built with the Go build module the change alters and those standing on a changed base. D9: builder and verdict rows. D15: the assigner's integration test gets a source revision changing a provider, its new consumer and a build module.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:21:07 +01:00
jschoubben b4855f21c3 Test rows: a missing graph state fails no recompute (review finding 3); a recipe change builds a Go module again (D9)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:21:07 +01:00
jschoubben 1387b10e78 Take a missing graph state as no graph yet and a Publisher's directory as the nearest module above it
The indexer's recompute and merge failed on listing a graph bucket not yet on the bus, and a package published from inside a module's directory had no publisher, so the dependents of a build module missed what imports its packages (review findings 3 and 4 of L3).

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:21:01 +01:00
jschoubben a1814c6571 Assigner row: the newest source revision's provider edge replaces the older one, and only it is waited on (ADR 0603 §9) 2026-11-03 17:19:05 +01:00
jschoubben 92b4307922 The watchman mirrors a want held by a rule, a provision asked and a build queued behind a package as waits with a bound, and raises a finding past it (ADR 0603 §8) 2026-11-03 17:18:36 +01:00
jschoubben 2c37f48f9d A source revision is delivered over the nodes present: the view leaves out a node the watchman says is away and names it, and the node counts once it runs its wants (ADR 0603 §7) 2026-11-03 17:16:30 +01:00
jschoubben 4a736858b6 Pin mesh-sdk cf5812101b0b, the group's head of feat/l3b-bus with the module graph taken out 2026-11-03 16:56:37 +01:00
jschoubben 3411b91571 The module graph is mesh-delivery's own: a Go module sdk/ with package graph, used by path by the indexer, the view and the checker, and the seat mesh-catalog's answers built from mesh-sdk's contract types (the operator, 2026-11-03: no graph-related code in mesh-sdk, only contracts; research 081 D11, D13) 2026-11-03 16:56:37 +01:00
jschoubben 0ce16ad773 Pin mesh-sdk 18b3eac47c8e, the group's head of feat/l3b-bus rebased onto main 2341e8e9 2026-11-03 16:46:40 +01:00
jschoubben 875d42fe3e On B1: the ledger test names its tool runner by its manifest's tool-runner, and ShelfWaiting takes B1's old-section counts (rebase onto main c4470f42) 2026-11-03 16:46:40 +01:00
jschoubben aacdba44e3 The judge's copy of the mesh-catalog seat is the SDK's entry: four optional verbs, so the old module's claim is not outgrown (ADR 0604 §1; F3) 2026-11-03 16:45:06 +01:00
jschoubben 39b3ce3c73 builder_builds_since names the operator's command nox builds as its reader, not mesh-catalog (ADR 0604) 2026-11-03 16:45:06 +01:00
jschoubben e1d0c9fe03 A merge that leaves a repository no module never deletes a module another trunk holds, and indexer_take_in takes a repository in (issue 1367)
dropRemoved hands the module over from the other trunk, read from the forge at the head the source names, so no wait in memory is needed. indexer_take_in with a repository takes in a head with no module.json as a merge that leaves none, and answers by name for a head with modules.
2026-11-03 16:45:06 +01:00
jschoubben 6bc16d74db Write and take in a merge that leaves no module.json, so a module-less repository's reach is not held for ever and a removed last module is dropped (issue 1126)
The forge bridge wrote no merged message for such a merge, so taken.<repo> never moved
and the view held 'the indexer has not yet taken in <head>' (hq #490-#492). The bridge
now writes it with no trees; the indexer drops every module of the repository on it,
after the same order check as any merged message. The view needed no change: with the
taken key moving it reaches the merge, which its new row proves.

Needs mesh-sdk fix/1126-no-module-reach (a merged message may carry no trees).
2026-11-03 16:45:06 +01:00
jschoubben 9f49bc92ad The seat mesh-catalog's four verbs answered from the indexer's modules, manifests and graph, with the old catalog's field names (captured answers as fixtures); served once the indexer claims the seat, in the release after L3's (ADR 0604 §1, §2) 2026-11-03 16:45:06 +01:00
jschoubben 4f6e1021c6 view_tiers: a source revision's tiers worked out from the module graph at each call, and per module what it waits on now, its build, each node's want and have and each provision it awaits, the nodes offline named away (ADR 0603 §6) 2026-11-03 16:45:06 +01:00
jschoubben 027c445bae Pin mesh-sdk 182d8757dba9, the group's head of feat/l3b-bus with the graph's ordering edges (ADR 0603 §1, §6) 2026-11-03 16:45:06 +01:00
jschoubben 7ba2668aa8 The check refuses a change whose module graph has a cycle and names it, worked out from the catalogue as the change leaves it; a cycle the trunk already has among the same modules is said, not the change's failure (ADR 0603 §2) 2026-11-03 16:45:06 +01:00
jschoubben a901b840a0 Pin mesh-sdk acad60547c84, and the indexer's graph nodes take their bases and provision edges from the contract's GraphEdgesOf (ADR 0603 §2) 2026-11-03 16:45:06 +01:00
jschoubben f70faed5b6 The indexer keeps the module graph as its state graph: each module's build and provision edges written at every recompute when they change, at most 2000 modules, a module's key deleted once the modules state no longer holds it (ADR 0603 §2) 2026-11-03 16:45:06 +01:00
jschoubben 5ee3b4c15f Pin mesh-sdk 25b371bc8397, the group's head of feat/l3b-bus with the module graph and the seat mesh-catalog's contract (ADR 0603 §2, ADR 0604 §1) 2026-11-03 16:44:47 +01:00
jschoubben 2f387a3f4c Pin mesh-sdk e7ce86ff62b5, the group's head of feat/l3b-bus (review round 2) 2026-11-03 16:44:47 +01:00
jschoubben 22aedbe97f A recompute with no merge writes the fingerprint's source revision as not known, so no release's Approve answers a build it never carried, though the source revision is still that release's (ADR 0601 §1; review round 2) 2026-11-03 16:44:47 +01:00
jschoubben 478f0110e9 Pin mesh-sdk 825483043549, the group's head of feat/l3b-bus (review round 1) 2026-11-03 16:44:47 +01:00
jschoubben 6d24922172 The node-engine's witness ping is granted by the tool runner's name its manifest gives, any service's instance on its own node while none says so, never a name in mesh-grants (ADR 0601 §4; review round 1, finding 4) 2026-11-03 16:44:47 +01:00
jschoubben 0fa00d5e6c The watchman raises what a node keeps of a module as it runs, for a manifest that does not yet say it, from the module's have, and clears it once the have says none (ADR 0601 §4; review round 1, finding 3) 2026-11-03 16:44:47 +01:00
jschoubben a41d2851c7 A release answers only the build whose fingerprint it moved: the indexer stamps the source revision at which a fingerprint last changed, a recompute with no merge the revision of its moment, and the assigner reads that stamp, never the Src a recompute keeps (ADR 0601 §1; review round 1, finding 1) 2026-11-03 16:44:47 +01:00
jschoubben ea5be10532 Pin mesh-sdk 09ec82cb3551, the group's head of feat/l3b-bus (ADR 0601) 2026-11-03 16:44:47 +01:00
jschoubben 6b9d211f72 The checker decides a node the sandbox did not judge by the contract's field (composer.Machine.NotJudged, verdict not-judged) and names it with the composer's words, never by a phrase in its reason (ADR 0593 §9; the prepare's seams finding on "not judged") 2026-11-03 16:44:47 +01:00
jschoubben b342c67b19 The grants part knows the tool runner by what its manifest says, never by its name, and keeps a node's tool runner user as the list in force holds it while no manifest there says it yet, said in the held condition (ADR 0601 §4; the prepare's seams finding on mesh-grants's own name) 2026-11-03 16:44:47 +01:00
jschoubben daf178c4ca A release's Approve answers the record ask of a move of the build alone to the build it carries: the assigner reads the release by the build's source revision from the gate's source and merges, and names it in the want; a move that changes a setting or a secret too is asked as before (ADR 0601 §1) 2026-11-03 16:44:37 +01:00
jschoubben a9aa6d29ea The assigner moves the bus module by its rules as any module, with no bar and no snapshot of its own, and a person may choose its policy as any module's: its node backs its streams up before it applies (ADR 0601 §1, §2) 2026-11-03 16:44:37 +01:00
jschoubben ecf96be8ce The judge's row for a field it does not know names a field mesh-sdk does not know now: core-credentials is one of its manifest words (ADR 0601 §4) 2026-11-03 16:44:37 +01:00
jschoubben ab3cb1ca9e The judge knows the tool runner by what its manifest says and keeps the kept streams itself, and this repository's check that no manifest names the ended controller reads them as text: mesh-sdk names no module (ADR 0601 §4) 2026-11-03 16:44:37 +01:00
jschoubben 054f089614 The bus has the upgrade policy its manifest says, as any module: the judge and the assigner rows read it from the manifest (ADR 0601 §1) 2026-11-03 16:44:37 +01:00
jschoubben 6301e49c73 Review round 2: a held module's object reads come from its running manifest; one with none keeps the list in force, said; indexer_judge asks the engine for the running build again (issue 774) 2026-11-03 16:44:37 +01:00
jschoubben 78fe1ce960 Review round 1: the indexer learns its build from the node-engine's seat; a held module's seats and hearings come from its running manifest; indexer_judge; one judge pass at a time (issue 774) 2026-11-03 16:44:37 +01:00
jschoubben 6645ac58c2 A manifest with a field the judge does not know waits instead of being refused; a held module keeps its bus users; tools list and redo the verdicts (ADR 0578 amended, issue 774) 2026-11-03 16:44:19 +01:00
mesh-tools c4470f42db Merge pull request 'Bus state by template: the judge's rules, manifest-accepted, the old-section count and the ledger account (issue 1966, B1)' (#146) from feat/bus-state into main 2026-11-03 15:24:36 +00:00
jschoubben 4bb790bedf Pin the group's mesh-sdk head f8bd12e in place of the local replace (issue 1966)
mesh/delivery merged: source.main moved to novox/mesh-delivery@c4470f42dba9
mesh/delivery-group group feat/bus-state merged: every member is merged
mesh/check pass 4bb790bedffe on 8f83608503da | unit 6460 run, 262 kept, 27 skipped | integration 6639 run, 264 kept, 35 skipped | over bound: unit, in…
2026-11-03 15:35:28 +01:00
jschoubben aa98729457 The zero rule takes the count of the accepted set the last pass held, and is not applied when it cannot be read (issue 1966 D4) 2026-11-03 15:28:58 +01:00
jschoubben 2fdb0516cd The checker's judge counts the old sections from the trunk case as the indexer does, so a change adding a section at 0 is refused at the merge check (issue 1966 D4) 2026-11-03 15:14:28 +01:00
jschoubben 28a6858863 The ledger account's subjects come from mesh-sdk's one naming of them (issue 1966 D5) 2026-11-03 15:13:38 +01:00
jschoubben 1ab0274547 The wanted accounts include the ledger kind, held in the tool runner's ledgers, in step with mesh-sdk's buscompose (issue 1966 D5b) 2026-11-03 13:39:21 +01:00
jschoubben 07af86b153 The grants part composes a ledger account per assignment of a module asking for a ledger, publishing only on its ledger histories and snapshots (issue 1966 D5) 2026-11-03 13:38:20 +01:00
jschoubben dd2e7c8e64 The indexer counts the modules on each old section, a tool reads it, and the judge refuses a section at 0 outside the foundation (issue 1966 D4) 2026-11-03 13:34:54 +01:00
jschoubben 756a3813e2 The indexer says manifest-accepted, with the bus-state entries whole, for a newly accepted verdict (issue 1966 D3) 2026-11-03 13:32:41 +01:00
jschoubben 9a508701ff One local name is one form, and the old state says what it accepts: 0 is taken as 1 (issue 1966 D2) 2026-11-03 13:31:42 +01:00
jschoubben 7ee7248fad The judge refuses bus state in requires that breaks a template's bounds, naming the entry and the field (issue 1966 D1) 2026-11-03 13:30:42 +01:00
mesh-tools 8f83608503 Merge pull request 'The checker waits its compose answer bound, not the SDK's 30 s, for the machines' compositions' (#144) from fix/1126-compose-answer-bound into main 2026-11-03 00:40:26 +00:00
jschoubben bedda86a22 Fold the call-bound check into the answer-bound settings test (test budget)
mesh/delivery merged: source.main moved to novox/mesh-delivery@8f83608503da
mesh/check being carried to bedda86a22c5 on 8f83608503da | from a62276bcafad
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-03 01:26:26 +01:00
jschoubben 0e26fd8ed5 The checker's calls to node-engines wait the compose-answer-bound, not the SDK's 30 s (issue 1126)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-03 01:13:36 +01:00
mesh-tools a62276bcaf Merge pull request 'Tests no longer race the wall clock; the checker invokes compose-candidate again' (#143) from fix/timing-tests into main 2026-11-02 22:17:53 +00:00
jschoubben 34a607a1f5 Give the checker back its compose-candidate invoke, now that the new indexer runs
mesh/delivery merged: source.main moved to novox/mesh-delivery@a62276bcafad
mesh/check being carried to 34a607a1f5cf on a62276bcafad | from 15d28ddf5bd3
The invoke was left out (mesh-delivery #142) only while the indexer built before the verb ran; the new indexer and the
node-engine's compose-candidate run on every machine since 22:57, so a check composes on the machines again instead of
passing with a warning (ADR 0593 §7, §9).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 23:04:44 +01:00
jschoubben f73b0bf6f6 Tests wait on events or the clock they are given, never on milliseconds a loaded machine outlasts
The check of 2026-11-02 22:35 on a loaded shanks failed two buildrun tests on wall-clock bounds of 60 ms, the
second failure of this kind tonight (the first: mesh-sdk's busretry tests, f02f142).

Quick fix: give the two buildrun asker tests bounds of seconds.
Thorough fix (chosen, because the kind recurs across every module of the repository): every test that raced a
wall clock now waits on an event, steps a clock it is given, or has a bound only a failing test reaches.
- buildrun asker tests: the steps' own times stand for the clock (bound an hour, starts and phases written in the
  past), no goroutine paces phases with sleeps.
- builder: the decline bound reads Builder.Now; the decline test steps it; the stream-wait test waits for five
  check reads instead of 100 ms.
- checker: the lost-ask try bounds an untaken ask at 200 ms only where that is what ends it; the program says when a
  merge turn changes, and the integration test waits on that line instead of 300 ms.
- teller: the bed makes the teller's own ends bucket (a 10 s stall at start the tests raced by 0.5 s), waits for its
  warrant subscription instead of 500 ms pauses, and the package-level now is restored with t.Cleanup.
- watchman: the recount, run in its own goroutine, is waited for instead of 10 ms.
- polls of 2-5 s become 10 s (unit) and 20 s (integration).

Under a throttled cgroup (half a core shared with two busy loops) beside the node-engine's suite, the buildrun
tests failed 5 of 20 runs before and passed 30 of 30 after; no test added.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 23:04:44 +01:00
mesh-tools 15d28ddf5b Merge pull request 'mesh-delivery-checker: leave compose-candidate out of its invokes until the new indexer runs' (#142) from fix/checker-invoke-later into main 2026-11-02 21:55:29 +00:00
jschoubben 22f3658a36 Leave compose-candidate out of the checker's invokes until the new indexer runs
mesh/delivery merged: source.main moved to novox/mesh-delivery@15d28ddf5bd3
mesh/check checking 22f3658a36a6 on 15d28ddf5bd3
The running indexer, built before the verb existed, refuses a manifest invoking it, and one refused manifest holds the
grants part's whole list: no new grant is written, so the new indexer never moves (issue 1126, 2026-11-02). Quick fix,
chosen because the thorough one (the grants part keeps only the refused module's grants) cannot be delivered while the
list is held; a follow-up adds the invoke back. Until then a check's compose-candidate call is refused on the bus and the
node is named silent: the check passes with that warning (engines.go, ADR 0593 §9).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 22:22:23 +01:00
mesh-tools 073e3fab65 Merge pull request 'sdk1: the indexer fingerprints by imported content; the checker asks the node-engines to compose' (#141) from feat/sdk1 into main 2026-11-02 20:42:16 +00:00
jschoubben 2b5a7fe831 Pack the checker's settings template as its files archive, or the indexer's judge refuses the manifest
mesh/delivery merged: source.main moved to novox/mesh-delivery@073e3fab65db
mesh/delivery-group group feat/sdk1 merged: every member is merged
mesh/check checking 2b5a7fe831c1 on 511e4b741497 | novox/mesh-delivery#141 in group feat/sdk1
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 21:19:38 +01:00
jschoubben 3b3b096be8 Place mesh-networking beside a node-engine check, so route-proxy's proofs of a composition are judged once this release runs
mesh/delivery-group group feat/sdk1 rejected: composed together: the checker's verdict at the members' heads (5fc952f974ca): mesh-build-go
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: mesh-build-go: the indexer's judge at the group's mesh-delivery head 3b3b096be8cdf8f49246ed3961ece3eb5c349d62 refuses the manifest …
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 20:40:01 +01:00
jschoubben 6313f3ea10 Place the node-engine beside a catalogue check, so mesh-build-go's composer test is judged once this release runs
The composer command moved to the node-engine (ADR 0593 §7); a catalogue check had only mesh-sdk beside it. Pin the build runner to the catalogue head whose composer test skips aloud until then.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 20:36:19 +01:00
jschoubben 712d136fe6 Pin the build runner to the release's new mesh-catalog head
mesh/delivery closed: a newer head of the same pull request
mesh/delivery-group group feat/sdk1 checking: 6 of 7 member(s) ready
mesh/check failed: integration: meshbuildgo/cmd/mesh-build-go.TestAComposeAskIsBuiltOnceAndAnsweredOnTheBus: NOT CHECKED: the node-engine's composer c…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 20:23:41 +01:00
jschoubben 09ac178888 Pin mesh-sdk at f02f142, the sdk1 head whose tests no longer leak package globals, so the group's check builds against it
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 20:14:55 +01:00
jschoubben d4600efd7d Pin the build runner to the release's mesh-catalog head, computed by go
mesh/delivery closed: a newer head of the same pull request
mesh/delivery-group group feat/sdk1 checking: 6 of 7 member(s) ready
mesh/check failed: integration: meshbuildgo/cmd/mesh-build-go.TestAComposeAskIsBuiltOnceAndAnsweredOnTheBus: NOT CHECKED: the node-engine's composer c…
The placeholder was written in local time and go refused it as a pseudo-version.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:41:56 +01:00
jschoubben 0edd0bca74 mesh-delivery-builder: fold the step-choice and step-run tests into two tables, within the test budget
Review finding 5: the repository's unit tests rose from 810 to 814 (over the 500 budget); eleven test
functions of buildrun become two, so the count is 805.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:38:09 +01:00
jschoubben 2fb1dc3214 mesh-delivery-checker: a placement that cannot be read leaves the composition not made, said
Review finding 4: an unreadable placement gave no nodes, so nobody was asked and the check passed. The
placement read now answers its error, and the verdict says the composition was not made and why.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:37:22 +01:00
jschoubben e5ce35cd00 mesh-delivery-checker: a composer at the node-engine's head that cannot run fails the composition
Review finding 3 (ADR 0593 §9): only a node that does not answer may let a check pass with a warning; a
sandbox composer that cannot be built or run was filed with the silent nodes. It now fails that node's
composition, naming the head.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:36:42 +01:00
jschoubben e31da597bb mesh-delivery-indexer: the version line takes a directory-replaced sibling by its content, so it never goes stale
Review finding 2 (ADR 0593 §2, §3): after the release no build has reported imports, and a module that
replaces a mesh Go module of its own repository by a directory had no go.sum line and no version for it,
so a change of buildstep or buildrun alone moved no importer. The indexer now reads go.mod's directory
replaces, publishes those modules, and puts their content digest on the version line; builds are pinned
to the other repositories' versions alone.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:35:58 +01:00
jschoubben 33675f548a Pin mesh-sdk at the sdk1 group's head 1c7356c, so the group's check builds every module against the narrowed SDK it was written for
The build runner's pin stays at its placeholder until mesh-catalog's final head is pushed.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:33:10 +01:00
jschoubben 008a85b73c mesh-delivery-indexer: the judge's tests ask nothing of the composition's offers, overflow or left-out reasons
ADR 0593 §7: OffersIssuedOutside, Resolution and UnknownFieldReason left mesh-sdk with the composition,
so the judge's tests keep its own reading and refusals and those proofs go to the node-engine's tests.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:23:17 +01:00
jschoubben 6fb743f521 mesh-delivery-indexer: the judge's tests judge manifests and compose nothing
ADR 0593 §7: the catalogue's composition (Resolution.Compose and Declaration, LeftOut, Resolve,
JudgeSettings, ApplySettings, HeldTemplateProblem) leaves mesh-sdk for the node-engine, so the judge's
tests keep only its verdicts and refusals; each proof that a manifest composes goes to the node-engine's
tests, and the helpers only those proofs used go with them.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:08:41 +01:00
jschoubben c0209b3d8e Builder's fetch test takes the named tree as data, not computed
The named tree is the indexer's computation (ADR 0593 §2): the builder reads it
from the module's value, so its test gives one as the indexer would write it.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:00:57 +01:00
jschoubben c12804f4d9 mesh-delivery-indexer: its integration test no longer reads verdicts through mesh-sdk's composition reader
ADR 0593 §7: compose and frombus left mesh-sdk for the node-engine, so how a composer holds a refused
module is the engine's to test; the indexer's test keeps proving the verdicts and refusals it writes.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:00:16 +01:00
jschoubben fbb50f8fcd mesh-delivery-checker: run check asks in its tests with the Go build module's real runner
ADR 0593 §1: the step runner left mesh-sdk for mesh-build-go's buildstep package, so the examiner's unit
tests and the module's integration test require it by the group's version (as the builder does) rather
than an imitation of its plan and kept-verdict rules.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:58:18 +01:00
jschoubben 0ce3409141 mesh-delivery-checker: make a check's composition from every placed node's engine answer
ADR 0593 §7, §9: the checker asks compose-candidate of each placed node in parallel within its own setting
compose-answer-bound (60 s); a refusal fails the check naming node and module, a silent, unready or
unreachable node passes with "not composed on that node: no answer". A group with a node-engine member is
composed by the composer at that head on the Go build seat over each node's input. The wait on the
assigner's compositions goes, and the checker invokes the one engine verb it needs.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:20 +01:00
jschoubben 46e53a81c2 mesh-delivery-checker: build mesh packages at the versions the indexer publishes instead of computing them
ADR 0593 §2: only the indexer computes a package's version; the checker follows its packages state and
hands those commits to a Go check, so LatestPackages leaves repoModules and mesh-sdk's
PackageVersionsOf has no reader here.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:20 +01:00
jschoubben 7cf47c04eb mesh-delivery-assigner: compose nothing; judge proposals with its own judge and compare the published compositions
ADR 0593 §7, §8: the check's composition, its compose asks to the Go build seat and the state
compositions go (the checker asks the engines); a proposal is judged without frombus; mesh-wants.upgrade
compare reads every node's composed-<node> through mesh-host.composed. The assigner no longer imports
compose, composer, frombus or buildrun.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:20 +01:00
jschoubben 8855735341 mesh-delivery-assigner: the placement judge is the assigner's own, judging a proposal from placement alone
ADR 0593 §1, §8: compose/judge moves back from mesh-sdk; every node answers a proposal, what a placement
changes is read from its entries, and a node silent past the answer bound (60 s) is named and the
proposal decided on the answers it has.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:20 +01:00
jschoubben c62a501bf0 mesh-grants: a node reads the indexer's candidates and verdicts whole, as its engine reads a key through a short consumer
ADR 0593 §7: compose-candidate's read of one key times out without the consumer grants, so both buckets
are granted as the builder's builds are; the node grant now equals mesh-host's golden node.json.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:19 +01:00
jschoubben 0b03b9c7c5 mesh-grants: create a module's object buckets and history streams, grant their readers read-only, and warn at 75 percent
ADR 0592 §5, §6, §9, §25 (the grants part's share by ADR 0593 §10): an object bucket and a stream that
keeps its history are created create-only with their caps and no age limit, refusing new data when full;
the owner alone writes its bucket and a module its grant names reads it; the part raises
mesh-grants.history-full once any passes 75 percent. Each engine also reads the indexer's manifest
verdicts by one key for compose-candidate.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:19 +01:00
jschoubben d283efdc9f mesh-grants: the machine's runtime hears a node's compositions bucket made later for the bundles it carries
ADR 0593 §6: a bundle such as the provisioner reads mesh-host.composed on the runtime's connection, so the
runtime account gets the stream listing and the creation advisory beside the read of each bucket.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:19 +01:00
jschoubben 74304052b4 mesh-grants: every node's engine writes its own compositions and the named readers read them all
ADR 0593 §6, §7: the grants part creates composed-<node> per node, grants each engine the writes of its
own bucket and a direct get of the indexer's candidates for compose-candidate, and grants
mesh-host.composed read-only, with the stream listing and creation advisory a per-node read needs, to
the provisioner, the assigner and the view alone; memberships issue it per node.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:19 +01:00
jschoubben 2a91caddaf mesh-delivery-checker: place no sibling for the controller's repository, which this group empties
ADR 0593 §11: the controller's repository holds nothing that builds once route-proxy moves, so its
sibling rule goes; its tests fold into the sibling test, which now uses mesh-sdk's rule.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:19 +01:00
jschoubben 78fedabeb1 mesh-grants: own the bus's composer, so a change to who may say what rebuilds only the grants part
ADR 0593 §1: buscompose's engine moves from mesh-sdk into mesh-grants/internal/buscompose, its golden
user lists unchanged; the controller's raise and assert code is left behind, and the merged switch
test runs inside the part's whole integration test.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:19 +01:00
jschoubben 5c20b9ef15 mesh-delivery-checker: the line test holds ADR 0591's case of a first failure longer than the forge's limit
The first failure's words are shortened; "failed (N)" and "and N-1 more" stay whole within the limit.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:19 +01:00
jschoubben fa85de8ef6 mesh-delivery-checker: write a member's preview whenever its fetch merged, and do not wait on a composition that cannot come
Review of 650a1d0: a member that only failed its tests got no preview, so the indexer held no manifest
of it, the assigner's composition only said it waits, and the check waited for ever. The preview is now
written whenever every build module's fetch merged; a member with no preview of a merge (a conflict, a
fetch that did not answer) makes the composition not run, said with the members it names, unawaited.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:19 +01:00
jschoubben 290e1696b2 mesh-delivery-checker: a check runs every member, level and the composition to its end and names every failure
The operator, 2026-11-02: "A failed check shows only its first fault". Group feat/1126-removal's
check failed four times, each verdict naming one fault, so three faults took three runs of 20-40 minutes.

The checker no longer returns at a member's first failure: every member is asked, every level's
failures and every member's refused step or conflict are joined in judging order, the manifests are
judged and the composition is read also when the tests failed, and a failed composition is added after
them. The status line says how many failed and names the first, cut so "; and N more" stays whole.
checker_verdicts and checker_pull answer the whole list (failed). Needs mesh-sdk's buildstep change
(every level runs after a failure) pinned at the group's head.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:19 +01:00
jschoubben b7551d2efb Indexer's tests compute with its own fingerprint package
So mesh-sdk can drop SourceFingerprint2, NamedTree and NoPackageVersion (SDK1)
without breaking them.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:41:22 +01:00
jschoubben 2e91f0b061 Fold the release's new tests into existing ones to stay within the test budget
mesh-delivery is over its budget (810 unit, 73 integration on main), so this
branch adds none net: its new cases are subtests, the three work-queue process
tests and the consumes test become one, the history stream bounds join the
stream values table, and the go/-only mesh-sdk merge check joins the one new
integration test. buildrun's 20 unit and 1 integration tests moved in from
mesh-sdk with it.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:40:57 +01:00
jschoubben 61b2b718e6 View reads the indexer's published packages with indexer_packages
A module's fingerprint takes the digests of the packages its last build
imported, so the published packages say why a merge moved a module or not;
the state needs a read tool like every other (ADR 0593 §2; D2).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:38:58 +01:00
jschoubben 2e23b0b736 Prove against the bus that a mesh-sdk merge moves only importers of a changed package
The indexer whole: two catalogue modules report their imports at their builds'
end, mesh-sdk merges a change of buildstep alone, the packages key names the
merge with a digest per package, the buildstep importer moves and the other
keeps its fingerprint though the version line would have moved it (ADR 0593
§2-§4; D11).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:37:37 +01:00
jschoubben b81e05387b Judge says its rules are its own
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:35:17 +01:00
jschoubben 525d7374b2 Judge object buckets and history streams as ADR 0592 decides
The indexer's judge takes a module's own object bucket (a plain local name,
once, no name its states take, none of the mesh's derived prefixes, 1 to 8192
MiB with no default, a grant only by a trusted value) and a stream that keeps
its history (no max age, deliveries or duplicates), counts such a stream among
the bus's predefined forms, gives a bucket's name one owner on the bus, and
refuses a read of a bucket its owner grants nobody. The seat table expects
the node-engine's compose-candidate verb (ADR 0593 §7; D5).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:34:54 +01:00
jschoubben 4c4380b94b Builder's integration test pulls asks with mesh-build-go's runner
The pulling code leaves mesh-sdk for mesh-build-go's own package (ADR 0593 §1),
so the bed imports it by its published version and tests the builder against
the real runner, not an imitation. The required version is a placeholder at
mesh-catalog 9c57f7af: the group pins the catalogue's head before its push.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:33:31 +01:00
jschoubben f6fa75e189 Builder's unit tests run an ask with a local runner, not mesh-sdk's
The build runner leaves mesh-sdk for mesh-build-go (ADR 0593 §1), another
repository the builder does not import; the tests answer asks as the builder
needs them answered.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:31:07 +01:00
jschoubben 604f1528d6 Builder owns the step runner; a build's imports reach its done key and log
buildrun leaves mesh-sdk for the builder's own Go module (ADR 0593 §1); the
checker, assigner, indexer and watchman require it by a replace to the sibling
directory. Issue 1930's bound was already on mesh-sdk's main and moves with it.
The builder copies the imports a build module answers to the done key and the
log's end entry, where the indexer keeps them (§2; D7).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:29:56 +01:00
jschoubben 2aa43ec249 Bridge reads no mesh package from a go.sum
Only the indexer computes what a fingerprint takes (ADR 0593 §2): the merged and
pull messages carry no Packages, and the kept inputs no longer hold them (D6).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:26:53 +01:00
jschoubben fba98ee80f Indexer computes fingerprints alone, over the packages a module imports
The fingerprint, named tree and package versions move from mesh-sdk into the
indexer's internal/fingerprint (ADR 0593 §2). The indexer publishes each reached
mesh Go module's package digests from the forge's listing in its new packages
state, keeps the imports a done build reports, and writes the import form only
when an input differs from the build that reported them (§4), so the form change
costs no rebuild and a package merge moves only its importers (§3). Candidates
take the same form against the group's member heads (D1-D4).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:25:57 +01:00
mesh-tools 511e4b7414 Merge pull request 'The grants part and the indexer without the controller; consumers far behind raise a condition (issue 1126 group B)' (#138) from feat/1126-removal into main 2026-11-02 13:41:16 +00:00
mesh-tools 4dd7dcfc14 Merge pull request 'A group check places its own catalogue head beside the other members (issue 1126)' (#139) from fix/1126-group-catalogue-beside into main 2026-11-02 12:33:04 +00:00
jschoubben d26781b403 Place a group's catalogue head beside its other members' tests (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@4dd7dcfc1411
mesh/check being carried to d26781b403df on 4dd7dcfc1411 | from be88162cde31
A group's check judges each member against the group's heads (ADR 0482 §2,
ADR 0573 §4), but the checker placed mesh-catalog at its trunk beside every
other member even when the group held a catalogue pull request, so
mesh-delivery's judge tests in group feat/1126-removal judged the new judge
against catalogue main and failed. The catalogue now follows the rule of
every other sibling: the member's head and ref in a group, the trunk alone.
A catalogue change's own trunk copy (ADR 0529 §2) is placed separately and
is unchanged.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 13:12:54 +01:00
jschoubben e2cb398186 Pin mesh-sdk to the group head of issue 1126
mesh/delivery merged: source.main moved to novox/mesh-delivery@511e4b741497
mesh/delivery-group group feat/1126-removal merged: every member is merged
mesh/check checking e2cb3981866e on 4dd7dcfc1411 | novox/mesh-delivery#138 in group feat/1126-removal
Every member builds against mesh-sdk abf5eac, the head of feat/1126-removal, so the group is checked as one contract.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:27:33 +01:00
jschoubben 6a4d88265d Say mesh-grants.consumer-behind again only when the consumers behind change, not their counts
The counts change every pass, so a consumer that stays behind was said to the operator again at each pass. The
condition names the consumers alone; how far each is stays in the last pass.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:23:23 +01:00
jschoubben 655692c7ba Pin mesh-sdk to the group head of issue 1126
Every member builds against mesh-sdk f569924, the head of feat/1126-removal, so the group is checked as one contract.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:20:14 +01:00
jschoubben 8f9766e46c Let the grants part say an account consumer far behind as mesh-grants.consumer-behind
The self-check's bus-consumer probe judged consumers the controller defined, and with the controller ended it judges none
(issue 1126). The grants part owns the account consumers, so it says when one stays more than 1000 messages behind its
stream's head at two passes running, the probe's bound and confirmation, and clears it once none is.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:03:29 +01:00
jschoubben 83c1b3ce38 Pin mesh-sdk to the group head of issue 1126
Every member builds against mesh-sdk 729a86b, the head of feat/1126-removal, so the group is checked as one contract.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:56:55 +01:00
jschoubben 000a24d261 Match the judge's copy of mesh-wants' upgrade to mesh-sdk (issue 1126)
mesh-sdk now says what upgrade's compare answers without the controller:
each node's own composition and the assigner's findings. The judge keeps
a copy of the seat, which must read the same.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:49:17 +01:00
jschoubben 3228d911cb Let a module move between repositories by the merges that move it (issue 1126, route-proxy)
route-proxy leaves the controller's repository, which is archived, and nothing registered a move since the controller
went: the indexer left a module named by another repository's merge as it was, forever. Now that merge waits, and the
holding repository's merge that drops the module hands it over in place, so its key never goes missing between the two;
merged the other way round, the key is deleted and written new. A trunk that still holds the module keeps it (ADR 0420
clause 7, ADR 0501 clause 3).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:48:34 +01:00
jschoubben 7e358b0f1d Recognise the control node by the bus module, not the controller (issue 1126)
The watchman held a node a control node, urgent after half an hour
silent, while its have named mesh-controller; the controller ended by
its unassignment (ADR 0590), so no node was one any more and the bus
node's silence stayed a warning. The control node is now the node whose
have holds the bus module, nats, which the watchman already hears by
that name.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:47:53 +01:00
jschoubben 8d4c13427a Build against mesh-sdk without the controller (issue 1126)
mesh-sdk's group B member renames the grant to Grant, drops the
controller principal, the signer and the lease fields from the user
list key, and the keeper's and config's take-over verbs. The grants
part, the assigner and the view use the new names; the judge's copies of
the health and config seats follow; the tests' fixtures lose the
controller's user, and the refused principal is one removing what the
closed set does not name, the controller's mirror being gone.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:46:12 +01:00
jschoubben 2049b1a370 Say the assigner's conditions as findings alone, the keeper being the one source (issue 1126, B8)
ADR 0590 clause 3 ends the choice of a condition source by the keeper's
take-over key, which mesh-sdk no longer defines. The assigner watched
that key and said every condition straight to the router while it named
the controller. It now writes each as a finding the keeper carries, and
says one straight only where it has no findings bucket at all.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:46:12 +01:00
jschoubben 43d4f511c5 Drop the lease write's findings, the controller's artifacts-gone and the issuing gate (issue 1126, B9)
ADR 0590 supersedes the lease write: mesh-grants says no lease event any
more, so the watchman's lease-altered and removal-failed findings and
their row had nothing to hear. The ended controller's consumers are
removed as hand acts, so their deletion is no consumer lost; only the
one on DEAD_LETTER_NOTICES is still read, since its removal hands the
notices to the watchman. The builder hears artifacts-gone from the
artifact sweep seat alone, and mesh-enrolment's tokens no longer report
an issuing gate that nothing can turn off.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:35:55 +01:00
jschoubben f7f3518a04 Take the controller out of the judge, the checker's siblings and the assigner (issue 1126, B8)
The controller ended by its unassignment (ADR 0590) and mesh-sdk drops
its seat, the grants seat and placement's adopt and hand-back. The
indexer's judge still carried the controller's seat entry, the old
removal words and the two placement verbs, and counted 58 seats; the
checker still placed mesh-controller beside the node-engine's and the
catalogue's tests and failed a node-engine check without its trunk; the
assigner still asked a compose of the running controller, served adopt
and hand-back as stubs, and kept a policy-split finding that could no
longer arise.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:32:56 +01:00
jschoubben d4aa1fe11b Hear the keeper alone and stop reading the controller's lease in mesh-grants (issue 1126, B7b)
ADR 0590 §3 ends the choice of a condition source by the keeper's
take-over key: the controller is unassigned and the keeper is the one
source. The part still read the take-over key, fell back to the
controller's condition events when it was absent, and read and watched
the controller's lease, which nothing writes any more. It now hears the
keeper's events alone and reads neither.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:27:19 +01:00
jschoubben 14046f9b14 Remove the lease write, take-over and their tools from mesh-grants (issue 1126, B7a)
The controller ended by its unassignment (ADR 0590), which supersedes the
lease write and its hand-back whole and makes the grants part's list the
only one the bus node follows. The lease write, its check, the removal
marker, the take-over and hand-back asks, their five tools, the seat
claim and the lease and takeover states had nothing left to act on; the
membership notice on its subject had no reader. A whole-module test on
the bus replaces the lease write's: with no controller at all, the part
publishes a membership in its state alone, makes the module's consumer
and writes the key the bus node follows.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:25:59 +01:00
mesh-tools be88162cde Merge pull request 'Without the controller: mesh-grants makes consumers, enrolment issues, the watchman keeps dead letters, and the controller cannot be placed again (issue 1126)' (#137) from fix/1126-gates-delivery into main 2026-11-02 09:51:14 +00:00
jschoubben a192052d82 Refuse any placement of the controller (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@be88162cde31
mesh/check being carried to a192052d82fe on be88162cde31 | from 0490043990e2
The grants part, the enrolment seat and the watchman now act without
reading the lease, so a controller placed again would write beside them
with nothing saying so. The assigner refuses every proposal, seed or
joining node's write that would place mesh-controller, naming why.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:38:22 +01:00
jschoubben 1dae2de835 Issue enrolments and keep dead letters without the lease (issue 1126, A2)
The controller ended by its unassignment, so its lease key is absent for
good and mesh-sdk reads that as not handed away: mesh-enrolment's issue
stayed off and the watchman never took DEAD_LETTER_NOTICES, whose notices
age out after a week. Both now act without reading the lease; the
watchman still waits, and says so, while the controller's consumer on the
notices stands. Neither manifest reads the lease any more.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:35:40 +01:00
mesh-tools 0490043990 Merge pull request 'view_reach answers one module's delivery reach' (#136) from fix/view-reach-filter into main 2026-11-02 09:33:51 +00:00
jschoubben e094e362f4 Run mesh-grants' handed behaviour without the lease (issue 1126, A1)
The controller ended by its unassignment and its lease key expired, so
every gate on "the lease is handed away" stayed shut for good: no account
consumer was created or matched, grant-missing was never raised, the
enrolment users kept the controller's subject, memberships were still
said on their subject and the sealed key never said LeaseHanded. The
handed branch now always runs, and a hand-back is gone.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:33:14 +01:00
jschoubben 8850a5673e view_reach takes a module, so one module's reach is asked without reading the whole repository
mesh/delivery merged: source.main moved to novox/mesh-delivery@0490043990e2
mesh/check being carried to 8850a5673e61 on 0490043990e2 | from cc9c681fa348
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:24:54 +01:00
mesh-tools cc9c681fa3 Merge pull request 'The lease write stops the controller on its placed node and reads the operator's answer from the right bucket (issue 774)' (#135) from fix/774-lease-write-node into main 2026-11-02 08:52:47 +00:00
jschoubben 8f87dbe94d mesh-delivery-teller: read asks from the router's bucket the grant names
mesh/delivery merged: source.main moved to novox/mesh-delivery@cc9c681fa348
mesh/check being carried to 8f87dbe94d76 on cc9c681fa348 | from f1c33439292d
The teller read the router's record from KV_asks, a bucket no composed grant
names, so every read was denied and its operator answers could not be read
back. It now reads messenger_asks, as mesh-grants does. The integration bed
grants the real subject and reads a record on the teller's credential, so it
no longer agrees with the mistake. (issue 774)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 09:44:26 +01:00
jschoubben 7cee3842b8 mesh-grants: stop the controller on its placed node and read asks from the router's bucket
The lease write addressed node-service-manager at the lease holder's host, a
hostname no node carries ("novox-be" on novox), so the operator's yes timed out
at its first step and failed without a log line. The node now comes from
placement (exactly one node placing mesh-controller, else refused by name), a
failed act is logged beside its condition, and the asks record is read from
messenger_asks, the bucket the composed grant names, instead of asks, which no
grant allows. The fixtures give the lease a host unlike the node, so the tests
no longer agree with the mistake. (issue 774)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 09:42:19 +01:00
mesh-tools f1c3343929 Merge pull request 'mesh-enrolment holds the enrolment seat; placement takes add-node and remove-node; the grants part composes enrolling and rotating nodes (issue 774)' (#134) from feat/l3-enrol into main 2026-11-02 07:56:39 +00:00
jschoubben 63c8687166 Let a bus module's tools bundle declare what it invokes (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@f1c33439292d
mesh/delivery-group group feat/l3-enrol merged: every member is merged
mesh/check failed (2 tests): integration: mesh-mail-reader/cmd/mesh-mail-reader.TestAcceptanceOneMatteringMailSendsExactlyOneTellNamingIt: timed out w…
The judge refused nats's manifest for its invokes (seat:mesh-hand-acts.hand-act,
which nats_bucket_expiry asks to record its hand act), and with one manifest
refused the grants part holds the whole bus user list, freezing every build
that adds bus permissions. The bundle asks through the node's tool runner,
which buscompose grants every tool and seat verb, so the invokes are granted:
the same condition that already lets the bundle declare emits (ADR 0357).
ADR 0235's rule stands for a bus module with no bundle.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 02:21:05 +01:00
jschoubben b13f65674b Pin mesh-sdk at dff0ce2, the enrolment group's rebased head, so every member builds the same contract (issue 774)
mesh/delivery-group group feat/l3-enrol checking: 4 of 4 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking b13f65674b35 on 9125fcc71206 | novox/mesh-delivery#134 in group feat/l3-enrol
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:48:53 +01:00
jschoubben e6ee939bd4 The indexer's closed seat set counts fifty-eight with mesh-enrolment (ADR 0587 §1)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:48:53 +01:00
jschoubben bde60130a8 The assigner's add-node test writes no lease: since the removal group the assigner reads none and its bed has no lease bucket
The enrolment's issue waits for the lease write itself (ADR 0587 §4); the assigner takes an add-node on its warrant at any time (ADR 0567 §8). (issue 774)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:48:53 +01:00
jschoubben 795724f5ca mesh-enrolment: catch up only on an ask answered yes, and drop the address key first at confirm, decline and refused add
ADR 0589 version 29, clause 6. At start and after a catch-up an address key is an expiry never heard only when its
enrolment key is gone and the router records its ask answered yes; one declined or not answered yet is no expiry, and one
not answered stays until its ask ends. A confirm, a decline and a refused add delete the address key before their other
steps, so one stopped midway never leaves a key the catch-up would take for an expiry and unplace a node that joined. A
refused add (mesh-placement's refusal of the proposal, not a call that did not reach it) now ends the enrolment as refused;
a call that failed still waits for again. Tests: rows in the end table (after a confirm stopped before its enrolment key
went, after a decline, after a refused add, a declined ask, an ask not answered: nothing unplaced) and the integration test's
holder-was-down case reading the yes; no new function.

(issue 774)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:45:53 +01:00
jschoubben 44fee9eb0e mesh-enrolment: end an enrolment that expired unseen from its address key, taking the node out of placement
ADR 0589 clause 6: a placed but unconfirmed node is always taken out again, on the warrant of its ask. The address key now
names the ask and the issue's time (delivery.EnrolmentAddress, mesh-sdk c7130e3), written just before the ask is raised and
with no expiry of its own, so it outlives the enrolment key; the holder deletes it at every end. An expiry, seen or not, ends
the enrolment in this order, each step safe to repeat: remove-node on the ask's warrant (only when placement holds the
node), enrolment-expired naming the rebuilt issued event (delivery.EnrolmentIssuedID), the vault's drop of the token, and
the address key last. Reconcile finds every address key that names an ask and whose enrolment key is gone (expired while
the holder was down, before its watch caught up, or an end that stopped) and ends it; it runs once the watch has caught up,
on every expiry, and again with a growing wait while a step fails. An address reserved by an issue under way (no ask yet) is
left alone. Tests: rows in the end table (expired unseen; the holder down with a drop that fails once; a reservation under
way) and the integration test's expiry case (the address key goes; a holder that was down unplaces on its pass); no new
function.

(issue 774)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:45:53 +01:00
jschoubben cbcbb9644b mesh-enrolment: keep the tunnel key in delivery.Enrolment's own field
mesh-sdk 22c03af carries the machine's tunnel key on the enrolment (OverlayKey, overlay-key, required at every stage), so the
holder's wrapper that kept it beside the contract is gone; the wire is unchanged. Test fixtures give the seeded enrolments a
real tunnel key, which the contract now requires.

(issue 774)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:45:53 +01:00
jschoubben d1a256b429 mesh-grants: give the enrolment fixture its tunnel key, which an enrolment now requires
mesh-sdk 22c03af (feat/l3-enrol) requires overlay-key on every enrolment; the fixture of the enrolment-user subtest carries one.

(issue 774, review round 1b)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:45:53 +01:00
jschoubben 40bbc7e0d3 mesh-enrolment: take the tunnel key at issue, place the node on the yes, take it off again at an unconfirmed end
The operator's decision of 2026-11-02 01:05, on mesh-sdk 4224665:
- issue takes overlay_key, the machine's tunnel public key as `nox-mesh-host key` prints it, and refuses a missing one. The
  token's first tunnel is whole: key, address, range (the hub's tunnel range in placement, the one the controller's token
  carries today; a hub with none is a finding, never a guess), hub key and hub endpoint.
- On the operator's yes the node is proposed into placement at once (add-node with its tunnel key alone, on the warrant), and
  only then is the token made, so the hub peers it before redeem. Redeem writes no placement: it refuses a tunnel key other
  than the one issued for, or a node placement does not hold yet, and the certificate's call carries all four keys presented.
- A revoke, a refusal or an expiry before confirm takes the node off placement (remove-node, same warrant) before the end is
  said, and the end brings the token's drop.
- The assigner applies a remove-node: the node's key deleted at once, with no ask, placement-changed carrying the warrant.
- The tunnel key is kept as one more member of the node key's JSON, beside delivery.Enrolment, until the contract carries it.
Tests: rows in the existing tables and both integration tests (the token's Missing() is empty; the node is placed before
redeem and leaves placement when its enrolment expires); no function added, mesh-delivery stays at 816.

(issue 774)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:45:53 +01:00
jschoubben 70f13a3775 mesh-enrolment: place before the vault is asked, read the grants pass whole, name the expired enrolment's issued event, carry the first tunnel
- redeem writes the node into placement before it asks the vault for the account and the certificate of the serving key
  presented (the vault reads the node's address from placement); a retry by the same keys still finishes (blocker 3).
- loadedVia reads mesh-grants.user-list's pass as the part's whole Result, the list state under user_list; the fixture
  testdata/grants-pass.json was written by mesh-grants' own Result, not by hand (blocker 4).
- enrolment-expired is delivery.EnrolmentExpiry with IssuedID, through its codec; a key that expired before its content was
  seen names no issued event, so nothing is said and its token is dropped at once (blocker 6).
- issue builds the joining node's first tunnel from placement (the hub, its endpoint, the node's address) and the hub's
  record (its tunnel key) and passes it to enrolment-token; no hub, or a hub with no key or endpoint, is a finding of issue
  (ADR 0587 §3, insight of 2026-11-02).
- The assigner takes an add-node only from mesh-enrolment's own bus user (stdio.Callers), never person.mesh-enrolment.
- The watchman raises machine.<node>.node-account-fallback from a node's health report's fall back and clears it when the
  report no longer carries one (ADR 0583 §6, blocker 7).
- mesh-enrolment's ten unit test functions are two table tests now (blocker 8): the repository is back at 816.

(issue 774)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:45:44 +01:00
jschoubben 189e64c486 mesh-enrolment: its integration test, whole on a real bus
The holder's code over a real nats-server bucket with a time to live per key: issue, the operator's yes, redeem as enrol.ace,
the node confirming as the login its key of node-logins names (the stand-in vault writes it at node-account), node-enrolled
and placement-changed said on their subjects, the user enrol.ace composed while the key is live and gone with it, the token
dropped only after the end; and a token never redeemed expires on the bus, is heard, says enrolment-expired and is dropped.
Found by it: a bucket with a time to live per key must keep one revision per key, or the bus expires nothing (nats-server
2.11.17), so the state enrolments declares no history. Paid for: the integration test is paid by merging the builder's
archive-only and root-module integration tests into one table test; the module's ten unit tests by merging the assigner's
three small composition tests into one table (net 2 of 10 paid; the rest is for the lead, the repository being over budget).

(issue 774, 774/S60)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:45:44 +01:00
jschoubben 1a1d3a1ecc Compose both of a rotating node's logins from the vault's node-logins, so its engine can test-dial the new one
ADR 0583 §1 (insight 2026-11-01) and §2: the grants part reads the vault's node-logins as its named reader and
composes a node user per login, both while the node rotates and the new one alone after rotation-done; each node's
engine reads its own node's key alone (composed by mesh-sdk while the vault serves rotate-node). The part follows the
state by watch, so a rotation begun or ended composes again at once. A second login is judged its node's when the
list is compared (Placed), and a hash changing by a rotation node-logins names is not counted as a writer changing
several hashes at once (sealed.go). The state not on the bus yet is a standing gap: every node logs in as node.<n>,
as today. Tested as a subtest of TestGrants, no new test function. Built against the group's mesh-sdk head 5000774
through a go.work outside the worktree; the pin is the lead's.

(issue 774, 774/S53)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:45:44 +01:00
jschoubben 288364f28d Compose each enrol.<node> user from the seat mesh-enrolment's keys, so a token issued by the seat can be redeemed
ADR 0587 §4: the grants part composes enrol.<node> from a key at live and the vault's hash, granted only the seat's
redeem and its own inbox; §5: when the key ends the user goes. The part reads mesh-enrolment.enrolments as its named
reader and follows it by watch: a key reaching live composes the user, and its end (by the holder, or by the bus at
its expiry) is a change of the key that takes it out, so no consume of the seat's ending events is added beside the
watch (the subtask named one; the watch already hears every end, the bus's expiry included). From the lease write the
user loses the controller's enrolment subject (§6) and the standing gap of the controller's tokens is dropped; before
it the gap stands. Tested as a subtest of TestGrants, no new test function. Built against the group's mesh-sdk head
5000774 through a go.work outside the worktree; the pin is the lead's.

(issue 774, 774/S52)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:45:32 +01:00
jschoubben 447bcf3a6b The assigner takes an add-node from mesh-enrolment and writes the joining node with no ask
propose takes the enrolment's warrant {node, address, ask, warrant} and refuses an add-node from any caller but
mesh-enrolment, with no warrant or with a warrant naming another node or address (delivery.AddNodeRefusal); the joining node
counts as having a record for this one change, as ADR 0587 §7 changes ADR 0567 §2. The assigner's process then writes
placement.<node> at once, with no node to answer and no ask raised, and says placement-changed carrying the enrolment's
warrant; the hand-act record of that warrant is the enrolment's, made when it was given. The holder of mesh-enrolment calls
propose with the change, the keys and the warrant read from the router's record of the ask. Tests: rows appended to the
seat's verbs test and a case to the whole-seat integration test; none added, so none to pay for (the budget is paid in
774/S60).

(issue 774, 774/S59)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:45:12 +01:00
jschoubben 07d26adcd3 mesh-enrolment: read the group's mesh-sdk contract instead of its own copies of the shapes
The seat, the enrolment state, redeem and its proof, the answer the vault signs, and the vault's two verbs' answers are now
mesh-sdk's (catalogue and delivery), which the vault, the grants part and the engine read too; the proof is the contract's
RedeemProof, the answer carries the login, account, certificate and the vault's signing key (read from its state signers),
and the bucket's values are written through the contract's codecs. The expiry event names the issued event by message id as
well as by the sequence, which a bundle cannot learn. The module builds against the group's mesh-sdk head through a go.work
outside the tree; the lead re-pins go.mod before the push. Tests merged into the same functions; none added.

(issue 774, 774/S54-S58)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:44:46 +01:00
jschoubben 02214cf633 mesh-enrolment: revoke, the expiry marker and the drop of the token
revoke ends an enrolment at any stage without an Approve and records it as an act done by hand (the vault retires an
account minted for a redeem under way); the bus's expiry marker of a key at live says enrolment-expired with the issued
event; the durable consumer of the seat's own ending events asks the vault to drop the token and its hash, acknowledging
only once the vault answers, never before an end. No event or key holds a token (ADR 0587 §5, §8). Run starts the watch,
the consumer and the operator's answers. Tests added: two table-style tests.

(issue 774, 774/S58)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:44:46 +01:00
jschoubben f904ff425e mesh-enrolment: confirm closes an enrolment on the node's own call
Only the node dialled as node.<node> confirms (the runtime's proof of the caller is the confirmation, ADR 0587 §6); the
holder then says node-enrolled with the key's content and deletes the address key (while it still names the node) and the
node's key. A stop between the event and the deletes is finished by calling confirm again; the event keeps one message.
release now answers what it could not delete. Tests added: one table-style test.

(issue 774, 774/S57)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:44:46 +01:00
jschoubben 89370b6f5c mesh-enrolment: redeem, the joining machine's one call
redeem answers only enrol.<node> as the runtime proves it, checks the proof against the identity key presented, spends the
token once by compare-and-set (the keys lose their expiry), asks the vault for the node's account and certificate, writes
the node into placement on the issue's warrant, answers once the bus node loaded node.<node>, signed by the vault; the
same keys called again finish what is not done and answer the same (ADR 0587 §5-§7). The proof is mesh-host's EnrolProof
with the node's name in the token's place, since the seat never holds the secret. Tests added: two table-style tests.

(issue 774, 774/S56)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:44:46 +01:00
jschoubben 4edd0e526c mesh-enrolment: issue, the operator's ask and the warrant, and again
issue refuses at once naming every finding (a bad name, a name a node record, a placement key or an open enrolment
holds, an expiry past a day, any call before the lease write), reserves the lowest free address by creating its key,
writes the enrolment at asked and asks the operator (ADR 0587 §4). On the yes the warrant is recorded, the vault
makes the token and both keys go live with the bus's expiry; a no or an expired ask takes both keys and says
enrolment-refused. Tests added: three table-style tests; none deleted here (paid with 774/S59's merges).

(issue 774, 774/S55)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:44:46 +01:00
jschoubben 5d5ee69775 Add the module mesh-enrolment: the seat's skeleton, its bucket of open enrolments and the tokens verb
The holder of the seat mesh-enrolment (ADR 0587 §1, §2, §8) runs as a bundle on its own account, keeps the bucket
enrolments with a time to live per key, and answers tokens: every open enrolment or one node's with stage, expiry,
ask and revision, the bucket's fill, whether issuing is on and why, and for one node at live the token sealed by
the vault and relayed, kept nowhere. Tests added: two (one table-style test of tokens, the manifest test); the
repository is over its budget, paid by merging tests in the assigner's seat tests in 774/S59's commit.

(issue 774, 774/S54)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:44:46 +01:00
mesh-tools 9125fcc712 Merge pull request 'mesh-grants writes the controller's lease away; the assigner and the grants part stop reading the controller (issue 774)' (#133) from feat/l3-removal into main 2026-11-02 00:35:38 +00:00
jschoubben 17ef1d577d Pin mesh-sdk at c21d8fd, the removal group's head, so every member builds the same contract (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@9125fcc71206
mesh/delivery-group group feat/l3-removal merged: every member is merged
mesh/check failed: mesh-build-go: the check of novox/mesh-catalog#590: the ask mvyhumdm860c7f's step on shanks did not end within 30m0s of its start |…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:53:42 +01:00
jschoubben da27cd1593 Name the lease write's earlier steps by their issues, not milestone labels
(issue 774, 774/S14)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:51:06 +01:00
jschoubben aa6a4e7a59 The indexer's closed seat set counts fifty-seven with mesh-grants
ADR 0582 clause 1 adds the seat mesh-grants to mesh-sdk's set; the indexer's guard of the closed set counts it.

(issue 774, 774/S1)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:51:06 +01:00
jschoubben 8bf45fbafe Integration test: mesh-grants writes the lease and hands it back on a real bus
ADR 0582 "How it is checked": against an embedded bus, a stand-in controller holds the lease in a bucket with an
expiry (2 s standing in for 15 s, through grants.LeaseExpiry, so the wait past it is seconds), with stand-in
node-service-manager and bus-tool seats (the expiry tool acting on the real bucket's stream, as nats-tools'
SetBucketExpiry does). On the warrant the key holder names mesh-grants, the expiry is gone, the controller's
create of the key is refused, and the lease still reads handed away past the old expiry; on the hand-back's
warrant the expiry is back and the controller takes the lease again. mesh-grants requires nats-server for it,
at mesh-sdk's version.

Budget: TestTheLeaseWriteOnTheBus (new, integration) is paid by merging the checker's
TestTheCheckerAwaitsTheAssignersComposition into TestTheCheckersLineSaysTheAssignersComposition as its first case.

(issue 774, 774/S23)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:40:00 +01:00
jschoubben e18be5cabe The watchman raises lease-altered and removal-failed as findings, and clears them when the lease and the removal read right
ADR 0582 clauses 4 and 6: mesh-grants says lease-altered and removal-failed "as a finding the watchman raises".
The watchman hears the seat mesh-grants' three events on core subjects (mesh-sdk WatchmanHears, its grant and
its subscriptions from one list, rather than a consume and a durable consumer), raises lease-altered naming
what changed and clears it at the next lease-handed, and raises removal-failed naming what failed and clears it
once what it names reads gone: the controller's consumer of DEAD_LETTER_NOTICES by its deletion advisory
(heard already), the stream ASSIGNMENTS by its own. The finding DEAD_LETTER_NOTICES.controller stays as it is.

Test: a row in TestTheWatchmansFindings; no new function.

(issue 774, 774/S22)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:37:54 +01:00
jschoubben a78fa3db9d mesh-grants reads placement alone and hears no controller event; this repository's manifests depend on the controller only by the lease
ADR 0549 §5, ADR 0564 §9, ADR 0571 §10: the part no longer reads mesh-controller.assignments (the union with
placement goes; placement not read is a failed read that places nothing) nor consumes the controller's
condition-raised and condition-cleared, the keeper's being the one source. ADR 0571 §13's test for this
repository replaces the guard of ADR 0549 §6 and its list ../assignments-readers, now empty and deleted: at
phase write no module.json here depends on mesh-controller beyond the lease and the three kept to the write
(catalogue.ManifestRefusals).

Tests: in TestGrants the fixture places through the seat's placement (assign, unassign), the subtest of the
stale assignments becomes 774/S29/AC1 (placement alone gives the fixture's three memberships with no read,
consume or call of the controller; the fixture is the suite's own, whose assignments named the same modules,
not a copy of live memberships), and the assignments-readers subtest becomes 774/S24/AC1. No new test function.

(issue 774, 774/S29, 774/S24)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:35:59 +01:00
jschoubben b81d7475e8 mesh-grants judges the controller's dependents with mesh-sdk's judge, and its keep list holds the three kept to the write
ADR 0571 §13: the judge moved to mesh-sdk (catalogue.Dependencies, Judge, KeepList; issue 774, 774/S4) so the
manifest tests of three repositories and grants_lease_check give one judgement; mesh-grants re-exports it by
its names. The keep list there holds the three kept to the write alone (774/S30), superseding the keep of
pacman's read at 4a84370. grants_lease_check no longer counts the controller's standing assignments, which
nothing reads any more.

Tests: TestTheLeaseCheckJudgesEveryDependencyOnTheController keeps the tool's own rows (what it read, the
lease, the streams); its dependency rows moved to mesh-sdk's TestTheLeaseWritesCheckJudgesEveryDependencyOnTheController
(774/S24's replacement of the moved rows), and TestTheKeepListNamesItsRecordsAndThreeToTheWrite went with them.

(issue 774, 774/S30, 774/S24)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:35:59 +01:00
jschoubben 7ec6767418 mesh-grants holds the seat mesh-grants and writes the controller's lease on the operator's warrant
ADR 0582: lease-write is refused, naming each check, until the lease check at phase write and every condition
of clause 2 read green (design 47's earlier issues resolved, every node trusting the vault's key, mesh-config's
compare empty from the indexer, the upgrade compare empty, the keeper's take-over done, the vault minting with
nothing held back, the bus module listing its expiry and removal tools). It asks the operator once; the yes
binds the act, the holder, the ask and the checks' digest, and the checks are read again on the warrant (one
that reads otherwise refuses it, named). Then, each step read back: node-service-manager.stop on the
controller's node, the list composed with Records.LeaseCut and waited on until the bus node's have says it
loaded it, the expiry removed through nats_bucket_expiry, the key holder written (with the ask and the time),
lease-handed said, the act recorded through mesh-hand-acts.warranted, and removal-due written with a day to
live. lease-altered is said once when the key or the expiry differs; lease-hand-back reverses it for a day
and is refused once the removal ran; removal-due's marker alone starts the removal of the controller's
consumer and the stream ASSIGNMENTS, recorded as hand acts, and a failure says removal-failed and writes the
key again with a day (grants_removal_due is the recovery for a missed marker). From the write memberships are
written to the state alone (ADR 0571 clause 10), the user-list key says lease-handed, and the user list's
hand-back answers gone (ADR 0547 §11). Every step waits on an event, never a timer.

Bent rule: S14-S21 are one commit, not eight: the steps are one state machine in leasewrite.go, and splitting
it by hunks would review the same file eight times for nothing it protects.

Tests: TestTheLeaseWrite (new, one function with a subtest per subtask), paid by the deletion of
TestTheKeepListNamesItsRecordsAndThreeToTheWrite (moved to mesh-sdk, S4/S30) and the assigner's two deletions
(S25). Fixtures shaped from each writer's code: systemd's Manager.Status, nats-tools' BucketExpirySet,
ConsumerRemoved and StreamRemoved, mesh-issues' Brief, mesh-delivery-view's TrustsAnswer, mesh-config's
Comparison, the assigner's upgrade compare, mesh-vault's vault_status.

(issue 774, 774/S14, 774/S15, 774/S16, 774/S17, 774/S18, 774/S19, 774/S20, 774/S21)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:31:35 +01:00
jschoubben 30ec4329e0 The assigner's integration tests place their modules and name the node's own composition; a placed node's backup answer stays in its want
The integration tests put placement (not assignments) and the node's own composition (not the controller's) on the bus; the
three that tested the controller's judgements of a move (waiting for a composition, a record move, a user-list revision) go,
their unit counterparts standing. Run locally against the embedded bus, all of this module's integration tests pass.
Found by them: the answer to a failed backup was dropped from a placed node's want at every pass, because the carry-over
compared the want's digest, which a placed node's want does not carry; fixed in wants.go, with a case in the existing unit test.
Tests: three integration tests deleted, none added; one case added to an existing unit test.

(issue 774, 774/S26, 774/S27)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:30:51 +01:00
jschoubben 3bae0d8fa2 Placement's adopt and hand-back are answered as gone, naming their records, and change nothing (ADR 0547 §11, ADR 0567 §11)
The controller's placement-export is no longer called, its answer reader, the seed and the hand-back flow of the holder are
removed, and a seed or hand-back proposal a previous process left open ends at its next pass.
Tests: the adopt, hand-back and seed cases of the seat's verb test and of Propose are replaced by one gone-case; the export
reader's table and the seed test go.

(issue 774, 774/S28)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:21:46 +01:00
jschoubben 7bd18306d4 The assigner reads no composition of the controller: upgrade compare and rule 7 read the digest a node's own have names (ADR 0571 §8)
mesh-controller.composed leaves the reads and the watches; upgrade compare lists the node composition digests of the haves
instead of comparing with the controller's policies, which the controller no longer writes.

(issue 774, 774/S27)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:20:13 +01:00
jschoubben 1302eb9e1f The assigner reads no assignment and no lease: the placement alone names where modules run (ADR 0567 §8, ADR 0549 §5)
Every handed-away branch is the only one: world.assigned is the placement, the pair comparison and its conditions go, the
lease and assignments buckets are no longer opened, watched or declared, and a proposal of any kind is taken.
Tests: the pair-comparison test and the controller-composition record-ask test are replaced by one placement test; the
rest are edited to place their modules instead of assigning them.

(issue 774, 774/S26)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:18:50 +01:00
jschoubben f2ed0938ce The assigner calls no verb of the controller after a placement is written: placement is the one source (ADR 0567 §7, §8)
Drops callAll, the refused-call conditions and the pin reads; the conditions a previous process raised are cleared once.
Tests: deletes four (the refused-call condition, its clearing, the contract of the calls, the condition's shape) and adds one.

(issue 774, 774/S25)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:13:50 +01:00
jschoubben 4a84370b2f mesh-grants: the lease check keeps pacman's read of declared-packages until the write's delivery
pacman still falls back to the controller's declared-packages for a node whose engine has
not written its own packages (ADR 0565 §6), so the check named that read as ended and would
refuse the lease write. It is now a kept read, dropped with the others at the write's own
delivery (issue 774, design 47 L3).

Quick fix chosen over removing pacman's fallback: it matches the record as it stands, and
the read goes with the other kept reads at the write's delivery.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 23:27:29 +01:00
mesh-tools 7d1b8864cc Merge pull request 'mesh-grants pins the mesh-sdk that grants each engine a read of its own declared-packages key (ADR 0565 §6, issue 1953)' (#132) from feat/node-packages into main 2026-11-01 17:02:47 +00:00
jschoubben b18c62f671 Pin the grants part to the group's mesh-sdk head b2a27ed5, so each engine may read its node's key of the packages mirror (ADR 0565 clause 6, issue 1953)
mesh/delivery merged: source.main moved to novox/mesh-delivery@7d1b8864cc08
mesh/delivery-group group feat/node-packages merged: every member is merged
mesh/check checking b18c62f671e1 on cc38ca3f8dc8 | novox/mesh-delivery#132 in group feat/node-packages
The node seat's verb packages compares the engine's key with the controller's mirror until the lease write; the grants part composes the engine's read of its own node's mirror key only from this mesh-sdk.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 16:24:04 +01:00
mesh-tools cc38ca3f8d Merge pull request 'Agree a toolchain by strict majority; a node on another version sits out (ADR 0584, issue 1578)' (#131) from fix/toolchain-majority into main 2026-11-01 15:04:20 +00:00
jschoubben d739516056 Name the agreed toolchain in the checker's check asks; pin mesh-sdk 67354f8 (ADR 0584, issue 1578)
mesh/delivery merged: source.main moved to novox/mesh-delivery@cc38ca3f8dc8
mesh/delivery-group group fix/toolchain-majority merged: every member is merged
mesh/check pass d739516056dd on 55a506975b01 | unit 7056 run, 243 kept, 16 skipped | integration 7194 run, 275 kept, 25 skipped | over bound: unit, in…
A check ask named no toolchain, so a node on the minority version could run it and KeepPasses record the pass as run on the agreed one. The check ask now carries the agreed toolchain and a node off it declines (mesh-sdk), for the checker's decline loop to put again. Pins the builder, indexer and checker.

Known limit: the checker puts a declined check ask again at once, with no pause; the declining puller skips that kind until its next pull wait, which paces a lone minority node.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 15:38:16 +01:00
jschoubben a6c225eb45 Pause before a declined ask is put again with a context's bound, not a timer (ADR 0437 §8, issue 1578)
The builder's manifest test refuses time.After. The previous commit's pause used it.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 15:36:52 +01:00
jschoubben 5085b681e9 Count no decline as an asking; bound declines by time and name the nodes (ADR 0584, issue 1578)
A build node off the agreed toolchain declines the ask and, idle, takes the ask put again at once, so three declines used the three askings and failed the build. A decline now uses no asking: the ask is put again after a short pause, and past a bound from the first decline the key fails naming the declining nodes and their reasons. A person reads which node sits out in the key's step text, which the view's builder_builds answers as stored.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 15:36:23 +01:00
jschoubben 9e407f8f76 Build with the toolchain a majority of build nodes report; say which nodes sit out (ADR 0584, issue 1578)
One build node on another version refused every build of the language. The builder and the checker now follow mesh-sdk's majority agreement; the key's step names a node that sits out ("ace sits out: typescript 26.12.1, agreed 26.13.0"), and a build ask a minority node declined is put again for a node on the agreed version. Check steps compare no toolchain, so clause 2 already holds for them. Pins builder, indexer and checker to mesh-sdk 71be15f.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 15:26:55 +01:00
jschoubben 8545e7fc9f Cite ADR 0584 where the builder waits on a toolchain the nodes do not agree on (issue 1578)
Comment only; the majority rule itself is in mesh-sdk's AgreedToolchain.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 15:22:04 +01:00
mesh-tools 55a506975b Merge pull request 'The assigner, builder and grants read off the controller; upgrade compare by watch (design 47 L5, L7)' (#130) from feat/l5-l7-main into main 2026-11-01 13:53:33 +00:00
jschoubben b7dfe4c637 builder: the held fake in its integration tests answers on the artifact sweep's seat, which the builder now asks instead of the controller
mesh/delivery merged: source.main moved to novox/mesh-delivery@55a506975b01
mesh/delivery-group group feat/l5-l7-main merged: every member is merged
mesh/check checking b7dfe4c6375a on 58748c2be161 | novox/mesh-delivery#130 in group feat/l5-l7-main
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 10:58:15 +01:00
jschoubben c4b05a3e93 Pin group A's mesh-sdk head on main
mesh/delivery-group group feat/l5-l7-main rejected: composed together: the checker's verdict at the members' heads (e32e2c9c4fea): integration: mesh-delivery-b…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed (2 tests): integration: mesh-delivery-builder/cmd/mesh-delivery-builder.TestADoneKeyWhoseBlobIsGoneIsBuiltAgainAtStart: builds.ca-tr…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:57:47 +01:00
jschoubben 38f2401f17 mesh-grants and the builder pin mesh-sdk at 8311ecb
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:28 +01:00
jschoubben c8b041ab37 mesh-wants upgrade reads through mesh-sdk's watched reader, keeping no mirror of its own
One helper serves every part that reads whole states (ADR 0580 clause 3): the assigner's own watched mirror goes, its
compare reads the large states from stdio.WatchedStates and each manifest once per digest. mesh-sdk pinned at 8311ecb.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:22 +01:00
jschoubben f52f47e1ff mesh-grants and the assigner read the grants part's key alone, root-free is each engine's word, and the hand-act log is the seat's alone (ADR 0571 clauses 9 and 11, ADR 0565 clause 11, issues 1953 and 1955)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:22 +01:00
jschoubben 213ebe7cfd The builder asks the artifact sweep's seat for held and no longer the controller (ADR 0571 clause 2, issue 1955)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:22 +01:00
jschoubben 90e278cb97 mesh-wants upgrade: read the large states by one watch each, started once, not a get per key, so compare answers in time (issue 1953 L5f)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:22 +01:00
mesh-tools 58748c2be1 Merge pull request 'The indexer judges every manifest once and says its verdict; the checker judges a change with the same judge (ADR 0578, 0579)' (#129) from feat/judge-once into main 2026-11-01 07:54:35 +00:00
jschoubben 8e9cc58301 Fall back to a captured copy only for mesh-host, which the checker places beside no mesh-delivery check (issue 1585)
mesh/delivery merged: source.main moved to novox/mesh-delivery@58748c2be161
mesh/delivery-group group feat/judge-once merged: every member is merged
mesh/check checking 8e9cc5830119 on b1f262d7d06e | novox/mesh-delivery#129 in group feat/judge-once
Review of fb96216: a missing mesh-sdk or catalogue in a merge check is the checker's fault and fails, never judged
against a stale copy. The captured placement guard is refreshed to mesh-host's main 233bb23.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 04:49:23 +01:00
jschoubben fb96216e46 Read a sibling the checker does not place beside mesh-delivery from its captured copy, said (issue 1585)
The judge's tests moved from mesh-sdk, where mesh-host was beside them; the checker places only mesh-sdk and the
catalogue beside mesh-delivery (besideRules), so the node-engine's path rules failed as not beside. A sibling the run
holds beside is read there; one it does not is read from testdata/beside and the log says which; only a sibling
with neither fails.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 04:47:22 +01:00
jschoubben 5059fef3e6 Expect the mesh packages a merged message carries in the forge bridge's whole test (ADR 0573 §2)
mesh/delivery-group group feat/judge-once rejected: composed together: the checker's verdict at the members' heads (253f35815198): unit: mesh-delivery-indexer/…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: mesh-delivery-indexer/judge.TestTheResolvedPathRulesAreTheNodeEnginesOwn: mesh-host is not beside this check in MESH_CHECK_BE…
Issue 1585: the bridge names the mesh packages each module's go.sum names (builder-1585b's fold); its integration test
still wanted none. All nine modules' integration suites pass locally (the lead allowed a whole local run this round).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 04:20:05 +01:00
jschoubben 564a04b45c Pin the group's mesh-sdk head after the gofmt fix
mesh/delivery-group group feat/judge-once checking: 6 of 6 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 564a04b45c42 on b1f262d7d06e | novox/mesh-delivery#129 in group feat/judge-once
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 03:26:58 +01:00
jschoubben edf7b3c0bf Answer review round 1: fresh is only what a take-in newly wrote, a merge keeps the verdict, the check compares with the trunk (ADR 0578, 0579)
mesh/delivery-group group feat/judge-once checking: 6 of 6 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking edf7b3c0bf3a on b1f262d7d06e | novox/mesh-delivery#129 in group feat/judge-once
Issue 1585, R1: fresh holds the digests a take-in newly wrote (a new module, a changed manifest); a registered or
taken-in module is judged too; a start pass that fails is judged again at the next message heard, no timer.
R2: a module's verdict, problems and warnings are carried while its digest stands and are no part of the compare.
R3: the checker judges the trunk beside the change and fails on every problem the change case adds, an untouched
manifest's included. R4: the judge reads the indexer's kept predefined keys, or the catalogue member's when it
changes them. A log line names a package no held module publishes. Every module pins mesh-sdk 62cddf4.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 03:12:09 +01:00
jschoubben 0e819eea4d Never refuse what the judge accepted before: warn, and refuse only a new or changed manifest (ADR 0579)
Issue 1585: the indexer keeps an accepted manifest accepted when a stricter judge re-judges it, its problems as
warnings carried in its value; a merge's new manifests are judged in full. The watchman raises manifest-outgrown as
a warning until mended. The checker fails only on a manifest the change adds or changes and names the rest as
warnings. Every module pins mesh-sdk 19c2042.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:45:18 +01:00
jschoubben f4ecc6fa64 Pin every mesh-delivery module to mesh-sdk feat/judge-once a20bdf7, the judge out and package versions in
Issue 1585: builder-1585b's package versions folded onto the judge once; one mesh-sdk head for the group.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:42:05 +01:00
jschoubben fa918318b2 Test the checker's latest package versions
The checker builds every Go check against them (issue 1585, S14); a row
in the repoModules test proves they come from the modules that hold each
package and leave out a package no module holds.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:41:28 +01:00
jschoubben 3f2303a086 Build checks against the group's package heads, not go.mod pins
ADR 0573 §4 makes refusing a member for its mesh-sdk pin a fault of the
check (issue 1585, S14). Every Go check ask now carries the latest
published version of each package, or the head of the group member that
publishes it, so the pin check, its forge reads and its tests go, and
the controller's tests read mesh-sdk at the version it is built with.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:41:22 +01:00
jschoubben ae6a54bca2 Take each mesh package's latest version into the fingerprint
ADR 0573 §2: a module builds against the latest published version of
each mesh package it uses, and a package's trunk merge moves every
dependent (issue 1585, S13). The bridge reads the packages each go.sum
names, the indexer keeps them and takes each one's trunk merge into the
fingerprint, a candidate takes a group member's head instead, and the
builder's ask carries the versions so the build module writes them.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:41:04 +01:00
jschoubben d6807233f2 Keep the reserved stream prefixes in the judge, the one part that reads them (ADR 0578 §4)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:38:00 +01:00
jschoubben 3ae308426a Merge four of the judge's moved tests into one, so the move adds no test to the budget
Issue 1585: three tests are split between mesh-sdk (their composition half) and the judge (their judging half).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:36:23 +01:00
jschoubben 8a98271680 Move the judge's rules and their tests from mesh-sdk into the indexer's judge (ADR 0578 §4)
Issue 1585, S6-S8: 192 judge-only declarations (the strict parser, every manifest problem function, the tables)
moved whole by an AST mover, methods becoming functions; 163 helpers composition keeps too are copied. 245 tests
move with them, reading the captured catalogue through a copy of internal/beside. judge.WithTemplatesFrom and
ReadTreeManifest judge what mesh-sdk's readers now only fill. mesh-delivery's own tests decode. Pinned to mesh-sdk 25a2cec.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:34:26 +01:00
jschoubben 0469641b9d Pin mesh-delivery's modules to mesh-sdk feat/judge-once 7cab079
Issue 1585: the group's mesh-sdk head (lenient Decode, verdict contract, frombus holding a refused module).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:21:32 +01:00
jschoubben 4f6f38e16a Keep every strict read of a manifest in the indexer's judge, and add no test to the budget
Issue 1585, S5-S8: the indexer's fingerprint reads go through judge.ForInputs, so judge/ is the one place in
mesh-delivery that calls mesh-sdk's rules; the command's request judging moves into judge.Request, tested in the
judge's table; two predefined tests become one.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:21:32 +01:00
jschoubben 5b6602544e Prove the judge once whole: verdicts, manifest-refused, and a bus reader that holds and goes on (ADR 0578)
Issue 1585: the indexer's merge test takes in three manifests more against the real bus; it writes their
verdicts, says each refusal, and mesh-sdk's bus reader composes the valid one and holds the refused, named; before
any verdict it takes each as accepted and names the field set aside. One test extended, no test added.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:17:57 +01:00
jschoubben efef7183ef Let grants, builder and checker decode manifests, and the checker judge a change with the indexer's judge (ADR 0578 §2, §3)
Issue 1585, S12: grants, builder and checker read with catalogue.Decode and grants holds a module the indexer
refused; the checker asks the Go build seat to run mesh-judge, built at mesh-delivery's trunk or at the group's
mesh-delivery head when it changes the judge, over the catalogue as the change leaves it, and fails on a refusal.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:15:54 +01:00
jschoubben a4772a5c1d Let the assigner decode manifests and take the indexer's verdicts, never judging (ADR 0578 §3, §5, §6)
Issue 1585, S11: world and wants read with catalogue.Decode; a trunk refusal is held, named and handed to the
composer in its input; a proposal's composition read that fails is said, not dropped; a reader that cannot read a
changed field is a warning, no longer a failure.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:11:54 +01:00
jschoubben 26852f4a9d Raise a refused manifest as the watchman's finding, so the operator hears it once (ADR 0578 §1)
Issue 1585, S10: the watchman watches the indexer's modules and finds manifest-refused for a module whose value
carries a refusal, ending it once accepted. Its mesh-sdk pin moves to the group head, so it takes health.Finding's
Ref (issue 1952 renamed Token).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:09:50 +01:00
jschoubben 82776466b2 Let the indexer write a verdict on every manifest it holds and say each refusal (ADR 0578 §1)
Issue 1585, S9: verdicts keyed by digest, written only when they differ; a module's value carries a refusal; a
new refusal is said as manifest-refused. Judged at start (a new judge judges again) and after every merge.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:08:21 +01:00
jschoubben 4837548349 Give the mesh one judge of manifests, in the indexer, and a command the checker runs (ADR 0578 §1, §2)
Issue 1585, S5: judge.Shelf judges each manifest alone and the rules between them, giving a problem between
manifests to each module whose removal ends it (naming alone blamed the module mounts for f1-circuits' problem).
cmd/mesh-judge answers a composer request so a Go build module runs it as it runs the composer.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:08:21 +01:00
mesh-tools b1f262d7d0 Merge pull request 'mesh-grants takes the grants of the node-engine's data items (ADR 0577)' (#128) from feat/data-items into main 2026-11-01 00:03:16 +00:00
jschoubben ddadb75bf4 mesh-grants pins mesh-sdk feat/data-items, so it grants the node-engine's data items state (ADR 0577)
mesh/delivery merged: source.main moved to novox/mesh-delivery@b1f262d7d06e
mesh/delivery-group group feat/data-items merged: every member is merged
mesh/check pass ddadb75bf474 on 91128d4f0af6 | unit 4510 run, 4259 kept, 49 skipped | integration 4589 run, 4361 kept, 51 skipped | over bound: unit, …
The only change taken in is ADR 0577's: each node's engine writes its own keys of mesh-host_data,
and each node's disk-load reads its own node's keys alone. Without it the engine's writes are
refused and disk-load measures nothing.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 00:19:09 +01:00
mesh-tools 91128d4f0a Merge pull request 'The watchman confirms a module unhealthy by time, and keeps a standing finding across its restart (issue 1952)' (#127) from fix/watchman-confirm into main 2026-10-31 21:34:06 +00:00
mesh-tools 5dbde07b7b Merge pull request 'mesh-grants builds memberships from the group mesh-sdk (issue 1952)' (#126) from fix/l4-quick into main 2026-10-31 21:34:03 +00:00
jschoubben 8a658a4290 Watchman: an unhealthy finding read back at the start is kept as it was (issue 1952)
mesh/delivery merged: source.main moved to novox/mesh-delivery@91128d4f0af6
mesh/check checking 8a658a4290c9 on 91128d4f0af6
At a restart a standing finding is observed again by the replayed failed have: kept, not confirmed again, and its urgent
signal runs on from the finding's Since; an urgent one stays urgent with no signal restarted.
Quick: skip the confirm when a finding stands. Thorough, chosen: the same, plus urgency read from the finding itself.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-10-31 22:01:59 +01:00
jschoubben fb8d2386b9 Watchman: a module is unhealthy when its have still says failed a minute after it first did (issue 1952)
A have that turned failed once and stayed unchanged was written once, so the old count of two writes never fired and the
finding was never written (fail2ban on novox). Confirm by time, with a signal, also at a restart.
Quick: raise the count to one write (loses the confirmation). Thorough, chosen: confirm by a time bound, as ADR 0564 s4's
signals are keys that expire, so no write count is needed.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-10-31 21:59:46 +01:00
jschoubben a2558171e1 Compose memberships with a carried holder's own seat events (issue 1952)
mesh/delivery merged: source.main moved to novox/mesh-delivery@5dbde07b7bb8
mesh/delivery-group group fix/l4-quick merged: every member is merged
mesh/check pass a2558171e11e on f83835b4b6ad | unit 2729 run, 4570 kept, 16 skipped | integration 2975 run, 4493 kept, 25 skipped | over bound: unit, …
The grants part composes every membership and runtime grant from
mesh-sdk, which now lists a held seat's events for its holder, so the
self-check the tool runner carries can say `checked`. Pinned to that
mesh-sdk head.

Quick vs thorough: quick, these fixes for L4's take-over now; the
thorough fix (research 032 / ADR 0240's health view extension) is
decided later.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-10-31 21:35:59 +01:00
mesh-tools f83835b4b6 Merge pull request 'mesh-placement keyed per node and module, and memberships from mesh-grants' own state (issue 774)' (#125) from feat/072-moves into main 2026-10-31 10:31:10 +00:00
jschoubben a313c3e843 Pin mesh-sdk at the group's head acbf80a in every delivery module the group touches (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@f83835b4b6ad
mesh/delivery-group group feat/072-moves merged: every member is merged
mesh/check failed: shanks: the check of novox/mesh-controller#487 failed: go mod download in modules/route-proxy: go: git.novox.be/novox/mesh-controll…
2026-10-31 10:25:20 +01:00
jschoubben e7f51caedb Read each membership's last value from mesh-grants' own state and say it on its subject without the assignments stream, so memberships keep flowing once nats_stream_remove takes the stream at the lease write (issue 774, research 072 row 33) 2026-10-31 10:25:20 +01:00
jschoubben 4b194215dc Read and write the seat mesh-placement's state as one key per module and node, converting each node's placement.<node> once at the holder's start (issue 774, research 072 row 37)
The holder writes a node's changed module keys, then its node key as the commit by compare-and-set at
the revision the proposal was laid over, then the placement.<node> mirror for readers not yet delivered.
Every reader assembles through mesh-sdk's PlacementKeys, which reads both forms.
2026-10-31 09:32:32 +01:00
mesh-tools af0fd83aec Merge pull request 'Teller: teller_end_ask ends one stale ask by hand (issue 774)' (#124) from fix/merge-ask-ends-on-merge into main 2026-10-31 01:33:02 +00:00
jschoubben 17abe6af32 Teller: teller_end_ask ends one stale ask by hand, so an ask whose key is never taken away cannot hold the router's room for a day (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@af0fd83aec09
mesh/delivery-group group fix/merge-ask-ends-on-merge merged: every member is merged
mesh/check checking 17abe6af32ce on af0fd83aec09 | novox/mesh-delivery#124 in group fix/merge-ask-ends-on-merge
On 2026-10-31 two merge asks of pull requests merged by another path held two of
the router's three places for the teller and a placement ask was refused for want
of room. The gate's fix ends such asks by itself; this tool is the recovery for any
ask that still outlives its question: it cancels the open ask at the router, or
drops one waiting for room, by its router id or its key, and logs who and why. The
tools bundle asks through the teller's new state ends, as indexer_take_in does.
2026-10-31 02:13:39 +01:00
mesh-tools 804d5cc065 Merge pull request 'A node-engine answers any verb of its own node, so a new engine verb needs no grants release (issue 774)' (#123) from fix/engine-seat-any-verb into main 2026-10-31 00:30:55 +00:00
mesh-tools fd62a36041 Merge pull request 'The assigner puts only changed values and keeps a placed node's record ask while it waits (issue 774)' (#121) from fix/assigner-puts-only-changes into main 2026-10-31 00:21:36 +00:00
jschoubben 14f68219cf Grants: pin mesh-sdk 748543c, so every node's engine is granted any verb and event of its node seat on its own node, composition and compose included (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@804d5cc06559
mesh/delivery-group group fix/engine-seat-any-verb merged: every member is merged
mesh/check checking 14f68219cf29 on 804d5cc06559 | novox/mesh-delivery#123 in group fix/engine-seat-any-verb
The grants part pinned eff81f1, before the verb composition (6bce1fb): the bus refused every engine's subscription to it and each engine reopened its link every few minutes. With 748543c a verb added to the node-engine seat needs no grants release first. Group with mesh-sdk fix/engine-seat-any-verb.
2026-10-31 01:14:33 +01:00
jschoubben 7235e4fb92 Keep a record ask the node's have waits on, and put no value a bucket already holds, so a node that composes its own is asked once and hears no want that did not change (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@fd62a36041be
mesh/check checking 7235e4fb921d on fd62a36041be
On novox the asks of postgres, keycloak and mongodb were judged stale against the controller's compositions, taken away and made again under a new id every pass (8 s), so no answer could count; and every put is a change the node composes again on.
Quick fix: the stale check keeps a record ask whose digest the node's have waits on; the pass skips a put whose value the bucket holds.
Thorough fix beside it (not here, another branch owns the file): the node-engine's composer composes again only when a want's value changed.
2026-10-31 01:09:13 +01:00
mesh-tools e559798972 Merge pull request 'Answer the controller's lease, what still depends on it and the lease write's check in one call (issue 774)' (#120) from feat/lease-tool into main 2026-10-30 22:49:20 +00:00
jschoubben 2dd5239726 Refuse the lease write's check when the indexer's manifests were not all read (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@e559798972b4
mesh/check being carried to 2dd5239726ca on e559798972b4 | from d409bdf20f48
Review of 09791f5: with the indexer's modules bucket readable but empty, nothing was read, so
nothing was found and both phases passed; the operator could have handed the lease away while an
unkept read still stood. Judge now refuses when no manifest was read, or fewer of the indexer's
modules than it holds (a provided manifest does not count for a missing one), saying why; a
missing or refused manifest is named as before, now under test.

The controller's grants leave the keep list: ADR 0571 §9 has no manifest read them from L7's
delivery, changing ADR 0567 §10 and ADR 0547 §5's shadow compare, so the assigner's and the grants
part's reads are refused, and a refusal names the record that ended a declaration where one does.

Quick fix: the counts in the grants part's Judge, as here. Thorough fix: the same coverage rule
in the shared mesh-sdk derivation once the keep list moves there; the quick one stands until then.
2026-10-30 23:41:11 +01:00
mesh-tools d409bdf20f Merge pull request 'The assigner waits only for provisions the composition keys (issue 774)' (#119) from fix/one-rule-for-provisions into main 2026-10-30 22:33:50 +00:00
jschoubben 09791f5a82 Answer the controller's lease, what still depends on it and the lease write's check in one call (issue 774)
The operator asked to remove the controller by finishing its hand-over; no tool said who holds
the lease, which manifests still read, invoke or consume mesh-controller, or whether the lease
write's check (ADR 0571 §13) would pass. grants_lease_check answers all three from bus state on
the grants part, which already reads the lease, every manifest at the indexer's trunk and the
bus's streams: the holder and what follows from it, every dependency with the record on the
named keep list that keeps it, the controller's streams and buckets someone consumes, and the
check run dry at the write (the three kept to it alone) and before its delivery, with reasons.

Quick fix: the judgement and the keep list in the grants part, as here.
Thorough fix: the keep list and the dependency derivation in mesh-sdk, shared by this tool and
the §13 manifest tests in mesh-catalog, mesh-delivery and mesh-tools. Chosen quick: the tests do
not exist yet and an sdk move needs a group; the move is named for the lead.
2026-10-30 23:30:09 +01:00
jschoubben 6298d9d67c Pin mesh-sdk at 6c5a8ef893b4, the one rule of what is keyed with its compose row (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@d409bdf20f48
mesh/delivery-group group fix/one-rule-for-provisions merged: every member is merged
mesh/check being carried to 6298d9d67c83 on 52a1570b3e3c | from mesh-sdk b74e23f973ab | novox/mesh-delivery#119 in group fix/one-rule-for-provisions
2026-10-30 22:29:35 +01:00
jschoubben 83391dba1b Wait in rule 8 only for the provisions the composition keys (issue 774)
Rule 8 asked a key for every `requires`, and the plans key only a handed module's needs and what a helper answers:
grafana waited on ace for oidc-client, which the controller still makes, and mesh-build-go for go-toolchain on its own
machine, for ever. MissingProvisions now takes catalogue.Awaited, the one rule the plan composition and mesh-provisioner
key by, judged against the providers placed somewhere.

Quick fix: drop the missing-key check. Thorough fix (chosen): the shared rule in mesh-sdk, so the three parts cannot
drift again. Needs mesh-sdk at 30098229b831.
2026-10-30 22:29:25 +01:00
mesh-tools 52a1570b3e Merge pull request 'The assigner calls the controller only with the arguments its contract declares, and a refused call is a condition (issue 774)' (#118) from fix/held-module-and-assigner-contract into main 2026-10-30 21:17:41 +00:00
jschoubben 8fe6442c47 Clear a refused pin's condition once the controller's pins agree, and take over a previous process's conditions only once the findings are read (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@52a1570b3e3c
mesh/delivery-group group fix/held-module-and-assigner-contract merged: every member is merged
mesh/check checking 8fe6442c4754 on c104e7500d21 | novox/mesh-delivery#118 in group fix/held-module-and-assigner-contract
A pin made by hand, as the condition's Needs asks, changed nothing the assigner looked at, so its urgent condition
never cleared, and a restart left every one raised. The assigner now reads the controller's pins (placement-export,
asked again only after the controller's compositions change, never on a timer) and clears a pin's condition once
they agree with the placement's after being seen differing, as a module's place already did. The first pass takes
over the call conditions a previous process raised, pins and places alike, and retries at the next pass when the
findings cannot be read.
2026-10-30 21:27:35 +01:00
jschoubben 1fef458c5f Send the controller only the arguments its contract declares, and make a refused call a condition (issue 774)
The assigner added "why" to every assign, unassign, pin and unpin since 2026-10-28; the controller's seat contract
takes no such argument, so it refused each one and did nothing. No approved placement reached the controller's
assignments, no bus account was minted for a newly placed module, and the refusal was only a journal line.

Quick fix: drop "why" from ControllerCalls. Thorough fix: a contract test that every argument ControllerCalls gives,
for every verb, is declared (and every required one given) in catalogue.ControllerVerbs, plus an urgent condition per
pair whose call was refused (verb, node, module, the controller's words), cleared when a later call is answered or the
two sources agree. Chosen: both here; a caller-side check of arguments against the seat's contract for every seat
call belongs in mesh-sdk and is noted for the lead. The warrant stays in the assigner's own record (placement-changed,
the hand-act log).

Every module pins mesh-sdk eff81f1220e2 (in review).
2026-10-30 21:23:39 +01:00
jschoubben c104e7500d Merge pull request 'The grants part writes its list whole however large, one refused principal no longer freezes it, and stale watchman keys close (issue 774)' (#117) from fix/sweep-2026-10-30 into main
Reviewed-on: #117
2026-10-30 18:13:00 +00:00
jschoubben e0b56c83f9 Pin mesh-sdk a3718126, whose bus retry never asks again a call its callee answered (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@c104e7500d21
mesh/delivery-group group fix/sweep-2026-10-30 merged: every member is merged
mesh/check checking e0b56c83f94c on 33973d533825 | novox/mesh-delivery#117 in group fix/sweep-2026-10-30
2026-10-30 18:43:13 +01:00
jschoubben d4ac50c4b1 Pin mesh-sdk 206f794e, the fix group's final library head (issue 774) 2026-10-30 18:37:07 +01:00
jschoubben f756731700 Pin mesh-sdk ef1ec1c9, the fix group's reviewed library head (issue 774) 2026-10-30 18:27:08 +01:00
jschoubben 255f8b2aca Close a stuck key whose watched key aged out or was deleted unheard, so its condition clears (issue 774)
A key that ages out of its bucket (the build seats' asks pass the stream's 168 h) leaves no marker a watch hears,
and one deleted while the watchman was down is not heard either: its stuck key stood unmoved for ever, and 26
stuck-key conditions of asks from 2026-10-21 were open on 10-30.

Quick fix: mark stuck keys older than the stream's maximum age moved.
Thorough fix, chosen because it covers every bucket the watchman mirrors and any way a key goes: at the start's
end, a stuck key not moved whose bucket was read whole at this start (each build seat's steps, the assigner's
wants) and whose key was not read standing is written moved, which clears its finding, and the closing is said
in the log. A bucket that could not be read closes nothing. A key that ages out while the watchman runs is
closed at its next start: a KV age-out emits no event, and no timer is added.
2026-10-30 18:25:56 +01:00
jschoubben ff58b16cf9 Pin mesh-sdk 61b67d61, so mesh-grants writes through the size guard and every delivery module rides out a user-list reload (issue 774) 2026-10-30 18:23:08 +01:00
jschoubben 788969bcf8 Write a kept user through mesh-sdk's AccountsEntry, so its lines are ComposeAccounts' own (issue 774)
The grants part repeated the composer's entry lines to keep a refused principal's grants of the list in force;
mesh-sdk 61b67d6 exports them as buscompose.AccountsEntry, so the copy goes. The byte-for-byte test of the kept
entry still passes. Needs the mesh-sdk pin at 61b67d6 or later.
2026-10-30 18:20:28 +01:00
jschoubben ce054dc8dd Judge nothing and owe no status for a candidate outside the mesh (issue 774)
The forge carries the operator's own repositories (jschoubben/hal, jschoubben/novadock) beside the mesh's; the
indexer writes their pull requests as candidates, the gate never names their trunk, so the checker said each
"cannot be judged" and owed the status error on mesh/check, which the forge module wrote into repositories the
mesh's forge account may not write: 403, 119 times each (sweep 2026-10-30 item 7).

Quick fix: name the two repositories as not the mesh's in the checker.
Thorough fix, chosen because any repository on the forge can meet it: a candidate every pull request of which is
of a repository the gate's source.main names no trunk of and touches no module is outside the mesh. The checker
writes it no verdict and owes it no status, takes away a status owed before, says it once in the log, and lists it
in checker_status as outside_the_mesh. A new mesh repository's first pull request adds a module, so it is still
said untrunked and owed error (issue 1422). Nothing is outside before the gate's source is read.
2026-10-30 18:12:33 +01:00
jschoubben 94f555277c Never name the private network the mesh provides as a module missing its grant (issue 774)
mesh-wireguard is provided by the mesh itself (mesh-sdk delivery.ProvidedByMesh): no repository builds it, so the
indexer holds no fingerprint of it and the assigner never wants it by one; the controller composes it with the
node's own. The grants part named it "no fingerprint of the new form: the indexer holds no value of it" on every
pass, a warning nothing could clear (sweep 2026-10-30 item 6).

Quick fix: drop mesh-wireguard by name from the missing list.
Thorough fix, chosen as cheap and covering every module the mesh may provide later: MissingGrants passes over any
module ProvidedByMesh names. The controller says the same of it in mesh-control (its grants and switch lines).
2026-10-30 18:10:55 +01:00
jschoubben 031cfb8277 Compose the user list principal by principal, so one refused principal never freezes it (issue 774)
Met twice on 2026-10-30 (the engine's acts, then mesh-host_health): the composer refused one principal as a
second writer, and that refusal ended the whole composition, so the list in force stood for every principal
while changes piled up; the next list that composed moved 52 users at once (16:37:28).

Quick fix: leave the refused principal out and write the rest (locks that principal out of the bus).
Thorough fix, chosen because the kind was met twice and it hides every other change: ComposeEach composes each
principal on its own; a refused one keeps its grants of the list in force (the part's sealed grants), written as
the composer writes an entry; one the list in force does not carry stays out; a list that would drop one the
list in force carries, whose grants were not kept, is held, said. The refusal is raised as the warning
mesh-grants.principal-refused, by name and why, cleared once it composes, and listed in the pass
(principals_refused). The entry renderer mirrors buscompose.ComposeAccounts's lines and is pinned to them by the
test (the entry the bus loaded is written back byte for byte); it belongs in mesh-sdk as an exported entry writer.
2026-10-30 18:10:26 +01:00
jschoubben c1b28cc4fe Keep each sealed list's permissions one key per user, so no value outgrows the bucket (issue 774)
The sealed-grants value held every user of the last two lists in one value; at 61 users it was refused with
"message size exceeds maximum" (JetStream 10054, 16:37 and 16:55 on 2026-10-30), so the permissions of the
list in force were never kept and a grant judged against them waited.

Quick fix: keep only the newest list in the one value (halves it; it still grows with the mesh).
Thorough fix, chosen because the value grows with every placed module and the kind was met twice: one key per
user and list, sealed-grants.<digest hex>.<user>, and an index sealed-lists naming the kept lists and their users,
written only once every user's key is there; the third list's keys are taken away, and the old one-value key read
as a fallback and removed. mesh-sdk's kvlimit guard (7b02f63) refuses an oversized value before the put through
process.State.Put, which the part's own store (cmd/mesh-grants ownStore.Put) writes through once pinned.
2026-10-30 18:07:26 +01:00
jschoubben 33973d5338 Merge pull request 'mesh-grants grants each provisioner client every machine's plans (issue 774)' (#116) from fix/provisioner-reads-every-plan into main
Reviewed-on: #116
2026-10-30 15:53:41 +00:00
jschoubben f800d672f3 mesh-grants grants each provisioner client every machine's plans through mesh-sdk af66ccb, so g14 and shanks read their plans again (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@33973d533825
mesh/delivery-group group fix/provisioner-reads-every-plan merged: every member is merged
mesh/check being carried to f800d672f344 on 98594c990287 | from mesh-sdk 42988a734b3d
2026-10-30 16:41:02 +01:00
jschoubben 98594c9902 Merge pull request 'mesh-grants composes the user list without granting the engine's state to a tool runner (issue 774)' (#115) from fix/runner-carries-no-engine-state into main
Reviewed-on: #115
2026-10-30 15:35:58 +00:00
jschoubben 57193ab2e4 Pin mesh-grants to the mesh-sdk that composes no user from the node-engine's manifest, so the whole user list composes again (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@98594c990287
mesh/delivery-group group fix/runner-carries-no-engine-state merged: every member is merged
mesh/check checking 57193ab2e474 on f7cd4698db7e | novox/mesh-delivery#115 in group fix/runner-carries-no-engine-state
Second meeting of the engine-state-in-runner kind; thorough: the runner
never carries the engine module. The live placement names mesh-host,
and with its health state the runner was a second writer, so every
grant since 13:54 was held back. The seeded-placement test now carries
the engine's manifest and refuses any runner write of mesh-host_*.
2026-10-30 16:32:43 +01:00
jschoubben f7cd4698db Merge pull request 'mesh-grants writes its memberships bare through the mesh-sdk codec (issue 774)' (#114) from fix/memberships-shape into main
Reviewed-on: #114
2026-10-30 14:42:13 +00:00
jschoubben 180a3fe5db Pin mesh-sdk at its fix/memberships-shape head after review round 1 (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@f7cd4698db7e
mesh/delivery-group group fix/memberships-shape merged: every member is merged
mesh/check checking 180a3fe5db30 on ab210fc5ab40 | novox/mesh-delivery#114 in group fix/memberships-shape
The decoder now refuses an old wrapper of a held-back membership; grants_membership says so for such a key.
2026-10-30 15:36:48 +01:00
jschoubben 4bb1970fef Write the grants part's memberships bare, through mesh-sdk's codec, so the tool runner serves again
The state memberships held the part's wrapper ({node, module, digest, membership, published, retired}) while every
reader takes the bare membership the assignments stream carries: novox's tool runner decoded an empty one and served
no tool. The part now writes membership.EncodeGrantsMembership of a membership published or unchanged, never one held
back. The pass record moves to the pass the part already keeps (user-list at pass): published or why not was there, the
retired seat verbs are added as Result.Retired; grants_membership reads both, so no new state is declared.

Issue 774 (the bus login list, L3). Quick fix: marshal m.Membership here. Thorough, chosen: one codec in mesh-sdk the
writer and the tool runner share, because a hand-built fixture agreed with the mistake. Pins mesh-sdk at the head of
its fix/memberships-shape.
2026-10-30 15:27:06 +01:00
mesh-tools ab210fc5ab Merge pull request 'Design 47's last milestones: watchman, grants gate, assigner wants and placement, builder log, view (issues 774, 1952, 1953, 1954, 1955)' (#113) from feat/774-l3-reland into main 2026-10-30 11:02:07 +00:00
jschoubben 8edc83950c Pin mesh-sdk 2da0de7, the release's sdk with main merged in (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@ab210fc5ab40
mesh/delivery-group group feat/774-l3-reland merged: every member is merged
mesh/check checking 8edc83950cea on 5588875c3f21 | novox/mesh-delivery#113 in group feat/774-l3-reland
2026-10-30 01:12:57 +01:00
jschoubben 8e470f3af4 Merge origin/main (delivery 1, #112) into the release, keeping the release's mesh-sdk pins (issue 774) 2026-10-30 01:12:57 +01:00
mesh-tools 5588875c3f Merge pull request 'Pin the parse-only mesh-sdk in the assigner, indexer, checker, builder and grants part, so they read design 47's manifests (issue 774)' (#112) from fix/774-new-words-first into main 2026-10-30 00:10:15 +00:00
jschoubben a2dd7a5c37 Pin the parse-only mesh-sdk 78c9ec8 in place of 6e4a301, so delivery 1 changes nothing but what a manifest may say, and keep main's checker test as it is (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@5588875c3f21
mesh/delivery-group group fix/774-new-words-first merged: every member is merged
mesh/check pass a2dd7a5c37ef on 883d976b5f00 | unit 9174 run, 546 kept, 51 skipped | integration 9952 run, 659 kept, 60 skipped | over bound: unit, in…
2026-10-30 00:38:11 +01:00
jschoubben 7536d9d14e Pin mesh-sdk 6e4a301, the release's final head, which keeps the old provisioner client's needs grant for one release (issue 774) 2026-10-30 00:18:04 +01:00
jschoubben 9b60eda451 Pin mesh-sdk 6e4a301 in the checker, and hold that the change's own failing test ends its level, as that sdk's build asks do (issue 774, issue 1289) 2026-10-30 00:17:24 +01:00
jschoubben 541ad1d2e2 Pin mesh-sdk 6e4a301 in the modules that parse manifests, so the running assigner, indexer, builder and grants part read the release's new manifest words before the release itself is checked (issue 774) 2026-10-30 00:17:24 +01:00
jschoubben ce7032c49b Pin mesh-sdk 007b6fd, the release's final head (issue 774) 2026-10-30 00:05:25 +01:00
jschoubben 08ecf10ef3 Grants: write withheld hearings in mesh-sdk's layout, so the self-check reads them by the sdk's names (issue 1955) 2026-10-29 23:56:21 +01:00
jschoubben ba502479e8 Builder: ask and hear the seat mesh-artifact-sweep, the name mesh-sdk 7bd0f6c gives the store's sweep, so its held and artifacts-gone reach their holder (issue 1955, ADR 0571 §2) 2026-10-29 23:56:21 +01:00
jschoubben e564eaf75f Grants: write the hearings it withholds to its state withheld on change, so the self-check's probe D-heard reads them from the bus and not the controller's store (issue 1955) 2026-10-29 23:51:48 +01:00
jschoubben a54d1acf45 View: answer builder_history from the builder's log, each build's end with its duration from its start (issue 1955, ADR 0571 §1) 2026-10-29 23:48:14 +01:00
jschoubben e82ea89c23 Assigner: read the running commit of a reader of the manifests from the builder's log, and hold the buckets it starts without in one list so the start test skips them (issue 1955, ADR 0571 §1) 2026-10-29 23:48:14 +01:00
jschoubben 06b58bc83b Indexer: take directories and outside-built registrations from the builder's log, watched from its start, so no build record of the controller's is read (issue 1955, ADR 0571 §1) 2026-10-29 23:48:14 +01:00
jschoubben 17c0734fb2 Builder: keep each build's start and end in its own module log build-log, and answer builder_builds_since from it, so readers and mesh-catalog read the builder's history without a bare stream (issue 1955, ADR 0571 §1, §9) 2026-10-29 23:48:14 +01:00
jschoubben c045057ca0 Assigner: write who holds which seat from placement into seat-holders, only the keys a change moves, and answer it from placement's seats, so readers no longer ask the controller's seats (issue 1955, ADR 0571 §4)
Pins mesh-sdk f9a496d.
2026-10-29 23:14:28 +01:00
jschoubben b44f01f89c Builder: write each build's start and end to its own log on the bus, and ask the seat artifact-store's held and hear its artifacts-gone, the controller's only until the lease write, so build history and the store's sweep leave the controller (issue 1955, ADR 0571 §1, §2)
Pins mesh-sdk f9a496d.
2026-10-29 23:13:32 +01:00
jschoubben 229a3e9855 Assigner: from the lease write rule 7 reads a placed node's own composition digest from its have, an unplaced node gets no want and is said, and the controller's compositions are read only until then (issue 1955, ADR 0571 §6) 2026-10-29 23:10:31 +01:00
jschoubben e5192f55ee View: read no build record and no composition of the controller's: fingerprints against the builder's done keys alone, a move's judgement from the node agent's have (issue 1955, ADR 0571 §1, §6) 2026-10-29 23:08:46 +01:00
jschoubben ee2f1efe78 Watchman: hold a failing network part at the machines it points at, as the controller does, so a machine others cannot reach is said once there and not as each reporter's own (issue 774)
Pins mesh-sdk 6eae076.
2026-10-29 23:06:19 +01:00
jschoubben 8a91871de6 Watchman: raise machine-network, network-rewritten, network-unreachable, machine-units, relogin-needed and directory-used-as-found from each node's own health report, under the controller's keys, so these alerts outlive the controller's lease (issue 774)
Pins mesh-sdk 94d0de5.
2026-10-29 23:01:03 +01:00
jschoubben 64a5074d58 Assigner: lay each held value of the group's own pull requests at their heads over its module's values in the composition check, judged against the head's declaration and named, so a check reads the held values itself (issue 1953 D7, ADR 0565 §5) 2026-10-29 22:43:27 +01:00
jschoubben 7b7796b6fa Assigner: hold a want while a provision its module requires has no key yet, and say it once it stays without one past the delivery bound, so a module never starts without its database unseen (issue 1954 D1)
Pins mesh-sdk 66ff33d.
2026-10-29 22:41:44 +01:00
jschoubben f56eb62d8b Assigner: hold a want until every provision its module consumes on that node is provided, and say a failed one as a finding, so a module never moves ahead of what it needs (issue 1954 D1, ADR 0570 §4)
Pins mesh-sdk 6e39ed1.
2026-10-29 22:39:38 +01:00
jschoubben 78f03b7422 Watchman: say needs-operator from mesh-provisioner's provisions in waits, so the row the haves could not carry has its facts on the bus (issue 1954 D2)
Pins mesh-sdk 4a96793.
2026-10-29 22:37:39 +01:00
jschoubben 3aec7f9b02 Assigner: hold the seat mesh-wants, keep a person's upgrade policy and judge moves by it, say a policy split with the controller as a finding, and answer the bus step, so the policies and the bus step leave the controller (issue 1953 D4-D6) 2026-10-29 22:34:08 +01:00
jschoubben 2488d839ab Bridge, indexer and view: carry the catalogue's predefined list on its merged message, and keep it and each open pull request's config declarations in the indexer's states, so mesh-config reads them in place of the controller's mirror and pull-declares (issue 1953 D1-D3)
The view's tools read both states. Pins mesh-sdk 2d2aa7b.
2026-10-29 22:29:04 +01:00
jschoubben 2531cd42b6 Checker: hold that the change's own failing test ends its level there, as mesh-sdk's build asks now do, so a failing check costs only its own tests (issue 1289)
Pins mesh-sdk 2d2aa7b.
2026-10-29 22:24:50 +01:00
jschoubben 5235bb2e79 Assigner and grants part: record hand acts and warrants through the seat mesh-hand-acts, the controller's log only while that seat has no holder and the controller holds its lease (issue 1953 D8)
Pins mesh-sdk 2d2aa7b in the L5 modules.
2026-10-29 22:24:00 +01:00
jschoubben 3b6f3af38e Grants part: create a module's log with the max age it declares, so a log's entries age out as its manifest says (issue 1952)
Its nats fixture gives the tools bundle's account a secret the tool runtime can reach, as mesh-sdk 2e5cc4f asks. Pins mesh-sdk 2e5cc4f.
2026-10-29 22:19:35 +01:00
jschoubben 15a15cbda5 Watchman: observe S1, S9 and S21 as the controller does and stop the needs-operator row, so the keeper's compare can empty with no condition lost (issue 1952 WM2, WM3, WM5)
S1 is urgent after half an hour for a control node (the node running mesh-controller). S9 also hears the bus module's
slow consumers and closed connections, each closure counted once. S21 is one finding per heard machine from its failed
haves and its provisioner clients' reports (PROV_APPLIED). DEAD_LETTERS is counted again at a letter's age and every
five minutes while letters are held, on the watchman's own pulse. needs-operator waits for L6: its inputs are not on the
bus. Pins mesh-sdk b6ef01d.
2026-10-29 21:59:46 +01:00
jschoubben 7dbf7a19a1 Grants part: compose a principal for every own bus account, a tools bundle's with what it may remove, so the bus tools remove the controller's dead-letter consumer and the engine's acts on their own account (issue 1952 S5)
Pins mesh-sdk 7000bd5.
2026-10-29 21:56:13 +01:00
jschoubben e1fd3eb0f6 View: answer status from the buckets it reads and derive motion with mesh-sdk's MotionOf, so status leaves the controller and the keeper judges a delivery as the view does (issue 1952 V1) 2026-10-29 21:52:42 +01:00
jschoubben 73280f85c3 Assigner: write each condition it says as a finding too, and say it straight only until the take-over, so its holds reach the operator through the condition keeper and never twice (issue 1952 WM6) 2026-10-29 21:49:25 +01:00
jschoubben b8e8981730 Grants part: hear root-not-free from the seat the take-over key names, never both, so the switch to the condition keeper moves it with the router (issue 1952 G1)
Watchman: subscribe exactly what its grant lets it hear (buscompose.WatchmanHears). Both pin mesh-sdk 69fcb6c.
2026-10-29 21:46:29 +01:00
jschoubben da787ae360 Watchman: hold mesh-watchdogs and write its watchdogs as findings, so health can leave the controller (issue 1952 WM1-WM5)
The signals rows S1, S8, S9, S10, S11, S21 and the haves' unhealthy and needs-operator are pulses whose expiry is a finding;
stuck keys and presence are its own findings; it keeps the dead letters from the lease write on its own consumer, lists and
shows them, and refuses deliver and drop until the hand-act log's home (issue 1953). Tools read every state it keeps and
start resync. Pins mesh-sdk 2a04bf0.
2026-10-29 21:44:18 +01:00
jschoubben 013b7420f6 mesh-grants: hold a grant only while the loaded list lacks its permissions, read from the part's own record of the lists it sealed, so a release whose bus tools are down still moves (issue 774) 2026-10-29 21:18:21 +01:00
jschoubben c7fbc530da mesh-grants: compose the user list from the running and the trunk manifest, and write a grant only once the loaded list carries it, so L3's parts never use a subject the bus has not loaded (issue 774) 2026-10-29 21:15:55 +01:00
jschoubben 98ad1aa252 Pin mesh-sdk 22c9ddd in the modules L3 changes, the L3 re-land group's sdk head (issue 774) 2026-10-29 21:07:00 +01:00
jschoubben b01cf63237 Revert "Merge pull request 'Revert L3: its node-engine removed every held module on novox, the bus server among them (issue 774)' (#109) from revert/774-l3 into main"
This reverts commit c1b4533201, reversing
changes made to d8274204fb.
2026-10-29 21:05:14 +01:00
mesh-tools 883d976b5f Merge pull request 'Builder: announce a done build again, so the controller composes a module whose composition and grant were lost (recovery of novox and ace, 2026-10-29)' (#111) from fix/announce-done-again into main 2026-10-29 19:47:58 +00:00
jschoubben ba4bfea0a4 Remove the PR description draft from the branch; it is kept outside the repository
mesh/delivery merged: source.main moved to novox/mesh-delivery@883d976b5f00
mesh/check checking ba4bfea0a407 on 883d976b5f00
2026-10-29 17:59:28 +01:00
jschoubben fb7ad5cd73 Add builder_announce_again so a done build can be taken in and composed again
Recovery of photos and link2pay on novox, 2026-10-29: their composition and grant were lost and no tool wrote a done key again.
Quick fix: rewrite the done key unchanged as a new revision. Thorough fix: ADR 0547, the node agent composes its own and the
assigner stops waiting on controller compositions (design 47).
2026-10-29 17:58:39 +01:00
mesh-tools 5e931eac19 Merge pull request 'The assigner never keeps a want back because a provider is unhealthy, and the canary gates only modules that run (issue 1960)' (#110) from fix/1960-no-hold-spread into main 2026-10-29 15:35:28 +00:00
jschoubben d54eab4052 Never keep a want back for an unhealthy provider, nor a first want for the canary where nothing runs: the module is wanted and its provider down is said (issue 1960)
mesh/delivery merged: source.main moved to novox/mesh-delivery@5e931eac191a
mesh/delivery-group group fix/1960-no-hold-spread merged: every member is merged
mesh/check pass d54eab4052d1 on c1b4533201c8 | unit 3318 run, 1951 kept, 38 skipped | integration 3994 run, 2125 kept, 38 skipped | over bound: unit, …
On novox on 2026-10-29 the assigner wrote no want for de-spiegel and link2pay (rule 5: no provider of route runs
healthy) nor photos (rule 5: no provider of s3-bucket), and none where no node of a module was online (rule 3). A
module with no want is on no list and nothing applies it; a site runs while the proxy in front of it is down.

- Quick fix: skip rules 3 and 5 for the three modules' first wants. Leaves rule 5 holding every move and the
  canary holding new assignments.
- Thorough fix, chosen: rule 5 holds no want; it is said per node (Outcome.Unreached, journaled once per words by the
  pass). The canary (rule 3) no longer holds the first want of a node that does not run the module. A node that runs
  it keeps the canary order: at an older composed and granted fingerprint of the new form its first want stays at what
  it runs; at an old-form build it waits for the canary as before (issue 1945); at an older fingerprint with no
  composition or grant it gets F. This departs from ADR 0429 §6, which makes every provider healthy a rule of every want.

Tests: rows changed in the existing rule tests, and rows for today's modules (route-proxy down: de-spiegel and
link2pay wanted; minio not healthy: photos moves; novox offline: a first want written, a move kept back).
2026-10-29 16:04:39 +01:00
mesh-tools c1b4533201 Merge pull request 'Revert L3: its node-engine removed every held module on novox, the bus server among them (issue 774)' (#109) from revert/774-l3 into main 2026-10-29 08:59:55 +00:00
jschoubben 6771db22ae Revert L3 (mesh-delivery, merge d827420): its node-engine removed every held module on novox, the bus server among them, at 08:54 CET on 2026-10-29 (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@c1b4533201c8
mesh/delivery-group group revert/774-l3 merged: every member is merged
mesh/check being carried to 6771db22ae69 on d8274204fbed | from mesh-catalog 1555241393fe | novox/mesh-delivery#109 in group revert/774-l3
L3 lands again with two fixes: a held module is never removed, and the grants cover L3's new subjects.
2026-10-29 09:42:29 +01:00
mesh-tools d8274204fb Merge pull request 'L3: mesh-grants writes the wanted accounts and grants, the checker's try seat, placement alone after the lease (issue 774, ADR 0561), rebased onto L2' (#105) from feat/774-vault-mints-l2 into main 2026-10-29 07:44:27 +00:00
jschoubben 17c79f25f0 Merge remote-tracking branch 'origin/main' into l3-md-main
mesh/delivery merged: source.main moved to novox/mesh-delivery@d8274204fbed
mesh/delivery-group group feat/774-vault-mints-l2 merged: every member is merged
mesh/check pass 17c79f25f047 on d4cbfc135918 | unit 1601 run, 5122 kept, 47 skipped | integration 1637 run, 5215 kept, 49 skipped | over bound: unit, …
2026-10-29 02:15:27 +01:00
mesh-tools d4cbfc1359 Merge pull request 'Assigner pins mesh-sdk 76f7c4d, which defines the seat mesh-delivery-checker, so L3's check can compose (issue 774)' (#108) from fix/assigner-pin-checker-seat into main 2026-10-28 23:59:03 +00:00
jschoubben e0af965d8b Assigner pins mesh-sdk 76f7c4d, which defines the seat mesh-delivery-checker: the check composes with the running assigner's mesh-sdk, so L3's checker claim is refused until this runs (issue 774, kind of issue 1585)
mesh/delivery merged: source.main moved to novox/mesh-delivery@d4cbfc135918
mesh/check being carried to e0af965d8b40 on d4cbfc135918 | from fa075dc880af
2026-10-29 00:35:07 +01:00
jschoubben 64d364a768 Assigner pins mesh-sdk 76f7c4d, a pull request's head holding 8eafc0c and 77b48a4: the checker refuses a pin to a merge commit on main (issue 774)
mesh/delivery-group group feat/774-vault-mints-l2 ready: every member ready, and composed together they pass
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 64d364a7686c on d4cbfc135918 | unit 1319 run, 5401 kept, 47 skipped | integration 1359 run, 5490 kept, 49 skipped | over bound: unit, …
2026-10-29 00:20:03 +01:00
jschoubben 2caaa6fc92 Merge main into L3: the assigner pins mesh-sdk main d747c11, which holds both L3's sdk and the seed's held-as-today fix (issue 774)
mesh/delivery closed: a newer head of the same pull request
mesh/delivery-group group feat/774-vault-mints-l2 checking: 3 of 4 member(s) ready
mesh/check failed: ace: the check of novox/mesh-host#168 failed: the pin of git.novox.be/novox/mesh-sdk/go at commit d4c61ee8ffd0 is on no pull reques…
2026-10-28 23:50:43 +01:00
mesh-tools fa075dc880 Merge pull request 'Assigner: pin mesh-sdk 06b13ec, so a seed's brief shows what stays held as today (issue 1126)' (#107) from fix/1126-seed-held into main 2026-10-28 22:20:54 +00:00
jschoubben f6df07605d Merge remote-tracking branch 'origin/main' into l3r/774-vault-mints
mesh/delivery-group group feat/774-vault-mints-l2 rejected: composed together: the checker's verdict at the members' heads (1c492872d414): shanks
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: shanks: novox/mesh-delivery#105 does not merge into the trunk: files in conflict: modules/mesh-delivery-assigner/go.mod, modules/me…
# Conflicts:
#	modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go
2026-10-28 23:00:04 +01:00
jschoubben fc3883b0b6 Assigner: pin mesh-sdk 77b48a4, so the seed's Approve names the differences it carries (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@fa075dc880af
mesh/delivery-group group fix/1126-seed-held merged: every member is merged
mesh/check pass fc3883b0b650 on 5b2e5bc53776 | unit 1199 run, 1015 kept, 33 skipped | integration 1218 run, 1081 kept, 26 skipped | over bound: unit (…
2026-10-28 22:41:03 +01:00
mesh-tools 5b2e5bc537 Merge pull request 'The checker writes a loud verdict where it cannot judge, so no Approve waits silently (issue 1733)' (#104) from fix/1733-no-hour into main 2026-10-28 21:29:56 +00:00
jschoubben 25953c6ae1 Assigner: pin mesh-sdk 06b13ec, so a seed's brief shows what stays held as today (issue 1126)
mesh/delivery-group group fix/1126-seed-held checking: 2 of 3 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 25953c6ae110 on 5b2e5bc53776 | novox/mesh-delivery#107 in group fix/1126-seed-held
2026-10-28 22:18:31 +01:00
mesh-tools 38f89c6976 Merge pull request 'The assigner serves the seat mesh-placement's verbs on the seat's own name (issue 1126)' (#106) from fix/1126-seat-names into main 2026-10-28 21:14:13 +00:00
jschoubben 49ea5c96e3 Assigner: serve the seat mesh-placement's verbs as <seat>.<verb>, the name the tool runner serves a seat's verb from (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@38f89c6976c9
mesh/check being carried to 49ea5c96e390 on 38f89c6976c9 | from 1f6f4eda3208
2026-10-28 22:07:05 +01:00
jschoubben 7692f201df Pin mesh-sdk 8eafc0c, the L3 group's head on mesh-sdk main with #120 and #121 (issue 774)
mesh/delivery-group group feat/774-vault-mints-l2 checking: the checker's verdict is out of date: novox/mesh-sdk@8eafc0c5ce3c was judged on trunk 53f26493697e,…
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 7692f201df51 on 5b2e5bc53776 | novox/mesh-delivery#105 in group feat/774-vault-mints-l2
2026-10-28 21:56:06 +01:00
jschoubben 09da8d741e The assigner's laying again and its seed again know the lease write too, so a proposal laid again after it is judged as its first laying was (issue 774, ADR 0567 §8; re-review) 2026-10-28 21:54:47 +01:00
jschoubben b2351a213d Follow L2 under L3: the builder, checker, indexer and view read the seat mesh-placement's placement, the seat's Leased is the lease handed away, and the view keeps no read of the indexer's placement L2 removed (issue 774, ADR 0567 §8, ADR 0549 §3) 2026-10-28 21:54:47 +01:00
jschoubben 2abceb1913 Pin mesh-sdk 4e6e5cc, the L3 group's head rebased on L2, in the six modules the group changes (issue 774) 2026-10-28 21:54:47 +01:00
jschoubben 685810720e mesh-grants: place by the catalogue's placement alone once the controller's lease is handed away, and refuse any other reader of the assignments in this repository (issue 774, ADR 0549 §3, §6)
Why: after the lease write the assignments are frozen, so the part's union kept a module the catalogue takes off a node
with its account, grant, membership and user-list entry. The lease is read as the assigner reads it
(buscompose.LeaseHandedAway); before it, or with no placement on the bus, the union stands. The read stays declared until
the first change after the lease write, named in modules/assignments-readers with the assigner; a TestGrants row refuses
any other module.json that reads mesh-controller.assignments, as mesh-catalog's checks/placement does for the catalogue.
2026-10-28 21:54:47 +01:00
jschoubben 8e25926e91 mesh-grants: the node-engine's manifest is placed on every node, so its declared state is asserted and created as a placed module's, and it adds no account (issue 774, ADR 0563 §2, §3) 2026-10-28 21:54:47 +01:00
jschoubben 68628bfd78 Pin mesh-sdk 277a5bd, the L3 group's head, in the assigner, builder, checker, indexer, view and grants; the assigner's manifest test names mesh-grants.grants, its integration test no unused import (issue 774) 2026-10-28 21:54:47 +01:00
jschoubben 1dbb7c5740 mesh-delivery-view: node_trusts answers in one call whether every node's record names the vault's signing key, the gate before the controller's lease is handed away (issue 774, ADR 0550 §2, design 47 L3 s6b) 2026-10-28 21:54:47 +01:00
jschoubben 9e3d1d5e68 The assigner reads the catalogue's placement alone once the controller's lease is handed away, and reads the lease to know it (issue 774, ADR 0549 §5, §6) 2026-10-28 21:54:47 +01:00
jschoubben 9a70d697af The view shows a row per module the catalogue's placement puts on a node, and reads the controller's assignments no more (issue 774, ADR 0549 §3) 2026-10-28 21:54:47 +01:00
jschoubben a4116a1de7 Builder, checker and indexer count a build node's toolchain only where the catalogue's placement puts its build module, through mesh-sdk's one gate, and read the controller's assignments no more (issue 774, ADR 0549 §3, issue 1940) 2026-10-28 21:54:47 +01:00
jschoubben 6f721dd536 mesh-grants: say every placed module left without a grant, in the pass and as the condition mesh-grants.grant-missing once the lease is handed away (issue 1126, ADR 0547 §5)
Why: the controller's grant writer skipped link2pay, snake, f1-circuits and txt-game in silence (it read the fingerprint by
their unchanged commit, a build of the old form) and the take-in said 'its grant written'. The part already reads the
fingerprint from the indexer's value, so those four are granted at their src2 done key's fingerprint; a row proves it.
What a pass leaves without a grant (no fingerprint of the new form with the indexer's none2, a refused manifest, no
hashed account) is now said rather than passed over. Quick: log it. Thorough (chosen): the pass's Missing, shown by the
grants tool, and a warning condition raised and cleared on change, as consumer-differs is.
2026-10-28 21:54:47 +01:00
jschoubben 354b9ea396 mesh-delivery-assigner: read grants from the source the lease names (issue 774, ADR 0547 §4)
Why: from the lease write the grants part's state grants is the source and the controller's bucket otherwise; the assigner follows the lease key (a pass only when the source changes), and neither bucket stops its start. Needs the group's mesh-sdk pin (GrantSource, LeaseHandedAway, the assigner a named reader of the lease).
2026-10-28 21:54:47 +01:00
jschoubben 3618831992 mesh-grants: write grants.<module>.<fp> in shadow into the state grants, say grant-written, and show every difference from the controller's in grants_grants (issue 774, ADR 0547 §5)
Why: the assigner switches its grants source when the controller's lease is handed away; until then the written keys are compared with the controller's, so the switch rests on a measured equality. Needs the group's mesh-sdk pin (PartGrantKey) before it builds.
2026-10-28 21:54:47 +01:00
jschoubben 843a1589d7 Fold the seat verbs' test into the try tools test; pin the sdk head (issue 774) 2026-10-28 21:54:47 +01:00
jschoubben fc6d5dcbdb mesh-delivery-checker claims its seat and serves try, try-read and try-end by the tools' own code, so the try outlives the controller (ADR 0547 §10, issue 774) 2026-10-28 21:54:47 +01:00
jschoubben 6d709e1fc1 mesh-grants: grants_accept_hashes releases the clause 7 hold once, for exactly the held users of the held list, with why, said as hashes-accepted (issue 774, ADR 0561 §7, §9) 2026-10-28 21:54:35 +01:00
jschoubben eb11b2c844 mesh-grants: hold a list that changes several loaded users' hashes at once with no rotation by the vault naming them, and raise user-list-held naming them (issue 774, ADR 0561 §7) 2026-10-28 21:54:35 +01:00
jschoubben 67e3ebf22f mesh-grants: grants_accounts reads the account consumers' differences in shadow, where no condition is raised (issue 774, ADR 0561 §6, §9) 2026-10-28 21:54:35 +01:00
jschoubben 9b31746cdb mesh-grants: raise a condition when an account consumer differs from its composed configuration, and bring one to it by hand with grants_consumer_match, never under the controller's lease (issue 774, ADR 0561 §6, §9) 2026-10-28 21:54:35 +01:00
jschoubben ebad58f7e2 mesh-grants: create the durable consumers an account needs from the lease write, compared in shadow until then, so a module assigned after the controller leaves still hears what it consumes (issue 774, ADR 0561 §6) 2026-10-28 21:54:35 +01:00
jschoubben 3c6fb7fc9e mesh-grants: compose the user list again when the vault says account-issued, so a minted account reaches the bus within one pass (issue 774, ADR 0561 §1) 2026-10-28 21:54:35 +01:00
jschoubben 71aa3e86c6 mesh-grants: write the bus accounts the mesh wants to a state only the vault reads, so the vault can mint them; grants_accounts reads it (issue 774, ADR 0561 §2, §9) 2026-10-28 21:54:35 +01:00
jschoubben 2d8ad2e66a Checker: no candidate is left without a verdict (issue 1733)
mesh/delivery merged: source.main moved to novox/mesh-delivery@5b2e5bc53776
mesh/delivery-group group fix/1733-no-hour merged: every member is merged
mesh/check pass 2d8ad2e66a8f on 38f89c6976c9 | carried from 1f6f4eda3208 | unit 470 run, 4760 kept, 13 skipped | integration 503 run, 4844 kept, 22 sk…
Once the gate's one-hour ask bound goes (mesh-catalog fix/1733-no-hour), the
gate asks the operator's Approve only on a verdict, so every path that left
a candidate without one now writes one, from an event and with no timer:
- a group with a member whose repository's trunk head is not known (a pull
  request the checker cannot claim) is not judged in part: its verdict fails
  naming that repository, and every status of the group says so (a pass of
  the others would have let it merge with that member never checked);
- the verdict of heads that cannot be judged replaces whatever the key held,
  such as a running verdict left by a check that can no longer run;
- a verdict key deleted is judged again on its delete (the checker watches its
  verdicts), and a check running when its key is deleted writes it anew.

Tests: three rows at the end of the no-trunk test; counts unchanged.
2026-10-28 21:50:03 +01:00
mesh-tools 1f6f4eda32 Merge pull request 'The checker and the builder use it; checker_ask_again asks a lost check again; the checker's integration bed writes a node record (issue 734's kind)' (#103) from fix/lost-check into main 2026-10-28 20:41:01 +00:00
mesh-tools 769815ac0f Merge pull request 'The assigner's seed and mesh-grants take mesh-wireguard as provided by the mesh (issue 1126)' (#102) from fix/1126-provided into main 2026-10-28 20:39:23 +00:00
jschoubben e7888024a8 Checker: delete a check's asks key at its end, so the state asks holds only the checks that run
mesh/delivery merged: source.main moved to novox/mesh-delivery@1f6f4eda3208
mesh/delivery-group group fix/lost-check merged: every member is merged
mesh/check checking e7888024a826 on 769815ac0fea | novox/mesh-delivery#103 in group fix/lost-check
Re-review of fix/lost-check: writing an empty list at the end kept one key
for every verdict key ever checked. Tests: every row of the lost-ask table
proves the key gone at the end; one row more (deaf after its build module
replaced it, asked again by hand); counts unchanged.
2026-10-28 20:39:50 +01:00
jschoubben 2007a79c45 Checker and builder pin mesh-sdk f91340f, which opens a silent steps watch again
mesh/delivery-group group fix/lost-check checking: 1 of 2 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 2007a79c45c3 on a3a0d8f2ceb3 | novox/mesh-delivery#103 in group fix/lost-check
2026-10-28 20:38:31 +01:00
jschoubben d8286d1790 Assigner and grants: take the private network as the mesh's own through mesh-sdk's one predicate, so the seed is not refused for it (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@769815ac0fea
mesh/delivery-group group fix/1126-provided merged: every member is merged
mesh/check checking d8286d1790fb on a3a0d8f2ceb3 | novox/mesh-delivery#102 in group fix/1126-provided
2026-10-28 20:38:14 +01:00
jschoubben ca74639a74 Checker integration test: the checker says its seventh state, asks, as its own
The new state asks (checker_ask_again's record of each running check's
asks) is kept and not readable, as hearing is.
2026-10-28 20:37:07 +01:00
jschoubben a58a73bb55 Checker integration tests: give the bed's node a record in the have, so the names check finds a name to ask with (ADR 0567 §12, issue 1126)
Since 9db9499 the checker asks no check while no node record is on the bus; the bed wrote none, so eight integration tests failed on main (PR #101's check). The fake build module now records the names an ask carries, and the first test asserts n1 is named.
2026-10-28 20:36:25 +01:00
jschoubben 16594feff1 checker_ask_again: refuse while an ask of the check waits in its build seat's stream
Review of fix/lost-check: an ask no node took yet has no step key, so
asking again then would run the check twice (issue 1791's kind). The
checker keeps the asks each running check put in a new state asks, and the
tool refuses unless every one of them has a step key. Asking again under the
same ask id is not done: the stream drops a second put of an id within its
de-duplication window and keeps both past it, so the waiting copy is never
replaced.

Tests: the hand row of the lost-ask table refuses while its ask waits;
counts unchanged.
2026-10-28 20:36:25 +01:00
jschoubben cb46ff8735 Checker and builder wait through mesh-sdk's HearEnds, so a deaf watch loses no step end in either
The checker's HeardEnds moved to mesh-sdk buildrun (fix/lost-check,
6aeb401): both modules pin it, and the builder, which waits on the same
Asker, now reads an ask's step at each step end of its build module too.
checker_ask_again and checker_status's lost stay in the checker.

Tests: one row in the builder's ask table (its watch deaf, read at a
nudge, done with no new ask); counts unchanged.
2026-10-28 20:31:04 +01:00
jschoubben 6f079f13a8 Checker: never lose a step's end, and ask a lost check again (issue 734's kind)
mesh-catalog #553's verdict stayed running after its step mvsgthyjed5786
passed on shanks at 19:09:31Z: the watch of that one step key, opened at
18:46:05Z, heard neither its start nor its end and never closed, so the
wait had no bound running and nothing would ever judge it.

- HeardEnds stands between the asker and each ask's watch: at every step
  end of the same build module (one watch per steps state), at a
  reconnect, the asks waited on read their keys now and are judged; a
  watch that closes is opened again, at most 3 times.
- A step the build module's next process ended (RestartReason, a delivery
  replacing it) counts as a lost ask: asked again under the claim, at most
  LostAskTries, then failed as not heard, judged again when the nodes change.
- checker_ask_again {group, why} ends a running verdict as asked again by
  hand; the checker hears its verdicts, ends the wait and claims it anew.
  Refused while a step of its pull requests runs, or for a verdict that
  is not running.
- checker_status names such checks under lost, and each step's what.

Tests: one table replaces the given-up test (rows for given up, replaced,
deaf and nudged, deaf and asked by hand); counts unchanged (147 unit, 10
integration in the checker).
2026-10-28 20:27:41 +01:00
mesh-tools a3a0d8f2ce Merge pull request 'Format two assigner files as gofmt writes them, so mesh-delivery's check passes again (issue 1126)' (#101) from fix/1126-assigner-gofmt into main 2026-10-28 19:08:03 +00:00
jschoubben 5fe8ab11f1 Assigner: format pass.go and placement.go as gofmt writes them, which the check requires (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@a3a0d8f2ceb3
mesh/check checking 5fe8ab11f1b4 on a3a0d8f2ceb3
2026-10-28 20:03:16 +01:00
mesh-tools b6eb43dd62 Merge pull request 'mesh-delivery-builder pins mesh-sdk 38dbe1d, so it accepts the assigner's claim on mesh-placement (issue 1126)' (#100) from fix/1126-builder-pin into main 2026-10-28 19:02:33 +00:00
jschoubben dc4419b5c5 Builder: pin mesh-sdk 38dbe1d, so it knows the seat mesh-placement and builds the assigner that claims it (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@b6eb43dd6271
mesh/check checking dc4419b5c50f on b6eb43dd6271
2026-10-28 19:57:29 +01:00
mesh-tools 24181c0242 Merge pull request 'The assigner holds the seat mesh-placement; the indexer, bridge and view drop the repository path (issue 1126, delivery 2 of 2)' (#99) from feat/1126-placement-seat into main 2026-10-28 18:33:09 +00:00
mesh-tools db737df597 Merge pull request 'mesh-grants: pin mesh-sdk 38dbe1d, so the bus loads the node agents' reads of the seat's states first (issue 1126, delivery 1 of 2)' (#98) from feat/1126-grants-first into main 2026-10-28 18:21:36 +00:00
jschoubben dd9acaaeb3 Assigner: bind the seat's states at each pass and say while it waits for them, since their creation can come before this credential may hear it (ADR 0567 §1, review round 3)
mesh/delivery merged: source.main moved to novox/mesh-delivery@24181c024212
mesh/delivery-group group feat/1126-placement-seat merged: every member is merged
mesh/check failed: integration: meshbuildgo/cmd/mesh-build-go.TestAComposeAskIsBuiltOnceAndAnsweredOnTheBus: the compose ask mvsg85b34d25b4 never ende…
2026-10-28 19:14:43 +01:00
jschoubben 451a4dfe74 Grants: pin mesh-sdk 38dbe1d, so the bus loads the node agents' reads of the seat mesh-placement's states before anything reads them (ADR 0567 §9, issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@db737df597c3
mesh/delivery-group group feat/1126-grants-first merged: every member is merged
mesh/check pass 451a4dfe74d9 on 2ac146078f47 | unit 1909 run, 715 kept, 3 skipped | integration 1931 run, 813 kept, 3 skipped | over bound: unit (slow…
2026-10-28 19:11:58 +01:00
jschoubben fb26ca5112 Pin mesh-sdk 38dbe1d, the group's head, in every module the seat touches (issue 1126) 2026-10-28 18:48:56 +01:00
jschoubben 389bb96ae4 Grants: read placement from the seat mesh-placement's state, as every reader does (ADR 0567 §9, issue 1126) 2026-10-28 18:47:49 +01:00
jschoubben 06e668cb90 Assigner: bound answers and asks by pulse keys instead of a timer, ask the operator for a hand-back, follow a partial write through, and call no controller verb for the seed (ADR 0567 §4, §5, §7, §9, review round 2) 2026-10-28 18:46:39 +01:00
jschoubben 297a3cb549 View: build_usage reads each node's capacity as the integer mesh-config resolves for it, default 0, since no repository names a node (ADR 0567 §12, issue 1126) 2026-10-28 18:35:14 +01:00
jschoubben 1d0467d4c3 Forge bridge: hold that a catalogue merge carries no placement against the retired field, as mesh-sdk now names it (ADR 0567 §11, issue 1126) 2026-10-28 18:34:19 +01:00
jschoubben 9db9499267 Checker: put every node's name on each check ask for the names check, so no repository lists them (ADR 0567 §12, issue 1126) 2026-10-28 18:34:02 +01:00
jschoubben deda4c985e Assigner: fold three small tests into their neighbours, so the seat's tests leave the counts flat (test budget) 2026-10-28 18:32:11 +01:00
jschoubben 06df6a6167 Assigner: prove a proposal answered, approved and written whole on the bus, and a moved key laid again (ADR 0567 §5, issue 1126) 2026-10-28 18:31:44 +01:00
jschoubben 1718d4b1d7 Assigner: follow every proposal from its answers through the Approve to the write at its revisions, keeping the controller in step (ADR 0567 §3-§8, issue 1126) 2026-10-28 18:31:43 +01:00
jschoubben 16b71ca685 Assigner: serve the seat's seven verbs, so a person proposes, withdraws, adopts, hands back and reads placement through the seat (ADR 0567 §2, §7, §10, issue 1126) 2026-10-28 18:31:43 +01:00
jschoubben 5d57b78fe9 Assigner: claim the seat mesh-placement with its stream, invokes and the resolved values it composes with (ADR 0567 §1, §6, §8, §9, issue 1126) 2026-10-28 18:22:40 +01:00
jschoubben 36332abf35 Assigner: drop the pull request's placement check and hold the seat's states as its own, since a placement change is no repository change (ADR 0567 §1, §4, §11, issue 1126) 2026-10-28 18:19:12 +01:00
jschoubben dbbdc95c0b View: drop a comment about the removed placement read 2026-10-28 17:59:34 +01:00
jschoubben fb0be84e24 View: drop indexer_placement and its reads, the seat mesh-placement answers placement (issue 1126) 2026-10-28 17:59:27 +01:00
jschoubben 730e44d762 Forge bridge: carry no placement files, placement is the seat mesh-placement's (issue 1126)
A catalogue merge or pull request no longer lists or reads placement/, so the Listed read and ConvertWith have no use and go.
2026-10-28 17:58:58 +01:00
jschoubben f4dd334b49 Indexer: stop holding the catalogue's placement, it is the seat mesh-placement's now (issue 1126)
Deletes the placement and candidate writes, the reread tool and its forge reads, the two buckets and the placement-changed event.
D1a and D1b are one commit: the index package, the named reads, the tools and the manifest do not compile apart.
2026-10-28 17:58:09 +01:00
mesh-tools 2ac146078f Merge pull request 'The delivery view forgets a write or event the bus refused, so 'on the machines' is not left stale (issue 1126)' (#97) from fix/1126-reach-now into main 2026-10-28 15:18:54 +00:00
jschoubben 726bbf1f1f The view writes a reach or says an event again when the bus refused it, so a refused write never leaves the reach state at an older have (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@2ac146078f47
mesh/check being carried to 726bbf1f1ffa on 2ac146078f47 | from e753009fbc71
The view remembered a reach value, a reached head and a motion event as written or said before the bus took them; a write refused at its start (no responders) was never made again while the derivation stayed the same, and mesh-delivery.show said mesh-delivery-view not running on shanks while node_haves said it running.

Quick fix: forget only the refused reach write. Thorough fix, chosen: every write and event the view makes after its lock (reach, reached head, delivered revision, motion) answers its error, and the model forgets what was refused, so the next change it takes in makes it again; one place, the writer of the state both mesh-delivery.show and knowledge-base.progress read.
2026-10-28 16:11:52 +01:00
mesh-tools e753009fbc Merge pull request 'The indexer never moves a module backwards: a late merged message writes nothing; indexer_heard and indexer_module_history read what it heard and kept (issue 1126)' (#96) from fix/1126-index-order into main 2026-10-28 14:44:34 +00:00
mesh-tools f39bc10e3b Merge pull request 'builder_build_again: a person can build a failed key again, as the builder does by itself (issue 1126)' (#95) from fix/1126-build-again into main 2026-10-28 14:27:08 +00:00
jschoubben 3629bcf057 Read only gitea's 404 status as not-found, so a 502 naming a sha with 404 in it is redelivered, not written backwards (issue 1126, review)
mesh/delivery merged: source.main moved to novox/mesh-delivery@e753009fbc71
mesh/check being carried to 3629bcf057c3 on e753009fbc71 | from f39bc10e3be7
2026-10-28 15:01:51 +01:00
jschoubben 743a3618ea Keep back only a merge older than the one taken in, asked once per message before any write (issue 1126, review)
Asking whether the merge contains the held commit refused forward merges
forever on a diverged or rewritten main or a squash-merged registered head,
failed every later merge when the forge no longer had the held commit, and
cost one forge call per held commit. The question is now the reverse: a merge
is kept back only when it is in the taken merge and differs from it. A merged
message asks it once, before it writes anything, and a late message writes no
module, deletion, placement or taken key. A take-in asks it once for its
module. A 404 is not known to be older and is written, and said; any other
failure to say leaves the message for redelivery.
2026-10-28 14:56:40 +01:00
jschoubben 77d738d973 Never let a merge move a module backwards, and make the forge messages and a module's history readable (issue 1126)
The indexer wrote mesh-catalog's merge 9cdac09a back over the later 69056948,
rolling kernel and mesh-build-image back on the nodes: a merged message's
commit was written whatever the held one was. Before writing or deleting a
module's value, the indexer now asks the forge whether the merge contains the
held commit, keeps the held value when it does not and says so, and leaves the
message for redelivery when the forge cannot say. Each applied merged message
is said in one line with the modules written.

No tool showed what the indexer heard of FORGE nor a modules key's history
with times, so the cause could not be proven: the indexer now records each
FORGE message it handles (sequence, deliveries, times, commit, outcome) in its
state heard, read by indexer_heard, and indexer_module_history reads
modules.<module>'s kept values with their times.

Quick fix (Src order in writeOverlay) rejected: Src is 0 once the source moved
on, as here. Thorough fix chosen: the order is the forge's, on every path that
writes a module (merged message, take-in).
2026-10-28 14:48:13 +01:00
jschoubben a3b4ac79af builder_build_again: a look the builder queued during the by-hand build still runs; say a look that ended early is not asked
mesh/delivery merged: source.main moved to novox/mesh-delivery@f39bc10e3be7
mesh/check being carried to a3b4ac79afaa on f39bc10e3be7 | from 31bbf207ef3b
2026-10-28 14:32:33 +01:00
jschoubben c6dd04a7cc builder_build_again: register the look in one critical section, and answer what the build did
Review round 1 (issue 1126 note 72): the inflight check, the watch and the look were three locks, so a second request or the builder's own look could build the key twice; the answer said asked when the look wrote nothing or the build failed before its asks.
2026-10-28 14:30:31 +01:00
jschoubben 48cd554f46 Add builder_build_again: build a failed key again by hand as a new revision (issue 1126 note 72)
The builder retakes a failed key only when it looks at its module again, which nothing does for an unchanged module. The tool makes it look now, with the claim's own rules, and refuses a key that is done or waiting.
2026-10-28 14:25:03 +01:00
mesh-tools 31bbf207ef Merge pull request 'Say loudly what cannot be judged, read only a merge's changed directories, and move the taken key only by an applied merge (issues 1422, 1126; group fix/1422-trunk-unknown)' (#94) from fix/1422-trunk-unknown into main 2026-10-28 10:34:50 +00:00
jschoubben 2b801f5bd4 indexer_take_in reads a held module again only when its record is behind the source's head, and the two new tests are rows (issue 1126, review)
mesh/delivery merged: source.main moved to novox/mesh-delivery@31bbf207ef3b
mesh/delivery-group group fix/1422-trunk-unknown merged: every member is merged
mesh/check being carried to 2b801f5bd49a on 31bbf207ef3b | from ae7ec702a5e9, mesh-catalog 9fd95693db54
A head older than the record, or one whose order the forge cannot say, would have written
an older manifest back. The new tests become rows of existing ones: the repository is over
its 500 unit tests.
2026-10-28 11:18:01 +01:00
jschoubben 790653dfde indexer_take_in reads a held module again at the source's head when its record stands behind it (issue 1126)
A merge whose merged message never reached the indexer had no recovery: take-in answered
held for mesh-vault, whose record stayed at 78d7ef7 after mesh-catalog#535.
2026-10-28 11:10:09 +01:00
jschoubben 89be76bb6a The forge bridge keeps the directories a failed try read whole, so a large merge converges over its tries (issue 1126)
Without it the first merge after the bridge starts, with nothing kept, still reads all 140
catalogue directories in one 30 s try and times out as before.
2026-10-28 11:10:09 +01:00
jschoubben 704fc8d467 The forge bridge reads a merge's module directories only where their tree changed (issue 1126)
mesh-catalog's 140 module directories are 420 forge reads per merge, past the 30 s bound
of a handled event while the forge is busy: the merged message of #535 timed out on every
try, and #3fb7951 needed seven (issue 1458 met the same kind at 131). A tree hash names its
bytes, so what was read at a tree is kept in the new state inputs and in memory, and a
merge reads only the directories it changed.
2026-10-28 11:10:09 +01:00
jschoubben 458e403a04 The indexer's taken key moves only by a merge it applied, never by the gate's source head (issue 1126)
A drained forge stream does not say a merge was applied: the forge bridge was still
retrying mesh-catalog#535's merged message when the indexer started, and the source's
head moved taken.mesh-catalog to 9fd9569, so the view and show said the merge was on the
machines while mesh-vault's record stood at 78d7ef7 (ADR 0556 clause 1).
2026-10-28 11:10:09 +01:00
jschoubben 93c0ae19dd Forge bridge: write a head pushed to a pull request into another base as closed, and say so (issue 1422)
The checker judges pull requests into main only and source.main names main's head only (ADR 0484 §11), but a live update
into another base (de-spiegel#6 into nox-mesh) became a candidate, unlike at the start (OpenPulls) and at its merge. Once
the gate seeds a repository's trunk head, such a candidate would be judged against main's head: a verdict that lies.
2026-10-28 11:03:39 +01:00
jschoubben 4e9b076408 Checker: say loudly that a pull request cannot be judged when its repository has no trunk head (issue 1422)
A repository never merged through the gate has no entry in source.main, so its pull requests were dropped without a log
line, a verdict or a status, and checker_status counted them as new. Now the verdict says it cannot be judged and why,
the forge status is error with those words, checker_status names it under cannot_be_judged, the log says it once per
head, and the heads are judged whole once the trunk head is known. Quick fix: this. Thorough fix (the gate seeding
source.main from the pull message's base head) is still to do: it needs mesh-sdk, the forge bridge and the gate.
2026-10-28 11:03:39 +01:00
mesh-tools ae7ec702a5 Merge pull request 'Delete a closed pull request's placement candidates, and give indexer_placement a summary (issue 1126, group fix/1126-seed-same)' (#93) from fix/1126-seed-same into main 2026-10-28 09:46:34 +00:00
jschoubben 2cccbfd096 mesh-delivery-indexer and mesh-delivery-view pin mesh-sdk 3028c7e, the group's head after review round 2 (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@ae7ec702a5e9
mesh/delivery-group group fix/1126-seed-same merged: every member is merged
mesh/check checking 2cccbfd096d8 on ae7ec702a5e9 | novox/mesh-delivery#93 in group fix/1126-seed-same
2026-10-28 10:02:46 +01:00
jschoubben 8f5e7f0c8f Fill go.sum for mesh-sdk 1062b33, the group's head (issue 1126) 2026-10-28 09:53:34 +01:00
jschoubben 75d3c40f41 mesh-delivery-indexer and mesh-delivery-view pin the group's mesh-sdk head 1062b33 (issue 1126; go.sum is filled once that head is pushed) 2026-10-28 09:51:48 +01:00
jschoubben 63582e44f3 Delete a closed pull request's placement candidates even when its closed message was missed, and give indexer_placement a compact summary form (issue 1126) 2026-10-28 09:25:09 +01:00
mesh-tools 4d4743d900 Merge pull request 'mesh-delivery-view passes a node's placement answer through as written, so its bodies reach readers (issue 1126, group fix/1126-shadow-bodies)' (#92) from fix/1126-shadow-bodies into main 2026-10-28 07:46:28 +00:00
jschoubben 289d1e8fa6 Format mesh-delivery-view's read.go as gofmt writes it (issue 1126, the group check's failure)
mesh/delivery merged: source.main moved to novox/mesh-delivery@4d4743d90065
mesh/delivery-group group fix/1126-shadow-bodies merged: every member is merged
mesh/check checking 289d1e8fa69a on 4d4743d90065 | novox/mesh-delivery#92 in group fix/1126-shadow-bodies
2026-10-28 01:49:54 +01:00
jschoubben 968b9bf2e6 mesh-delivery-view: a node's candidate answer is shown as the node wrote it; pin mesh-sdk 362a3f1 (issue 1126, probe #532)
mesh/delivery-group group fix/1126-shadow-bodies rejected: composed together: the checker's verdict at the members' heads (46e1e846f8db): unit: modules/mesh-de…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed (2 tests): unit: modules/mesh-delivery-view.gofmt: not as gofmt writes them (run gofmt -w): modules/mesh-delivery-view/internal/view…
The bodies of each difference never reached the answer shown: the view decoded
the node's answer with its mesh-sdk, which did not know the field, and encoded
it again. The answer now rides through as stored; a decoded copy only says its
state.
2026-10-28 01:39:34 +01:00
mesh-tools bab2e1391a Merge pull request 'mesh-grants: stop rewriting the shadow user list every 3 s when nothing changed (issue 1126, group fix/1126-switch-safe)' (#91) from fix/1126-switch-safe into main 2026-10-27 23:52:23 +00:00
jschoubben 663c9cbc34 mesh-grants: pin mesh-sdk 7359d18, the group's head (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@bab2e1391ae5
mesh/delivery-group group fix/1126-switch-safe merged: every member is merged
mesh/check pass 663c9cbc348d on a49fd9cc65f7 | unit 2992 run, 1008 kept, 36 skipped | integration 3025 run, 1102 kept, 29 skipped | over bound: unit (…
2026-10-28 00:34:13 +01:00
jschoubben 3d916a685e mesh-grants: the same list said again moves neither the sealed key nor the shadow list (issue 1126)
Live since the take-over the shadow list was written every 3 s (revision 99270
to 99357 in 90 s, nothing differing): the sealed key read back says [] for the
nil left-out a pass composes, so every pass wrote the key again; each write
moved the bus node's report of what it loaded, whose revision and time were
inputs of the shadow list, and woke the next pass. The bus itself reloads only
when the list's bytes change (its log: two configurations alternating with the
placement, f8567865 and 473f9ad3), so this loop was load, not the reload.
2026-10-28 00:20:44 +01:00
mesh-tools a49fd9cc65 Merge pull request 'mesh-grants: take-over allowed when the grants part's list only adds rights, every loss still refused (issue 1945, ADR 0562)' (#90) from fix/1945-takeover-adds into main 2026-10-27 21:36:25 +00:00
jschoubben 3192d89edf mesh-grants: refuse a take-over over an uncompared deny; ask words only for added rights
mesh/delivery merged: source.main moved to novox/mesh-delivery@a49fd9cc65f7
mesh/check checking 3192d89edfcb on a49fd9cc65f7
Review round 1 of issue 1945's fix found two ways the guard could let a loss
through or hide one from the operator.

A publish deny was never compared: grants.User carried no deny, so a widened
allow that brings a new deny (a lost publish right) passed as an added right.
The user now carries its deny; a deny the shadow adds is a loss, and while
nats_users answers no deny for the loaded list, a user whose grants differ
and whose shadow grant has a deny refuses, named as a deny the loaded list
cannot be compared on. None of today's 41 differing users carries a deny
(only the four node-tools users do, and they are the same in both lists).

The ask said "takes none away" whenever rights were added, whatever else
differed. Its words now name the added rights only when nothing else refuses
(Refusals: unread list, Removing, a missing hash), and otherwise say what the
yes would be refused for, naming the users; Decide uses the same Refusals.

issue 1945
2026-10-27 22:33:53 +01:00
jschoubben 9edfcc40e9 mesh-grants: allow take-over when the lists differ only by added rights
The operator's yes to take-over was refused twice (2026-10-26 13:48 and
2026-10-27 21:19 UTC) because 41 users differ, every difference a right the
grants part's list adds. The controller's list is not changed (ADR 0547 §1),
so ADR 0547 §8's equality could never hold again. ADR 0562 changes it:
take-over is allowed when no user is added or dropped, no loaded user loses a
publish, subscribe or responses right and every hash is held; the ask names
the added rights by user and kind, and a yes after they changed is refused.

Tests: rows from the captured live comparison of 21:19 (testdata, no
hashes) in the compare table, and rows in the take-over table for a right
removed, added rights changed since the ask, and only added rights recorded.

issue 1945
2026-10-27 22:28:22 +01:00
mesh-tools 68bdda1a4d Merge pull request 'mesh-grants and the assigner: grant every node the served keys it composes with (issue 1126, group fix/1126-node-creds)' (#89) from fix/1126-node-creds into main 2026-10-27 20:32:34 +00:00
jschoubben bb7f2ff365 Merge remote-tracking branch 'origin/main' into fix/1126-node-creds
mesh/delivery merged: source.main moved to novox/mesh-delivery@68bdda1a4dbf
mesh/delivery-group group fix/1126-node-creds merged: every member is merged
mesh/check checking bb7f2ff365c7 on 68bdda1a4dbf | novox/mesh-delivery#89 in group fix/1126-node-creds
2026-10-27 21:18:57 +01:00
mesh-tools da51481620 Merge pull request 'mesh-grants: a seat keeps its holder under any placement, and no list or membership leaves a claimed seat without one (issue 1945)' (#88) from fix/1945-grants-seats into main 2026-10-27 20:14:04 +00:00
jschoubben c93f0d15e4 Grant the served keys of the trunk's manifest too, and pin mesh-sdk 7a8e9f9 in the assigner and mesh-grants on main's bus helpers (issue 1126, review round 1)
A node composing a consumer reads the served keys of the trunk's manifest of a
provider elsewhere, which may serve a setting the build it runs does not.
2026-10-27 21:13:41 +01:00
jschoubben e0cb285993 Merge remote-tracking branch 'origin/main' into fix/1126-node-creds
# Conflicts:
#	modules/mesh-delivery-assigner/go.mod
#	modules/mesh-delivery-assigner/go.sum
2026-10-27 21:12:25 +01:00
jschoubben a7d03f7c82 Pin mesh-sdk 538ffba, the group's head, in the assigner and mesh-grants (issue 1126) 2026-10-27 21:04:08 +01:00
jschoubben ea3bfa5e6e Grant every node the keys a provider elsewhere serves from, and give the composition check frombus's new reads, pinning mesh-sdk cebc651 (issue 1126)
mesh-grants declares each module's served settings (catalogue.ServedSettings), so
the user list grants every other node's agent those keys of mesh-config_resolved
alone: shanks then settles what novox's mailu serves. The assigner's check of
every node reads no build, archive or served value: it composes as before.
2026-10-27 21:02:49 +01:00
jschoubben ec0b7e2dcc Keep holding a membership that would drop a capability its holder still asks for (issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-delivery@da514816204b
mesh/check being carried to ec0b7e2dcc06 on da514816204b | from 24cb379434f5
A dropped verified-sender comes from a root-free word that failed for one
pass, not from a release, and losing it cut the operator's approvals on
2026-10-27. Holdings.CapabilitiesLost holds such a membership with its own
reason while the holder is assigned and its manifest still asks for it; a
dropped tool or field and a moved seat stay published.
2026-10-27 20:56:09 +01:00
jschoubben feb32600bb Hold a list or membership only when a seat would lose its last holder (issue 1945)
Holding the user list on any loss would freeze it after take-over at the
first release that drops a tool or moves a seat (ADR 0547 §7), with nothing
to accept a deliberate loss; memberships had the same gap. The lead's decision
in review round 1: one pure guard, Holdings.LastHolders, holds the list and
each membership only when a seat held in force would have no holder in what
is composed now while an assigned module still claims it, the 09:21 kind.
A dropped tool, verb or field, or a seat handed to another node, is written.
2026-10-27 20:54:30 +01:00
jschoubben bc1978ec67 Keep every seat's holder under a placement, and hold a user list that would lose a grant (issue 1945)
The user list of 2026-10-27 09:21 granted no holder for nine seats: the seeded
placements hold only the seats the controller had on record, and the grants part
read those holds as every holder (records.go heldWherePlaced). A placement's
holds are now the holders on record, as catalogue.World's Holdings are: a mesh
seat no placement holds is held by derivation, wherever its claimant is placed.

The membership loss guard (Losses) did not cover the user list: after take-over
the list that lost those seats went out. UserLosses applies the same guard per
user against the loaded list, and holdsBack holds a list that would lose a grant,
or whose loaded grants cannot be read, raising the held warning.

Quick fix chosen over moving the on-record rule into mesh-sdk: mesh-grants is the
only part reading placement holds itself; the others pass them to the catalogue.
2026-10-27 20:54:30 +01:00
mesh-tools 24cb379434 Merge pull request 'Every delivery part opens its bus connection through mesh-sdk's module helpers (issue 1242)' (#87) from fix/1242-bus-helper into main 2026-10-27 19:49:22 +00:00
jschoubben 96be0c3377 Move every delivery part's bus connection onto mesh-sdk's module helpers: credential, pinned dial, own-stream subjects, durable consumer, and refuse a hand-written one (issue 1242)
mesh/delivery merged: source.main moved to novox/mesh-delivery@24cb379434f5
mesh/delivery-group group fix/1242-bus-helper merged: every member is merged
mesh/check checking 96be0c3377c4 on c38e81c7f5e9 | novox/mesh-delivery#87 in group fix/1242-bus-helper
2026-10-27 20:40:49 +01:00
mesh-tools c38e81c7f5 Merge pull request 'A want's self comes from the current placement alone, so a node no longer placed gets the controller's composition instead of a stale self want (issue 1126)' (#86) from fix/1126-self-wants into main 2026-10-27 15:25:20 +00:00
jschoubben bb63d83f84 Derive a want's Self from the placement now, in every rule, so a want written while a node was placed does not outlive it (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@c38e81c7f5e9
mesh/check being carried to bb63d83f849e on c38e81c7f5e9 | from 2915d74822cd
After the placement seed's revert the rules that hold a want (a hold, rules 3 and 5) kept g14's and shanks's Self wants: the held branch copied them whole, and the rule branch wrote a want naming no composition, which EncodeWant refuses. Module.Self, set once from the placement, is now the one source, and every want the rules write or keep is derived from it: an unplaced node's kept want names the controller's composition of the same fingerprint, asked as any record move; with none, it stays and rule 7 says why.
2026-10-27 16:19:02 +01:00
mesh-tools 2915d74822 Merge pull request 'A placement pull request is carried, kept as candidates, and checked against every placed node's answer before it merges (issue 1126, ADR 0559)' (#85) from feat/1126-l2b into main 2026-10-27 07:21:16 +00:00
jschoubben 06d4f45e18 Pin mesh-sdk cadd7b1, the group's final head, in every member module (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@2915d74822cd
mesh/delivery-group group feat/1126-l2b merged: every member is merged
mesh/check checking 06d4f45e1885 on 2915d74822cd | novox/mesh-delivery#85 in group feat/1126-l2b
2026-10-27 02:44:11 +01:00
jschoubben 77dacfbb17 Grant the node agents the placement candidates through the grants part at the group's sdk head, and show in indexer_placement the answer of every node placed at the head or on the trunk, so the seed's nodes can answer and be seen (issue 1126, ADR 0559 §3, §7; review round 1) 2026-10-27 02:43:00 +01:00
jschoubben d8592d9a4f Pin mesh-sdk 062c29c, the group's final head, in every member module that uses the candidate contract (issue 1126) 2026-10-27 02:30:16 +01:00
jschoubben ea4a163187 Pin mesh-sdk a37d262, the group's head, in the bridge, the indexer and the view (issue 1126) 2026-10-27 02:29:28 +01:00
jschoubben fc92202e04 Take a later composition into a verdict that failed only because placed nodes had not answered, so a sleeping node's answer passes the check on its event with no new check (issue 1126, ADR 0559 §5) 2026-10-27 02:29:20 +01:00
jschoubben d8da56c6da Compose a catalogue pull request's own placement in the assigner's check and fail it on any placed node's unintended difference, refusal or silence past ten minutes, so a placement change is proven equal to what runs before it merges (issue 1126, ADR 0559 §5) 2026-10-27 02:29:20 +01:00
jschoubben c2b1ba4c5d Pin mesh-sdk ba398e2, the group's head with the node's whole candidate answer (issue 1126) 2026-10-27 02:20:17 +01:00
jschoubben 04bb2d1398 Show placement candidates and each node's answer in indexer_placement, add indexer_placement_reread, and drop a group's candidates when its head changes no placement file, so a missed message or a revert is recoverable by tool (issue 1126, ADR 0559 §3, §7, §8) 2026-10-27 02:18:21 +01:00
jschoubben ad60ba3a46 Write a catalogue pull request's changed placement as placement-candidates <group>.<node> and delete them when it merges or closes, so each placed node composes it in shadow and answers (issue 1126) 2026-10-27 02:13:55 +01:00
jschoubben ea3eac51bd Carry a catalogue pull request's placement files at its head when it touches placement/, so the indexer can write them as candidates for the nodes to answer before the merge (issue 1126) 2026-10-27 02:13:55 +01:00
mesh-tools cacf537004 Merge pull request 'The checker's failure is the build step's failure, and a verdict that hides its failing test is refused (issue 1901)' (#84) from fix/1901-failure-contract into main 2026-10-26 23:44:21 +00:00
jschoubben f4a86f5884 Merge main into fix/1901-failure-contract: the checker pins mesh-sdk a2759ec, the group's sdk head (issue 1901)
mesh/delivery merged: source.main moved to novox/mesh-delivery@cacf537004c9
mesh/delivery-group group fix/1901-failure-contract merged: every member is merged
mesh/check checking f4a86f58843d on cacf537004c9 | novox/mesh-delivery#84 in group fix/1901-failure-contract
2026-10-27 00:39:35 +01:00
mesh-tools 9f1b227fb8 Merge pull request 'The indexer fills each repository's taken key from merges it applied or a drained stream's heads, so a merge heard while the state was missing reads on the machines (issue 1280)' (#83) from fix/1280-indexer-fills-taken into main 2026-10-26 23:22:01 +00:00
jschoubben 6c79e456a1 The indexer confirms a forge message's ack with the server before it fills taken keys (issue 1280)
mesh/delivery merged: source.main moved to novox/mesh-delivery@9f1b227fb8c0
mesh/check checking 6c79e456a100 on 9f1b227fb8c0
2026-10-27 00:20:16 +01:00
jschoubben 0b980203fe Taken keys are filled only from merges the indexer applied or a drained forge stream's heads (issue 1280) 2026-10-27 00:18:23 +01:00
jschoubben 73bf290f58 The indexer fills every repository's taken key from the source's head at start and when the state appears (issue 1280) 2026-10-27 00:14:41 +01:00
mesh-tools 475bb4bb0b Merge pull request 'The view's tools read their own reach by its bare name, and a test holds every state the commands read to what is issued (issue 1280)' (#82) from fix/1280-view-reads-its-reach into main 2026-10-26 22:48:25 +00:00
jschoubben efadc691e7 The view's tools read their own reach by its bare name, as the mesh issues it, and a test holds every state the commands read to what is issued (issue 1280)
mesh/delivery merged: source.main moved to novox/mesh-delivery@475bb4bb0b8f
mesh/check being carried to efadc691e7ea on 475bb4bb0b8f | from 4af36c69a761
view_reach asked for mesh-delivery-view.reach, a name the mesh never issued, so it failed on the machines;
the same line was not gofmt'd, which failed the try of main. Quick fix: the name. Thorough for this module: the
manifest test now refuses any stdio.State literal in cmd/ that is neither an own state nor listed in reads.
2026-10-26 23:27:05 +01:00
mesh-tools 4af36c69a7 Merge pull request 'Delivery records are true at the source: the indexer records what it took in, the view derives what reached the machines, a settle verb for held deliveries (issue 1280)' (#80) from fix/delivery-records-true into main 2026-10-26 21:48:50 +00:00
mesh-tools bd107d2015 Merge pull request 'A version that fails on one machine while another runs it healthy is that machine's failure, and goes no further meanwhile (issue 1496, ADR 0557 §1-§3)' (#81) from fix/1496-start-health into main 2026-10-26 21:37:58 +00:00
jschoubben f007ca4227 A node that runs a version alone-failed elsewhere still gets its want at it (ADR 0557 §2, issue 1496)
mesh/delivery merged: source.main moved to novox/mesh-delivery@bd107d20154c
mesh/delivery-group group fix/1496-start-health merged: every member is merged
mesh/check checking f007ca42279a on 4b36cc29a6ae | novox/mesh-delivery#81 in group fix/1496-start-health
Review of 50c4674: the bar on new wants naming F while a node fails it alone
also caught a node that runs F with no live want, the issue-1945 recovery path:
it went on neither list and its node-engine gave the module up. The bar now
holds only a node that neither wants nor runs F.
2026-10-26 22:34:25 +01:00
jschoubben 50c4674dfa While a node fails a version alone, no further node gets a want for it (ADR 0557 §2, issue 1496)
ADR 0557 version 9: a failure that is one node's holds nothing and sets nothing
back, but the version reaches no node that does not want it yet until it is
over. A node whose want does not name F gets no new want naming it (rule 2, in
words naming the failing nodes); wants already written stay.
2026-10-26 22:30:00 +01:00
jschoubben c12b870104 The indexer binds its taken state when it appears, as issue 1790 asks of every state a delivery adds: an indexer started before the bus holds it still takes merges in and says the key it did not write (issue 1280, ADR 0556 clause 1)
mesh/delivery merged: source.main moved to novox/mesh-delivery@4af36c69a761
mesh/delivery-group group fix/delivery-records-true merged: every member is merged
mesh/check checking c12b870104e8 on bd107d20154c | novox/mesh-delivery#80 in group fix/delivery-records-true
2026-10-26 22:25:39 +01:00
jschoubben 92aedb9365 The first pass after a start clears failed-alone for every module and known node (ADR 0557, issue 1496)
Review round 2 of 16e4e61: gating the first-pass clear on a failed have left a
condition an earlier process raised standing for good once its node recovered
while no assigner watched. The first pass now clears it for every module and
every node the presence bucket knows, a quiet clear at the router for one that
is not open.
2026-10-26 22:21:00 +01:00
jschoubben 368204115c Pin the teller at mesh-sdk 68d65e6, the group's sdk head merged with sdk main (issue 1280) 2026-10-26 22:20:45 +01:00
jschoubben 971cdb44e2 Merge remote-tracking branch 'origin/main' into fix/delivery-records-true
# Conflicts:
#	modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go
#	modules/mesh-delivery-indexer/go.mod
#	modules/mesh-delivery-indexer/go.sum
#	modules/mesh-delivery-indexer/manifest_test.go
#	modules/mesh-delivery-indexer/module.json
#	modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go
#	modules/mesh-delivery-view/go.mod
#	modules/mesh-delivery-view/go.sum
#	modules/mesh-delivery-view/internal/view/read.go
#	modules/mesh-delivery-view/manifest_test.go
#	modules/mesh-delivery-view/module.json
2026-10-26 22:20:45 +01:00
jschoubben 16e4e61a0f Only an online healthy witness, and only a first failure, keep a version from being held; failed-alone clears when a node leaves (ADR 0557, issue 1496)
Review of 2ee7589: a node that sleeps after running F healthy kept its stale
have as a witness, so F stayed unheld while it failed on the online nodes; and a
failed-alone condition was never cleared once its node left the module.

A witness must now be online by the watchman's presence. F failing on a second
node is no longer one node's failure: the hold and the set-back stand (ADR 0557
version 6, §2). A remembered failed-alone key whose node is no longer one of the
module's nodes is cleared once; the first pass after a start clears it for every
node the presence bucket knows, for a module with a failed have.
2026-10-26 22:16:38 +01:00
mesh-tools 4b36cc29a6 Merge pull request 'Placement from the catalogue: the bridge carries it, the indexer writes it, the assigner compares it (issue 1126)' (#76) from feat/1126-nodes-compose into main 2026-10-26 21:11:26 +00:00
mesh-tools 31ddd09087 Merge pull request 'The checker ends a check whose heads no candidate holds, and starts a group's check once its heads settle (issue 1890)' (#79) from fix/1890-end-superseded-checks into main 2026-10-26 21:10:56 +00:00
jschoubben 68173de8bd Pin the indexer, the teller and the view at mesh-sdk b4310df: the delivery-records work merged with sdk main (issue 1280) 2026-10-26 22:08:40 +01:00
jschoubben 960e3309bc Merge remote-tracking branch 'origin/main' into fix/delivery-records-true
# Conflicts:
#	modules/mesh-delivery-view/go.mod
#	modules/mesh-delivery-view/go.sum
2026-10-26 22:07:28 +01:00
jschoubben 2ee7589474 A version healthy on another node is not held for one node's failed health check (ADR 0557, issue 1496)
On ace, 2026-10-26 21:19, a newly assigned mesh-build-node failed its first
health check at 8a98386f (later 31be2f24) while shanks, g14 and novox had run it
healthy for an hour. Being first by name, ace was the canary, and the assigner
held the version and moved the three healthy nodes back.

A failed health check of the newest fingerprint now holds it only when no other
node's have names it healthy (ADR 0557 §2 keeps ADR 0429 §6's hold and set-back
for that case). Otherwise the failure is that node's (ADR 0557 §1): nothing is
held or set back, the node keeps its want, and the assigner raises the node's
condition once (failed-alone.<node>.<module>), naming the node, the module, the
fingerprint and the have's words, and clears it once when it ends; the first
pass after a start clears what a previous process may have raised.
2026-10-26 22:06:32 +01:00
jschoubben 297992a4ae Pin mesh-sdk 9796444 (l2-next: feat/1126-nodes-compose merged with sdk main #101, the standing-test race fixes) (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@4b36cc29a6ae
mesh/delivery-group group feat/1126-nodes-compose merged: every member is merged
mesh/check checking 297992a4ae55 on 4b36cc29a6ae | novox/mesh-delivery#76 in group feat/1126-nodes-compose
2026-10-26 22:05:41 +01:00
jschoubben 3f85230417 Pin mesh-sdk 5f45175 (l2-next: feat/1126-nodes-compose merged with sdk main) after merging main (issue 1126) 2026-10-26 21:59:21 +01:00
jschoubben 43c3158bc6 Merge remote-tracking branch 'origin/main' into feat/1126-nodes-compose
# Conflicts:
#	modules/mesh-delivery-view/go.mod
#	modules/mesh-delivery-view/go.sum
2026-10-26 21:57:03 +01:00
jschoubben d95e9f3229 End a check whose heads no candidate holds, and start a group's check once its heads settle (issue 1890)
mesh/delivery merged: source.main moved to novox/mesh-delivery@31ddd090876f
mesh/check pass d95e9f322913 on d581f9659a78 | unit 191 run, 525 kept | integration 201 run, 591 kept | composition passed on novox
2026-10-26 21:44:12 +01:00
mesh-tools d581f9659a Merge pull request 'Tries record their asks; the try limit counts tries on a build node; build_usage shows capacity, hours and steps (issue 1890)' (#78) from fix/capacity-follows-cpu-share into main 2026-10-26 20:33:34 +00:00
jschoubben ea94f9234c Merge remote-tracking branch 'origin/main' into fix/capacity-follows-cpu-share
mesh/delivery merged: source.main moved to novox/mesh-delivery@d581f9659a78
mesh/delivery-group group fix/capacity-follows-cpu-share merged: every member is merged
mesh/check checking ea94f9234c97 on 3dd94f714d6e | novox/mesh-delivery#78 in group fix/capacity-follows-cpu-share
2026-10-26 21:29:44 +01:00
jschoubben c19468b04e Pin the checker and the view at mesh-sdk 805b4d9: the capacity work merged with sdk main 2026-10-26 21:29:44 +01:00
jschoubben c47afd8f4b build_usage says a seat's build hours, and when it is outside them
mesh/delivery closed: a newer head of the same pull request
mesh/delivery-group group fix/capacity-follows-cpu-share checking: 1 of 3 member(s) ready
mesh/check checking c47afd8f4b88 on 3dd94f714d6e | novox/mesh-delivery#78 in group fix/capacity-follows-cpu-share
A build module's capacity.<node> now carries its node's build window (the operator, 2026-10-26: g14
builds only from 18:00 to 06:00); the seat shows it, outside it says it takes nothing new, and a
value the module cannot read shows as any hour with the value and why. The checker's and the view's
SDK pin moves to mesh-sdk d00dac1.
2026-10-26 21:22:36 +01:00
jschoubben afd5a3ba07 A module a node wants and runs nothing countable of holds no head back; the reached head's event names its merges as not yet on the machines; pin mesh-sdk e44cbaf (review of issue 1280, ADR 0556 clause 2) 2026-10-26 21:15:31 +01:00
jschoubben 7dbad4c6d2 Pin mesh-sdk 33144781 (taken, reach, settle layouts) in the indexer, the view and the teller (issue 1280) 2026-10-26 21:05:29 +01:00
jschoubben 083a5042f6 The view reads the wants by module once per derivation of the reach 2026-10-26 21:03:56 +01:00
jschoubben 351ca7c290 The teller checks a settle ask's warrant against mesh-delivery.settle, the act its options bind (issue 1280, ADR 0556 clause 5) 2026-10-26 20:58:20 +01:00
jschoubben f907be2c14 The view derives per repository up to which merge every node runs it, keeps it as its state reach and says each reached head, so a fix on the machines reads delivered (issue 1280, ADR 0556 clauses 2 and 3) 2026-10-26 20:56:12 +01:00
jschoubben 6201f58c20 The indexer says per repository the last merge it took in, so the view knows its modules speak for that merge (issue 1280, ADR 0556 clause 1) 2026-10-26 20:49:00 +01:00
jschoubben 8fee764c73 Pin mesh-sdk feat/1126-nodes-compose 03dce12, the group's head: the checker refuses a pin that is neither trunk nor the group's head (issue 1126)
mesh/delivery-group group feat/1126-nodes-compose checking: 4 of 6 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 8fee764c734e on d581f9659a78 | novox/mesh-delivery#76 in group feat/1126-nodes-compose
2026-10-26 20:35:45 +01:00
mesh-tools 3dd94f714d Merge pull request 'The checker fails any repository's change that adds a binary, in any commit (issue 1577)' (#77) from fix/1577-checker-refuses-binaries into main 2026-10-26 19:32:27 +00:00
jschoubben c716670be0 The checker pins the SDK of review round 1, and its hid-the-failure cases join the older build module's test (issue 1901) 2026-10-26 20:19:07 +01:00
jschoubben 352066dde6 Review of L2: the assigner binds the indexer's placement state when it appears, with no restart (D1); the three new unit tests folded into existing ones, so no count rises (D2); pin mesh-sdk 3968849 (issue 1126)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 352066dde611 on 3dd94f714d6e | novox/mesh-delivery#76 in group feat/1126-nodes-compose
2026-10-26 20:12:00 +01:00
jschoubben d6bd31a166 Say the try limits as what they are: a check when a try is asked
Tries let in while waiting may later all run at once; each node's build capacity bounds what runs
on it (review of issue 1890's try cap).
2026-10-26 20:11:08 +01:00
jschoubben c0a4b139a2 A try queued behind the build seat's checks no longer blocks the next try
Only tries a build node holds count toward the 4 at once; tries waiting for a node are bounded
apart at 8, and the refusal names both counts (issue 1890: at 19:53 four tries queued behind 44
checks for up to 47 minutes refused every further try).
2026-10-26 20:05:51 +01:00
jschoubben 6d60953f98 A running try names its asks and where each stands; build_usage says a seat's CPU-bound capacity
The checker wrote a try's asks only at its end, so a 31-minute try could not be told queued from
running (issue 1890). The try's record now names each ask as it is put, then the node that took
it, its state and the phase its step says, written on the step key's own changes. build_usage
names a running try's asks and takes a build module's capacity.<node> (configured bounded by its
node's build CPU, issue 1806's follow-up) when it is current with mesh-config.
2026-10-26 19:58:39 +01:00
jschoubben 17ed06ee4d Pin mesh-sdk's fix/1901-failure-contract head 81ec7bd7 (issue 1901) 2026-10-26 19:50:47 +01:00
jschoubben 9427dac823 The checker says loudly when a build module hid a failure's cause (issue 1901)
A failed test with no cause, or a passing line as its cause, was worded as if it were the failure: a try of mesh-catalog
failed with a passing '✔' line of the first test file. Under mesh-sdk's failure contract the verdict's words and its
status line now start with 'the build module hid the failure (<module> at <version>)', and the check still fails.
2026-10-26 19:50:05 +01:00
jschoubben d74e082155 Checker refuses a program a commit of the branch added and a later one removed: it stays in history (issue 1577)
mesh/delivery merged: source.main moved to novox/mesh-delivery@3dd94f714d6e
mesh/delivery-group group fix/1577-checker-refuses-binaries merged: every member is merged
mesh/check checking d74e0821552c on df8b9ac3b1b5 | novox/mesh-delivery#77 in group fix/1577-checker-refuses-binaries
2026-10-26 19:47:53 +01:00
jschoubben 2e0013ec25 The assigner's wants test goes on to placement: a placed node's want names the build alone and its own want says it composes its own (issue 1126, ADR 0547 §2)
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: ace: novox/mesh-catalog#505 does not merge into the trunk: files in conflict: .gitignore | 2e0013ec2500 on df8b9ac3b1b5 | unit 1245…
2026-10-26 19:44:55 +01:00
jschoubben 2493aa6529 Pin mesh-sdk feat/1126-nodes-compose 9713c30: placement carries a settings-form module's values (issue 1126)
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: ace: novox/mesh-catalog#505 does not merge into the trunk: files in conflict: .gitignore | 2493aa65299e on df8b9ac3b1b5 | unit 1245…
2026-10-26 19:40:38 +01:00
jschoubben 85c89b16ef The grants part composes what the catalogue's placement places beside the assignments, and holds a seat held once for the mesh where placement says (issue 1126, ADR 0549 §3, G5)
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-catalog/checks/nobinaries.TestTheCatalogueHoldsNoCompiledProgram: modules/mesh-config/cmd/mesh-config…
2026-10-26 19:33:27 +01:00
jschoubben 4f75925410 Nodes compose: placement from the catalogue, node records, self wants, own and pair credentials, core boxes and the authority from the vault (issue 1126)
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-catalog/checks/nobinaries.TestTheCatalogueHoldsNoCompiledProgram: modules/mesh-config/cmd/mesh-config…
One commit for the branch feat/1126-nodes-compose of mesh-delivery: its 16 commits squashed so its history does not carry the 17.8 MB compiled mesh-delivery-assigner a commit added by mistake and a later one removed (issue 1577). The tree equals the former head ff6c038d. The operator allowed this rewrite of the feature branch on 2026-10-26.
2026-10-26 19:30:18 +01:00
jschoubben 820c458f4c The captured clean fetches carry the programs a current build module answers, so lines asserted whole do not warn (issue 1577) 2026-10-26 19:16:25 +01:00
jschoubben dc9d65524d A check judged by a build module that reports no programs says so in its status line, still passing: the refusal did not run (issue 1577) 2026-10-26 19:08:00 +01:00
jschoubben 1a4c788ed7 Checker fails a change whose fetch reports a compiled program, and warns when a build module reports none: build output kept getting committed (issue 1577) 2026-10-26 19:01:40 +01:00
mesh-tools df8b9ac3b1 Merge pull request 'Checker, builder and indexer bind states created after they start (issue 1790)' (#75) from fix/1790-state-appears into main 2026-10-26 14:47:40 +00:00
jschoubben fcb1e40582 Pin mesh-sdk fix/1790-state-appears at 65ee43f: a refused hearing of a bucket's creation is said and heard again at a reconnect (issue 1790, review)
mesh/delivery merged: source.main moved to novox/mesh-delivery@df8b9ac3b1b5
mesh/delivery-group group fix/1790-state-appears merged: every member is merged
mesh/check being carried to fcb1e4058262 on df8b9ac3b1b5
2026-10-26 15:25:53 +01:00
jschoubben 4973372612 Merge remote-tracking branch 'origin/main' into fix/1790-state-appears 2026-10-26 15:25:06 +01:00
jschoubben 3f6c085e8a Pin mesh-sdk fix/1790-state-appears at 3ac644b (issue 1790) 2026-10-26 15:14:00 +01:00
jschoubben 040e3097b5 checker, builder, indexer: a state of their own not on the bus at start is bound when it appears; mesh-grants composes each principal's hearing of its buckets' creation (issue 1790)
A delivery restarted the checker and the builder before the grants part created their new state hearing;
OptionalBucket answered nil and the hearing was never kept. They, and the builder's reasks and queues and the
indexer's takeins, now open their own states through mesh-sdk process.AwaitBucket (mesh-sdk
fix/1790-state-appears d515c71), which binds on the bus's STREAM.CREATED advisory and writes the latest kept
value. mesh-grants takes the same SDK, whose composer grants that advisory.
2026-10-26 15:13:00 +01:00
mesh-tools feef96ed16 Merge pull request 'build_usage: how much each build node is used and whether it may build, in one call' (#74) from feat/build-usage-tool into main 2026-10-26 14:09:32 +00:00
jschoubben 4f722899df View: build_usage does not answer eligibility it cannot read; a resolved capacity key with no readable value is the puller's 0
mesh/delivery merged: source.main moved to novox/mesh-delivery@feef96ed160e
mesh/check checking 4f722899df25 on feef96ed160e
A capacity this tool cannot read is unknown to it, not 0: the seat says eligible_known false and the node not known.
A key mesh-config holds without a readable value is what the puller reads as 0, and was dropped silently.
2026-10-26 14:58:47 +01:00
jschoubben 166a918b43 View: build_usage counts keys a build module ended for being left running apart, lists every node a capacity names, and says capacity unknown when mesh-config cannot be read
Since mesh-sdk #90 a build module ends a left-running key as failed at the moment it notices; counted as finished,
such a step put hours into the medians and the time at capacity. The view takes mesh-sdk's step bound and restart
reason from the SDK it now pins.
2026-10-26 14:58:47 +01:00
jschoubben 1b4c860f63 Checker: its announcement test expects tries readable, as the delivery view now reads them 2026-10-26 14:58:47 +01:00
jschoubben 3b6945b2db View: build_usage answers how the build nodes are used in one call; the checker's tries name their asks
The operator's question (how much are the build nodes used, is ace eligible) took six calls over four tools.
build_usage reads it from the bus's states: seats held (have), capacity (mesh-config resolved), pauses, the
build modules' steps by kind with left-running keys, the last hour and day with total and median time and time
at capacity, the running tries, and the backlog the view program reads from the seats' streams. A try now names
the asks it put, and the checker announces its tries readable, so the view tells a try's asks from a check's.
2026-10-26 14:58:41 +01:00
mesh-tools 4174e23c04 Merge pull request 'The checker and the builder end or re-ask what the build seat gave up, and a try can be ended by hand (issue 734)' (#73) from fix/734-lost-ask into main 2026-10-26 13:54:17 +00:00
jschoubben 3993175e45 Checker, builder and grants part: pin mesh-sdk main, which holds both the dead-letter hearing (#92) and the placement (#91) (issue 734)
mesh/delivery merged: source.main moved to novox/mesh-delivery@4174e23c0492
mesh/delivery-group group fix/734-lost-ask merged: every member is merged
mesh/check checking 3993175e457f on 4174e23c0492
2026-10-26 14:45:33 +01:00
jschoubben 60c7ffc981 Checker, builder and grants part: hear DEAD_LETTERS again until heard, say when they do not, and compose the same grant (issue 734, review round 1)
mesh-grants moves to the same mesh-sdk (3203358c), so its shadow user list grants the checker and the builder the
DEAD_LETTERS reads the controller's list will, and the take-over is not blocked by a difference. The checker and the
builder hear DEAD_LETTERS through buildrun.DeadLetterHearing: in the background, again at every reconnect and every ask
waited for while deaf, and keep whether they hear it in their new state hearing, which checker_status and builder_stuck
say as dead_letters. checker_try says that a try ended by the not-taken bound leaves its ask in the stream.

Not done as asked: a module health tool. The catalogue refuses a tool check on a module with no runtime of its own and no
other check beside it (ADR 0227 rule 8, ADR 0366 §1), so the deaf state is said in the status tools and the log.
2026-10-26 14:44:53 +01:00
jschoubben 33de9dde4f Checker and builder: wait on the build seats through mesh-sdk's LostAskWaiter, so an ask the seat gave up on ends every asker's wait (issue 734)
The waiter moved to mesh-sdk buildrun (fix/734-lost-ask, d96b9fd6), where every asker of the build seats shares it.
The builder hears DEAD_LETTERS for the go, node and image seats and asks a given-up part of a build again at once, with
the seat's words as why, instead of waiting for the check bound. The checker keeps what it does with a lost ask (a try
ends as error, a check is asked again) and its verdict words.
2026-10-26 14:44:52 +01:00
jschoubben 6b85220fa4 Checker: an ask the build seat gives up on ends its try as error and asks a check again; a try no node takes ends at the step bound; checker_try_end ends one by hand (issue 734)
A check ask handed out five times during a bus reload was given up into DEAD_LETTERS and no build node wrote a step for it,
so try-1793017340003993788 stayed running for ever, held one of the four try places, and checker_try of its head kept
answering its id. The checker now hears the copies DEAD_LETTERS keeps of the build seats' given-up asks (an ordered
consumer from its start; the grant is mesh-sdk/mesh-controller DeadLetterReads, branch fix/734-lost-ask) and ends the
step it waits for: a try ends as error saying why, a check is asked again under its claim, up to three times, then fails
as not heard. A try whose ask no node takes within the step bound ends the same way (a check's backlog stays no fault,
ADR 0525 §9). checker_try_end ends a try by hand, freeing its place and stopping its judging.
2026-10-26 14:44:40 +01:00
mesh-tools 1cbae7d41c Merge pull request 'The checker asks the packages a node could not test again, on a node that has what they need (issue 890, ADR 0552)' (#72) from feat/890-placement into main 2026-10-26 13:40:45 +00:00
mesh-tools b3a4b09f5b Merge pull request 'The indexer takes in a registered module whose repository became readable, and serves indexer_take_in (issue 1945)' (#71) from fix/indexer-takes-in into main 2026-10-26 13:39:13 +00:00
mesh-tools c8b26735c8 Merge pull request 'The assigner says when it holds the bus back instead of skipping it silently (issue 1945)' (#70) from fix/bus-bar-loud into main 2026-10-26 13:13:22 +00:00
jschoubben 907e092abc mesh-delivery-indexer reads unread registered modules again in one background run, so a forge timeout holds no merged message or source revision past its ack wait (issue 1945, review round 1)
mesh/delivery merged: source.main moved to novox/mesh-delivery@b3a4b09f5b6f
mesh/check being carried to 907e092abc45 on b3a4b09f5b6f
The take-in tool's bound now says the take-in is still running and where its answer will be.
2026-10-26 14:11:12 +01:00
jschoubben 3073495a06 checker: pin mesh-sdk 33395f68, feat/890-placement merged with main (issue 890)
mesh/delivery merged: source.main moved to novox/mesh-delivery@1cbae7d41cd0
mesh/delivery-group group feat/890-placement merged: every member is merged
mesh/check checking 3073495a0670 on 1cbae7d41cd0
2026-10-26 14:06:10 +01:00
jschoubben 16c411e436 Merge remote-tracking branch 'origin/main' into feat/890-placement 2026-10-26 14:05:40 +01:00
jschoubben 2d841870a8 Assigner: only a put still on its steps is named when a node leaves neither list (issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-delivery@c8b26735c8cf
mesh/check pass 2d841870a873 on c8b26735c8cf | carried from 1d416827157d | unit 185 run, 518 kept | integration 195 run, 584 kept | composition passed…
Review round 2: an ended put stays in the bucket and owns no want.
2026-10-26 14:03:48 +01:00
jschoubben 7f8ce7f5c0 checker: the placed ask names the unmet packages, so a package without a kept verdict is not run or counted twice (issue 890, ADR 0552 §4; review round 1). Pins mesh-sdk 45564e69. 2026-10-26 14:02:16 +01:00
jschoubben 07b5f13474 mesh-delivery-indexer reads a registered module again once its repository becomes readable, and takes one in on ask (issue 1945)
A registered module whose repository the forge answered 404 (car-hunter, snake, before the forge's account could read
them) was read again only when the indexer started again or the controller wrote a new build record of it, so the grants
part's composed user list stayed short of their subjects and the take-over was refused. A failed read now keeps the
module as unread, read again on the next source revision and the next merged message (no timer of its own), and the new
tool indexer_take_in (the tools bundle mesh-delivery-indexer-tools, through the indexer's takeins state, as the
builder's builder_ask_again) takes one in now and answers what it stored or why not.
2026-10-26 14:01:34 +01:00
jschoubben f4aac599ca Assigner: name a running put when a node leaves neither list, and say the bus bar in plain words (issue 1945)
Review round 1: while an operator's put runs through its steps it owns the want, so the clear said
'the node does not run it' of a node that runs the bus.
2026-10-26 14:00:32 +01:00
jschoubben 2d63f0ba62 Assigner: say the bus bar instead of skipping it silently, and clear a neither condition only in true words (issue 1945)
A bus whose move only the operator's put may make (ADR 0546 §8) got no want and nothing in the outcome:
nats on novox, running an old-form build, was on no list while the pass cleared its condition and logged
that it had a want again. The bar now names its holding, puts a node without a live want on neither list
(its condition needs the operator: assigner_put), and a neither condition is cleared as 'has a want again'
only when a live want exists.
2026-10-26 13:55:42 +01:00
mesh-tools 1d41682715 Merge pull request 'mesh-grants says loudly when it holds the user list back, and clears it after a restart (issue 1945)' (#69) from fix/grants-hold-loud into main 2026-10-26 12:44:36 +00:00
jschoubben 705765d113 checker: a placed ask that every node declines for longer than its bound (30 min) fails the check naming the need (issue 890, ADR 0552 review) 2026-10-26 13:42:46 +01:00
jschoubben 16a97ed65d checker: ask the packages a node left unmet again of a node that has what their tests need, and put a declined ask again (issue 890)
A check that landed on ace passed with mesh-build-node's integration tests skipped. The build module now leaves such a
package unmet; the checker keeps the passes and asks the levels from the first unmet one with the need named, putting the
ask again under a new id when a node lacking it declines it. Re-pins mesh-sdk feat/890-placement.
2026-10-26 13:40:00 +01:00
mesh-tools ecf532af0e Merge pull request 'checker_status reads the node-engine at its running commit, as the checker decides (issue 568)' (#68) from fix/status-and-holds into main 2026-10-26 12:39:56 +00:00
jschoubben ce600df98a Say the held-list state on a part's first pass, so a warning raised before a restart is cleared
mesh/delivery merged: source.main moved to novox/mesh-delivery@1d416827157d
mesh/check being carried to ce600df98afc on 1d416827157d
2026-10-26 13:37:31 +01:00
jschoubben b4a6a34705 mesh-grants: a user list held back after take-over raises the warning mesh-grants.user-list-held and clears it when the list advances, so the hold is not only in a tool's output (issue 1945) 2026-10-26 13:36:39 +01:00
jschoubben a7e51b416b checker_status reads the node-engine at its running commit, as the checker's decision does, so it stops listing every candidate as waiting for a trunk move (issue 568)
mesh/delivery merged: source.main moved to novox/mesh-delivery@ecf532af0e85
mesh/check being carried to a7e51b416bc1 on ecf532af0e85 | from 71f786f14662
2026-10-26 13:25:18 +01:00
mesh-tools 71f786f146 Merge pull request 'mesh-grants: take-over and hand-back by the operator's Telegram Approve, the sealed list, and every grant the loaded list holds (issue 1945)' (#67) from feat/1945-user-list-switch into main 2026-10-26 12:21:50 +00:00
mesh-tools 848c6fdc56 Merge pull request 'Checker: one check per set of heads, a trunk or sibling move carries the verdict, a merge turn holds a claim (ADR 0548, issue 568)' (#65) from feat/548-one-check into main 2026-10-26 12:01:32 +00:00
mesh-tools ead9ee2d13 Merge pull request 'Indexer: index a module registered from any branch, by its registered ref, never a newer pull request's (issue 1945)' (#66) from fix/indexer-registered-modules-2 into main 2026-10-26 12:01:24 +00:00
mesh-tools 4861d1f78d Merge pull request 'Checker: keep no package whose tests skipped, so a skipped pass is never reused as proof (issue 890)' (#64) from fix/checker-skips-not-kept into main 2026-10-26 12:01:19 +00:00
jschoubben 5c76c9d3ac mesh-grants: a list in force with no users kept is checked against the users the bus loaded, so the first list after take-over keeps every user (review of the switch, issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-delivery@71f786f14662
mesh/delivery-group group feat/1945-user-list-switch merged: every member is merged
mesh/check being carried to 5c76c9d3ac84 on 71f786f14662 | novox/mesh-delivery#67 in group feat/1945-user-list-switch
2026-10-26 12:54:55 +01:00
jschoubben 699cbfb7c1 Indexer: a registered module takes its registered ref, not a newer branch's record; no read at an older commit after a failure
mesh/delivery merged: source.main moved to novox/mesh-delivery@ead9ee2d138d
mesh/check checking 699cbfb7c131 on ead9ee2d138d
2026-10-26 12:52:29 +01:00
jschoubben 319d56be7e Indexer: a registered module is read from every repository it was built from, whatever the branch, and at the registered commit when the head is gone 2026-10-26 12:45:35 +01:00
mesh-tools 11d3678984 Merge pull request 'Indexer: index a module registered from its own repository, so the grants part composes its grants (issue 1945)' (#63) from fix/indexer-registered-modules into main 2026-10-26 11:33:41 +00:00
jschoubben f55f150896 mesh-grants: after take-over the list in force is not replaced by one composed past a failed read or one dropping its users for want of a hash; the switch's effect says when the bus node does not watch the key (review of the switch, issue 1945). Pins mesh-sdk c2a410bb. 2026-10-26 12:31:31 +01:00
jschoubben e1f1b5d18b Checker: a package with skipped tests is withheld from the kept verdicts, not the whole result (issue 890)
mesh/delivery merged: source.main moved to novox/mesh-delivery@4861d1f78d28
mesh/delivery-group group fix/checker-skips-not-kept merged: every member is merged
mesh/check failed: ace: the check of novox/mesh-catalog#492 refused: the installed go version could not be read: `go version` with GOTOOLCHAIN=local f…
2026-10-26 12:29:14 +01:00
jschoubben f0fe454145 Checker: a result with skipped tests keeps no verdict, so a skipped pass is run again (issue 890) 2026-10-26 12:23:57 +01:00
jschoubben 4026f6c188 checker: a check is never held by a turn its own heads wait in, the node-engine read at its running commit is named and carried when it moves, and a gate bucket made after the start is opened before the next claim (ADR 0548 §3, §6; issue 568, review round 1)
mesh/delivery merged: source.main moved to novox/mesh-delivery@848c6fdc5648
mesh/delivery-group group feat/548-one-check merged: every member is merged
mesh/check failed: ace: the check of novox/mesh-delivery#65 refused: the installed go version could not be read: `go version` with GOTOOLCHAIN=local f…
2026-10-26 12:14:50 +01:00
jschoubben 29cfb8c177 Pin mesh-sdk at the group's head 83ee7769 (issue 1945) 2026-10-26 12:06:58 +01:00
jschoubben a092ee9ff9 mesh-grants writes the user list sealed to the bus node and names it in user-list.sealed; the switch turns that key; take-over state says switched only once the bus node loaded it (ADR 0547 clauses 7, 8; issue 1945)
The bus node can only take over a list that exists: each pass composes the accounts block with the vault's hashes, seals
it to the key the bus node says in its have, puts it in the part's own object bucket by digest and writes the key with
digest, revision, user count, users left out and which list to follow. take-over is refused, naming why, while no list is
sealed to the bus node's current key or the loaded list does not grant the bus node its read.

Review of fix/1945-takeover-blockers: no responders on the router's record is an error, not 'no record'; an answer whose
record cannot be reached is not closed; an ask that failed to publish is taken back at the router; an error acting on an
answer is logged. Pins mesh-sdk 175a5567.
2026-10-26 11:59:41 +01:00
jschoubben 6c3cbee35c checker: pin mesh-sdk at the group's head f5c233a9288c, which holds the merge turns and the carried verdict (ADR 0548 §10; issue 568) 2026-10-26 11:45:13 +01:00
jschoubben 4b23db28b3 mesh-grants: an open take-over ask is always closed (router record, expiry, failed publish, any outcome), the switch is said recorded not switched, the refusal condition is cleared, the excuse is limited to the claiming machines' subscriptions (issue 1945) 2026-10-26 11:43:39 +01:00
jschoubben 646f83642a Indexer: read a registered module at the source's head when it names one, else at the registered commit
mesh/delivery merged: source.main moved to novox/mesh-delivery@11d3678984af
mesh/check pass 646f83642a01 on 11d3678984af | unit 171 run, 502 kept | integration 181 run, 568 kept | composition passed in 39ms on novox
2026-10-26 11:41:46 +01:00
jschoubben d9bfba1e62 Indexer: read one directory of a registered module's repository, leave a repository the source names to its merges, accept master as a trunk 2026-10-26 11:36:22 +01:00
jschoubben f1bb7747ae checker: one integration test through the real bus proves a merge turn holds a check off until it goes and a carry of unchanged digests runs nothing; it extends the equal-inputs test, so the count stays (ADR 0548 §3, §5; issue 568 D7) 2026-10-26 11:36:15 +01:00
jschoubben d2eb3e5ecf checker: the line of a carried verdict names the head on the new trunk, then where it was carried from; a verdict being carried is pending and says so (ADR 0548 §6, §8; issue 568 D6) 2026-10-26 11:35:16 +01:00
jschoubben b0b1cf4f01 mesh-grants: a retired verb and a multiply claimed mesh seat's added subjects are named, not counted, in the comparison (issue 1945) 2026-10-26 11:34:52 +01:00
jschoubben d91ea41f64 checker: only a head git refused gets a whole check after a trunk move; a conflicting carry fails naming the files and asks nothing (ADR 0548 §7; issue 568 D5) 2026-10-26 11:34:08 +01:00
jschoubben 5fed429827 checker: a verdict names the siblings it read at their trunk, and a sibling's move is carried as a trunk move is (ADR 0548 §6; issue 568 D4) 2026-10-26 11:33:35 +01:00
jschoubben 9a569bfb8d mesh-grants: derive that a bundle loads from build.artifacts, as the indexer's manifest has no derived bundles, so route-proxy's helper is carried (issue 1945) 2026-10-26 11:33:28 +01:00
jschoubben e2c41ffa5a checker: a carry keeps the composition it passed while no composed fingerprint changed, and composes again otherwise (ADR 0548 §5; issue 568 D3) 2026-10-26 11:32:31 +01:00
jschoubben 13e4ed8515 checker: a trunk move never ends a check; once it ended, its pass is carried and names the commits it was carried from, and a failed carry names the move (ADR 0548 §1, §5; issue 568 D2) 2026-10-26 11:30:46 +01:00
jschoubben c66fe11e16 checker: claim no check of a repository an approved merge holds a turn on, and claim it when the turn goes (ADR 0548 §3; issue 568 D1) 2026-10-26 11:29:25 +01:00
jschoubben 75d0eb32be mesh-grants: take-over and hand-back are asked of the operator on their channel and run on their yes after the refusals; take-over no longer always refused; grants_take_over_state (issue 1945) 2026-10-26 11:26:37 +01:00
jschoubben c44b9f91cd Indexer: gofmt 2026-10-26 11:23:27 +01:00
jschoubben 82da252cf0 Indexer: index the manifests of modules registered from their own repositories, so the grants part has no gap for them 2026-10-26 11:23:21 +01:00
jschoubben ce72b67cef mesh-grants: hear the condition events through the controller's durable consumer and acknowledge them; the provided mesh-wireguard manifest always wins (issue 1945) 2026-10-26 11:20:42 +01:00
mesh-tools 32bc4f3c48 Merge pull request 'Checker: read every test result of a level and name every failure in the verdict, the try and the status line (issue 1921)' (#62) from fix/1921-every-failure-r into main 2026-10-26 10:18:11 +00:00
jschoubben 2a94d16834 mesh-grants: ask the controller's root-free verb and wake on its condition events, so a verified sender is composed (issue 1945)
The part composed every membership with no root-free input, so telegram's verified-sender was dropped and the membership held as a shadow
difference. It now asks mesh-controller.root-free for the machines a composition needs, per pass, failing closed and said once, and wakes
on root-not-free condition-raised and condition-cleared.
2026-10-26 11:11:43 +01:00
jschoubben da7baa5e39 mesh-grants: take mesh-wireguard's manifest from mesh-sdk, where the mesh provides it, so its node-tools grants are composed (issue 1945) 2026-10-26 11:11:43 +01:00
mesh-tools ed1125b66d Merge pull request 'Assigner: take a stale ask away only once its held condition's clear was said, so a failed clear is retried (issue 1906)' (#61) from fix/1906-clear-retried into main 2026-10-26 10:09:42 +00:00
jschoubben 366bc750da Assigner: clear a stale hold's condition before taking its ask away, so a failed clear is retried at the next pass (issue 1906)
mesh/delivery merged: source.main moved to novox/mesh-delivery@ed1125b66d91
mesh/check pass 366bc750da87 on ed1125b66d91 | unit 168 run, 489 kept | integration 21 run, 712 kept | composition passed on ace
2026-10-26 11:03:09 +01:00
jschoubben 04a56b7aff Name every failed test in a check's verdict and a try's failed list (issue 1921)
mesh/delivery merged: source.main moved to novox/mesh-delivery@32bc4f3c48b2
mesh/delivery-group group fix/1921-every-failure-r merged: every member is merged
mesh/check checking 04a56b7aff66 on 32bc4f3c48b2
The checker stopped reading a level's test results at the first that did
not pass and kept only its first failure, so a check named one fault per
round. It now reads and counts every result and writes every failed test
with its first line into the verdict's Failures and the try's failed
list (at most 50, then how many more); Failed keeps the first one's
words whole (issue 1901). Pins mesh-sdk fix/1921-every-failure.
2026-10-26 10:58:13 +01:00
mesh-tools 2f697216ee Merge pull request 'Assigner: take away hold, restore and record asks whose fingerprint the trunk left behind (issue 1906)' (#60) from fix/1906-stale-asks into main 2026-10-26 09:49:22 +00:00
jschoubben 769dab43a2 Assigner: keep the held record of a fingerprint whose asks went stale (issue 1906)
mesh/delivery merged: source.main moved to novox/mesh-delivery@2f697216eee4
mesh/check pass 769dab43a24d on 2f697216eee4 | unit 168 run, 488 kept | integration 178 run, 554 kept | composition passed on ace
The held record is the memory that a fingerprint failed (ADR 0408 section 4); a revert returns the
trunk to exactly it, and without the key rule 3 would move the canary onto the failed build with
nothing asked. A stale hold ask now clears only the condition (raised again on a revisit), and the
hold is asked again under a new asking. Also read the single want key for a record ask.
2026-10-26 10:33:30 +01:00
jschoubben 98a05997ae Assigner: keep a held-back node's record ask, and clear the held record and condition of a stale hold (issue 1906)
Review round 1: a record ask is about the composition of any fingerprint a node may still want
(trunk, want, have), not only the trunk's, so deleting it looped with the wants rule that asks it
again and the operator was never asked. It is now stale only when the module left the trunk or no
read composition of those fingerprints has its digest. A hold or restore ask taken away as stale
also takes its held record and clears its held condition.
2026-10-26 10:31:01 +01:00
jschoubben 07dee528c5 Assigner: take away hold, restore and record asks whose fingerprint the trunk left behind (issue 1906)
An ask outlived its fingerprint: a lift left the restore asks of its fingerprint on every node,
and no pass removed an ask of a fingerprint that is no longer the module's trunk fingerprint,
so the operator kept being asked about a module that runs the trunk healthy. Each pass now
takes those asks away (the teller then withdraws the router ask), and a lift takes the restore
asks of its fingerprint. Nothing is taken away while the trunk or a composition is unread.
2026-10-26 10:25:50 +01:00
mesh-tools 7ddbf40715 Merge pull request 'mesh-grants: read manifests as the indexer does and say every refused one, so claude-code counts as placed (issue 1945)' (#59) from fix/1945-grants-inputs into main 2026-10-26 09:00:58 +00:00
jschoubben aea22bf793 mesh-grants: read manifests for their inputs, as every delivery part does, not against a list from the controller
mesh/delivery merged: source.main moved to novox/mesh-delivery@7ddbf40715ff
mesh/check pass aea22bf793b4 on 7ddbf40715ff | unit 168 run, 479 kept | integration 178 run, 545 kept | composition passed on novox
The part reads seats, buckets, logs and streams, never a config value, and
the merge gate already judged the fallbacks; reading the controller's
predefined mirror would tie the part to the leaving controller (ADR 0547
clause 1). ParseManifestForInputs takes a fallback as it declares itself.

Issue: 1945
2026-10-26 09:36:18 +01:00
jschoubben 72889be87f mesh-grants: judge manifests against the predefined keys on the bus, and say every refused one
A manifest falling back to a predefined key (claude-code, dunst) was parsed
with no list, refused silently, and its module counted as placed nowhere, so
three memberships would lose claude-code's reach addresses and were held back.
The part now reads the list the controller mirrors (mesh-controller.predefined,
key list), follows it, and lists every manifest it cannot read in its gaps,
its pass (refused) and its log.

Issue: 1945
2026-10-26 09:33:29 +01:00
mesh-tools 86db64904b Merge pull request 'mesh-grants: create a declared module's missing buckets, logs and streams on the event; publish memberships past retired seat verbs (issues 1790, 1945)' (#58) from fix/1790-grants-create-buckets into main 2026-10-26 08:17:22 +00:00
jschoubben 12f16bbebd mesh-grants: create a declared bucket, log or stream the bus lacks, so a new module state no longer waits for a controller start
mesh/delivery merged: source.main moved to novox/mesh-delivery@86db64904b2a
mesh/delivery-group group fix/1790-grants-create-buckets merged: every member is merged
mesh/check checking 12f16bbebd40 on 13380e3d0e52 | novox/mesh-delivery#58 in group fix/1790-grants-create-buckets
A module's new state reached the bus only when the controller started (issue 1790, met three
times: puts, queues, mesh-vault's bus-accounts). The operator allowed one more grant in the
controller's composer on 2026-10-26 08:54, creation only: the part's process now creates each
bucket, log or stream a placed module's manifest declares and the bus lacks, with the same
configuration the controller asserts (mesh-sdk BucketStreamConfig, Log.Config,
ModuleStreamConfig), on the event that brought the manifest, never on a timer. It says asserted
and logs one line per module; a stream the bus holds is never updated or deleted, its difference
only said; a create refused is said and tried at the next event; one created at this manifest
and gone since is made again only by hand. grants_assert asks the process to do the same for one
module now. The user list, the grants and any change to a held stream stay in shadow. mesh-sdk
moves to fix/1790-grants-create-buckets, whose composer grants this, so the shadow list is
composed as the controller's.

Issue: 1790
2026-10-26 09:11:23 +01:00
jschoubben 4ec40eec9a mesh-grants: publish a membership that drops only a retired seat verb, and say every one held back
The guard that holds back a membership losing anything held back five since 01:24: the frozen
controller's memberships still serve the delivery seat's mark-ready, which mesh-sdk d506f21
retired on purpose (ADR 0421), so every membership composed with today's seat catalogue looked
like a loss. A seat verb the catalogue defines no longer is now dropped from the comparison and
named in the pass (retired); any other loss, a module's own seat's verbs included, is still
refused. The log named only the first held membership: it now says each one, with what it would
lose, whenever the set changes, and grants_membership with neither node nor module lists them.

Issue: 1945
2026-10-26 09:11:03 +01:00
mesh-tools 13380e3d0e Merge pull request 'mesh-grants: the shadow user list takes each user's hash from the vault and says per user whether one is held (issue 1945, ADR 0547 clause 9)' (#57) from fix/1945-vault-hashing into main 2026-10-26 07:48:07 +00:00
jschoubben 9132b6a296 mesh-grants: take each user's hash from the vault's bus-accounts and say per user whether one is held, so take-over waits until every user has a credential
mesh/delivery merged: source.main moved to novox/mesh-delivery@13380e3d0e52
mesh/check pass 9132b6a296b9 on 13380e3d0e52 | unit 168 run, 473 kept | integration 178 run, 539 kept | composition passed in 36ms on novox
ADR 0547 clause 9: the vault writes the hash of each bus account it holds into bus-accounts.<user>. The grants part
watches that state (no timer), carries the hash with each shadow user in memory only, and its user-list tool says per
user whether a hash is held and how its line would differ from the loaded one (same, adds, left out for want of a
credential, loses its login); never the hash, since bcrypt salts make two hashes incomparable. Take-over names the
users with no hash.

Issue: 1945
2026-10-26 02:29:54 +01:00
mesh-tools bcb5aafdbe Merge pull request 'Builder: given-up asks no longer hold a re-ask back for ever, and a tool reads a build seat's queue (issue 1945)' (#56) from fix/1945-reask-dead-ahead into main 2026-10-26 00:24:30 +00:00
jschoubben 3e8bd9b0de Builder: count only deliverable asks ahead, drop the old message of an ask asked again, and read a seat's queue (issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-delivery@bcb5aafdbe80
mesh/check pass 3e8bd9b0def2 on bcb5aafdbe80 | unit 168 run, 472 kept | integration 178 run, 538 kept | composition passed on shanks
mesh-delivery #51's re-ask never fired for mesh-build-node.4e7b118e: messages the
SEAT_NODE_BUILD_GO workers had given up on stay on the work queue, and each counted
as waiting ahead, so the ask was never asked again (note 62). A message ahead now
waits only when no node wrote its step and no ask of its kind asked after it was
taken; the old message of an ask asked again is deleted from its stream; and
builder_seat_queue (answered by the builder through its queues state, as only its
account may read the stream) shows each message and whether it still waits.
2026-10-26 01:19:30 +01:00
mesh-tools b842b9422d Merge pull request 'Builder and checker read tools read only what they answer, in parallel and bounded (issue 1827)' (#55) from fix/1827-bounded-reads into main 2026-10-25 23:44:25 +00:00
jschoubben 9db15c7cf4 Builder and checker read tools read only the keys they need, in parallel, at most 1000 a call (issue 1827)
mesh/delivery merged: source.main moved to novox/mesh-delivery@b842b9422d2a
mesh/check checking 9db15c7cf4fc on b842b9422d2a
builder_stuck, checker_kept, checker_verdicts, checker_status, checker_previews, checker_pull and checker_try listed a
bucket of thousands of keys and read each value (builder_stuck also its history) one round trip at a time before
filtering, so they timed out at 30 s. Now keys are narrowed by what the key carries first, history is read only for rows
kept, values are read sixteen at a time, at most 1000 a call with the answer saying it was cut, steps newest first, a new
try reads only the newest 64 tries, and each tool logs the values read and the time taken. Same kind as the view's fix
(PR 52); the helper is copied, not put in mesh-sdk, as a few lines do not earn a group delivery.
2026-10-26 00:39:08 +01:00
jschoubben ba47b47e24 Merge pull request 'mesh-delivery-view: pin mesh-sdk at its main so a have's user-list field decodes (issue 1945)' (#54) from fix/1945-view-sdk-pin into main
Reviewed-on: #54
2026-10-25 23:06:21 +00:00
jschoubben b566563dc5 mesh-delivery-view: pin mesh-sdk at its main so a have's user-list field decodes (issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-delivery@ba47b47e2421
mesh/check pass b566563dc592 on ba47b47e2421 | unit 576 run, 63 kept | integration 177 run, 538 kept | composition passed on shanks
2026-10-26 00:01:12 +01:00
jschoubben 2a856e2a76 Merge pull request 'mesh-grants: publish memberships for new placements now; the user list, assertions and grants in shadow (ADR 0547 milestone 1)' (#53) from fix/1945-memberships into main
Reviewed-on: #53
2026-10-25 22:44:11 +00:00
jschoubben ce7d60cb6e Merge pull request 'The delivery view reads only the keys a call names, in parallel, so node_haves and assigner_wants answer within their time (issue 1945)' (#52) from fix/view-reads-fast into main
Reviewed-on: #52
2026-10-25 22:03:21 +00:00
jschoubben 26ba7404bf Pin mesh-grants to mesh-sdk b7b5c6e, whose composer grants it reading every membership (review of mesh-grants, B1)
mesh/delivery merged: source.main moved to novox/mesh-delivery@2a856e2a76ea
mesh/delivery-group group fix/1945-memberships merged: every member is merged
mesh/check checking 26ba7404bfc4 on ce7d60cb6ed0 | novox/mesh-delivery#53 in group fix/1945-memberships
2026-10-25 22:54:01 +01:00
jschoubben b4af9cdb7a mesh-grants publishes only a first or an additive membership, and holds no lock across the bus (review of 1ff5049)
Composed without a root-free judgement, a membership would drop a kind's verified-sender and the
operator's channel approvals would stop being honoured; any loss now stays a shadow difference,
named. A tool no longer waits on a pass's round trip. compose-now says the controller serves no
verb to write a module's grant again.
2026-10-25 22:52:00 +01:00
mesh-tools db53ec975f Merge pull request 'Delivery parts keep running when the bus refuses a bucket they can work without; the builder asks a lost build again; the assigner says why a node gets no want (issue 1945)' (#51) from fix/1945-ask-again-and-why into main 2026-10-25 21:43:11 +00:00
jschoubben 1ff5049d03 mesh-grants publishes memberships on change; the user list, assertions and grants stay in shadow (issue 1945)
Since release 6 nobody publishes memberships, so a module placed since cannot serve its seat
or reach a new state (the operator, 2026-10-25 22:31). The part publishes each membership on
the assignments stream only when it differs from the one last published, composes from the
manifest the wanted build names, and says a refused publish and tries again at the next event.
Pins mesh-sdk 881da7f, whose composer grants mesh-grants that publish.
2026-10-25 22:37:06 +01:00
jschoubben 41f25d3129 Read only the keys a view tool needs, in parallel, so node_haves and assigner_wants answer within the call's 30 s
mesh/delivery merged: source.main moved to novox/mesh-delivery@ce7d60cb6ed0
mesh/check pass 41f25d3129fd on 73be3df28cd4 | unit 21 run, 579 kept | integration 21 run, 655 kept | composition passed on shanks
On 2026-10-25 node_haves and assigner_wants timed out at 30 s four times, even narrowed to one node and one module,
while assigner_want answered at once. Each listed every key of its bucket and then read every value one after the
other, filtering only afterwards; every value is a round trip from the tool through the runtime to the bus, which
opens the bucket and gets the key, so a few hundred keys outlasted the call. Now a node and a module read that one
key directly, a node or a module alone filters the keys before any value is read, and the values are read sixteen
at a time, at most 1000 a call, the answer saying when it was cut. indexer_modules, indexer_candidates and
builder_builds read their values the same parallel way. The kind: a read tool that reads a whole bucket key by key;
every list tool of the view is covered.
2026-10-25 22:33:02 +01:00
jschoubben 4628ba26b4 Delivery parts: take the essential and optional bucket opens from mesh-sdk (issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-delivery@db53ec975f01
mesh/delivery-group group fix/1945-ask-again-and-why merged: every member is merged
mesh/check pass 4628ba26b41a on 73be3df28cd4 | unit 167 run, 443 kept | integration 177 run, 509 kept | composition passed on 3 machines
The open helpers were copied into seven parts; code shared between
modules lives in mesh-sdk, so they are mesh-sdk process's now and the
parts are pinned at fix/1945-optional-buckets (e0d1de04), moved to its
main once it merges, one delivery group with mesh-sdk.
2026-10-25 22:30:22 +01:00
jschoubben 20027e53ff Delivery parts: a bucket the bus refuses at start stops a part only when it cannot work without it (issue 1945)
Met twice on 2026-10-25: the builder of #48 exited on a refused read of
the gate's source and crash-looped, and its new reasks bucket would have
done the same. A grant reaches the bus with a delivery, so a refusal or a
timeout at start is often only a grant not there yet; exiting for it
stopped the whole part until a hand step on the bus.

Every startup open in the parts is now essential or optional. Essential
ones (a part's own state and the input it exists for) still stop the
start, with an error naming the bucket and its grant. Optional ones are
said and left: watches the bus refuses too. The builder builds trunk keys
without the gate's source, which it opens again at the next candidate
that asks a trunk head; toolchains and previews open again at their next
event, as a missing bucket did. builder_ask_again says that asking a
running step again builds it twice.
2026-10-25 22:29:31 +01:00
jschoubben d4a054bfed Assigner: say why a node gets no want, in the journal and in its removed want (issue 1945)
A new assignment whose trunk build was not done fell through the rules
with nothing said: no want, no neither-list entry, no log line, and the
rule holding it back was computed and dropped. The pass now logs the rule
when a node's reason appears or changes and when it has a want again, one
line per change. A node whose want is removed and that does not run the
module gets the reason in that removed want's words, so assigner_want
answers why; a node with no key gets no removed want, since its node agent
would then remove the module and stop holding its resources back.
2026-10-25 22:29:03 +01:00
jschoubben c623e8d235 Builder: ask again an ask its stream can no longer answer, and give it tools to read and ask again (issue 1945)
A build ask its seat's stream gave up on, dropped or ended with no step was
waited for with no end, and on restart taken over and waited for again: no
build node would ever answer it, and nothing could ask it again (trunk key
of mesh-build-node at 4e7b118e, building since 19:06; seven keys at 18:44).

At the step bound, a context's bound and no timer of its own, the builder
reads what the bus says of an ask with no step: gone from its stream, or
first in line while a node took a later ask. Such an ask is asked again
under a new id the key names, logged and said as build.asked-again, at most
three askings, then the key fails with why. The bus's max-deliveries
advisory would tell it sooner, but the builder is granted neither it nor the
workers' information, and the controller is not changed here.

builder_stuck reads every waiting key with its asks, askings and steps;
builder_ask_again writes the new reasks state, which the builder watches
and answers.
2026-10-25 22:29:03 +01:00
jschoubben 998fe3a515 Add mesh-grants in shadow: compose the bus's user list, memberships and assertions from the bus and compare, writing only its own state (ADR 0547 clauses 5-8, issue 1945)
The grants part must compose with the controller's own composer before it can take over the
list; in shadow it proves the two agree, names every input the bus does not carry yet, and
takes no effect. It claims no seat (mesh-sdk's seat list does not name mesh-grants for the
running composer), serves module tools, and keeps running when a read or write is refused.
2026-10-25 22:27:47 +01:00
mesh-tools 73be3df28c Merge pull request 'The assigner writes wants whatever keeps its puts from opening, and never moves a held put's want (ADR 0546, issue 1945)' (#50) from fix/assigner-starts-without-puts into main 2026-10-25 21:22:55 +00:00
jschoubben eb65535f45 A want marked put and held stands while its put is not heard: the rules give it back only on a put heard released, so a start before the puts are open moves no node (ADR 0546 clause 5)
mesh/delivery merged: source.main moved to novox/mesh-delivery@73be3df28cd4
mesh/check pass eb65535f45db on 73be3df28cd4 | unit 167 run, 433 kept | integration 177 run, 499 kept | composition passed on ace
2026-10-25 22:11:44 +01:00
jschoubben fa6cdbbe61 The puts' watch never gates the assigner's first pass: it is not waited for at the start, and one that ends before it handed what stands only stops the puts (ADR 0546) 2026-10-25 22:06:36 +01:00
jschoubben 462ec9cfb1 The assigner opens the puts' bucket again on a later pass once it is there and granted, so a put works without a restart (ADR 0546, ADR 0201) 2026-10-25 21:59:24 +01:00
jschoubben 9950689a11 The assigner starts and writes wants whatever keeps the puts' bucket from opening or being watched (not there, not granted, no answer): it takes no put further until it starts again, said in its journal (ADR 0546) 2026-10-25 21:58:36 +01:00
mesh-tools 71a6a5bf77 Merge pull request 'Put a version of a module on a node with one tool call: the assigner's put, release and puts (ADR 0546)' (#49) from feat/put-version into main 2026-10-25 20:52:05 +00:00
1274 changed files with 166797 additions and 10784 deletions
+3
View File
@@ -1 +1,4 @@
vendor/
# A compiled program never enters a commit (issue 1577): `go build .` in a command directory leaves one there.
modules/*/cmd/*/mesh-*
!modules/*/cmd/*/mesh-*.go
@@ -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"
}
}
}
}
}
}
]

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