Skip to content

Commit d489318

Browse files
fix(pm/os-regen-merge): step 2 keeps a MIXED path's merged bytes, and reads main's side off the pinned tip (#22843)
Fixes #22829 Clause-②: no — a PM merge helper under `scripts/pm/**`; no package's `files[]` ships it, and no public surface moves (`skip-changeset`). ## Root cause (measured, not inferred) The card's lead is falsified: the rerun read the **right** pre-merge base. The defect is step 2's per-file RULE, and it fires on a first run too. Step 2 took main's whole side of every routed path that **both** sides had moved, on the premise that the driver silently dropped one side there. That premise holds for a wholly generated row. It is false for a **MIXED** row (a generated half plus hand-written rows, declared by `mixed:` on its `REGEN_ARTIFACTS` row). The driver defers a mixed path only when deferring is lossless; otherwise it text-merges (both sides' rows kept) or conflicts. So "both sides changed it" does not mean a side was dropped. Taking main's side there is a pure revert of the branch's rows. What step 2 computed on the measured rerun, replayed against the real objects: | input | value | |---|---| | recorded base / pre-merge tip / main tip | `e84aeb36ce` / `68ba543e` / `b7cd1af9df` (= the merge `64e53f1d`'s merge-base and its two parents) | | `branch_edited` = `git diff --name-only e84aeb3 68ba543 -- PATTERNS` | `content/docs/permissions/system-context.mdx` | | `main_edited` = `git diff --name-only e84aeb3 origin/main -- PATTERNS` (live ref; at least `b7cd1af9df`) | against `b7cd1af9df` alone: 22 paths, `system-context.mdx` among them; main changed it 9+/10− (census figures, row 14 deleted) | | conflicted set in the record | the two tenant-audit files, both unrouted, so `system-context.mdx` is not in it | | the driver's verdict on `system-context.mdx` | clean TEXT merge: `64e53f1d` holds the branch's rows 11b/23b **and** main's edits | | what step 2 then ran | `git restore --source=b7cd1af9df -- content/docs/permissions/system-context.mdx` (via the per-pattern loop), and step 3 committed it | **Replay on the real objects.** A throwaway worktree at `64e53f1d` was given the record above. - The pre-fix script (`d8c7d3864`) exits 0 and prints `TAKING main's side of content/docs/permissions/system-context.mdx`. Step 3 committed `bf8a1750b`, and row 11b's branch text is absent from HEAD (count 0). - The fixed script exits 0 and prints `KEEPING the merged bytes of content/docs/permissions/system-context.mdx (MIXED …)`. Step 3 has nothing to commit. Row 11b is present (count 1) and main's row-14 deletion is kept too. ### Which base and which tips step 2 reads (M1) | run | merge base | `branch_edited` | `main_edited` before | `main_edited` now | restore source | |---|---|---|---|---|---| | plain | `git merge-base HEAD origin/main`, read before step 1 | base .. pre-merge HEAD | base .. **live** `origin/main` (same sha as `$main_side`, pinned one statement earlier) | base .. `$main_side` | `$main_side` | | rerun | recorded `base=` | recorded base .. recorded `branch_tip=` | recorded base .. **live** `origin/main`, after this run's own `git fetch` | recorded base .. recorded `main_tip=` | recorded `main_tip=` | The second column of `main_edited` is the second defect, in the same step and with the same symptom. On a rerun after main advanced, a path only the branch had edited read as "both sides", and step 2 restored the recorded tip's bytes (the base's) over the branch's edit. It did not fire on the measured rerun (`b7cd1af9df` had already moved `system-context.mdx`). The fixture below reproduces it on demand. ## The change (`scripts/pm/os-regen-merge.sh` only) 1. **Step 2 keeps a MIXED path's merged bytes.** This applies to a path both sides moved that is not in the conflicted set. It prints a per-path `KEEPING the merged bytes … (MIXED …)` notice, and step 4's regeneration re-derives the generated half as before. - Which paths are mixed is read from the driver's own `REGEN_ARTIFACTS` `mixed:` rows **at the pre-merge tip**, which is the copy the driver ran with (`git show BRANCH_TIP:scripts/regen-artifacts.mjs`). - The read happens only when some path moved on both sides. If the table cannot be read, step 2 **refuses before step 1 merges anything** and never guesses. 2. **`main_edited` reads the pinned `$main_side`**, never the live `origin/main`. 3. The by-hand step 2 printed by the `advanced` and `orphan` refusals now names the MIXED exception. Without it, the by-hand recipe reproduced the same revert. 4. The header's step list said step 2 takes main's side of paths "THE BRANCH HAS NOT EDITED". The code never did that, and the card quotes the line. It now states the actual rule, and a new header section records the measurement above. No change to `.gitattributes`, `scripts/git-merge-regen.mjs`, `scripts/regen-artifacts.mjs` or any generator. No gate, workflow or script file was added. Everything lives in the existing `--self-test`, which `lint.yml` already runs unconditionally. ## Classification of all 16 routed patterns (`OS_REGEN_CLASSES`, in the script) Measured at each generator's write path on `d8c7d3864`. The script's table holds this classification; the self-test checks it against the real `.gitattributes` and against the driver's `mixed:` rows, in both directions. | pattern | class | generator (how it writes) | |---|---|---| | `packages/spec/liveness/state-counts/**` | whole | `gen:liveness-counts`, `writeTextShardDir` | | `packages/spec/authorable-surface/**` | whole | `gen:schema`, renders each shard | | `packages/spec/authorable-surface.base.json` | whole | `gen:schema --update-base`, `serializeSurfaceBase` | | `packages/spec/authorable-defaults/**` | whole | `gen:schema` | | `packages/spec/json-schema.manifest/**` | whole | `gen:schema` | | `packages/spec/api-surface/**` | whole | `gen:api-surface` | | `packages/spec/src/meta-spelling/meta-url-data.generated.ts` | whole | `gen:meta-url-spelling`, `render()` then write (reads the old file only to compare) | | `packages/spec/export-origins/**` | whole | `gen:export-origins` | | `packages/spec/declaration-map/**` | whole | `gen:declaration-map` | | `packages/spec/api-surface-signatures.json` | whole | `gen:api-surface` | | `docs/audits/2026-07-unknown-key-strictness-ledger.counts/**` | whole | `gen:strictness-ledger`, `writeTextShardDir` | | `content/docs/references/**` | whole | `gen:docs` (its header: no preserve/marker mechanism, by design) | | `content/docs/permissions/system-context.mdx` | **mixed** | `gen:system-context-census` = `check-system-context-census.mjs --fix`, which rewrites census counts inside hand-written prose | | `skills/*/references/_index.md` | whole | `gen:skill-refs`, `generateIndex()` | | `skills/objectstack-ui/references/react-blocks.md` | whole | `gen:react-blocks`, one `emit` of the whole file | | `scripts/platform-object-tenancy-census.json` | whole | `gen:platform-object-tenancy-census`, `renderArtefact` | ## Fixtures (self-test cases 12, 13, 13a, 13b) - **12, classification pin.** The real table agrees with the real `.gitattributes` and the real driver (an empty problem list). Three firing controls show the check can go red: - a newly routed pattern reds `unclassified`; - a dropped route reds `stale`; - a driver with no `mixed:` rows reds `mixed-mismatch`. - On a red, the run prints the remedy: classify the pattern, and give a `mixed` row its `mixed:` on the driver row. - **13, one fixture per `mixed` row.** There is one today: `content/docs/permissions/system-context.mdx`, built at that path and routed by the table's own pattern. The fixture driver uses `git merge-file`, the real driver's non-lossless limb. - The branch hand-edits a row that main never touches. Main edits two other rows of the same file. A plain file conflicts. The resolution is committed. Main then advances on a routed path only the branch had edited, and the rerun runs. - Asserted: the path is routed; the rerun exits 0 with `KEEPING`, never `TAKING`; HEAD holds the branch row **and** both main edits (3 of 3); the branch-only path keeps the branch bytes. A guard reds if the table ever names zero mixed rows. - A first-run leg (same fixture, no conflict anywhere) asserts that the branch row survives there too. - **13a, discriminating mutations** (the repo's `perl \Q..\E` convention). Each one puts back one fix's line and reproduces exactly its own half of the defect. - **13b.** An unreadable driver table refuses step 2, and origin/main is not merged. ## Readings | reading | tree | result | |---|---|---| | baseline self-test | `d8c7d3864` | 127 ok, exit 0 | | **red before** (fixtures in, fix out) | `326e49850` | 131 ok, 4 FAIL: rerun `0/0/1` (wanted `0/1/0`: TAKING, no KEEPING); branch row 2 of 3; branch-only path `branchonly v0`; unreadable table `0/0/merged` (wanted `1/1/unmerged`) | | first-run leg against the pre-fix script (one-off probe, not kept) | `d8c7d3864` script | `0/0/0`, wanted `0/1/1`: the branch row is lost on a first run as well | | **green after** | `a280febb3` | 140 ok, 0 FAIL, `✓ os-regen-merge self-test: all cases pass.` | | mutation `mixed` (`case "$mixed_edited" in` replaced by `case "" in`) | `a280febb3` | anchor 1 site / changed / parses = `1/1/0`; run reads `0/1/0/branchonly v1-BRANCH`: exit 0, TAKING the mixed path, branch row gone, the other half intact | | mutation `ref` (`main_edited` back on `origin/main`) | `a280febb3` | `1/1/0`; run reads `0/0/1/branchonly v0`: the branch-only path reverted to base bytes, the mixed half intact | Line budget: 2262 lines before, 2511 after, net **+249** (+259 / −10) against the +250 cap. ## Gates (all at `a280febb3`) - The 24 commands `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives all exit 0. - `--ran` reconciliation: `✓ dispatch-gates --ran: 24 derived famil(ies) accounted for — 24 run, 0 NOT-MEASURED`. - Also run, as named by the dispatch: `bash scripts/pm/os-regen-merge.sh --self-test` (exit 0, 140 ok) and `pnpm check:pm-governed-merges` (exit 0). - `scripts/check-shell-escape-residue.mjs` does not exist on this tree, so there is nothing to run there. - No package is touched, so there is no build closure and no package test or typecheck. - `pnpm check:nul-bytes`: OK, plus a raw control-byte scan of the script: 0 hits. ## Acceptance notes (observations, not filed) - This self-test has no battery roster and no verdict handshake (AGENTS.md, "Writing a `--self-test`"). The gap predates this PR. The new per-mixed-row loop carries its own non-vacuity guard. Carrier: none. - `lint.yml`'s comment above this step still says "five small fixture repos to pin 23 cases". It is drift in a comment; the current count is 140. - The worktree was cut from `origin/main` `d8c7d3864` rather than the dispatch's `efcbac73c`, because the standing dev contract says `origin/main`. Between the two commits, `scripts/pm/os-regen-merge.sh`, `.gitattributes` and `scripts/git-merge-regen.mjs` have an empty diff. Family, for the reviewer: #18062, then #18895 (landed in #18941 for its measured case). Both stay as they are; this PR touches neither. --- _Generated by [Claude Code](https://claude.ai/code/session_011u73oxZ5X95qrARPTeoU6v)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8c105e3 commit d489318

1 file changed

Lines changed: 259 additions & 10 deletions

File tree

0 commit comments

Comments
 (0)