feat: wikitool upstream merge/verify - code procedure for taking a stack update (4.5.0-beta.1, #30)
CI / verify (push) Successful in 57s
Release / release (push) Successful in 35s

Files changed:
- AGENTS.md
- CHANGES.md
- VERSION
- instructions/gates.md
- instructions/private-instance.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli.py
- tools/chemenu/commands/dist_cmd.py
- tools/chemenu/commands/run_budget.py
- tools/chemenu/commands/upstream_cmd.py
- tools/chemenu/ownership.py
- tools/chemenu/tests/test_upstream_cmd.py
This commit is contained in:
2026-09-04 07:08:49 +02:00
parent abe5497cda
commit 686c08bb14
13 changed files with 880 additions and 80 deletions
+45 -63
View File
@@ -108,69 +108,47 @@ So the merge has to be scoped. That is the procedure below, and it is not option
## Taking a stack update
Take the machinery, never the content. The merge is held open, the content stages are forced
back to your own state, and only then does it close.
```bash
tools/wikitool upstream merge --remote upstream --branch main
```
**Three files under those stages are machinery, not content**, and forcing them back is how an
upstream contract change gets silently discarded:
Take the machinery, never the content. This is the command form of the same idea a hand-rolled
merge would need: hold the merge open, force the content stages back to your own state, restore
only the paths that are machinery, and only then let it close. Which paths those are is not a
short literal list any more (see below) - it is `chemenu.ownership.is_stack_owned`, the same
predicate `dist_cmd.py`'s export reads, so a stack change that adds a new machinery path under a
content stage is recognised automatically rather than needing this document edited first.
| Path | Why it must take the upstream side |
**What counts as machinery under a content stage**, for readers who want the shape rather than
the code:
| Path | Why it takes the upstream side |
|---|---|
| `kb/CONTRACT.md` | The stack's own knowledge-layer contract. Every rule in it is enforced by `wikitool`; an instance never edits it |
| `kb/CONVENTIONS.md.template` | The template your `kb/CONVENTIONS.md` was filled from. The filled file is yours; the template is the stack's |
| `raw/CONTRACT.md` | The raw stage's contract, for the same reason as the first row |
| `<stage>/CONTRACT.md` (`kb/CONTRACT.md`, `raw/CONTRACT.md`, `work/CONTRACT.md`, `reports/CONTRACT.md`) | The stack's own stage contract. Every rule in it is enforced by `wikitool`; an instance never edits it |
| any `*.template` under a content stage (`kb/CONVENTIONS.md.template`, each `kb/<name>/COLLECTION.md.template`, and any later one) | The template your filled file was adopted from. The filled file is yours; the template is the stack's |
Everything else under `kb/` and `raw/` is yours, `kb/CONVENTIONS.md` and each
`kb/<name>/COLLECTION.md` included - they bind your corpus, and they are exactly what the
restore below is protecting.
Everything else under `kb/`, `raw/`, `work/` and `reports/` is yours, `kb/CONVENTIONS.md` and
each `kb/<name>/COLLECTION.md` included - they bind your corpus, and they are exactly what
`upstream merge` protects.
```bash
BEFORE=$(git rev-parse HEAD)
git fetch upstream
# --no-commit holds the merge open; it may report conflicts under kb/ or raw/,
# which the next four lines are about to make irrelevant.
git merge --no-commit --no-ff upstream/main || true
# Whatever the merge did to the content stages, undo it. HEAD is still your
# pre-merge commit while the merge is open, so this restores exactly your side.
git rm -rq --cached --ignore-unmatch kb raw
rm -rf kb raw
git checkout HEAD -- kb raw
# ...then take the upstream side back for the machinery that lives among it.
# MERGE_HEAD is still resolvable while the merge is open.
git checkout MERGE_HEAD -- kb/CONTRACT.md kb/CONVENTIONS.md.template raw/CONTRACT.md
git commit --no-edit
```
Then **check that it worked**, rather than trusting that it did. The same three paths are
excluded here, spelled out rather than held in a variable so that the check can be read on its
own and copied on its own:
```bash
git diff --name-only "$BEFORE" HEAD -- kb raw \
| grep -vE '^(kb/CONTRACT\.md|kb/CONVENTIONS\.md\.template|raw/CONTRACT\.md)$'
```
Must print nothing.
An empty result is the proof that the update touched machinery only. A non-empty one means a
path slipped through - inspect it before going further.
**The exclusion is not cosmetic.** Without it the check reports *empty* for an update that just
ate a `kb/CONTRACT.md` change - it would be confirming the failure it exists to catch. If one of
the three paths does not appear in the diff at all, that is fine: it means upstream did not
touch it.
The command itself checks its own result the same way `upstream verify` would, immediately
after committing, and refuses loudly - without rolling the commit back - if anything landed
outside a stack-owned path. A refusal there is a bug report, not something to work around by
hand; see [tools/CONTRACT.md](../tools/CONTRACT.md) for the full error contract, including what
a real conflict in `tools/`/`types/`/`instructions/` leaves behind.
Then, as after any stack change: `doctor`, `docs verify`, `instructions verify`, `migrate status`,
`lint`. A `migrate status` with outstanding links means the update crossed a compatibility
boundary - follow [migrate-corpus.md](migrate-corpus.md) before doing anything else.
**Why not just `git merge upstream/main`?** Because of the table above: a page the upstream
*adds* arrives with no conflict and no message. You would find out when `lint` starts reporting
pages you never wrote - if you noticed at all.
**Why not just `git merge upstream/main`?** A page the upstream *adds* arrives with no conflict
and no message under a plain merge - measured in the table further up this document. You would
find out when `lint` starts reporting pages you never wrote, if you noticed at all. `upstream
merge` closes exactly that gap: the content stages never see the upstream's version at all.
**Checking a merge you resolved by hand instead** (or auditing a past one): `tools/wikitool
upstream verify --since <rev-before> --until <rev-after>` runs the same check `upstream merge`
runs on itself, without doing the merge.
## Where stack development happens
@@ -186,17 +164,21 @@ merge above. Nothing is lost by the detour: the fix has to pass that CI either w
## Decision points
- **Merge conflict in `kb/` or `raw/`?** Expected, and already handled: the update procedure
above overwrites those stages with your own afterwards, so the conflict resolves itself.
Never resolve one by hand with `git add -A` - that is exactly how the upstream version, which
git left sitting in your working tree, gets committed into your instance.
- **`git diff` after the merge shows something under `kb/` or `raw/`?** Stop - unless it is one
of the three machinery paths the check excludes, which is the update working as intended. For
anything else the scoping step did not take: do not publish; find out which path came through
and where from.
- **Merge conflict in `kb/`, `raw/`, `work/` or `reports/`?** Expected, and already handled:
`upstream merge` overwrites those stages with your own afterwards, so the conflict resolves
itself. Never resolve one by hand with `git add -A` in a merge you are running yourself
instead - that is exactly how the upstream version, which git left sitting in your working
tree, gets committed into your instance.
- **`upstream merge` exits 1 after committing?** Read the message: its own postcheck found
content outside a stack-owned path in the commit it just made. The commit is **not** rolled
back - inspect it (`git show`, or `tools/wikitool upstream verify --since <before> --until
HEAD`) and decide by hand whether to revert it, fix forward, or report it as a stack bug. This
should not happen; if it does, `chemenu.ownership.is_stack_owned` disagreed with itself between
the restore and the check, which is exactly what the shared predicate is meant to prevent.
- **Conflict in `tools/`, `types/` or `instructions/`?** You changed the stack locally, which
step "Where stack development happens" says not to do. Take the upstream side and re-file the
change as an issue there.
step "Where stack development happens" says not to do. `upstream merge` leaves the merge open
rather than guessing - take the upstream side for the named paths and re-file the change as an
issue there, or resolve deliberately and finish the commit yourself.
- **...but you changed how *your pages* are written?** That is not a stack change and the rule
above does not apply to it. Language, section headings, naming forms, tone, relationship
labels and the confidence rubric live in `kb/CONVENTIONS.md`, and each collection's authoring