feat: wikitool upstream merge/verify - code procedure for taking a stack update (4.5.0-beta.1, #30)
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:
@@ -103,6 +103,24 @@ The setup this gate exists for - a private instance that takes stack updates fro
|
||||
upstream - is [private-instance.md](private-instance.md). Step 4 there arms it, deliberately
|
||||
*before* the first `publish`: added afterwards it leaves open exactly the window it closes.
|
||||
|
||||
### Mass-Update Gate blind spot: `upstream merge`
|
||||
|
||||
`upstream merge` (a private instance taking a stack update - see
|
||||
[private-instance.md](private-instance.md)) can update or delete dozens of stack-owned paths in
|
||||
one commit, and the Mass-Update Gate does not see any of it. The gate counts *working-tree*
|
||||
changes before `publish` stages them; by the time `upstream merge` commits, the change is
|
||||
already history, and the commit it made is not what a later `publish` would be staging - that
|
||||
publish sees only whatever this session adds on top. A merge touching 200 files therefore goes
|
||||
out ungated the moment it is pushed.
|
||||
|
||||
This is not a hole to patch by making `upstream merge` route through the gate: the gate's
|
||||
question ("is this too much to publish?") does not apply to a change that only ever touches
|
||||
stack-owned paths that are, by definition, not this instance's own content. The check that
|
||||
actually matters here is `upstream merge`'s own postcheck - it re-verifies the merge commit
|
||||
against `upstream verify`'s logic immediately after committing, and refuses loudly (uncommitted
|
||||
state is not rolled back; the commit is left for a human to look at) if anything landed outside
|
||||
a stack-owned path. That is the safeguard for this command, not the Mass-Update Gate.
|
||||
|
||||
## Iteration Budget Gate and loop-breaker
|
||||
|
||||
Every `wikitool` call is counted per session. Calls are refused past **60 in a session**, or
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user