Skip to content

fix(cli): remove the ghost field types from generate.ts's three vocabularies - #14675

Merged
os-trump merged 2 commits into
mainfrom
claude/issue-13871-generate-codegen-ghost-field-types
Sep 2, 2026
Merged

fix(cli): remove the ghost field types from generate.ts's three vocabularies#14675
os-trump merged 2 commits into
mainfrom
claude/issue-13871-generate-codegen-ghost-field-types

Conversation

@os-trump

@os-trump os-trump commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes #13871

packages/cli/src/commands/generate.ts carried three hand-authored field-type
vocabularies and none had ever been checked against the FieldType enum they
claim to describe. This removes every label in them that names a type the
platform does not have, and adds a pin so the class cannot reopen.

The triage gated this card on two readings before any edit. Both were posted on
the issue before the first file was touched — #13871 (comment) — and are summarised here.

Reading ① — where the labels came from

History was read on a shallow clone: origin/main is grafted at 67192ce0d4
(2026-09-01), so git log --diff-filter=A names that graft commit as the
"adding" commit for every file — an artifact. The real introducing commits are
reachable through the deeper-fetched stale origin/claude/* branches, and that
is the boundary this reading is declared against.

label sites introduced by date
'slug' 'ip_address' 'encrypted' 'integer' 'uuid' 'geo_point' FIELD_TYPE_MAP 77241bdec9 "feat: Phase 9 … generate types CLI …" 2026-02-12 06:07
'slug' 'ip_address' 'encrypted' 'integer' 'uuid' 'geo_point' FIELD_TYPE_SQL_MAP db015d2827 "feat(cli): add generate client, generate migration, codemod, and doctor deprecation scanner commands" 2026-02-12 13:52
'slug' 'ip_address' 'encrypted' 'integer' 'uuid' the switch (fType) db015d2827 (same 399-insertion block) 2026-02-12 13:52

Pinned per label AND per site (git log -S"case 'slug'", git log -S"slug: 'VARCHAR(255)'", …), not inferred from one whole-file match.

The reason is not a per-label decision. Neither commit message carries a
rationale. The migration codegen simply mirrored the vocabulary the types codegen
had invented six hours earlier. And the decisive negative reading: git log -S
over the whole 127-commit reachable history of
packages/spec/src/data/field.zod.ts returns zero commits for every one of
the six tokens. They are not leftovers of retired spec types — they never
existed on the other side.

Reading ② — whether the input surface is FieldType-gated

Traced: Generate.runrunMigrationGenerationloadConfig
generateMigrationTs(config)String(fieldDef.type || 'text') → the switch.
loadConfig itself performs no validation — it is bundleRequire plus a
named-export merge. The gate, when there is one, lives in the authored config
module, as config.ts's own doc comment states: "Every define* helper in
@objectstack/spec is a Schema.parse(config) … the rejection happens while the
config MODULE is being evaluated, inside bundleRequire"
.

Measured on both doors, driven, not inferred.

Probe A — the gate (defineStack from packages/spec/src/index.ts via tsx):

[strict defineStack] type='text':       ACCEPTED
[strict defineStack] type='number':     ACCEPTED
[strict defineStack] type='slug':       REJECTED : Invalid field type 'slug'. Valid types: text, textarea, email, url, phone, password, secret, ...
[strict defineStack] type='ip_address': REJECTED : Invalid field type 'ip_address'. Did you mean 'address'?
[strict defineStack] type='encrypted':  REJECTED : Invalid field type 'encrypted'. Valid types: ...
[strict defineStack] type='integer':    REJECTED : Invalid field type 'integer'. Did you mean 'number'?
[strict:false defineStack] type='slug': ACCEPTED : {"type":"slug","label":"Probe Field"}

Probe B — the real codegen entry (the actual oclif command run as
migration --dry-run over a plain-object config export — the ungated door):

      table.string('f_slug').nullable();          # ghost arm fires
      table.string('f_ip_address').nullable();    # ghost arm fires
      table.text('f_encrypted').nullable();       # ghost arm fires
      table.integer('f_integer').nullable();      # ghost arm fires
      table.text('f_address').nullable();         # REAL FieldType member, no arm: default
      table.text('f_secret').nullable();          # REAL FieldType member, no arm: default

The reading. The input surface is not structurally FieldType-gated by the
CLI, but it is not an alternative declared vocabulary either. Two doors:

  • Door 1 — every supported authoring path. os init scaffolds
    export default defineStack({ … }) and every config in this repo goes through
    a define* helper, which is strict by default. All the labels are refused
    before the generator runs a line. The arms are unreachable.
  • Door 2 — an unvalidated escape hatch (plain-object export, or
    defineStack(x, { strict: false })). Nothing parses; the ghost arms fire.

Door 2 does not make the labels correct — this is the branch the triage flagged
as the critical one. A type outside FieldType is refused by every other door
(os validate, os build, os serve all keep hearing the rejection; config.ts
says so explicitly), so emitting a bespoke column for it does not serve it, it
advertises an acceptance surface no runtime can honour. The third disposition
— record the semantics and change no code — therefore does not hold
, and the
ghost finding is not itself wrong. Recorded as a reading rather than a fork: the
three-way split is a per-member disposition and every member lands inside it.

Per-member disposition

Every one is a dead case ⇒ delete. Disposition ② (fix the spelling, do not
delete) applies to zero members on the evidence, and the triage's own example
is why: it asked whether 'integer' should become number — and number
already has its own entry in both maps and its own table.decimal arm, so there
was nothing to correct it to.

label disposition evidence
'slug' delete No FieldType counterpart at all — the platform has no slug type, and never had one (zero hits in field.zod.ts history). Its arm keeps text/email/phone/url/select/password/color.
'ip_address' delete, not re-spelled to address The spec's own Did you mean 'address'? is a Levenshtein hint, not a semantic identity: address is a structured postal address stored as JSON on the row, not an IP. Re-spelling would have handed address a VARCHAR(45) column.
'encrypted' delete The concept exists today as secret — but secret landed 2026-05-31 (6514f8df1e, "secret field type with encrypt-on-write to sys_secret"), three and a half months after encrypted was invented here, so this was never a misspelling of it. Re-spelling would also have taken a decision this card does not own: secret has no entry in these tables at all, and what TS/SQL type it deserves belongs to the coverage card.
'integer' delete number already occupies both maps and its own arm, so there is no spelling to fix. rating — a real member — keeps the table.integer arm.
'uuid' delete Never a FieldType member either. The two type: 'uuid' hits elsewhere in the repo are a database column type in DriverIntrospection, not a field type. lookup/master_detail keep the table.uuid arm.
'geo_point' delete Maps only. Same reasoning as encrypted: the real GPS member is location, which has no entry in either map, so re-spelling would be deciding location's column type rather than removing a ghost.

What changed, and what deliberately did not

Changed — packages/cli/src/commands/generate.ts, 3 files, +224/-16 overall:

  • the switch (fType) in generateMigrationTs (the card's surface): five ghost
    case labels removed; every arm keeps its real members.
  • FIELD_TYPE_MAP and FIELD_TYPE_SQL_MAP: six ghost keys each, plus a doc
    block on each stating the invariant and naming the pin.
  • generate-field-type-vocabulary.pin.test.ts: the guard.
  • a patch changeset for @objectstack/cli.

Declared scope extension. The card names four labels in the switch. Measuring
mechanically rather than checking only those four found a fifth in the same
switch ('uuid') and six in each of the two sibling tables. Those were taken
in-place under the bounded exemption — ① same defect class (one hand-authored
field-type vocabulary in one file, propagated across its three tables), ②
mechanical with the correct shape pinned by FieldType itself, ③ no other claim
holds this file and no open PR touches it, ④ same gate family, no new
verification surface — and because one guard that closes the whole class beats
three-quarters of a guard. The extension was declared on the issue before the
first edit
, in the same comment as the readings. If a reviewer wants the two
sibling tables trimmed back out, they are a contiguous slice of one commit.

Deliberately NOT changed:

Behaviour. Unchanged for every config the platform accepts — those configs
cannot carry these types. For a config that bypasses validation, a field typed
with one of the six now falls to the same default any unknown type gets:
table.text / TEXT / unknown.

Red-first

The pin was written and run against the unmodified generate.ts first. It
failed on all three vocabularies, naming every ghost, while the two non-vacuity
control assertions passed — so the extractors demonstrably read something:

Test Files  1 failed (1)
     Tests  3 failed | 2 passed (5)

AssertionError: FIELD_TYPE_MAP keys on types that are not FieldType members:
  + [ "integer", "slug", "uuid", "ip_address", "geo_point", "encrypted" ]
AssertionError: FIELD_TYPE_SQL_MAP keys on types that are not FieldType members:
  + [ "integer", "slug", "uuid", "ip_address", "geo_point", "encrypted" ]
AssertionError: the field-type switch cases on types that are not FieldType members:
  + [ "slug", "ip_address", "encrypted", "integer", "uuid" ]

Ablation — the pin can actually fail

Run after the implementation was committed, so the restore leg has a real
reference. No rebuild is involved on either leg, and that is a property of the
subject, not an omission
: the pin reads generate.ts with fs.readFileSync
from the checkout, so a source mutation reaches it directly; the only dist it
touches is spec's, for FieldType, which is not mutated. A trap restore EXIT INT TERM with an absolute path was armed before the mutation.

HEAD=c4216584fc
HEAD_BLOB=3eec38566e48bf08358e87f58d80c13531c12d9e
MUTATION_ON_DISK injected_text_hits=1 erased_text_hits=0
MUTATED_BLOB=a4557af9e2214525c74e8f7a533f1b82b437bd33
ABLATION_PIN_EXIT=1
  Test Files  1 failed (1) | Tests  1 failed | 4 passed (5)
  AssertionError: the field-type switch cases on types that are not FieldType members: expected [ 'integer' ] to deeply equal []
RESTORED_BLOB=3eec38566e48bf08358e87f58d80c13531c12d9e
GIT_DIFF_HEAD_AFTER_RESTORE=[]
RESTORE_PROVED byte-identical to HEAD blob, git diff HEAD empty
PIN_AFTER_RESTORE_EXIT=0
  Test Files  1 passed (1) | Tests  5 passed (5)

The mutation re-added exactly one ghost label (case 'integer':), was proved on
disk by counting the injected text and the erased text separately (not by the
editor's exit code, and not by a bare git diff --stat), and the pin named
exactly that one label. Restore is proved by observing state — the blob hash
matches the HEAD blob and git diff HEAD is empty — never by a git checkout
exit code, and the restore names HEAD explicitly rather than restoring from a
possibly-polluted index.

Verification

All at the final commit 3d150eb078 — the branch was merged up to origin/main
2aa8456cf2 first, because dispatch-gates.mjs refused the earlier tree as
STALE (10 commits behind, 7 of the files the families derive from changed, the
deriving script itself among them). Every exit code below was captured before
any pipe.

run result
pnpm --workspace-concurrency=2 --filter '@objectstack/cli^...' build exit 0
pnpm --filter @objectstack/cli typecheck exit 0 — and the new test file is really inside the program (tsc --listFiles names it, so this is not a vacuous "typecheck is clean")
pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 exit 0 — Test Files 159 passed (159), Tests 2079 passed (2079)
the pin alone exit 0 — Test Files 1 passed, Tests 5 passed
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands 36 commands, derived from the merged tree with no stale warning
the 36 + pnpm check:nul-bytes 31 exit 0, 6 NOT MEASURED (below)
pnpm lint (whole repo) exit 0 — the WHOLE repo (eslint . --no-inline-config), no narrowing claimed or needed

Heavy runs went through scripts/pm/os-verify-lock.sh; verdicts are read from
its VERDICT line, never a bare $?.

NOT MEASURED — 6 of the 37, in each gate's own words

None is a finding, and none is about this diff. Five need a whole-repo build
(this container built only @objectstack/cli's closure) and one needs GitHub API
access this seat does not have (repo-scoped REST returns 403
GitHub access is not enabled for this session).

  • node scripts/check-test-completeness.mjs (exit 3) — "Nothing was measured:
    this gate exited before parsing a single summary line … ⛔ It is NOT a finding"
    .
    Wants a saved turbo run test log.
  • node scripts/pm/check-half-states.mjs (exit 3) — "Treat this exit as an
    unread instrument, never as a quiet board"
    .
  • pnpm check:dual-build-cjs-loads (exit 3) — "PREREQUISITE NOT MET — this gate
    reads built output, and some package has no dist/ … ⛔ This is NOT a pass:
    nothing was measured."
  • pnpm check:i18n (exit 1) — "Nothing was checked: no bundle was compared and
    no config was parsed"
    .
  • pnpm check:i18n-coverage (exit 1) — "Nothing was measured: no config was
    linted and no count was compared"
    .
  • pnpm check:type-check-debt (exit 3) — "⛔ This is NOT a pass and NOT a
    finding: nothing was measured"
    .

CI runs the whole farm on this PR with a full build, which is where these six get
their real verdict.

Deviations and notes for review

  • Scope extension to the two sibling tables and the fifth switch ghost —
    declared above and on the issue before the first edit, with the exemption's
    four conditions spelled out. Trimmable in one slice if the reviewer prefers.
  • The merge commit exists only because the gate derivation refused a stale
    tree; it carries no changes of its own.
  • The commit trailers follow this session's harness-level attribution
    instruction, which specifies an exact Co-Authored-By line. That line names a
    model, which the dispatch asked to avoid; the conflict is recorded here rather
    than resolved silently.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…ularies (#13871)

`generate.ts` carried three hand-authored field-type vocabularies — the
`FIELD_TYPE_MAP` that `os generate types` reads, the `FIELD_TYPE_SQL_MAP` that
`os generate migration --format sql` reads, and the `switch (fType)` in the
typescript migration generator — none of which had ever been checked against the
`FieldType` enum they claim to describe. Between them they named six types the
platform has never had: `slug`, `ip_address`, `encrypted`, `integer`, `uuid`,
and `geo_point`.

Measured, not assumed. `git log -S` over the whole reachable history of
`packages/spec/src/data/field.zod.ts` returns zero commits for every one of those
tokens, so they are not leftovers of retired types — they were invented here (the
maps first, the migration codegen mirroring them six hours later) and propagated
table to table inside this one file.

Both doors into the generator were driven:

  - Through every supported authoring path the arms are dead. `os init` scaffolds
    `export default defineStack({ … })`, `define*` is a strict `Schema.parse`,
    and a field typed `slug` is refused during config-module evaluation, inside
    `bundleRequire`, before the generator runs a line.
  - Through a config that parses nothing (a plain-object default export, or
    `defineStack(x, { strict: false })`) any string reaches `fType` and the ghost
    arms fire — `slug` emitted `table.string`, `integer` emitted `table.integer`.

So the labels never served a valid input, and on the one input class that could
reach them they advertised an acceptance surface no runtime can honour.

Every ghost is deleted rather than re-spelled, per member: `number` already had
its own entry and arm so `integer` had nothing to correct to; `address` is a
structured postal address, not an IP; and the concepts that later arrived under
other names (`secret`, `location`) have no entry in these tables at all, which is
a coverage question rather than a spelling one and is filed separately.

Behaviour is unchanged for every config the platform accepts. For a config that
bypasses validation, one of the six now falls to the same default any unknown
type gets — `table.text` / `TEXT` / `unknown`.

The pin reads all three vocabularies out of the source and fails on any key or
case label that is not a `FieldType` member, with a non-vacuity control on each
extraction and a structural assertion that a fourth vocabulary cannot arrive
unmeasured. It is forward-only: real members with no entry still fall to the
deliberate default, which it does not prejudge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…d-types

Keeps the gate-family derivation and the verification on a tree that is at
origin/main, per scripts/pm/dispatch-gates.mjs's stale-tree refusal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 2aa8456cf2d66ec3825d262686fe4218e57cfd27.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 22 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 2aa8456cf2d66ec3825d262686fe4218e57cfd27packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6627976a5f10964f8f2bbde851d3b7def1dc3e40 — the merge of head 3d150eb0786aba6955e8e88fa0080aa5b3f1902c into base 2aa8456cf2d66ec3825d262686fe4218e57cfd27, 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 6627976a5f10964f8f2bbde851d3b7def1dc3e40 && git checkout 6627976a5f10964f8f2bbde851d3b7def1dc3e40
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2aa8456cf2d66ec3825d262686fe4218e57cfd27 3d150eb0786aba6955e8e88fa0080aa5b3f1902c && git checkout -B drift-repro 2aa8456cf2d66ec3825d262686fe4218e57cfd27 && git merge --no-ff 3d150eb0786aba6955e8e88fa0080aa5b3f1902c

node scripts/docs-audit/affected-docs.mjs --json 2aa8456cf2d66ec3825d262686fe4218e57cfd27

⚠️ 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 2aa8456cf2d66ec3825d262686fe4218e57cfd27 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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

队列构建 33670772221 红了。队列跑的是全量套件(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: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时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 队列共有 13 个失败构建(不含本次)。

分诊清单:

  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 2, 2026

Copy link
Copy Markdown
Contributor

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

队列构建 33674214419 红了。队列跑的是全量套件(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: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时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 队列共有 16 个失败构建(不含本次)。

分诊清单:

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

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

This was referenced Sep 2, 2026
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

2 participants