Skip to content

Commit 96cf32b

Browse files
os-elon-muskclaude
andauthored
fix(pm): make the rerun os-regen-merge prescribes able to perform step 2 (#18941)
Fixes #18895 Clause-②: no `scripts/pm/os-regen-merge.sh` told the operator, on every conflict exit, to resolve and rerun it "to redo the generated-artifact half", and there was no tree state in which that rerun could perform step 2. The run that starts the merge now records the three shas it already reads before step 1, and the rerun reads them back. ## Reproduced first — the card filed a reading, deliberately not a run The card is explicit that the rerun was never executed and that a fix validated against its prose would be validated against a reading. So the first thing built was a fixture harness that runs the **unmodified** script (`origin/main`, md5 `2b146f4a8e7a68614d0893d081a1105d`) through the whole operator sequence: run it, resolve the conflict, commit, rerun. Each fixture is a real bare origin plus a clone with a driver registered, carrying **both halves of one merge** — they pull step 2 in opposite directions: - `gen/mixed.txt` — both sides move it, the driver text-merges it and it **CONFLICTS**, so the operator sees both sides and merges them by hand; - `gen/deferred.txt` — both sides move it, the driver **DEFERS**: exit 0, keeps ours, says nothing. Main's side is gone, and nothing downstream reports it. Step 2 against the pre-merge base is the only thing that puts it back. ### OLD code, one fixture per conflict-exit branch | branch taken | the exit's instruction | rerun, tree dirty | rerun, resolution committed | |---|---|---|---| | class 1 only (non-generated) | "then rerun this script" | exit 1, `working tree not clean` | **exit 0**, 0 sides taken, main's side still dropped | | class 2 only (MIXED routed) | "then continue with step 4" | exit 1, `working tree not clean` | **exit 0**, 0 sides taken, main's side still dropped | | class 1 + class 2 | "then rerun this script" | exit 1, `working tree not clean` | **exit 0**, 0 sides taken, main's side still dropped | | class 3 (NOT_DRIVER_MANAGED) | "then rerun this script" | exit 1, `working tree not clean` | **exit 0**, 0 sides taken, main's side still dropped | Verbatim, from the class-1+2 run on the old script: ``` [run 1] exit 1 [run 1] conflict headline: x merge stopped on conflicts in BOTH non-generated and generated files: [run 1] says 'rerun this script': 1 [run 1] gen/silent.txt = silent v1-BRANCH main's side present: 0 [leg A: rerun while the tree is dirty] exit 1 :: x working tree not clean - commit or drop local changes first [leg B: commit the resolution, rerun on a clean tree] exit 0 [leg B] merge-base HEAD origin/main = d62ed07 ; origin/main = d62ed07 [leg B] 'TAKING main s side' notices: 0 [leg B] 'KEEPING the branch s bytes' notices: 2 [leg B] main's side restored in worktree: 0 [leg B] main's side restored in HEAD: 0 [control] by-hand step 2 against the pre-merge base d3055bd: [control] gen/silent.txt = silent v1-MAIN ``` The control line is the whole point: the repair was **always available**, and only the base was wrong. `merge-base HEAD origin/main` equals `origin/main` on that line — the card's mechanism, observed. ### The two branches the card left unmeasured, and one it did not count The card asked whether `:699` and `:737` reach the same dead end by the same route. **They do, and the route is not a property of any branch**: every conflict exit returns the operator to the same preamble, which refuses a dirty tree and derives a wrong base from a clean one. The branch only decides which text prints before `exit 1`. One correction to the card's count, measured: there are **four** conflict exits, not three, and the fourth (class 2 only) did not say "rerun this script" at all — it said "continue with step 4". It reaches the same place by **omitting** step 2 rather than by prescribing an inert one, which is strictly quieter. All four now carry one shared instruction, emitted from one function. ### NEW code, the same four fixtures ``` [run 1] exit 1 (unchanged: step 1 still stops on the conflict) [run 1] says 'rerun this script': 1 (all four branches, one shared text) [leg A: rerun while the tree is dirty] exit 1 (unchanged: the dirty refusal still holds) [leg B: commit the resolution, rerun] exit 0 [leg B] first line: -> RERUN: the merge committed in HEAD is the one this worktree recorded [leg B] 'TAKING main s side' notices: 1 (the DEFERRED path, and only it) [leg B] main's side restored in worktree: 1 [leg B] main's side restored in HEAD: 1 ``` and the operator's hand resolution of the conflicted path survives: ``` gen/mixed.txt in HEAD: RESOLVED both intents record: phase=done base=9596489... branch_tip=31872de... main_tip=f2b78b5... second run: -> step 2 is already discharged for the merge in HEAD - nothing to merge and nothing to redo (exit 0) ``` ## What the record is, and what it is not It is **not** the `--base` flag the header refuses. Nothing is supplied by the operator and nothing is guessed: the run that starts the merge writes the base, its own pre-merge tip and the `origin/main` tip it is about to merge into `$GIT_DIR/os-regen-merge-base`, and the rerun reads them back. It is the same base, at the only moment it can still be read. A base it cannot read is one it **refuses to invent**. Three findings changed the shape the dispatch suggested, each measured: 1. **The rerun must not re-read `origin/main` at all.** The suggested detection asked whether `git merge-base HEAD origin/main` equals `origin/main`'s tip. In this repo `origin/main` is a **shared ref** a sibling's fetch advances, so between the conflict and the rerun it can move, and that test then answers "not a rerun" for a merge that plainly happened. Containment is asked against the **recorded** main tip instead, and step 2 restores from that pinned sha. A moved `origin/main` prints a notice and is not merged in — the rerun repairs the recorded merge only. 2. **A completed run marks its record `done` instead of deleting it.** "Leave nothing behind" and "refuse when no base is recorded" cannot both hold: a clean completion and a rerun nobody performed leave trees that read **identically** (clean, HEAD contains `origin/main`, step 2 provably inert). Deleted, the refusal would fire on a finished merge. Marked, the settled tree reports "already discharged" and exits 0 exactly as it always did — pinned as a case. 3. **The conflict exit records which paths it left conflicted.** Without that, the rerun's step 2 takes main's side of the MIXED path the operator just hand-merged — reverting the resolution the message asked for. A conflicted path is one git could **not** resolve silently, so no side was dropped there; step 2 chooses a side only where the driver chose one without saying so. Both directions are pinned, because a fixture with only one of these paths would pass with the other's rule deleted. Refusals, all non-zero, all printing the record: a **missing** record in the rerun tree (and it computes the by-hand step 2 from the merge commit's own two parents, which is what the PR 18752 round did by hand), a record whose branch tip is **not an ancestor** of HEAD, and a record naming a commit this repository does not have. ``` x HEAD already contains origin/main and NO pre-merge base is recorded - refusing to run step 2 against a base that is knowably wrong. ... HEAD is a MERGE COMMIT, so both sides are still readable from it: ... base=$(git merge-base 9d58a6f5... cab1efd9...) git restore --source=cab1efd9... -- (every path in BOTH lists) # tree only, never --staged ``` `scripts/regen-artifacts.mjs`, `scripts/git-merge-regen.mjs` and `scripts/check-regen-pending.mjs` are read, never edited. The driver's `os-regen-pending` marker is theirs and is neither written nor read here; the header says so. Both files live in `$GIT_DIR` for the same reason — in a linked worktree that is `.git/worktrees/NAME`, so parallel agents never read each other's merge. ## Verification - `bash scripts/pm/os-regen-merge.sh --self-test`: **79 cases before, 122 after**, all pass, both measured in-tree (the baseline copy run out of tree fails case 9c on a relative ledger path, which is the harness, not the script). Case 10 drives the prescribed sequence end to end; case 10b is the **discriminating mutation** — neutralising the single `rr_mode="$(rr_classify)"` call site restores the old behaviour exactly, and the same fixture then reads exit 0, zero sides taken, and main's side still dropped in the commit. The card is reproducible on demand, in the tree, forever. - Derived gate families: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from this worktree, **23 commands, all exit 0**, each captured with redirect-then-`$?`. `--ran` reconciliation: `23 derived famil(ies) accounted for - 23 run, 0 NOT-MEASURED (a DERIVED zero - all 23 recorded an exit code and none of them is 3)`. The set includes `check:bash32-floor`, `check:self-test-wired`, `check:self-test-workflow-commands`, `check:scripts-symbol-anchors`, `check:entry-guard`, `check:parse-guard`, `check:nul-bytes`. - No workspace package is touched, so there is no dependency-closure build and no package test or typecheck owed. No tracked test file names this script: its `--self-test` is its suite. - `pnpm lint` narrowed, and the narrowing is a **measurement** rather than a skip, at `752a3abff`: the population read from `eslint.config.mjs` itself is `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` (plus narrower TS-only blocks); `--format json` over the one changed file returns 1 entry with 0 errors and the single message `File ignored because no matching configuration was supplied.` — i.e. **0 files linted**; and the config declares no `parserOptions.project` and no typed rules (its own comment at line 328 says so), so type-aware linting is off and this diff cannot move the verdict on any untouched file. - Control-character self-scan over the changed file: no match (`check:nul-bytes` green as well). - `skip-changeset` measured rather than assumed: the root package is `private: true`, and across the 76 published `packages/*` manifests, **0** `files[]` entries reach `scripts/pm`. Nothing publishes. ## Reader test An operator who hits a class-2 conflict, resolves it, commits and reruns as the message says now gets step 2 against the pre-merge base: main's dropped side is restored and committed, their own resolution is left alone, and step 4's regeneration then re-derives the generated artifact on a known-good base — carrying both sides, as PR 18752's round showed a correct step 2 buys. ## Acceptance notes - The branch is 3 commits behind `origin/main` on four disjoint paths (`content/docs/data-modeling/analytics.mdx`, `content/docs/ui/dashboards.mdx`, `packages/spec/scripts/check-duration-unit-keys.ts`, `scripts/pm/check-widening-tells.mjs`), none of them this file, none generated, so the joint-breakage re-check is scoped to nothing and the merge was deliberately not taken. Re-deriving the gate families after a fresh fetch returns the identical 23 commands. Declared, not skipped. - Step 2 was also fixed to restore from a **pinned sha** rather than the `origin/main` ref on the plain path too, since a sibling's fetch can advance that shared ref mid-run. The self-test's source-spelling pin moved with it: it now pins the non-staging verb against that variable and keeps both staging spellings as absences. - **noted, not filed:** the `lint.yml` step comment describing this self-test says "five small fixture repos to pin 23 cases". That was already stale before this PR (the tree's own count was 79 across nine fixture builders) and is now 122. It is prose in a workflow comment with no gate reading it, and no PR or person is routed to that file by this change — successor: none. Left as it is rather than widening this diff into `.github/workflows/`. --- _Generated by [Claude Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 39abe1d commit 96cf32b

1 file changed

Lines changed: 632 additions & 21 deletions

File tree

0 commit comments

Comments
 (0)