Repository navigation
Commit d489318
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
0 commit comments