Compare commits

..
Author SHA1 Message Date
jschoubben 8439ec117c Pin mesh-sdk at the group's head acbf80a in every module the group touches, so the provisioner carries what a consumer contributes and route-proxy keeps its routes; count a removal confirmed only when the bus says the stream or consumer is not found (issue 774)
mesh/delivery ready: it is merged on the Approve
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…
mesh/delivery-group group feat/072-moves rejected: composed together: the checker's verdict at the members' heads (10e5c18979c3): shanks
2026-10-31 10:24:49 +01:00
jschoubben a40b4d3d3f Let the vault mint every wanted bus account whoever holds the controller's lease, keep its mint over a controller's issued value, and tell a node which modules it judged handed, so no account waits on a node reporting to the controller (issue 774) 2026-10-31 09:49:15 +01:00
jschoubben 92d6a5e9a0 Carry a plan entry's contribution as an opaque payload in the provisioner client, so the client knows no provision's fields and route-proxy reads its routes with its own contract (issue 774, research 072 row 34) 2026-10-31 09:45:07 +01:00
jschoubben d959d37f8e Pin mesh-sdk at the group's head in the placement readers (issue 774) 2026-10-31 09:30:33 +01:00
jschoubben 85c336f260 Read the seat mesh-placement's state through the sdk's PlacementKeys in mesh-conditions, mesh-config, mesh-provisioner, mesh-vault and mesh-self-check, so they read its keys per module and node and the form before alike while it is delivered (issue 774, research 072 row 37) 2026-10-31 09:27:15 +01:00
jschoubben 21fddffc6e Refuse a route received as a file, naming every module still on that path, since a route is a provision served from the seat's plans alone (issue 774, research 072 row 34) 2026-10-31 09:23:56 +01:00
jschoubben 252c8537c0 Read a plan entry's composed contributions and the consumer's address, so the client keeps reading plans once the seat carries what route-proxy serves its routes from (issue 774, research 072 row 34) 2026-10-31 09:23:56 +01:00
jschoubben 52c875f870 Copy the controller's hand acts into mesh-hand-acts' log once, and let the bus module's tools delete the ASSIGNMENTS stream at the lease write (issue 774, research 072 rows 26, 33) 2026-10-31 09:15:53 +01:00
mesh-tools a68b39f7b5 Merge pull request 'The gate takes away an ask once no delivery it covers can still merge by it (issue 774)' (#577) from fix/merge-ask-ends-on-merge into main 2026-10-31 01:33:01 +00:00
jschoubben fdb8deea4c Gate: an ask ends once no delivery it covers can still merge by it, so an ask of a pull request merged another way cannot hold the teller's room for a day (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@a68b39f7b512
mesh/delivery-group group fix/merge-ask-ends-on-merge merged: every member is merged
mesh/check checking fdb8deea4c9e on a68b39f7b512 | novox/mesh-catalog#577 in group fix/merge-ask-ends-on-merge
endStaleAsks skipped every merged delivery, so a pull request merged without the
Approve kept its merge ask in the gate's asks until it lapsed: on 2026-10-31 two
such asks held two of the router's three places for the teller and a placement
ask was refused for want of room. Every look now takes away a standing ask none of
whose deliveries is still open, read from what the gate keeps, so a failed take
away is retried. A settle asked again for another set takes the earlier settle ask
away for the same reason.
2026-10-31 02:16:08 +01:00
mesh-tools dcf3950f7b Merge pull request 'The vault reads a machine's placement once per answer and says how long each answer took (issue 774)' (#576) from fix/vault-own-loop into main 2026-10-31 00:40:42 +00:00
jschoubben b60138990b Read a machine's placement once per answer of own, and say how long every answer took and every refusal, so an answer of 143 credentials is not 143 bus reads and an ask that went unanswered can be told from one never heard (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@dcf3950f7b07
mesh/check being carried to b60138990b34 on dcf3950f7b07 | from 0e78909a75b9
Quick fix: the plan reader keeps the machine's one placement key for the answer, as it keeps each module's assignment (issue 1126, 2026-10-28); the second time one answer's reads per credential held own past the node-engine's 15 s.
Thorough fix (not here): the node-engine asks own only when a secret or a credential it composes from changed, and the assigner writes no want that is unchanged.
2026-10-31 01:05:01 +01:00
mesh-tools 0e78909a75 Merge pull request 'The provisioner says why a key stays asked when its provider reads no plans of the seat (issue 774)' (#575) from fix/holder-says-unread-plans into main 2026-10-30 23:43:14 +00:00
jschoubben bb18c4d14f Say why a key stays asked when its provider's helper reads no plans of the seat, so a provider left on the controller's assignments is named instead of waited on in silence (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@0e78909a75b9
mesh/check being carried to bb18c4d14fc3 on 0e78909a75b9 | from d412991c6992
2026-10-31 00:27:55 +01:00
mesh-tools d412991c69 Merge pull request 'One rule for whether a plan hands its module, so no part reads a provisions-only plan as handed (issue 774)' (#574) from fix/plan-hands into main 2026-10-30 23:18:59 +00:00
jschoubben 6bd3da7736 Read every module whole for the plan-hands check, so its verdict follows their source, and judge a shared value's consumer by whether its plan hands it; leave mesh-provisioner out of the group (review round 1, issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@d412991c6992
mesh/delivery-group group fix/plan-hands merged: every member is merged
mesh/check being carried to 6bd3da773683 on ca48943b925a | from mesh-sdk 125bb9177189 | novox/mesh-catalog#574 in group fix/plan-hands
2026-10-31 00:00:46 +01:00
jschoubben 1b0c09763e Pin mesh-sdk 17fc9b9 in mesh-vault and mesh-provisioner-client for Plan.Hands, and say what providerPlan answers now that a plan of provisions alone exists (issue 774) 2026-10-31 00:00:46 +01:00
jschoubben 3c3d018dbc Ask mesh-sdk's Plan.Hands wherever the vault and the provisioner client judge a module handed, and refuse in the catalogue check any line that takes a plan existing for it, so a plan of provisions alone never again withholds a module's secrets (issue 774) 2026-10-31 00:00:46 +01:00
mesh-tools ca48943b92 Merge pull request 'The vault answers a module's own secrets unless its plan hands it, not whenever a plan exists (issue 774)' (#573) from fix/secret-made-keyed-plan into main 2026-10-30 23:00:16 +00:00
jschoubben d5c4f8aaa9 Answer a module's own secrets to its node-engine when its plan only keys a provision, so twenty modules whose route the provisioner now keys compose again (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@ca48943b925a
mesh/check being carried to d5c4f8aaa968 on ca48943b925a | from 5be46a80cb35
2026-10-30 23:48:29 +01:00
mesh-tools 5be46a80cb Merge pull request 'The provisioner follows the one rule for provisions, and the gate says a group's members when they change (issue 774)' (#572) from fix/one-rule-for-provisions into main 2026-10-30 22:33:52 +00:00
mesh-tools cb91a4b9c2 Merge pull request 'The provisioner client takes a plan with no templates as nothing to fill (issue 774)' (#570) from fix/client-takes-plans-without-templates into main 2026-10-30 21:33:22 +00:00
jschoubben 843c1c337c Compose plans with mesh-sdk 6c5a8ef893b4, the one rule of what is keyed (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@5be46a80cb35
mesh/delivery-group group fix/one-rule-for-provisions merged: every member is merged
mesh/check being carried to 843c1c337cdd on cb91a4b9c292 | from mesh-sdk b74e23f973ab | novox/mesh-catalog#572 in group fix/one-rule-for-provisions
A module not handed now has a plan for what a provider's helper answers of it, so grafana's route is keyed and
route-proxy's helper hears it; handed modules' plans are unchanged. Deliver after #570, whose client reads a plan with
no templates.
2026-10-30 22:29:44 +01:00
jschoubben fdd1833143 The gate says a group again when a member joins, leaves or pushes, or its why changes, so the forge never shows a stale member count (issue 774)
On 2026-10-30 mesh-catalog #571 joined fix/held-module-and-assigner-contract at 21:59; the group stayed
checking, so mesh/delivery-group kept saying "3 of 3 member(s) ready" for a group of four, and #571 had
no mesh/delivery-group status until the state changed.

Quick fix: compare the why too. Thorough fix: compare the why and the member ids (each names its head),
so a push that keeps the count is said as well. Chose thorough: same size, covers the push case.
2026-10-30 22:29:42 +01:00
mesh-tools 5d9cead336 Merge pull request 'The provisioner reads a provider's outcome per offer, and modules subscribe before they wait, so gitea's credentials, routes and early events are not lost (issue 774)' (#571) from fix/held-module-and-assigner-contract into main 2026-10-30 21:17:44 +00:00
jschoubben b5475cfa5c Subscribe as soon as the runtime serves, never after a guessed wait, so no event sent at a start is missed (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@5d9cead336ab
mesh/delivery-group group fix/held-module-and-assigner-contract merged: every member is merged
mesh/check checking b5475cfa5c3d on 395a37dddab9 | novox/mesh-catalog#571 in group fix/held-module-and-assigner-contract
mesh-healers slept 2 s before its first subscribe to the keeper's conditions; its whole test delivered
condition-raised once the lease was read, often before that, and the event was dropped: "never: the would-act
written" in about two runs of three on main, failing the group check. Eight modules had the same shape.

Quick fix: sleep after a failed subscribe in mesh-healers alone.
Thorough fix (chosen, the kind is in eight modules): every loop that asks the runtime for events or a watch
waits for stdio.Served() and subscribes at once, sleeping only after a refusal (mesh-healers, mesh-config,
mesh-mail, mesh-issues, mesh-provisioner, mesh-watcher, mesh-delivery-gate); mesh-knowledge, whose pinned
mesh-sdk has no Served, subscribes first and sleeps after a refusal. The healers' whole test delivers the
event only once the bundle has subscribed it, as the runtime's consumer holds it until then.
2026-10-30 21:57:31 +01:00
jschoubben 7522961ae3 Let a plan of provisions alone fill nothing, so a module not handed can have one for its route (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@cb91a4b9c292
mesh/check being carried to 7522961ae3b0 on cb91a4b9c292 | from 5d9cead336ab
A plan with no templates archive made the client fetch an empty source and fail the module's report. The next
mesh-provisioner composes such plans (a module not handed gets one for what a provider's helper answers, its route), so
this reaches the nodes first.

Quick fix: none (a failed report per routed module). Thorough fix (chosen): a plan with no archive is read as provisions
alone by the one client every node runs.
2026-10-30 21:42:03 +01:00
jschoubben b8ce4f0c0a Read a provider's outcome per offer, and read route-proxy's and node-tools' entries, so provisions a helper does not answer stop failing and routed keys stop standing asked (issue 774)
mesh/delivery-group group fix/held-module-and-assigner-contract rejected: composed together: the checker's verdict at the members' heads (c82f18b5e6bb): integr…
mesh/delivery closed: the pull request closed unmerged
mesh/check failed: integration: mesh-healers/cmd/mesh-healers.TestTheHealersWholeActOnTheBusAndBrake: never: the would-act written | b8ce4f0c0ac5 on 3…
The sweep of 2026-10-30 20:10: <node>.claude-code.gitea-read and <node>.mesh-build-node.npm-package-registry failed on
all four nodes with "gitea does not serve": gitea's provider helper answers only forge-account and refuses every other
bind into gitea.bound, and the provisioner took any entry of a module declaring `bound` as the outcome. And
novox.gitea.route, novox.keycloak.route and <node>.claude-code.mcp-endpoint stood asked: route-proxy and node-tools keep
entries the manifest did not read.

Quick fix: add route-proxy.bound and node-tools.bound to the reads. Thorough fix: judge each offer by who answers it
(answered-by: helper reads the entries, any other offer of a module that names a helper is judged by its have), and say
on gitea's forge-account that the helper answers it. Chose both: the reads alone leave gitea's two provisions failed.
2026-10-30 21:26:31 +01:00
jschoubben 395a37ddda Merge pull request 'A catalogue check parses every manifest as the nodes do; mesh-provisioner holds a refused manifest alone (issue 774)' (#567) from fix/one-bad-item-holds-one-module into main
Reviewed-on: #567
2026-10-30 18:31:01 +00:00
jschoubben b4b22aac20 Pin mesh-provisioner to mesh-sdk 754edfae, so a manifest the mesh refuses holds that module alone in its plans (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@395a37dddab9
mesh/delivery-group group fix/one-bad-item-holds-one-module merged: every member is merged
mesh/check checking b4b22aac2060 on 0695ae25dbd2 | novox/mesh-catalog#567 in group fix/one-bad-item-holds-one-module
PlansHeld reads the same frombus input that refused every node on 2026-10-30 at 19:13; at 754edfae it holds the
module whose manifest the mesh refuses, with why, and composes every other module's plan. Quick and thorough fix are
the SDK's (one rule in frombus input); this is the pin.
2026-10-30 19:27:56 +01:00
jschoubben faaa6d3df2 Judge every manifest as the nodes read it in the catalogue's own check, so a manifest they refuse never reaches main (issue 774)
ca8d6f33 gave power's bar item attributes its bar places by none of; ParseManifest refuses that, and at 19:13 on
2026-10-30 every node-engine refused to compose. No check of #565 parsed the manifest: that judgement was
mesh-controller's `module check`, run over every manifest by the old merge gate, and since the checker moved to
mesh-delivery it runs this repository's Go tests and the composition, where no test parsed a manifest whole.

Quick fix: a row refusing node-bar block attributes other than bar, place and order. Thorough fix: checks/manifests
parses every modules/*/module.json with mesh-sdk's ParseManifestWith against predefined.json and its templates beside
it, then CatalogueProblems, IdentityProblems and DataProblems over the shelf, the functions the nodes and registration
use. Chose thorough: it refuses every kind of manifest the nodes refuse, not this attribute alone. It fails at
7e905a66 naming power's item 2 and passes at 0695ae25.
2026-10-30 19:27:03 +01:00
jschoubben 0695ae25db Merge pull request 'Revert power's bar item attributes, which every node-engine refuses, so machines compose again (issue 774)' (#566) from fix/power-item-attributes into main
Reviewed-on: #566
2026-10-30 18:16:15 +00:00
jschoubben eb1cf1bb44 Revert power's bar item attributes shows and state: every node-engine refuses them (placed by bar, place), so no machine could compose any module after #565 (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@0695ae25dbd2
2026-10-30 19:15:54 +01:00
jschoubben 7e905a6694 Merge pull request 'The provisioner holds only a bad module, the vault keeps bus accounts by declaration, and silent faults from the release sweep speak up (issue 774)' (#565) from fix/sweep-2026-10-30 into main
Reviewed-on: #565
2026-10-30 18:12:53 +00:00
jschoubben 2455445dab 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-catalog@7e905a66944e
mesh/delivery-group group fix/sweep-2026-10-30 merged: every member is merged
mesh/check checking 2455445dabee on 72cee59acc32 | novox/mesh-catalog#565 in group fix/sweep-2026-10-30
2026-10-30 18:43:13 +01:00
jschoubben bc433b8304 Take the compiled mesh-vault program out of the tree and ignore it, since build output never belongs in the catalogue (issue 774) 2026-10-30 18:40:50 +01:00
jschoubben 647c5c3c06 Pin nine modules to the fix group's mesh-sdk, 206f794e, which carries Follow, the bus retry and the forms list (issue 774)
mesh-provisioner, mesh-config, mesh-issues and mesh-healers need KeptState.Follow; mesh-build-go, -image and -node the
retry of a state write the bus delivered to no one; claude-code asks.Condition; mesh-vault the credential forms list.
Each passes go vet, go vet -tags integration and go test on this pin, built with no go.work.
2026-10-30 18:39:49 +01:00
jschoubben 434214e615 Judge a bus account by its declaration, never by a broker- prefix, so the controller's address is kept (issue 774)
683fd547 and b7f87822 took every `broker-<id>` own secret for a bus account. mesh-controller's broker-address is the
bus's address (MESH_BROKER_ADDRESS_FILE, read by broker.FromEnvironment to issue enrolment tokens and credentials):
c1 would have refused to keep it, and it was counted among the "owed bus-account hashes" every minute.

A name is now a bus account when it is declared: `broker` or `provisioner-broker`, or the own secret of an account the
grants part wants, which it composes from the manifests' bus processes (what catalogue.ToolsBusSecret names). A
`broker-<id>` holding an account the controller issued before its want was heard is still hashed. Anything else is
kept as given, never refused, never minted over, never hashed and never owed. The mint path replaced only wanted
names already, so the address was never at risk of replacement there; the test now proves it.

Quick fix: except broker-address by name. Thorough fix: the declaration decides, for every name. Chose thorough.
Table rows: broker-address kept as given, not replaced, owed nothing; broker-nats-tools replaced by a minted account
once wanted and refused a plain value after. They fail on the previous code.
2026-10-30 18:38:59 +01:00
jschoubben 21c86c819c Follow the runtime's watches through mesh-sdk's KeptState.Follow, not four loops of their own (issue 774)
mesh-provisioner, mesh-config, mesh-issues and mesh-healers each wrote a watch-again loop (`watched`). mesh-sdk
61b67d61 adds KeptState.Follow, which asks again with backoff and says each give-up, and its watches now answer a
change at once and run the handler beside the runtime (the provisioner's "names no watch of this bundle"). The four
modules use it; their loops are gone. Built and tested against the mesh-sdk group head through a go.work that is not
committed: their mesh-sdk pins move to that head with the group.
2026-10-30 18:33:32 +01:00
jschoubben 3aae6c0890 Say on a failed provisioning report when the failure began, kept while only its words change (issue 774)
The client dated each report with the time it was said, and says a failure again whenever its words change, so the
controller's apply-failing alarm, which reads that time, saw a new failure: on novox the vault's refusal replaced a
bus refusal at 16:56 and the alarm cleared for five minutes while every module still failed.

Quick fix: the controller keeps a raised alarm while any failure stands (done there). Thorough fix: the report says
failing-since, kept from the module's last report (this run's, else the applied bucket's) while it stays failed,
whatever its reason says; any other status ends it. Chose both: the controller can now judge the failure's age
rather than its newest words, which is its change to make.
2026-10-30 18:32:30 +01:00
jschoubben f3b13de20d Answer a move the vault refuses for holding another value with fields, not words alone (issue 774)
move-in's refusal "the vault already holds another value for ..." was an error's text, which the node-engine and the
controller could only quote. It is now an answer: `refused` with the reason holds-another-value and the held value's
record (generation, changed by, changed at, origin; never the value), `why` in words, and no read-back, so an
engine that does not read `refused` still fails as before ("answered with no read-back"). keepLocked's refusal is a
typed error (HeldAnother) whose text is unchanged for every other path.

Quick fix: a stable prefix in the text. Thorough fix: the fields. Chose thorough, as asked; mesh-host's
moveIntoTheVault has to read `refused` to use them.
2026-10-30 18:30:45 +01:00
jschoubben b7f878226e Replace a held bus-account value that is no account by a minted account when its user is wanted (issue 774)
The three random values kept before the vault judged them (own/novox/nats/broker-nats-tools,
own/novox/mesh-controller/broker-address and broker-management) are held, so minting refused them ("held already")
and their users stay off the bus. A value that is no account has no hash and logs in nowhere, so replacing it
breaks no login the grants part composed. When the grants part wants the user (every want is heard at the vault's
start), the vault now mints an account in its place, judged again under the store's lock, keeping the value it
replaces beside it. A held account is still never minted anew (ADR 0561 §4).

Quick fix: let an operator rotate the three by hand. Thorough fix: the mint path replaces any such value, so no
other one stays behind. Chose thorough.
2026-10-30 18:29:48 +01:00
jschoubben 683fd547b2 Keep a bus account in the vault only as an account: a random value for a broker name is not issued (issue 774)
own/novox/nats/broker-nats-tools (moved in today) and mesh-controller's broker-address and broker-management (moved in
from the controller's store) are random values, not the account JSON a bus user is hashed from, so the hash step says
"not JSON" and the grants part leaves those users off the bus. The controller made them (fixed there, c421629f); the
vault kept them, since nothing it keeps judged a covered name's value.

Quick fix: refuse a plain value in move-in. Thorough fix: keepLocked, which every way into the store passes, refuses
for any covered name (broker, provisioner-broker, broker-<binary>, which catalogue.ToolsBusSecret names) a value that
is no account, saying "not issued". Chose thorough: one rule where every value is kept.
2026-10-30 18:29:01 +01:00
jschoubben f52545c4ba Stop asking the forge again for a mesh/check it refuses for good, and end the ask when its key is deleted (issue 774)
gitea asked an owed status again every 30 s forever, even on 403 and 404 (jschoubben/hal and jschoubben/novadock, 119
times each in the sweep), and a key the checker deleted did not end a loop already running.

Quick fix: return from askAgain on 403/404. Thorough fix: the same, said once with its cause; a delete of the key
marks its revision seen, so any running ask again ends before its next write; a newer revision still asks anew.
The checker no longer owes statuses outside the mesh (mesh-delivery), which removes the trigger. Not done: raising
the module's health, since gitea's TypeScript SDK has no seat publish for a condition (mesh-sdk).
2026-10-30 18:23:55 +01:00
jschoubben 2066021fca Read the vault's credential forms from mesh-sdk's one list, as the provisioner client does (issue 774)
pair.go kept its own copy of the forms a plan may name; the copy in the provisioner client went in #564, after it
refused plans naming `none` while the vault's copy took them. Quick fix and thorough fix are the same here: the vault's
constants are mesh-sdk's (catalogue.CredentialForm*), and a form is judged by catalogue.IsCredentialForm, so a new
form is added in one place. No new test: behaviour is unchanged, and the existing form tests pass on the shared list.
mesh-vault's mesh-sdk pin moves to 9379e9a9, the provisioner client's, the first with the list.
2026-10-30 18:22:09 +01:00
jschoubben ca8d6f33a5 Say on power's draw item which state its bar block reads, so the bar's holder can be granted it (issue 774)
i3status-rust on g14 and shanks cannot watch power.draw ("was issued: none", 148 times): its read was granted from
the bar contribution's data (shows: state, options.state; catalogue.ReadsGranted), and design 57 L3 moved the block
to an item, whose attributes said nothing of it, while ReadsGranted reads contributions alone.

Quick fix: `reads: power.draw` on i3status-rust, which grants every machine's draw instead of its own key (ADR 0260).
Thorough fix: the item says its read (shows: state, state: power.draw), and mesh-sdk's ReadsGranted grants a held
item's read as it grants a contribution's, keyed to the machine. Chose thorough; its mesh-sdk half is another
repository's, and until it lands the fault stays.
2026-10-30 18:19:59 +01:00
jschoubben 0e6ca453be Say grafana's refused admin password once with its cause, and ask again hourly, not every 30 s (issue 774)
Grafana on ace answers 401 to the vault's admin password, every 30 s since at least 2026-10-05. I think the cause is
the adoption: Grafana keeps the admin password it was first given in its database and applies
GF_SECURITY_ADMIN_PASSWORD__FILE only when it creates it; ace's Grafana came with its database before the mesh minted
own/ace/grafana/admin (2026-09-30, generation 1, unchanged since), so the minted value never took (keycloak's
admin, hq issue 179, is the same kind).

Quick fix: none in code; the password in Grafana's database must become the vault's (grafana cli admin
reset-admin-password in the container), an act on ace no tool offers yet. Thorough fix: a step that converges the
admin password at every start, which needs a container entrypoint the node-engine's declaration does not carry.
This commit does what the catalogue can now: the refusal is said once with its cause and what ends it, asked again
hourly, and its end is said.
2026-10-30 18:18:16 +01:00
jschoubben 2d28a52d61 Ask each machine's engine for the root-free word again: the engines were deaf, not absent (issue 774)
The sweep took the node seat node-engine's root-free for one nothing answers; the engines had been refused their
subscriptions by the bus since 12:10 (fixed in mesh-host), so the address was right. This puts it back and keeps
what the previous commit added: every failed ask said with its error, a fresh word not thrown away, and the failure
as the held ask's reason.
2026-10-30 18:14:42 +01:00
jschoubben 7531c3257d Give claude-code's runtime its forge credential file, and say a credential it cannot place as a condition (issue 774)
The file gitea-read.secret was declared with no owner, so it was root's at 0600; claude-code's tools run in the
node's runtime as the operator's account and could not read it ("permission denied", ~870 times a machine), said
only in the journal. A file a bundle reads is the runtime account's only when a word of the bundle names it
(catalogue givenTo); this one was found through MESH_CLAUDE_CODE_STATE alone.

Quick fix: name the file with the bundle word MESH_CLAUDE_CODE_FORGE, which the reader now uses. Thorough fix: the
same, plus a failure to place the credential raises a warning condition of the module on the operator-channel seat,
once per reason, cleared when it places again. Chose thorough: the fault ran unseen for days. A change of the
credential now restarts the node's runtime, as for every file a bundle word names.
claude-code's mesh-sdk pin moves to 2da0de75 (main's), the first with asks.Condition.
2026-10-30 18:13:44 +01:00
jschoubben aec2ed3c24 Announce every state another module reads as readable, and refuse a catalogue that does not (issue 774)
The controller grants a read only from the owner's announcement (ADR 0495 clause 3). gitea announced `bound`
unreadable, so mesh-provisioner bound no forge account; disk-load announced `readings` unreadable, refused to restic,
nfs-server and mounts; restic announced `good` unreadable, which the self-check reads. Nothing in the catalogue
compared readers with announcements since the gate's manifest check went (clause 5).

Quick fix: mark those three states readable. Thorough fix: also checks/readable, which reads every owner's Go and
TypeScript announcement and refuses any `reads` of a state it does not announce readable, so the next owner to
announce cannot repeat it. Chose thorough: the same kind hit three owners at once.
Reads of other repositories' owners (mesh-controller.lease, mesh-grants.accounts) are not judged here.
2026-10-30 18:07:53 +01:00
jschoubben bc27189627 Ask the root-free word of the controller's verb, which answers, and say every failed ask, so Telegram carries asks that offer an approval again (issue 774)
The messenger asked seat:node-engine.root-free@<machine>, which nothing on the mesh answers (the engine's acts are
left out of its declared state, mesh-host #178; ADR 0571 §11's self-check probe is not held either), and deleted
its kept word on each failure without a log line: every ask from an agent or a merge was held, silently.

Quick fix: point RootFreeVerb back at mesh-controller.root-free. Thorough fix: the same, plus every failed ask is
logged with its error, the failure is kept beside the word and becomes the held ask's reason, a fresh word is no
longer thrown away by one failed ask (an answer is still judged only on a word asked then), and a held ask whose
reason changes is logged. Chose thorough: the silence is what hid the fault for a day.
2026-10-30 18:01:54 +01:00
jschoubben 6d67c22bf3 One bad module must not drop its node: the provisioner composes each module's plan on its own (issue 774)
novox since 15:36 (nats's built manifest) and ace since 15:37 (baserow's config.json) had no plans at all, so the
vault refused every secret their clients asked. The holder now composes with frombus.PlansHeld: a module whose build
or plan refuses is held, its why kept in the new state refused and read by provisioner_plans, its last plan and its
provisions left as they were, and every other module on the node composed and written. Pins mesh-sdk 59311407.
2026-10-30 17:55:19 +01:00
jschoubben 72cee59acc Merge pull request 'The provisioner client accepts credential form none through the mesh-sdk list (issue 774)' (#564) from fix/credential-forms-one-list into main
Reviewed-on: #564
2026-10-30 15:10:28 +00:00
jschoubben 67dcf252ec Check a plan's credential form against mesh-sdk's one list, so the client takes plans naming none again (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@72cee59acc32
mesh/delivery-group group fix/credential-forms-one-list merged: every member is merged
mesh/check checking 67dcf252ecfa on 82ac613b4ed0 | novox/mesh-catalog#564 in group fix/credential-forms-one-list
Since 13:55 the client refused every plan with credential-form none
(gitea's forge-account, the artifact store), and g14 and shanks placed no
files for mesh-build-go, mesh-build-node, mesh-build-image or ssh-client.
Second meeting of the credential-form list kind: the controller's copy was
fixed alone before. Thorough: one list in mesh-sdk; this client drops its
copy and pins mesh-sdk 9379e9a. A none entry is never a holder's issued
value: its bound values come from the provider's entry, and a secret named
for it fails loudly instead of waiting on a vault that mints nothing.
2026-10-30 16:06:46 +01:00
jschoubben 82ac613b4e Merge pull request 'Leave the engine's acts out of the bus tools' removals, so the bus's user list is composed again (issue 774)' (#563) from fix/774-nats-tools-no-engine-acts into main
Reviewed-on: #563
2026-10-30 11:38:25 +00:00
jschoubben 787563358b Leave the engine's acts out of the bus tools' removals, so ace's tool runner, which carries the bundle on its own account, is no second writer and the grants part composes the bus's user list again (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@82ac613b4ed0
mesh/check checking 787563358b67 on 82ac613b4ed0
2026-10-30 12:34:22 +01:00
mesh-tools 8f212e559a Merge pull request 'Design 47's last milestones: the keeper, self-check, healers, hand-acts, provisioner, vault, artifact sweep, disk readings, credential forms (issues 774, 1952, 1953, 1954, 1955)' (#560) from feat/774-l3-reland into main 2026-10-30 11:01:48 +00:00
mesh-tools 1aa22efb0d Merge pull request 'mesh-delivery-gate: an answer stands only for the ask it answers, so the approved release merges (issue 1730)' (#562) from fix/gate-answer-stands-for-its-ask into main 2026-10-30 11:01:15 +00:00
jschoubben d6a069ae49 mesh-delivery-gate: an answer stands only for the ask it answers, and a merge never handed says why
mesh/delivery merged: source.main moved to novox/mesh-catalog@1aa22efb0d78
mesh/check being carried to d6a069ae4959 on 1aa22efb0d78 | from 12c2574c9d54
On 2026-10-29 the group feat/774-vault-mints-l2 was approved and never handed: hq#495 kept a Decline of an earlier ask beside the Approve of the next, readyAsks skipped it and handTurn returned nil for every member, with no log line. The Approve and a new ask now clear an earlier Decline, a run's answer names its ask (answered_ask), an approved merge that waits for nothing and is not handed logs why once and is a stalled line, and delivery_turns lists no unwritten turn while the check gates no merge.
2026-10-30 11:35:35 +01:00
jschoubben f3a57dcc56 mesh-self-check: the whole-run integration test answers the setting probes with the probes it asserts, since the default list is D13 and D-fill
mesh/delivery merged: source.main moved to novox/mesh-catalog@8f212e559a3e
mesh/delivery-group group feat/774-l3-reland merged: every member is merged
mesh/check checking f3a57dcc56fb on 1aa22efb0d78 | novox/mesh-catalog#560 in group feat/774-l3-reland
2026-10-30 10:21:28 +01:00
jschoubben 35ff67bda0 Design 47's last milestones: the keeper, self-check, healers, hand-acts, provisioner, vault, artifact sweep, disk readings, credential forms (issues 774, 1952, 1953, 1954, 1955)
mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
One commit with exactly the tree of the reviewed head 35b4509c0a, on main 12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
2026-10-30 02:21:51 +01:00
mesh-tools 12c2574c9d Merge pull request 'Let the forge's sshd take a burst of connections from one build machine, so a check's parallel fetch is not dropped (issue 774)' (#561) from fix/gitea-ssh-maxstartups into main 2026-10-30 00:54:02 +00:00
jschoubben 8bf85863ce gitea: raise the forge's sshd MaxStartups to 100:30:200, so a burst of the mesh's machines fetching at once is not dropped
mesh/delivery merged: source.main moved to novox/mesh-catalog@12c2574c9d54
mesh/check being carried to 8bf85863cedb on 12c2574c9d54 | from f61265f7a0f3
2026-10-30 01:38:58 +01:00
mesh-tools f61265f7a0 Merge pull request 'Pin the parse-only mesh-sdk in the build modules, so they build design 47's manifests (issue 774)' (#559) from fix/774-new-words-first into main 2026-10-30 00:10:07 +00:00
jschoubben 22ff0d1000 Pin the parse-only mesh-sdk 78c9ec8 in the build modules, which read each manifest they build, so the new words are read first (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@f61265f7a0f3
mesh/delivery-group group fix/774-new-words-first merged: every member is merged
mesh/check pass 22ff0d1000f3 on 96b129e54441 | judged by the candidate's mesh-build-go at d351f9c41ff8
2026-10-30 00:39:37 +01:00
jschoubben 7d43574783 Pin mesh-sdk 6e4a301 in the build modules, which read each manifest they build, so the new words are read first (issue 774) 2026-10-30 00:23:30 +01:00
mesh-tools 96b129e544 Merge pull request 'mesh-config: read config declarations from each placed module's own manifests on the bus, never from the controller (issue 1953)' (#558) from fix/1953-config-from-builds into main 2026-10-29 19:47:55 +00:00
jschoubben 0d71d8b56c Move the head so the checker takes #558 again: a reopened pull request is not taken back as a candidate (recovery of de-spiegel, operator's yes 2026-10-29 17:56 CET)
mesh/delivery merged: source.main moved to novox/mesh-catalog@96b129e54441
mesh/check being carried to 0d71d8b56c55 on 96b129e54441 | from 030f9ee4e780
2026-10-29 18:10:27 +01:00
jschoubben 96a37f58ca mesh-config: read declarations from each placed module's own manifests as built, and placement alone, so a module from its own repository resolves (issue 1953)
mesh/delivery closed: the pull request closed unmerged
mesh/check pass 96a37f58ca76 on 030f9ee4e780 | unit 1173 run, 3272 kept, 13 skipped | integration 1212 run, 3274 kept, 22 skipped | over bound: unit, …
The controller's declared-config mirror never held de-spiegel's keys, and Resolve skipped
an assigned module it held no declarations of, silently: de-spiegel stayed held on novox
with its values set. Declarations now come from the indexer's trunk manifest with the
wanted build's laid over it per node; which module runs where from the seat
mesh-placement alone. A placed module with no readable manifest is listed as unresolved
with why, logged, and keeps its last resolved values.
2026-10-29 15:19:24 +01:00
mesh-tools 030f9ee4e7 Merge pull request 'Revert L3: its node-engine removed every held module on novox, the bus server among them (issue 774)' (#557) from revert/774-l3 into main 2026-10-29 08:59:53 +00:00
jschoubben c8240c38bb Revert L3 (mesh-catalog, merge 15552413): 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-catalog@030f9ee4e780
mesh/delivery-group group revert/774-l3 merged: every member is merged
mesh/check being carried to c8240c38bb89 on 030f9ee4e780 | from 1555241393fe | novox/mesh-catalog#557 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:30 +01:00
mesh-tools 1555241393 Merge pull request 'L3: mesh-vault mints, retires and signs; nats makes its certificate; messenger asks each engine (issue 774, ADR 0561), rebased onto L2' (#556) from feat/774-vault-mints-l2 into main 2026-10-29 07:44:23 +00:00
jschoubben add6f143ca mesh-delivery-gate: an answer stands only for the ask it answers, and a merge never handed says why
mesh/delivery merged: source.main moved to novox/mesh-catalog@1555241393fe
mesh/delivery-group group feat/774-vault-mints-l2 merged: every member is merged
mesh/check pass add6f143ca23 on 158348ede789 | unit 1601 run, 5122 kept, 47 skipped | integration 1637 run, 5215 kept, 49 skipped | over bound: unit, …
On 2026-10-29 the group feat/774-vault-mints-l2 was approved and never handed: hq#495 kept a Decline of an earlier ask beside the Approve of the next, readyAsks skipped it and handTurn returned nil for every member, with no log line. The Approve and a new ask now clear an earlier Decline, a run's answer names its ask (answered_ask), an approved merge that waits for nothing and is not handed logs why once and is a stalled line, and delivery_turns lists no unwritten turn while the check gates no merge.
2026-10-29 02:15:34 +01:00
jschoubben b46089b698 Merge remote-tracking branch 'origin/main' into l3-mc-main 2026-10-29 02:15:33 +01:00
mesh-tools 158348ede7 Merge pull request 'mesh-delivery-gate asks the operator's Approve on a verdict only: the one-hour fallback goes (issue 1733)' (#555) from fix/1733-no-hour into main 2026-10-28 21:29:52 +00:00
mesh-tools ff4c6c0435 Merge pull request 'The Go, Node and image build modules watch their settings file and say a capacity of 0 in their health' (#554) from fix/park-wakes into main 2026-10-28 21:03:17 +00:00
jschoubben c193d2b389 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 ready: every member ready, and composed together they pass
mesh/delivery closed: a newer head of the same pull request
mesh/check pass c193d2b38970 on 158348ede789 | unit 1319 run, 5401 kept, 47 skipped | integration 1359 run, 5490 kept, 49 skipped | over bound: unit, …
2026-10-28 21:56:06 +01:00
jschoubben 10447d8c1e mesh-config reads the placement beside the controller's assignments while both stand, as L2 has it and the vault does, and is listed as a reader of them (issue 774, ADR 0549 §5; re-review) 2026-10-28 21:54:36 +01:00
jschoubben 5be6576117 Follow L2 under L3: the vault and mesh-config read the seat mesh-placement's placement, and the check that only listed modules read the controller's assignments moves into noplacement, as L2 deleted the placement check (issue 774, ADR 0567, ADR 0549 §3, §6) 2026-10-28 21:54:36 +01:00
jschoubben 24fad2b92a mesh-vault: pin mesh-sdk 4e6e5cc, the L3 group's head rebased on L2 (issue 774) 2026-10-28 21:54:36 +01:00
jschoubben d3eb5cae80 mesh-vault: secret-ask answers the callers its manifest names, each named as the runtime proved it, within the same bounds; the controller's relay as before (issue 774, ADR 0563 §8) 2026-10-28 21:54:36 +01:00
jschoubben 2043200201 messenger: ask each machine's own engine whether it is root-free, the node seat node-engine's root-free, not the controller's (issue 774, ADR 0563 §6) 2026-10-28 21:54:36 +01:00
jschoubben ef618e8199 nats: the bus's certificate command moves here from mesh-controller with the same output, made once and checked, so the bus module makes its own certificate when a mesh is raised from nothing (issue 774, design 47 L3 s11) 2026-10-28 21:54:36 +01:00
jschoubben c00005c141 mesh-vault signs: its signing key and authority made once with their operator copies, the verb sign for a node's own certificate from bus state, the event signed, the state signers and the tools that read and reassert them (issue 774, ADR 0550 §2) 2026-10-28 21:54:36 +01:00
jschoubben bd5f170af0 The placement check's list of modules still reading the controller's assignments only shrinks against the trunk's copy (issue 774, ADR 0529 clause 2) 2026-10-28 21:54:36 +01:00
jschoubben 6bbb8329e4 mesh-config resolves for the modules the catalogue's placement puts on a node alone, and the placement check refuses any other module that reads the controller's assignments unless listed with why (issue 774, ADR 0549 §3, §6) 2026-10-28 21:54:36 +01:00
jschoubben e6b913cac7 mesh-vault: account_retire refuses while the controller holds the lease or before the wants are read whole, as the passes do (issue 774, review round 1) 2026-10-28 21:54:36 +01:00
jschoubben 411c1b8de8 mesh-vault: prove the vault whole on a real bus mints only from the lease write, and try a failed mint again at the pass; adopt the operator key before the watches open (issue 774, ADR 0561 §1-§4) 2026-10-28 21:54:36 +01:00
jschoubben 64e6688ec8 mesh-vault: tell where modules stand and which node holds the bus from the catalogue's placement first, so the vault does not depend on the controller's assignments for them (issue 774, ADR 0549 §3, §5) 2026-10-28 21:54:36 +01:00
jschoubben 2d57c9663a mesh-vault: serve issue-account, the hand path that mints one wanted account now, and record a rotation at most once per loaded list's revision; a covered account still never rotates in place (issue 774, ADR 0561 §4, §9) 2026-10-28 21:54:36 +01:00
jschoubben 5c1729523b mesh-vault: retire an account the grants part no longer wants only within ADR 0230's bounds, keeping its value, and re-enable it when wanted again; account_retire for a person past the bound (issue 774, ADR 0561 §2) 2026-10-28 21:54:36 +01:00
jschoubben 1628db6c80 mesh-vault: mint only while the controller's lease is handed away, read afresh before each mint, and say in vault_status what is held back and why (issue 774, ADR 0561 §3, §9) 2026-10-28 21:54:36 +01:00
jschoubben 0b9f19b612 mesh-vault: mint each account the grants part wants and the vault holds no value or adopted hash for, in the controller's shape, hashed before account-issued and secret-changed (issue 774, ADR 0561 §1, §2, §4) 2026-10-28 21:54:36 +01:00
mesh-tools 39ce149f74 Merge pull request 'mesh-build-go's compose integration test gives its module one slot (issue 1126)' (#553) from fix/1126-capacity-transition into main 2026-10-28 20:24:49 +00:00
jschoubben f37f93e812 Drop the unreachable 'before its check ends' caveat (issue 1733)
mesh/delivery merged: source.main moved to novox/mesh-catalog@158348ede789
mesh/delivery-group group fix/1733-no-hour merged: every member is merged
mesh/check pass f37f93e812df on ff4c6c04358a | carried from mesh-delivery 1f6f4eda3208 | unit 470 run, 4760 kept, 13 skipped | integration 503 run, 48…
2026-10-28 21:00:09 +01:00
jschoubben cf4879eaa1 Ask the Approve when the check's verdict arrives, not after an hour (issue 1733) 2026-10-28 20:59:46 +01:00
jschoubben 1e377fe862 Go build module: the compose test gives its module a slot, since a node with no capacity set now pulls nothing (issue 1126, ADR 0567 §15)
mesh/delivery merged: source.main moved to novox/mesh-catalog@39ce149f7435
mesh/check checking 1e377fe8628c on 39ce149f7435
The test built its module with no settings file and no Capacity; the new
buildstep.NodeCapacity answers 0 for that (no default names a node), so the
puller took no ask and the test timed out after 90 s in check 205f634c30df.
Failed first locally at origin/main af89893d (90.1 s), passes with the slot (2.5 s).
2026-10-28 20:23:42 +01:00
jschoubben 5c6899e641 Build modules: pin mesh-sdk 9e5666b (fix/park-wakes)
mesh/delivery merged: source.main moved to novox/mesh-catalog@ff4c6c04358a
mesh/delivery-group group fix/park-wakes merged: every member is merged
mesh/check pass 5c6899e64166 on 39ce149f7435 | judged by the candidate's mesh-build-go at 01347a5ab27d
2026-10-28 20:08:55 +01:00
jschoubben 848c0cbd03 Build modules: give the puller the settings file and say a capacity of 0 in health (needs the mesh-sdk fix/park-wakes pin) 2026-10-28 20:02:30 +01:00
mesh-tools af89893d42 Merge pull request 'No repository names a node: placement files, particulars and per-node build defaults go (issue 1126, delivery 2 of 2)' (#552) from feat/1126-placement-seat into main 2026-10-28 18:33:00 +00:00
jschoubben 105c4d628c Particulars check: skip the real-catalogue test when the checker hands no node names yet (ADR 0567 §16)
mesh/delivery merged: source.main moved to novox/mesh-catalog@af89893d4202
mesh/delivery-group group feat/1126-placement-seat merged: every member is merged
mesh/check failed: integration: meshbuildgo/cmd/mesh-build-go… | 105c4d628cbc on d836a31aa721 | judged by the candidate's mesh-build-go at 565c52f06c02
2026-10-28 18:53:44 +01:00
jschoubben 59cad7154a Pin mesh-sdk to the group's head 38dbe1d in the build modules, mesh-config, the gate and the vault; the vault's test answers by proposal (issue 1126) 2026-10-28 18:52:35 +01:00
jschoubben 3b35e78733 build-hours takes one window and names no node, node names enter digests and checks by their clauses, and a leftover particulars file is refused 2026-10-28 18:35:06 +01:00
jschoubben e072abb35e Go build module: a package whose tests read MESH_NODE_NAMES has the node list in its digest, so a new node misses a kept verdict 2026-10-28 18:16:34 +01:00
jschoubben e389e770d9 Build modules: capacity and build hours are this node's own values, no default names a node; the Go test tool hands the check's node names to the tests 2026-10-28 18:15:33 +01:00
jschoubben 00613b438c gitea commit trees: drop the listed directories, which only the bridge's placement files used 2026-10-28 18:12:13 +01:00
jschoubben d1f42564e3 Node names come from the run, not a file of the repository: the particulars check reads MESH_NODE_NAMES and fails without it 2026-10-28 18:11:23 +01:00
jschoubben 5548e1030c mesh-config reads placement from the seat's state (held by the assigner), not the indexer's 2026-10-28 18:10:58 +01:00
jschoubben 7bb07ce349 New check: no placement directory and no manifest reading the indexer's placement, so the repository path cannot come back 2026-10-28 17:58:53 +01:00
jschoubben 4e3b140d43 Delivery gate: a placement change is no pull request path, so the ask drops its placement wording and case 2026-10-28 17:58:22 +01:00
jschoubben b5aa226649 Build capacity is an integer per node layer, default 0: no manifest names a node's share (manifest side; the reader follows mesh-sdk S6) 2026-10-28 17:57:57 +01:00
jschoubben 77b511a79a Delete the placement check: placement is bus state, no repository file holds it 2026-10-28 17:57:15 +01:00
mesh-tools d836a31aa7 Merge pull request 'power: its loop and boot tests run on a fake clock, so a busy build node cannot fail them (issue 1126)' (#551) from fix/power-loop-flake into main 2026-10-28 16:26:05 +00:00
mesh-tools 683ce7e37f Merge pull request 'dnsmasq starts at boot: its drop-in no longer orders after a target the package orders it before (issue 1126)' (#550) from fix/dnsmasq-cycle into main 2026-10-28 16:26:01 +00:00
jschoubben 3494548c88 power: the loop and run tests drive their own clock, so a loaded build node cannot starve their timing (flake in #549's check)
mesh/delivery merged: source.main moved to novox/mesh-catalog@d836a31aa721
mesh/check checking 3494548c8830 on d836a31aa721
2026-10-28 17:12:11 +01:00
mesh-tools 3feeefc70f Merge pull request 'gitea: set any field of a branch's protection or a repository's settings, saying whether each trunk stays protected (issue 1126, ADR 0566)' (#549) from feat/protect-as-trunk into main 2026-10-28 16:02:32 +00:00
jschoubben 3f552a17f6 dnsmasq: the drop-in no longer orders after network-online.target and its address wait is 2 s at most (cycle dropped the start at boot, issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-catalog@683ce7e37f29
mesh/check being carried to 3f552a17f697 on 683ce7e37f29 | from 3feeefc70f7c
2026-10-28 17:00:14 +01:00
jschoubben 16ea3824b5 gitea: gate and judge every trunk the gate's source names, not main alone
mesh/delivery merged: source.main moved to novox/mesh-catalog@3feeefc70f7c
mesh/check checking 16ea3824b5ea on 3feeefc70f7c
The operator, 2026-10-28: the gates and trunk_rule apply to every trunk,
the branches the mesh installs from, and later there may be more than
main. The trunks are read at the call from mesh-delivery-gate's source,
in one function (trunksOf), and fall back to main, said, only when it
names none. A force-push onto any trunk is refused, and trunk_rule says
whether the branch asked is a trunk.
2026-10-28 16:52:50 +01:00
jschoubben fabb62da00 gitea: refuse a force-push onto a rule covering main until the module can ask the operator's Approve for it
The operator, 2026-10-28: a call waits for their Approve only if it removes
branch protection or allows a force-push to main. No TypeScript tool can
ask yet (no asker in the TypeScript SDK; ADR 0286's mark is not built in
the runner), so such a call is refused with nothing sent, and every other
call runs at once. No tool removes a rule.
2026-10-28 16:48:55 +01:00
jschoubben 8068cf74a7 gitea: read a repository's settings back in the forge's own form, and take the settings it was missing
enable_prune is never in the forge's answer and mirror_interval comes back
as a Go duration, so a write that worked failed its read-back. The first is
now said as not read back, the second compared as a duration.
allow_merge_update, default_update_style and has_code are taken,
projects_mode needs has_projects true, and the description names what is
left out: the name, the tracker and wiki objects, and the mirror's
credentials.
2026-10-28 16:44:11 +01:00
jschoubben 6e97ba3867 gitea: fail a protection or settings write the forge answered and did not apply, and take every field gitea 1.27.3 edits
Gitea reads the push whitelist and force-push allowlist flags only beside
their parent set true and passes over a missing default branch, both with
a 200, so the tool said set for what did not change. It now refuses a
dependent field without its parent, reads the rule back and names each
field not applied, and says what the forge changed beside the call. The
bypass allowlist fields complete the 32. gitea_repo_settings_set_full sets
a repository's settings, its default branch among them, as the forge's
administrator: gitea_default_branch_set is refused as mesh-tools.
2026-10-28 16:39:54 +01:00
jschoubben 448310908c gitea: set any field of a branch protection rule through a tool, so a trunk's merge whitelist and override need no hand at the forge
gitea_branch_protection_set_full takes every field of gitea's branch
protection API by its own name, changes only those given, writes as the
forge's administrator, logs the required why, answers the rule before and
after, and says whether the controller's trunk rule (judgedRule in
mesh-control build_source.go) now holds. The operator, 2026-10-28: a
generic tool for any configuration; design 47 L2 needs de-spiegel's main
set as a trunk.
2026-10-28 16:25:16 +01:00
mesh-tools 58309a9729 Merge pull request 'power: every reboot, poweroff and halt is bounded at 10 minutes (group fix/shutdown-bound, issue 1126)' (#547) from fix/shutdown-bound into main 2026-10-28 15:05:06 +00:00
jschoubben 6724dab0cf power: bound every shutdown at ten minutes, not systemd's thirty (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-catalog@58309a972945
mesh/delivery-group group fix/shutdown-bound merged: every member is merged
mesh/check pass 6724dab0cf96 on 387ee59140c4 | unit 2192 run, 3794 kept, 57 skipped | integration 2208 run, 3829 kept, 57 skipped | over bound: unit, …
On ace's reboot of 2026-10-28 every unit had stopped within 17 s, yet the bus and
polkit were started again during the shutdown, nothing stopped them a second
time, and the reboot waited on reboot.target's 30-minute job timeout, silent,
until the operator forced it. node-power owns the shutdown on every machine, so
its drop-ins on reboot, poweroff and halt shorten the bound; each target keeps
its own forcing action, so the last phase still ends every process, unmounts
and syncs. The cause, node-tools starting bundles again while it stops, is
fixed in mesh-tools.
2026-10-28 15:30:54 +01:00
mesh-tools 387ee59140 Merge pull request 'mesh-build-image: a node that cannot build here now stays healthy and says not eligible, so it never holds the delivery (issue 1126)' (#546) from fix/1126-ineligible-healthy into main 2026-10-28 14:20:40 +00:00
mesh-tools 85bbeb37e3 Merge pull request 'mesh-vault: own answers a node within its bound — assignments from a watched mirror, one read per module, one synced record per answer (issue 1126)' (#545) from fix/1126-vault-own into main 2026-10-28 14:20:35 +00:00
jschoubben 363039f337 mesh-build-image README: a node without tun is healthy and not eligible, as the module now says (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-catalog@387ee59140c4
mesh/check checking 363039f337ad on 387ee59140c4
2026-10-28 15:01:42 +01:00
jschoubben a9d96b30be mesh-vault: a watch of the assignments begun again keeps nothing an earlier attempt took, and a module the mirror lacks is read directly before a credential is refused (issue 1126, review round 1)
mesh/delivery merged: source.main moved to novox/mesh-catalog@85bbeb37e3e0
mesh/delivery-group group fix/1126-vault-own merged: every member is merged
mesh/check pass a9d96b30be8d on 69056948bfa8 | unit 1639 run, 4201 kept, 46 skipped | integration 1674 run, 4215 kept, 47 skipped | over bound: unit, …
A watch that failed partway kept what it had taken; begun again, the runtime does not hand a key deleted meanwhile, so
a removed assignment read as standing and credentialRefusal could hand a consumer credential to a node no longer
running its module. Each attempt now begins from nothing held, and a late delivery to an older attempt changes nothing.
A 'not on the node' refusal is confirmed by a direct read first, since a watched mirror can be behind.
2026-10-28 14:55:36 +01:00
jschoubben 644a34cf49 mesh-build-image stays healthy on a node that cannot build here now (no tun) and says it is not eligible: the assigner read unhealthy as a failed delivery and held the build (issue 1126) 2026-10-28 14:50:28 +01:00
jschoubben 277a4d0060 mesh-vault answers own in one pass: assignments from its watch, each module's once, one synced record per answer (issue 1126)
Seed-9's probe (13:31-13:36Z): novox's own of 57 secrets and 146 credentials took 26 s, past the node-engine's 15 s,
and ace and novox refused their whole shadow answer. own read the controller's assignment over the bus once per secret
and per credential (203 reads), read every record of every node, and synced the hand-out record once per value, under
the one lock every node's answers share.

Now the vault watches the assignments mirror (a watch, never a poll) and reads it from memory once caught up, over the
bus until then; one answer reads each module's assignment once; only own/<node>/ records are read; and every hand-out
of one answer is recorded in one synced write, none handed when it cannot be.

Quick fix weighed: a longer bound in the node-engine (VaultWithin). Thorough fix chosen: the cost grows with every
module and every node, and the lock is shared by every shadow answer and every apply.
2026-10-28 14:47:20 +01:00
mesh-tools 69056948bf Merge pull request 'The kernel module loads tun at boot and keeps the running kernel's modules across an upgrade; mesh-build-image takes no ask where it cannot build (group fix/ace-tun)' (#543) from fix/ace-tun into main 2026-10-28 13:07:42 +00:00
mesh-tools 9cdac09ad9 Merge pull request 'knowledge-base.progress takes since: what changed in the last hour; issue-tracker.changes says each issue's moves from what to what' (#541) from feat/progress-since into main 2026-10-28 11:56:32 +00:00
jschoubben 86d37a14e7 mesh-build-image: pin mesh-sdk 1618837, the group's head (fix/ace-tun)
mesh/delivery merged: source.main moved to novox/mesh-catalog@69056948bfa8
mesh/delivery-group group fix/ace-tun merged: every member is merged
mesh/check pass 86d37a14e7ab on 9cdac09ad962 | carried from 25f615456423 | unit 2293 run, 3984 kept, 16 skipped | integration 2331 run, 4031 kept, 24 …
2026-10-28 12:50:47 +01:00
jschoubben d13f74df40 A node that cannot open /dev/net/tun takes no image build, and every node loads tun at boot and keeps its running kernel's modules
An image build node up 24 days had lost its running kernel's modules to a linux-lts upgrade, so tun
could not load and every RUN line failed in pasta. mesh-build-image now proves the tun device and,
whatever its look refuses, judges its node not eligible (capacity 0 with the reason), so the asks go
to a node that can build them. The kernel module loads tun at boot and declares kernel-modules-hook,
which keeps the running kernel's modules across an upgrade. Needs mesh-sdk fix/ace-tun (Puller.Eligible).
2026-10-28 12:44:58 +01:00
jschoubben dfe490d9e2 issue-tracker.changes reads each issue's moves over a window from the tracker's own log, so progress since says from what each issue moved, not only where it is
mesh/delivery merged: source.main moved to novox/mesh-catalog@9cdac09ad962
mesh/check being carried to dfe490d9e245 on 9cdac09ad962 | from 25f615456423
2026-10-28 12:33:01 +01:00
jschoubben 4f317201c9 knowledge-base.progress takes since: only what changed in a window, read from the tracker's and mesh-delivery's history, so a five-minute report says what moved (the operator, 2026-10-28) 2026-10-28 12:33:01 +01:00
mesh-tools 25f6154564 Merge pull request 'Retire the spelling catalogue in the words check, and refuse a compiled program anywhere in the repository (group words/catalog)' (#540) from words/catalog into main 2026-10-28 11:31:37 +00:00
jschoubben b8ba9dd966 Widen the no-binaries check to every tracked file: a program under checks/ is refused too
mesh/delivery merged: source.main moved to novox/mesh-catalog@25f615456423
mesh/delivery-group group words/catalog merged: every member is merged
mesh/check being carried to b8ba9dd9669c on 25f615456423 | from 774fcdfa93ad, hq 8f8393fa9343 | novox/mesh-catalog#540 in group words/catalog
2026-10-28 12:17:21 +01:00
jschoubben 002a0e1a12 Keep the claude-code fixture equal to the renderer's catalog wording 2026-10-28 12:17:21 +01:00
jschoubben 9bb85a37f2 Retire the spelling catalogue: the words check lists it and skips module paths in imports 2026-10-28 12:17:21 +01:00
mesh-tools 774fcdfa93 Merge pull request 'gitea announces from the forge's events, not a poll: an edited base and a close are announced at once, one pass at start (issue 1422)' (#539) from fix/1422-gitea-events into main 2026-10-28 11:16:46 +00:00
jschoubben 470687ea4c Gitea announces from the forge's events and one pass at the start, not by polling (issue 1422)
mesh/delivery merged: source.main moved to novox/mesh-catalog@774fcdfa93ad
mesh/check being carried to 470687ea4cce on 774fcdfa93ad | from 2a03c106f4b9
An edited pull request is announced into its new base at once; a close without a merge is announced by its event; the 30 s/5 min polls and the 10 min sweep are gone.
2026-10-28 12:05:03 +01:00
mesh-tools 2a03c106f4 Merge pull request 'Gate seeds a never-merged repository's trunk head and follows a retargeted base; gitea announces a retarget and sets a default branch and a pull request's base (issue 1422; group fix/1422-trunk-unknown)' (#537) from fix/1422-trunk-unknown into main 2026-10-28 10:34:46 +00:00
jschoubben f5d4b2f86e Gate: take a retargeted pull request's new base, so a retarget to main is seeded and asked and one away from main is not (issue 1422)
mesh/delivery merged: source.main moved to novox/mesh-catalog@2a03c106f4b9
mesh/delivery-group group fix/1422-trunk-unknown merged: every member is merged
mesh/check being carried to f5d4b2f86ed4 on 2a03c106f4b9 | from 9fd95693db54, mesh-delivery ae7ec702a5e9
A re-announcement updated the base head but never the base: a pull request retargeted from nox-mesh to main stayed 'into
nox-mesh' and was never asked or seeded, and one retargeted away from main kept its open ask.
2026-10-28 11:12:21 +01:00
jschoubben 7c2aa0a739 gitea: set a default branch and a pull request's base by named tools; announce a retargeted pull request again (issue 1422) 2026-10-28 11:07:31 +01:00
jschoubben e057670bc4 Gate: seed source.main from the latest announcement's base head, not the newest delivery's (issue 1422)
A delivery made earlier can be announced again after a newer one, with main's head moved on: its base_sha is the freshest.
The gate now keeps when each base_sha was announced and seeds from the latest.
2026-10-28 11:05:38 +01:00
jschoubben 0c6b7b5d64 Gate: seed source.main with the trunk head of a repository it names none of (issue 1422)
source.main moved only at a merge, so a repository never merged through the gate had no trunk head and the checker could
never judge its pull requests. The gate already hears main's head on each gitea.pull.updated (base_sha); it now adds it for
a repository source.main lacks, from the newest open pull request into main, in one compare-and-set write that never
replaces a head. A pull request into another base seeds nothing (ADR 0484 §11).
2026-10-28 11:03:40 +01:00
mesh-tools 9fd95693db Merge pull request 'mesh-vault takes what runs of a node's own secret through move-in, on the node's ask (issue 1126 C4, group fix/1126-seed-same)' (#535) from fix/1126-seed-same into main 2026-10-28 09:46:28 +00:00
mesh-tools 1a9570f5e9 Merge pull request 'messenger: say what waits in its burst window, and stop reporting a state failure that has passed (issue 1507)' (#536) from fix/1507-config-ask-silent into main 2026-10-28 09:17:59 +00:00
jschoubben d871258f8b Hold that the state error clears once a write succeeds again (issue 1507, review)
mesh/delivery merged: source.main moved to novox/mesh-catalog@1a9570f5e92f
mesh/check being carried to d871258f8b00 on 1a9570f5e92f | from 3fb7951524e6
2026-10-28 10:08:13 +01:00
jschoubben e7abe3e3e4 messenger: clear the state problem once a write succeeds again, so the status stops saying a failure that passed (issue 1507) 2026-10-28 10:05:09 +01:00
jschoubben 0974d1a57d mesh-vault's own_drift row carries the id as a node runs it, its module's name first, and the vault pins mesh-sdk 3028c7e (issue 1126, cause C4; review round 2)
mesh/delivery merged: source.main moved to novox/mesh-catalog@9fd95693db54
mesh/delivery-group group fix/1126-seed-same merged: every member is merged
mesh/check being carried to 0974d1a57d99 on 2a03c106f4b9 | from 9fd95693db54
2026-10-28 10:02:46 +01:00
jschoubben 6cea12a023 messenger: say what waits in the outbox in the history, so an ask held for its burst window does not read as lost (issue 1507) 2026-10-28 10:02:27 +01:00
jschoubben a17356a162 Fill go.sum for mesh-sdk 1062b33, the group's head (issue 1126) 2026-10-28 09:53:34 +01:00
jschoubben 35ad58e6e2 mesh-vault pins 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 da5da4f6f7 The vault takes what runs through move-in's replace_running, not a verb of its own, so no seat promises anything new (issue 1126, cause C4) 2026-10-28 09:48:14 +01:00
jschoubben 671a6a1f47 own-take-running has a person's switch, own_take_switch, kept in the vault's store (issue 1126, cause C4) 2026-10-28 09:39:16 +01:00
jschoubben 47ed384d97 The vault takes the value that runs on a machine over the one it was first handed, from that machine alone (issue 1126, cause C4)
move-in never replaces a value the vault holds, so where the controller made an own secret anew after the move, the
new value runs while the vault keeps the first. The verb own-take-running takes it from the node-engine of the machine
it is for, keeps the replaced value as the previous generation, raises the generation and says own-replaced without a
value; own_drift lists every own secret a node's shadow answer says another value of, mesh-wide. The module needs the
mesh-sdk head of fix/1126-seed-same-own-resync (delivery.OwnValuesDiffer, the seat's verb).
2026-10-28 09:37:54 +01:00
mesh-tools 3fb7951524 Merge pull request 'claude-code: render a 660 s timeout for the mesh console, so mesh_wait is not cut at a minute (issue 1871)' (#533) from fix/1871-wait-returns-early into main 2026-10-28 07:46:26 +00:00
jschoubben ae96afcaa8 Render a 660 s timeout for the mesh console and the MCP idle bound, so mesh_wait is not cut at a minute (issue 1871)
mesh/delivery merged: source.main moved to novox/mesh-catalog@3fb7951524e6
mesh/check being carried to ae96afcaa8da on 3fb7951524e6 | from 0c63b4c906c6
2026-10-28 03:52:32 +01:00
mesh-tools 0c63b4c906 Merge pull request 'mesh-delivery-gate: ask the operator's Approve only once the check has a verdict (issue 1733) — wait for its check before approving this one' (#530) from fix/ask-after-check into main 2026-10-27 23:25:26 +00:00
jschoubben 354d62340d mesh-delivery-gate: the group re-push row holds no verdict at the new heads, so only the missing-run rule keeps it unasked (issue 1733, review round 2)
mesh/delivery merged: source.main moved to novox/mesh-catalog@0c63b4c906c6
mesh/check being carried to 354d62340d4d on 0c63b4c906c6 | from 00d155da60f5
2026-10-28 00:07:39 +01:00
jschoubben dea390ac97 mesh-delivery-gate: the Approve's verdict bound counts from the latest head, restarts with its check, and speaks the operator's time (issue 1733, review round 1)
A group member made ready in an event handler has no ready run when its sibling
is looked at; the bound counted from the older sibling alone, so a re-push past
60 minutes asked at once as unanswered. Such a member now counts as ready now.

The bound also counts from a check starting again at the same heads, or its
verdict being taken away, so a pass that turns back into running never asks at
once as unanswered. Times in the wait and the ask are the operator's zone
(Europe/Brussels, as the messenger says them), and a placement change whose pass
holds no composition says that no node answered its placement.
2026-10-28 00:05:36 +01:00
mesh-tools 00d155da60 Merge pull request 'Take the placement files out again: every node composing its own fails (issue 1126, revert of #528) — approve now' (#529) from revert/1126-seed-4 into main 2026-10-27 22:59:36 +00:00
jschoubben 89c30c44be mesh-delivery-gate: while the check gates no merge, ask the Approve only once the check has a verdict (issue 1733)
The operator, 2026-10-27 23:54: "if you don't want me to approve it, don't send
the approval request". The gate asked the Approve the moment a pull request
opened, before its check answered, and five changes were approved unchecked.

Now the ask waits for the checker's verdict at the heads it covers (a group's
verdict of all its heads together; a placement change's verdict ends only once
its nodes answered, ADR 0559 §5). A pass or a failure is asked, the failure in
its words, since check-gates-merge false lets the operator merge it anyway. A
check with no verdict 60 minutes after the heads became ready is asked anyway,
saying so, and asked again in new words when its verdict comes.

Quick fix (only the ask's wording) against thorough (the one place that decides
when to ask, ungatedWaits): thorough, which is one place here.
2026-10-27 23:59:09 +01:00
jschoubben bd65c839a8 Take the placement files out again: every node composing its own fails (novox: nats' ${dir:mesh-state} not declared; ace, g14, shanks: the build index unread at a bus pause), so each returns to the controller's compositions (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-catalog@00d155da60f5
mesh/check checking bd65c839a881 on 00d155da60f5
2026-10-27 23:56:08 +01:00
mesh-tools b7a036e186 Merge pull request 'Seed the placement files a third time (issue 1126) — Approve ONLY after the lead says all four nodes answer 'same'' (#528) from feat/1126-seed-3 into main 2026-10-27 22:53:41 +00:00
jschoubben 3abf7cd50b Seed the placement files a third time, now that every node agent reads its placement only on a successful read and its shadow names every difference (issue 1126, ADR 0549 §4)
mesh/delivery merged: source.main moved to novox/mesh-catalog@b7a036e18693
mesh/check checking 3abf7cd50bdf on b7a036e18693
2026-10-27 23:52:35 +01:00
mesh-tools d7e8a8824e Merge pull request 'Take the placement files out again: since the seed no delivery reaches any node (issue 1126, revert of #525)' (#527) from revert/1126-seed-3 into main 2026-10-27 22:48:46 +00:00
jschoubben 83e9ce5b19 Take the placement files out again: since the seed no delivery reaches any node, because every node agent runs the controller's compositions while the wants ask them to compose their own (issue 1126, revert of #525)
mesh/delivery merged: source.main moved to novox/mesh-catalog@d7e8a8824e9c
mesh/check checking 83e9ce5b1957 on d7e8a8824e9c
2026-10-27 23:47:52 +01:00
mesh-tools 653cead338 Merge pull request 'Seed the placement files again from what each node runs now (issue 1126, ADR 0549 §4) — Approve only after every node answers 'same'' (#525) from feat/1126-seed-2 into main 2026-10-27 22:02:13 +00:00
jschoubben d1ca199265 Seed the placement files again from what each node runs now, so placement lives in the catalogue (issue 1126, ADR 0549 §4)
mesh/delivery merged: source.main moved to novox/mesh-catalog@653cead33815
mesh/check checking d1ca19926529 on 653cead33815
2026-10-27 22:04:46 +01:00
mesh-tools bf94af0a82 Merge pull request 'gitea: gitea_team_rights reads a forge team's members and units and a user's effective rights, read-only (issue 1563)' (#524) from fix/1563-team-rights into main 2026-10-27 21:00:57 +00:00
mesh-tools 476a59162f Merge pull request 'mesh-build-go, mesh-build-node: a missing bubblewrap fails the sandbox proofs on a build node instead of skipping them (issue 1309)' (#523) from fix/1309-bwrap-fatal into main 2026-10-27 21:00:53 +00:00
jschoubben 36d5993323 gitea_team_rights: refuse a forge user that does not exist; keep ForgeRepository's doc comment on it (issue 1563)
mesh/delivery merged: source.main moved to novox/mesh-catalog@bf94af0a820f
mesh/check checking 36d59933239e on bf94af0a820f
2026-10-27 21:54:51 +01:00
jschoubben f8ddcc98f6 Add gitea_team_rights: read a forge team's members, units and a user's effective rights, with the administrator's login, GET only (issue 1563) 2026-10-27 21:50:59 +01:00
jschoubben 9109524502 A missing bubblewrap fails the sandbox proofs on a build node instead of skipping them
mesh/delivery merged: source.main moved to novox/mesh-catalog@476a59162f55
mesh/check checking 9109524502f9 on bf94af0a820f
The levels a build module judges hand the tests MESH_BUILD_NODE=1; the tests that run a step in bubblewrap fail on it when bwrap is missing. Issue 1309
2026-10-27 21:36:28 +01:00
mesh-tools 9bd8247915 Merge pull request 'mesh-issues: cover a merged fix from the delivery view's reached head, not only its once-said event (issue 1950)' (#522) from fix/1950-delivered-on-machines into main 2026-10-27 20:32:21 +00:00
jschoubben 116751130f Cover a merged fix from the delivery view's reached head in the sweep, not only from its once-said event, so a fix named after the saying is delivered (issue 1950)
mesh/delivery merged: source.main moved to novox/mesh-catalog@9bd824791509
mesh/check being carried to 116751130f5c on 9bd824791509 | from c05aa5e6eafd
2026-10-27 21:17:53 +01:00
mesh-tools c05aa5e6ea Merge pull request 'Take the placement files out again, so every node returns to the controller's compositions (recovery, issue 1126)' (#521) from revert/1126-seed into main 2026-10-27 14:06:34 +00:00
jschoubben b95b0a4750 Take the placement files out again: the node agents cannot watch their haves with '>' (refused by their grant), so no node can compose its own; without its placement key each node returns to the controller's compositions (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-catalog@c05aa5e6eafd
mesh/check checking b95b0a475044 on c05aa5e6eafd
2026-10-27 09:23:26 +01:00
mesh-tools 04fa2e3286 Merge pull request 'Seed the placement files from what each node runs today, so placement lives in the catalogue (issue 1126, ADR 0549 §4)' (#520) from feat/1126-seed into main 2026-10-27 08:20:33 +00:00
jschoubben 0c61bad102 Seed the placement files from what each node runs today, so placement lives in the catalogue (issue 1126, ADR 0549 §4)
mesh/delivery merged: source.main moved to novox/mesh-catalog@04fa2e3286df
mesh/check checking 0c61bad10238 on 04fa2e3286df
2026-10-27 08:34:10 +01:00
mesh-tools 02597b66d7 Merge pull request 'The issue tracker learns the merge of a fix it never heard, so an old resolved issue no longer reads 'nothing heard from the delivery' (issue 1950)' (#517) from fix/1950-old-fixes into main 2026-10-27 07:21:13 +00:00
mesh-tools 457c60972c Merge pull request 'The placement check and the delivery gate read the group's mesh-sdk, so replicated seats pass and placement pull messages are read (issue 1126)' (#519) from feat/1126-l2b into main 2026-10-27 07:21:09 +00:00
mesh-tools 5ae337b14d Merge pull request 'Every build module clears at start what a previous process left running, held by the catalogue's check (issue 1890)' (#518) from fix/1890-step-owner into main 2026-10-27 07:19:39 +00:00
jschoubben 2ac511ce6b Pin checks/placement and mesh-delivery-gate to mesh-sdk cadd7b1, the group's final head (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-catalog@457c60972c59
mesh/delivery-group group feat/1126-l2b merged: every member is merged
mesh/check checking 2ac511ce6b19 on 457c60972c59 | novox/mesh-catalog#519 in group feat/1126-l2b
2026-10-27 02:44:12 +01:00
jschoubben e955b53bf0 Pin mesh-delivery-gate to mesh-sdk 062c29c, so it reads a catalogue pull message that carries placement files instead of refusing it (issue 1126; review round 1) 2026-10-27 02:43:13 +01:00
jschoubben 454ac0fec7 Pin checks/placement to mesh-sdk 062c29c, the L2b group's head, so the catalogue's placement check judges with the replicated-seat rule the seed needs (issue 1126) 2026-10-27 02:30:16 +01:00
jschoubben 3c6f2dc274 mesh-issues: a learned merge is kept as of now, closed untimed issues get no fix time, the forge read is declared, the sweep rotates its asks (issue 1950)
mesh/delivery merged: source.main moved to novox/mesh-catalog@02597b66d7de
mesh/check checking 3c6f2dc274a9 on 457c60972c59
2026-10-27 01:30:37 +01:00
jochen 39419b1892 Pin the build modules to mesh-sdk db1007ff, which deletes a gone process's ask key so its redelivery still answers (issue 1890)
mesh/delivery merged: source.main moved to novox/mesh-catalog@5ae337b14def
mesh/delivery-group group fix/1890-step-owner merged: every member is merged
mesh/check checking 39419b1892c9 on 5ae337b14def
2026-10-27 01:30:31 +01:00
jschoubben b51f03c4ff mesh-issues: a fix whose pull request has no record learns its merge from the delivery or the forge, so a delivered event can cover it (issue 1950) 2026-10-27 01:24:57 +01:00
jochen 3d42988afa Hold every build module to ending what a previous process left running (issue 1890)
A replaced mesh-build-go left a check ask's step running and its start
sweep spared it (issue 1890, note 21). mesh-build-go, mesh-build-node and
mesh-build-image pin mesh-sdk 981a35b5, whose steps name their owner and
whose sweep ends another owner's at once; each runs the sweep through one
method that main calls and that its test runs through the conformance
kit's left-running case. The failurekit check now refuses a build module
whose tests do not run that case, test step or not.
2026-10-27 01:22:13 +01:00
mesh-tools 64530a8dcb Merge pull request 'Every build module runs the failure contract's conformance kit, and the catalogue refuses one that does not (issue 1901)' (#516) from fix/1901-failure-contract into main 2026-10-26 23:44:16 +00:00
jschoubben e8a953818c Merge main into fix/1901-failure-contract: pin mesh-sdk a2759ec (the group's sdk head), the failure kit check's marker lists what it reads (issue 1936), and the registry test's password can no longer match a random workspace name (issue 1901)
mesh/delivery merged: source.main moved to novox/mesh-catalog@64530a8dcb08
mesh/delivery-group group fix/1901-failure-contract merged: every member is merged
mesh/check checking e8a953818c71 on 64530a8dcb08 | novox/mesh-catalog#516 in group fix/1901-failure-contract
2026-10-27 00:39:35 +01:00
mesh-tools f0f7b6737e Merge pull request 'Delivery records are true at the source: the gate keeps no state after the merge, and the issue tracker reads delivered from the view (issue 1280)' (#515) from fix/delivery-records-true into main 2026-10-26 21:48:46 +00:00
jschoubben c1cd8461bc Merge main (#514, issue 1733) into the delivery-records group
mesh/delivery merged: source.main moved to novox/mesh-catalog@f0f7b6737e11
mesh/delivery-group group fix/delivery-records-true merged: every member is merged
mesh/check checking c1cd8461bc6e on 53bf4bb84cb1 | novox/mesh-catalog#515 in group fix/delivery-records-true
2026-10-26 22:29:36 +01:00
mesh-tools 53bf4bb84c Merge pull request 'The merge Approve says the check's real state, and is asked again when it passes or fails (issue 1733)' (#514) from fix/1733-ask-says-the-check into main 2026-10-26 21:25:37 +00:00
jschoubben 4677b8a2f5 mesh-delivery-gate: pin mesh-sdk 68d65e6, the group's sdk head merged with sdk main (issue 1280) 2026-10-26 22:21:21 +01:00
jschoubben cc6e478c4f Merge remote-tracking branch 'origin/main' into fix/delivery-records-true 2026-10-26 22:21:12 +01:00
mesh-tools c17ab34a28 Merge pull request 'A reads-outside marker lists what its tests read, so its package's check is reused until one of those files changes (issue 1936)' (#513) from fix/1936-declared-reads into main 2026-10-26 21:18:14 +00:00
jschoubben 099605ec70 mesh-delivery-gate: the Approve's ask says the check's actual state at its heads, never a pass it has not had (issue 1733)
mesh/delivery merged: source.main moved to novox/mesh-catalog@53bf4bb84cb1
mesh/check checking 099605ec70a2 on 53bf4bb84cb1
With check-gates-merge false a head is ready and asked before any verdict exists, yet the ask said "<pr> passed its
checks" (a group: "passed its checks"): on 2026-10-26 the operator approved mesh-delivery #79 and a six-repository group
on that claim while their checks had not answered.

The ask now reads the checker's verdict at exactly the heads it covers (checkAt, beside checkerAt): "passed its check"
only for a pass there; otherwise "its check has no verdict yet[: it is running (since HH:MM UTC)]" or "its check failed:
<first failure>", and the Approve clause says it merges "now, before its check ends" or "although its check failed".
While the check gates no merge, a pending ask whose check then passes or fails (or loses a pass it named) is withdrawn
and asked again in new words (the ask's AskCheck against the state now, endStaleAsks, the review-record path's
withdrawAsk); a check that starts running is no new word, so the wait stays one message. Whether a merge waits is
unchanged. The gate's log lines and stalled lines that claimed a pass for a ready head say the same state.

Quick fix: replace "passed its checks" by neutral words in the two summaries. Thorough fix: one reader of the check's
state at the ask's heads (checkAt) used by every text that names it, and the ask re-said when that state changes.
Chosen: thorough, as it costs one helper and one case in endStaleAsks, and the quick one would still leave a pending
ask silent about a failure that came after it.

Tests: rows in TestAnApprovedPullRequestIsMergedThroughTheMergeTool (gated pass; ungated no verdict, running, then
passed, then failed) and the ungated group's wording; no new test function.
2026-10-26 22:18:08 +01:00
jschoubben 488b7bfc58 checks/health: its marker names health-undeclared for what it counts (issue 1936)
mesh/delivery merged: source.main moved to novox/mesh-catalog@c17ab34a28cc
mesh/check checking 488b7bfc5816 on c17ab34a28cc
2026-10-26 22:13:14 +01:00
jschoubben d58249d0d8 checks/health: its marker lists the predefined keys, the modules' templates and health-undeclared it reads (issue 1936)
ReadTreeManifest reads predefined.json and each templated module's templates (WithTemplatesFrom), and the test reads
the root's health-undeclared through Kept: none was listed, so a change to them reused a stale verdict. No other
marker's package reads manifests through ReadTreeManifest beyond what it lists. Review finding F1 on ba49a7f0.
2026-10-26 22:13:01 +01:00
jschoubben d224b74ff1 mesh-build-go: a marked package's digest hashes every file of its own directory, Go files its build tags exclude too (issue 1936)
go list leaves a file its build tags exclude (an integration test) in IgnoredGoFiles, which the digest did not hash;
mesh-provisioner-client's unit test globs *_test.go and reads two of them, so a change there reused a stale unit pass.
Review finding F2 on ba49a7f0.
2026-10-26 22:13:01 +01:00
jschoubben 234d2d5534 mesh-delivery-gate: pin mesh-sdk b4310df, the delivery-records work merged with sdk main (issue 1280) 2026-10-26 22:11:55 +01:00
mesh-tools 78d7ef71cc Merge pull request 'The catalogue's placement files: listed at a merge and checked (issue 1126)' (#505) from feat/1126-nodes-compose into main 2026-10-26 21:11:16 +00:00
jschoubben 1a738ed4fc Merge remote-tracking branch 'origin/main' into fix/delivery-records-true 2026-10-26 22:08:50 +01:00
jschoubben c085975146 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-catalog@78d7ef71cc27
mesh/delivery-group group feat/1126-nodes-compose merged: every member is merged
mesh/check checking c0859751461a on 78d7ef71cc27 | novox/mesh-catalog#505 in group feat/1126-nodes-compose
2026-10-26 22:05:42 +01:00
jschoubben ba49a7f0f9 mesh-build-go: a reads-outside marker lists what its tests read, so its package is kept until one of those files changes (issue 1936)
47 packages carried the marker and were never kept: about 2000 tests per level ran on every check, whatever it
changed. The marker now lists, as paths or globs from the repository's root (? before a path whose absence is read),
what the package's tests read outside it; the digest hashes those files with the package's own non-Go files. A marker
that lists nothing, names an unclean path, or a line that matches nothing keeps today's never-kept digest.

The 46 markers list their reads; checks/deps and checks/trunk lose theirs (their tests read only temporary
directories). Digest form 6.

Quick fix of issue 1936, chosen by the operator 2026-10-26 21:56; the thorough fix (the Go test log's files kept with
each verdict) stays on the issue.
2026-10-26 22:02:58 +01:00
jschoubben be7e98d25c 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 b1413949f0 Merge remote-tracking branch 'origin/main' into feat/1126-nodes-compose 2026-10-26 21:56:51 +01:00
mesh-tools eba4bab00a Merge pull request 'The image build context is the module's own directory, so a Dockerfile in a subdirectory builds (issue 1703)' (#512) from fix/1703-image-context into main 2026-10-26 20:36:08 +00:00
mesh-tools 216701fcba Merge pull request 'The build modules size their capacity by CPU share and keep build hours (issues 1890, 1806)' (#511) from fix/capacity-follows-cpu-share into main 2026-10-26 20:33:30 +00:00
jschoubben 8e116cb0e6 Merge main into fix/capacity-follows-cpu-share; pin the build modules at mesh-sdk 805b4d9
mesh/delivery merged: source.main moved to novox/mesh-catalog@216701fcbace
mesh/delivery-group group fix/capacity-follows-cpu-share merged: every member is merged
mesh/check checking 8e116cb0e6e4 on 7c5c29421f02 | novox/mesh-catalog#511 in group fix/capacity-follows-cpu-share
Main's #508 (the binary refusal) pinned mesh-sdk fe11db9; 805b4d9 is the capacity work merged with
sdk main, so it holds both.
2026-10-26 21:29:33 +01:00
jschoubben dcf32576a8 mesh-build-image: build with the module's directory as context, so a Dockerfile in a subdirectory finds its COPY paths (issue 1703)
mesh/delivery merged: source.main moved to novox/mesh-catalog@eba4bab00aa7
mesh/check checking dcf32576a880 on eba4bab00aa7
photos' admin-client, client and server Dockerfiles live in subdirectories and copy
paths from the module's root (admin-client/..., client-server/serve.mjs), as build-agent
built them and as the fingerprint reads the module's whole directory. Taking the
Dockerfile's own directory as context failed every such COPY with copier: stat
"/admin-client/package.json": no such file or directory, on shanks and on ace.

Quick fix (rejected): move photos' Dockerfiles to its repository root; it changes the
operator's photo application and leaves the trap for the next module.
Thorough fix (chosen): one context for every image, the module's directory; it never
leaves that directory, so the isolation of ADR 0487 section 2 holds.
2026-10-26 21:25:24 +01:00
jschoubben 3f8b0b4248 Build modules take asks only within their node's build hours
mesh/delivery-group group fix/capacity-follows-cpu-share rejected: composed together: the checker's verdict at the members' heads (63694fe9222d): unit: ..test
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 3f8b0b42480b on 7c5c29421f02 | novox/mesh-catalog#511 in group fix/capacity-follows-cpu-share
mesh-build-go, -node and -image declare build-hours (node=HH:MM-HH:MM in the node's local time,
empty = any hour) and hand it to the SDK puller (the operator, 2026-10-26: g14 builds only from
18:00 to 06:00); their SDK pin moves to mesh-sdk d00dac1, which has it.
2026-10-26 21:22:09 +01:00
mesh-tools 7c5c29421f Merge pull request 'mesh-issues: a milestone or commit link moves no issue and keeps no worker current (issue 1781)' (#510) from fix/1781-moved-ignores-milestone-links into main 2026-10-26 20:17:09 +00:00
jschoubben 9e41438f1b A merge of a module running nowhere countable reads not yet on the machines in the gate and the tracker; a settle ask is taken away once answered or lapsed; pin mesh-sdk e44cbaf (review of issue 1280) 2026-10-26 21:15:31 +01:00
mesh-tools 3690c149c4 Merge pull request 'Forge rights read as the user sees them; write-only grant for mesh-tools; progress answers in minimal detail (issues 1945, 1947)' (#509) from fix/progress-and-unwritable into main 2026-10-26 20:08:05 +00:00
jschoubben 671fdb7dc2 Pin mesh-sdk 33144781 (reach and settle layouts) in the gate (issue 1280) 2026-10-26 21:05:29 +01:00
jschoubben eab6918459 The tracker takes the view's per-repository reached head as it takes the whole-mesh one: a merge at or before it is delivered (issue 1280, ADR 0556 clause 6) 2026-10-26 21:02:41 +01:00
jschoubben ff2632356b The gate says whether a merged delivery is on the machines, derived from the view's reach, and a person settles a merge held without a passing check on the operator's answer (issue 1280, ADR 0556 clauses 4 and 5) 2026-10-26 21:02:04 +01:00
jschoubben 1e6b157865 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 1e6b15786562 on eba4bab00aa7 | novox/mesh-catalog#505 in group feat/1126-nodes-compose
2026-10-26 20:35:47 +01:00
mesh-tools 5b75233406 Merge pull request 'The catalogue's check uses the shared binary refusal, commit by commit (issue 1577)' (#508) from fix/1577-checker-refuses-binaries into main 2026-10-26 19:32:31 +00:00
jschoubben 4548b97910 Build modules pin the SDK whose parsers keep a crashed file's and a subtest's panic's cause (issue 1901, review round 1) 2026-10-26 20:18:32 +01:00
jschoubben 1faf317069 Review of L2: the vault's own fails closed: a pair credential's consumer is read standing, the shared value goes only to a holder its record names, and unread assignments refuse, never hand (C1, C2); the placement check pins mesh-sdk 3968849 (issue 1126, ADR 0550 §5)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 1faf31706919 on 5b75233406fe | novox/mesh-catalog#505 in group feat/1126-nodes-compose
2026-10-26 20:12:21 +01:00
jschoubben 245143d948 Build modules run at most as many steps as their node's build CPU gives, and say a check's phases
Each of mesh-build-go, -node and -image declares cores-per-step (6, 2, 2) and hands it to the
SDK puller, which bounds the configured capacity by floor(slice quota cores / cores-per-step),
read live; thread limits and the status tools follow the effective capacity (issue 1806's
follow-up: three Go checks on shanks' 12-core slice starved past the 30-minute step bound).
mesh-build-go says its lint and test phases with the ask id (issue 1890).
2026-10-26 19:55:31 +01:00
jschoubben 358ef45536 Pin mesh-sdk's fix/1901-failure-contract head 81ec7bd7 (issue 1901) 2026-10-26 19:50:45 +01:00
jschoubben 851e8782d5 Nodes compose: the catalogue's placement files and their check, mesh-config reading placement, the vault's own and pair credentials, the forge's commit trees (issue 1126)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 851e8782d569 on da29f77b1081 | novox/mesh-catalog#505 in group feat/1126-nodes-compose
One commit for the branch feat/1126-nodes-compose of mesh-catalog: its 15 commits squashed so its history does not carry two compiled programs committed by mistake and removed later, mesh-config (5.5 MB) and checks/placement (6.2 MB) (issue 1577). The tree equals the former head 2db56067. The operator allowed this rewrite of the feature branch on 2026-10-26.
2026-10-26 19:49:39 +01:00
jschoubben 175b2cd791 Build modules report failed tests through mesh-sdk's failure contract and run its conformance kit (issue 1901)
mesh-build-node kept the first fifteen lines of node --test's output, so a passing line of the first file stood for the
failure. Both build modules now parse through the SDK (go test -json, node --test's TAP report written to a file, the
type-check and go vet as console failures), name themselves in the result, and run the SDK's captured cases; the
catalogue check failurekit refuses a node-build-* module with a test step whose tests do not run the kit.
2026-10-26 19:48:39 +01:00
jschoubben 8530a9730f The build modules take mesh-sdk's report of programs left in a branch's history (issue 1577)
mesh/delivery merged: source.main moved to novox/mesh-catalog@5b75233406fe
mesh/delivery-group group fix/1577-checker-refuses-binaries merged: every member is merged
mesh/check checking 8530a9730fbc on 5b75233406fe
2026-10-26 19:47:38 +01:00
jschoubben ee48e5a01e gitea_get_file refuses a bad path before its size listing reaches the forge; the guard test expects the listing (issue 1577) 2026-10-26 19:32:00 +01:00
mesh-tools da29f77b10 Merge pull request 'The catalogue's check refuses a compiled program under modules/, and main tracks none (issue 1577)' (#507) from fix/1577-no-committed-binaries into main 2026-10-26 18:13:59 +00:00
jschoubben 854dbfb3f3 mesh-issues: a milestone or commit link moves no issue and keeps no worker current (issue 1781)
mesh/delivery merged: source.main moved to novox/mesh-catalog@7c5c29421f02
mesh/check being carried to 854dbfb3f3f0 on 7c5c29421f02 | from 3690c149c4ad
The seed linked 188 issues at once and every one read as moved at the seed's minute, hiding when each
really moved; on a worked issue the link also counted as its worker's activity. These links only place
an issue, so last-moved and the worker's activity leave them out.
2026-10-26 19:12:12 +01:00
mesh-tools 5a4ac49913 Merge pull request 'Milestones are records in the issue tracker, and progress reads them (issue 1781)' (#506) from feat/1781-milestone-records into main 2026-10-26 18:06:48 +00:00
jschoubben 69a3b3f7e0 The forge tools cut a request the forge never answers and refuse a file too big for an answer without fetching it: a 5 MB read stalled every read (issue 1577) 2026-10-26 19:03:18 +01:00
jschoubben 3cd826830b The build modules' fetch step says which changed files are compiled programs, so the checker can refuse them (issue 1577) 2026-10-26 19:00:54 +01:00
jschoubben 94dab68635 Refuse a compiled program under modules/, and untrack the two main held: build output reaches the catalogue only by a slip nothing caught (issue 1577)
mesh/delivery merged: source.main moved to novox/mesh-catalog@da29f77b1081
mesh/check being carried to 94dab6863523 on da29f77b1081 | from 5a4ac4991395
2026-10-26 18:43:46 +01:00
jschoubben 76e4ec4dc6 mesh-issues and mesh-knowledge: milestones are records of the issue tracker, and progress reads them (ADR 0493 §1 to §4, issue 1781)
mesh/delivery merged: source.main moved to novox/mesh-catalog@5a4ac4991395
mesh/delivery-group group feat/1781-milestone-records merged: every member is merged
mesh/check checking 76e4ec4dc680 on 26908a89506f | novox/mesh-catalog#506 in group feat/1781-milestone-records
mesh-issues keeps each design's milestone in the state milestones (history 5), says milestone-set and
milestone-moved, and serves milestones, milestone-set and milestone-move. An issue's milestone is its own link
of kind milestone, so at most one milestone per issue is checked inside the issue's own write; kind commit
links a fixing commit. The triage links no milestone, pull request or commit. import with milestones=N seeds
a design's table once. mesh-knowledge's progress reads issue-tracker.milestones and its table parse is gone.

Squashed from four commits so the branch history does not carry a compiled bundle that was committed by
mistake (6 MB); the path is now in .gitignore.
2026-10-26 18:42:51 +01:00
jschoubben c584dc57b9 gitea: mesh/check is the mesh's own status and never blocks; signed commits, an outdated branch and a rejected review are said as blocking the mesh's merge (issue 1945, review round 2)
mesh/delivery merged: source.main moved to novox/mesh-catalog@3690c149c4ad
mesh/check being carried to c584dc57b95e on 3690c149c4ad | a compiled program is not refused: mesh-build-node reported no programs
2026-10-26 18:37:02 +01:00
jschoubben e1e5a8ff0d gitea: mesh-tools writes a user's delivered repository and main's rule stays the operator's: only its merge whitelist gets mesh-tools, read back; what else blocks the mesh's merge is said (issue 1945, the operator's answer 2026-10-26 18:30) 2026-10-26 18:34:03 +01:00
jschoubben c85472285d gitea: main's rule read back must say what was written, since the forge silently drops a whitelist login that cannot write (issue 1945, review round 1) 2026-10-26 18:23:42 +01:00
jschoubben f0fbe193fa gitea: mesh-tools administers a user's delivered repository and puts main's rule right, permissions read as the account sees them; knowledge-base.progress: minimal answers leave items to the tree, failed calls warn before parse faults (issues 1945, 1947) 2026-10-26 17:59:34 +01:00
mesh-tools 26908a8950 Merge pull request 'knowledge-base.progress: how far a design, milestone, issue or record has come, in one call (issue 1947)' (#504) from feat/1947-progress into main 2026-10-26 16:42:30 +00:00
mesh-tools 27a7757b97 Merge pull request 'The mesh writes the repositories it delivers, and asks no Approve for a merge it cannot carry out (issue 1945)' (#503) from fix/forge-writes-delivered-repos into main 2026-10-26 16:25:07 +00:00
jschoubben 87fe2c45cf Knowledge base: progress counts an issue done only once its fix is delivered, and says every gap (issue 1947, review round 1)
mesh/delivery merged: source.main moved to novox/mesh-catalog@26908a89506f
mesh/delivery-group group feat/1947-progress merged: every member is merged
mesh/check pass 87fe2c45cfed on 26908a89506f | carried from 27a7757b970e, hq 3a5c4c7c4d9c | unit 2091 run, 2327 kept, 6 skipped | integration 2112 run…
A resolved issue whose delivery is held or failed is stuck, one merged and not delivered
is moving; a row the tracker did not answer, an answer that does not read and linked
issues cut by the list's limit are said as warnings instead of passing silently.
2026-10-26 17:02:47 +01:00
jschoubben 24b244bc88 mesh-delivery-gate: ask no Approve of a merge the forge account cannot carry out, and say why (issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-catalog@27a7757b970e
mesh/check pass 24b244bc88dc on 27a7757b970e | carried from 6b6d5cba7195 | unit 2046 run, 2327 kept, 6 skipped | integration 2066 run, 2346 kept, 10 s…
car-hunter #2 was approved and its commit status then refused 403. Before the ask the
gate reads merge.writes for each repository it covers; where the merging account cannot
write, nothing is asked, the delivery waits on 'the forge account cannot write <repo>:
gitea_grant_write' and a condition is raised. gitea.repo.writable reads it again and
asks an approved, refused merge of it again.
2026-10-26 16:39:56 +01:00
jschoubben 5baf8ad1d7 Knowledge base: progress is proven over the bus with the module started whole (issue 1947)
The verb asks three seats and the records module; only a run over a real bus shows its
grants and subjects line up with what the manifest invokes.
2026-10-26 16:39:19 +01:00
jschoubben 8da33e43d4 Knowledge base: progress says how far a design, milestone, issue or record has come (issue 1947)
A lead's five-minute report and 'is design 47 nearing its end' were joined by hand from
the design file, the tracker and the deliveries. One verb now works the state out
(ADR 0424 §1), minimal for every five minutes, detailed for the report's tree.
Milestones are read from the design's table until the tracker's milestone records
(issue 1781) are delivered.
2026-10-26 16:37:54 +01:00
jschoubben 52de4c320e gitea: the merging account writes every repository that builds a registered module, read from the indexer's modules (issue 1945)
car-hunter #2 was approved and its commit status refused 403: mesh-tools only read the
repositories of a user. It is now a write collaborator on each repository the indexer's
modules state names (watched over the bus: at the start and on each registration), read
back and said as gitea.repo.writable; the operator's other repositories stay read-only.
gitea_grant_write and gitea_unwritable_repos do and show it by hand; gitea_commit_statuses
answers merge.writes.
2026-10-26 16:36:50 +01:00
jschoubben 5e516317e9 Issue tracker: list rows say when each issue last moved, and list takes linked (issue 1947)
The knowledge base's progress verb reads a milestone's issues in one call and must say
when each last moved, and the issues linked to a record or a design; neither was
readable without one get per issue.
2026-10-26 16:29:56 +01:00
mesh-tools 6b6d5cba71 Merge pull request 'Image builds run their RUN steps on pasta, and every build failure keeps its real error (issue 1703)' (#502) from fix/1703-image-error-shown into main 2026-10-26 14:48:17 +00:00
mesh-tools 8393e29621 Merge pull request 'An approved group merges whole or not at all, and two groups never interleave in one repository (issue 1275)' (#501) from fix/1275-group-merge-whole into main 2026-10-26 14:47:34 +00:00
mesh-tools 61563abf2f Merge pull request 'Every forge account the mesh's parts use reads every repository (the operator, 2026-10-26)' (#500) from fix/fetch-reads-all into main 2026-10-26 14:47:29 +00:00
jschoubben 071c6885d9 The mesh's forge accounts all read every repository, and a build's fetch says the forge's own words
mesh/delivery merged: source.main moved to novox/mesh-catalog@61563abf2f93
mesh/delivery-group group fix/fetch-reads-all merged: every member is merged
mesh/check checking 071c6885d923 on 8393e2962193
mesh-catalog #495 gave read on every repository to the tools' account alone. The build modules
fetch over ssh as nox-builder (the forge-account provider's builders account, ADR 0466 §3), so
ace's and g14's fetch of jschoubben/car-hunter and jschoubben/snake still failed; the machines'
agents (nox-agent, ADR 0410 §1) could not read them either.

- gitea: readall.ts grants every account of one list, meshAccounts in tools-account.ts (mesh-tools,
  nox-builder, nox-agent; a Go test holds the two Go names to internal/forgeaccount's constants),
  on the same start pass, forge events and tools. gitea_unreadable_repos answers per account and
  names the accounts the forge does not hold yet; gitea_grant_read takes accounts as well as
  repositories. An absent account is asked nothing; the start pass runs again ten minutes apart
  until every account was there once.
- mesh-build-go, mesh-build-node, mesh-build-image: pin mesh-sdk ff81bdf8 (fix/fetch-reads-all),
  whose forgekey.Said keeps git's words past the bare "error:" line: the mirror's line said only
  "git -C: error:". Their own git reasons (ls-files, ls-remote, rev-parse, repack) go through it too.
2026-10-26 15:27:38 +01:00
jschoubben 8c5f27627b mesh-build-image: give a RUN step pasta's network, since the default netavark bridge cannot be set up rootless (issue 1703)
mesh/delivery merged: source.main moved to novox/mesh-catalog@6b6d5cba7195
mesh/check checking 8c5f27627b67 on 6b6d5cba7195
Every RUN failed before it started: 'setup network: netavark: nftables error ... Could not process rule'. Reproduced on
shanks with buildah 1.45.2, a clean environment and a private XDG_RUNTIME_DIR; --network pasta builds, and reaches the
network from a RUN. The sysrq integration row now fails if the RUN never started.
2026-10-26 15:23:19 +01:00
jschoubben 82bd6f49ff A half-merged group keeps its repositories until whole or declined, and the operator's word does not ask it again while another pass it would wait for is under way (issue 1275, review round 1)
mesh/delivery merged: source.main moved to novox/mesh-catalog@8393e2962193
mesh/check checking 82bd6f49ff9c on 8393e2962193
A held half-merged group did not count as under way, so another group sharing a repository could start; asked
again with delivery_merge_again, each then waited on the other. Passes are now read from every approved,
undeclined Approve not merged whole (approvedMerges leaves out merged and refused members), and
delivery_merge_again is refused while passWaits names a pass, naming it. The forge's mergeable is also false
for a work in progress: said so.
2026-10-26 15:23:01 +01:00
mesh-tools ceb3aac9b3 Merge pull request 'Build modules size their steps from the node's build share once it is given (issue 1806, change A)' (#499) from fix/build-cpu-yields into main 2026-10-26 14:22:50 +00:00
jschoubben 98c66fb1e4 Build modules: a failed command's reason says its error, not its last progress line (issue 1703)
buildah writes STEP lines on stdout and its error on stderr, so the last line of both hid every image build's cause.
mesh-build-image's reason is now stderr's last lines (600 bytes) with the exit status, and the journal gets both tails;
mesh-build-node keeps stderr's end and stdout's start apart; mesh-build-go's cause skips go build's '# package' line.
2026-10-26 15:21:01 +01:00
jschoubben 07ec186031 Merge an approved group only when every member merges now, and let no other merge land between its members (issue 1275)
On 2026-10-26 13:40 group fix/734-lost-ask merged two of its three members: the forge answered mesh-delivery#73's
merge 405 "Please try again later", and in the seconds between, group feat/890-placement merged mesh-delivery#72,
so #73 then conflicted.

- mesh-delivery-gate: before any member is handed, every member is read for its required statuses, its head still
  the approved one, and the forge's own mergeable (in conflict, or still being worked out); the forge's "no" is asked
  again every 5 s for 2 minutes, then the group is held and said urgent with nothing merged.
- mesh-delivery-gate: an approved merge waits while another approved merge's pass is under way into a repository it
  goes into (handed until merged whole, or stopped), whether or not mesh/check gates merges (it waits for a merge,
  never for a check: ADR 0551).
- mesh-delivery-gate: tool delivery_merge_again asks a refused or held merge again on the operator's word.
- gitea: the merge asks again after the forge's 405 "Please try again later" (1 s, 2 s, 4 s), and a pull request
  carries the forge's mergeable, which gitea_commit_statuses answers.
2026-10-26 14:53:22 +01:00
jschoubben 2aecbe8055 Merge main into fix/build-cpu-yields; pin mesh-sdk at its head (ADR 0553, ADR 0554)
mesh/delivery merged: source.main moved to novox/mesh-catalog@ceb3aac9b39a
mesh/delivery-group group fix/build-cpu-yields merged: every member is merged
mesh/check being carried to 2aecbe80553c on ceb3aac9b39a | from 9a8e54d988fe, mesh-host bba9081ca127
2026-10-26 14:47:27 +01:00
jschoubben 3def293d94 Build modules: no thread limit without the composer's share; with it, the share over their own capacity (ADR 0553 round 2)
Change A rebuilds the three build modules, so their runtimes restart once; it
changes nothing else about their builds until change B gives them the share.
2026-10-26 14:41:12 +01:00
mesh-tools 9a8e54d988 Merge pull request 'mesh-build-go says which packages it cannot test here, and mesh-build-node's tests need the Node toolchain (issue 890, ADR 0552)' (#498) from feat/890-placement into main 2026-10-26 13:40:41 +00:00
mesh-tools 2da9329df4 Merge pull request 'pacman: the round and the history tools say in plain words what they answer' (#497) from fix/pacman-findable into main 2026-10-26 13:39:16 +00:00
jschoubben ecbee53cfd Merge main into fix/build-cpu-yields; pin mesh-sdk at its head (ADR 0553) 2026-10-26 14:23:33 +01:00
jschoubben dcdf4238c7 Build modules: no cap of their own; a step's threads are its part of the node's one share (ADR 0553, issue 1806)
Review round 1: a cap per module let three build modules take three shares
of the node. The share is now one node value held by the node-engine's
yielding slice; each step divides it by every build slot of the node, from the
composer's words, or assumes every build seat at its own capacity without them.
2026-10-26 14:22:05 +01:00
jschoubben d3423f54ed mesh-build-go: resolve the go.mod and go.sum the main merge left in conflict; pin mesh-sdk 33395f68 (issue 890)
mesh/delivery merged: source.main moved to novox/mesh-catalog@9a8e54d988fe
mesh/delivery-group group feat/890-placement merged: every member is merged
mesh/check checking d3423f54ed58 on 9a8e54d988fe | novox/mesh-catalog#498 in group feat/890-placement
2026-10-26 14:06:37 +01:00
jschoubben 0e322d087f mesh-build-go: pin mesh-sdk 33395f68, feat/890-placement merged with main (issue 890) 2026-10-26 14:05:38 +01:00
jschoubben 0300a6fd99 pacman: drop the plain-word phrases from replaces (it holds shell commands only, ADR 0245); the descriptions' first lines carry them
mesh/delivery merged: source.main moved to novox/mesh-catalog@2da9329df448
mesh/check being carried to 0300a6fd99b5 on 2da9329df448 | from b1e610b2135a
2026-10-26 14:04:09 +01:00
jschoubben d21b81b00a mesh-build-go: node-build-node also needs bubblewrap and a toolchain installed whole (its mark and lock), as mesh-build-node's integration tests do; pins mesh-sdk 45564e69 (issue 890, review round 1) 2026-10-26 14:02:27 +01:00
mesh-tools b1e610b213 Merge pull request 'Build modules end the steps a previous process left running, and serve build_<lang>_step_end (issue 1890)' (#496) from fix/1890-left-running-steps into main 2026-10-26 12:57:34 +00:00
jschoubben 0134697f97 Build modules: the round ends asks past the step bound; pin the sdk head (issue 1890)
mesh/delivery merged: source.main moved to novox/mesh-catalog@b1e610b2135a
mesh/delivery-group group fix/1890-left-running-steps merged: every member is merged
mesh/check pass 0134697f9739 on b1e610b2135a | carried from a4f5df797b68, mesh-sdk b4cd95e7c4a4 | unit 3132 run, 3022 kept, 7 skipped | integration 31…
2026-10-26 13:52:53 +01:00
jschoubben 21583d14ad Pin mesh-sdk at fix/build-cpu-yields' head (issue 1806) 2026-10-26 13:49:54 +01:00
mesh-tools a4f5df797b Merge pull request 'gitea: the tools' forge account reads every repository (the operator, 2026-10-26)' (#495) from fix/forge-account-reads-all into main 2026-10-26 12:46:37 +00:00
jschoubben ac2c56f2a9 Build modules: steps end by compare-and-set; pin the sdk head with the review's fixes (issue 1890) 2026-10-26 13:44:00 +01:00
jschoubben b4a7bb9dea gitea: say that a repository made private later is granted only at the next start or by hand, and let one unreadable permission refuse its repository, not the whole pass
mesh/delivery merged: source.main moved to novox/mesh-catalog@a4f5df797b68
mesh/check being carried to b4a7bb9dea50 on a4f5df797b68
2026-10-26 13:43:46 +01:00
jschoubben 64b9ded2db Build modules: a per-node processor cap, and each step's threads within it (issue 1806)
The operator's desktop slows while it builds (shanks at 92%, the Go build
runtime at 1650% of 24 processors). The build modules declare cpu-cap
(default shanks=50,g14=50,ace=75), which the composer turns into the runtime's
quota at idle priority; each step's go, node and buildah commands get
GOMAXPROCS and UV_THREADPOOL_SIZE for their part of it, and the status tools
show the limits and what the kernel holds the runtime to.
2026-10-26 13:41:05 +01:00
jschoubben 67d6a5a7c1 mesh-build-go: report the needs a package's check-needs marker names, test it only where the node has them, decline an ask needing what the node lacks (issue 890)
mesh-build-node's integration tests read the Node toolchain installed only where the node holds node-build-node, and
skipped on ace: its package now declares node-build-node beside its tests; the integration digest names it, a node
lacking it leaves the package unmet for the checker to place, and declines a placed ask. Re-pins mesh-sdk feat/890-placement.
2026-10-26 13:37:52 +01:00
jschoubben 5fe5f26e7e Build modules: a restart ends the steps it left running, a step shows what it is, build_*_step_end ends one (issue 1890) 2026-10-26 13:27:46 +01:00
jschoubben b3221b6ab8 pacman: a plain-words search finds pacman_round and pacman_upgrades first (answer on the first line, phrases they replace) 2026-10-26 13:27:09 +01:00
jschoubben 0e7e220d49 gitea: give the tools' account read on every repository, at the start and on the forge's events, so a user's private repository no longer answers its tools 404 2026-10-26 13:22:08 +01:00
mesh-tools 879600202a Merge pull request 'Vault: adopt the hashes of bus accounts it holds no value for, handed sealed by the bus node (issue 1945, ADR 0550 clause 4)' (#494) from feat/1945-user-list-switch into main 2026-10-26 12:21:47 +00:00
mesh-tools 565d0a848a Merge pull request 'Gate: merge turns, a merge waits for a running check of its repository, merges on a carried verdict with no second ask (ADR 0548, issue 568)' (#493) from feat/548-one-check into main 2026-10-26 12:01:28 +00:00
mesh-tools 71e094b2de Merge pull request 'mesh-build-go: count skipped tests per package, so the checker keeps no pass that skipped (issue 890)' (#492) from fix/checker-skips-not-kept into main 2026-10-26 12:01:13 +00:00
mesh-tools f96704fa0f Merge pull request 'mesh-build-node tests: never install a toolchain from the registry in the integration level; skip as NOT RUN when none is installed (issue 890)' (#491) from fix/build-node-tests-offline into main 2026-10-26 11:34:17 +00:00
mesh-tools 86fdf74a0a Merge pull request 'Desk channel: keep what was shown in memory while the store does not answer, so a clearing replaces the urgent notification (issue 384)' (#490) from fix/desk-clear-lost into main 2026-10-26 11:33:47 +00:00
mesh-tools 843281c0e5 Merge pull request 'Knowledge base: withdraw a clause's 'changed by' mark once no accepted record changes it (issue 466)' (#489) from fix/kb-stale-marks into main 2026-10-26 11:33:34 +00:00
jschoubben e9279ce3c2 gate: find an approved merge by its ask, not by which member's run was seen first, so a group's turns are written before its first member is handed at every look (ADR 0548 §3; issue 568, review round 1)
mesh/delivery merged: source.main moved to novox/mesh-catalog@565d0a848a85
mesh/delivery-group group feat/548-one-check merged: every member is merged
mesh/check checking e9279ce3c209 on 565d0a848a85 | novox/mesh-catalog#493 in group feat/548-one-check
2026-10-26 12:32:46 +01:00
jschoubben c9414b1c98 Vault: pin mesh-sdk at the switch group's head c2a410bb, one sdk for the adoption and the switch (issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-catalog@879600202a09
mesh/delivery-group group feat/1945-user-list-switch merged: every member is merged
mesh/check checking c9414b1c9856 on 879600202a09 | novox/mesh-catalog#494 in group feat/1945-user-list-switch
2026-10-26 12:31:56 +01:00
jschoubben ea53b2f920 mesh-build-go: each package counts its skipped tests (issue 890)
mesh/delivery merged: source.main moved to novox/mesh-catalog@71e094b2de59
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:19 +01:00
jschoubben 99cc073e2a gate: pin mesh-sdk at the group's head 9c61918ac36f, where a turn never holds the check of heads waiting in it (ADR 0548 §3; issue 568, review round 1) 2026-10-26 12:15:20 +01:00
jschoubben b94150fcdb gitea: the merge tool reads a carried verdict's line as judged on the new trunk, so it merges a carried verdict (ADR 0548 §8; issue 568, review round 1) 2026-10-26 12:15:20 +01:00
jschoubben a01715ddf5 Skip the Node build module's toolchain tests by name where no Node toolchain is installed, instead of installing one from the registry in a level that has no network; merge three tests that share one fixture
mesh/delivery merged: source.main moved to novox/mesh-catalog@f96704fa0ff4
mesh/check checking a01715ddf508 on 565d0a848a85
On ace, which holds no Node build module, five integration tests each spent 70 s in npm's network retries and failed every mesh-catalog check run there (five verdicts since 00:07). The tests now copy the node's installed toolchain or are skipped as NOT RUN; the registry is never reached. Three tests sharing the fixture are one, so the toolchain is copied three times instead of five (10 integration tests in the module, now 8).
2026-10-26 12:05:09 +01:00
jschoubben e2136ec0c9 The vault takes a handing whole once, refusals and all, and names the refused users in its last adoption (review of the adoption, ADR 0547 clause 9) 2026-10-26 12:03:14 +01:00
jschoubben a30c168512 desk-channel: take the work when the store does not answer, and keep what it holds at the next work and the minute's standing
mesh/delivery merged: source.main moved to novox/mesh-catalog@86fdf74a0a83
mesh/check checking a30c16851246 on 565d0a848a85
Review round 1: offering the work again let a redelivered raise follow its own clearing and
replace it, and a redelivery is bounded where an outage is not. The work is now taken; what
was shown stays in memory, recalled first, and is kept at the start of the next work and at
the minute's standing, stopping at the first write the store does not answer. A desk that
restarts before then loses where those are shown, and its log said how many it held.
2026-10-26 11:46:21 +01:00
jschoubben f885250913 desk-channel: keep where a message is shown when the store does not answer, so its clearing still replaces it
On 2026-10-26 09:18 the bus reloading its accounts made the desk's state store answer
'no responders' just as the urgent 'A new build of mesh-controller cannot be delivered'
was shown. The desk logged that it could not keep the notification ids and acked the work;
the router's clearing at 09:34 then found no record, was shown as a new notification, and
the urgent one stayed on the operator's screen for hours.

Now what was shown and not kept is held in memory, recalled before the store by edits,
reminders, clicks and a notifier restart, kept at the next piece of work, and its work is
offered again until the store keeps it, without being shown twice. An edit of a message
nothing records is said in the journal.
2026-10-26 11:46:09 +01:00
jschoubben 7627503d9d gate: 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:30 +01:00
jschoubben 351c7c4bf4 gate: deliveries and show say an approved merge waits for the check of the named delivery on the named repository, and the turns bucket is read by the checker alone, written by the gate alone (ADR 0548 §3, §9; issue 568 G6) 2026-10-26 11:44:29 +01:00
jschoubben 55a26b22cf gate: delivery_turns reads every merge turn with the check it waits for, and delivery_hand_now hands a held merge at once on the operator's word (ADR 0548 §4; issue 568 G5) 2026-10-26 11:43:40 +01:00
jschoubben 100e74f283 gate: an approved merge whose trunk moved waits for its carried verdict and merges on it with no second ask, never on the old one (ADR 0548 §8; issue 568 G4) 2026-10-26 11:43:18 +01:00
jschoubben c8e1e2b2fa gate: a merge held past 30 minutes by another delivery's check is a stalled line naming that check and since when, said as delivery.<id>.stalled (ADR 0548 §4; issue 568 G3) 2026-10-26 11:41:42 +01:00
jschoubben d0d00f922a gate: an approved merge writes its turn on every repository it goes into before it is handed, kept for a group until its last member merged, read by the checker (ADR 0548 §3; issue 568 G2) 2026-10-26 11:41:26 +01:00
jschoubben c2bd7760b2 gate: hand no approved merge into a repository a running check of other heads judges, and hand it on that check's end (ADR 0548 §2; issue 568 G1) 2026-10-26 11:40:35 +01:00
jschoubben f0fd53281b The vault says once why a look of the bus module's have adopted nothing, and again when it changes (ADR 0547 clause 9) 2026-10-26 11:37:14 +01:00
jschoubben fb039250f6 The vault adopts the hashes of the bus accounts it holds no value for from the bus node's handing (ADR 0547 clause 9, issue 1945)
take-over waits on a hash for every user of the loaded list, and the vault holds no value for the controller's own
account, the node accounts and the enrolled devices. The bus node's engine now hands every loaded user's hash in the
bus module's have, sealed to the vault's key (mesh-host and mesh-sdk on the same branch). The vault answers its key in
node-key, reads the haves (manifest reads node-engine.have, granted from the manifest, no controller change), watches
the bus module's have, opens the handing with its own key, and writes each user it holds no value for into
bus-accounts marked adopted, with the list and node it came from, saying account-hashed. It never touches a user whose
covered account it holds or whose key is its own hash, writes an adopted key again only for another hash, and refuses
a handing from any node the assignments do not name as holding the bus module.

Tools: account_adopt adopts now from the have as the watch does; secret_own, unnarrowed by module, lists the adopted
accounts and the last adoption.
2026-10-26 11:36:33 +01:00
jschoubben 447e6a13cd Knowledge base: withdraw a clause's changed mark when its changing record's accepted links drop it, on every judgement and by a tool (stale marks)
mesh/delivery merged: source.main moved to novox/mesh-catalog@843281c0e5f0
mesh/check checking 447e6a13cd68 on 565d0a848a85
2026-10-26 11:22:12 +01:00
mesh-tools c51a46b802 Merge pull request 'Build modules: run every package's tests of a level, with a fixture of two failing packages (issue 1921)' (#488) from fix/1921-every-failure-r into main 2026-10-26 10:18:08 +00:00
jschoubben 15a5fe09b6 mesh-build-go, mesh-build-node: run every package of a check's level after a failure (issue 1921)
mesh/delivery merged: source.main moved to novox/mesh-catalog@c51a46b802d3
mesh/delivery-group group fix/1921-every-failure-r merged: every member is merged
mesh/check checking 15a5fe09b62e on c51a46b802d3 | novox/mesh-catalog#488 in group fix/1921-every-failure-r
Pins mesh-sdk fix/1921-every-failure-r, whose check ask no longer stops a level at its first failing test step. A captured fixture with a failing test in each of two packages proves the build module's test step names both.
2026-10-26 10:58:07 +01:00
mesh-tools 3ded9e7800 Merge pull request 'Vault: name a hash record's secret in a field called of, so the runtime keeps it (issue 1945)' (#487) from fix/1945-vault-hash-field into main 2026-10-26 09:00:51 +00:00
jschoubben e7aaa8420e Vault: name a hash record's secret in a field called of, so the runtime accepts it
mesh/delivery merged: source.main moved to novox/mesh-catalog@3ded9e780043
mesh/check pass e7aaa8420eac on 3ded9e780043 | unit 2018 run, 2325 kept, 5 skipped | integration 2040 run, 2344 kept, 9 skipped | over bound: unit, in…
The runtime refuses a state field whose name ends in secret, so no bus-account hash could be written on novox. The test state now applies the same check.

Issue: 1945
2026-10-26 09:25:35 +01:00
mesh-tools 22381d7522 Merge pull request 'Vault: read own secrets' generations, and hash each bus account it is handed for the grants part (issue 1945, ADR 0547 clause 9)' (#486) from fix/1945-vault-own-read into main 2026-10-26 07:48:03 +00:00
jschoubben 1c55e5d44d Vault: take and hash a process's own bus account (broker-<process id>) as a covered account, so take-over is not held by every process user
mesh/delivery merged: source.main moved to novox/mesh-catalog@22381d75224f
mesh/check failed: integration: meshbuildnode/cmd/mesh-build-node.TestAFixtureBundleBuiltTwiceHasOneDigest: no toolchain of this lock is installed und…
The controller hands each process account of a core or moved module with origin issued (ADR 0436 §2); the vault took
only broker and provisioner-broker, refused these and never hashed them. Owed hashes are now said once per pass, and
account_hash says who asked when it refuses too (review round 1).

Issue: 1945
2026-10-26 02:35:57 +01:00
jschoubben 32b912cfe5 Vault: hash each bus account it is handed into bus-accounts.<user>, once per generation, so the grants part can compose the user list without the controller
The controller still mints each covered account and hands it to the vault (ADR 0374); ADR 0547 clause 9 has the vault
write the bcrypt hash of what it holds into its own state, readable by the grants part, and say account-hashed. It
hashes on keep, at start every held account with no key of its generation, and again by hand (account_hash); a refused
write is owed and written at the next pass. secret_own says per bus account whether this generation's hash is written.

Issue: 1945
2026-10-26 02:28:28 +01:00
jschoubben e572937d97 Vault: secret_own reads the module-own secrets it keeps, as fingerprints, so a generation can be told without the value
mesh/delivery closed: a newer head of the same pull request
mesh/check pass e572937d97fa on 567736f7795a | unit 2143 run, 2198 kept, 5 skipped | integration 2165 run, 2217 kept, 9 skipped | over bound: unit, in…
Issue: 1945
2026-10-26 02:09:03 +01:00
mesh-tools 567736f779 Merge pull request 'Build modules' step and toolchain lists read only the newest keys they answer, in parallel (issue 1827)' (#485) from fix/1827-bounded-reads into main 2026-10-25 23:56:12 +00:00
jschoubben e901769cb2 Read the newest steps first: sort the node's keys by id (start order) before the cut, and say older steps were not read
mesh/delivery merged: source.main moved to novox/mesh-catalog@567736f7795a
mesh/check failed: integration: meshbuildnode/cmd/mesh-build-node.TestAFixtureBundleBuiltTwiceHasOneDigest: no toolchain of this lock is installed und…
The review found a cut read took an arbitrary 1000 keys, so the newest steps could be missing. A step id is
base-36 milliseconds plus random digits, so its string order is its start order; the newest max(limit*4, 100)
keys are read now, 1000 at most. The toolchains read logs when it drops keys past its cap.
2026-10-26 00:43:53 +01:00
jschoubben 8f9c78bcba Read only the node's own steps, in parallel and bounded, so build_*_steps answers within the call's 30 s (issue 1827)
On a steps state of 1164 keys / 20 MB (mesh-build-node: 10034 keys) the read tools listed every key and
then read each value one round trip at a time, filtering by node only after the Get, so they timed out
at 30 s. Now the keys are filtered by the node's prefix before any Get, the values are read 16 at a
time, at most 1000 a call (the answer says it was cut), and one log line gives rows read and time.
build_*_toolchains reads its values the same parallel way. The kind: a read tool that reads a whole
state key by key; every read tool of the three modules is covered.
2026-10-26 00:39:33 +01:00
mesh-tools 82e7e3e326 Merge pull request 'mesh-provisioner-client: a module that leaves or is placed again asks the vault afresh (issue 1945)' (#484) from fix/1945-held-secret-on-leave into main 2026-10-25 23:21:44 +00:00
jschoubben 363a3d3f7e Provisioner client: drop a module's held vault secrets when it leaves or is placed again, so it asks afresh (issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-catalog@82e7e3e32675
mesh/check pass 363a3d3f7e8f on 82e7e3e32675 | unit 2015 run, 2322 kept, 5 skipped | integration 2037 run, 2341 kept, 9 skipped | over bound: unit, in…
A re-assigned module was filled with the generation it held before the vault minted a new one; the bus refused the account.
2026-10-26 00:03:43 +01:00
jschoubben 609013a263 Merge pull request 'Gate: a merge learnt seconds late moves the trunk; and a setting check-gates-merge so the merge check can stop gating (operator 2026-10-25 22:56)' (#483) from fix/merge-without-check-gate into main
Reviewed-on: #483
2026-10-25 22:18:41 +00:00
jschoubben 32ad59fc64 mesh-delivery-gate and gitea: a setting check-gates-merge; false asks a reviewed pull request's Approve and merges it without waiting for mesh/check
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/check pass 32ad59fc6402 on 609013a2638f | unit 2010 run, 2322 kept, 5 skipped | integration 2032 run, 2341 kept, 9 skipped | over bound: unit, in…
The operator, 2026-10-25 22:56: "run the check locally only, but not as a gate, it loses us too much time". Tonight each
merge ask waited 3-7 minutes for mesh/check, and every merge into main refused the next approved merge for a check judged
on the trunk before (mesh-delivery#52). Both settings default to true, so nothing changes until the operator sets them.
False: the gate makes a reviewed pull request ready and asks its Approve with no verdict, a trunk's move puts nothing out
of date, a verdict is kept and shown, and a group still merges in one pass; the merge tool merges a head the Approve
names whatever its mesh/check says. delivery_status and show say the mode.
2026-10-25 23:07:50 +01:00
jschoubben 93f655dac9 mesh-delivery-gate: a merge learnt from the open list seconds after it landed moves its trunk and source.main; delivery_status shows source.main
mesh-delivery#51 merged 21:43:11 and the gate read the open list at 21:43:14, before the forge's merged event: taken as
learnt late, it moved no trunk and its group's finish left it out of source.main, so the checker kept judging every
mesh-delivery head on 73be3df. A late merge within replayLate of its landing is now taken as heard as it happened, and
one a gate before this left is written at the catch-up of the start (never backwards).
2026-10-25 23:01:37 +01:00
mesh-tools 573a59e3cc Merge pull request 'mesh-provisioner-client: retry a refused write on the node-engine's wake and ask the vault again at once (issue 924)' (#482) from fix/provisioner-retries-on-declaration into main 2026-10-25 21:08:48 +00:00
jschoubben d0566f9ade Fill a refused file again once the node-engine's declaration may let it through (ace, 2026-10-25 21:17)
mesh/delivery merged: source.main moved to novox/mesh-catalog@573a59e3cc07
mesh/delivery-group group fix/provisioner-retries-on-declaration merged: every member is merged
mesh/check pass d0566f9ade9a on 573a59e3cc07 | unit 2301 run, 3410 kept, 10 skipped | integration 2326 run, 3431 kept, 13 skipped | over bound: unit, …
The node-engine refused mesh-build-go's files as not named as filled 32 s before
its want arrived, and nothing filled them after the apply that named them: the
runtime crash-looped on its missing broker credential. The client now fills
every module again when the node-engine touches its write socket after such an
apply (mesh-host fix/provisioner-retries-on-declaration), says each retry and
each refusal it waits on, and asks the vault again at once for a refused secret
when that wake or the module's own assignment may have ended the refusal.
2026-10-25 21:42:34 +01:00
jschoubben 9d9dfda19b Merge pull request 'Gate: hand every member of an approved group in pin order, settle pull requests closed on the forge, and say a stopped pass as a condition (issue 1275)' (#480) from fix/1275-group-hands-every-member into main
Reviewed-on: #480
2026-10-25 19:28:33 +00:00
jschoubben cdbe2945cf Merge pull request 'Build modules' steps live two days and their pause has a state of its own, so the steps bucket no longer fills (2026-10-25)' (#481) from fix/steps-bucket-ages into main
Reviewed-on: #481
2026-10-25 18:06:39 +00:00
jschoubben 4e82a824be Announce the pauses state in the three build modules, and pin mesh-sdk 518dae5 (an unreadable pause pulls as not paused)
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/delivery-group group fix/steps-bucket-ages held: a member is held: novox/mesh-sdk@518dae5ec9c8 (merged without a passing check: only a person decides what…
mesh/check pass 4e82a824be7a on cdbe2945cf6e | unit 1994 run, 2322 kept, 5 skipped | integration 2016 run, 2341 kept, 9 skipped | over bound: unit, in…
Without it in Kept, stdio.KeptState refuses the state before the bus (mesh-sdk go/keeps.go
refusedUnannounced), so the pause could be neither read nor written.
2026-10-25 18:57:08 +01:00
jschoubben 78d26867d2 Give the build modules' steps state an age of two days, and keep a node's pause in a pauses state of its own
mesh-build-go's steps bucket filled to the bus's 64 MiB cap (27,370 keys, none ever removed) and
refused the end of a passed check, so the check read failed; mesh-build-node's (17.8 MB) and
mesh-build-image's grow the same way. A step's key is per step: its reader reads its end within
the step's bound, and a check's verdict is kept by the checker, so two days is ample. The node's
pause key lived in the same bucket and would have ended by itself; it moves to a pauses state
without an age (mesh-sdk fix/steps-bucket-ages, Puller.PausePut). No node is paused today, so
nothing is carried over.
2026-10-25 18:53:04 +01:00
jschoubben 0c216d99d0 Gate takes a merge learnt after the fact as past: no trunk move, the forge's time, never source.main (issue 1275, review round 2)
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/check pass 0c216d99d0c0 on 9d9dfda19b7e | unit 2181 run, 2147 kept, 5 skipped | integration 2203 run, 2166 kept, 9 skipped | over bound: unit, in…
A pull request found merged only by its absence from the open list went through PullMerged, which moved every
other open delivery of its repository back to an old trunk (ready ones to proposed, composed checks out of
date), dated the merge now, and so let a later catch-up write the old commit into source.main. pullMerged now
takes such a merge as late: its own delivery is merged and its group closed, no other trunk moves, its time is
the forge's or none, and LearntLate keeps it out of source.main in finish, settleForgeMerges and the catch-up.

The gitea tool gitea_commit_statuses now answers the pull request's merged_at, and the gate reads a gone pull
request by its number alone, so a deleted branch does not fail the read.
2026-10-25 18:37:59 +01:00
jschoubben 145c54d19c Gate settles pull requests closed with no event, looks again at a line's bound, and keeps a waiting head's condition over a restart (issue 1275, review round 1)
The conditions the gate now says would otherwise be wrong three ways:

- A pull request closed or merged on the forge with no event heard stayed ready here for days (de-spiegel #4,
  closed 2026-10-17) and would raise a condition. After a complete read of the open list (shorter than the
  forge tool's 1000), each delivery not final whose pull request is missing is read from the forge and taken as
  its close or merge; a merge learnt so does not move source.main, as it is undated and could move it back.
- A line that comes due on the clock (a state's bound, the author's bound, an answered refusal, an
  out-of-date refusal turning urgent after 10 minutes) waited for an unrelated event. The gate now asks a look
  at the earliest such time.
- After a restart the heads waiting on the forge start their bound again, so their conditions were cleared and
  raised again later. A condition is cleared only once the open list was read since start, and not while the
  forge still shows its head open and waiting.

Also: an empty path list ranks with a truncated one in the merge order, and a seat that keeps refusing
conditions is retried on the clock 10 looks in a row, then at events only, said once in the log.
2026-10-25 18:32:19 +01:00
jschoubben 67092161bd Gate hands every member of an approved group, in pin order, and says a stopped pass (issue 1275)
Met again 2026-10-25: group fix/puller-wakes-on-capacity was merged half. Three causes in the gate:

- The next member's turn compared the group's state with "delivering", a word GroupState no longer says
  since release 6 renamed it "merging" (ready.go groupTurnToMerge). Once the first member's merge was heard
  from the forge the group read merging, and the second member waited for ever. Group states are now named
  constants, the turn goes on in ready and merging (groupMayMerge), and a test holds every word to it.
- Members merged in id order, so mesh-catalog went before the mesh-sdk commit it pins. membersOf now orders
  by mergeRank: mesh-sdk first, then members that change no go.mod or go.sum, then those that do.
- No condition was raised: the controller's self-check that read `stalled` went with release 6
  (mesh-controller dc456d56). The gate now says its stalled lines as conditions itself on the
  operator-channel seat (ADR 0437 §9) after every look, clears those listed no more, and keeps which it
  raised in its new bucket conditions. A held group's line says "half merged" when a member merged.

A pass held for a required status no longer retries on a timer: the status turning success is the event
that asks the merge again.
2026-10-25 18:20:58 +01:00
mesh-tools 2df0503034 Merge pull request 'Let the build modules follow their capacity on the bus, so a node raised from capacity 0 starts pulling (issue 1887)' (#479) from fix/puller-wakes-on-capacity into main 2026-10-25 17:02:40 +00:00
jschoubben 23c827e791 Take the mesh-sdk head that wakes a puller on its capacity, within the test budget, in the three build modules (issue 1887)
mesh/delivery merged: source.main moved to novox/mesh-catalog@2df0503034dc
mesh/delivery-group group fix/puller-wakes-on-capacity merged: every member is merged
mesh/check pass 23c827e7910c on a85d90374195 | judged by the candidate's mesh-build-go at eab02c80fe86
2026-10-25 17:44:50 +01:00
jschoubben 3138570c6b Let the build modules follow their capacity on the bus, so a node raised from capacity 0 starts pulling (issue 1887)
g14's Go builder sat idle at capacity 2 because nothing woke its parked
puller. mesh-build-go, mesh-build-node and mesh-build-image now pin mesh-sdk
b9911dc1, whose puller watches the config value capacity as mesh-config
resolved it and wakes on each change, and hand it that watch.
2026-10-25 17:28:00 +01:00
jschoubben a85d903741 Merge pull request 'nextcloud: the module makes its own mesh-admin account right on a 401 and backs off on a refusal, so its shares poll stops being refused every minute (issue 1253)' (#478) from fix/nextcloud-admin-401 into main
Reviewed-on: #478
2026-10-25 16:01:37 +00:00
jschoubben 1cc54959af nextcloud: repair the account only on a 401, never reset the throttle (issue 1253, review 2)
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/check pass 1cc54959afcf on a85d90374195 | unit 1994 run, 2322 kept, 5 skipped | integration 2016 run, 2341 kept, 9 skipped | over bound: unit, in…
Resetting Nextcloud's brute-force record for the docker gateway cleared it for
every client behind that address: someone guessing passwords could drive the
throttle to 429 and have the module's poll wipe it. A throttle is no evidence
the password is wrong, so a 429 is now backed off only, and the back-off
outlasts the throttle. The account's existence is read from user:info's exit
status, since user:list pages.
2026-10-25 16:57:46 +01:00
jschoubben 962bc359a7 nextcloud: repair only the module's own account, read its existence from the user list, no admin group (issue 1253, review 1)
occ's user:info says 'user not found' on stdout with an empty stderr, so the
error read as 'occ produced no output' and mesh-admin was never made on novox:
occ now says stderr and stdout both, and existence is read from user:list.
The repair could reset whatever account the user setting names, the
operator's included, and the 'admin' fallback is a hand-made install's human
administrator: only mesh-admin is repaired now, and it is the default. The
shares poll needs no admin rights, so the account gets no group. After a
repair the throttle of the caller's address is reset, and a failing repair
reaches the back-off's one loud line with occ's message.
2026-10-25 16:54:51 +01:00
jschoubben 67b13c42d3 nextcloud: repair the account on a throttle too, since a throttle follows refusals (issue 1253)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 67b13c42d32a on f27f4c8cd58c
Right after the delivery the first answer may be Nextcloud's 429, not a 401;
without a repair then the account would never be made and the back-off would
wait out a throttle that cannot end.
2026-10-25 16:40:54 +01:00
jschoubben 4021724ca4 nextcloud: make the mesh's own account match its secret when Nextcloud refuses it, and back off a refusal that stays (issue 1253)
The image creates NEXTCLOUD_ADMIN_USER only on a fresh install. novox's
instance was installed before the mesh adopted it, so mesh-admin was never
created there, and the shares poll asked Nextcloud every minute with a login
that cannot pass: 401, then Nextcloud's brute-force throttle (429), which
also held the backup before a delivery.

On a refused login the client now adds the account through occ (admin
group), or sets its password to the module's own secret, passing it by
OC_PASS so it is on no argv, and asks once more. A refusal that stays is
said once and the poll backs off from 5 minutes to an hour instead of nine
lines of XML every minute. An empty setting no longer becomes an empty name.
2026-10-25 16:40:01 +01:00
jschoubben f27f4c8cd5 Merge pull request 'mesh-build-go: a package that fails without an assertion carries its real cause, never "PASS" (issue 1901)' (#477) from fix/1901-package-cause into main
Reviewed-on: #477
2026-10-25 15:07:40 +00:00
jschoubben 155124b59a mesh-build-go: a cause is the runtime's line at column 0, and only go's exact summary lines are dropped (issue 1901, review round 1)
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/delivery-group group fix/1901-package-cause held: a member is held: novox/mesh-catalog@155124b59af4 (merged without a passing check: only a person decides…
mesh/check pass 155124b59af4 on f27f4c8cd58c | unit 2015 run, 2883 kept, 5 skipped | integration 2037 run, 2978 kept, 9 skipped | over bound: unit, in…
An indented log line of another running test that held 'panic:' won over the
real timeout panic, since the lines were trimmed before matching. Go prints
the runtime's lines at column 0; a test's log is indented.
2026-10-25 15:58:07 +01:00
jschoubben 6ec6fdaad2 mesh-build-go: a race the race detector reports is a failure's cause, in a test or after PASS (issue 1901)
mesh-controller's try was worded 'PASS' again because the race detector
reported after the tests passed and the program exited 66. The cause is now
the race: its access, the frame that made it and where its goroutine was
created, instead of PASS or testing's generic 'race detected during execution
of test'. Tests from captured go test -race -json (1901/AC4).
2026-10-25 15:52:20 +01:00
jschoubben 663f3de975 mesh-build-go: a package that fails without an assertion carries its real cause, never "PASS" (issue 1901)
A try of mesh-controller kept 'PASS' as its only failure: the package's test
program stopped after printing PASS and the package-level failure took its
first line. Now a failure that is not one test's assertion names the tests that
were running and carries the panic line (a timeout's with its running tests),
a signal or fatal error line, a build's first error, or words saying the
program exited during which tests or after they passed; go test's summary lines
are never kept. Tests from captured go test -json of eight real failures
(1901/AC4).
2026-10-25 15:38:28 +01:00
jschoubben 68f7431d98 Merge pull request 'The provisioner client fills every module again when the socket returns and when its node's own composition moves (issue 1944)' (#476) from fix/r6-node-composition into main
Reviewed-on: #476
2026-10-25 00:18:25 +00:00
jschoubben 53749833bf Fill every module again when the node-engine's socket returns and when the node's own composition moves, so a file an apply left empty is filled (issue 1945)
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/delivery-group group fix/r6-node-composition held: a member is held: novox/mesh-sdk@10b6e202320f (merged without a passing check: only a person decides wh…
mesh/check checking 53749833bfbd on 2167b95a3699 | novox/mesh-catalog#476 in group fix/r6-node-composition
2026-10-25 02:02:02 +02:00
mesh-tools 2167b95a36 Merge pull request 'R6: the gate keeps only the merge's states; walk, plan and step code and their verbs go (issue 1946)' (#475) from feat/r6 into main 2026-10-24 23:00:25 +00:00
jschoubben d09cbf1523 Ignore the binaries a local go build leaves in three module roots (issue 1943)
mesh/delivery delivering: 0 machine step(s) passed
mesh/delivery-group group feat/r6 merged: every member is merged
mesh/check pass d09cbf152317 on 6fb0ceeef817 | unit 4378 run, 4719 kept, 15 skipped | integration 5060 run, 4917 kept, 23 skipped | over bound: unit, …
2026-10-25 00:02:45 +02:00
jschoubben ac6712cbb5 Pin mesh-sdk to feat/r6 748c564, which carries main's composer (issue 1943) 2026-10-25 00:02:45 +02:00
jschoubben 5b32342ac2 Merge main into feat/r6: keep R6's gate, which already carries #474's catch-up at start, and drop the seat and walk adoption R6 removed
The reads-outside markers, mesh-build-go's compose job and gitea's forge-account fix come in from main.
2026-10-24 23:03:42 +02:00
jschoubben 6fb0ceeef8 Merge pull request 'R4g finish: mesh-build-go's compose job, the reads-outside marker in the digest, merge-check.sh gone (issues 1295, 1621-1623, 1935)' (#473) from feat/r4g-finish into main
Reviewed-on: #473
2026-10-24 20:56:49 +00:00
mesh-tools c651161787 Merge pull request 'gitea's forge-account reads the forge's own host key file before scanning its sshd, so a forge restart no longer refuses every forge account (issue 1939)' (#472) from fix/1939-forge-host-key into main 2026-10-24 20:05:39 +00:00
jschoubben ee216e4e63 Merge pull request 'The gate keeps a merge it hears before it holds its seat and catches source.main up when it does, so a missed merge no longer leaves the checker on an old trunk (issue 1726)' (#474) from fix/1726-trunk-catch-up into main
Reviewed-on: #474
2026-10-24 19:47:47 +00:00
jschoubben b67b5af5e6 Pin the gate to mesh-sdk feat/r6 at d7a9b968 (issue 1946) 2026-10-24 21:18:26 +02:00
jschoubben d700ceca20 Pin the gate to mesh-sdk feat/r6 at 7f9cc0e5 (issue 1946) 2026-10-24 20:43:41 +02:00
jschoubben 3a1017f559 Pin the gate to mesh-sdk feat/r6 at e45bdc08 (issue 1946) 2026-10-24 20:22:07 +02:00
jschoubben a17d0c8e93 The messenger holds and tells what the delivery view says is in motion, not the controller's plans (issue 1946) 2026-10-24 18:25:38 +02:00
jschoubben 9fa6710296 Pin the gate to mesh-sdk feat/r6 at a523a8cb (issue 1946) 2026-10-24 18:18:06 +02:00
jschoubben 05f849979c Match a fix to a delivered revision by what the gate says of its merge; keep #474's source catch-up at start (issues 1946, 1726) 2026-10-24 18:13:40 +02:00
jschoubben 4dee77df23 Pin the gate to the head of mesh-sdk feat/r6 (issue 1946) 2026-10-24 17:54:30 +02:00
jschoubben 8a79877a15 A newer delivered revision covers the older merges of its repository, as the forge says ancestry (issue 1946) 2026-10-24 17:03:20 +02:00
jschoubben 2139e83a8a Remove a binary built by mistake 2026-10-24 16:58:33 +02:00
jschoubben e7ca8f7af1 The backup module says on the bus when a backup asked for now has ended, so no one polls backed-up (issue 1944, ADR 0539 §16) 2026-10-24 16:58:19 +02:00
jschoubben eb79996336 The tracker derives a fix's delivered from the delivery view's event, not the gate's gone states (issue 1946) 2026-10-24 16:50:50 +02:00
jschoubben 1a271a1fb1 The group's integration test hears the forge's merges once both members are handed (issue 1946, R6-C3) 2026-10-24 16:43:48 +02:00
jschoubben 30d04f6bef Pin the gate to mesh-sdk's release 6 verb tables and prove it holds the seat as they have it (issue 1946, R6-C3)
The integration tests hear the forge's own word that a pull request merged, as the real flow does, and fail on any call of the controller.
2026-10-24 16:35:15 +02:00
jschoubben 5ff337e0dc Gate keeps the merge's six states and asks the controller nothing (issue 1946, R6-C1, C2, C4)
Release 6 deletes the plans: the walk half of the holder, the controller port, the carry-over of the former module and the verbs times, what-if, release, stop and retire-history go; the table holds proposed, ready, rejected, merged, held and closed; the delivery's status and view name the source revision the merge moved.
2026-10-24 16:26:36 +02:00
jschoubben 2fabbe556e Pin to the group's new mesh-sdk head bfc3883a, whose composer reads a fallback to a predefined key as the indexer does (ADR 0348 §7)
mesh/delivery delivered
mesh/delivery-group group feat/r4g-finish merged: every member is merged
mesh/check checking 2fabbe556e73 on 6fb0ceeef817 | novox/mesh-catalog#473 in group feat/r4g-finish
2026-10-24 14:57:35 +02:00
jschoubben 89b9212c4d Pin to the group's new mesh-sdk head 19c1d359, whose composer names the reader's pinned SDK instead of a bare (devel) (ADR 0482 clause 5)
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: shanks: novox/mesh-catalog#473: modules/mesh-build-go/go.mod names mesh-sdk 19c1d35939bc, which is neither on mesh-sdk's trunk nor …
2026-10-24 14:12:09 +02:00
jschoubben 40da368ee3 Date an adopted merge once, by the walk it was adopted from, so a later walk it follows never makes it look newer than a forge merge heard after it (issue 1726, review)
mesh/delivery delivered
mesh/check pass 40da368ee34e on ee216e4e63f1 | unit 246 run, 4189 kept | integration 228 run, 4249 kept | over bound: unit, integration (slowest unit …
2026-10-24 13:55:10 +02:00
jschoubben 865f3b6455 Say what pacman-tools reads outside: the root predefined.json (issue 1935)
mesh/delivery-group group feat/r4g-finish rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: shanks: novox/mesh-catalog#473: modules/mesh-build-go/go.mod names mesh-sdk bc0c25e51052, which is neither on mesh-sdk's trunk nor …
2026-10-24 13:48:18 +02:00
jschoubben 03524ecd13 Mark pacman-tools as reading outside its directory: its upgrade-order test reads the root predefined.json, which its digest does not cover (issue 1935) 2026-10-24 13:48:09 +02:00
jschoubben 15101c9ce6 Move source.main for a merge the gate caught up after a gap, so the checker re-checks open heads on the trunk that is (issue 1726)
The checker takes a repository's trunk head only from the gate's source.main. On 2026-10-24 gitea's
restarted poll announced mesh-catalog #471's merge while the gate was starting and could not yet tell
whether it held the seat; the gate acknowledged the event unread. It then adopted the controller's walk
of the merge commit, which carries no merge time, so every older merge of the repository looked newer
and source.main was never moved: #472 was checked on the trunk before #471 and its merge refused.

- An event heard before the gate can tell whether it holds the seat is offered again, not taken.
- An adopted merge is dated by its walk's creation, so it settles into source.main as a forge merge does.
- Each time the gate comes to hold the seat it weighs the newest merge of each repository against
  source.main once more, so a merge marked there and never written (as #471) is written; never backwards.
2026-10-24 13:41:56 +02:00
jschoubben f910b0ceb8 Pin mesh-build-go to the group's mesh-sdk head after main's merge, so it builds on the SDK the group delivers 2026-10-24 13:28:16 +02:00
jschoubben 91c3e954ae Merge main into feat/r4g-finish (#471) 2026-10-24 13:26:24 +02:00
jschoubben 545c72bb59 Merge pull request 'gitea's watcher answers its handshake without waiting for the forge, so a slow forge at a tool-runner restart no longer kills its tools and events (issue 1726)' (#471) from fix/1726-gitea-handshake-not-on-forge into main
Reviewed-on: #471
2026-10-24 11:10:39 +00:00
jschoubben 7afcd0282d Set up the forge's webhook without holding the bundle's import, so a slow forge never costs gitea its handshake and its tools (issue 1726)
At 12:37 on 2026-10-24 node-tools on novox was replaced; gitea's watcher bundle awaited the webhook's set-up on the forge's admin API at the top of its import, outlasted the 10 s handshake and was killed, never to start again. gitea_commit_status, gitea_merge_pull_request and gitea_events were then served nowhere: the forge bridge's calls found no responders and no new pull request was announced.
2026-10-24 13:00:57 +02:00
jschoubben 75156fce00 gitea: forge-account reads the forge's own host key file before scanning its sshd (issue 1939)
mesh/delivery delivered
mesh/check pass 75156fce007e on c65116178720 | unit 246 run, 4192 kept | integration 228 run, 4252 kept | over bound: unit, integration (slowest unit …
2026-10-24 12:50:20 +02:00
jschoubben 16d7b1898d Mark the packages whose tests read files outside their directory, found by the review (issue 1935) 2026-10-24 04:38:49 +02:00
jschoubben f04465537b Say in realtree_test that a missing trunk fails, not skips (issue 1295) 2026-10-24 04:37:52 +02:00
jschoubben e870a7f65f Pin to the finish group's mesh-sdk head dd0d156b, so every member names one SDK (issue 1622) 2026-10-24 04:28:14 +02:00
jschoubben e8a59171c7 Merge remote-tracking branch 'origin/feat/r4g-finish-composer' into HEAD 2026-10-24 04:26:03 +02:00
jschoubben ebb25757a2 Merge remote-tracking branch 'origin/feat/r4g-finish-1935' into HEAD 2026-10-24 04:26:02 +02:00
jschoubben 6a70735489 Run a compose ask on the Go build seat: build mesh-sdk's composer at the asked commit once, fill its manifests from the indexer and run it shut off (issue 1622) 2026-10-24 04:14:21 +02:00
jschoubben a715c8821f mesh-build-go: a catalogue module's non-Go files enter its packages' digests; markers where tests read other modules (issue 1935) 2026-10-24 03:59:08 +02:00
jschoubben b6ed8599c8 mesh-build-go: a package marked reads-outside is never kept, so tests that read files outside their directory run on every check (issue 1935) 2026-10-24 03:46:45 +02:00
jschoubben ae6f7d5b54 Make a missing trunk copy a failure again, now the checker places it (issue 1295) 2026-10-24 03:43:49 +02:00
jschoubben 25176e1ccd Merge pull request 'mesh-build-go: lint the unit level's directories as many at once as its tests, so a large repository's check ends within its bound (issue 1930)' (#470) from fix/1930-parallel-lint into main
Reviewed-on: #470
2026-10-24 00:16:55 +00:00
jschoubben 7e1f3de77a mesh-build-go: lint directories side by side like the tests, so a large repository's check stays inside its bound (issue 1930)
mesh/delivery delivered
mesh/check failed: mesh-build-go: the check of novox/mesh-catalog#470: the ask mvlnph6fd84a7b's step on shanks did not end within 30m0s of its start |…
2026-10-24 02:01:36 +02:00
jschoubben 4b1bed4b9d Merge pull request 'mesh-mail-reader: read messenger's sent_on as a list, so an unchanged set of mails is signalled once, not every 160 s (issue 1926)' (#469) from fix/1926-mail-signal-once into main
Reviewed-on: #469
2026-10-23 22:36:10 +00:00
jschoubben 25d69406b7 Merge pull request 'mesh-build-go: a failed test keeps its last lines and its cause, not its first fifteen (issue 1901, with mesh-delivery)' (#466) from fix/1901-rebased into main
Reviewed-on: #466
2026-10-23 22:35:57 +00:00
jschoubben f836c021e1 Merge pull request 'gitea: the checker's owed results are taken at once and written in the background, so the runner keeps the watch and every mesh/check reaches the forge (issue 1726)' (#468) from fix/1726-owed-watch-not-blocked into main
Reviewed-on: #468
2026-10-23 22:35:33 +00:00
jschoubben 450f0bbe79 Merge pull request 'The build modules pin mesh-sdk's list-form level digest, so a check of a repository with a package named token ends (issue 1930, with mesh-sdk and mesh-delivery)' (#467) from fix/1930-digest-shape into main
Reviewed-on: #467
2026-10-23 22:08:24 +00:00
jschoubben 5a7076a6a2 Take the checker's owed without waiting on the forge, so a large owed never times its watch out (issue 1726)
mesh/delivery delivered
mesh/check checking 5a7076a6a25e on f836c021e14f
The runtime gives a watch's current values 10 s and waits for each to be taken; a forge write per key made ~200 keys outlast it at every attempt, so after node-tools restarted on novox at 23:25:58 the watch never held and no verdict reached mesh/check. Writes now run in the background, a few at a time, each key's in revision order.
2026-10-23 23:52:52 +02:00
jschoubben 30b22c198e Pin the build modules to mesh-sdk 403a5d7, whose level digest is a list, so a check of a repository with a package named token ends (issue 1930)
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792875397682979170 (novox/mesh-catalog at 6fb0ceee into main,…
mesh/delivery-group group fix/1930-digest-shape merged: every member is merged
mesh/check checking 30b22c198e6a on 450f0bbe7984 | novox/mesh-catalog#467 in group fix/1930-digest-shape
2026-10-23 23:36:31 +02:00
jschoubben 753e8fc202 mesh-mail-reader: read tell's sent_on as the list it is, so a told set is not sent again; leave past-due and duplicate mails out (issue 1926)
mesh/delivery delivered
mesh/check checking 753e8fc202c3 on 4b1bed4b9d5b
2026-10-23 23:27:23 +02:00
mesh-tools e5202445ba Merge pull request 'mesh-build-go: build_go_test takes packages as a typed list too, so checks run again (issue 1925)' (#465) from fix/1925-packages-shape into main 2026-10-23 20:44:09 +00:00
jschoubben dcae699323 mesh-build-go: a clipped failure starts with its cause, not the marker, for an older checker (issue 1901/AC3)
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@753e8fc202c3 (merged as 4b1bed4b into main, walk plan-17927949853…
mesh/delivery-group group fix/1901-rebased merged: every member is merged
mesh/check checking dcae699323c8 on 25d69406b740 | novox/mesh-catalog#466 in group fix/1901-rebased
2026-10-23 22:26:29 +02:00
jschoubben 70ef99595a mesh-build-go: build_go_test takes packages as a typed list too (issue 1925)
mesh/delivery delivered
mesh/check failed: shanks: the check of novox/mesh-catalog#465 failed: the unit level: packages is a list of package patterns | 70ef99595aa9 on e52024…
2026-10-23 22:21:41 +02:00
jschoubben 746eb885a3 Test 1901/AC3: an older pair of build module and checker still agree (issue 1901) 2026-10-23 22:14:28 +02:00
jschoubben 2cdb9b8fc4 mesh-build-go: a failed test keeps its last lines and its error line, not its first fifteen (issue 1901) 2026-10-23 22:13:41 +02:00
jschoubben e830045545 Merge pull request 'R4g+R5 (mesh-catalog): the build modules pull asks, scripts become tests, build-agent references go (design 47)' (#464) from feat/r4g into main
Reviewed-on: #464
2026-10-23 17:49:43 +00:00
jschoubben 454fb8a918 Write a pull request's check verdict into its key after the key is created: the status event can be taken first (R4g, issue 1887)
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792777718506798022 (novox/mesh-catalog at e8300455 into main,…
mesh/delivery-group group feat/r4g merged: every member is merged
mesh/check checking 454fb8a918ad on 3bec357968da | novox/mesh-catalog#464 in group feat/r4g
2026-10-23 19:42:54 +02:00
jschoubben c32474ee40 Judge the delivery-before-check order from the step records, not a poll: at capacity 1 the check starts the instant the delivery ends (R4g, issue 1887)
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: integration: mesh-delivery-gate/cmd/mesh-d… | c32474ee40f4 on 3bec357968da | judged by the candidate's mesh-build-go at 2e667ac15ce7
2026-10-23 19:19:45 +02:00
jschoubben f95f3f430e Give the delivery-ask integration test a bus of its own: the level shares one bus across directories, so a named stream there could be taken by another test (R4g, issue 1887) 2026-10-23 19:15:52 +02:00
jschoubben 0f768bc97b Skip the trunk comparison, saying NOT COMPARED, when the catalogue's own copy is not beside: the checker before R4g places none, so R4g's own check could never pass (issue 1295)
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: integration: meshbuildgo/cmd/mesh-build-go… | 0f768bc97b65 on 3bec357968da | judged by the candidate's mesh-build-go at 4046f8f8652e
2026-10-23 17:38:28 +02:00
jschoubben 721db20887 Take the committed 10 MB repositories binary back out of the gitea module and ignore it (R4g review)
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-catalog/chec… | 721db208876d on 3bec357968da | judged by the candidate's mesh-build-go at ead411584e58
2026-10-23 17:22:20 +02:00
jschoubben 1fc7b84ad2 Merge remote-tracking branch 'origin/feat/r4g' into feat/r4g
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-catalog/chec… | 1fc7b84ad2a0 on 3bec357968da | judged by the candidate's mesh-build-go at b3c53883f575
2026-10-23 17:20:26 +02:00
jschoubben 70b16dc594 Merge origin/main (keep-main-rules) and fix/forge-requires-no-status (ADR 0534): no required status anywhere, and with keep-main-rules false the program takes away a required status once and changes nothing else 2026-10-23 17:20:19 +02:00
jschoubben d6b9cef9d0 Prove a check ask's digest is the digest tool's answer in the call, so the checker keys kept verdicts by it (issue 1917)
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-catalog/chec… | d6b9cef9d005 on 3bec357968da | judged by the candidate's mesh-build-go at 769f9d906be2
2026-10-23 17:19:38 +02:00
jschoubben b85d65b53f Merge remote-tracking branch 'origin/main' into feat/r4g
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-catalog/chec… | b85d65b53f9e on 3bec357968da | judged by the candidate's mesh-build-go at f4ff9305030a
2026-10-23 17:18:51 +02:00
jschoubben 3bec357968 Merge pull request 'The forge module changes no repository's branch protection unless keep-main-rules is true (issue 1903)' (#463) from fix/gitea-keep-main-rules into main
Reviewed-on: #463
2026-10-23 15:14:21 +00:00
jschoubben b60077850d Pin mesh-sdk at feat/r4g's head a2c813a6, its test fixture fix, with go.sum tidied
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-catalog/chec… | b60077850dfb on 3bec357968da | judged by the candidate's mesh-build-go at 47bcc1b5810e
2026-10-23 17:06:11 +02:00
jschoubben a00962871c Give checks/trunk a full module path, so go mod verify in the checks that use it treats it as the local module it is (R4g group check)
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: g14: novox/mesh-catalog#464: checks/health/go.mod names mesh-sdk a9560890ee26, which is neither on mesh-sdk's trunk nor the group's…
2026-10-23 17:04:08 +02:00
jschoubben 90ff4c485a Pin checks/health to the mesh-sdk that knows the seat node-build-go, so its real-tree test reads every manifest
mesh/delivery-group group feat/r4g rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: g14: fetch of novox/mesh-catalog#464: build_go_fetch on g14: the step g14.mvl3o921790ccf failed: go mod verify in checks/config: ch…
2026-10-23 16:58:14 +02:00
jschoubben f08abb90d4 Pin the build modules to the mesh-sdk head whose puller says a poisoned ask and bounds a step from its start (issues 1907, 1887 review round 2) 2026-10-23 16:52:19 +02:00
jschoubben a493c90c0d Require node-tools in the three build modules, so the delivery plan puts the tool runner they pull asks through before them on every node (issue 1911) 2026-10-23 16:50:03 +02:00
jschoubben a1febd3ad8 Say in gitea's comments that no controller status is written since R4g, so the merge waits on mesh/check alone (issue 1887, review round 2 item 6) 2026-10-23 16:47:57 +02:00
jschoubben 14b7158fc0 Run gofmt and go vet before the Go unit level's tests and give their time in its result, so a lint failure ends the level at once and the check's line can say what it cost (issue 1887 D7-go, review round 2 item 5; ADR 0529 §1) 2026-10-23 16:44:22 +02:00
jschoubben 0066b702f4 Merge remote-tracking branch 'origin/main' into feat/r4g 2026-10-23 16:30:55 +02:00
jschoubben 281909e83d Keep nats on main's mesh-sdk pin: only its test subjects change in R4g, so the bus does not move (R4g) 2026-10-23 16:27:06 +02:00
jschoubben d0a3a8ebc6 Record the checksums of mesh-sdk feat/r4g 9205222a for the group's modules (R4g) 2026-10-23 16:26:11 +02:00
jschoubben 2246ccc492 Pin mesh-sdk at feat/r4g's head 9205222a, the merge of lanes A, B, S8 and F1, so every module r4g touches builds against one SDK
go.sum gains its lines from go mod tidy once mesh-sdk feat/r4g is pushed.
2026-10-23 16:22:02 +02:00
jschoubben 93f4f67a8f Merge branch 'feat/r4g-asks' into feat/r4g 2026-10-23 16:20:28 +02:00
jschoubben 78499235b6 Merge branch 'feat/r4g-catalog' into feat/r4g 2026-10-23 16:20:28 +02:00
jschoubben ecd60fe074 Run gofmt and go vet with every build tag in each directory the Go unit level runs, so what the repositories' scripts checked is every repository's check (issue 1887 D7-go, ADR 0529 §1) 2026-10-23 16:16:37 +02:00
jschoubben dd74d4adee Restore the doc wording of the repositories program after dropping E1a (review round 1) 2026-10-23 16:10:19 +02:00
jschoubben fa39738981 Revert "Swap hq's repo-check-alone rule to mesh/check like every main: nothing writes repo-check once build-agent goes (issue 1296, E1a)"
This reverts commit e4ec060ac7.
2026-10-23 16:10:09 +02:00
jschoubben 612486be18 Remove the committed compiled test binary checks/words/words.test and ignore *.test (review round 1) 2026-10-23 16:10:00 +02:00
jschoubben 3c5ff051de Pin mesh-sdk at feat/r4g's head c3915ffb, the merge of lane B's layouts, so every r4g module builds against one SDK
go.sum gains its lines from go mod tidy once mesh-sdk feat/r4g is pushed.
2026-10-23 16:01:15 +02:00
jschoubben 17e9efa101 Merge branch 'feat/r4g-catalog' into feat/r4g 2026-10-23 15:59:59 +02:00
jschoubben 3111546ff1 Ask the Node build seat a check with its levels, as the SDK's check job now carries them (issue 1887 A3node) 2026-10-23 15:59:31 +02:00
jschoubben 68dfca1057 Gate the token mint's main-rule write by keep-main-rules too
mesh/delivery delivered
mesh/check pass 68dfca105794 on 3bec357968da | unit 4443 run, 0 kept, 15 skipped | integration 4483 run, 0 kept, 23 skipped | over bound: unit, integr…
The operator, 2026-10-23: "disable the gitea branch protection reset thing". setUpToolsAccount (run at every token mint) called ensureMainMerges regardless; it now does so only when the setting is true.
2026-10-23 15:57:44 +02:00
jschoubben 77f33d317f Keep main's rules only when the setting keep-main-rules is true (default false)
The operator, 2026-10-23: "disable the gitea branch protection reset thing". The 10-minute tick and the start-time
ensureMainMerges, and the repositories sync's changes to existing repositories' main rules, now run only when the new
setting is true; new repositories are still made. The sync says in its outcome that rules were left.
2026-10-23 15:53:30 +02:00
jschoubben 2175b26c56 Merge pull request 'Move the gate's source.main for a merge heard only from the forge, so the checker judges on the trunk that is (issue 888)' (#461) from fix/888-forge-merge-moves-source into main
Reviewed-on: #461
2026-10-23 13:49:17 +00:00
jschoubben e21ccf1138 Merge pull request 'Checks under 5 minutes (mesh-build-go): siblings only in the digests that read them, selected dirs in a pool, a mirror and verify once, and a change judged by its own build module (issues 1896, 890, 1898; ADR 0535, 0536)' (#460) from feat/fastcheck into main
Reviewed-on: #460
2026-10-23 13:49:17 +00:00
jschoubben 08442b95cb Merge pull request 'Betterbird leaves the catalogue: the operator removed it from both desktops (issue 806)' (#462) from fix/806-betterbird-leaves-the-catalogue into main
Reviewed-on: #462
2026-10-23 13:49:16 +00:00
jschoubben c304d3fb97 Pull the image build seat's asks beside the tools, at most the node's capacity (issue 1887 A3image, ADR 0525 §2-§6) 2026-10-23 15:40:43 +02:00
jschoubben 8356d30e1b Pull the Node build seat's asks beside the tools, delivery first and at most the node's capacity (issue 1887 A3node, ADR 0525 §2-§6) 2026-10-23 15:39:30 +02:00
jschoubben 6e823d1f9b Serve the Go build seat's verbs from the SDK's one form (issue 1887 A3go) 2026-10-23 15:38:19 +02:00
jschoubben d0ba022d20 Stop hearing the retired build seats' events in gitea and the catalogue module, so no manifest consumes a seat nothing claims once node-build-agent goes (issue 1887 E2 with A9) 2026-10-23 15:37:17 +02:00
jschoubben 6bfdbc5758 Take the build agent and the toolchain images out of the catalogue's prose, and refuse the word where it comes back outside the one module still naming it (issue 1295, E6) 2026-10-23 15:34:11 +02:00
jschoubben c4fc565180 Read the trunk's copy only from MESH_CHECK_BESIDE/mesh-catalog: unset skips the comparison saying NOT COMPARED, set without the copy fails (issue 1293, E5a) 2026-10-23 15:34:11 +02:00
jschoubben db937546ae Pull the Go build seat's asks beside the tools, delivery first and at most the node's capacity, so no part chooses a node for a Go build or check (issue 1887 A3go, ADR 0525 §2-§6) 2026-10-23 15:33:24 +02:00
jschoubben 2c32728395 Delete merge-check.sh and merge-check-typescript.sh: every step they ran is a Go test or the build module's; a test refuses their return (issues 1295 and 1293, E5c) 2026-10-23 15:31:10 +02:00
jschoubben 20ea65f0ac Count the long-running resources without health in a Go test over the real catalogue, through mesh-sdk's catalogue package (issue 1295, E5b) 2026-10-23 15:31:10 +02:00
jschoubben df9e3bd7e2 Run each checks/<x> over the real tree as a Go test, the shrink-only lists against the trunk's copy; the words check within the unit bound (issue 1293, E5a) 2026-10-23 15:28:39 +02:00
jschoubben 643de08882 Make the delivery gate ask the controller nothing of checks: no Check or Queue on the port, no checked event, no lost-check sweep, only mesh/check and the checker's verdict (issue 1318, E4a and E4b) 2026-10-23 15:24:13 +02:00
jschoubben a252aae862 Make the build modules and the delivery parts core issue owners and drop build-agent (issue 1041, E3) 2026-10-23 15:12:21 +02:00
jschoubben 2038943c3e Write no merge-check.sh in a new repository's first commit and announce none: tests run by convention (issue 1296, E1b) 2026-10-23 15:11:37 +02:00
jschoubben e4ec060ac7 Swap hq's repo-check-alone rule to mesh/check like every main: nothing writes repo-check once build-agent goes (issue 1296, E1a) 2026-10-23 15:06:29 +02:00
jschoubben ae8ab366a8 Remove the betterbird module: the operator unassigned it from g14 and shanks (issue 806, ADR 0379)
mesh/delivery failed: its walk failed, carrying its own commit: a gate on a first machine (what it carried put back), a build, a machine
mesh/check failed: integration: nats-tools/cmd/mesh-nats-snapshot.TestTheDeclaredDumpRunsInTheBussImage: image_test.go:141: docker run -d --rm --name …
2026-10-23 15:00:36 +02:00
jschoubben e9ce56e838 Say in the test why the waits are 20 s: load, not a retry (issue 1404)
mesh/delivery delivered
mesh/delivery-group group feat/fastcheck merged: every member is merged
mesh/check checking e9ce56e83861 on e21ccf113842 | novox/mesh-catalog#460 in group feat/fastcheck
2026-10-23 14:58:14 +02:00
jschoubben ee176b76ea Wait 20 s, not 5 s, for each step of the gate's path over the bus: a loaded build node failed the ask wait twice (issue 1404)
mesh/delivery superseded: a newer head of the same pull request
mesh/check checking ee176b76ea3c on 89e15c7bcda6 | novox/mesh-catalog#460 in group feat/fastcheck
2026-10-23 14:56:04 +02:00
jschoubben cf071270a0 Skip the docker tests where the daemon is unreachable, not only where the binary is missing
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: integration: mesh-delivery-gate/cmd/mesh-delivery-gate.TestAPassingVerdictAsksTheApproveAndItsApproveMergesWithinSecondsOverARealBu…
The checker's integration level runs shut off, so docker is found but its daemon
is not, and TestTheDeclaredDumpRunsInTheBussImage failed the group check on g14
(issue 1823). MESH_REQUIRE_DOCKER=1 still fails.
2026-10-23 14:41:00 +02:00
jschoubben 8bc54a3e27 Merge remote-tracking branch 'origin/main' into fix/888-forge-merge-moves-source
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792763407391518822 (novox/mesh-catalog at 2175b26c into main,…
mesh/check failed: integration: nats-tools/cmd/mesh-nats-snapshot.TestTheDeclaredDumpRunsInTheBussImage: image_test.go:141: docker run -d --rm --name …
2026-10-23 14:17:34 +02:00
jschoubben 29f4a366c2 Pin mesh-build-go to mesh-sdk 2248b985, the group's final SDK head with the 0.1.16 version (issues 890, 1899)
mesh/delivery superseded: a newer head of the same pull request
mesh/delivery-group group feat/fastcheck rejected: composed together: the checker's verdict at the members' heads (a5ea6d8de38e): integration: nats-tools/cmd/m…
mesh/check failed: integration: nats-tools/cmd/mesh-nats-snapshot.TestTheDeclaredDumpRunsInTheBussImage: image_test.go:141: docker run -d --rm --name …
2026-10-23 14:16:40 +02:00
jschoubben 7ec760bca2 Compile the workspace's own mesh-build-go and run digest and test as it under runner workspace, so a change to this module is judged by its candidate (issue 1898 K1, K2)
The checker asks runner workspace when a merge changes modules/mesh-build-go; the step's
shut-off process now takes a step spec (digest, unit, integration) and every answer says
judged_by. ADR 0535 §1, §3, §5.
2026-10-23 14:15:22 +02:00
jschoubben 22f7a78503 Dissociate a workspace from the mirror after the fetch, so a shut-off step finds every object in the tree (issue 890 C2 follow-up) 2026-10-23 14:09:34 +02:00
jschoubben 3ff6a8b502 Download the go.mod directories side by side and verify a go.mod and go.sum pair once, so a fetch spends no minutes in serial go mod steps (issue 890, ADR 0536 s2) 2026-10-23 14:08:20 +02:00
jschoubben 24cbc8961e Fetch a repository and its siblings through a bare mirror in the cache, updated under a lock, so a check transfers only new objects (issue 890, ADR 0536 s2) 2026-10-23 14:06:42 +02:00
jschoubben 547f4ee27d Compile and run only the go.mod directories a selection lies in, and run them side by side up to a bound (issue 890, ADR 0536 s3) 2026-10-23 14:04:43 +02:00
jschoubben 47dbf87e33 Fold the siblings into a package digest only when its tests reach MESH_CHECK_BESIDE, so a sibling move keeps unrelated verdicts (issue 1896) 2026-10-23 14:02:59 +02:00
jschoubben 15378cfba7 Merge pull request 'The build modules refuse every step and report unhealthy under a 20 GiB free-space floor, release workspaces, and sweep after 2 h (issues 1839 and 1894, ADR 0532)' (#458) from fix/1839-build-floor into main
Reviewed-on: #458
2026-10-23 11:29:04 +00:00
jschoubben 539a31103b mesh-build-image: let the module's own guard decide whether the real-buildah tests run, so a check sandbox where rootless buildah cannot unshare skips them loudly instead of failing every catalogue check (issue 1894)
mesh/delivery delivered
mesh/delivery-group group fix/1839-build-floor merged: every member is merged
mesh/check failed: g14: the check ran past its bound of 30m0s, stuck at: test the integration level of novox/mesh-catalog#458 on g14 | 539a31103b53 on…
2026-10-23 12:38:09 +02:00
jschoubben dfcdf234c5 Never move source.main back for a forge-heard merge, by dating the head it holds (issue 888, review round 1)
A merge the gate handed is known by the bridge's word before the forge's, and finish wrote it already; an older forge-only
merge heard in between overwrote it, since only the forge's word was dated and the write never compared with the head held.
A merge is now dated by either word, and the write leaves a head that is a later merge, or one the gate cannot date while
the merge is older than the records it keeps (a pruned newer merge).
2026-10-23 12:36:58 +02:00
jschoubben 5bc9578634 Move source.main for a merge heard only from the forge, so the checker judges on the trunk that is (issue 888)
The checker reads the trunk only from the gate's source.main, and the gate moved it only in finish, for a merge it handed to
the forge bridge on an Approve. A pull request merged at the forge directly moved every other delivery's trunk (trunkMoved)
and left source.main behind, so the checker kept judging candidates on the trunk before it (catalogue 89e15c7b after
6c0024e4, 2026-10-23).

settleForgeMerges now writes such a merge into source.main by compare-and-set, never a merge replayed as history or older
than one of its repository already heard; an approved delivery merged at the forge before the gate handed it is moved by
finish with its group, once.
2026-10-23 12:23:40 +02:00
jschoubben 3ca48ff230 Refuse build steps under a free-space floor and release workspaces, so the build modules stop filling a build node's disk (issue 1839)
mesh/delivery-group group fix/1839-build-floor checking: 1 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: novox: the check ran past its bound of 30m0s, stuck at: test the unit level of novox/mesh-catalog#458 on novox | 3ca48ff23005 on 89…
mesh-build-go, mesh-build-node and mesh-build-image refuse every fetch, test
and build, and say unhealthy, while their work or cache filesystem has less
than 20 GiB free (build-agent's floor), so the builder chooses another node
instead of filling this one. Each serves build_<x>_release, removing a
workspace once its caller's last step answered; a failed fetch removes its
own; a workspace never released goes two hours after its last use (ADR 0509)
in place of a day, never while a step holds it. Pins mesh-sdk's
fix/1839-build-floor (buildstep.Floor, buildstep.Workspaces).
2026-10-23 12:17:58 +02:00
jschoubben 79777d0f78 gitea: keep the operator's own login on main's merge whitelist beside mesh-tools (ADR 0534 §2)
The operator merges by hand at the forge when the mesh's own path is stuck,
without changing a rule. The logins come from the trusted config value
main-mergers, merged into the runtime config both programs read.
2026-10-23 12:10:10 +02:00
jschoubben d2036f2a63 gitea: require no status on main, judge mesh/check in the merge tool instead (ADR 0534)
The forge's required status was a second copy of the gate mesh-delivery and
the merge tool already keep; a stuck status writer held it too (issues 1886,
1891) and only a hand change at the forge let a fix merge. Main keeps no push,
the tools' merge whitelist and no administrator override. The merge tool
judges mesh/check whether or not the branch requires it, and on a branch that
requires none merges only a head the operator's Approve names. The protection
tool loses its status argument.
2026-10-23 12:01:57 +02:00
jschoubben 6c0024e43b Merge pull request 'Make the catalogue's tests pass in the build sandbox, and give the integration level the installed Node toolchain read-only (issue 1891, ADR 0533)' (#459) from fix/logrotate-base-parses into main
Reviewed-on: #459
2026-10-23 09:48:22 +00:00
jschoubben a2e5a20f52 mesh-build-node: write the install's registry credential in the account's home, never beneath the opened cache, and refuse to open a toolchain holding one (issue 1891 review, ADR 0533 §2)
mesh/delivery failed: its walk failed, carrying its own commit: a gate on a first machine (what it carried put back), a build, a machine
mesh/check failed: integration: meshbuildnode/cmd/mesh-build-node.TestAFixtureBundleBuiltTwiceHasOneDigest: build_integration_test.go:71: no toolchain…
2026-10-23 11:45:20 +02:00
jschoubben 845479d7cd mesh-build-node: prove the npm download cache beside the toolchains stays its owner's (ADR 0533 §2) 2026-10-23 11:30:26 +02:00
jschoubben 2ebcb18097 realMachine: a change carrying a lock no release has installed runs with the toolchain installed last, and says so, so a lock change is not refused by its own check (issue 1891, ADR 0533 §3) 2026-10-23 11:23:11 +02:00
jschoubben 00e54d46d7 Give the integration level the node's installed Node toolchains read-only, so the Node build module's tests build offline (issue 1891, ADR 0533)
mesh-build-go binds /var/lib/mesh-build-node-cache/toolchain read-only into the
integration level's one shut-off process when it is there; the unit level never.
mesh-build-node opens its installed toolchains to other accounts (folders 0755,
files 0644, writable by its owner alone; its cache 0711, so the npm cache beside
them stays its own), and realMachine copies the toolchain of its own lock from
there instead of npm ci from the registry, which the level cannot reach.
2026-10-23 11:22:05 +02:00
jschoubben 933370df7c gitea, minio: compile the sources beside themselves in the test script, as the old check did (issue 1891)
The resolve hook needed the typescript package, which mesh-build-node keeps
out of the workspace (notCopied): tsc is reached only as the bin/tsc
command. So the test scripts now do what merge-check-typescript.sh did
before the tests: tsc over the module's sources with --outDir ., so the
sources' ./x.js imports resolve. The emitted .js is ignored by git.
2026-10-23 10:22:41 +02:00
jschoubben 437c524b81 gitea, minio, mosquitto: tests run as the Node build module runs them, tsc -p . then npm test (issue 1891)
The old TypeScript check compiled every source beside itself, and into
dist/, before it ran the tests; mesh-build-node runs `tsc -p .` (these
tsconfigs say noEmit) and then the package's test script, so:

- gitea and minio: the tests import the .ts sources, which import each
  other as ./x.js. A resolve hook in test/ resolves such an import, when no
  .js is there, to the .ts beside it and compiles it with the toolchain's
  TypeScript (gitea's sources use parameter properties, which type
  stripping refuses). 57 of gitea's tests and minio's links test failed.
- mosquitto: its ctrl test imports ../dist/client.js; its test script now
  builds first, as gitea's does.

Test scripts and a test helper only; what is built and delivered is the same.
2026-10-23 10:18:57 +02:00
jschoubben dc96d7c2dd gitea: take a rule's null contexts as the forge's type says, so the strict build the checker runs compiles (issue 1891)
The checker builds a package with a build script by its tsconfig (strict),
where requiredOf and TrunkForge's appliedProtection said
status_check_contexts is string[] and BranchProtection says string[] | null:
four TS2345 errors, hidden on main behind earlier failures. Types only; the
emitted code is the same, and a test holds that null contexts require nothing.
2026-10-23 10:00:09 +02:00
jschoubben f9247342d1 pacman, ssh-client: tests read nothing of the machine, so the unit level passes shut off and on a desk (issue 1891)
pacman-conf proved the shipped pacman.conf through its Include of
/etc/pacman.d/mirrorlist, which the shut-off unit level does not have: the
proven file now reads a mirror list of its own, and a guard test holds that
every repository of the shipped file includes the list reflector writes.

ssh-client's Test case for a run without an agent read the account's
SSH_AUTH_SOCK, so it failed on any desk with an agent: it now sets its own,
and also proves the environment's agent is the one reported.
2026-10-23 09:38:40 +02:00
jschoubben b1d875410a logrotate: dry-run the base against logs of its own, so the shut-off unit level does not stat the machine's /var/log (issue 1891)
The unit level runs in bubblewrap, where /var/log is not there; the base's
/var/log/wtmp block has no missingok, so logrotate -d failed its stat and
every mesh-catalog check that reached this test failed.
2026-10-23 09:10:09 +02:00
mesh-tools 89e15c7bcd Merge pull request 'R4e (mesh-catalog): build modules push and narrow, the gate stops judging, gitea writes mesh/check from the checker's owed state, and the census fixes' (#457) from feat/r4e into main 2026-10-22 22:12:51 +00:00
mesh-tools a21da33d48 Merge pull request 'Make buildah's runtime directory before every buildah run, so mesh-build-image's health does not fail for want of it (issue 1884)' (#456) from fix/1884-runtime-dir into main 2026-10-22 21:48:47 +00:00
jschoubben b53b0e7e2c Make buildah's runtime directory before every buildah run, so a machine without it is not unhealthy forever (issue 1884)
mesh/merge-gate head b53b0e7e2cc4 merged into trunk 48dd41db91a6 — pass: builds mesh-build-image → ace, shanks; no bus step; every machine composes with th…
mesh/repo-check head b53b0e7e2cc4 merged into trunk 48dd41db91a6 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: modules/gitea.test: ✔ the same merge, head or close announced twice has the same id, and another has another (7.92618ms) | b5…
The look made <store>/xdg only after `buildah --version`, which refuses first
when its XDG_RUNTIME_DIR is missing: the directory was never made on ace.
2026-10-22 23:39:51 +02:00
mesh-tools 48dd41db91 Merge pull request 'Fill a module again when a node-engine returns after any failed fill, not only a missing socket (issue 1873)' (#455) from fix/1873-fill-path into main 2026-10-22 21:19:09 +00:00
jschoubben 16331cdfdc Fill a module again when the node-engine's socket returns after any failed fill, not only one for want of the socket (issue 1873)
mesh/merge-gate head 16331cdfdce6 merged into trunk 66c404fd39a5 — pass: builds mesh-provisioner-client → ace, g14, novox, shanks; no bus step; every machi…
mesh/repo-check head 16331cdfdce6 merged into trunk 66c404fd39a5 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: mesh-build-go: no go build node qualifies: builders disagree: go1.27.1-X:nodwarf5 recipe 74744709d277ded9 on g14; go1.27.3-X:nodwar…
On ace postgres's fill of superuser.secret failed at 22:12 with the engine's own
"is a directory", what a container runtime had left at the path. The engine
delivered at 22:42 clears such a directory, but the client tried again only
modules whose fill had failed for want of the socket (issue 1165), so postgres
was never filled again and its old failure was said every 30 s. A newer engine
may write what the older could not: any failed fill is now tried again when a
node-engine makes its socket again.
2026-10-22 22:56:16 +02:00
jschoubben ab3722d3b3 Test that a fill the node-engine failed is tried again when its socket returns (issue 1873) 2026-10-22 22:53:39 +02:00
mesh-tools 66c404fd39 Merge pull request 'Let a run read from a node's pacman log never hold back a new upgrade, so the mesh can upgrade nodes at all (issue 1841)' (#454) from fix/1841-stale-run into main 2026-10-22 20:14:07 +00:00
jschoubben 3ad9d42be9 Name the captured log's machine by its role, not its name (issue 1841)
mesh/merge-gate head 3ad9d42be923 merged into trunk 083515da1be7 — pass: builds pacman → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head 3ad9d42be923 merged into trunk 083515da1be7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: i3/cmd/i3-tools.TestTheMainFileAndEveryModulesContributionLoadTogether: manifest_test.go:247: expected the launcher, clipboar…
2026-10-22 22:05:27 +02:00
mesh-tools 083515da1b Merge pull request 'Give /var/lib/systemd and /var/lib/postgres back to their packages and keep the two modules' own files under /var/lib/mesh (issue 1873, delivery A)' (#453) from fix/1873-system-state-dirs into main 2026-10-22 20:04:28 +00:00
jschoubben ac5d257a1c Let no run read from pacman's log, nor a run whose transaction ended, hold an asked upgrade back; read one run of the addressed machine whole (issue 1841)
Every run imported from the log ends at finished or failed and is never checked after, so the
one-run-at-a-time rule counted each as still going: novox's ask of 2026-10-22 22:00 was refused on
its install-time run 20220123T194900Z, and every node holds such runs. The rule now holds back a new
run only for a run the mesh ran whose transaction may still go on.

pacman_upgrades refused a run without its node, and the console takes node out of the arguments of
an address that names the machine, so no run could be read whole: a run without a node is now that
machine's.
2026-10-22 22:04:23 +02:00
jschoubben 896bd88380 Write mesh/check from the checker's owed state, read by a watch (R2-1)
mesh/repo-check head 896bd88380b3 merged into trunk a21da33d4857 beside mesh-controller#441@d51324df — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.s…
mesh/merge-gate head 896bd88380b3 merged into trunk a21da33d4857 — pass: group feat/r4e composed: every machine composes with the change as it did without …
mesh/delivery covered after its stop: a later walk that contains it was delivered: plan-1792707174627079192 (novox/mesh-catalog at 89e15c7b into main, do…
mesh/delivery-group group feat/r4e closed: a member was closed before the group merged
mesh/check failed: ace: novox/mesh-catalog#457: modules/gitea/go.mod names mesh-sdk 104533e8c0c9, which is neither on mesh-sdk's trunk nor the group's…
R4e round 2 finding R2-1, ADR 0521: the delivery plan draws no edge that
orders the forge module before the forge bridge, and the forge module's
durable consumer keeps its deliver policy when its filters change, so a
check.status the bridge emitted before the forge module consumed it was lost.
An in-memory revision guard also forgot itself on a restart.

The forge module now reads mesh-delivery-checker.owed, which the checker
announces readable, by a watch: the watch hands over the latest value of every
key whenever the module starts, so a status written while it was away is
written then, and only the latest. Each mesh/check is written once per
revision, an older one dropped; a forge refusal is asked again on a bound
until written or overtaken; a refused read is said once and asked again until
granted. No tool writes mesh/check, and no check.status is consumed.
2026-10-22 21:45:53 +02:00
jschoubben 24f50c7c86 mesh-build-go: name go.mod among the narrowing test's paths, since mesh-sdk 104533e8 keeps only the manifest and the lock files beside them (review finding S1) 2026-10-22 21:39:55 +02:00
jschoubben e0bf87c2e3 Give /var/lib/systemd and /var/lib/postgres back to their packages, and keep the modules' files in their own directories (issue 1873)
mesh/repo-check head e0bf87c2e3da merged into trunk ef1e98f8678c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/merge-gate head e0bf87c2e3da merged into trunk ef1e98f8678c — pass: group fix/1873-system-state-dirs composed: every machine composes with the change …
mesh/delivery delivered
mesh/delivery-group group fix/1873-system-state-dirs merged: every member is merged
mesh/check failed: unit: i3/cmd/i3-tools.TestTheMainFileAndEveryModulesContributionLoadTogether: manifest_test.go:247: expected the launcher, clipboar…
The systemd and postgres modules' state directories said place ".", which is
/var/lib/systemd and /var/lib/postgres: directories the systemd and postgresql
packages own (root 0755; postgres 0700). The node-engine took both before issue
339's rule and gave them to the operator's account, 0700, on every machine; postgres's
grants directory, placed by default, lay inside /var/lib/postgres too.

What each module keeps moves to a directory of its own, "data", placed "mesh"
(/var/lib/mesh/systemd, /var/lib/mesh/postgres): the unlink record, and postgres's
own secrets (the same values: the vault keys them by name), consumers.json and
grants. The store's data stays /var/lib/mesh-store, untouched.

The node-engine keeps no found owner or mode for a directory, and forgets one no
longer declared as it stands; it sets the declared owner and mode on a directory it
recorded at the same path under the same id. So for this delivery "state" names
the package's directory with the package's owner and mode, and the engine gives it
back; the next change drops "state". The postgres container is recreated, its
secret's volume moving.

checks/owned/system-known keeps systemd state and postgres state, declared for
this delivery only; postgres's consumers and grants leave the list.
2026-10-22 21:31:58 +02:00
mesh-tools ef1e98f867 Merge pull request 'Keep the pacman keeper's state in a directory of its own, not in the package database's /var/lib/pacman (issue 1841)' (#452) from fix/1841-keeper-state-dir into main 2026-10-22 19:31:24 +00:00
jschoubben e3db481491 Pin every R4e member module to mesh-sdk's final R4e head 104533e8, which holds review fixes S1 and S2, so the group pins one SDK 2026-10-22 21:29:19 +02:00
jschoubben 21b4925892 Merge remote-tracking branch 'origin/fix/r4e-k1' into r4e-int2 2026-10-22 21:28:24 +02:00
jschoubben 098c6d3ec7 Place pacman's state outside the package manager's own directory, and refuse a module directory in a system package's (issue 1841)
mesh/merge-gate head 098c6d3ec70f merged into trunk 2f05e70734cd — pass: builds pacman → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head 098c6d3ec70f merged into trunk 2f05e70734cd — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
mesh/check failed: unit: clipmenu/cmd/clipmenu-tools.TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock: manifest_test.go:199: the launcher …
pacman's state directory said place ".", which resolves to /var/lib/pacman: the
package manager's database directory. The node-engine used it as found and
reported it unhealthy on every machine, and wrote keeper.json into it. It is now
the mesh's directory for the module (place "mesh", /var/lib/mesh/pacman), as
docker's is; keeper.json, the keeper's bus secret and the root helper's socket
follow through ${dir:state}.

checks/owned rule 5 refuses a directory that is, lies in or holds a /var/lib
directory a system package owns (checks/owned/system, read from shanks through
pacman_owns and tmpfiles.d), and any resource inside one. systemd and postgres
collide today and are listed in checks/owned/system-known under issue 1873; that
list only shrinks.
2026-10-22 21:19:15 +02:00
jschoubben 0fdfecfa29 K3: pin gitea to mesh-sdk 104533e8, the group's final SDK head after S1 and S2 (ADR 0500 §2) 2026-10-22 21:19:11 +02:00
jschoubben 97f0f4813c K2: keep the captured claude-code manifest in a test file (issue 1751)
The catalogue's paths check (ADR 0260) reads every file of a module but its documentation and tests, and
the verbatim manifest names claude-code's own paths; as a constant in a _test.go file it stays a real
capture without being taken for one module reaching into another's state.
2026-10-22 21:18:18 +02:00
jschoubben 8d3f81c2c0 Write mesh/check from the forge bridge's own event, never from a tool (K1)
Review finding K1 of R4e, issue 1301 S4 (ADR 0503 §3, ADR 0513 §4: the forge
requires mesh/check alone). gitea_check_status assumed only the bridge could
call it, but a tool is told no caller and node-tools invokes every tool, so any
session could set mesh/check to success and merge past the checker: the fault
issue 293 closed for the two statuses before it.

gitea now consumes mesh-delivery-forge-bridge.check.status, a subject the bus
grants to the bridge's own account alone, and check.ts writes mesh/check from
it: the source is checked, the head's whole sha and the state, and a revision
older than one already written on the head is dropped so a redelivered stale
pending never hides a verdict; a forge refusal throws, so the event is
delivered again. gitea_check_status is removed, and gitea_commit_status keeps
refusing mesh/check.
2026-10-22 21:14:01 +02:00
jschoubben 8f10d3192c K3: pin gitea to mesh-sdk 875c680d, the group's one SDK head (ADR 0500 §2)
gitea's Go code (cmd/repositories) changes in the R4e group, so it pins the SDK head every other member
pins; it still held 66e8fdaa.
2026-10-22 21:08:33 +02:00
jschoubben 1e3439429f K2: read a manifest for its build inputs in mesh-build-go and mesh-build-image (issue 1751, ADR 0514 §2)
The build step parsed the manifest as a judge of config values, so claude-code, dunst and xterm, whose
values fall back to a predefined key, got a fingerprint but their tools bundle was refused at the build.
The builder holds no published list of predefined keys; ParseManifestForInputs is its read. mesh-build-node
reads module.json with its own narrow struct and needs no change.
2026-10-22 21:08:12 +02:00
jschoubben e9e8c7c901 Pin every R4e member module to mesh-sdk's final R4e head 875c680d, which holds main's #68 and #69, so the group pins one SDK 2026-10-22 20:45:17 +02:00
jschoubben db35f2b0cb Merge origin/main into feat/r4e (R4e integration, before the final SDK pin) 2026-10-22 20:43:32 +02:00
jschoubben f90d3bfcc0 Merge feat/r4e-leaf into feat/r4e again: the gate's read states announced readable, pulls too (R4e integration) 2026-10-22 20:36:42 +02:00
jschoubben 37c47f8f95 Mark the delivery gate's pulls readable, as build-agent reads it (issue 1790) 2026-10-22 20:36:21 +02:00
jschoubben a45e467ddd Merge feat/r4e-leaf into feat/r4e again: the gate's read states announced readable (R4e integration) 2026-10-22 20:35:52 +02:00
jschoubben d3f9a1855d Mark the delivery gate's asks, merges, owed and source readable, as mesh-delivery's parts read them (issue 1790) 2026-10-22 20:35:29 +02:00
jschoubben c97cea21cf Merge feat/r4e-leaf into feat/r4e (R4e integration)
gitea's index.ts conflicted on imports: kept both, the leaf's statesAnnouncement and the gate's checkStatusTool.
mesh-delivery-gate is pinned to the sdk lane's newest head d1af87a (the leaf had a90f523) until the final SDK head.
2026-10-22 20:34:48 +02:00
mesh-tools 2f05e70734 Merge pull request 'Upgrade the nodes one at a time on the bus, with a package record, a kernel hold, an after-upgrade check, a rollback and a fallback kernel (upgrade safety, issues 1840 and 1841)' (#450) from feat/upgrade-safety into main 2026-10-22 18:34:15 +00:00
jschoubben 5ab9d6c296 Merge feat/r4e-buildmods into feat/r4e (R4e integration) 2026-10-22 20:30:01 +02:00
jschoubben e61c5ee5bf Merge feat/r4e-gate into feat/r4e (R4e integration) 2026-10-22 20:30:00 +02:00
mesh-tools b5f812270e Merge pull request 'Let restic's thinning wait up to ten minutes for a held repository lock instead of failing at once (issue 620)' (#451) from fix/620-thinning-waits-for-a-lock into main 2026-10-22 18:16:37 +00:00
jschoubben a18eeeef5a Say gitea's states with the SDK's emit, as the toolchain's SDK has no keeps yet (issue 1790, issue 1852) 2026-10-22 19:04:21 +02:00
jschoubben 5b6dfc70f4 Wait until the older clipmenud holds the lock before the launcher starts (issue 1814, S1) 2026-10-22 19:03:03 +02:00
jschoubben 3cce15fa81 restic: let the thinning wait up to ten minutes for a held repository lock (issue 620)
mesh/merge-gate head 3cce15fa811e merged into trunk 835bf22b7d40 — pass: builds restic → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head 3cce15fa811e merged into trunk 835bf22b7d40 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | 3cce1…
A restic outside the night's lock, such as a listing while a delivery plan backs up
data modules before their moves, held the lock 246 ms before the thinning asked for it,
and the thinning failed at once, raising an urgent condition for a timing collision.
restic's --retry-lock now waits up to ten minutes on both of the thinning's forgets;
past that it fails with restic's words as before.
2026-10-22 18:30:16 +02:00
jschoubben 86ba54b06e Take the SDK at the milestone's mesh-sdk head 8892a117 in pacman, which holds main's SDK and the milestone's (issue 1840)
mesh/repo-check head 86ba54b06e09 merged into trunk b5f812270eae beside mesh-controller#439@abe356a2 — pass: its merge-check.sh passed, each part in its to…
mesh/merge-gate head 86ba54b06e09 merged into trunk b5f812270eae — pass: group feat/upgrade-safety composed: every machine composes with the change as it d…
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792694058124013462 (novox/mesh-catalog at 2f05e707 into main,…
mesh/delivery-group group feat/upgrade-safety merged: every member is merged
mesh/check failed: unit: i3/cmd/i3-tools.TestTheMainFileAndEveryModulesContributionLoadTogether: manifest_test.go:247: expected the launcher, clipboar…
2026-10-22 18:14:25 +02:00
jschoubben af91a7e660 Merge remote-tracking branch 'origin/main' into feat/upgrade-safety 2026-10-22 18:14:16 +02:00
jschoubben d45767bb2f Keep another module's path out of i3's fixture (issue 1814, S2) 2026-10-22 18:13:58 +02:00
jschoubben 027ea030fa Qualify a short base image name before buildah reads it (issue 1824) 2026-10-22 18:13:09 +02:00
jschoubben 6eb9eb53de Commit the lock files of mssql, mongodb, model-usage and mesh-catalog, and refuse a Node module without one (issue 1868, ADR 0469 §5) 2026-10-22 18:12:32 +02:00
jschoubben 0270027bf9 Take the SDK at the milestone's mesh-sdk head f8b8f3a6 in pacman, which the delivery group's members must pin exactly (issue 1840) 2026-10-22 18:07:40 +02:00
mesh-tools 835bf22b7d Merge pull request 'only-office: stop declaring the old lib directory (issue 1854)' (#449) from fix/1854-drop-old-lib into main 2026-10-22 16:06:46 +00:00
jschoubben e85b7da63b Let i3's contribution test read its fixture, not the siblings' manifests (issue 1814, S2, ADR 0495 §1) 2026-10-22 18:05:04 +02:00
jschoubben c112df0c1c Announce the states the forge module keeps (issue 1790, ADR 0495 §2 §5) 2026-10-22 18:03:06 +02:00
jschoubben 8e791896f1 Announce the states the delivery gate keeps (issue 1790, ADR 0495 §2 §5) 2026-10-22 18:02:11 +02:00
jschoubben b07a101caf Answer each named path's git object id from gitea_commit_trees (issue 1758, S1-S2, ADR 0499 §3) 2026-10-22 17:59:23 +02:00
jschoubben 72effa1fb1 Copy an upstream artifact into the artifact store by its pinned digest (issue 1587, S11-S13, ADR 0498 §4) 2026-10-22 17:57:05 +02:00
jschoubben e53ac973e9 mesh-delivery-gate: stop asking the controller's forget-checks (issue 1318, the gate's part)
The controller's forget-checks goes with the merge gate's checks (ADR 0503 §3)
and 1318 removes the verb in the same group, so the gate must stop calling it
first: a closed pull request is stopped and asks the controller nothing, and
the manifest no longer invokes it. delivery-check stays for hq's base until
R4g (the lead's decision for 1302).
2026-10-22 17:50:44 +02:00
jschoubben f1f18bf2bd mesh-delivery-gate: read mesh/check alone on a base that requires it (issue 1302, S1-S10)
Once main requires mesh/check, the controller publishes no verdict for it and
the gate, which moved deliveries on the controller's checked, would hold every
pull request proposed. A head's mesh/check is now its verdict, from the
forge's status event (gitea now sends the status's words) or its open pull
requests. The Approve is asked only on the checker's verdict at exactly the
heads the ask covers, so a member judged alone never passes its group; a
group on mesh/check asks no composed check and owes no mesh/merge-gate; a
member's mesh/check that changes withdraws the ask. Nothing is asked of the
controller for such a base, and recheck refuses, naming checker_pull. hq's
mesh/repo-check keeps today's path.
2026-10-22 17:50:25 +02:00
jschoubben c41c968086 gitea: judge and write only the statuses a base requires, so merges go on mesh/check (issue 1301, S5-S7)
Once main requires mesh/check, nothing writes mesh/merge-gate, and the merge
tool, which judged on it, would refuse every merge. It now judges on the
status the base requires: mesh/check's line names the trunk it judged, an
out-of-date one is refused and nothing is asked (the checker re-checks on a
trunk move, ADR 0447 §4); hq's mesh/repo-check is judged as before. The
announcer, the verdict, a trunk's move and a build's start write the
controller's two statuses only where a base still requires them, so hq keeps
mesh/repo-check and no stale mesh/merge-gate appears anywhere else.
2026-10-22 17:49:53 +02:00
jschoubben 939475cb63 gitea: only the forge bridge writes mesh/check, through a tool of its own (issue 1301, S4)
Once main requires mesh/check, a hand that set it could pass a merge past the
checker, the gap issue 293 closed for the two statuses before it.
gitea_commit_status now refuses mesh/check in every state, and the new
gitea_check_status writes it alone; the bus grants that tool only to a module
whose manifest invokes it, the forge bridge (the lead's choice A).
2026-10-22 17:49:53 +02:00
jschoubben 5e24deb0f1 gitea: require mesh/check in place of the two old statuses on every main the gate judges (issue 1301, S1-S3)
ADR 0447 §1 has main require mesh/check alone. The repositories program now
requires it in place of mesh/merge-gate and mesh/repo-check on every listed
repository and on every other one whose main requires mesh/merge-gate, so the
switch needs no hand-kept list (lead decisions L4, L11). A main requiring
mesh/repo-check alone (hq) keeps it until issue 1296 (ADR 0503 §5).
2026-10-22 17:49:53 +02:00
jschoubben 76a0a726f0 issue 1841/S15: put the booted kernel first when GRUB's menu is generated again, so the fallback kernel, sorted first by version, never becomes the default entry (ADR 0508 §9) 2026-10-22 17:49:21 +02:00
jochen df821c8a4a only-office: stop declaring the old lib directory, emptied on the operator's word (issue 1854)
mesh/merge-gate head df821c8a4ab6 merged into trunk 49caf17ff56e — pass: builds only-office → novox; no bus step; every machine composes with the change as…
mesh/repo-check head df821c8a4ab6 merged into trunk 49caf17ff56e — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: clipmenu/cmd/clipmenu-tools.TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock: manifest_test.go:199: the launcher …
Issue 1843 kept the old /var/lib/only-office/lib declared only to close it to 0700 while it held
data. Its contents are deleted now, so the declaration has no job left; mesh-host removes the
empty directory once it is no longer declared.
2026-10-22 17:49:08 +02:00
jschoubben f40269caec issue 1841/S2: leave the login manager's configuration path out of pacman's captured log, which the paths check refuses in another module's fixture 2026-10-22 17:45:29 +02:00
jschoubben 2149d8f3e7 Merge remote-tracking branch 'origin/main' into feat/upgrade-safety 2026-10-22 17:40:30 +02:00
jschoubben e65c8c8961 issue 1841/S13: describe the package view, the runs, the round, the root helper and the health read from the bus in pacman's README 2026-10-22 17:39:52 +02:00
jschoubben ea8e0def6e issue 1841/S12: prove the round on a real bus with two made-up nodes, so the second node is seen to start only after the first passed and never after a failed check 2026-10-22 17:39:15 +02:00
jschoubben f8ac498d64 issue 1841/S11: judge the upgrades' health from the runs, rounds and held kernels on the bus, so a node that slept through a run is judged by what happened, not by a unit's age (folds issue 1579) 2026-10-22 17:38:08 +02:00
jschoubben 276422a85b issue 1841/S8: join the round to the keeper's loop, so a node starts the moment the node before it passed and a pass is answered from the operator's channel 2026-10-22 17:35:36 +02:00
jschoubben 28ef6e9e27 issue 1841/S9: carry out the upgrade's root acts through a root helper on a socket only the keeper opens, so no sudo rule lets the keeper's account run anything as root (ADR 0510) 2026-10-22 17:34:20 +02:00
jschoubben f339d0aab7 Merge branch 'feat/upgrade-safety' into p2
# Conflicts:
#	modules/pacman/cmd/pacman-tools/main.go
#	modules/pacman/cmd/pacman-tools/transaction.go
#	modules/pacman/module.json
2026-10-22 14:44:41 +02:00
jschoubben 43fccb843c issue 1841/S10: return a node to a run's record, saying first what each package goes back to and from where
pacman_rollback plans from the bus: each package now -> record from the node's cache, else the archive's
exact version, else the archive's repository on the record's date; added packages listed and removed only
when asked; foreign packages untouched. apply is an ask the node's keeper runs as a rollback run through the
root helper's new Rollback act (RootActs, upgrades.go), with rollback.started, rollback.finished and the
after-upgrade check (TakeAsk's rollback case, transaction.go).
2026-10-22 14:44:24 +02:00
jschoubben f550f1c6b2 issue 1841/S8: upgrade the nodes one at a time in a weekly round in upgrade-order, stopping at a failure and passing a node only on the operator's Approve, so one bad upgrade never reaches every machine 2026-10-22 14:44:17 +02:00
jschoubben 71f47c3291 issue 1841/S5: refuse the root helper's new maps act too until its client is built in 2026-10-22 14:40:09 +02:00
jschoubben dc703d1511 Merge branch 'feat/upgrade-safety' into p2 2026-10-22 14:40:00 +02:00
jschoubben d70135bf08 issue 1841/S5: join the upgrade parts to the keeper's loop, so a node's log, records, unfinished runs and asks are taken up by its own keeper
At its start the keeper reads the log into runs once, prunes old records and carries unfinished runs on;
after every released lock it carries them on again; it takes each ask at <node>.* and prunes records daily.
The root helper's client is a constructor (newRootActs) the helper replaces; until then every act is
refused, named.
2026-10-22 14:39:59 +02:00
jschoubben ff0f43ea2f Merge branch 'feat/upgrade-safety' into p1 2026-10-22 14:39:29 +02:00
jschoubben 2b2517e4a5 issue 1841/S7: name the processes still mapping a replaced file and fail the check on a DKMS module not built for an installed kernel, so a machine is never left unable to load its drivers unnoticed 2026-10-22 14:39:29 +02:00
jschoubben b7a4a1a6b0 Merge branch 'feat/upgrade-safety' into p2 2026-10-22 14:38:05 +02:00
jschoubben c1a9ad2abc issue 1840/S6: take the SDK's process delete for the not-current mark, now that the process state has one 2026-10-22 14:38:02 +02:00
jschoubben c93f77ac19 Merge branch 'feat/upgrade-safety' into p2
# Conflicts:
#	modules/pacman/go.mod
2026-10-22 14:37:59 +02:00
jschoubben 75f3f52589 Merge branch 'feat/upgrade-safety' into p1 2026-10-22 14:37:58 +02:00
jschoubben 900a2da3e3 issue 1841/S6: check after every upgrade the files it left, the news since the last one, units newly failed and a restart needed, so the operator sees what an upgrade asks of them 2026-10-22 14:37:58 +02:00
jschoubben b7f3664839 issue 1840/S10: run the keeper whole against a real bus, so its key, its event and another instance's view are proved together
Written and compiled under the integration tag; the merge check runs it.
2026-10-22 14:37:45 +02:00
jschoubben 9511074f40 Take the SDK whose process can delete a key (issue 1840/S1) 2026-10-22 14:37:28 +02:00
jschoubben 9651a8b2da issue 1840/S9: prove pacman's output puts no command line or environment on the bus
A fixture of pacman and checkupdates output holding command lines, environment variables and messages
leaves only names, versions and repositories in installed, updates and package.changed, and no command
the keeper runs names a package, a foreign one least of all.
2026-10-22 14:37:00 +02:00
jschoubben 32c66a98c1 Merge branch 'feat/upgrade-safety' into p2 2026-10-22 14:36:14 +02:00
jschoubben 62437f8964 issue 1840/S8: answer the package view from the bus, so every node is read at once and none is asked
pacman_packages answers one package on every node, or each node's counts, its explicit packages no module
declares and its foreign ones apart, the versions that differ and the packages on some nodes only, with a
node over the bus's cap marked not current. pacman_updates, pacman_installed and pacman_foreign read the
keeper's states instead of the machine; pacman_updates says when each node's list last changed.
2026-10-22 14:36:11 +02:00
jschoubben ac4d156cb7 Merge branch 'feat/upgrade-safety' into p1 2026-10-22 14:35:22 +02:00
jschoubben 95e69276c3 Carry out an upgrade as an ask its node's keeper takes, through the root helper, and follow a run to its end after a restart, so a hand upgrade is a recorded, checked run like any other (issue 1841/S5) 2026-10-22 14:35:19 +02:00
jschoubben 56b285d3e8 issue 1840/S7: prove updates is read at start, after a transaction, after the mirrors and daily, and written only on change
A failed read keeps the last list and says reachable false, so a list the node can no longer confirm is
never shown as current, and a first read that fails is never an empty list taken for an answer.
2026-10-22 14:33:20 +02:00
jschoubben 5cc401683c Merge branch 'feat/upgrade-safety' into p2 2026-10-22 14:32:13 +02:00
jschoubben 79ce72a7b7 issue 1840/S6: emit through the same EventSink as the upgrade parts, so one connection serves both 2026-10-22 14:32:10 +02:00
jschoubben f40fa6e8e0 issue 1841/S4: format the gate's patterns as gofmt does, which the merge check requires 2026-10-22 14:32:01 +02:00
jschoubben b015576a13 Hold a kernel an installed DKMS module's own bound excludes, with its headers, so an upgrade never leaves a machine whose storage driver cannot load (issue 1841/S4) 2026-10-22 14:31:53 +02:00
jschoubben e4fb4cc793 Merge branch 'feat/upgrade-safety' into p2 2026-10-22 14:31:39 +02:00
jschoubben 1de9a903c8 issue 1840/S6: keep the node's packages in installed, read at the keeper's start and at every released lock
The keeper reads pacman's database into installed.<node> and writes it only when it differs from the key,
says each change as package.changed, and keeps the last value when the list is over the bus's cap, marking
the node not current until it fits again; the health names the size. The loop runs over injected states,
events, clock, watch and machine, and leaves hooks (start, after a transaction, state watches, wakes) for the
upgrade parts of ADR 0508.
2026-10-22 14:31:33 +02:00
jschoubben 0bb6584437 Take the node's package record before a run and read it back, refusing a run whose packages are not current, so a rollback always has a true record to return to (issue 1841/S3) 2026-10-22 14:30:34 +02:00
jschoubben c0bc0d4826 Merge branch 'feat/upgrade-safety' into p1 2026-10-22 14:29:29 +02:00
jschoubben efac1b4856 Keep every upgrade as a run on the bus and read the log's past upgrades once, so pacman_upgrades answers every node's history without asking a machine (issue 1841/S2) 2026-10-22 14:29:23 +02:00
jschoubben bc77afa6bb issue 1841/S16: kernel_boot answers the loader, entries, kernels, booted entry and the fallback
ADR 0508 §9: the operator sees whether every boot menu lists the fallback kernel, and the room left on /boot.
2026-10-22 14:28:23 +02:00
jschoubben e4373768af issue 1841/S14: declare the fallback kernel and its headers after the boot-entries unit
ADR 0508 §9: every DKMS module is built for the fallback kernel too, and its entry is in place first.
2026-10-22 14:27:24 +02:00
jschoubben 2cb2e27cdf issue 1838: name the kernel module's fixtures by what they show (as in the fix's own branch) 2026-10-22 14:27:15 +02:00
jschoubben 0f4e631cdd issue 1841/S15: place the fallback kernel's boot entry from the loader the machine booted with
ADR 0508 §9 and §10: systemd-boot gets an entry from the booted command line, GRUB a regenerated menu only when its kernels change, a found kernel-copying hook is taken over, a full boot partition fails loudly.
2026-10-22 14:26:59 +02:00
jschoubben 802c72e4ed Take the SDK that lets a process put, list and watch its states (issue 1840/S5) 2026-10-22 14:23:43 +02:00
jschoubben eb717cc514 Declare the pacman keeper and its bus shapes, so upgrades run as rounds on the bus instead of a weekly timer on every node at once (issue 1841/S1, with 1840/S5's manifest)
The keeper process runs under its own account with a bus account of its own,
keeps the six states and emits the nine events of ADR 0507 and ADR 0508; its
config (upgrade-order, record-days, round-start-minutes) lands in its config
file and restarts it. shapes.go fixes every key's value and event body as the
interface the keeper and the verbs share. The weekly timer and its service are
dropped, which the node-engine removes.
2026-10-22 14:23:22 +02:00
1005 changed files with 75719 additions and 29536 deletions
+13
View File
@@ -14,6 +14,7 @@ modules/claude-code/claude-code
modules/mesh-watcher/mesh-watcher
modules/dnsmasq/dnsmasq
modules/mesh-issues/cmd/mesh-issues/mesh-issues
modules/mesh-issues/mesh-issues
modules/mounts/cmd/mounts/mounts
modules/mesh-knowledge/cmd/mesh-knowledge/mesh-knowledge
modules/mesh-knowledge/mesh-knowledge
@@ -28,3 +29,15 @@ modules/brother/cmd/brother/brother
modules/mesh-build-go/cmd/mesh-build-go/mesh-build-go
modules/mesh-config/mesh-config
modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
*.test
# binaries a local go build leaves in a module root
/checks/paths/paths
/modules/mesh-build-go/mesh-build-go
/modules/mesh-delivery-gate/mesh-delivery-gate
modules/mesh-config/cmd/mesh-config/mesh-config
# Built by their modules' build step (issue 1577), never committed.
modules/forticlient/cmd/forticlient-tools/forticlient-tools
modules/networkmanager/uplink-tools
checks/words/words
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest.
modules/*/module.json
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/config
go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
+1 -1
View File
@@ -110,7 +110,7 @@ func Check(root string) (map[string][]string, error) {
// The modules taking a whole layer were found after the list was first written and before the trunk held a copy
// (issue 735), so they stand in it too.
var seed = map[string]bool{
"betterbird": true, "fail2ban": true, "influxdb": true, "keycloak": true, "mailu": true, "searxng": true,
"fail2ban": true, "influxdb": true, "keycloak": true, "mailu": true, "searxng": true,
"baserow": true, "confluence": true, "gitea": true, "gitlab": true, "grafana": true, "home-assistant": true,
"icecast": true, "jira": true, "letta": true, "matrix": true, "nodered": true, "supabase": true, "unifi": true,
}
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest and the files it names.
modules
+33
View File
@@ -0,0 +1,33 @@
package main
import (
"path/filepath"
"testing"
"git.novox.be/novox/mesh-catalog/checks/trunk"
)
// The script's comparison of this list with the trunk's (novox/hq ADR 0529 clause 2): the list of modules not yet moved gains none
// the trunk's copy at the merge base does not hold. The catalogue itself is judged against the list by
// TestTheCatalogueSaysConfigInEveryMovedModule. Skipped, and said, where no trunk is beside.
func TestTheUnmovedListHoldsNoModuleTheTrunksDoesNot(t *testing.T) {
path, ok := trunk.Copy(t, "checks/config/unmoved")
found, err := Check(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
unmoved, err := Unmoved("unmoved")
if err != nil {
t.Fatal(err)
}
base := seed
if ok {
if base, err = Unmoved(path); err != nil {
t.Fatal(err)
}
}
_, _, added := Judge(found, unmoved, base)
for _, m := range added {
t.Errorf("checks/config/unmoved gains %s, which the trunk's copy does not hold: the list only shrinks", m)
}
}
+5
View File
@@ -0,0 +1,5 @@
module checks/credentialform
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
+2
View File
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+96
View File
@@ -0,0 +1,96 @@
// Package credentialform refuses an offer that does not say the form of its credential: an own secret, an issuer, or
// `form` pair or none (the lead, 2026-10-29). It reads every modules/*/module.json through mesh-sdk's
// CredentialFormProblems.
//
// It refuses a route received as a file too (issue 774, research 072 row 34, the operator on 2026-10-31: "there is only
// 1 path, the other is simply deprecated/invalid"): a route is a provision, served from the seat mesh-provisioner's
// plans alone, and a manifest's `receives.route` is the deprecated path. A module still moving off it is listed in
// stillReceivingRoutes with why; every refusal names them all, and a listed module that no longer receives one is
// refused too, so the list only shrinks.
package credentialform
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Problems is every offer of the manifests under root/modules that says no credential form, one line each.
func Problems(root string) ([]string, error) {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(files)
var out []string
var receiving []string
reads := map[string]bool{}
var binders []string
for _, f := range files {
raw, err := os.ReadFile(f)
if err != nil {
return nil, err
}
var m catalogue.Manifest
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", f, err)
}
out = append(out, catalogue.CredentialFormProblems(m)...)
if _, received := m.Receives["route"]; received {
receiving = append(receiving, m.Module)
} else if _, listed := stillReceivingRoutes[m.Module]; listed {
out = append(out, fmt.Sprintf("%s no longer receives route; take it off stillReceivingRoutes", m.Module))
}
var kept struct {
Reads []string `json:"reads"`
State []json.RawMessage `json:"state"`
}
_ = json.Unmarshal(raw, &kept) // reads may hold objects in another module; only the provisioner's are read
if m.Module == "mesh-provisioner" {
for _, r := range kept.Reads {
reads[r] = true
}
}
if len(m.Provides) > 0 && keepsBound(kept.State) {
binders = append(binders, m.Module)
}
}
for _, module := range binders {
if !reads[module+".bound"] {
out = append(out, fmt.Sprintf("%s provides and keeps a `bound` state, and mesh-provisioner's manifest does not read %s.bound", module, module))
}
}
for _, module := range receiving {
if _, listed := stillReceivingRoutes[module]; !listed {
out = append(out, fmt.Sprintf("%s receives route as a file: a route is a provision, served from the seat "+
"mesh-provisioner's plans alone (issue 774); still on the deprecated path: %s", module,
strings.Join(receiving, ", ")))
}
}
return out, nil
}
// stillReceivingRoutes is every module still receiving its routes as a file, and why it has not moved yet.
var stillReceivingRoutes = map[string]string{
"route-adapter": "migration scaffolding writing into a predecessor's proxy (novox/hq ADR 0104), deleted when that " +
"migration ends; it reads the received file its TypeScript adapter turns into the predecessor's route files",
}
// keepsBound says whether a manifest's `state` names `bound`, as a plain name or an object with a name.
func keepsBound(state []json.RawMessage) bool {
for _, s := range state {
var name string
var obj struct {
Name string `json:"name"`
}
if json.Unmarshal(s, &name) == nil && name == "bound" || json.Unmarshal(s, &obj) == nil && obj.Name == "bound" {
return true
}
}
return false
}
+54
View File
@@ -0,0 +1,54 @@
package credentialform
import (
"os"
"path/filepath"
"strings"
"testing"
)
func aCatalogue(t *testing.T, manifests map[string]string) string {
t.Helper()
root := t.TempDir()
for module, body := range manifests {
dir := filepath.Join(root, "modules", module)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
return root
}
// An offer that says no form is refused and named; one that says pair, none or an own secret passes; a route received
// as a file is refused but for the module still moving off it; and the real catalogue is clean.
func TestEveryOfferSaysTheFormOfItsCredential(t *testing.T) {
root := aCatalogue(t, map[string]string{
"silent": `{"module":"silent","provides":[{"name":"thing","scope":"mesh"}]}`,
"binder": `{"module":"binder","provides":[{"name":"d","credential":{"form":"pair"}}],"state":["bound"]}`,
"mesh-provisioner": `{"module":"mesh-provisioner","reads":["other.bound"]}`,
"says": `{"module":"says","provides":[{"name":"a","credential":{"form":"pair"}},{"name":"b","credential":{"form":"none"}},{"name":"c","credential":{"own":"jwt"}}]}`,
// A route received as a file (issue 774, research 072 row 34): refused, naming every module still on it; the one
// listed as still moving passes.
"old-proxy": `{"module":"old-proxy","receives":{"route":"/var/lib/old-proxy/mesh.json"}}`,
"route-adapter": `{"module":"route-adapter","receives":{"route":"/var/lib/route-adapter/mesh.json"}}`,
})
got, err := Problems(root)
if err != nil {
t.Fatal(err)
}
if len(got) != 3 || !strings.Contains(got[0], `silent provides "thing"`) ||
!strings.Contains(got[1], "binder") || !strings.Contains(got[1], "mesh-provisioner") ||
!strings.HasPrefix(got[2], "old-proxy receives route") || !strings.Contains(got[2], "old-proxy, route-adapter") {
t.Fatalf("refused %v, wanted the silent offer, the binder the provisioner does not read and the received route", got)
}
real, err := Problems(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
if len(real) != 0 {
t.Fatalf("the catalogue has offers with no credential form:\n%s", strings.Join(real, "\n"))
}
}
+3
View File
@@ -0,0 +1,3 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every manifest, for what each offer says of its credential's form.
modules/*/module.json
+128
View File
@@ -0,0 +1,128 @@
// Package failurekit refuses a build module whose tests do not run mesh-sdk's conformance kit (issue 1901, its thorough
// fix): a module that claims a node-build-* seat and serves a test step must hold a test that calls conformance.Run, so
// its parse of the test runner's output is held to the failure contract (a failing test named with its cause, never a
// passing line in its place). A build module with no test step (mesh-build-image builds images and runs no tests) is
// exempt from that, and said so. Every build module, test step or not, must also hold a test that calls
// conformance.LeftRunning with its own start sweep, so a step a previous process left running is ended at its start
// (issue 1890).
package failurekit
import (
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
)
// Judged is what the check says of one build module: Refused says why it fails the check, "" when it passes; Exempt
// says why the kit does not apply.
type Judged struct {
Module, Refused, Exempt string
}
type manifest struct {
Claims []struct {
Name string `json:"name"`
} `json:"claims"`
Tools []json.RawMessage `json:"tools"`
}
// Judge reads every modules/<m>/module.json of the catalogue at root and judges each module that claims a node-build-*
// seat.
func Judge(root string) ([]Judged, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Judged
for _, p := range manifests {
raw, err := os.ReadFile(p)
if err != nil {
return nil, err
}
var m manifest
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", p, err)
}
seat := ""
for _, c := range m.Claims {
if strings.HasPrefix(c.Name, "node-build-") {
seat = c.Name
}
}
if seat == "" {
continue
}
dir := filepath.Dir(p)
j := Judged{Module: filepath.Base(dir)}
var refused []string
test := testTool(m.Tools)
switch {
case test == "":
j.Exempt = fmt.Sprintf("it claims %s and serves no test step, so the failure contract's cases do not apply", seat)
default:
runs, err := runsTheKit(dir, "conformance.Run(")
if err != nil {
return nil, err
}
if !runs {
refused = append(refused, fmt.Sprintf("it claims %s and serves the test step %s, and no test of it calls conformance.Run "+
"(mesh-sdk go/buildstep/conformance): run the kit with the module's own parse path (issue 1901)", seat, test))
}
}
// Every build module keeps steps, so every one runs the left-running case, test step or not (issue 1890).
sweeps, err := runsTheKit(dir, "conformance.LeftRunning(")
if err != nil {
return nil, err
}
if !sweeps {
refused = append(refused, fmt.Sprintf("it claims %s, and no test of it calls conformance.LeftRunning "+
"(mesh-sdk go/buildstep/conformance): run the kit's left-running case with the module's own start sweep (issue 1890)", seat))
}
j.Refused = strings.Join(refused, "; ")
out = append(out, j)
}
return out, nil
}
// testTool is the name of the module's test step tool (<prefix>_test), or "".
func testTool(tools []json.RawMessage) string {
for _, raw := range tools {
var name string
if json.Unmarshal(raw, &name) != nil {
var t struct {
Name string `json:"name"`
}
if json.Unmarshal(raw, &t) != nil {
continue
}
name = t.Name
}
if strings.HasSuffix(name, "_test") {
return name
}
}
return ""
}
// runsTheKit says a _test.go file under dir calls the kit's function call (such as "conformance.Run(").
func runsTheKit(dir, call string) (bool, error) {
found := false
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
if err != nil || found || d.IsDir() || !strings.HasSuffix(p, "_test.go") {
return err
}
raw, err := os.ReadFile(p)
if err != nil {
return err
}
found = strings.Contains(string(raw), call) &&
strings.Contains(string(raw), `"git.novox.be/novox/mesh-sdk/go/buildstep/conformance"`)
return nil
})
return found, err
}
+77
View File
@@ -0,0 +1,77 @@
package failurekit
import (
"os"
"path/filepath"
"testing"
)
// 1901/AC7 and 1890/AC8: a build module with a test step and no kit is refused, one that runs the kit passes, one with no
// test step is exempt from the failure cases but not from the left-running case, a module that runs only the failure
// cases is refused, and a module that claims no node-build-* seat is not judged.
func TestABuildModuleWithATestStepMustRunTheKit(t *testing.T) {
root := t.TempDir()
const imp = "package main\n\nimport \"git.novox.be/novox/mesh-sdk/go/buildstep/conformance\"\n\n"
const sweep = "func TestSweep(t *testing.T) { conformance.LeftRunning(t, nil) }\n"
kit := imp + "func TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n" + sweep
for path, body := range map[string]string{
"modules/a/module.json": `{"claims":[{"name":"node-build-a"}],"tools":["build_a_fetch","build_a_test"]}`,
"modules/a/cmd/a/check_test.go": "package main\n",
"modules/b/module.json": `{"claims":[{"name":"node-build-b"}],"tools":[{"name":"build_b_test"}]}`,
"modules/b/cmd/b/kit_test.go": kit,
"modules/c/module.json": `{"claims":[{"name":"node-build-c"}],"tools":["build_c_build"]}`,
"modules/c/cmd/c/kit_test.go": imp + sweep,
"modules/f/module.json": `{"claims":[{"name":"node-build-f"}],"tools":["build_f_build"]}`,
"modules/g/module.json": `{"claims":[{"name":"node-build-g"}],"tools":["build_g_test"]}`,
"modules/g/cmd/g/kit_test.go": imp + "func TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n",
"modules/d/module.json": `{"claims":[{"name":"other"}],"tools":["d_test"]}`,
"modules/e/module.json": `{"claims":[{"name":"node-build-e"}],"tools":["build_e_test"]}`,
"modules/e/cmd/e/kit.go": kit, // not a test file
} {
p := filepath.Join(root, path)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Judge(root)
if err != nil {
t.Fatal(err)
}
want := map[string]string{"a": "refused", "b": "passes", "c": "exempt", "e": "refused", "f": "refused", "g": "refused"}
if len(got) != len(want) {
t.Fatalf("judged %+v, want a, b, c, e, f and g", got)
}
for _, j := range got {
is := "passes"
if j.Refused != "" {
is = "refused"
} else if j.Exempt != "" {
is = "exempt"
}
if is != want[j.Module] {
t.Errorf("%s %s (%+v), want it %s", j.Module, is, j, want[j.Module])
}
}
}
// Every build module of this catalogue with a test step runs the kit.
func TestTheCataloguesBuildModulesRunTheKit(t *testing.T) {
got, err := Judge(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
if len(got) == 0 {
t.Fatal("no module claims a node-build-* seat: the check reads the wrong tree")
}
for _, j := range got {
switch {
case j.Refused != "":
t.Errorf("%s: %s", j.Module, j.Refused)
case j.Exempt != "":
t.Logf("%s is exempt: %s", j.Module, j.Exempt)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/failurekit
go 1.22
+7
View File
@@ -0,0 +1,7 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Judge("../.."): every manifest, and the directory of each module claiming a node-build-* seat, walked for a test that
# calls the kit. The build modules are the mesh-build-* directories; a build module named otherwise needs its line here.
modules/*/module.json
modules/mesh-build-*
+8
View File
@@ -0,0 +1,8 @@
# The calls of the controller's hand-act log a manifest of this catalogue may still invoke, each `<module> <verb> <why>`:
# the fallback of a recorder (mesh-sdk handacts.Recorder) while the seat mesh-hand-acts has no holder that answers, until
# the controller's lease is written to a holder that is no controller. The check of this directory (handacts) refuses any
# other, and an entry whose module no longer invokes it: the list only shrinks, and it is empty before the lease write
# (novox/hq ADR 0565 §11; issue 1953, issue 774).
mesh-config hand-act fallback until the lease write: its sets, clears and held values record through mesh-hand-acts first
mesh-config warranted fallback until the lease write: its warrants record through mesh-hand-acts first
mesh-healers drill fallback until the lease write: a drill records through mesh-hand-acts first
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/handacts
go 1.22
+109
View File
@@ -0,0 +1,109 @@
// Command handacts refuses a manifest that records into the controller's hand-act log (novox/hq ADR 0565 §11): every
// recorder records through the seat mesh-hand-acts from its delivery, so no manifest invokes
// `seat:mesh-controller.hand-act`, `.warranted` or `.drill`. The recorders' fallbacks to the controller's log while the
// seat has no holder are named in checks/handacts/fallbacks, each module and verb with why; that list only shrinks, and
// an entry whose module no longer invokes the verb is refused too.
//
// go run . <catalog root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// Verbs are the controller's hand-act verbs, as a manifest invokes them, and Allowed the list of named fallbacks.
var Verbs = []string{"hand-act", "warranted", "drill"}
const (
invokePrefix = "seat:mesh-controller."
Allowed = "checks/handacts/fallbacks"
)
// Findings is every refusal in the catalog at root, sorted.
func Findings(root string) ([]string, error) {
allowed := map[string]bool{}
raw, err := os.ReadFile(filepath.Join(root, Allowed))
if err != nil && !os.IsNotExist(err) {
return nil, err
}
for _, line := range strings.Split(string(raw), "\n") {
if f := strings.Fields(line); len(f) >= 3 && !strings.HasPrefix(f[0], "#") {
allowed[f[0]+" "+f[1]] = true
} else if len(f) > 0 && !strings.HasPrefix(f[0], "#") {
return nil, fmt.Errorf("%s: %q is not `<module> <verb> <why>`", Allowed, line)
}
}
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
var out []string
invoked := map[string]bool{}
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Invokes []string `json:"invokes"`
}
if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" {
continue
}
for _, inv := range m.Invokes {
verb, ok := strings.CutPrefix(strings.TrimSpace(inv), invokePrefix)
if !ok || !contains(Verbs, verb) {
continue
}
invoked[m.Module+" "+verb] = true
if !allowed[m.Module+" "+verb] {
out = append(out, fmt.Sprintf("%s invokes %s%s: record through seat:mesh-hand-acts.%s (novox/hq ADR 0565 §11), "+
"or name the fallback in %s", m.Module, invokePrefix, verb, verb, Allowed))
}
}
}
for entry := range allowed {
if !invoked[entry] {
out = append(out, fmt.Sprintf("%s names %q, which is no longer invoked: take it off the list, which only shrinks",
Allowed, entry))
}
}
sort.Strings(out)
return out, nil
}
func contains(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: handacts <catalog root>")
os.Exit(2)
}
found, err := Findings(os.Args[1])
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, f := range found {
fmt.Println(f)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("no manifest records into the controller's hand-act log but the fallbacks named")
}
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// No manifest invokes the controller's hand-act verbs but a named fallback; a named fallback no longer invoked is
// refused (novox/hq ADR 0565 §11's catalog test).
func TestFindings(t *testing.T) {
for _, row := range []struct {
name string
files map[string]string
want string // empty: clean
}{
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
{"an unnamed call", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.warranted"]}`},
"a invokes seat:mesh-controller.warranted"},
{"a named fallback, and the controller's own", map[string]string{
"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.drill"]}`,
"modules/mesh-controller/module.json": `{"module":"mesh-controller","invokes":["seat:mesh-controller.hand-act"]}`,
Allowed: "# why\na drill fallback until the lease write\n"}, ""},
{"a fallback no longer invoked", map[string]string{Allowed: "gone hand-act fallback\n"}, `names "gone hand-act"`},
{"another verb of the controller", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.conditions"]}`}, ""},
} {
t.Run(row.name, func(t *testing.T) {
root := t.TempDir()
for path, text := range row.files {
p := filepath.Join(root, path)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Findings(root)
if err != nil {
t.Fatal(err)
}
if all := strings.Join(got, "\n"); (row.want == "") != (all == "") || !strings.Contains(all, row.want) {
t.Fatalf("findings = %q, want one containing %q", all, row.want)
}
})
}
}
// The run of this check over the real tree.
func TestTheCatalogRecordsOnlyThroughTheSeat(t *testing.T) {
got, err := Findings(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
for _, f := range got {
t.Error(f)
}
}
+3
View File
@@ -0,0 +1,3 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every manifest, for what it invokes.
modules/*/module.json
+5
View File
@@ -0,0 +1,5 @@
module checks/health
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
+2
View File
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+70
View File
@@ -0,0 +1,70 @@
// Package health is the catalogue's count of long-running resources that do not say how they are ready (novox/hq ADR 0240 rule
// 8), kept in health-undeclared and judged by a test over the real tree (ADR 0529 clauses 2 and 4): it replaces the step of
// merge-check.sh that read it out of the controller's `module check`. Every manifest is read through mesh-sdk's catalogue
// package, the one the controller counts with.
package health
import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Count is the long-running resources without `health` of every module under root/modules.
func Count(root string) (int, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return 0, err
}
sort.Strings(manifests)
if len(manifests) == 0 {
return 0, fmt.Errorf("no manifest under %s: the count would be zero for any catalogue", filepath.Join(root, "modules"))
}
n := 0
for _, path := range manifests {
m, err := catalogue.ReadTreeManifest(root, filepath.Dir(path))
if err != nil {
return 0, fmt.Errorf("%s: %w", path, err)
}
n += len(catalogue.Undeclared(m))
}
return n, nil
}
// Kept is the number the file keeps: the first number of its first line.
func Kept(path string) (int, error) {
raw, err := os.ReadFile(path)
if err != nil {
return 0, err
}
first, _, _ := strings.Cut(string(raw), "\n")
digits := ""
for _, r := range strings.TrimSpace(first) {
if r < '0' || r > '9' {
break
}
digits += string(r)
}
n, err := strconv.Atoi(digits)
if err != nil {
return 0, fmt.Errorf("%s does not begin with the number it keeps: %q", path, first)
}
return n, nil
}
// Judge says what is wrong with a count against the kept number; empty when they agree. It only goes down: a rise is refused,
// and a fall asks the new number written in health-undeclared, so the count cannot rise again.
func Judge(kept, counted int) string {
switch {
case counted > kept:
return fmt.Sprintf("the long-running resources without health rose from %d to %d: declare how each new one is ready (ADR 0240 rule 8)", kept, counted)
case counted < kept:
return fmt.Sprintf("the long-running resources without health fell from %d to %d: write %d in health-undeclared, so the count cannot rise again", kept, counted, counted)
}
return ""
}
+88
View File
@@ -0,0 +1,88 @@
package health
import (
"os"
"path/filepath"
"strings"
"testing"
)
func aCatalogue(t *testing.T, manifests map[string]string) string {
t.Helper()
root := t.TempDir()
for module, body := range manifests {
dir := filepath.Join(root, "modules", module)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
return root
}
// A service that stays up and does not say how it is ready is counted; one that says, and a file, are not.
func TestOnlyALongRunningResourceWithoutHealthIsCounted(t *testing.T) {
root := aCatalogue(t, map[string]string{
"quiet": `{"module":"quiet","resources":[{"id":"svc","type":"service","unit":"quiet.service","state":"running"}]}`,
"says": `{"module":"says","resources":[{"id":"svc","type":"service","unit":"says.service","state":"running",` +
`"health":{"kind":"unit"}}]}`,
"file": `{"module":"file","resources":[{"id":"cfg","type":"file","path":"/etc/file.conf","content":"x"}]}`,
})
counted, err := Count(root)
if err != nil {
t.Fatal(err)
}
if counted != 1 {
t.Fatalf("counted %d, wanted the one quiet service", counted)
}
}
func TestTheCountMayOnlyGoDown(t *testing.T) {
for _, c := range []struct {
kept, counted int
says string
}{
{58, 58, ""},
{58, 59, "rose from 58 to 59"},
{58, 57, "fell from 58 to 57"},
} {
got := Judge(c.kept, c.counted)
if (c.says == "") != (got == "") || !strings.Contains(got, c.says) {
t.Errorf("kept %d counted %d: %q, wanted %q", c.kept, c.counted, got, c.says)
}
}
}
func TestTheKeptCountIsTheFirstNumberOfTheFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "health-undeclared")
if err := os.WriteFile(path, []byte("58\nThe long-running resources ...\n"), 0o644); err != nil {
t.Fatal(err)
}
if n, err := Kept(path); err != nil || n != 58 {
t.Fatalf("%d %v", n, err)
}
if err := os.WriteFile(path, []byte("many\n"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := Kept(path); err == nil {
t.Fatal("a file with no number gave a count")
}
}
// The script's `module check` run over the real tree (novox/hq ADR 0240 rule 8, ADR 0529 clause 2 and 4): the long-running
// resources that do not say how they are ready, counted as the controller counts them, are the number health-undeclared keeps.
func TestTheCatalogueSaysHowItsLongRunningResourcesAreReady(t *testing.T) {
kept, err := Kept(filepath.Join("..", "..", "health-undeclared"))
if err != nil {
t.Fatal(err)
}
counted, err := Count(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
if say := Judge(kept, counted); say != "" {
t.Fatal(say)
}
}
+10
View File
@@ -0,0 +1,10 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# every manifest through mesh-sdk's ReadTreeManifest: the root's predefined keys and each templated module's
# templates (files/ by default, or the directory its manifest names); and the root's kept count of resources that do not say how they are ready.
modules/*/module.json
predefined.json
modules/*/files
modules/*/templates
health-undeclared
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest.
modules/*/module.json
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest, and whether the path each image artifact builds from is there.
modules
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/locks
go 1.22
+105
View File
@@ -0,0 +1,105 @@
// Command locks refuses a TypeScript module that names packages beyond the SDK in its package.json and commits no
// package-lock.json (novox/hq ADR 0469 §5, issue 1868): the Node build module installs a module's own packages only
// from its committed lock, so such a module is refused by the builder and could not be delivered.
//
// go run . <catalogue root>
//
// Exits 1 on a finding. The packages that count are those the build module counts (mesh-build-node fetch.go
// ownDependencies): the `dependencies` beyond the SDK, typescript and the @types packages.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// Finding is one module refused: it names Packages beyond the SDK and has no lock file.
type Finding struct {
Module string
Packages []string
}
// Check reads every module under root/modules that builds a TypeScript artifact.
func Check(root string) ([]Finding, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Build struct {
Artifacts []struct {
Language string `json:"language"`
} `json:"artifacts"`
} `json:"build"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
typescript := false
for _, a := range m.Build.Artifacts {
typescript = typescript || a.Language == "typescript"
}
if !typescript {
continue
}
dir := filepath.Dir(path)
pkgRaw, err := os.ReadFile(filepath.Join(dir, "package.json"))
if os.IsNotExist(err) {
continue
}
if err != nil {
return nil, err
}
var pkg struct {
Dependencies map[string]json.RawMessage `json:"dependencies"`
}
if err := json.Unmarshal(pkgRaw, &pkg); err != nil {
return nil, fmt.Errorf("%s: package.json: %w", m.Module, err)
}
var names []string
for n := range pkg.Dependencies {
if n != "@novox/mesh-sdk" && n != "typescript" && !strings.HasPrefix(n, "@types/") {
names = append(names, n)
}
}
if len(names) == 0 {
continue
}
if _, err := os.Stat(filepath.Join(dir, "package-lock.json")); err != nil {
sort.Strings(names)
out = append(out, Finding{Module: m.Module, Packages: names})
}
}
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
fmt.Printf("%s names packages beyond the SDK (%s) and has no package-lock.json: commit one made by the build module's toolchain (novox/hq ADR 0469 §5)\n", f.Module, strings.Join(f.Packages, ", "))
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("locks: every TypeScript module that names packages beyond the SDK commits its lock")
}
+75
View File
@@ -0,0 +1,75 @@
package main
import (
"os"
"path/filepath"
"reflect"
"testing"
)
// module writes modules/<name> with a TypeScript artifact, the package.json given ("" for none) and, when lock, a lock.
func module(t *testing.T, root, name, language, pkg string, lock bool) {
t.Helper()
dir := filepath.Join(root, "modules", name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
write := func(file, body string) {
if err := os.WriteFile(filepath.Join(dir, file), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
write("module.json", `{"module":"`+name+`","build":{"artifacts":[{"name":"tools","kind":"bundle","language":"`+language+`","from":"index.ts"}]}}`)
if pkg != "" {
write("package.json", pkg)
}
if lock {
write("package-lock.json", "{}")
}
}
const sdkOnly = `{"dependencies":{"@novox/mesh-sdk":"^0.1.1"},"devDependencies":{"typescript":"^5","@types/node":"^22"}}`
const withPg = `{"dependencies":{"@novox/mesh-sdk":"^0.1.1","pg":"^8","@types/pg":"^8"}}`
func TestAModuleNamingOnlyTheSDKNeedsNoLock(t *testing.T) {
root := t.TempDir()
module(t, root, "plain", "typescript", sdkOnly, false)
module(t, root, "none", "typescript", "", false)
got, err := Check(root)
if err != nil || len(got) != 0 {
t.Fatalf("%v %v", got, err)
}
}
func TestAModuleNamingMorePackagesWithoutALockIsRefusedNamingThem(t *testing.T) {
root := t.TempDir()
module(t, root, "db", "typescript", withPg, false)
module(t, root, "db-locked", "typescript", withPg, true)
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
if want := []Finding{{Module: "db", Packages: []string{"pg"}}}; !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestAModuleThatIsNotTypeScriptIsNotJudged(t *testing.T) {
root := t.TempDir()
module(t, root, "go", "go", withPg, false)
if got, err := Check(root); err != nil || len(got) != 0 {
t.Fatalf("%v %v", got, err)
}
}
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): every TypeScript module that names packages
// beyond the SDK commits its lock.
func TestTheCatalogueCommitsTheLocksItNeeds(t *testing.T) {
found, err := Check("../..")
if err != nil {
t.Fatal(err)
}
for _, f := range found {
t.Errorf("%s names packages beyond the SDK (%v) and has no package-lock.json: commit one made by the build module's toolchain (novox/hq ADR 0469 §5)", f.Module, f.Packages)
}
}
+7
View File
@@ -0,0 +1,7 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest, a TypeScript module's package.json and whether its lock is there.
modules/*/module.json
modules/*/package.json
modules/*/package-lock.json
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest.
modules/*/module.json
+5
View File
@@ -0,0 +1,5 @@
module checks/manifests
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
+2
View File
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+61
View File
@@ -0,0 +1,61 @@
// Package manifests judges every module's manifest as the node-engine reads it, and the catalogue as one shelf: each
// modules/*/module.json through mesh-sdk's ParseManifestWith against the predefined.json at the root, with its templates
// read from the tree beside it (WithTemplatesFrom), then the rules between manifests (CatalogueProblems,
// IdentityProblems, DataProblems).
//
// Why it is here (issue 774): on 2026-10-30 mesh-catalog ca8d6f33 gave power's bar item two attributes its bar places
// by none of. ParseManifest refuses that, and every node-engine refused to compose at 19:13; no check of the pull
// request had parsed the manifest. That judgement was mesh-controller's `module check`, which the merge gate ran over
// every manifest before the checker moved to mesh-delivery; the checker runs this repository's Go tests and the
// composition, and no test parsed a manifest whole. This one does, in the same functions the nodes use.
package manifests
import (
"fmt"
"os"
"path/filepath"
"sort"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Problems is what is wrong with the manifests under root/modules, one line each: a manifest the mesh refuses, named by
// its path, then the rules between the manifests that parse.
func Problems(root string) ([]string, error) {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(files)
list, err := catalogue.ReadPredefined(root)
if err != nil {
return nil, err
}
var out []string
shelf := catalogue.Shelf{}
for _, f := range files {
raw, err := os.ReadFile(f)
if err != nil {
return nil, err
}
rel, _ := filepath.Rel(root, f)
m, err := catalogue.ParseManifestWith(raw, list)
if err == nil {
m, err = catalogue.WithTemplatesFrom(m, filepath.Dir(f))
}
if err != nil {
out = append(out, fmt.Sprintf("%s: %v", rel, err))
continue
}
if _, twice := shelf[m.Module]; twice {
out = append(out, fmt.Sprintf("%s: %s was already given; two manifests name one module", rel, m.Module))
continue
}
shelf[m.Module] = m
}
between := catalogue.CatalogueProblems(shelf)
between = append(between, catalogue.IdentityProblems(shelf, catalogue.DefaultLongestMachine)...)
between = append(between, catalogue.DataProblems(shelf)...)
sort.Strings(between)
return append(out, between...), nil
}
+51
View File
@@ -0,0 +1,51 @@
package manifests
import (
"os"
"path/filepath"
"strings"
"testing"
)
// Every manifest of the catalogue is one the nodes can use, and the catalogue holds together as one shelf. A row is a
// manifest written into a catalogue of its own: power's bar item as mesh-catalog ca8d6f33 gave it (issue 774) is
// refused naming the attribute, and the same item without it passes.
func TestEveryManifestIsOneTheNodesCanUse(t *testing.T) {
for _, row := range []struct {
name, attributes, refused string
}{
{name: "the bar places by bar and place", attributes: `"bar":"bottom","place":"status","order":"45"`},
{name: "an attribute the bar places by none of (ca8d6f33)",
attributes: `"bar":"bottom","place":"status","order":"45","shows":"state","state":"power.draw"`,
refused: `"shows" is none of them`},
} {
root := t.TempDir()
dir := filepath.Join(root, "modules", "bar-user")
if err := os.MkdirAll(filepath.Join(dir, "files"), 0o755); err != nil {
t.Fatal(err)
}
manifest := `{"module":"bar-user","version":"1","moved":true,` +
`"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` +
`"items":[{"collection":"contribution:node-bar:block","attributes":{` + row.attributes + `},"template":"files/bar"}]}`
for path, body := range map[string]string{"module.json": manifest, "files/bar": "{\"shows\":\"command\"}\n"} {
if err := os.WriteFile(filepath.Join(dir, path), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Problems(root)
if err != nil {
t.Fatal(err)
}
joined := strings.Join(got, "\n")
if (row.refused == "") != (len(got) == 0) || !strings.Contains(joined, row.refused) {
t.Errorf("%s: refused %q, want %q", row.name, joined, row.refused)
}
}
real, err := Problems(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
if len(real) != 0 {
t.Fatalf("the catalogue holds manifests the nodes cannot use:\n%s", strings.Join(real, "\n"))
}
}
+4
View File
@@ -0,0 +1,4 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every module's directory (its manifest and the templates beside it), and the list of predefined keys.
modules
predefined.json
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/nobinaries
go 1.22
+84
View File
@@ -0,0 +1,84 @@
// Package nobinaries refuses a compiled program anywhere in the repository (issue 1577; first a checks/ binary slipped past a modules/-only rule): it is build output, made by the module's build step,
// and `go build` in a module's folder writes one beside go.mod where `git add -A` takes it.
package nobinaries
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Kind says what executable format the first bytes of a file are, or "" when they are none.
func Kind(head []byte) string {
switch {
case bytes.HasPrefix(head, []byte("\x7fELF")):
return "ELF"
case bytes.HasPrefix(head, []byte("MZ")):
return "PE"
case bytes.HasPrefix(head, []byte{0xcf, 0xfa, 0xed, 0xfe}), bytes.HasPrefix(head, []byte{0xce, 0xfa, 0xed, 0xfe}),
bytes.HasPrefix(head, []byte{0xfe, 0xed, 0xfa, 0xcf}), bytes.HasPrefix(head, []byte{0xfe, 0xed, 0xfa, 0xce}),
bytes.HasPrefix(head, []byte{0xca, 0xfe, 0xba, 0xbe}) && len(head) >= 8 && head[7] < 0x20 && head[6] == 0 && head[5] == 0 && head[4] == 0:
return "Mach-O"
}
return ""
}
// Files are the files of the catalogue's root that the change holds, anywhere: the tracked ones when the root is a git
// checkout (an ignored binary a local build left is not the change's), else every file there but .git's.
func Files(root string) ([]string, error) {
cmd := exec.Command("git", "-C", root, "ls-files", "-z")
if out, err := cmd.Output(); err == nil && len(out) > 0 {
var files []string
for _, f := range strings.Split(strings.TrimRight(string(out), "\x00"), "\x00") {
if _, err := os.Lstat(filepath.Join(root, f)); err == nil { // a file the change deletes is not there
files = append(files, f)
}
}
return files, nil
}
var files []string
err := filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && d.Name() == ".git" {
return filepath.SkipDir
}
if d.IsDir() || !d.Type().IsRegular() {
return nil
}
rel, _ := filepath.Rel(root, p)
files = append(files, rel)
return nil
})
return files, err
}
// Find names each file of files (paths from root) that is a compiled program, with its format.
func Find(root string, files []string) ([]string, error) {
var found []string
for _, f := range files {
p := filepath.Join(root, f)
if st, err := os.Lstat(p); err != nil || !st.Mode().IsRegular() {
continue
}
fh, err := os.Open(p)
if err != nil {
return nil, err
}
head := make([]byte, 8)
n, err := io.ReadFull(fh, head)
fh.Close()
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
return nil, err
}
if k := Kind(head[:n]); k != "" {
found = append(found, fmt.Sprintf("%s (%s)", f, k))
}
}
return found, nil
}
+89
View File
@@ -0,0 +1,89 @@
package nobinaries
import (
"os"
"os/exec"
"path/filepath"
"testing"
)
func TestFindNamesEachCompiledProgramAndNothingElse(t *testing.T) {
root := t.TempDir()
files := map[string][]byte{
"modules/a/a": []byte("\x7fELF\x02\x01\x01\x00rest"),
"modules/b/cmd/b/b.exe": []byte("MZ\x90\x00"),
"modules/c/c": {0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1},
"modules/d/main.go": []byte("package main\n"),
"modules/e/short": []byte("\x7f"),
"modules/f/empty": {},
"modules/g/Foo.class": {0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 52}, // Java, not Mach-O
"checks/words/words": []byte("\x7fELF\x02\x01\x01\x00rest"), // outside modules/ too
}
var names []string
for f, b := range files {
p := filepath.Join(root, f)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, b, 0o644); err != nil {
t.Fatal(err)
}
names = append(names, f)
}
got, err := Find(root, names)
if err != nil {
t.Fatal(err)
}
if len(got) != 4 {
t.Fatalf("found %v; wanted modules/a/a, modules/b/cmd/b/b.exe and modules/c/c and checks/words/words only", got)
}
}
// The catalogue's own tree holds no compiled program: a compiled program is build output, so remove it and add it to .gitignore.
func TestTheCatalogueHoldsNoCompiledProgram(t *testing.T) {
root := filepath.Join("..", "..")
files, err := Files(root)
if err != nil {
t.Fatal(err)
}
found, err := Find(root, files)
if err != nil {
t.Fatal(err)
}
for _, f := range found {
t.Errorf("%s: a compiled program is build output: remove it and add it to .gitignore (issue 1577)", f)
}
}
// A compiled program outside modules/ is refused too (a binary under checks/ slipped past the modules/-only rule): Files holds
// every tracked file of a git checkout and every file of a plain folder, and Find names the binary in both.
func TestAProgramUnderChecksIsRefused(t *testing.T) {
for _, git := range []bool{true, false} {
root := t.TempDir()
p := filepath.Join(root, "checks", "words", "words")
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte("\x7fELF\x02\x01\x01\x00rest"), 0o644); err != nil {
t.Fatal(err)
}
if git {
for _, args := range [][]string{{"init", "-q"}, {"add", "-A"}} {
if out, err := exec.Command("git", append([]string{"-C", root}, args...)...).CombinedOutput(); err != nil {
t.Fatalf("git %v: %v: %s", args, err, out)
}
}
}
files, err := Files(root)
if err != nil {
t.Fatal(err)
}
found, err := Find(root, files)
if err != nil {
t.Fatal(err)
}
if len(found) != 1 {
t.Errorf("git=%v: found %v; wanted checks/words/words (ELF)", git, found)
}
}
}
+1
View File
@@ -0,0 +1 @@
# every tracked file of the repository (git ls-files, no path limit): the whole tree, so this package's verdict is never kept.
+4
View File
@@ -0,0 +1,4 @@
# The modules of this catalogue that still read the controller's assignments (mesh-controller.assignments), each with
# why. The check of this directory (noplacement) refuses any other module that reads them, and an entry here whose module no longer
# does: the list only shrinks, and it is empty before the controller's lease is written to a holder that is no
# controller (novox/hq ADR 0549 §3, §6; issue 774).
+7
View File
@@ -0,0 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/noplacement
go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
+170
View File
@@ -0,0 +1,170 @@
// Command noplacement refuses what the repository path of placement left behind (novox/hq ADR 0567 §10, §12).
//
// Placement, which module runs on which node, is bus state held by the seat mesh-placement; no file of the
// catalogue says it, and no manifest reads it from the delivery indexer, which no longer keeps it. It refuses:
//
// 1. a `placement` directory (or file) at the root of the catalogue;
//
// 2. a `particulars` file at the root (the mesh's node names come from the checker's run, §16);
//
// 3. a manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…`
// or `mesh-delivery-indexer_placement…`) in any string value, such as `reads`;
//
// 4. a manifest, other than mesh-controller's, that reads the controller's assignments (`mesh-controller.assignments`):
// every reader takes placement from the seat mesh-placement (novox/hq ADR 0549 §3, ADR 0567), and the lease write is
// refused while one still reads them (ADR 0549 §6; issue 774). The modules that still read them for what the
// placement does not carry are named in checks/noplacement/assignments-readers, each with why; that list only
// shrinks: an entry whose module no longer reads them is refused too.
//
// go run . <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// Prefixes are the names of the indexer's placement parts; a manifest string starting with one names them.
var Prefixes = []string{"mesh-delivery-indexer.placement", "mesh-delivery-indexer_placement"}
// Findings is every refusal in the catalogue at root, sorted.
func Findings(root string) ([]string, error) {
var out []string
if _, err := os.Lstat(filepath.Join(root, "placement")); err == nil {
out = append(out, "placement: the repository holds no placement; it is bus state of the seat mesh-placement (novox/hq ADR 0567 §10)")
}
if _, err := os.Lstat(filepath.Join(root, "particulars")); err == nil {
out = append(out, "particulars: the repository lists no node names; the checker hands them to the check (novox/hq ADR 0567 §16)")
}
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
for _, file := range manifests {
raw, err := os.ReadFile(file)
if err != nil {
return nil, err
}
var doc any
if err := json.Unmarshal(raw, &doc); err != nil {
return nil, fmt.Errorf("%s: %w", file, err)
}
module := filepath.Base(filepath.Dir(file))
for _, s := range named(doc, nil) {
out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-placement's (novox/hq ADR 0567 §12)", module, s))
}
}
readers, err := assignmentsReaders(root)
if err != nil {
return nil, err
}
out = append(out, readers...)
sort.Strings(out)
return out, nil
}
// AssignmentsRead is the controller's assignments as a manifest reads them, and Allowed the file, relative to the
// catalogue's root, that names the modules still reading them: `<module> <why>` per line, `#` a comment.
const (
AssignmentsRead = "mesh-controller.assignments"
Allowed = "checks/noplacement/assignments-readers"
)
// assignmentsReaders is every finding on the reads of the controller's assignments: a module other than the controller
// reading them that Allowed does not name, and a module Allowed names that no longer reads them (issue 774).
func assignmentsReaders(root string) ([]string, error) {
allowed := map[string]bool{}
raw, err := os.ReadFile(filepath.Join(root, Allowed))
if err != nil && !os.IsNotExist(err) {
return nil, err
}
for _, line := range strings.Split(string(raw), "\n") {
if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") {
allowed[f[0]] = true
}
}
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
var found []string
reads := map[string]bool{}
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Reads []string `json:"reads"`
}
if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" {
continue // a manifest that does not read is the manifest checks' to refuse
}
for _, r := range m.Reads {
if strings.TrimSpace(r) != AssignmentsRead {
continue
}
reads[m.Module] = true
if !allowed[m.Module] {
found = append(found, fmt.Sprintf("%s reads %s: read where modules run from mesh-delivery-assigner.placement, "+
"the seat mesh-placement's (novox/hq ADR 0549 §3, ADR 0567); the lease write is refused while a module reads "+
"the assignments (ADR 0549 §6)", m.Module, AssignmentsRead))
}
}
}
for module := range allowed {
if !reads[module] {
found = append(found, fmt.Sprintf("%s names %s, which no longer reads %s: take it off the list, which only shrinks",
Allowed, module, AssignmentsRead))
}
}
return found, nil
}
// named collects the strings of a decoded manifest that start with one of the Prefixes.
func named(v any, into []string) []string {
switch v := v.(type) {
case string:
for _, p := range Prefixes {
if strings.HasPrefix(v, p) {
return append(into, v)
}
}
case []any:
for _, e := range v {
into = named(e, into)
}
case map[string]any:
for _, e := range v {
into = named(e, into)
}
}
return into
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: noplacement <catalogue root>")
os.Exit(2)
}
found, err := Findings(os.Args[1])
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, f := range found {
fmt.Println(f)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("no placement directory, no manifest reads the indexer's placement, and only the modules listed read the controller's assignments")
}
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-catalog/checks/trunk"
)
func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
for _, row := range []struct {
name string
files map[string]string
want string // empty: clean
}{
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-assigner.placement"]}`}, ""},
{"a placement directory", map[string]string{"placement/ace.json": `{}`}, "placement: the repository holds no placement"},
{"a particulars file", map[string]string{"particulars": "ace\n"}, "particulars: the repository lists no node names"},
{"a read of the indexer's placement", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-indexer.placement"]}`}, `a: names "mesh-delivery-indexer.placement"`},
{"a read of its candidates", map[string]string{"modules/b/module.json": `{"module":"b","reads":["mesh-delivery-indexer.placement-candidates"]}`}, "placement-candidates"},
{"its event", map[string]string{"modules/c/module.json": `{"module":"c","on":[{"event":"mesh-delivery-indexer.placement-changed"}]}`}, "placement-changed"},
// Issue 774 (ADR 0549 §3, §6): a reader of the controller's assignments is refused unless the list names it, the
// controller's own manifest is not judged, and a name on the list whose module no longer reads them is refused.
{"an unlisted reader of the assignments", map[string]string{"modules/r/module.json": `{"module":"r","reads":["mesh-controller.assignments"]}`}, "r reads mesh-controller.assignments"},
{"a listed reader and the controller", map[string]string{"modules/l/module.json": `{"module":"l","reads":["mesh-controller.assignments"]}`,
"modules/mesh-controller/module.json": `{"module":"mesh-controller","reads":["mesh-controller.assignments"]}`, Allowed: "# why\nl its plan\n"}, ""},
{"a listed module that reads them no more", map[string]string{Allowed: "gone no longer reads\n"}, "names gone, which no longer reads"},
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-placement.placement"]}`}, ""},
} {
t.Run(row.name, func(t *testing.T) {
root := t.TempDir()
for path, text := range row.files {
p := filepath.Join(root, path)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Findings(root)
if err != nil {
t.Fatal(err)
}
if all := strings.Join(got, "\n"); (row.want == "") != (all == "") || !strings.Contains(all, row.want) {
t.Fatalf("findings = %q, want one containing %q", all, row.want)
}
})
}
}
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2).
func TestTheCatalogueHoldsNoPlacementOfTheRepositoryPath(t *testing.T) {
got, err := Findings(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
for _, f := range got {
t.Error(f)
}
// Issue 774 (ADR 0549 §3, §6; the comparison of novox/hq ADR 0529 clause 2): the list of modules still reading the
// controller's assignments gains none the trunk's copy at the merge base does not hold. Its first copy gains nothing.
path, ok := trunk.Copy(t, Allowed)
if !ok {
return
}
names := func(path string) map[string]bool {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
out := map[string]bool{}
for _, line := range strings.Split(string(raw), "\n") {
if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") {
out[f[0]] = true
}
}
return out
}
base := names(path)
for module := range names(filepath.Join("..", "..", Allowed)) {
if !base[module] {
t.Errorf("%s gains %s, which the trunk's copy does not hold: the list only shrinks", Allowed, module)
}
}
}
+6
View File
@@ -0,0 +1,6 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# the root, for a placement directory, and every manifest, for a read of the indexer's placement states.
?placement
modules/*/module.json
+3
View File
@@ -0,0 +1,3 @@
module checks/noscripts
go 1.22
+19
View File
@@ -0,0 +1,19 @@
package noscripts
import (
"errors"
"os"
"path/filepath"
"testing"
)
// The catalogue's checks are Go tests (novox/hq ADR 0529 clause 5, ADR 0445): the checker runs every nested Go module's tests by
// the language's convention and no script at the root says what else to run. A script back at the root would be a check nobody
// runs (ADR 0503 §3: since R4e the checker has not run them), so it is refused.
func TestNoScriptOfTheOldCheckStandsAtTheRoot(t *testing.T) {
for _, name := range []string{"merge-check.sh", "merge-check-typescript.sh", "check-nested-modules.sh"} {
if _, err := os.Stat(filepath.Join("..", "..", name)); !errors.Is(err, os.ErrNotExist) {
t.Errorf("%s stands at the catalogue's root (%v): what it ran is a Go test in checks/ or a step of the build module", name, err)
}
}
}
+7
View File
@@ -0,0 +1,7 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# that none of the old check scripts stands at the root.
?merge-check.sh
?merge-check-typescript.sh
?check-nested-modules.sh
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/owned
go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
+181 -13
View File
@@ -11,13 +11,23 @@
// another module, after ${dir:<id>} is resolved within each module and both homes are read as one root.
// Archives are not containers for this rule. This holds for ~/.ssh what checks/sshdir held (ADR 0415 §6).
//
// go run . <catalogue root>
// 5. a directory that is, lies inside or holds a directory a system package owns under /var/lib, or any other
// resource inside one (issue 1841: pacman's state directory, placed with "place": ".", was /var/lib/pacman, the
// package manager's own database directory). A placed directory is resolved as the mesh resolves it with the
// default data root: "." is /var/lib/<module>, "mesh" /var/lib/mesh/<module>, neither /var/lib/<module>/<id>.
// The system directories are checks/owned/system, captured from a machine (pacman -Qo, /usr/lib/tmpfiles.d);
// checks/owned/system-known names the exceptions still to fix, by module, resource and issue. That list only
// shrinks: an entry that no longer collides is refused, and with -base one the trunk's copy does not hold.
//
// go run . [-base <trunk's system-known>] <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path"
@@ -27,6 +37,12 @@ import (
"strings"
)
// The lists of rule 5, relative to the catalogue's root.
const (
SystemList = "checks/owned/system"
SystemKnown = "checks/owned/system-known"
)
// Home is the one root both machine homes are read as.
const Home = "${home}"
@@ -43,9 +59,9 @@ type Finding struct {
}
type resource struct {
ID, Type, Path, Unknown string
Knows []string
HasKnows bool
ID, Type, Path, Unknown, Place string
Knows []string
HasKnows bool
}
type parsed struct {
@@ -65,6 +81,7 @@ func load(file string) (*parsed, error) {
ID string `json:"id"`
Type string `json:"type"`
Path string `json:"path"`
Place string `json:"place"`
Unknown *string `json:"unknown"`
Knows []string `json:"knows"`
} `json:"resources"`
@@ -81,7 +98,7 @@ func load(file string) (*parsed, error) {
out.Module = filepath.Base(filepath.Dir(file))
}
for _, r := range m.Resources {
x := resource{ID: r.ID, Type: r.Type, Path: r.Path, Knows: r.Knows, HasKnows: r.Knows != nil}
x := resource{ID: r.ID, Type: r.Type, Path: r.Path, Place: r.Place, Knows: r.Knows, HasKnows: r.Knows != nil}
if r.Unknown != nil {
x.Unknown = *r.Unknown
if x.Unknown == "" {
@@ -143,14 +160,23 @@ type declared struct {
}
// Check reads every manifest under root/modules and answers what it refuses.
func Check(root string) ([]Finding, error) {
func Check(root string, base []string) ([]Finding, error) {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(files)
system, err := readSystem(filepath.Join(root, SystemList))
if err != nil {
return nil, err
}
known, err := readLines(filepath.Join(root, SystemKnown))
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
}
var out []Finding
var paths []declared
collided := map[string]bool{}
type container struct{ module, path string }
var containers []container
for _, file := range files {
@@ -170,6 +196,7 @@ func Check(root string) ([]Finding, error) {
dirs[r.ID] = "/placed/" + m.Module + "/" + r.ID
}
}
out = append(out, inSystem(m, system, known, collided)...)
for _, r := range m.Resources {
p := norm(r.Path, dirs)
if p != "" {
@@ -212,6 +239,28 @@ func Check(root string) ([]Finding, error) {
}
}
}
for _, k := range known {
if !collided[k] {
module, id, _ := strings.Cut(k, " ")
id, _, _ = strings.Cut(id, " ")
out = append(out, Finding{5, module, id, fmt.Sprintf("is listed in %s, and no longer lies in a system "+
"package's directory: take the line out, so the list only shrinks", SystemKnown)})
}
}
if base != nil {
held := map[string]bool{}
for _, b := range base {
held[b] = true
}
for _, k := range known {
if !held[k] {
module, id, _ := strings.Cut(k, " ")
id, _, _ = strings.Cut(id, " ")
out = append(out, Finding{5, module, id, fmt.Sprintf("is a new line in %s, which the trunk's copy "+
"does not hold: a module's directory is placed outside a system package's, never listed", SystemKnown)})
}
}
}
for _, d := range paths {
for _, c := range containers {
if c.module != d.module && (d.path == c.path || strings.HasPrefix(d.path, c.path+"/")) {
@@ -222,21 +271,140 @@ func Check(root string) ([]Finding, error) {
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
// systemDir is one directory a system package owns, and what says so.
type systemDir struct{ Path, Source string }
// readLines is a list's lines, without comments and blank lines.
func readLines(file string) ([]string, error) {
raw, err := os.ReadFile(file)
if err != nil {
return nil, err
}
found, err := Check(root)
var lines []string
for _, line := range strings.Split(string(raw), "\n") {
if line = strings.TrimSpace(line); line != "" && !strings.HasPrefix(line, "#") {
lines = append(lines, line)
}
}
return lines, nil
}
// readSystem is the list of system directories: "<absolute path> <package>: <how it was read>" per line. Absent or
// empty, the check fails: a list that says nothing would let every module through.
func readSystem(file string) ([]systemDir, error) {
lines, err := readLines(file)
if err != nil {
return nil, fmt.Errorf("rule 5 reads the system packages' directories from %s: %w", file, err)
}
if len(lines) == 0 {
return nil, fmt.Errorf("%s names no system package's directory, so rule 5 would pass everything", file)
}
var out []systemDir
for _, line := range lines {
p, source, _ := strings.Cut(line, " ")
if !strings.HasPrefix(p, "/var/lib/") || strings.TrimSpace(source) == "" {
return nil, fmt.Errorf("%s: %q is not \"/var/lib/<directory> <package>: <how it was read>\"", file, line)
}
out = append(out, systemDir{path.Clean(p), strings.TrimSpace(source)})
}
return out, nil
}
// placedAt is where the mesh places a pathless directory with the default data root (mesh-sdk catalogue dirsFor).
func placedAt(module string, r resource) string {
switch r.Place {
case ".":
return "/var/lib/" + module
case "mesh":
return "/var/lib/mesh/" + module
}
return "/var/lib/" + module + "/" + r.ID
}
// inSystem is rule 5 for one module: each resource whose resolved path collides with a system package's directory,
// unless system-known lists it (then it is marked collided, so a stale line is found).
func inSystem(m *parsed, system []systemDir, known []string, collided map[string]bool) []Finding {
real := map[string]string{}
for _, r := range m.Resources {
if r.Type == "directory" && r.Path == "" && r.ID != "" {
real[r.ID] = placedAt(m.Module, r)
}
}
for _, r := range m.Resources {
if r.ID != "" && r.Path != "" {
real[r.ID] = r.Path
}
}
var out []Finding
for _, r := range m.Resources {
p := ""
if r.Path != "" {
p = norm(r.Path, real)
} else if r.Type == "directory" {
p = real[r.ID]
}
if !strings.HasPrefix(p, "/") {
continue
}
for _, s := range system {
inside := p == s.Path || strings.HasPrefix(p, s.Path+"/")
holds := r.Type == "directory" && strings.HasPrefix(s.Path, p+"/")
if !inside && !holds {
continue
}
listed := ""
for _, k := range known {
if strings.HasPrefix(k, m.Module+" "+r.ID+" ") || k == m.Module+" "+r.ID {
listed = k
}
}
if listed != "" {
collided[listed] = true
break
}
how := "is or lies inside"
if holds {
how = "holds"
}
out = append(out, Finding{5, m.Module, r.ID, fmt.Sprintf("resolves to %q, which %s %s, the directory of the "+
"system package %s; place it under /var/lib/mesh/%s (\"place\": \"mesh\") or a path of its own", p, how,
s.Path, s.Source, m.Module)})
break
}
}
return out
}
func main() {
baseFile := flag.String("base", "", "the trunk's copy of "+SystemKnown+": a line it does not hold is refused")
flag.Parse()
root := "."
if flag.NArg() > 0 {
root = flag.Arg(0)
}
var base []string
if *baseFile != "" {
lines, err := readLines(*baseFile)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
base = append([]string{}, lines...)
}
found, err := Check(root, base)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
fmt.Printf("%s: %s: %s (rule %d, novox/hq ADR 0422 §8, issue 1024)\n", f.Module, f.ID, f.Why, f.Rule)
cite := "novox/hq ADR 0422 §8, issue 1024"
if f.Rule == 5 {
cite = "issue 1841"
}
fmt.Printf("%s: %s: %s (rule %d, %s)\n", f.Module, f.ID, f.Why, f.Rule, cite)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("owned paths: every directory and archive says how it treats unknown files; no module declares inside another's directory")
fmt.Println("owned paths: every directory and archive says how it treats unknown files; no module declares inside another's directory, or in a system package's")
}
+102 -1
View File
@@ -20,7 +20,10 @@ func manifest(t *testing.T, root, module, body string) {
func check(t *testing.T, root string) []Finding {
t.Helper()
found, err := Check(root)
if _, err := os.Stat(filepath.Join(root, SystemList)); os.IsNotExist(err) {
lists(t, root, "/var/lib/pacman pacman: pacman -Qo\n", "")
}
found, err := Check(root, nil)
if err != nil {
t.Fatal(err)
}
@@ -35,6 +38,20 @@ func only(t *testing.T, found []Finding, rule int, module, id string) {
}
}
// lists writes the catalogue's list of system directories and its known exceptions.
func lists(t *testing.T, root, system, known string) {
t.Helper()
if err := os.MkdirAll(filepath.Join(root, "checks", "owned"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, SystemList), []byte(system), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, SystemKnown), []byte(known), 0o644); err != nil {
t.Fatal(err)
}
}
func dir(id, path, rest string) string {
return `{"id":"` + id + `","type":"directory","path":"` + path + `"` + rest + `}`
}
@@ -145,3 +162,87 @@ func TestPlacedDirectoriesOfTwoModulesAreTheirOwn(t *testing.T) {
t.Fatalf("refused %+v", found)
}
}
// 1841/AC18: a module's directory that resolves to a /var/lib directory a system package owns, lies inside one or
// holds one, is refused, whether placed (place ".", or by default beneath the module's own) or stated; a placed
// directory saying place "mesh" lies under /var/lib/mesh and passes (issue 1841: pacman's state was /var/lib/pacman).
func TestADirectoryOfASystemPackageIsRefused(t *testing.T) {
system := "/var/lib/pacman pacman: pacman -Qo\n/var/lib/systemd/coredump systemd: tmpfiles.d/systemd.conf\n"
for name, resources := range map[string][]string{
"placed-own": {`{"id":"state","type":"directory","place":".","unknown":"program"}`},
"placed-beneath": {`{"id":"coredump","type":"directory","unknown":"program"}`},
"stated-inside": {dir("d", "/var/lib/pacman/keeper", `,"unknown":"program"`)},
"stated-holding": {dir("d", "/var/lib/systemd", `,"unknown":"program"`)},
"file-inside": {`{"id":"state","type":"directory","place":"mesh","unknown":"program"}`,
`{"id":"f","type":"file","path":"/var/lib/pacman/keeper.json"}`},
} {
module := map[string]string{"placed-own": "pacman", "placed-beneath": "systemd"}[name]
if module == "" {
module = "keeper"
}
root := t.TempDir()
lists(t, root, system, "")
manifest(t, root, module, mod(module, resources...))
found := check(t, root)
if len(found) != 1 || found[0].Rule != 5 || found[0].Module != module ||
!strings.Contains(found[0].Why, "/var/lib/") || !strings.Contains(found[0].Why, "package") {
t.Errorf("%s: found %+v; want one finding of rule 5 for %s naming the path and the package", name, found, module)
}
}
root := t.TempDir()
lists(t, root, system, "")
manifest(t, root, "pacman", mod("pacman",
`{"id":"state","type":"directory","place":"mesh","unknown":"program"}`,
`{"id":"f","type":"file","path":"${dir:state}/keeper.json"}`,
dir("other", "/var/lib/pacman-other", `,"unknown":"program"`)))
if found := check(t, root); len(found) != 0 {
t.Fatalf("a directory under /var/lib/mesh/pacman was refused: %+v", found)
}
}
// 1841/AC18: a known exception passes; one that no longer collides is refused, so the list only shrinks; one the trunk's
// copy does not hold is refused; and an absent list of system directories fails the check rather than passing it.
func TestKnownSystemExceptionsOnlyShrink(t *testing.T) {
system := "/var/lib/systemd systemd: tmpfiles.d/systemd.conf\n"
known := "# comment\nsystemd state issue 1873\n"
root := t.TempDir()
lists(t, root, system, known)
manifest(t, root, "systemd", mod("systemd", `{"id":"state","type":"directory","place":".","unknown":"program"}`))
if found := check(t, root); len(found) != 0 {
t.Fatalf("a known exception was refused: %+v", found)
}
if found, err := Check(root, []string{"systemd state issue 1873"}); err != nil || len(found) != 0 {
t.Fatalf("a known exception held by the trunk was refused: %+v %v", found, err)
}
if found, err := Check(root, []string{}); err != nil || len(found) != 1 || found[0].Rule != 5 {
t.Fatalf("an exception the trunk does not hold passed: %+v %v", found, err)
}
manifest(t, root, "systemd", mod("systemd", `{"id":"state","type":"directory","place":"mesh","unknown":"program"}`))
only(t, check(t, root), 5, "systemd", "state")
root = t.TempDir()
manifest(t, root, "a", mod("a"))
if err := os.MkdirAll(filepath.Join(root, "checks", "owned"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, SystemList), []byte("# nothing\n"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := Check(root, nil); err == nil {
t.Fatal("an empty list of system directories passed")
}
}
// 1841/AC18: the catalogue as it stands passes rule 5 with its own lists.
func TestTheCatalogueKeepsOutOfSystemDirectories(t *testing.T) {
found, err := Check("../..", nil)
if err != nil {
t.Fatal(err)
}
for _, f := range found {
if f.Rule == 5 {
t.Errorf("%s: %s: %s", f.Module, f.ID, f.Why)
}
}
}
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest; its lists are this package's own files.
modules/*/module.json
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"testing"
"git.novox.be/novox/mesh-catalog/checks/trunk"
)
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): every rule holds.
func TestTheCatalogueKeepsEveryRuleOfOwnedPaths(t *testing.T) {
found, err := Check("../..", nil)
if err != nil {
t.Fatal(err)
}
for _, f := range found {
t.Errorf("%s: %s: %s (rule %d, novox/hq ADR 0422 §8)", f.Module, f.ID, f.Why, f.Rule)
}
}
// And the list of system directories' known exceptions holds no line the trunk's copy at the merge base does not (rule 5). A trunk
// with none had no list: its first copy. Skipped, and said, where no trunk is beside.
func TestTheSystemListHoldsNoLineTheTrunksDoesNot(t *testing.T) {
path, ok := trunk.Copy(t, "checks/owned/system-known")
var base []string
if ok {
lines, err := readLines(path)
if err != nil {
t.Fatal(err)
}
base = append([]string{}, lines...)
}
found, err := Check("../..", base)
if err != nil {
t.Fatal(err)
}
for _, f := range found {
if f.Rule == 5 {
t.Errorf("%s: %s: %s (rule %d, issue 1841)", f.Module, f.ID, f.Why, f.Rule)
}
}
}
+23
View File
@@ -0,0 +1,23 @@
# The directories under /var/lib that a system package owns (rule 5 of checks/owned, issue 1841). A module's
# directory, placed or stated, may not be one of these, lie inside one or hold one: the package's program and the
# package manager keep their own files there, and the node-engine would judge, chmod or chown the directory.
#
# One line per directory: "<path> <package>: <how it was read>". Captured on the workstation on 2026-10-22, through
# the mesh: pacman's own database by `pacman -Qo` (pacman_owns), the rest from the `d`, `D` and `Q` lines of
# /usr/lib/tmpfiles.d (node-reads.search for "/var/lib/"). A directory found to belong to a package is added here.
/var/lib/pacman pacman: pacman -Qo /var/lib/pacman/ (pacman 7.1.0)
/var/lib/alsa alsa-utils: tmpfiles.d/alsa-utils.conf
/var/lib/colord colord: tmpfiles.d/colord.conf
/var/lib/containers podman: tmpfiles.d/podman.conf
/var/lib/dbus dbus: tmpfiles.d/dbus.conf
/var/lib/geoclue geoclue: tmpfiles.d/geoclue.conf
/var/lib/lastlog util-linux: tmpfiles.d/lastlog2.conf
/var/lib/libuuid util-linux: tmpfiles.d/uuidd.conf
/var/lib/machines systemd: tmpfiles.d/systemd-nspawn.conf
/var/lib/openvpn/chroot networkmanager-openvpn: tmpfiles.d/nm-openvpn-tmpfiles.conf
/var/lib/portables systemd: tmpfiles.d/portables.conf
/var/lib/postgres postgresql: tmpfiles.d/postgresql.conf
/var/lib/private systemd: tmpfiles.d/systemd.conf
/var/lib/rpcbind rpcbind: tmpfiles.d/rpcbind.conf
/var/lib/systemd systemd: tmpfiles.d/systemd.conf
/var/lib/tpm2-tss/system/keystore tpm2-tss: tmpfiles.d/tpm2-tss-fapi.conf
+5
View File
@@ -0,0 +1,5 @@
# Modules whose directory lies in a system package's directory today (rule 5 of checks/owned, issue 1841), each with
# the issue that moves it out: "<module> <resource id> issue <number>". This list only shrinks: a line whose module
# no longer collides is refused, and so is a line the trunk's copy does not hold.
postgres state issue 1873
systemd state issue 1873
+22 -7
View File
@@ -2,8 +2,9 @@
// issue 1838): a module is for any mesh, so no manifest, file, code string, identifier, comment, test name or
// fixture of a covered module names a node of this one.
//
// The names are in the file `particulars` at the catalogue's root, one a line, `#` comments — the one place
// they may be written. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
// The names are the mesh's own, not the repository's: the checker hands them in the environment value
// MESH_NODE_NAMES, comma-separated, from the node records it reads (novox/hq ADR 0567 §16). A run with none fails
// loudly, since it would pass on anything. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
// whole and in any case; a hyphen, an underscore or a CamelCase boundary parts one, so a longer word holding a name is another word.
// The forge is not a node name: its host (git.novox.be) and the organisation path (novox/<repo>) are blanked
// before a line is read.
@@ -34,14 +35,28 @@ func main() {
if len(os.Args) > 1 {
root = os.Args[1]
}
os.Exit(check(root))
os.Exit(check(root, namesFromEnv()))
}
// NamesEnv is the environment value that carries the mesh's node names (mesh-sdk delivery.NodeNamesEnv).
const NamesEnv = "MESH_NODE_NAMES"
// namesFromEnv is the names NamesEnv holds, comma-separated, trimmed, empty entries left out.
func namesFromEnv() []string {
var out []string
for _, n := range strings.Split(os.Getenv(NamesEnv), ",") {
if n = strings.TrimSpace(n); n != "" {
out = append(out, n)
}
}
return out
}
// check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable.
func check(root string) int {
names, err := readList(filepath.Join(root, "particulars"))
if err == nil && len(names) == 0 {
err = fmt.Errorf("particulars lists no name — the check would pass on anything")
func check(root string, names []string) int {
var err error
if len(names) == 0 {
err = fmt.Errorf("no node names (%s is empty) — the check would pass on anything", NamesEnv)
}
var covered []string
if err == nil {
+15 -7
View File
@@ -74,26 +74,24 @@ func TestANodeNamedPathIsRefusedThroughRun(t *testing.T) {
}
}
func TestCheckReturnsOneOnAFindingAndTwoOnAnUnreadableList(t *testing.T) {
func TestCheckReturnsOneOnAFindingAndTwoOnNoNames(t *testing.T) {
root := t.TempDir()
write := func(p, s string) {
p = filepath.Join(root, p)
os.MkdirAll(filepath.Dir(p), 0o755)
os.WriteFile(p, []byte(s), 0o644)
}
write("particulars", "ace\n")
write("checks/particulars/covered", "kernel\n")
write("modules/kernel/a", "clean\n")
if got := check(root); got != 0 {
if got := check(root, []string{"ace"}); got != 0 {
t.Errorf("clean = %d, want 0", got)
}
write("modules/kernel/a", "on ace\n")
if got := check(root); got != 1 {
if got := check(root, []string{"ace"}); got != 1 {
t.Errorf("finding = %d, want 1", got)
}
os.Remove(filepath.Join(root, "particulars"))
if got := check(root); got != 2 {
t.Errorf("no list = %d, want 2", got)
if got := check(root, nil); got != 2 {
t.Errorf("no names = %d, want 2", got)
}
}
@@ -114,3 +112,13 @@ func TestOnlyCoveredModulesAreRead(t *testing.T) {
t.Errorf("got %v", got)
}
}
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no node name of this mesh in a covered module.
func TestTheCoveredModulesNameNoNode(t *testing.T) {
if _, set := os.LookupEnv(NamesEnv); !set {
t.Skip("ADR 0567 §16: this checker hands no node names (before D14 is delivered); the skip goes once it is")
}
if code := check("../..", namesFromEnv()); code != 0 {
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
}
}
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# check("../.."): every file of the modules the covered list names. The node names come from the run (MESH_NODE_NAMES).
modules
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/paths
go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
+2 -1
View File
@@ -251,7 +251,8 @@ func main() {
}
var grown []string
if baseDir == "" {
fmt.Println("NOT COMPARED: no base branch's lists were given, so additions to them were not judged")
fmt.Fprintln(os.Stderr, "NOT COMPARED: no base branch's lists were given (-base <dir>), so additions to them were not judged; that is a failure, not a pass (issue 1295)")
os.Exit(2)
} else {
baseShared, _, err1 := list(filepath.Join(baseDir, "shared"))
baseKnown, _, err2 := list(filepath.Join(baseDir, "known"))
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest and every file of a module.
modules
+63
View File
@@ -0,0 +1,63 @@
package main
import (
"path/filepath"
"testing"
"git.novox.be/novox/mesh-catalog/checks/trunk"
)
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no module names another's state on its machine,
// and the known list holds only what still occurs.
func TestNoModuleNamesAnothersState(t *testing.T) {
root := filepath.Join("..", "..")
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
shared, _, err := list("shared")
if err != nil {
t.Fatal(err)
}
known, _, err := list("known")
if err != nil {
t.Fatal(err)
}
refused, gone := Judge(found, shared, known)
for _, f := range refused {
t.Error(f)
}
for _, k := range gone {
t.Errorf("%q is in checks/paths/known and no longer occurs: take it out, so it cannot come back", k)
}
}
// And neither list gains a line the trunk's copies at the merge base do not hold, unless the line says why after `# allow:`. Fails, saying
// NOT COMPARED, where no trunk is beside (issue 1295).
func TestTheListsOfPathsHoldNoLineTheTrunksDoNot(t *testing.T) {
sharedPath, okShared := trunk.Copy(t, "checks/paths/shared")
knownPath, okKnown := trunk.Copy(t, "checks/paths/known")
if !okShared || !okKnown {
t.Log("the trunk holds not both lists: their first copies, to which nothing was added")
return
}
shared, sharedWhy, err := list("shared")
if err != nil {
t.Fatal(err)
}
known, knownWhy, err := list("known")
if err != nil {
t.Fatal(err)
}
baseShared, _, err := list(sharedPath)
if err != nil {
t.Fatal(err)
}
baseKnown, _, err := list(knownPath)
if err != nil {
t.Fatal(err)
}
for _, g := range append(Grown("shared", shared, sharedWhy, baseShared), Grown("known", known, knownWhy, baseKnown)...) {
t.Error(g)
}
}
+1
View File
@@ -1,3 +1,4 @@
# Paths every program on a machine reads as the machine's own, not as a message from the module that
# writes them (checks/paths, novox/hq ADR 0260). One per line, with why.
/etc/hosts # every resolver on the machine reads it; the hostname module writes it for all of them
/var/lib/mesh-build-node-cache # allow: the Go build module gives its integration level the node's installed Node toolchains read-only, so the Node build module's tests build offline (issue 1891, ADR 0533)
+3
View File
@@ -0,0 +1,3 @@
module checks/planhands
go 1.26.0
+48
View File
@@ -0,0 +1,48 @@
// Package planhands refuses a module's line that takes a plan existing for its module handed to the provisioner (issue
// 774). Whether a plan hands its module is mesh-sdk's one rule, catalogue.Plan.Hands: since the seat mesh-provisioner keys a
// provision its provider's helper answers for a module not handed, a plan naming provisions alone exists for modules
// whose files and own secrets stay their node's (on 2026-10-30 the vault read every such plan as handed and answered
// twenty modules' own secrets to nobody, and the provisioner client refused each as a plan it could not act on). A line
// that tests whether a plan exists either asks Hands on the same line, or says "not handing": it reads only who binds
// whom, or that there is no plan at all.
package planhands
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// existence is a test of whether an assignment's or a plan's `provisioning` is there: nil, or an empty raw value.
var existence = regexp.MustCompile(`Provisioning\s*[!=]=\s*nil|len\([^)]*Provisioning\)\s*[!=]=\s*0`)
// Problems is each such line under root's modules/ that neither asks Hands nor says "not handing", as
// `path:line: text`, sorted. Test files are left out: a test may build a plan by hand.
func Problems(root string) ([]string, error) {
var out []string
err := filepath.WalkDir(filepath.Join(root, "modules"), func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") {
return nil
}
raw, err := os.ReadFile(p)
if err != nil {
return err
}
rel, _ := filepath.Rel(root, p)
for i, line := range strings.Split(string(raw), "\n") {
if existence.MatchString(line) && !strings.Contains(line, ".Hands()") && !strings.Contains(line, "not handing") {
out = append(out, fmt.Sprintf("%s:%d: %s", rel, i+1, strings.TrimSpace(line)))
}
}
return nil
})
sort.Strings(out)
return out, err
}
+45
View File
@@ -0,0 +1,45 @@
package planhands
import (
"os"
"path/filepath"
"strings"
"testing"
)
// A line that takes a plan existing for its module handed is refused and named; one that asks Hands, one marked "not
// handing" and any test file pass; and the real catalogue is clean.
func TestNoModuleTakesAPlanExistingForItsModuleHanded(t *testing.T) {
root := t.TempDir()
for f, body := range map[string]string{
"modules/vault/own.go": "if a.Provisioning != nil {\n",
"modules/client/plan.go": "if len(a.Provisioning) == 0 {\n",
"modules/vault/rule.go": "return a.Provisioning != nil && a.Provisioning.Hands(), nil\n",
"modules/client/binds.go": "if a.Provisioning != nil { // binds, not handing\n",
"modules/vault/own_test.go": "if a.Provisioning == nil {\n",
"modules/client/notgo/plan.md": "if a.Provisioning == nil {\n",
} {
p := filepath.Join(root, f)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Problems(root)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 || !strings.HasPrefix(got[0], "modules/client/plan.go:1:") || !strings.HasPrefix(got[1], "modules/vault/own.go:1:") {
t.Fatalf("refused %v, wanted the client's and the vault's existence tests alone", got)
}
real, err := Problems(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
if len(real) > 0 {
t.Errorf("a module takes a plan existing for its module handed; ask catalogue.Plan.Hands, or mark a line that reads "+
"only who binds whom \"not handing\":\n%s", strings.Join(real, "\n"))
}
}
+3
View File
@@ -0,0 +1,3 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every module whole (a directory is read whole, so a module's Go source at any depth enters the digest), for a line that takes a plan existing for its module handed.
modules
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/predefined
go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
+6
View File
@@ -0,0 +1,6 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# the root's list of predefined keys and every manifest that names one.
predefined.json
modules/*/module.json
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"path/filepath"
"sort"
"testing"
"git.novox.be/novox/mesh-catalog/checks/trunk"
)
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): predefined.json removes no key a module still
// names, judged against the trunk's copy at the merge base. A trunk with none had no keys; with no trunk to read, nothing can be
// seen removed, and the test says so.
func TestThePredefinedListRemovesNoKeyAModuleStillNames(t *testing.T) {
tree, err := Keys(filepath.Join("..", "..", File))
if err != nil {
t.Fatal(err)
}
path, ok := trunk.Copy(t, File)
if !ok {
t.Log("no trunk copy of " + File + ": no removal can be seen")
return
}
base, err := Keys(path)
if err != nil {
t.Fatal(err)
}
named, err := Named(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
removed := Removed(base, tree, named)
keys := make([]string, 0, len(removed))
for k := range removed {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
t.Errorf("%q is removed from %s and still named by %v", k, File, removed[k])
}
}
+5
View File
@@ -0,0 +1,5 @@
module checks/readable
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
+2
View File
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+333
View File
@@ -0,0 +1,333 @@
// Command readable refuses a catalogue where a module reads another module's state that the owner does not announce
// readable (novox/hq ADR 0495 clause 3, issue 1790): the controller grants a read only from the owner's announcement, so
// such a read is refused on the live mesh, said only in the controller's journal. The sweep of 2026-10-30 (issue 774)
// found three: gitea's `bound` (read by mesh-provisioner, so no forge account was bound), disk-load's `readings` (read by
// restic, nfs-server and mounts) and restic's `good` (read by the self-check). The merge gate's check of reads against
// the owner's manifest went with clause 5, and announcetest holds a module to its own list, not to its readers.
//
// An owner announces from its code. A Go module announces when its code imports mesh-sdk's go/announce: every
// announce.State literal there is read, its Name a string, a constant of the package, or a constant of mesh-sdk this
// check knows (Known). A TypeScript module announces when its code says the event `states` or calls the SDK's keeps:
// every `{ name: "...", readable: true }` there is read. An owner that announces nothing from code is read as before
// on the mesh, and is not judged; neither is an owner outside the catalogue (another repository's).
//
// go run . <catalog root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"git.novox.be/novox/mesh-sdk/go/health"
)
// Known are the mesh-sdk constants an announcement may name a state by, by the selector the code writes.
var Known = map[string]string{
"health.GoodState": health.GoodState,
"health.ReadingsState": health.ReadingsState,
}
const announcePkg = `"git.novox.be/novox/mesh-sdk/go/announce"`
// Announced is what one owner's code announces: each state, readable or not.
type Announced map[string]bool
// Findings is every refusal in the catalogue at root, sorted.
func Findings(root string) ([]string, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
dirs := map[string]string{}
reads := map[string][]string{}
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Reads []json.RawMessage `json:"reads"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
dirs[m.Module] = filepath.Dir(path)
for _, r := range m.Reads {
var s string
if json.Unmarshal(r, &s) == nil {
reads[m.Module] = append(reads[m.Module], s)
}
}
}
var out []string
owners := map[string]Announced{}
for reader, rs := range reads {
for _, r := range rs {
owner, state, ok := strings.Cut(r, ".")
dir, inCatalogue := dirs[owner]
if !ok || !inCatalogue || owner == reader {
continue
}
a, seen := owners[owner]
if !seen {
var problems []string
a, problems, err = AnnouncedBy(dir)
if err != nil {
return nil, err
}
owners[owner] = a
for _, p := range problems {
out = append(out, owner+": "+p)
}
}
if a == nil {
continue // announces nothing from code: read as before
}
readable, keeps := a[state]
switch {
case !keeps:
out = append(out, fmt.Sprintf("%s reads %s.%s, which %s's code does not announce: the read is refused on the mesh", reader, owner, state, owner))
case !readable:
out = append(out, fmt.Sprintf("%s reads %s.%s, which %s's code announces not readable: the read is refused on the mesh; announce it readable", reader, owner, state, owner))
}
}
}
sort.Strings(out)
return dedupe(out), nil
}
func dedupe(in []string) []string {
var out []string
for i, s := range in {
if i == 0 || s != in[i-1] {
out = append(out, s)
}
}
return out
}
var (
tsAnnounces = regexp.MustCompile(`emit\(\s*"states"|\bkeeps\(`)
tsState = regexp.MustCompile(`\{\s*name:\s*"([a-z0-9][a-z0-9-]*)"\s*(,\s*readable:\s*(true|false)\s*)?\}`)
)
// AnnouncedBy is what the module in dir announces from its code, nil when it announces nothing; problems are
// announcements whose state's name cannot be read.
func AnnouncedBy(dir string) (Announced, []string, error) {
var out Announced
var problems []string
pkgs := map[string][]*ast.File{}
fset := token.NewFileSet()
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if n := d.Name(); path != dir && (n == "node_modules" || n == "dist" || n == "test" || n == "testdata" || strings.HasPrefix(n, ".")) {
return filepath.SkipDir
}
return nil
}
name := d.Name()
switch {
case strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go"):
f, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
return err
}
pkgs[filepath.Dir(path)] = append(pkgs[filepath.Dir(path)], f)
case strings.HasSuffix(name, ".ts") && !strings.HasSuffix(name, ".d.ts") && !strings.HasSuffix(name, ".test.ts"):
raw, err := os.ReadFile(path)
if err != nil {
return err
}
if !tsAnnounces.Match(raw) && !strings.Contains(string(raw), "KEPT") {
return nil
}
for _, m := range tsState.FindAllStringSubmatch(string(raw), -1) {
if out == nil {
out = Announced{}
}
out[m[1]] = out[m[1]] || m[3] == "true"
}
}
return nil
})
if err != nil {
return nil, nil, err
}
for _, files := range pkgs {
imports := false
consts := map[string]ast.Expr{}
for _, f := range files {
for _, im := range f.Imports {
if im.Path.Value == announcePkg && (im.Name == nil || im.Name.Name == "announce") {
imports = true
}
}
for _, decl := range f.Decls {
g, ok := decl.(*ast.GenDecl)
if !ok || g.Tok != token.CONST {
continue
}
for _, spec := range g.Specs {
v := spec.(*ast.ValueSpec)
for i, n := range v.Names {
if i < len(v.Values) {
consts[n.Name] = v.Values[i]
}
}
}
}
}
if !imports {
continue
}
if out == nil {
out = Announced{}
}
for _, f := range files {
ast.Inspect(f, func(n ast.Node) bool {
lit, ok := n.(*ast.CompositeLit)
if !ok {
return true
}
states := []*ast.CompositeLit{}
switch {
case isAnnounceState(lit.Type):
states = append(states, lit)
case isStateSlice(lit.Type):
for _, e := range lit.Elts {
if c, ok := e.(*ast.CompositeLit); ok && (c.Type == nil || isAnnounceState(c.Type)) {
states = append(states, c)
}
}
default:
return true
}
for _, s := range states {
name, readable, problem := stateOf(s, consts)
if problem != "" {
problems = append(problems, fmt.Sprintf("%s: %s", fset.Position(s.Pos()), problem))
continue
}
out[name] = out[name] || readable
}
return false
})
}
}
return out, problems, nil
}
func isAnnounceState(e ast.Expr) bool {
s, ok := e.(*ast.SelectorExpr)
if !ok {
return false
}
x, ok := s.X.(*ast.Ident)
return ok && x.Name == "announce" && s.Sel.Name == "State"
}
func isStateSlice(e ast.Expr) bool {
a, ok := e.(*ast.ArrayType)
return ok && isAnnounceState(a.Elt)
}
// stateOf is one announce.State literal's name and whether it is readable (Readable true, or Readers named).
func stateOf(lit *ast.CompositeLit, consts map[string]ast.Expr) (string, bool, string) {
var name string
var readable bool
named := false
for _, e := range lit.Elts {
kv, ok := e.(*ast.KeyValueExpr)
if !ok {
return "", false, "an announce.State without field names cannot be read; name its fields"
}
key, _ := kv.Key.(*ast.Ident)
if key == nil {
continue
}
switch key.Name {
case "Name":
v, ok := resolve(kv.Value, consts, 0)
if !ok {
return "", false, fmt.Sprintf("the state's name %s cannot be read: name it by a string, a constant of the package, or a mesh-sdk constant in checks/readable's Known", exprString(kv.Value))
}
name, named = v, true
case "Readable":
if id, ok := kv.Value.(*ast.Ident); ok {
readable = readable || id.Name == "true"
}
case "Readers":
readable = true
}
}
if !named {
return "", false, "an announce.State without a Name"
}
return name, readable, ""
}
func resolve(e ast.Expr, consts map[string]ast.Expr, depth int) (string, bool) {
if depth > 8 {
return "", false
}
switch v := e.(type) {
case *ast.BasicLit:
if v.Kind == token.STRING {
s, err := strconv.Unquote(v.Value)
return s, err == nil
}
case *ast.Ident:
if c, ok := consts[v.Name]; ok {
return resolve(c, consts, depth+1)
}
case *ast.SelectorExpr:
s, ok := Known[exprString(v)]
return s, ok
}
return "", false
}
func exprString(e ast.Expr) string {
switch v := e.(type) {
case *ast.Ident:
return v.Name
case *ast.SelectorExpr:
return exprString(v.X) + "." + v.Sel.Name
case *ast.BasicLit:
return v.Value
}
return fmt.Sprintf("%T", e)
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Findings(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
for _, f := range found {
fmt.Println(f)
}
if len(found) > 0 {
os.Exit(1)
}
}
+90
View File
@@ -0,0 +1,90 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// A read of another module's state is refused here when the owner's code announces that state not readable, or not at
// all (the sweep of 2026-10-30, issue 774); an owner that announces nothing from code, or lives in another repository,
// is not judged.
func TestFindings(t *testing.T) {
const goOwner = "package main\n\nimport \"git.novox.be/novox/mesh-sdk/go/announce\"\n\n"
for _, row := range []struct {
name string
files map[string]string
want string // empty: clean
}{
{"go, readable by a constant", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": goOwner + "const S = \"s\"\n\nvar _ = announce.State{Name: S, Readable: true}\n",
"modules/o/cmd/o/main_test.go": goOwner + "var _ = announce.State{Name: \"s\"}\n"}, ""},
{"go, not readable", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": goOwner + "var Kept = []announce.State{{Name: \"t\", Readable: true}, {Name: \"s\"}}\n"},
"r reads o.s, which o's code announces not readable"},
{"go, a known mesh-sdk name and readers", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.good","o.s"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": goOwner + "var _ = []announce.State{{Name: health.GoodState, Readable: true}, {Name: \"s\", Readers: []string{\"r\"}}}\n"}, ""},
{"go, a name that cannot be read", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": goOwner + "var _ = announce.State{Name: other.S, Readable: true}\n"},
"the state's name other.S cannot be read"},
{"go, a state not announced", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": goOwner + "var _ = announce.State{Name: \"t\"}\n"},
"r reads o.s, which o's code does not announce"},
{"typescript, not readable", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.bound"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/kept.ts": `export const KEPT = [{ name: "announced" }, { name: "bound" }];`},
"r reads o.bound, which o's code announces not readable"},
{"typescript, readable", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.bound"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/index.ts": `await keeps([{ name: "bound", readable: true }]);`}, ""},
{"an owner that announces nothing, and one outside the catalogue", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.s","mesh-controller.lease"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": "package main\n\nvar _ = 1\n"}, ""},
} {
t.Run(row.name, func(t *testing.T) {
root := t.TempDir()
for path, text := range row.files {
p := filepath.Join(root, path)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Findings(root)
if err != nil {
t.Fatal(err)
}
joined := strings.Join(got, "\n")
if row.want == "" && len(got) != 0 || row.want != "" && !strings.Contains(joined, row.want) {
t.Errorf("findings %q, want %q", joined, row.want)
}
})
}
}
// The catalogue itself: every state a module reads of another module that announces from code is announced readable.
func TestTheCatalogueReadsOnlyWhatItsOwnersAnnounceReadable(t *testing.T) {
got, err := Findings(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
for _, f := range got {
t.Error(f)
}
}
+6
View File
@@ -0,0 +1,6 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every manifest, for what it reads; and every module's Go and TypeScript code, for the states it announces.
modules/*/module.json
modules/*/cmd
modules/*/*.go
modules/*/*.ts
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/secrets
go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest and the files it names.
modules
+25
View File
@@ -0,0 +1,25 @@
package main
import (
"testing"
"git.novox.be/novox/mesh-catalog/checks/trunk"
)
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): the known list gains no line the trunk's copy
// at the merge base does not hold. Without a trunk copy the lines in seed are the trunk's.
func TestTheKnownListHoldsNoLineTheTrunksDoesNot(t *testing.T) {
known, err := Known("known")
if err != nil {
t.Fatal(err)
}
base := seed
if path, ok := trunk.Copy(t, "checks/secrets/known"); ok {
if base, err = Known(path); err != nil {
t.Fatal(err)
}
}
for _, line := range Added(known, base) {
t.Errorf("checks/secrets/known gains %q, which the trunk's copy does not hold: the list only shrinks — protect the key instead (novox/hq issue 342)", line)
}
}
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest.
modules/*/module.json
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/templates
go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest and its templates.
modules
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"testing"
"git.novox.be/novox/mesh-catalog/checks/trunk"
)
// The script's comparison of this list with the trunk's (novox/hq ADR 0529 clause 2): the list of modules keeping file text inline
// gains none the trunk's copy at the merge base does not hold. The catalogue itself is judged against the list by
// TestTheCatalogueKeepsFileTextInlineOnlyWhereListed. Skipped, and said, where no trunk is beside.
func TestTheInlineListHoldsNoModuleTheTrunksDoesNot(t *testing.T) {
path, ok := trunk.Copy(t, "checks/templates/inline")
found, err := Check("../..")
if err != nil {
t.Fatal(err)
}
listed, err := Listed(listFile)
if err != nil {
t.Fatal(err)
}
base := seed
if ok {
if base, err = Listed(path); err != nil {
t.Fatal(err)
}
}
_, _, added := Judge(found, listed, base)
for _, m := range added {
t.Errorf("checks/templates/inline gains %s, which the trunk's copy does not hold: the list only shrinks", m)
}
}
+5
View File
@@ -0,0 +1,5 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# Check("../.."): every manifest.
modules/*/module.json
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/trunk
go 1.22
+65
View File
@@ -0,0 +1,65 @@
// Package trunk is where a catalogue check finds the trunk's copy of a list that may only shrink (novox/hq ADR 0529 clause 2):
// the file as the catalogue's base commit holds it, to judge a change's list against.
//
// The checker fetches the change's own repository at its base commit beside the change, as it fetches a sibling, and says where
// in MESH_CHECK_BESIDE; the copy is under the repository's name (the same convention the other repositories' tests use). With
// MESH_CHECK_BESIDE unset, or set without the catalogue's copy in it, the test fails saying NOT COMPARED WITH THE TRUNK and
// why: the checker places the change's own repository since R4g (issue 1295), so a missing copy is a fault, never a pass. A trunk that holds no such file had
// none: the list's first copy, which nothing was added to.
package trunk
import (
"errors"
"fmt"
"os"
"path/filepath"
"testing"
)
// Repository is the catalogue's name beside a check.
const Repository = "mesh-catalog"
// BesideEnv is where a merge check says the clones beside the one checked are.
const BesideEnv = "MESH_CHECK_BESIDE"
// Copy is the trunk's copy of the file at rel (a path from the catalogue's root), written to a file of the test's own: its path,
// and whether the trunk holds one (false: its first copy). It fails the test when no copy of the catalogue is beside.
func Copy(t testing.TB, rel string) (string, bool) {
t.Helper()
raw, found, err := Read(rel)
switch {
case err != nil:
t.Fatal(err)
case !found:
t.Logf("the trunk holds no %s: its first copy", rel)
return "", false
}
path := filepath.Join(t.TempDir(), filepath.Base(rel))
if err := os.WriteFile(path, raw, 0o644); err != nil {
t.Fatal(err)
}
return path, true
}
// Read is the trunk's copy of rel and whether the trunk holds one. err is a trunk not beside the check (MESH_CHECK_BESIDE unset,
// or without the catalogue's directory), or a copy unreadable: a comparison that cannot be made is never a pass.
func Read(rel string) (content []byte, found bool, err error) {
beside := os.Getenv(BesideEnv)
if beside == "" {
return nil, false, fmt.Errorf("NOT COMPARED WITH THE TRUNK: %s is not set, so the trunk's copy of %s is not beside this run; the checker places the change's own repository there (issue 1295)", BesideEnv, rel)
}
dir := filepath.Join(beside, Repository)
if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {
return nil, false, fmt.Errorf("NOT COMPARED WITH THE TRUNK: %s is not beside this check in %s=%s, so %s is not judged against the trunk's copy; the checker places the change's own repository there at its trunk (issue 1295)", Repository, BesideEnv, beside, rel)
} else if err != nil {
return nil, false, err
}
raw, err := os.ReadFile(filepath.Join(dir, rel))
if errors.Is(err, os.ErrNotExist) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
return raw, true, nil
}
+43
View File
@@ -0,0 +1,43 @@
package trunk
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestTheCloneBesideTheCheckIsTheTrunk(t *testing.T) {
beside := t.TempDir()
dir := filepath.Join(beside, Repository, "checks", "x")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "known"), []byte("a\nb\n"), 0o644); err != nil {
t.Fatal(err)
}
t.Setenv(BesideEnv, beside)
raw, found, err := Read("checks/x/known")
if err != nil || !found || string(raw) != "a\nb\n" {
t.Fatalf("read %q found %v err %v", raw, found, err)
}
if _, found, err := Read("checks/x/absent"); err != nil || found {
t.Fatalf("a file the trunk does not hold: found %v err %v; wanted its first copy", found, err)
}
}
func TestWithNothingBesideTheComparisonFailsAndSaysNotMade(t *testing.T) {
t.Setenv(BesideEnv, "")
_, found, err := Read("checks/x/known")
if found || err == nil || !strings.Contains(err.Error(), "NOT COMPARED") {
t.Fatalf("found %v, err %v; wanted a failure saying NOT COMPARED", found, err)
}
}
func TestACloneBesideWithoutTheCatalogueFailsAndSaysNotMade(t *testing.T) {
t.Setenv(BesideEnv, t.TempDir()) // the siblings are there, the catalogue's own base is not
_, found, err := Read("checks/x/known")
if found || err == nil || !strings.Contains(err.Error(), "NOT COMPARED") || !strings.Contains(err.Error(), "not beside") {
t.Fatalf("found %v, err %v; wanted a failure naming the missing copy", found, err)
}
}
+3
View File
@@ -0,0 +1,3 @@
# The modules of the catalogue whose texts still name the build agent or the toolchain images, and why (checks/words,
# buildagent_test.go). One module directory a line, with its reason after a #. It only shrinks.
modules/mesh-build-go # build.go:45 says "the build agent's recipe" in a step's description: the asks lane's module (issue 1887), not E6's to edit
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"os"
"strings"
"testing"
)
// The build agent and the toolchain images are gone (novox/hq ADR 0524 §5, issue 1295 and E6 of milestone R4g): no text a module
// can show names them. Where the word stands for a reason that is another lane's or still waits for its change, buildagent-allowed
// names the module's directory with that reason and the date it is dated from; the list only shrinks: a directory on it that no
// longer names the word is refused, so the line is taken out.
var gone = []string{"build-agent", "build agent", "toolchain image"}
func allowedModules(t *testing.T) map[string]string {
t.Helper()
raw, err := os.ReadFile("buildagent-allowed")
if err != nil {
t.Fatal(err)
}
out := map[string]string{}
for _, line := range strings.Split(string(raw), "\n") {
entry, why, _ := strings.Cut(line, "#")
if entry = strings.TrimSpace(entry); entry != "" && !strings.HasPrefix(line, "#") {
if strings.TrimSpace(why) == "" {
t.Fatalf("%q is allowed without a reason", entry)
}
out[entry] = strings.TrimSpace(why)
}
}
return out
}
func TestNoTextOfTheCatalogueNamesTheBuildAgentOutsideTheModulesAllowed(t *testing.T) {
texts, _, err := collect("../..")
if err != nil {
t.Fatal(err)
}
allowed := allowedModules(t)
seen := map[string]bool{}
for _, f := range check(gone, texts) {
module := f
if rest, ok := strings.CutPrefix(f, "modules/"); ok {
module = "modules/" + strings.SplitN(rest, "/", 2)[0]
}
if _, ok := allowed[module]; ok {
seen[module] = true
continue
}
t.Errorf("%s (the build agent and the toolchain images are gone: say what stands in their place)", f)
}
for module, why := range allowed {
if !seen[module] {
t.Errorf("buildagent-allowed names %s (%s), which names the word no more: take the line out, so it cannot come back", module, why)
}
}
}
+3
View File
@@ -58,6 +58,9 @@ func checkLines(lines []retiredLine, texts []text) []string {
for _, l := range lines {
rx := linePattern(l)
for _, t := range texts {
if !strings.Contains(t.s, l.daemon) { // the pattern holds the daemon's name whole: the cheap test first
continue
}
if m := rx.FindStringSubmatchIndex(t.s); m != nil {
line := t.line
if line > 0 {
+71 -36
View File
@@ -43,18 +43,63 @@ func main() {
if len(os.Args) > 1 {
root = os.Args[1]
}
os.Exit(run(root))
}
// run reads every text a module can show under root and answers the command's exit code: 0 clean, 1 a finding, 2 a list or a
// file unreadable. The command prints what it found; a test of the real catalogue reads the code.
func run(root string) int {
words, err := readList(filepath.Join(root, "retired-words"))
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
os.Exit(2)
return 2
}
if len(words) == 0 {
fmt.Fprintln(os.Stderr, "words: retired-words lists no word — the check would pass on anything")
os.Exit(2)
return 2
}
texts, files, err := collect(root)
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
return 2
}
findings := check(words, texts)
for _, f := range findings {
fmt.Println(f)
}
if len(findings) > 0 {
fmt.Printf("words: %d use(s) of a word the glossary retired (novox/hq ADR 0244) — say it in the glossary's word\n", len(findings))
return 1
}
// And no daemon's line (novox/hq ADR 0272): what a module tells the operator to type is a `nox` line.
lines, err := readLines(filepath.Join(root, "retired-lines"))
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
return 2
}
if len(lines) == 0 {
fmt.Fprintln(os.Stderr, "words: retired-lines lists no line — the check would pass on anything")
return 2
}
if findings := checkLines(lines, texts); len(findings) > 0 {
for _, f := range findings {
fmt.Println(f)
}
fmt.Printf("words: %d daemon's line(s) in what a module shows — the operator types one command, nox (novox/hq ADR 0272)\n",
len(findings))
return 1
}
fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines),
len(texts), files)
return 0
}
// collect reads every text a module can show under root: the strings of its own code that is not a test, and every string of
// its manifests.
func collect(root string) ([]text, int, error) {
var texts []text
files := 0
err = filepath.WalkDir(filepath.Join(root, "modules"), func(path string, d fs.DirEntry, err error) error {
err := filepath.WalkDir(filepath.Join(root, "modules"), func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
@@ -87,38 +132,7 @@ func main() {
texts = append(texts, found...)
return nil
})
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
os.Exit(2)
}
findings := check(words, texts)
for _, f := range findings {
fmt.Println(f)
}
if len(findings) > 0 {
fmt.Printf("words: %d use(s) of a word the glossary retired (novox/hq ADR 0244) — say it in the glossary's word\n", len(findings))
os.Exit(1)
}
// And no daemon's line (novox/hq ADR 0272): what a module tells the operator to type is a `nox` line.
lines, err := readLines(filepath.Join(root, "retired-lines"))
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
os.Exit(2)
}
if len(lines) == 0 {
fmt.Fprintln(os.Stderr, "words: retired-lines lists no line — the check would pass on anything")
os.Exit(2)
}
if findings := checkLines(lines, texts); len(findings) > 0 {
for _, f := range findings {
fmt.Println(f)
}
fmt.Printf("words: %d daemon's line(s) in what a module shows — the operator types one command, nox (novox/hq ADR 0272)\n",
len(findings))
os.Exit(1)
}
fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines),
len(texts), files)
return texts, files, err
}
// readList reads retired-words: one word or phrase a line; blank lines and lines starting with # are not words.
@@ -152,9 +166,22 @@ func pattern(word string) *regexp.Regexp {
func check(words []string, texts []text) []string {
var out []string
// A text can hold a word only if it holds the word's first part, in any case: the regular expression is run on those alone,
// which keeps the check of the whole catalogue within the unit level's bound.
lower := make([]string, len(texts))
for i, t := range texts {
lower[i] = strings.ToLower(t.s)
}
for _, w := range words {
rx := pattern(w)
for _, t := range texts {
first := ""
if parts := strings.Fields(w); len(parts) > 0 {
first = strings.ToLower(parts[0])
}
for i, t := range texts {
if !strings.Contains(lower[i], first) {
continue
}
if m := rx.FindStringSubmatchIndex(t.s); m != nil {
line := t.line
if line > 0 {
@@ -179,6 +206,9 @@ func excerpt(s string, from, to int) string {
return strings.Join(strings.Fields(s[a:b]), " ")
}
// modulePath matches a string that is only a module or import path ("git.example.org/org/repo/go/pkg").
var modulePath = regexp.MustCompile(`^[a-z0-9.-]+\.[a-z]+/[A-Za-z0-9_./-]+$`)
// goStrings returns a Go file's string literals, adjacent ones joined across `+` as the program joins them.
func goStrings(path, rel string) ([]text, error) {
src, err := os.ReadFile(path)
@@ -204,6 +234,11 @@ func goStrings(path, rel string) ([]text, error) {
if err != nil {
v = lit
}
if modulePath.MatchString(v) {
// An import path or another module path names code, not words an agent reads.
cur, joining = nil, false
continue
}
if cur != nil && joining {
cur.s += v
} else {
+8
View File
@@ -87,3 +87,11 @@ func TestRetiredLinesAreReadAsDaemonAndCommand(t *testing.T) {
t.Fatal("a line with no command was read")
}
}
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no word the glossary retired, and no
// daemon's line, in any text a module's tools can show an agent.
func TestTheCatalogueHoldsNoRetiredWordAndNoDaemonsLine(t *testing.T) {
if code := run("../.."); code != 0 {
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
}
}
+7
View File
@@ -0,0 +1,7 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# run("../.."): the root's lists of retired words and lines, and every module's texts.
retired-words
retired-lines
modules
+1 -1
View File
@@ -1,5 +1,5 @@
58
The long-running resources of this catalogue that do not say how they are ready (`health`, novox/hq ADR 0240
rule 8), as `module check` counts them. It may only go down: merge-check.sh fails a change that raises it, and
rule 8), as `module check` counts them. It may only go down: checks/health fails a change that raises it, and
one that lowers it writes the new number on the first line. From 2026-11-18, or once it is 0, a long-running
resource without `health` is refused.
-140
View File
@@ -1,140 +0,0 @@
#!/bin/sh
# The catalogue's TypeScript part of its own check (novox/hq issue 302's `mesh-check-also`), declared in
# merge-check.sh's header and run by the build seat in the TypeScript toolchain, after the Go part. The media
# catalogue's twin is issue 311. Before this, a TypeScript module's change ran nothing of its own before it
# merged: the Go part said "NOT TESTED HERE", and a type error in the forge module would first have shown in
# its walk.
#
# For every touched module that has a package.json, in a copy of it outside the clone:
#
# 1. its dependencies are the toolchain's own (/app/node_modules: the mesh's SDK, Node's types and what the
# runtime carries), copied beside it, never fetched: the check holds no credential for the package
# registry. A module that depends on a package the toolchain does not hold **fails the check**, by name:
# it could not be type-checked or tested, and what could not run is a failure, never a note (novox/hq
# issue 385);
# 2. every source that is not a test is type-checked with the toolchain's compiler and the flags a build
# compiles with — the same check the build makes, before the merge rather than in the walk;
# 3. its tests (test/*.test.ts) run under Node's own runner with types stripped, as its own `npm test` does
# after its build: the sources are compiled to dist/ and beside themselves first, so a test that imports
# the compiled module, or a source that imports its sibling by its .js name, finds it.
#
# Said, never passed silently: a module with no tests is said NOT TESTED on every run that touches it —
# nothing of it could fail. A module whose tests exist and could not run fails (novox/hq issue 385).
#
# By hand: MESH_CHECK_MODULES=gitea sh merge-check-typescript.sh, in the toolchain image (it reads
# /app/node_modules), or with TOOLCHAIN_MODULES pointing at a node_modules that holds the same.
#
# **In an agent's session, outside the toolchain image** (novox/hq issue 742), the same node_modules is
# assembled first, in a throwaway directory under the check's own workspace, the way the image's `deps` stage
# assembles it: the toolchain's own package.json and lock (mesh-tools' node-tools/, read from the forge this
# clone came from, so the list is never copied here) installed with `npm ci`, then the SDK the mesh last
# published (`@novox/mesh-sdk@latest`), the @novox scope from the forge's package registry, read without a
# credential. Node and npm are the dev-tools module's; nothing is installed on the machine, no npm
# configuration or cache of the account's is read or written. Only when neither TOOLCHAIN_MODULES nor
# /app/node_modules is there: the forge's check, in the image, fetches nothing, as before. The forge is
# MESH_FORGE (default: the origin remote's), the owner MESH_FORGE_OWNER (default: the origin's owner).
set -eu
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
deps="${TOOLCHAIN_MODULES:-/app/node_modules}"
if [ -z "${TOOLCHAIN_MODULES:-}" ] && [ ! -f "$deps/typescript/bin/tsc" ]; then
origin=$(git remote get-url origin 2>/dev/null || true)
forge="${MESH_FORGE:-$(printf '%s\n' "$origin" | sed -n 's#^\(https\{0,1\}://[^/]*\)/.*#\1#p')}"
owner="${MESH_FORGE_OWNER:-$(printf '%s\n' "$origin" | sed -n 's#^https\{0,1\}://[^/]*/\([^/]*\)/.*#\1#p')}"
if [ -z "$forge" ] || [ -z "$owner" ]; then
echo "NOT CHECKED: no TypeScript toolchain at $deps, and no forge to assemble one from (origin is '$origin'; set MESH_FORGE and MESH_FORGE_OWNER, or TOOLCHAIN_MODULES)"
exit 1
fi
echo "no TypeScript toolchain at $deps: assembling one from $forge/$owner/mesh-tools node-tools/ and $owner's package registry (novox/hq issue 742)"
tc="$work/toolchain"
mkdir -p "$tc"
printf '@%s:registry=%s/api/packages/%s/npm/\n' "$owner" "$forge" "$owner" >"$work/npmrc"
# Read with Node's own fetch (dev-tools declares Node, not curl); a status that is not 2xx fails.
if ! { node -e '
const [base, dir] = process.argv.slice(1);
Promise.all(["package.json", "package-lock.json"].map(async (f) => {
const r = await fetch(base + f);
if (!r.ok) throw new Error(base + f + ": " + r.status);
require("fs").writeFileSync(dir + "/" + f, await r.text());
})).catch((e) => { console.error(e.message); process.exit(1); });' \
"$forge/$owner/mesh-tools/raw/branch/main/node-tools/" "$tc" &&
(cd "$tc" && export npm_config_userconfig="$work/npmrc" npm_config_cache="$work/npm-cache" &&
npm ci --ignore-scripts --no-audit --no-fund --loglevel=error &&
npm install --ignore-scripts --no-audit --no-fund --no-save --loglevel=error "@$owner/mesh-sdk@latest"); }; then
echo "NOT CHECKED: the TypeScript toolchain could not be assembled from $forge (novox/hq issue 742)"
exit 1
fi
deps="$tc/node_modules"
fi
tsc="$deps/typescript/bin/tsc"
if [ ! -f "$tsc" ]; then
echo "NOT CHECKED: no TypeScript compiler at $tsc — this part runs in the TypeScript toolchain"
exit 1
fi
# The modules the change reaches: the planner's answer (hq ADR 0238), and every module a changed file is in —
# a change to a module's tests alone moves no build (hq ADR 0267), and its tests must still run.
touched=$({
printf '%s\n' "${MESH_CHECK_MODULES:-}" | tr ',' '\n' | sed -e 's#^modules/##' -e '/\//d'
printf '%s\n' "${MESH_CHECK_CHANGED:-}" | tr ',' '\n' | sed -n 's#^modules/\([^/]*\)/.*#\1#p'
} | sed '/^$/d' | sort -u)
failed=""
for m in $touched; do
d="modules/$m"
[ -f "$d/package.json" ] || continue
copy="$work/$m"
mkdir -p "$copy"
(cd "$d" && tar --exclude=./node_modules --exclude=./dist -cf - .) | (cd "$copy" && tar -xf -)
# What it depends on, against what the toolchain holds.
missing=$(node -e '
const p = require(process.argv[1]); const have = require("fs").readdirSync(process.argv[2]);
const scoped = (n) => n.startsWith("@") ? n.split("/")[0] : n;
const names = Object.keys({ ...(p.dependencies || {}), ...(p.devDependencies || {}) });
const held = (n) => have.includes(n) || (n.startsWith("@") && have.includes(scoped(n)) &&
require("fs").existsSync(process.argv[2] + "/" + n));
console.log(names.filter((n) => n !== "typescript" && !held(n)).join(" "));' "$copy/package.json" "$deps")
if [ -n "$missing" ]; then
echo "FAILED: $d could not be checked — it depends on $missing, which the toolchain does not hold and the check cannot fetch (novox/hq issue 385)"
failed="$failed $m"
continue
fi
mkdir -p "$copy/node_modules"
for p in "$deps"/*; do
case "$(basename "$p")" in typescript | esbuild | @esbuild) continue ;; esac
cp -r "$p" "$copy/node_modules/"
done
sources=$(cd "$copy" && find . -name '*.ts' ! -name '*.d.ts' ! -path './test/*' ! -path './node_modules/*' ! -path './dist/*' | sort)
if [ -n "$sources" ]; then
echo "type-checking $d (TypeScript)"
# shellcheck disable=SC2086 # the module's sources, none with a space in its name
if ! (cd "$copy" && node "$tsc" --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 \
--types node $sources); then
failed="$failed $m"
continue
fi
else
echo "$d has no TypeScript sources of its own to type-check: its tests read its files"
fi
if ! ls "$copy"/test/*.test.ts >/dev/null 2>&1; then
echo "NOT TESTED: $d is TypeScript and has no tests of its own"
continue
fi
if [ -n "$sources" ]; then
# shellcheck disable=SC2086
(cd "$copy" && node "$tsc" --module NodeNext --moduleResolution NodeNext --target ES2022 --types node \
--rootDir . --outDir dist $sources && node "$tsc" --module NodeNext --moduleResolution NodeNext \
--target ES2022 --types node --rootDir . --outDir . $sources)
fi
echo "testing $d (TypeScript)"
if ! (cd "$copy" && node --test --test-reporter=spec --experimental-strip-types \
--no-warnings=ExperimentalWarning test/*.test.ts); then
failed="$failed $m"
fi
done
if [ -n "$failed" ]; then
echo "TypeScript modules that failed:$failed"
exit 1
fi
-219
View File
@@ -1,219 +0,0 @@
#!/bin/sh
# mesh-check-toolchain: go
# mesh-check-also: typescript merge-check-typescript.sh
#
# The catalogue's own check (novox/hq ADR 0237 as amended): the second layer of a pull request's merge
# check, `mesh/repo-check`, run by the build seat in the mesh's Go toolchain.
#
# The gate — the manifests the change touches through the running controller's module check, every machine
# of the facts snapshot composed with them and validated by the node-engine's own validator, the replays
# against this tree — is the build seat's first layer (`mesh/merge-gate`), run because the mesh's module
# graph builds these modules from here. It is not repeated here. This is the repository's own:
#
# 1. every manifest through the controller's module check, so one module's change cannot leave another
# it shares a rule with refused (MESH_GATE is the controller the mesh runs) — and the count of
# long-running resources without `health` held to the number in health-undeclared, which only goes down;
# 2. the Go tests of every module the change touches that has them, under the race detector when the
# toolchain has a C compiler. A requirement of the mesh's own forge the module cache does not hold (a
# pinned SDK commit) is taken from its repository's clone beside the check (checks/deps); a module whose
# dependencies still cannot be had **fails the check**, by name and why — its tests could not run, and a
# test that could not run is a failure, never a note (novox/hq issue 385). The integration tests (`//go:build
# integration`) run here, with -tags integration, and only here. A module written in
# TypeScript is type-checked and tested by
# merge-check-typescript.sh, declared above, in the TypeScript toolchain (novox/hq issues 302, 311);
# 3. the words (novox/hq ADR 0244): no word the glossary retired for the tools, as copied in
# retired-words, in any text a module's tools can show an agent — every string in its own code that is
# not a test, and every description in its manifest. Run over every module, not only the touched ones,
# so a word newly retired is found everywhere it stands. The check's own test runs first.
# 4. the paths (novox/hq ADR 0260): no module names a path under /run, /var/lib, /usr/local/lib or /etc
# that another module's manifest places — modules talk over the bus, not through each other's files.
# Over every module; checks/paths/shared and checks/paths/known narrow it, and known only shrinks.
# 5. the agent's home (novox/hq ADR 0266): no module unpacks an archive, or writes into a file, under
# ${machine:agent-home} — the node-engine does both after checking the path, and the agent account owns
# that home and could swap a link in between.
# 6. the secrets (novox/hq issue 342): no mergeable file fills a key with a vault secret that a setting could
# replace — the key is `protected`, or left out of the file's `takes-settings`. checks/secrets/known names
# the nested ones the controller cannot protect alone yet, and only shrinks.
# 7. the list of predefined keys (novox/hq ADR 0348 clause 7): a change to predefined.json removes no key a
# module still names, judged against the trunk's copy at the merge base. Whether a removed key still holds a
# value at the mesh or a node is mesh-config's to refuse; this check reads no store. The list itself and
# every manifest against it are judged by the controller's module check in step 1.
# 8. the config form (novox/hq ADR 0348 clauses 1 and 11): no module says `settings`, `takes-settings` or
# `${setting:<key>}`, in its manifest or a template it names, or has a mergeable file naming no keys (which
# takes its whole layer), unless checks/config/unmoved lists it as not yet moved; that list only shrinks,
# judged against the trunk's copy at the merge base.
# 9. the file text (novox/hq ADR 0349 §2): no module keeps a file's text inline in its manifest (`content`)
# unless checks/templates/inline lists it as not yet moved; each names its template in files/. That list
# only shrinks, judged against the trunk's copy at the merge base.
# 10. the mail-store seat (novox/hq ADR 0378 §3, issue 800): no manifest but modules/mesh-mail claims it.
# 11. owned paths (novox/hq ADR 0422 §8, issue 1024; it holds ~/.ssh for ssh-client as ADR 0415 §6 asked):
# every directory and archive says `unknown` (move-out, module or program) and a valid `knows`; `move-out`
# only under a home; and no module declares a path inside a directory of another module.
# checks/sshdir (ADR 0415 §6) stays while main runs it; it goes under issue 1035 once main runs checks/owned.
# 12. the image builder (novox/hq ADR 0487 §3, issue 1704): only modules/buildah provides rootless-image-build.
# 13. the image's place (novox/hq ADR 0487 §1, issue 1707): every image builds from a Dockerfile in its own module's directory.
set -eu
(cd checks/words && go test -count=1 . && go run . ../..)
# Node names (novox/hq ADR 0507 §9, issue 1838): no node name of this mesh in a covered module; the names are in
# `particulars`, the covered modules in checks/particulars/covered.
(cd checks/particulars && go test -count=1 . && go run . ../..)
# The toolchains (novox/hq ADR 0477 clause 4): the package go only in modules/go, nodejs and npm only in modules/nodejs.
(cd checks/toolchains && go test -count=1 . && go run . ../..)
# The image builder (novox/hq ADR 0487 §3): only modules/buildah provides rootless-image-build, docker never.
(cd checks/imagebuild && go test -count=1 . && go run . ../..)
# The image's place (novox/hq ADR 0487 §1): every image builds from a Dockerfile in its own module's directory.
(cd checks/imageplace && go test -count=1 . && go run . ../..)
# The agent's home (novox/hq ADR 0266): no archive, and no file written into, under it.
(cd checks/agenthome && go test -count=1 . && go run . ../..)
# The mail-store seat (novox/hq ADR 0378 §3): only mesh-mail may claim it.
(cd checks/mailstore && go test -count=1 . && go run . ../..)
# ~/.ssh (novox/hq ADR 0415 §6): only ssh-client may name a path under it. Main's merge check runs this line, so
# checks/sshdir stays until main runs checks/owned; it goes under issue 1035.
(cd checks/sshdir && go test -count=1 . && go run . ../..)
# Owned paths (novox/hq ADR 0422 §8): unknown files named per directory; no path inside another module's directory.
(cd checks/owned && go test -count=1 . && go run . ../..)
# The secrets (novox/hq issue 342): a password is not a setting, so no setting can replace the vault's. The
# known list only shrinks: judged against the trunk's copy at the merge base, a line it does not hold is refused.
secrets_base=""
secrets_ref="origin/${MESH_CHECK_BASE:-main}"
if merge_base=$(git merge-base HEAD "$secrets_ref" 2>/dev/null); then
if git cat-file -e "$merge_base:checks/secrets/known" 2>/dev/null; then
secrets_base=$(mktemp)
git show "$merge_base:checks/secrets/known" >"$secrets_base"
fi
else
echo "NOT COMPARED WITH THE TRUNK: no merge base with $secrets_ref; checks/secrets/known is held to the list it was first written with"
fi
(cd checks/secrets && go test -count=1 . && if [ -n "$secrets_base" ]; then go run . -base "$secrets_base" ../..; else go run . ../..; fi)
# The list of predefined keys (novox/hq ADR 0348 clause 7): no key removed that a module still names. The trunk's
# list at the merge base is what it is judged against; a trunk without the file had none.
predefined_base=""
if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then
predefined_base=$(mktemp)
if ! git show "$merge_base:predefined.json" >"$predefined_base" 2>/dev/null; then
echo '{}' >"$predefined_base"
fi
else
echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; no removal from predefined.json can be seen"
fi
(cd checks/predefined && go test -count=1 . && if [ -n "$predefined_base" ]; then go run . -base "$predefined_base" ../..; else go run . ../..; fi)
# The config form (novox/hq ADR 0348 §1, §11): a moved module says no old name, and the list of modules not yet moved
# only shrinks, judged against the trunk's copy at the merge base; a trunk without one is the list's first copy.
unmoved_base=""
if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then
if git cat-file -e "$merge_base:checks/config/unmoved" 2>/dev/null; then
unmoved_base=$(mktemp)
git show "$merge_base:checks/config/unmoved" >"$unmoved_base"
fi
else
echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; checks/config/unmoved is held to the list it was first written with"
fi
(cd checks/config && go test -count=1 . && if [ -n "$unmoved_base" ]; then go run . -base "$unmoved_base" ../..; else go run . ../..; fi)
# The file text (novox/hq ADR 0349 §2): a file's text is a template in files/, outside the modules not yet moved; the
# list of those only shrinks, judged against the trunk's copy at the merge base, a trunk without one being its first.
inline_base=""
if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then
if git cat-file -e "$merge_base:checks/templates/inline" 2>/dev/null; then
inline_base=$(mktemp)
git show "$merge_base:checks/templates/inline" >"$inline_base"
fi
else
echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; checks/templates/inline is held to the list it was first written with"
fi
(cd checks/templates && go test -count=1 . && if [ -n "$inline_base" ]; then go run . -base "$inline_base" ../..; else go run . ../..; fi)
# The lists that narrow the paths check may not grow unseen: the base branch's copies are what they are
# judged against (MESH_CHECK_BASE, main when unset), read from the clone the check runs in.
base_lists=""
base_ref="origin/${MESH_CHECK_BASE:-main}"
if git rev-parse --verify --quiet "$base_ref^{commit}" >/dev/null 2>&1 &&
git cat-file -e "$base_ref:checks/paths/known" 2>/dev/null && git cat-file -e "$base_ref:checks/paths/shared" 2>/dev/null; then
base_lists=$(mktemp -d)
git show "$base_ref:checks/paths/known" >"$base_lists/known"
git show "$base_ref:checks/paths/shared" >"$base_lists/shared"
fi
(cd checks/paths && go test -count=1 . && if [ -n "$base_lists" ]; then go run . -base "$base_lists" ../..; else go run . ../..; fi)
if [ -n "${MESH_GATE:-}" ]; then
checked=$("$MESH_GATE" module check modules/*/module.json) || { printf '%s\n' "$checked"; exit 1; }
# **The count only goes down** (novox/hq ADR 0240 rule 8): the long-running resources that do not say how
# they are ready, as the controller counts them, against the number kept in health-undeclared. A change
# that raises it fails; one that lowers it writes the new number there, so it can never rise again.
kept=$(sed -n 's/^\([0-9][0-9]*\).*/\1/p' health-undeclared | head -n 1)
counted=$(printf '%s\n' "$checked" | sed -n 's/^long-running resources without health: \([0-9][0-9]*\)$/\1/p')
if [ -z "$counted" ]; then
echo "NOT COUNTED: the controller the mesh runs is older than the count of resources without health (ADR 0240 Phase B)"
elif [ "$counted" -gt "$kept" ]; then
echo "the long-running resources without health rose from $kept to $counted: declare how each new one is ready (ADR 0240 rule 8)"
exit 1
elif [ "$counted" -lt "$kept" ]; then
echo "the long-running resources without health fell from $kept to $counted: write $counted in health-undeclared, so the count cannot rise again"
exit 1
else
echo "long-running resources without health: $counted, as kept"
fi
else
echo "NOT CHECKED: no controller was built beside this check, so the manifests were not read by one"
fi
race=""
if command -v gcc >/dev/null 2>&1; then race="-race"; else echo "NOT RACE-CHECKED: the toolchain holds no C compiler"; fi
# The modules the change reaches are the controller's planner's answer (MESH_CHECK_MODULES, hq ADR 0238):
# a module by its name, a new one by its directory. By hand, without it, the directories the changed files
# are in.
if [ -n "${MESH_CHECK_MODULES:-}" ]; then
touched=$(printf '%s\n' "$MESH_CHECK_MODULES" | tr ',' '\n' | sed -e 's#^modules/##' -e '/\//d' | sort -u)
else
touched=$(printf '%s\n' "${MESH_CHECK_CHANGED:-}" | tr ',' '\n' | sed -n 's#^modules/\([^/]*\)/.*#\1#p' | sort -u)
fi
# A module's dependencies: fetched, or a forge requirement taken from its clone beside the check
# (MESH_CHECK_BESIDE); its tests run against them (checks/deps, novox/hq issue 385). The helper's own test
# runs first.
(cd checks/deps && go test -count=1 .)
deps=$(mktemp -d)
trap 'rm -rf "$deps"' EXIT
(cd checks/deps && go build -o "$deps/deps" .)
untestable=""
for m in $touched; do
[ -d "modules/$m" ] || continue
if [ ! -f "modules/$m/go.mod" ]; then
[ -f "modules/$m/package.json" ] && echo "modules/$m is TypeScript: merge-check-typescript.sh type-checks and tests it"
continue
fi
if ! modfile=$(GOPRIVATE=git.novox.be "$deps/deps" -beside "${MESH_CHECK_BESIDE:-}" -work "$deps" "modules/$m"); then
echo "FAILED: modules/$m could not be tested — $modfile"
untestable="$untestable modules/$m"
continue
fi
[ -n "$modfile" ] && echo "modules/$m takes its forge dependencies from the clones beside the check"
echo "testing modules/$m"
unformatted=$(cd "modules/$m" && gofmt -l .)
if [ -n "$unformatted" ]; then
echo "modules/$m is not gofmt'd: $unformatted"
exit 1
fi
cgo=0
[ -n "$race" ] && cgo=1
# The integration tests (a file whose first line is `//go:build integration`: a test that starts a bus, a store or
# another process) run here and nowhere else: the unit suite stays under a second (novox/hq playbook 07).
(cd "modules/$m" && CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go vet -tags integration ${modfile:+"-modfile=$modfile"} ./... &&
CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go test -tags integration ${modfile:+"-modfile=$modfile"} $race -count=1 ./...)
# The docker tests of the bus's snapshot program and entrypoint (novox/hq ADR 0480 clause 6) start throwaway nats
# containers, and the Go toolchain container holds no docker, so above they skip with a "NOT RUN" line. Where the
# build seat says docker exists (MESH_CHECK_DOCKER=1) they run here by name, and a missing docker fails the check
# (MESH_REQUIRE_DOCKER=1 makes the tests fail instead of skip). Without it the step says it did not run.
if [ "$m" = nats ]; then
if [ "${MESH_CHECK_DOCKER:-}" = 1 ]; then
(cd modules/nats && MESH_REQUIRE_DOCKER=1 CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go test -tags integration ${modfile:+"-modfile=$modfile"} -count=1 \
-run 'TestTheDeclaredDumpRunsInTheBussImage|TestTheEntrypointReloadsTheServerWithoutDroppingAConnection|TestTheStoreSurvivesTheNewDeclarationTheOldAndTheNewAgain|TestASnapshotOfALiveBusRestoresToTheSameContent|TestTheSnapshotUserCannotChangeTheBus' \
./cmd/mesh-nats-snapshot) || { echo "the bus's docker tests failed"; exit 1; }
else
echo "NOT RUN: the bus's five docker tests (modules/nats/cmd/mesh-nats-snapshot) skipped: this check has no docker (MESH_CHECK_DOCKER is not 1)"
fi
fi
done
if [ -n "$untestable" ]; then
echo "the check fails: the tests of$untestable could not run (novox/hq issue 385)"
exit 1
fi
@@ -0,0 +1,9 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# the copies of this package in every module that carries one.
modules/xorg/internal/desktop/*.go
modules/lemurs/internal/desktop/*.go
modules/i3/internal/desktop/*.go
modules/xterm/internal/desktop/*.go
modules/adwaita/internal/desktop/*.go
@@ -23,7 +23,7 @@ import (
// manifest's.
const NamesFileEnv = "MESH_ARTIFACT_STORE_NAMES_FILE"
// CatalogueModules and CatalogueModule are the catalogue's tools these definitions are read through.
// CatalogueModules and CatalogueModule are the catalog's tools these definitions are read through.
const (
CatalogueModules = "mesh-catalog.catalog_modules"
CatalogueModule = "mesh-catalog.catalog_module"
@@ -143,21 +143,21 @@ func PinsIn(manifest json.RawMessage, names StoreNames) []pinned {
return out
}
// Definitions is what the catalogue's definitions pin in the store, and what of them could not be read.
// Definitions is what the catalog's definitions pin in the store, and what of them could not be read.
type Definitions struct {
// Pins are by Key(repository, digest).
Pins map[string][]Pin
// Read is how many definitions were read; Unread says, per module, why its definition was not.
Read int
Unread map[string]string
// NotAsked says why the catalogue could not be asked at all, when it could not.
// NotAsked says why the catalog could not be asked at all, when it could not.
NotAsked string
// NamesNotRead says why the store's older names could not be read, when they could not.
NamesNotRead string
Names StoreNames
}
// Catalogue reads the definitions through the catalogue's tools, and keeps each definition it read by its
// Catalogue reads the definitions through the catalog's tools, and keeps each definition it read by its
// module and commit, which never change together.
type Catalogue struct {
Ask Asking
@@ -171,21 +171,21 @@ type catalogueEntry struct {
Commit string `json:"commit"`
}
// Read asks the catalogue for every module and reads each definition, bounded by DefinitionsTimeout.
// Read asks the catalog for every module and reads each definition, bounded by DefinitionsTimeout.
func (c *Catalogue) Read(ctx context.Context, names StoreNames) *Definitions {
defs := &Definitions{Pins: map[string][]Pin{}, Unread: map[string]string{}, Names: names}
if c == nil || c.Ask == nil {
defs.NotAsked = "this bundle has no way to ask the catalogue"
defs.NotAsked = "this bundle has no way to ask the catalog"
return defs
}
raw, err := c.Ask(CatalogueModules, map[string]any{})
if err != nil {
defs.NotAsked = "the catalogue's modules could not be asked: " + err.Error()
defs.NotAsked = "the catalog's modules could not be asked: " + err.Error()
return defs
}
answer, output, ok := answerOf(raw)
if !ok {
defs.NotAsked = "the catalogue refused its modules: " + lastLine(output)
defs.NotAsked = "the catalog refused its modules: " + lastLine(output)
return defs
}
var list struct {
@@ -193,7 +193,7 @@ func (c *Catalogue) Read(ctx context.Context, names StoreNames) *Definitions {
Error string `json:"error"`
}
if err := json.Unmarshal(answer, &list); err != nil || list.Error != "" || list.Modules == nil {
defs.NotAsked = "the catalogue's modules answer is not readable: " + firstOf(list.Error, errText(err), lastLine(output))
defs.NotAsked = "the catalog's modules answer is not readable: " + firstOf(list.Error, errText(err), lastLine(output))
return defs
}
c.mu.Lock()
@@ -0,0 +1,129 @@
package main
// The artifact store's door and the bus, as the holder reaches them through this module's runtime.
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"artifactstoretools/internal/registry"
)
// door is registry.Store at the store's address as this machine reaches it.
type door struct{ s registry.Store }
func (d *door) Address() string { return d.s.Address }
func (d *door) Hold(ctx context.Context, r string) (bool, error) { return d.s.Hold(ctx, r) }
func (d *door) Holds(ctx context.Context, r string) error { return d.s.Holds(ctx, r) }
func (d *door) IsIndex(ctx context.Context, r string) (bool, error) {
return d.s.IsIndex(ctx, r)
}
func (d *door) LetGo(ctx context.Context, r string) error { return d.s.LetGo(ctx, r) }
func (d *door) SpareKept(ctx context.Context, keptImages, touching []string) error {
spare, err := d.s.SpareKeptIndexes(ctx, keptImages, touching)
if err == nil {
d.s.Spare = spare
}
return err
}
// DoorAt reads the store's address (host:port) from the file the manifest fills from the seat mesh-artifact-store's
// reach, each time, so a store that moved is reached where it is now.
func DoorAt(file string) func() (Door, error) {
return func() (Door, error) {
if file == "" {
return nil, fmt.Errorf("this bundle was given no file naming the artifact store's address")
}
raw, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("the artifact store's address: %w", err)
}
address := strings.TrimSpace(string(raw))
if address == "" {
return nil, fmt.Errorf("the artifact store's address is empty in %s", file)
}
return &door{s: registry.Store{Address: address}}, nil
}
}
// runtimeBus is the holder's bus through the runtime: its reads, its findings and its seat's event.
type runtimeBus struct{}
func (runtimeBus) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
func (runtimeBus) Get(state, key string) ([]byte, bool, error) {
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, false, err
}
return e.Value, true, nil
}
func (runtimeBus) History(state, key string) ([]stdio.StateRevision, error) {
return stdio.State(state).History(key)
}
func (runtimeBus) LogSince(read string, at time.Time) ([]stdio.LogEntry, error) {
var out []stdio.LogEntry
page, err := stdio.Log(read).Since(at, "", 0)
for err == nil {
out = append(out, page.Entries...)
if page.Next == 0 {
return out, nil
}
page, err = stdio.Log(read).Read(page.Next, "", 0)
}
return nil, err
}
func (runtimeBus) Put(state, key string, value any) error {
_, err := stdio.State(state).Put(key, value)
return err
}
func (runtimeBus) Delete(state, key string) error { return stdio.State(state).Delete(key) }
func (runtimeBus) Publish(subject string, body any) error {
return stdio.SeatPublish(subject, body, "")
}
// follow watches the builder's done keys and the wants: a build's end sweeps (once the lease names no controller) and
// observes again; a want changed observes again. Event-driven: nothing reads them on a timer.
func follow(h *Holder) {
<-stdio.Served()
watch := func(state string, handle func(stdio.StateChange) error) {
for {
if err := stdio.State(state).Watch("", handle); err == nil {
return
} else {
h.logf("watching %s: %v; asked again in 10s", state, err)
}
time.Sleep(10 * time.Second)
}
}
observe := func() {
if err := h.Observe(context.Background()); err != nil {
h.logf("image-missing could not be observed: %v", err)
}
}
watch(BuildsRead, func(c stdio.StateChange) error {
if c.Current || c.Op != stdio.OpPut {
return nil
}
var b struct {
State string `json:"state"`
}
if json.Unmarshal(c.Value, &b) == nil && b.State == "done" {
go func() { h.AfterBuild(c.Key); observe() }()
}
return nil
})
watch(WantsRead, func(c stdio.StateChange) error {
if !c.Current {
go observe()
}
return nil
})
observe()
}
File diff suppressed because it is too large Load Diff

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