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

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
@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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…ss — a NOT_DRIVER_MANAGED path is resolved by regeneration, not by hand (objectstack-ai#18047) (objectstack-ai#18089)

Fixes objectstack-ai#18047

`scripts/pm/os-regen-merge.sh`'s step-1 conflict report partitioned the
conflicted set against the `merge=os-regen` routing list alone, so a
generated path that is deliberately **not** routed was labelled
`NON-generated` and sent to a hand merge — while the third line of the
same message forbids resolving a generated file textually. The operator
could satisfy neither sentence, and nothing in the output said which
governed.

## Premise readings, taken before writing (worktree at `57343f761`)

| # | premise | reading | when |
|:--|:--|:--|:--|
| P1 | the partition keys solely on routing membership; the script
references `regen-artifacts.mjs` 0 times | **holds** — `grep -c
'regen-artifacts' scripts/pm/os-regen-merge.sh` ⇒ `0`;
`non_regen_conflicts` was the set difference at `:368-374` |
2026-09-14T00:28Z |
| P2 | `NOT_DRIVER_MANAGED` is exported as data and names
`packages/spec/src/migrations/registry.ts` | **holds, and is not
sufficient** — see “the design call” below | 2026-09-14T00:31Z |
| P3 | `git check-attr merge` reads `unspecified` for `registry.ts`,
`os-regen` for the lit control | **holds** —
`packages/spec/src/migrations/registry.ts: merge: unspecified`,
`packages/spec/authorable-surface/system.json: merge: os-regen` |
2026-09-14T00:29:43Z |
| P4 | no self-test fixture covers a generated + NOT-routed conflict |
**holds** — case 6 builds only driver-routed MIXED rows; the word
`NOT_DRIVER_MANAGED` did not appear in the file | 2026-09-14T00:30Z |

⚠️ The worktree was cut from `origin/main` after a sibling fetch
advanced it past the sha in the dispatch: base is `57343f761`, with
`7ef05f997` an ancestor of it (`git merge-base --is-ancestor` ⇒ exit 0).

## The design call triage fenced — no new marker was invented, and P2
needed one more reading

Triage left open whether the generated-but-unrouted set is
machine-readable or stays prose, and fenced it: *「⚠️ If the implementer
finds the three-case partition **cannot** be done without inventing that
marker, that is a new surface ⇒ report it rather than inventing one
silently.」*

`NOT_DRIVER_MANAGED` does name `registry.ts`, so membership is readable
as data and the script reads it. But membership alone **cannot carry
class 3's message**, and that is a reading of the ledger rather than a
judgement call: of its 30 tracked entries, *“resolve by regeneration”*
is correct for **three** and wrong for the other 27.

- `packages/spec/src/migrations/registry.ts`, `skills/README.md` and
`content/docs/ai/skills-reference.mdx` are MIXED — a generator owns the
text between a marker pair, a human owns everything outside it. The
module's own header names exactly these three as the files
`NOT_DRIVER_MANAGED` *“turns away”*. One more of that shape
(`content/docs/permissions/tenant-audit-census.mdx`) is reached through
a directory entry.
- the fifteen `test-typecheck-debt.json` ledgers,
`docs-import-surface.baseline.json` and their neighbours are
**shrink-only ratchets whose own entries say a merge must never
recompute them**. `packages/sdui-parser/objectui-lockstep.json` cannot
be regenerated here at all (it needs a sibling checkout); the scaffold
templates' generator refuses a file it did not already stamp;
`packages/spec/src/conversions/registry.ts` and `docs/audits/**` have no
generator whatsoever.

⛔ The entry's `gen` field is **not** the discriminator either. It is an
accounting field — recorded where the generator appears in no
`REGEN_ARTIFACTS` row — so every ratchet above carries one while
`docs-import-surface.baseline.json`, which `gen:docs` really does write,
carries none. Keying the message on `gen` would send **seventeen** paths
to a regeneration their own ledger entry forbids: this card's defect
again, one class over.

The discriminator used instead is the **generated-region marker pair in
the conflicted file**, which is the property the message actually
depends on. ⛔ That is not a new marker: both vocabularies are already
written by the tree's own generators, and `scripts/check-role-word.mjs`
spells the second one once as a consumer and states the rule — *“a
rename happens at the generators and arrives here, not the other way
round.”* So no new surface was created and nothing was added to
`NOT_DRIVER_MANAGED`; the diff is one file.

⚠️ **Reported rather than assumed:** class 3 therefore prints **two**
per-path readings, not one. The card's suggested wording is class 3's
*marked* shape; the *unmarked* shape gets the opposite instruction. A
single blanket “resolve by regeneration” for all 30 entries would have
been a new unobeyable instruction of exactly the reported kind.

## The three printed cases

Classes 1 and 2 are **byte-for-byte unchanged** and, when no class-3
path is present, the branch they live in is the pre-existing
`if/elif/else` verbatim — all 51 existing self-test cases pass
untouched.

1. **neither routed nor declared** → today's message, unchanged; and now
the only one carrying the blanket “⛔ Do not resolve generated files
textually” line, which is true of a set that by construction holds no
generated file.
2. **routed and conflicted (MIXED)** → today's message, unchanged
(objectstack-ai#14733's fix, and it is correct).
3. **declared in `NOT_DRIVER_MANAGED`** → new, one reading per path.
Real output, from the new fixture:

```text
✗ merge stopped on conflicts, and some are in files a generator writes which
  are deliberately NOT driver-managed — ⛔ neither the non-generated rule nor
  the MIXED one governs those, so each is named below with its own:
    ⚠ generated/marked.txt — GENERATED IN MARKED REGIONS, deliberately NOT driver-managed.
      ⛔ Do NOT hand-merge the generated regions — resolve them by REGENERATION:
      take either side to reach a committable state, commit the merge (step 3),
      then run its generator and commit that as its own commit:
        pnpm gen:fixture-marked
      ⚠ THE TWO SIDES DIFFER OUTSIDE THE GENERATED REGIONS (4 line(s)).
        Taking a side DROPS the other side's hand-written text there —
        silently, and with every gate green: a `check:` on this file proves
        it equals its generated sources and is no witness for the prose.
        ⛔ Carry those lines over BEFORE you regenerate.
    ⚠ generated/regions-only.txt — GENERATED IN MARKED REGIONS, deliberately NOT driver-managed.
      ⛔ Do NOT hand-merge the generated regions — resolve them by REGENERATION:
      take either side to reach a committable state, commit the merge (step 3),
      then run its generator and commit that as its own commit:
        pnpm gen:fixture-regions
      ✓ the two sides are identical outside the generated regions, so taking
        either side drops no hand-written text.
    ⚠ ledgers/whole.json — generator-touched and deliberately NOT driver-managed.
      It carries no generated-region markers, so there is no half a
      regeneration would restore: resolve it BY HAND (semantic merge, both
      intents stack). ⛔ Do NOT regenerate it as part of this merge — the
      ledger keeps the driver off it because a mid-merge recompute describes
      the half-merged tree; its `why` in scripts/regen-artifacts.mjs is the
      authority on what it may be regenerated from, and when.
  non-generated (resolve by hand — semantic merge, both intents stack):
    build/ignored.json
    src/plain.txt
  Resolve every class above by ITS OWN rule, then rerun this script to redo
  the generated-artifact half.
```

**The addendum's caveat is answered, not delegated.** Comment
`5654282996` asked for the check the tooling never makes, and the objectstack-ai#18062
transplant `5654438150` supplied its live cost: on PR objectstack-ai#17835
`registry.ts` was resolved take-a-side-and-regenerate, and
`step18.conversionIds` / `step18.rationale` — hand-authored regions
**outside** the markers — were dropped silently, so a 17→18 hop stopped
applying while a 115-family gate sweep stayed green. The script now
reads both sides out of the index it already holds (`:2:` ours, `:3:`
theirs), strips the generated regions from each, and reports whether the
remainders differ, with a line count. The clean case prints its own `✓`,
so the finding is falsifiable rather than decorative.

## How the script learns generatedness

`NOT_DRIVER_MANAGED` is read **at run time** from
`scripts/regen-artifacts.mjs` — one `node --input-type=module -e` call
importing the module through `pathToFileURL` — for the same reason
`.gitattributes` is read at run time. ⛔ No hard-coded path list. The
regeneration command is built by the module's own `ownerRunCommand`,
never assembled in the shell, so the string this script prints stays the
command the `pre-commit` gate spawns.

Three things the reader is deliberate about:

- **`untracked: true` rows are dropped.** They are gitignored build
output git never merges; the day one becomes tracked,
`git-merge-regen.mjs --self-test` refuses, so this skip hides nothing.
Pinned: a tracked file at such a path gets no class-3 reading.
- **An unreadable ledger is loud.** A silent empty list would restore
the defect with the evidence removed, so the run says the ledger could
not be read and makes no class-3 claim it cannot support. Pinned in case
9d.
- **One `git diff` per ledger row, and ⛔ never with an empty pathspec**
— `git diff --diff-filter=U --` with no pathspec matches *everything*,
which would promote every conflict into class 3. An empty ledger runs no
`git diff` at all.

## The class-3 fixture (P4's gap), and the discriminating reading

`--self-test` gains `st_fixture_ndm_conflict`, a synthetic repo whose
conflicts are all unrouted and whose ledger declares four of the five.
It carries every reading class 3 has to make in one run, including both
halves of the card's own dark-control warning — *an unlisted path reads
exactly like a non-generated one from the routing side alone*:

| fixture path | declared? | markers? | expected class |
|:--|:--|:--|:--|
| `generated/marked.txt` | yes | yes, sides differ **outside** them | 3
— regenerate + the carry-over finding |
| `generated/regions-only.txt` | yes | yes, sides differ only **inside**
| 3 — regenerate, `✓` no prose at stake (the firing control) |
| `ledgers/whole.json` | yes | no | 3 — ⛔ do **not** regenerate in this
merge |
| `build/ignored.json` | yes, `untracked` | no | 1 — the row is dropped
|
| `src/plain.txt` | ⛔ **no** | no | 1 — today's message, unchanged |

Two mutations keep the new cases falsifiable, in the style cases 6b and
8b already use. **9b** empties the ledger read and the whole set
collapses back into class 1: `GENERATED IN MARKED REGIONS` ⇒ 0,
`conflicts in NON-generated files` ⇒ 1, `Do not resolve generated files
textually` ⇒ 1 — the reported defect, reproduced on demand. **9c** pins
the real ledger rather than the fixture that models it: the module still
declares `packages/spec/src/migrations/registry.ts`, still records
`gen:migration-registry` for it, a fabricated path is absent (the
control), and `git check-attr` still reads `unspecified` for it — so
routing it or dropping its entry reddens here instead of silently
reverting the label.

## Verification

```text
✓ os-regen-merge self-test: all cases pass.
```

79 cases, at `1f8f50dd9`: the 51 that existed before, unchanged and
unweakened (no case deleted, no expectation loosened), plus 28 new ones.
Before the change the same file ran 51/51.

`node scripts/pm/dispatch-gates.mjs --commands
scripts/pm/os-regen-merge.sh` derived 26 families; all 26 ran, all
exited 0, and `--ran` reconciles:

```text
Run reconciliation — 26 derived, 26 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 26 derived famil(ies) accounted for — 26 run, 0 NOT-MEASURED
```

Named in that set and worth quoting: `pnpm check:bash32-floor`, `pnpm
check:nul-bytes`, `pnpm check:parse-guard`, `pnpm check:entry-guard`,
`pnpm check:pnpm-filter-targets`, `node
scripts/check-self-test-wired.mjs`, `node
scripts/check-scripts-symbol-anchors.mjs` — each exit 0.

⛔ **shellcheck is not a family here**: it is installed nowhere in this
container and nothing in `.github/workflows/` or `package.json` invokes
it, so there was no shell-escape residue check to derive. `bash -n`
parses clean, as do both mutated copies the self-test builds.

`pnpm lint` is CI's run, not this PR's, and the narrowing is measured
rather than asserted: ① the population read from eslint's own config —
every `files` glob in `eslint.config.mjs` is
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` or a narrower JS/TS subset, and
no shell extension appears in any of them; ② the file count from
`--format json` — this PR's one changed path returns `errorCount: 0`
with `"File ignored because no matching configuration was supplied."`,
i.e. it contributes **zero** files to the linted population; ③
invariance for untouched files — `eslint.config.mjs:327` records that
the repo *“never enables type-aware linting (no `parserOptions.project`,
no typed `@typescript-eslint` rules) for ANY file”*, so no file this
diff does not touch can change verdict.

## Changeset

⛔ None owed, and the mechanism is the label rather than a path rule:
`changeset-check` in `pr-automation.yml` counts **added**
`.changeset/*.md` files and errors when the count is zero unless the PR
carries `skip-changeset` (or is the changesets release PR). There is no
path-based exemption in the gate, so the label is the declaration.
`scripts/pm/os-regen-merge.sh` ships in no package's `files[]` — it is a
PM-loop tool run by hand, invoked by no workflow — so nothing published
moves and `skip-changeset` is the correct declaration.

## Acceptance notes

Out of scope, noted and ⛔ not filed:

- **objectstack-ai#8360** (open, `pm:on-hold`) — *“`migrations/registry.ts` still
text-merges”*. Adjacent and named by triage as possibly making this
moot: that card is about the file conflicting **at all**, this one about
what the script says when it does. Whoever takes objectstack-ai#8360 lands on a path
this PR's class-3 reading already covers; nothing here blocks or
pre-empts it. Successor: objectstack-ai#8360's implementer.
- **objectstack-ai#17602** (open, p1) — the driver exiting 0 while discarding one
side. Same family (*“a zero from this tooling is not evidence”*),
different file (`scripts/git-merge-regen.mjs`). Untouched here.
Successor: objectstack-ai#17602's implementer.
- `content/docs/permissions/tenant-audit-census.mdx` carries a `BEGIN
GENERATED:` region and is declared only through the
`content/docs/permissions/**` directory entry, so it now reads as class
3 — correctly, since its region is regenerated by
`scripts/tenant-audit-census.mjs` and guarded by
`check-tenant-audit-census.mjs`. Noted because it is the one class-3
path the card does not name. Successor: none; no change is owed.

`Clause-②: no`

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_

Co-authored-by: Claude <noreply@anthropic.com>
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