Skip to content

fix(pm/os-regen-merge): step 2 keeps a MIXED path's merged bytes, and reads main's side off the pinned tip - #22843

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22829-os-regen-merge-rerun-mixed-rows
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22829-os-regen-merge-rerun-mixed-rows

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 e84aeb36ce 68ba543e -- PATTERNS content/docs/permissions/system-context.mdx
main_edited = git diff --name-only e84aeb36ce 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

…run over a MIXED path (red before the fix)

The self-test gains a classification table for all sixteen merge=os-regen
patterns (fifteen whole, one mixed), held equal to .gitattributes and to the
driver's `mixed:` rows in both directions, and one rerun fixture per mixed row:
a branch-edited hand row main never touched, a conflict elsewhere, the merge
committed, main advancing past the record, then the prescribed rerun.

Red on this commit by design: the rerun takes main's side of the mixed path
(the branch row is lost) and reverts the branch-only path to its base bytes.

Claude-Session: https://claude.ai/code/session_011u73oxZ5X95qrARPTeoU6v
Co-authored-by: Claude <noreply@anthropic.com>
…reads main's side off the pinned tip

Step 2 took main's whole side of every routed path both sides had moved,
on the premise that the driver silently dropped one side there. That holds
for a wholly generated row and not for a MIXED one: the driver defers a
mixed path only when that is lossless and otherwise text-merges or
conflicts, so taking main's side reverted the branch's hand-written rows,
exit 0, every gate green. Replayed on the real objects of the measured
rerun (merge 64e53f1, recorded base e84aeb3, tips 68ba543 and
b7cd1af): the old script restored main's side of
content/docs/permissions/system-context.mdx and committed it without row
11b; this one keeps the merged bytes and commits nothing.

Which paths are mixed is read off the driver table at the pre-merge tip,
and an unreadable table refuses step 2 before step 1 merges anything.

The set main moved is now read against the pinned main side instead of
the live origin/main, which a rerun's own fetch had moved past the
recorded merge, so a branch-only edit read as "both sides" and was
reverted to the base bytes.

The by-hand step 2 the two refusals print now names the mixed exception.
Two discriminating mutations put each fix's line back and reproduce
exactly its own half of the defect.

Claude-Session: https://claude.ai/code/session_011u73oxZ5X95qrARPTeoU6v
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 11, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 13:33
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 13:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit d489318 Oct 11, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22829-os-regen-merge-rerun-mixed-rows branch October 11, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants