From 57f95fc6bb28af3d6e7b1d038b6a3cd32e40b46b Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 7 Oct 2026 21:11:42 +0200 Subject: [PATCH] Issue 297: every open decision conflicts with the next one to merge The written decision index is the one file every decision PR edits, at the end of its topic, so on 2026-10-07 each merge forced the others to rebase and be checked again. The diagnosis weighs four fixes and recommends generating the index on read. --- .../00-report.md | 71 ++++++++++++++++++ .../01-diagnosis.md | 72 +++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 04-ISSUES/297-two-open-decisions-always-conflict/00-report.md create mode 100644 04-ISSUES/297-two-open-decisions-always-conflict/01-diagnosis.md diff --git a/04-ISSUES/297-two-open-decisions-always-conflict/00-report.md b/04-ISSUES/297-two-open-decisions-always-conflict/00-report.md new file mode 100644 index 00000000..145b66f1 --- /dev/null +++ b/04-ISSUES/297-two-open-decisions-always-conflict/00-report.md @@ -0,0 +1,71 @@ +--- +status: located +opened: 2026-10-07 +located-in: [hq (02-DECISIONS/README.md, 00-META/checks/index.py)] +fixed-by: +amended-design: +--- + +# 297. Two open decisions always conflict + +## Symptom + +`02-DECISIONS/README.md` holds a generated index between `` and +``. `00-META/checks/index.py --write` writes it, and `index.py` fails any pull request +whose index is not current. So every pull request that adds a decision record also adds a line to that +file. The index groups records by `topic:` and sorts each group by number, so a new record's line always +lands at or near the end of its group. Two open decisions on the same topic insert into the same place. + +On 2026-10-07 four pull requests carrying decisions were open together: hq #168 (ADR 0241), #171 (issue +295 and ADR 0242), #172 (ADR 0243) and #173 (ADR 0245). Each merge to `main` made the next open +decision pull request unmergeable. Each one then had to be rebased, have its index written again, be +pushed, and be checked again from the start by the build seat. The heads the build seat checked, from the +controller's journal (times UTC): + +| PR | heads checked | merged | +|---|---|---| +| #168 | `297945e7` 17:06, `af225efc` 17:21, `c224cea3` 18:18, `b9caffaa` 18:26, `c462837f` 18:36 | 19:04 | +| #171 | `1f161c3c` 17:41, `18774cbe` 18:15, `998980bb` 18:26 | 18:33 | +| #173 | `71339dc4` 17:47, `666095c4` 17:54, `24aeeadf` 18:30, `10ab240e` 18:57, `6cacf772` 19:06 | open | + +That makes three rebases of #168, two of #171 and four of #173 in about two hours. Every one was +followed by a full check. + +## Evidence that the index is what conflicted + +Each old head was replayed onto the `main` that came after it, with `git merge-tree`: + +- **#171** at `1f161c3c`, onto `main` after #172 merged (ADR 0243): conflict in + `02-DECISIONS/README.md`. The two sides are the 0243 line and the 0242 line, one after the other in + the same topic. +- **#168** at `297945e7` and at `af225efc`, onto the same `main`: conflict in `02-DECISIONS/README.md`. +- **#168** at `b9caffaa`, onto `main` after #171 merged (ADR 0242): conflict in + `02-DECISIONS/README.md`. 0241 sorts before 0242, so its line had to go above the line #171 had just + added. +- **#173** at `666095c4`, onto `main` after #174 merged (ADR 0244): conflict in a design document, + and the index merged cleanly. Its first head, `71339dc4`, is not available to replay. + +Not every rebase that day was forced by the index. #168 and #171 were rebased once more after #174 merged +because the words check that #174 introduced failed on their prose. #173 changed its number twice +(0242, then 0244, then 0245) because the numbers it took were used first by other branches. Its last two +heads merged cleanly. Even so, three of the forced rebases came from the index alone, and every +decision pull request open at the same time as another is exposed to it. + +## Why it matters + +- Every rebase needs a full check from the build seat. With four decisions open, merging them one after + another costs about as many checks as there are pairs of them, not as many as there are decisions. +- The rebased line is a copy of a fact the record already states in its own `topic:` and `status:`. + The conflict is between two copies of frontmatter, never between two decisions. +- The index also marks status (`*(superseded)*`, `*(proposed)*`). ADR 0019 already says a decision + index is a status view generated from frontmatter "never written to disk", and in the same record it + says this index is written. The two statements disagree, and the merge conflicts come from the + second one. + +## How it is checked + +Today `index.py` checks that the written index is current, and that is what turns every concurrent +decision into a conflict. A fix is checked by a replay of the 2026-10-07 case: two branches off one +`main`, each adding a decision on the same topic, both merge after the first one lands with no rebase, +and `sh merge-check.sh` passes on both and on the merge. [01-diagnosis.md](01-diagnosis.md) weighs the +fixes. diff --git a/04-ISSUES/297-two-open-decisions-always-conflict/01-diagnosis.md b/04-ISSUES/297-two-open-decisions-always-conflict/01-diagnosis.md new file mode 100644 index 00000000..5ca32943 --- /dev/null +++ b/04-ISSUES/297-two-open-decisions-always-conflict/01-diagnosis.md @@ -0,0 +1,72 @@ +# 297 — diagnosis + +## 2026-10-07: where the conflict comes from + +`index.py` builds the index from every record's `topic:`, `status:` and heading. It groups the records +under six topics and sorts each group by number. `--write` replaces the text between the markers in +`02-DECISIONS/README.md`, and without `--write` the script fails when that text differs from what it +would build. `merge-check.sh` runs it, so the build seat's `mesh/repo-check` fails any pull request whose +index is stale. The rule that requires this is ADR 0019's: the index is written because "a reader looking +at the folder on a forge sees the folder, not a command", and it is checked so that it cannot drift. + +New records get the highest numbers, so each new line lands at the end of its topic's group. Two branches +that each add a record to the same topic edit the same lines of the same file, and a textual merge +cannot settle that. A record numbered below one that merged first, as ADR 0241 was below 0242, has to go +above the line just added, and that conflicts too. A decision that supersedes another also edits that +older record's line to add the status mark. + +Ruled out: + +- **The forge being strict.** The protection on `main` requires only `mesh/repo-check`. It does not + require a branch to be current. The conflicts are real textual conflicts, replayed with + `git merge-tree` (see the report). +- **The record numbers.** Number collisions forced rebases of their own (#173 was renumbered twice). + Issue 155 and playbook 03 already deal with those, and they are a separate problem. Without any + collision, the index still conflicts. + +## The fixes + +**A. Generate the index when it is read, and stop storing it.** Remove the markers and the list between +them. `02-DECISIONS/README.md` keeps the six topics in their reading order with a sentence for each, and +says where the generated list is. The list is generated from frontmatter by the `hq-status` skill, which +already renders "the ADR index" and says that view is "always generated, always ephemeral". It is also +generated by the records module, which reads this repository for the mesh and can serve the list as a +read. `index.py` keeps the part of its check that still means something: every record has a topic, and +the topic is one of the six. `records.py` already checks the `topics` part. With no line to add, no +decision pull request touches a shared file. +*Cost:* a reader on the forge no longer sees the whole list in one page. They see the topics and the +folder, and each record names its topic. That is the reason ADR 0019 gave for writing the index, so this +fix needs a decision that supersedes that paragraph. + +**B. One index file per decision.** For example `02-DECISIONS/index/NNNN.md`, each holding one line. +Nothing conflicts, but nothing is gained either. The record's own frontmatter is already one file per +decision with that fact in it. A folder of one-line files is no more readable on a forge than the folder +of records, and it is a third copy of `topic:` that has to be checked against the record. + +**C. Let `index.py --write` run after each merge on `main`.** Pull requests stop touching the index, +and a job rewrites it once a merge lands. `main` is protected and every merge needs a pull request and a +person's approval, so the job opens an index pull request after every decision merges. That doubles the +merges a person approves, and leaves the index on `main` stale until they do. `index.py` on every other +pull request would then fail on the stale index, or would have to stop checking it, which gives up the +reason for writing it. + +**D. A union merge for the README** (`merge=union` in `.gitattributes`). The forge's own merge may not +read the attribute. Where it does, a union keeps both sides in the order the merge sees them, not in +number order. The ADR 0241 and 0242 hunk of 2026-10-07 would have landed 0242 above 0241, and `index.py` +would fail on `main` after the merge, which is worse than failing before it. + +## Recommendation: A + +A removes the conflict where it starts: a decision pull request no longer edits a file that every other +decision pull request edits. It also ends ADR 0019's disagreement with itself, since a decision index +with status marks is the kind of view that record says is never written to disk. The cost, a forge +reader without the full list on one page, is real but small. The topics and their order stay written, +and the list is one skill or one tool call away. B adds a copy without removing the shared file's +purpose. C makes each decision cost two approvals. D trades a conflict before the merge for a broken +`main` after it. + +A needs a decision record superseding the "the index is written" paragraph of ADR 0019, per playbook 02. +After that, the work is a change to `index.py` and `02-DECISIONS/README.md` in this repository, and the +`hq-status` skill's index view as the place the list is read. If the records module should serve the +list, that is a separate change in the catalogue. Until the decision is taken, the cost of rebasing stays +with whoever lands decisions in parallel.