Skip to content

fix(devx): partition os-regen-merge.sh's step-1 conflict message by class - #14733

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-14671-os-regen-merge-mixed-message
Sep 3, 2026
Merged

fix(devx): partition os-regen-merge.sh's step-1 conflict message by class#14733
baozhoutao merged 1 commit into
mainfrom
claude/issue-14671-os-regen-merge-mixed-message

Conversation

@claude

@claude claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #14671

os-regen-merge.sh step 1 called every git merge conflict "in NON-generated
files" and told the operator "do not resolve generated files textually" —
correct for an ordinary conflict, but exactly backwards for a merge=os-regen
path the driver text-merged and left conflicted (a MIXED row
git-merge-regen.mjs declined to defer because deferring would silently drop
hand-written prose). Following that line's instruction on a MIXED conflict —
take one side whole — is precisely the outcome the driver refused to allow.

scripts/git-merge-regen.mjs (the driver) is untouched — it behaved
correctly throughout; the defect was entirely in the wrapper's summary of it.
No relaxation of the deferral rules, no "generated always takes ours"
shortcut.

The fix

Partition the conflicted set (git diff --name-only --diff-filter=U) against
the merge=os-regen path list the script already reads at :220, reusing it
as a git diff pathspec the same way step 2 already does for
branch_edited/main_edited. No new inputs, no new config.

Any conflict on a regen path is necessarily one the driver declined to defer
(a non-mixed row always resolves with exit 0, no markers), so a regen-path
conflict always means "hand-resolve the prose," never "take one side" — the
opposite of what a wholly-generated deferred path calls for.

Before (every conflict, regardless of class):

✗ merge stopped on conflicts in NON-generated files — resolve those by hand
  (semantic merge, both intents stack), then rerun this script to redo the
  generated-artifact half. ⛔ Do not resolve generated files textually.

After — non-regen conflicts only (unchanged):

✗ merge stopped on conflicts in NON-generated files — resolve those by hand
  (semantic merge, both intents stack), then rerun this script to redo the
  generated-artifact half. ⛔ Do not resolve generated files textually.

After — regen-path conflicts only (MIXED, driver-declined; the
suppressed-line case):

✗ merge stopped on conflicts in GENERATED files the driver declined to defer
  (MIXED — a generated half plus hand-written prose; see its notice above).
  Hand-resolve the prose; the anchor numbers do not matter here — take
  either side of them, then run the regeneration command the driver printed
  above, then continue with step 4:
    gen/mixed.txt

The "⛔ Do not resolve generated files textually" line is gone here — it is
correct advice for a deferrable regen path and actively wrong, silently
destructive advice for a MIXED one. The driver's own notice (including the
regeneration command it prints, e.g. pnpm gen:system-context-census) is
captured from the merge's output and echoed above this message rather than
scrolling past unread — so the operator is pointed at the driver's own
remedy, not a generic "step 4".

After — both classes present:

✗ merge stopped on conflicts in BOTH non-generated and generated files:
  non-generated (resolve by hand — semantic merge, both intents stack):
    src/prose.txt
  generated, MIXED — the driver declined to defer these (see its notice
  above). Hand-resolve the prose; the anchor numbers do not matter here —
  take either side of them, then run the regeneration command the driver
  printed above:
    gen/mixed.txt
  Resolve both, then rerun this script to redo the generated-artifact half.

Each file is named under its own class; the suppressed line stays suppressed
here too, since some of the generated conflicts present in this run do need
hand-resolution.

Self-test

Added st_fixture_regen_conflict — a fixture whose merge driver is a small
script that runs a real git merge-file (not the existing fixture's
true no-op) and, on a genuine 3-way conflict, prints a driver-shaped remedy
(mirroring git-merge-regen.mjs's actual "NOT deferred… take either side and
then run: pnpm gen:fixture-mixed" notice) before exiting non-zero — the same
move the real driver makes for a MIXED row it cannot losslessly defer.

Three new case blocks:

  • regen-only MIXED conflict — asserts the new message, that it does NOT
    say "NON-generated files", and — the assertion that matters — the
    absence of "Do not resolve generated files textually"; also asserts the
    driver's own notice and its regeneration command are still shown.
  • the discriminating mutation — proves the absence assertion is not
    vacuous: a literal (non-regex, perl -0777 \Q..\E) replacement of the
    regen-branch message back to the old unconditional one, run against the
    same fixture, and the suppressed line reappears in that run's output —
    i.e. the pin reds on the original bug.
  • both classes present — a non-regen conflict (src/prose.txt) alongside
    the regen-path MIXED conflict (gen/mixed.txt); asserts both files are
    named under their own class and the suppressed line stays absent.
$ bash scripts/pm/os-regen-merge.sh --self-test
...
  ok    a regen-only MIXED conflict fails the run
  ok    and does NOT call it a non-generated-file conflict
  ok    and does NOT forbid textual resolution of the generated file
  ok    and DOES say the file needs hand-resolving
  ok    and names the conflicted regen path
  ok    and the driver's own notice is still shown
  ok    and the driver's regeneration command is still shown
  ok    the mutation anchor was found and replaced (falsifiability check)
  ok    the mutation actually changed the script text
  ok    and the mutated script still parses
  ok    mutated: the suppressed line is back (proves the assertion bites)
  ok    a mixed-classes conflict fails the run
  ok    and says BOTH classes are present
  ok    and names the non-generated file
  ok    and names the generated (regen) file
  ok    and the suppressed line is absent here too
✓ os-regen-merge self-test: all cases pass.

(38 cases total pass, including all pre-existing ones — none were changed.)

Scope

$ git diff --stat 4d0d9445a 777e44e77
 scripts/pm/os-regen-merge.sh | 236 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 232 insertions(+), 4 deletions(-)

Only scripts/pm/os-regen-merge.sh changed. scripts/git-merge-regen.mjs
(the merge driver) is untouched — confirmed by the diff above, and by design:
it classified the MIXED file, declined to defer, explained why, and printed
the right remedy throughout. The defect was entirely in the wrapper's summary
of a correctly-behaving driver.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands scripts/pm/os-regen-merge.sh derived 14 families; all 14 ran
(under scripts/pm/os-verify-lock.sh) and passed —
node scripts/pm/dispatch-gates.mjs --ran ran.list confirms 14 derived, 14 run, 0 NOT-MEASURED, 0 UNRUN (check-test-completeness.mjs is a
PREREQUISITE-NOT-MET/NOT-MEASURED by its own design when run without a saved
turbo run test log — not a red, per its own printed text — and CI supplies
that log). bash scripts/pm/os-regen-merge.sh --self-test (the always-runs
lint.yml step) passes directly.

skip-changeset applies: this PR touches only scripts/pm/**.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV


Generated by Claude Code

…lass

os-regen-merge.sh step 1 called every merge conflict "in NON-generated
files" and told the operator "do not resolve generated files textually" --
correct for a plain non-regen conflict, but exactly backwards for a
`merge=os-regen` path the driver text-merged and left conflicted (a MIXED
row `git-merge-regen.mjs` declined to defer because the deferral would have
silently dropped hand-written prose). Following that line's instruction on
a MIXED conflict -- taking one side whole -- is precisely the outcome the
driver refused to allow.

Partition the conflicted set (`git diff --diff-filter=U`) against the
`merge=os-regen` path list the script already reads at :220, reusing it as
a pathspec the same way step 2 already does. Three messages:

  - non-regen only: today's message, unchanged.
  - regen-path only (MIXED, driver-declined): "hand-resolve the prose, take
    either side of the anchors" -- the "do not resolve generated files
    textually" line is suppressed, since it is about deferrable regen paths
    and is actively wrong advice for a MIXED one.
  - both classes: name each file's class; the suppressed line stays
    suppressed since some of the generated conflicts present DO need
    hand-resolution.

The merge's own output (including the driver's remedy, e.g. its printed
regeneration command) is now captured and echoed on both the success and
failure paths, so the operator sees the driver's own notice rather than a
generic pointer to "step 4".

Pinned in --self-test: a MIXED-conflict fixture (a real driver that
text-merges and conflicts, not the existing fixture's `true` no-op) for the
regen-only and both-classes cases, whose key assertion is the ABSENCE of
the "do not resolve generated files textually" line -- proven
discriminating by mutating the script back to the old unconditional message
and watching that assertion red.

scripts/git-merge-regen.mjs (the driver) is untouched; it behaved correctly
throughout. No relaxation of the deferral rules.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions github-actions Bot added the size/m label Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 23:08
@baozhoutao
baozhoutao enabled auto-merge September 2, 2026 23:08
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33695084575 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 26 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33696601161 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 29 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 224f8ea Sep 3, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14671-os-regen-merge-mixed-message branch September 3, 2026 00:20
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