Skip to content

fix(cli): one column width for the file family in both os generate migration formats - #18014

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-17883-generate-migration-file-column-width
Sep 13, 2026
Merged

os-sales merged 5 commits into
mainfrom
claude/issue-17883-generate-migration-file-column-width

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #17883

Clause-②: no

os generate migration gave the file family — file / image / avatar / video / audio — two different column widths depending on --format. The typescript format emitted a bare table.string(name), which generate.ts itself states is knex's varchar(255), while --format sql emitted VARCHAR(2048) for the same field.

The stop condition, measured first

Triage ruled the direction (the two halves must agree) with a hard fence: if 255 turns out to be deliberate, stop and route the card to needs-user-decision instead of forcing the alignment. generate.ts:1456 says VARCHAR(50) and VARCHAR(2048) "were widths this file invented", and invented numbers have history. Five readings, taken on this branch's base 611795e40:

  1. ADR-0104 records the maintainer ruling on [finding] FILE_REFERENCE_TYPES disagree about their column: driver-sql puts file/image/avatar/video/audio in JSON_COLUMN_TYPES, packages/cli generate.ts gives them VARCHAR(2048) — and neither side is obviously the one that should move #15041 verbatim: "The driver is the side that moves; the generator's VARCHAR(2048) already states the ruled end-state and stands." Its column section adds the operative criterion: the driver's width mirror "answers for the family what the generator answers, so the pin that sweeps the other string classes can sweep this one — a width the two sides disagree on would be this fork again, one level down."
  2. driver-sql's own constant header (MEDIA_ID_VARCHAR_CHARS = 2048) states it as a rule, not a taste: "A driver that created knex's default varchar(255) here would open a fresh divergence between the two producers of the same column."
  3. media-column-move.ts:101MEDIA_ID_MOVE_WIDTH = 2048, used in the type varchar(2048) using (…) retype. Read as a control only; not touched.
  4. History: git log -S on the arm shows the family joined the typescript switch's REFERENCE arm in 431979e67 (fix(cli): give every FieldType member a generate.ts vocabulary entry, and make the next gap loud #14834), a card about giving every FieldType member an entry — a presence fix. That arm's derivation is the target row's id column (table.string('id').primary()), which was never the file family's derivation. The 255 was inherited by sharing an arm, never chosen for this family.
  5. No dialect depends on it: the driver's own ceiling is MAX_VARCHAR_CHARS = 16383, and both the SQL format and the driver already ship 2048 for this family everywhere.

No reading makes 255 deliberate, so the alignment stands and the card does not move to needs-user-decision. The typescript half is the side that was out of step.

The change

The family leaves the reference arm and takes one of its own. The width is read from the SQL format's own entry rather than retyped beside it, so the two formats cannot drift apart again — a second literal 2048 would be the same defect one line over.

-    table.string('cover_image').nullable();
+    table.string('cover_image', 2048).nullable();

⛔ The SQL half did not move. 2048 is the already-shipped target; the repair is the typescript half joining it, never the two halves meeting in the middle. The new pin asserts the agreed width against driver-sql's own constant precisely so that two halves that had both drifted to knex's default could not satisfy it.

Acceptance — both directions, five types, with the control the file documents

One probe object, one field per type, both formats, before and after the fix. The whole delta is five lines:

     table.string('id').primary();
-    table.string('f_file').nullable();        +    table.string('f_file', 2048).nullable();
-    table.string('f_image').nullable();       +    table.string('f_image', 2048).nullable();
-    table.string('f_avatar').nullable();      +    table.string('f_avatar', 2048).nullable();
-    table.string('f_video').nullable();       +    table.string('f_video', 2048).nullable();
-    table.string('f_audio').nullable();       +    table.string('f_audio', 2048).nullable();
     table.text('f_text').nullable();               [control — byte-identical]
     table.string('f_lookup').nullable();           [control — byte-identical]
     table.string('f_master_detail').nullable();    [control — byte-identical]
     table.string('f_user').nullable();             [control — byte-identical]
     table.string('f_tree').nullable();             [control — byte-identical]
     table.string('f_autonumber').nullable();       [control — byte-identical]
  • Field by field, all five: --format sql gives VARCHAR(2048) and the typescript format gives table.string(name, 2048) for file, image, avatar, video and audio. One verified is not five, so the pin sweeps FILE_REFERENCE_TYPES imported from the spec rather than a list written out.
  • The control did not move: f_text — the non-file family the file itself documents at generate.ts:1403-:1405, where both halves already agree — is byte-identical before and after, and so is every other non-family column and the entire --format sql emission (the diff above has no hunk on the SQL side at all). ⛔ No control was invented; the documented one was used.

Reverse verification. With the fix committed, the pre-fix generate.ts was put back on disk and the new pin run against it: 5 of 8 tests red, one per family member, with the three control/anti-vacuity tests staying green — the discriminating direction. On-disk proof of the mutation rather than an editor's exit code: the injected resolver went 4 occurrences ⇒ 0, the blob hash moved 8d8c34a01fccbeff. Restore verified by state, not by exit code: blob back to 8d8c34a0, git diff HEAD empty. The subject resolves through a relative intra-package import (./generate.js), never through a dependency's exports, so no dist sits between the edit and the verdict.

Premise falsified — acceptance item 4 pointed at the wrong table

The dispatch order asked for the self-describing table at generate.ts:1461 to be updated, on the reading that it "still records the pre-#15989 three-way reading (driver varchar(255)) and is wrong on its first column". Re-derived on this branch, that is not so:

:1460  //   f_phone  driver varchar(255)   sql gen varchar(50)    ts gen varchar(255)
:1461  //   f_url    driver varchar(255)   sql gen varchar(2048)  ts gen varchar(255)

Those two rows are about phone and url — the STRING family, not the file family. #15989 moved the file family to 2048; it did not touch url, whose driver column is still declaredVarcharLength(field) = DEFAULT_STRING_VARCHAR_CHARS = 255 with no declared maxLength. The rows are the measured pre-#16091 reading kept deliberately as the rationale for the entries that replaced them ("VARCHAR(50) and VARCHAR(2048) were widths this file invented"). Rewriting driver varchar(255) to 2048 there would have introduced an error, so those lines are byte-identical in this PR.

The stale text about the file family is a different paragraph — generate.ts:1375-:1381, which held the family out of scope on the ground that it "is in the driver's JSON_COLUMN_TYPES today", untrue since #15989 landed. That paragraph is rewritten, which serves what acceptance item 4 actually asked for: no table left out of date the moment the fix lands. The same stale sentence in generate-field-type-vocabulary.pin.test.ts's header is corrected with it.

Verification

Base 611795e40; readings taken at 3b3ba8a35. Exit codes captured before any pipe.

Run Result
pnpm --filter '@objectstack/cli^...' build VERDICT command-exit 0 (dependency closure)
pnpm --filter @objectstack/cli exec vitest run --project unit VERDICT command-exit 0 — 206 files, 2958 tests passed
pnpm --filter @objectstack/cli typecheck VERDICT command-exit 0 (tsc --noEmit + check:test-typecheck, ledger held)
derived gate family, scripts/pm/dispatch-gates.mjs --commands 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN, every one exit 0
eslint . --no-inline-config --format json exit 0 — 6705 files, 0 errors, 0 warnings (whole tree, not a narrowing)

Both typecheck programs were shown to contain the changed files (tsc -p tsconfig.json --listFiles: 1532 files, commands/generate.ts and the new pin both present), so the green is about them and not around them.

Three runs needed a prerequisite before they measured anything, and each was re-run to a real verdict rather than recorded as a pass: check:dual-build-cjs-loads and check:i18n-coverage both exited 3 (PREREQUISITE NOT MET — built output missing) until the build they prescribe was run, then exit 0; check:type-check-debt exited 124 at a 240s budget and then 3 under --max-old-space-size=4096 (the child tsc OOM'd inside my own cap), and exit 0 at 6144 MB, which the gate names as its own CI-shaped ceiling.

The integration tier is declared to CI: this diff touches no spawn entry, no bin/, no test/helpers/serve-process.ts and no driver or kernel boot path.

Changeset — measured, not assumed

patch on @objectstack/cli. packages/cli is published (not private), files[] is dist README.md CHANGELOG.md, and tsconfig.build.json compiles src. After pnpm --filter @objectstack/cli build, the changed subject is present in the shipped path: packages/cli/dist/commands/generate.js carries the new resolver (4 occurrences) and the sized emission (1). Positive control, a known shipped symbol in the same file: 3 hits. Negative control, a nonsense token: 0 hits. Something published moves, so a changeset is owed; a bug fix in a released package takes patch.

Acceptance notes

  • ⛔ Fences honoured: media-column-move.ts is read as a control and not edited; the os migrate files-to-references retype statement is untouched; no width outside the file family moved; packages/spec is not touched; content/docs/releases/** is not touched. No export was added to a published entry point.
  • noted, not filed: on MySQL an index over a varchar(2048) utf8mb4 column exceeds InnoDB's 3072-byte index key limit, and since [finding] os generate migration emits no declared index at all — a generated table carries none of the object's unique constraints, while driver-sql creates them #16317 both formats emit declared unique indexes. This is not created here — the SQL format and the driver already build the column at 2048 — and it is unmeasured (no MySQL cell in this container), so it is an observation rather than a card. Successor: the driver-sql seat, on the next card that touches the media column width; on a per-dialect pin it would surface in Temporal Conformance (live PG + MySQL).
  • noted, not filed: generate-string-family-width.pin.test.ts excludes FILE_REFERENCE_TYPES from the character-catch-all sweep and gives a per-deployment reason that is now only half true (the driver's arm is per-deployment; the generator's width is not). The exclusion is still correct and the file family now has its own pin, so nothing is uncovered. Successor: whoever next edits that pin's derivation.
  • The new pin lives at packages/cli/src/commands/generate-file-reference-width.pin.test.ts, beside its seven sibling generate-*.pin.test.ts files, rather than under packages/cli/test/** as the claim comment's declared file face spelled it. Same package, unit tier, and check:tier-file-adoption / check:cross-package-test-inputs both pass.

Generated by Claude Code

…mats

`os generate migration` answered one file-family field with two widths
depending on `--format`: the sql format states `VARCHAR(2048)` while the
typescript format spelled a bare `table.string(name)`, knex's varchar(255).
The typescript half now reads its width off the sql vocabulary instead of
riding the reference arm, so the two formats of one command cannot disagree.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
…mats

The new pin measures the two formats against each other AND against the
driver's own `MEDIA_ID_VARCHAR_CHARS`, so two halves that both drifted to
knex's default could not satisfy it. Controls: `f_text`, the non-file family
the generator documents, and the reference arm the family used to ride.

The vocabulary pin's `tsColumn` reader stopped at the column name, so a sized
call read as no column at all; it now captures the argument list.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 3 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-flow.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/deployment/cli.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/objectql/types.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-4.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 60b99552a2f88ee8b06f28f8bec3caa952e60f3cpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 8ebbd1c8bc0c72c521e898690b5b9275238bd966 — the merge of head 78adc8ea86a61980dff05cb29651c2c4b4acf035 into base 60b99552a2f88ee8b06f28f8bec3caa952e60f3c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8ebbd1c8bc0c72c521e898690b5b9275238bd966 && git checkout 8ebbd1c8bc0c72c521e898690b5b9275238bd966
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 60b99552a2f88ee8b06f28f8bec3caa952e60f3c 78adc8ea86a61980dff05cb29651c2c4b4acf035 && git checkout -B drift-repro 60b99552a2f88ee8b06f28f8bec3caa952e60f3c && git merge --no-ff 78adc8ea86a61980dff05cb29651c2c4b4acf035

node scripts/docs-audit/affected-docs.mjs --json 60b99552a2f88ee8b06f28f8bec3caa952e60f3c

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 60b99552a2f88ee8b06f28f8bec3caa952e60f3c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Sep 13, 2026
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Head reviewed: 3b3ba8a35c70b390f36b4e9fa645b4fdb4f57ffe (merge-base 611795e40558e722334bebd1239a0cff7cd55d42). Readings taken 2026-09-13T11:55:18Z – 11:57:10Z against a freshly fetched tree; origin/main = 32a32143036f34cab05772af3982384acd2a522f.

Verdict: PASS.

Independence: SELF-REVIEW. I dispatched this card's dev, so this review shares my dispatch order's assumptions — including, as it turned out, a wrong one. That is disclosed here, not smoothed over: the item below that this PR falsified is mine, and I would not have caught it if the dev had simply obeyed the order.

⛔ My acceptance item 4 was wrong, and obeying it would have written a false statement into the file

My order named generate.ts:1461 as a stale self-describing table to bring in line with the family's 2048. It is not the file family's table at all. On the base:

$ git show 611795e405:packages/cli/src/commands/generate.ts | sed -n '1460,1461p'
  //   f_phone  driver varchar(255)   sql gen varchar(50)    ts gen varchar(255)
  //   f_url    driver varchar(255)   sql gen varchar(2048)  ts gen varchar(255)

That is the STRING family's measured three-producer row — a different fork, a different card. url's driver column is sized by declaredVarcharLength (packages/drivers/driver-sql/src/sql-driver.ts:16199, dispatched at :16981), which returns the declaration verbatim and 255 when a field declares none. The media width is a different branch of the same switch: MEDIA_ID_VARCHAR_CHARS = 2048 at :284, reached at :17029 and emitted at :17703. Rewriting the f_url row to 2048 would have stated a width the driver does not emit for url — an error introduced by the repair.

The dev measured the item, refused it, and said so in ## Premise falsified — acceptance item 4 pointed at the wrong table. That is the right outcome and I am recording it as my error, not the dev's deviation. It is the second dispatch-order item of this round to be a default I applied without checking the criterion (the first: requiring a changeset on #17965, where nothing published moves). Two in one round is a pattern, not a slip.

The paragraph that really was stale about this family is :1375:1381 — "is in the driver's JSON_COLUMN_TYPES today", untrue since #15989 — and it is the one rewritten.

What the diff actually does

$ git diff --numstat 611795e405 3b3ba8a35c70
16	0	.changeset/17883-generate-migration-file-family-width.md
25	9	packages/cli/src/commands/generate-field-type-vocabulary.pin.test.ts
195	0	packages/cli/src/commands/generate-file-reference-width.pin.test.ts
85	10	packages/cli/src/commands/generate.ts

The production change inside those 85 added lines is small: the five file-family cases leave the REFERENCE_VALUE_TYPES arm and take one of their own, whose width is read from this file's own SQL vocabulary rather than retyped beside it —

case 'image': case 'file': case 'avatar': case 'video': case 'audio':
  colMethod = `table.string('${fieldName}', ${fileReferenceVarcharChars(fType)})`;

— and fileReferenceVarcharChars parses FIELD_TYPE_SQL_MAP's own VARCHAR(2048) entry, throwing rather than falling back when it cannot, on the stated ground that "the only wrong answer is a plausible width emitted anyway". One source ⇒ the two formats agreeing is true by construction, which is the property the card asked for and is stronger than the two literals it could have been.

The five FIELD_TYPE_SQL_MAP entries did not move, and the entire --format sql half is untouched — the direction is the typescript half joining the already-shipped 2048, never the two halves meeting at some third value. ADR-0104's ruling is quoted at the change site, so the next reader does not have to re-derive it.

Docs drift — checked by hand, because the checker says it cannot see this class

The advisory listed content/docs/protocol/objectql/types.mdx, and that page is the checker's own named blind spot (#11430: it states column mapping by the ObjectQL type names it maps from, sharing no token with the emitter, and it was the page a previous emitter-only diff falsified in four places). Since this PR changes a width, I re-read it rather than trusting the row:

  • The Type Conversion Matrix (:1205:1221) has no row for file / image / avatar / video / audio — the families listed at VARCHAR(255) are select/radio, lookup/master_detail/tree, and autonumber, and footnote † at :1239:1243 names exactly those three as the deliberate non-followers. The file family is absent from both.
  • The ### file section (:1138:1181) states the stored value as "an opaque sys_file id string" and states no column width.
  • Controls on that page: live control file18 hits (the grep can find things); fabricated control → 0 hits (it can return zero).

⇒ This diff falsifies nothing on that page. The other three listed pages restate no width at all (data-flow.mdx and deployment/cli.mdx return zero for 2048|varchar\(|VARCHAR\(; lifecycle.mdx:147's only 2048 is Memory: 2048 MB in a sample log).

⛔ Release-owned guardrail held, and it was measured, not assumed:

$ git diff --name-only 611795e405 3b3ba8a35c70 -- 'content/docs/'
(zero rows)

Changeset — owed, present, and at the right level

The dev did not take my order's default here either; it measured the criterion. packages/cli's files[] is ["dist","README.md","CHANGELOG.md"] and generate.ts is under src/ ⇒ the built dist/commands/generate.js carries the change ⇒ something published moves ⇒ a changeset is owed, and skip-changeset would have been wrong. patch is the right level: the command surface gains nothing and loses nothing; what changes is emitted DDL for tables generated from now on, and the changeset states plainly that no regeneration is required of anyone because syncSchema/initObjects never alter an existing column's type.

Verification I am relying on, and what it does not cover

The dev's evidence is unusually load-bearing and I checked its shape rather than only its conclusions:

  • Acceptance runs both directions off one probe object, five of five family members, before and after, in both formats — and the control is the one this file itself documents (f_text, byte-identical), not one invented for the occasion. The whole before/after delta is exactly those five lines, with no hunk at all on the SQL side.
  • Ablation with a predicted direction: the pre-fix generate.ts put back under the new pin turns it red 5 of 8, one failure per family member, the three control and anti-vacuity tests staying green. Proven on disk (injected occurrences 4 → 0, blob 8d8c34a01fccbeff) and restored by state (blob back to 8d8c34a0, git diff HEAD empty), not by an editor's exit code.
  • 62 derived gates, 62 run, 0 NOT-MEASURED, 0 UNRUN. Three of them did not measure anything on first contact and were each driven to a real verdict instead of being recorded as a pass: two returned exit 3 (PREREQUISITE NOT MET, no dist) and passed after the prescribed build; check:type-check-debt hit exit 124 at a 240s budget, then exit 3 because the child tsc OOM'd inside the dev's own cap, then exit 0 at the 6144 MB ceiling the gate's own output names as CI-shaped. ⭐ A gate that reports "prerequisite not met" is not a green gate, and it was not filed as one.

Not covered, stated rather than papered over: no dialect was exercised — there is no live PostgreSQL or MySQL cell in that container, so nothing here is a DDL measurement. The dialect answer comes from CI's Temporal Conformance (live PG + MySQL) and the Dogfood tier, which is why this review does not conclude until ③ is green on this head.

Acceptance notes I am accepting as filed, with judgement

  • MySQL index-key ceiling — an index over a varchar(2048) utf8mb4 column exceeds InnoDB's 3072-byte key limit, and both formats emit declared unique indexes since [finding] os generate migration emits no declared index at all — a generated table carries none of the object's unique constraints, while driver-sql creates them #16317. This PR does not create that: the sql format and driver-sql already build the column at 2048, so the exposure predates it and is unchanged by it. Correctly noted rather than filed, with a named successor (the driver-sql seat, on the next card touching the media column width) — a successor who can actually be named is the whole test for whether an acceptance note is allowed to stand in for a card.
  • Pin file locationgenerate-file-reference-width.pin.test.ts sits at packages/cli/src/commands/, not packages/cli/test/ as my claim comment's declared file face spelled it. Declared by the dev as a deviation. On the measurement it is the correct location and my declared face was the wrong one: all eight sibling generate-*.pin.test.ts files live there, same package, same unit tier, and check:tier-file-adoption and check:cross-package-test-inputs both pass. Third small item of this round on my side of the line.
  • generate-string-family-width.pin.test.ts's exclusion reason is now half true (the driver's arm is per-deployment; the generator's width is not). The exclusion itself stays correct and the family now has its own pin, so nothing is left uncovered. Noted with a successor.

What this review does not do

⛔ No approving review, ⛔ no merge — this is a managed-surface PR and neither is mine to give. Next: ③ green on 3b3ba8a35c70b390f36b4e9fa645b4fdb4f57ffe, then the ready flip and the arm, then the landing record on #17883.

Reviewed-by: os-sales — domain:cli execution PM seat #6024
Independence: SELF-REVIEW (dispatching seat; the one falsified premise above was mine)


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review September 13, 2026 12:21
@os-sales
os-sales enabled auto-merge September 13, 2026 12:21
@os-sales
os-sales added this pull request to the merge queue Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

    @objectstack/cli:test:  FAIL   integration  test/format-zod-union.test.ts > [#5341] `os validate` delivers a union branch prescription > the specimen fails on exactly one issue, and that issue is the 
      ↳ 失败原因: @objectstack/cli:test: AssertionError: expected [ …(2) ] to have a length of 1 but got 2
    @objectstack/cli:test:  FAIL   integration  test/format-zod-union.test.ts > [#5341] `os validate` delivers a union branch prescription > leaves the `--json` payload exactly as it was — full, and neste
      ↳ 失败原因: @objectstack/cli:test: AssertionError: expected [ …(2) ] to have a length of 1 but got 2
    

↳ 失败原因 是判读的关键:超时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,按失败测试文件聚合):

  • test/format-zod-union.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

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

分诊清单:

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

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

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

The queue build failed, and the failure is not this PR's

Read at 2026-09-13T12:38:49Z – 12:40:20Z. Standing down rather than re-queueing, with the reason measured rather than asserted.

What failed: Test Core (3/6) in queue build 34756944488

FAIL  integration  test/format-zod-union.test.ts
  > [#5341] `os validate` delivers a union branch prescription
  > the specimen fails on exactly one issue, and that issue is the union
  AssertionError: expected [ …(2) ] to have a length of 1 but got 2

It is an assertion on product behaviour, not on a duration or a timestamp, so by the triage comment's own rule it is a real behaviour change and ⛔ not something a re-queue may be used to wish away.

Why it is not this PR's. The assertion runs ObjectStackDefinitionSchema.safeParse(SORT_ALIAS_STACK) — a @objectstack/spec schema — against a specimen whose whole content is a list view's sort clause:

list: { , sort: [{ field: 'name', direction: 'desc' }] }

This PR touches zero packages/spec files, and nothing on the os validate path:

$ git diff --name-only 611795e405 3b3ba8a35c70
.changeset/17883-generate-migration-file-family-width.md
packages/cli/src/commands/generate-field-type-vocabulary.pin.test.ts
packages/cli/src/commands/generate-file-reference-width.pin.test.ts
packages/cli/src/commands/generate.ts

What the queue build actually contains. The queue branch is gh-readonly-queue/main/pr-18014-1e20f816ea81f2ba02e9ab2dd6a60bd183150e0a, head bd9f81fc622260af7181181d286aeb48a62763ef:

$ git log --oneline origin/main..bd9f81fc6222
bd9f81fc6 fix(cli): one column width for the file family in both os generate migration formats (#18014)
1e20f816e feat(spec)!: retire the bare string `sort` clause on the list-view doors — the PRODUCER half of the sort seam (#17914)
f52bfa90f docs(adr-0133): …
a39e385ee feat(pm): …

#17914 is in this build, ahead of this PR, and is a breaking change to the very clause the failing specimen is made of — the list-view sort door. Of everything in the build that is not already on main, it is the only change that touches the failing subject. This is the triage checklist's case 3: a semantic collision with a PR in the same batch, ⛔ not a regression in the PR that happened to be behind it.

Controls, so this is not one reading standing alone:

What I have not measured: I did not execute format-zod-union.test.ts on either side. The causal claim above rests on the file face and on what is and is not in the build, not on a reproduction.

What I am doing about it. Nothing to this PR. There is no fix to port: the seam is #17914's to carry — either its own diff updates the CLI specimen to the retired clause's replacement, or the domain:spec seat decides the CLI test's shape separately. ⛔ It is not mine to fix here: packages/spec belongs to the domain:spec seat whichever lane needs it, #17914 is another seat's PR on another account, and widening this PR to chase it would be exactly the reflex that turns one queue failure into two red PRs.

⛔ No re-queue yet — a re-queue now rebuilds the same collision and burns a full-queue cycle for everything behind it. The re-queue becomes correct once #17914 has landed or left the queue, and it is the one re-run this failure is entitled to.

This PR stays watched until it is green and merged.


Generated by Claude Code

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 13, 2026
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

⛔ Correction — my previous comment claimed more than the evidence supported

In 5653324611 I wrote "the failure is not this PR's" as a settled conclusion. One control I did not take falsifies the strong form of that claim. Read at 2026-09-13T12:53:01Z – 13:06:39Z.

The control I missed. #17914 had its own merge-queue entry, built and tested alone, and the very shard that failed on this PR's build passed there:

run 34756926770 | event merge_group | branch gh-readonly-queue/main/pr-17914-f52bfa90f…
  Test Core (3/6)   SUCCESS   12:23:03Z → 12:37:55Z

I compared only against main at 2f1a6f696, which predates #17914. #17914's own queue build was the one reading that could have falsified me, and I did not look at it before concluding. That is the error, and it is mine.

A second control, since taken: main now carries #17914 (origin/main = 1e20f816ea81f2ba02e9ab2dd6a60bd183150e0a, merged 12:51:14Z) and its post-merge push CI (run 34758226868) came back Test Core: 7 rows, zero failures at 13:06:39Z.

What is still established

The mechanism is read from the diff, not inferred from timing: 1e20f816e retires the bare-string sort clause on the list-view doors and makes the replacement { field, order }[] with order required on the entry. The failing specimen is sort: [{ field: 'name', direction: 'desc' }]direction is not the accepted key and order is absent — which is a shape that yields two issues where the test pins one. And this PR touches zero packages/spec files.

What is NOT established, and I am not going to assert it

Why the same shard is green on #17914 alone and on main, but red with this PR added. My hypothesis is tier/package selection — the CLI integration tier being selected only when packages/cli is affected, so #17914 never ran the test it broke while this PR did. ⛔ I have not measured that, and offering it as the explanation would repeat the mistake this comment is correcting. The honest statement is that the mechanism above is real and the attribution is open.

What that changes about the next step — nothing, and here is why

Either way the same action is correct and it is also the decisive experiment: this PR's base is now stale (it was validated on 32a32143036f, which predates #17914), so I am updating the branch to current main rather than re-queueing a stale head. Its own CI then runs against a tree that contains #17914, on a PR whose diff guarantees the CLI tier is selected.

  • If format-zod-union.test.ts fails there, the seam is real and reaches this PR; it gets root-caused from that run, ⛔ not from this comment's hypothesis.
  • If it passes, the queue failure was something else and that is diagnosed from scratch.

⛔ Still no re-queue on the stale head, ⛔ no re-run used as a substitute for an answer, ⛔ nothing skipped, disabled or quarantined, and ⛔ no empty commit. This PR stays watched until it is green and merged.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Resolved by measurement: main is broken, this PR does not participate

This closes the question 5653470881 left open. A three-leg control was run in dedicated worktrees, exit codes captured before any pipe:

tree head vitest run test/format-zod-union.test.ts
PR head (main + this PR's 4 files) c1e5ce24c697 exit 1 — 2 failed / 11 passed
origin/main alone, this diff absent 1e20f816ea81 exit 1 — 2 failed / 11 passed, byte-identical failures
before #17914 f52bfa90f471 exit 0 — 13 passed

The third leg is what makes this a control rather than a blind reading: it can go green, and does. ⇒ Red on both sides of this PR's diff. origin/main is already broken; this PR's four files do not participate — they are packages/cli/src/commands/generate*.ts plus a changeset, while the failing test exercises packages/cli/src/utils/format.ts and @objectstack/spec's view schema.

Two corrections to my own record

My earlier correction over-corrected. In 5653470881 I wrote that if the test failed on the rebased head then "the seam is real and reaches this PR". It fails there — and it also fails on main without this PR, which is the reading that decides. The stand-down in 5653324611 reached the right conclusion; what was wrong with it was the basis, not the verdict. I have now been wrong in both directions on this attribution, once by asserting past my evidence and once by retreating past it. Both are recorded rather than quietly reconciled.

My stated mechanism was wrong on the second issue. I predicted "order is missing and required". Measured against the built spec on each side:

AFTER  #17914 (1e20f816e) — 2 issues:
  [0] code=invalid_value      path=views.0.list.sort.0.order
      "Invalid option: expected one of \"asc\"|\"desc\""
  [1] code=unrecognized_keys  path=views.0.list.sort.0   keys=["direction"]
BEFORE #17914 (f52bfa90f) — 1 issue:
  [0] code=invalid_union      path=views.0.list.sort     "Invalid input"

[0] is invalid_value from z.enum(['asc','desc'])not an invalid_type/required report. A patch author matching on code or message would have matched the wrong thing.

The break is deeper than the count, and one green is now vacuous

The length assertion masks a second, independent break. The next line of the block is expect(issues[0]!.code).toBe('invalid_union'), and after #17914 views[].list.sort is no longer a union at all — the bare-string arm was the union's other member. The subject of that describe block is a rejection reaching the terminal from behind a z.union, and that shape no longer exists at this door. It is also why the block's third test still passes: it asserts the prescription reaches the terminal, and it does — by the ordinary non-union path the file was never about. ⛔ So the minimal patch is not 12; it is re-pointing the specimen at a door that is still a z.union with a strict branch carrying a curated prescription, or re-authoring the block. That is an authoring decision, not a mechanical one, and it was deliberately not made here.

File face of the repair: packages/cli/test/format-zod-union.test.ts — test-only. ⛔ Not packages/spec/**: #17914 is the ruled end-state (#17053, ADR-0049) and editing it would reverse a landed ruling.

Sequencing, and why nothing changes on this PR

The repair does not belong in this PR. This PR's card is the file-family column width; folding an unrelated consumer repair into it would widen the PR and would make a red that main already owns read as this PR's. The consequence that matters for sequencing: this PR cannot reach green through the merge queue until main is repaired, because the queue rebuilds it onto current main. ⛔ No re-queue until then — it would fail identically and burn a full-queue cycle for everything behind it.

⚠️ A second finding, stated as open because it is not closed

CI reports Test Core (3/6) success on 1e20f816e — the same commit where this test measures red. Partial evidence, no conclusion drawn:

  • Those main jobs did real work: Run this shard's tests at 834s and 817s, not a cache hit.
  • But Test Core (5/6) on that same commit ran its tests in 2 seconds, which is not a test run.
  • turbo.json's explicit inputs for @objectstack/cli#test pin several cross-package files by name but not packages/spec/src/ui/view.zod.ts, and the CI slice runs with --only.

That points at a scheduling or cache artifact rather than a passing test, but ⛔ it is not closed: the Actions log download 403s from these containers (redirect to blob storage, denied by the egress proxy), and the local partition does not reproduce CI's slicing. The experiment that settles it: read step 9 (Compute this shard's package set, which prints the shard's item list) of jobs 103727415113 (5/6) and 103727415143 (6/6), from a seat that can reach Actions logs. If it holds, a required gate has been reporting green over a test it never ran — which is worth more than this one repair.

This PR stays watched. ⛔ Nothing skipped, disabled or quarantined; ⛔ no empty commit; ⛔ no re-queue while main is red.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Re-anchored to the current head 78adc8ea86a61980dff05cb29651c2c4b4acf035, base 60b99552a2f88ee8b06f28f8bec3caa952e60f3c. Read 2026-09-13T15:57:48Z.

Verdict: PASS, carried forward unchanged from 5653116364 — not re-derived, because the reviewed content provably did not move.

The head advanced twice since that review (3b3ba8a35c70c1e5ce24c69778adc8ea86a6), both times by a base merge and neither time by a push of mine. A review of record naming a superseded sha would be stale, so rather than assert "nothing changed", here are the blob hashes of all four subject files at the reviewed head and at the current one:

file 3b3ba8a35c70 78adc8ea86a6
.changeset/17883-…-file-family-width.md f8ffcb8d95 f8ffcb8d95 identical
packages/cli/src/commands/generate.ts 8d8c34a0f7 8d8c34a0f7 identical
…/generate-file-reference-width.pin.test.ts f94e979bfe f94e979bfe identical
…/generate-field-type-vocabulary.pin.test.ts 9da764822b 9da764822b identical

Control that can actually fail, so four "identical" rows are not a vacuous comparison — a file the base merges did change must differ:

| packages/cli/test/format-zod-union.test.ts | 2a57835089 | 3a55542ea2 | differs — control fires |

That control is the #18043 repair (fb2f01dde on main), which is also why this head is green. The PR's own diff face is unchanged at 4 files, +321/−19.

③ on this head

34 checks, enumerated (total_count 34 = 34 returned, so the reading is not partial): 31 success, 3 skipped, zero failures. Test Core (3/6) — the shard that was red on the two previous heads — is completed/success, as are all six shards and the aggregate.

⛔ Note the acceptance standard this does not rest on: earlier in this card's history a green Test Core on main coexisted with the same test failing when run directly, and I carried a shard-composition explanation for it that has since been withdrawn as wrong (see 5653663099 here and 5653935720 on #18032). What licenses "green" this time is not the badge alone: the repair is on main as fb2f01dde, verified by rev-list --parents = 2 fields and merge-base --is-ancestor with the PR head as a negative control that fires.

Reviewed-by: os-sales — domain:cli execution PM seat #6024
Independence: SELF-REVIEW (dispatching seat; carried forward from the original, which disclosed the same)


Generated by Claude Code

@os-sales
os-sales added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit b06b2db Sep 13, 2026
35 checks passed
@os-sales
os-sales deleted the claude/issue-17883-generate-migration-file-column-width branch September 13, 2026 16:20
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Landing record — ledger 50

This belongs on card #17883. It is here because #17883 has become unreachable. Measured 2026-09-13T16:22:06Z – 16:24:33Z, with controls:

probe #17883 control
GET /issues/17883 404 GET /issues/18032 → 200
PATCH /issues/17883 404
GET /issues/17883/comments 200
GET /issues/17883/timeline 200
https://github.com/…/issues/17883 (HTML, following redirects) 404, no redirect
present in GET /issues?state=all (300 rows scanned) absent #18032 present

The same shape holds for #17853 (ledger 49's card). Both are cards closed by a Fixes line on a merged PR. POST /issues/17883/comments returns HTTP 422 — Could not resolve to a node with the global id of 'I_kwDOQ8CKns8AAAABQ_CXmQ', three attempts, ⛔ not transient; the MCP path 404s identically, so this is a GitHub-side condition on the issue node, ⛔ not this seat's channel.

The residue disposal this record would normally announce cannot be performed and is now moot — there is no card left to carry pm:dispatched or an assignee. That is a fact about the board, ⛔ not a step I skipped. The last state I read on it (2026-09-13T13:47Z) was open, labels bug / pm:dispatched / domain:cli / priority:p3, assignee os-sales.

⚠️ Reported to the maintainer as an event, not filed as a card — a card disappearing mid-flight is theirs to rule on, and this seat is under an instruction to stop after this work.


#17883 → PR #18014b06b2db5c46cd3e114161253c390d2d252ab7b91

Enqueued 2026-09-13T15:58:27Z (timeline added_to_merge_queue — the only authority on enqueue), merged 2026-09-13T16:20:44Z. Readings taken 2026-09-13T16:21:08Z and 16:21:23Z against a freshly fetched origin/main = b06b2db5c46c.

Reading 1 — shape and ancestry

$ git rev-list --parents -n 1 b06b2db5c46c | awk '{print NF}'
2
$ git merge-base --is-ancestor b06b2db5c46c origin/main
exit 0

Two fields ⇒ one parent ⇒ squash, which is what the queue does whatever merge_method was armed to.

Negative control that can actually fail — after a squash the PR head must NOT be an ancestor of main, and a rebase or a true merge would make it one:

$ git merge-base --is-ancestor 78adc8ea86a61980dff05cb29651c2c4b4acf035 origin/main
NOT ancestor -> control fires

Reading 2 — content, with three controls

$ git grep -c 'fileReferenceVarcharChars' origin/main -- packages/cli
origin/main:packages/cli/src/commands/generate.ts:4

$ git grep -n "table.string('\${fieldName}', \${fileReferenceVarcharChars" origin/main -- packages/cli/src/commands/generate.ts
origin/main:packages/cli/src/commands/generate.ts:2796:
  colMethod = `table.string('${fieldName}', ${fileReferenceVarcharChars(fType)})`;
  • Live control (a pre-existing symbol in the same file, must be non-zero — proves the grep can find things): FIELD_TYPE_SQL_MAP10.
  • Fabricated control (must be 0 — proves it can return zero): fileReferenceVarcharCharsXYZZY0 hits.
  • Before/after control that can actually fail — the same subject on the squash's own parent, where it must be absent: git grep -c 'fileReferenceVarcharChars' b06b2db5c46c^ -- packages/cli0 hits.

What landed

4 files, +321/−19. The production change inside them is small: the five FILE_REFERENCE_TYPES cases leave the reference arm and take one of their own, whose width is read from FIELD_TYPE_SQL_MAP's own VARCHAR(2048) entry rather than retyped beside it, and which throws rather than falling back when it cannot parse one. ⇒ The two os generate migration formats agreeing is now true by construction, not by a reviewer noticing. ⛔ The five SQL-map entries did not move and the --format sql half is untouched: the typescript half joined the already-shipped 2048, ⛔ the two halves did not meet in the middle.

Changeset: patch on @objectstack/cli, measuredfiles[] is ["dist","README.md","CHANGELOG.md"] and generate.ts is under src/, so the built dist/commands/generate.js carries it ⇒ something published moves.

Contract review of record: 5653116364, re-anchored to the landed head at 5654363305 with the four subject blobs proven identical across the two base merges and a control that fires.

The detour, recorded because it is the part worth remembering

This PR was green, armed and queued at 12:22Z, and was ejected at 12:51:14Z — not by anything in its own diff. #17914 landed the retirement of the bare-string list-view sort clause, which left packages/cli/test/format-zod-union.test.ts asserting on a union that no longer existed; main itself was red. It was repaired by another seat in #18043 (fb2f01dde), and this PR went through on the first re-queue after that.

My own record through it, kept here rather than tidied away: I first asserted the failure was "not this PR's" without taking the one control that could have falsified it (#17914's own queue build, which was green on the same shard), then over-corrected past the evidence in the other direction when the rebased head also went red. The three-tree measurement settled it — origin/main alone was red byte-identically, and the pre-#17914 tree was green. Separately I carried a shard-composition correlation as a candidate explanation; it is withdrawn as wrong. And I claimed the repair card #18032 on an absence reading that had already expired — the other seat's PR existed 64 seconds after my sweep and 12 seconds before my claim posted — so I stopped my dev and stood down.

⛔ Through all of it: no test was skipped, disabled or quarantined; no empty commit; no close-and-reopen; and the one re-queue this PR used was spent only after the cause was on main and verified.

Disposal

⛔ Not performed, and not skippable-by-choice: see the header — the card is unreachable, so there is nothing left to clear. Seven-for-seven on residue surviving auto-close stands as today's count; this one is unmeasurable rather than clean.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants