diff --git a/02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md b/02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md index 4b8c9673..6cb798c5 100644 --- a/02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md +++ b/02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md @@ -11,6 +11,14 @@ extends: 02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-p # 239. A delivery is owned by the mesh-delivery module and runs from commit to delivered +> **Superseded in part — 2026-10-08, by [ADR 0249](0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md).** +> Decision 4's sentence "a declared order that contradicts an inferred one is a cycle" no longer holds. +> Two rules ordering one pair both ways were always a cycle, and a group of the controller, the node-engine +> and the lab was refused for it (issue 309). The order rules now have a precedence: declared, then built +> by and version skew, then engine before controller. The higher rule decides the pair and says what it +> won over. Only rules of one rank in opposite directions are refused, naming both. Everything else here +> stands. + ## Context [ADR 0238](0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md) diff --git a/02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md b/02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md new file mode 100644 index 00000000..ea3bde6b --- /dev/null +++ b/02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md @@ -0,0 +1,99 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-08 +deciders: jochen +reconstructed: false +supersedes-in-part: + - 0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md +extends: 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md +--- + +# 249. A delivery group's order rules have a precedence, and a declared order outranks them all + +## Context + +[ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md) +decision 4 orders a delivery group's members by a line `after: ` and by three inferred rules: +*built by* (a member that moves a module goes before a member whose modules are built by it or stand on +it), *version skew* (the controller before a manifest change) and *engine before controller* (the +node-engine before the controller, from ADR 0236's order for its walks). It says nothing about two rules +ordering one pair in opposite directions. The planner treated every such pair as a cycle, and a declared +order against an inferred one as a cycle too. + +On 2026-10-08 the group `feat/a-machine-joins-through-the-tunnel` held three pull requests, each `ready` +on its own: the controller's, the node-engine's and the lab's +([issue 309](../04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/00-report.md)). The controller's +repository also holds the build agent, and its pull request moved it. Every source-built module is built +by the build agent, so *built by* put the controller's member first. *Engine before controller* put the +node-engine's member first. The group was rejected: "its order contradicts itself", naming the two +members and neither rule. The person merged it by hand, the controller first, since that member only added +a verb the new node-engine calls. That is the order *built by* gave, and the order +[ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md) asks for: a verb is promised before +it is required. + +The rules are not of one kind. *Built by* and *version skew* are read from the graph and from what the +change does: which module builds which, and whether a manifest changes. *Engine before controller* is +read from two module names alone. It holds when the controller starts sending something the node-engine +must first accept. It is wrong when the node-engine starts asking for something the controller must first +serve, as here. The rule cannot tell the two apart. + +## Considered Options + +1. **A precedence among the rules, and a declared order above them all.** Chosen. +2. **Narrow *built by* so it never applies through the build agent to the node-engine.** It would end + this case. But a builder that changes how the node-engine is built does have to go first, and the next + pair of rules that disagree would be refused the same way. Rejected. +3. **Drop *engine before controller*.** ADR 0236's reason still holds when the node-engine is what must + accept something new. Rejected. +4. **Keep refusing, and name the two rules.** Then every group that moves the controller's repository and + the node-engine together is refused, and the person must still merge by hand. A declared order could not + help, because a declared order against an inferred one was also a cycle. Rejected. + +## Decision + +1. **When rules order one pair of members in opposite directions, the rule of higher precedence decides.** + From highest to lowest: + - *declared*: an `after:` line a person wrote; + - *built by* and *version skew*: what the graph and the change say; + - *engine before controller*: the default for walks, read from module names alone. +2. **A declared order outranks every inferred rule.** This replaces ADR 0239 decision 4's sentence "a + declared order that contradicts an inferred one is a cycle". A person who knows the order the rules + cannot see writes one line, and the group follows it. +3. **The order says which rule won.** Each pair carries its rule and the rules it won over. The delivery's + `show`, `groups` and plan note print them, for example "the controller's member before the node-engine's: + built by, over engine before controller". +4. **Rules of one rank in opposite directions are still refused.** Two `after:` lines naming each other, + or *built by* both ways, have no precedence between them. The group is rejected. The reason names both + members, both rules and the direction each gives, and says how to declare the order. A cycle through + three members or more is refused naming the pairs in it. + +## Consequences + +- The group of issue 309 is ordered without a person: the controller's member first. +- A group can no longer be refused because a person's `after:` disagrees with an inferred rule. The person + can now overrule *version skew*, and a manifest change merged before the controller that parses it fails + at the gate, not silently. The rule the person overruled is printed on the pair. +- *Engine before controller* now decides only where nothing in the graph or the pull requests does. +- The ranks are a list in the controller's planner. A new inferred rule must be given a rank when it is + added. + +**How it is checked.** The controller's tests `TestAnOrderRuleYieldsToAStrongerOne` and +`TestAGroupIsOrderedByTheGraphAndWhatItsPullRequestsSay` check that *built by* wins over *engine before +controller*, that a declared line wins over both and over *version skew*, that each pair names what it won +over, and that equal-rank rules both ways are refused with both rules named and the `after:` lines to +write. mesh-delivery's `TestAGroupSaysWhichOrderRuleWonOrWhyNoneDid` and +`TestACycleWithoutAContradictionNamesItsPairs` check that the refusal and the won-over rules reach +`groups` and the group's state. The replay R309 in mesh-lab's register fails before the fix and passes on it. + +## References + +- [Issue 309](../04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/00-report.md), its report and + diagnosis. +- [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md) + decision 4, the sentence this replaces. +- [ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md), + *Rollout order*, where *engine before controller* comes from. +- [ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md), a verb promised before it is + required. +- [Design 47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md), *A delivery group*. diff --git a/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md b/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md index 4ee07eb2..9ab4c2e4 100644 --- a/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md +++ b/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md @@ -2,8 +2,9 @@ layer: to-be status: designed code: [] -updated: 2026-10-07 +updated: 2026-10-08 decisions: + - 02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md - 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md - 02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md - 02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md @@ -93,6 +94,10 @@ controller's `plan-moved` event, never inferred from time. - **Order**: `after: ` lines in a member's description, and the controller's `delivery-order` over the graph (ADR 0239 decision 4). The plan shows the order and why each pair is ordered: *declared*, *built by*, *version skew*, *engine before controller*, *by name*. +- **Precedence** (ADR 0249): when rules order one pair both ways, the higher one decides. *Declared* comes + first, then *built by* and *version skew*, then *engine before controller*. The pair shows the rule that + won and the rules it won over. Rules of one rank both ways are a contradiction. The group is `rejected`, + and the reason names both members, both rules and the `after:` line that would settle it. - **Check**: the members' heads composed together, one gate over every machine with all their definitions. It is asked by mesh-delivery through `delivery-check` when the group forms or a member's head moves, and posted on every member's head as `mesh/delivery-group`. diff --git a/04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/00-report.md b/04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/00-report.md new file mode 100644 index 00000000..4dd92c5f --- /dev/null +++ b/04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/00-report.md @@ -0,0 +1,74 @@ +--- +status: resolved +opened: 2026-10-08 +located-in: [mesh-controller cmd/mesh-controller (delivery.go, orderOf), mesh-catalog modules/mesh-delivery (holder.go, the group's state)] +fixed-by: novox/mesh-controller PR #134, novox/mesh-catalog PR #119 +replay: R309 +amended-design: 03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md +--- + +# 309. Two order rules ordered one pair both ways + +## Symptom + +On 2026-10-08 the delivery group `feat/a-machine-joins-through-the-tunnel` held three pull requests: the +controller's mesh-controller #132, the node-engine's `mesh-host` #52 and the lab's mesh-lab #61. Each was +`ready` on its own. The delivery's `show` said of the group: + +- `"state": "rejected"`; +- `"why": "its order contradicts itself: novox/mesh-controller@…, novox/mesh-host@…"`. + +The group's `pairs` held two orderings of the same two members: + +- the controller's member before the node-engine's, `why: "built by"`; +- the node-engine's member before the controller's, `why: "engine before controller"`. + +Nothing would ever deliver the group. The person merged it by hand, the controller first, since that +member only added a verb the new node-engine calls. + +## Why it is a design issue + +[ADR 0239](../../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md) +decision 4 states four order rules and says what to do when a declared order contradicts an inferred +one. It does not say what to do when two inferred rules contradict each other. The planner treated that +as a cycle, so a group that moved the controller's repository and the node-engine together could never be +delivered. The refusal named two members and no rule, so the person could not tell what had clashed or +what to write. A declared `after:` line could not help, because a declared order against an inferred one +was a cycle too. + +## Cause + +The controller's pull request moved the build agent, which lives in the controller's repository. Every +source-built module is built by the build agent, including the node-engine. *Built by* therefore put the +controller's member first. *Engine before controller* put the node-engine's member first. Both were +added as pairs, and the planner's sort found no member with nothing before it. + +## Fix + +[ADR 0249](../../02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md) +gives the rules a precedence: *declared*, then *built by* and *version skew*, then *engine before +controller*. + +- **The controller (novox/mesh-controller PR #134).** `orderOf` collects each rule's claim, and for each + two members the claims of the highest rank decide. The pair carries the rules it won over. Claims of one + rank in opposite directions are a contradiction. Both pairs are kept, the members are the cycle, and the + contradiction names both rules, the member each puts first and the `after:` line that settles it. +- **mesh-delivery (novox/mesh-catalog PR #119).** It keeps the rules each pair won over and the + contradictions, lists them in `groups` and the plan note, and refuses a group in the contradiction's + words. A cycle through three members or more names the pairs in it. An older controller's answer reads + as before. + +Replayed, the group is ordered: the controller's member, then the node-engine's, then the lab's. The pair +reads "built by, over engine before controller". That is the order the person chose by hand. + +## How it is checked + +**The check:** the replay R309 in mesh-lab's register (novox/mesh-lab PR #64), `TestReplay309` in the +controller. It replays this group's members, reach and graph using only what `orderOf` had before the +fix. Proved with the register's `cmd/prove`: it fails on the commit before the fix (the group refused as +a cycle) and passes on it. The controller's `TestAnOrderRuleYieldsToAStrongerOne` checks the precedence +and the refusal's words. mesh-delivery's `TestAGroupSaysWhichOrderRuleWonOrWhyNoneDid` checks that they +reach `groups` and the group's state. + +**Not checked:** that a new inferred rule is given a rank. One added without a rank sorts below every other +rule, and nothing refuses that. diff --git a/04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/01-diagnosis.md b/04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/01-diagnosis.md new file mode 100644 index 00000000..eb64ccb5 --- /dev/null +++ b/04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/01-diagnosis.md @@ -0,0 +1,37 @@ +# 309 — diagnosis + +## 2026-10-08 + +- The delivery's `show` for the group gave both pairs and `cycle` with the controller's and the + node-engine's members. The lab's member was ordered alone, since it moves no module. +- The pairs come from the controller's `delivery-order` verb, `orderOf` in + `cmd/mesh-controller/delivery.go`. mesh-delivery only stores them and says "its order contradicts + itself" with the cycle's ids (`GroupState` in `holder.go`). So the order is the controller's to fix, and + the words are mesh-delivery's. +- *Built by*: `orderOf` adds a pair for any edge of the dependency relation from a module the second + member moves to one the first moves. The controller's plan for #132 moved `build-agent`, + `mesh-controller` and `route-proxy`. The relation (`inventory.dependenciesOf`) gives every source-built + module an edge `built-by` to the build seat's holder, so `mesh-host → build-agent` ordered the + controller's member first. The pair was labelled *built by* although the edge was the build agent's, not + the controller's. +- *Engine before controller*: the node-engine's member moved `mesh-host`, the controller's moved + `mesh-controller`, and the node-engine's did not move the controller. The rule put the node-engine's + member first. +- `add` dropped a pair only when the same direction was already there, so both directions stood and + the planner's sort stalled. Every two-way pair was a cycle, whatever the rules. +- The person's order, the controller first, was right for this change. The new node-engine calls a verb + the new controller adds, which is ADR 0246's case: a verb is promised before it is required. + *Engine before controller* comes from ADR 0236's order for its walks, where the node-engine had to + accept something new first. The rule reads only module names, so it cannot tell the two cases apart. *Built + by* is read from the graph. + +**Ruled out.** + +- That the relation was wrong. The node-engine is source-built and is built by the build agent. +- Narrowing *built by* so it never applies through the build agent. A builder change can matter to what + it builds, and the next pair of disagreeing rules would be refused the same way. +- Leaving it to a declared `after:` line alone. ADR 0239 made a declared order against an inferred one a + cycle too, so no line could settle it. + +**Decided.** Precedence, with a declared order above all inferred rules (ADR 0249). That supersedes the one +sentence of ADR 0239 decision 4 that made a declared order against an inferred one a cycle.