Skip to content

fix(objectql): a cleared optional single-value select or lookup stores null, so a second unique clear is no longer refused 409 (#29277) - #29464

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-uzstmh
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-uzstmh

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Requested by Steve · project thread
Fixes #29277

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

What changes

Why

'' is neither an option code nor a record id. Stored as sent, it was a value to SQL UNIQUE (the second cleared record on a unique column was refused 409) and not empty to $empty, whose ruled table reads only null as empty on these types. The formula matcher, having and CEL isBlank already read '' as empty, so the SQL face now agrees with them for every newly cleared row.

Tests

New files:

  • packages/objectql/src/validation/record-validator.single-value-blank.test.ts (8 tests): the population derived from FieldType and the table, with every untyped null_only type classified into exactly one storage class; the joined set (11 types) and the left-out set (9 types) pinned; the left-out types keep ''; '' becomes null on every optional member, unique or not; required (with and without a defaultValue) keeps ''; multiple: true is the multi_value row and keeps ''; lifecycle columns untouched; whitespace and non-blank values untouched; purity.
  • packages/core/src/utils/blank-storage-form.test.ts (7 tests): each arm's reach (typed incl. required and lifecycle created_at; unique text by field and by index, plain text and password out; one-string select / lookup / image in, required / multiple: true / json / autonumber / created_by out); the lifecycle list; an unresolvable definition declares none; blankStorageForm per reach.
  • packages/runtime/src/seed-blank-as-null-replay.integration.test.ts (2 tests), real ObjectQL + SQLite SqlDriver + SeedLoaderService: one seed '' per arm (number, date, unique text, select) lands null and a replay skips all four (totalUpdated: 0, totalSkipped: 4); control: a plain text column stored null and reseeded '' is still updated and stores ''.
  • packages/rest/src/rest-data-single-value-blank.test.ts (11 tests), real ObjectQL + SQLite SqlDriver + RestServer routes, physical column read past the engine: POST '' on all eleven joined types stores NULL; PATCH clear on all eleven; unique select + lookup created '' twice and cleared by PATCH twice both land; a unique image created '' twice both land; a declared unique-index member; batch create and createMany; a real duplicate still 409 UNIQUE_VIOLATION; required select / lookup still 400 VALIDATION_FAILED / required (one with a defaultValue); a blank on insert takes the declared defaultValue exactly as null does; the $empty pin: on a row written '' past the engine, $empty: true answers only the newly cleared row, $empty: false answers the legacy '' row and the valued row, = '' answers only the legacy row.

Forced edits to existing tests (each pinned that a select / lookup '' stays '', which the ruling reverses):

  • packages/objectql/src/validation/record-validator.blank-typed-value.test.ts: the string-stored control list drops the option and reference types (they keep their "not typed" assertion).
  • packages/objectql/src/validation/record-validator.optional-unique-blank.test.ts: the "other string-stored" control is now autonumber only.
  • packages/objectql/src/engine-optional-unique-blank-door.test.ts: the unique pick (select) / ref (lookup) columns move from the kept-'' set to the covered-null set.
  • packages/objectql/src/engine-blank-typed-value-door.test.ts and packages/rest/src/rest-data-blank-typed-value.test.ts: the c_lookup / c_select controls now expect null; c_text still ''.

Round 3 forced edits (no expectation changed):

  • packages/runtime/src/seed-blank-reference-real-driver.integration.test.ts: the comment at the loan-empty assertion said the insert door "does not null" a blank lookup; it now says an optional lookup's '' is stored as null since objectql: a cleared optional unique lookup or select still stores '' and the second cleared record is refused 409 UNIQUE_VIOLATION #29277 and the replay compares it in that form. Its expectations (totalUpdated: 0, totalSkipped: 6) are unchanged and now pass.
  • packages/objectql/src/federated-injected-column-readers.test.ts: four VOCABULARY rows (validation/record-validator.ts#<module> :: created_at / created_by / updated_at / updated_by) deleted, as the census asks when a seam use leaves the source: the lifecycle names moved to core's ENGINE_LIFECYCLE_COLUMNS.
Red on main, green on head

The two new files were committed first (0acdf485) and run against origin/main 96fb7b5d's objectql build: rest Tests 9 failed | 2 passed (11) (POST stored ''; unique [201, 409] on create, [200, 409] on PATCH and the index member; batch [true, false]; defaults not applied; $empty: true answered []), unit Tests 2 failed | 5 passed (7). Two of those rest reds were my own fixture faults (a tree value naming no record of its object, a whitespace lookup id the referential-integrity check refuses), fixed in a71af048; the whitespace case moved to the unit file. On head: rest 11/11, unit 8/8.

Ablations

Made with scripts/ablation-replace.mjs (anchor hit once), objectql rebuilt, marker proven in dist by scripts/ablation-dist-preflight.mjs ("marker present in 4 built files"), each restored with git checkout HEAD, blob hash equal to HEAD, then a final rebuild with the marker absent from all 8 built files and the rest file green (10/10 round 1, 11/11 round 2).

Ablation Red
L1: the new arm off (isSingleValueCodeColumn never true) rest Tests 8 failed | 2 passed (10) (every positive case, including the $empty pin), unit 2 failed
L2: the table-row check removed (a multiple: true column joins) unit a multiple: true column is the multi_value row, not this arm
L3 (round 2): FILE_REFERENCE_TYPES dropped from the one-string set rest Tests 3 failed | 8 passed (11) (POST, PATCH, unique image), unit "" on every optional member becomes null
L4 (round 2): STRUCTURED_JSON_TYPES added to the one-string set unit the left-out types keep ""
L5 (round 3): the replay comparand off (isNoOpReplay compares the raw seed value), metadata-protocol rebuilt runtime Tests 2 failed | 2 passed (4): seed-blank-as-null-replay (replay totalUpdated 4) and seed-blank-reference-real-driver :161 (1), the CI red; the plain-text control and the required case stay green. Restored from a copy, blob hash equal, rebuilt, both green.

The required / lifecycle / whitespace exclusions are the #29176 lines, ablated on PR #29271; the required and default pins here pass on base and head, as they must.

Acceptance notes

Verification

Head 22a77935 (origin/main 96fb7b5d merged in at e607df78; a full-declaration rebuild of the runtime and rest closures). CI runs the rest: see the PR's check-runs.

Suites, typecheck, lint, gates
Run Result
core full suite + typecheck Test Files 138 passed (138) · Tests 3021 passed · typecheck exit 0
metadata-protocol full suite + typecheck Test Files 305 passed, 4 skipped (309) · Tests 22161 passed, 26 skipped · typecheck exit 0
objectql full suite + typecheck Test Files 533 passed, 1 failed (534) on the first run: the injected-column census (re-keyed above, then 5 passed (5)) · Tests 10320 · typecheck exit 0
runtime full suite + typecheck Test Files 527 passed (527) · Tests 9678 passed, 19 skipped · typecheck exit 0
rest full suite + typecheck Test Files 376 passed (376) · Tests 7329 passed, 352 skipped · typecheck exit 0
pnpm lint exit 0
dispatch-gates --commands (63 derived), reconciled with --ran 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN; 62 exit 0 (check:type-check-debt first queue-timed out behind the suites, exit 3, then rerun under the lock: exit 0)
node scripts/check-empty-changeset.mjs --base origin/main exit 1, by design: the DELIBERATE CORRECTION class for .changeset/29176-optional-unique-blank-null.md (see What changes). Not restored, no skip-changeset; the seat confirmed. Check Changeset is not a required context.
check-changeset-no-major against this body (offline --event) exit 0: level axis Clause-②: yes (widening) / Metadata-protocol: none, patch

The plugin-teardown self-test needed git fetch origin 621a4876… and check:dual-build-cjs-loads a full ./packages/** build first; both then exit 0. Full dogfood not run: no ! in the title and no (narrowing) arm.


Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/core, @objectstack/metadata-protocol, @objectstack/objectql, touching 23 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/core/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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 399df5bba888ff7cf7531d01d8dcb61d7690438a.

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/core/src/index.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 43 pages)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 51 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 399df5bba888ff7cf7531d01d8dcb61d7690438a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ea18058ba0432ef0b6d927d0745d84631a3de680 — the merge of head 22a779351bf40b62e478721343b78d8360b53b7d into base 399df5bba888ff7cf7531d01d8dcb61d7690438a, 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 ea18058ba0432ef0b6d927d0745d84631a3de680 && git checkout ea18058ba0432ef0b6d927d0745d84631a3de680
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 399df5bba888ff7cf7531d01d8dcb61d7690438a 22a779351bf40b62e478721343b78d8360b53b7d && git checkout -B drift-repro 399df5bba888ff7cf7531d01d8dcb61d7690438a && git merge --no-ff 22a779351bf40b62e478721343b78d8360b53b7d

node scripts/docs-audit/affected-docs.mjs --json 399df5bba888ff7cf7531d01d8dcb61d7690438a

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

… row plus a stated one-string set; file references join (#29277)

Claude-Session: https://claude.ai/code/session_01ADHkmRPC4f86r9bWGuYWJu
Co-authored-by: Claude <noreply@anthropic.com>
…write door and the seed replay (#29277)

The write door's three blank-as-null arms (#20308 typed, #29176 unique
text, #29277 one-string null_only) move to @objectstack/core as
blankAsNullColumns / blankStorageForm. The door applies it, and the
seed loader's replay compares a seed value in that stored form through
a new blankAsNull replay comparand, so a seed '' the door stores as null
is no longer a change on every replay. Corrects a stale comment in the
seed blank-reference integration test.

Claude-Session: https://claude.ai/code/session_01ADHkmRPC4f86r9bWGuYWJu
Co-authored-by: Claude <noreply@anthropic.com>
…ames moved to core (#29277)

The four VOCABULARY rows for record-validator's SKIP_FIELDS literal go:
the names now come from @objectstack/core's ENGINE_LIFECYCLE_COLUMNS.

Claude-Session: https://claude.ai/code/session_01ADHkmRPC4f86r9bWGuYWJu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl and removed size/l labels Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Contract-review-claim: session_01ADHkmQJdgarUjPLNgFBY67 · head 22a7793 · 2026-11-02T17:44Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 22a779351bf40b62e478721343b78d8360b53b7d
Local-runs: none

Read: card #29277 (body and all 8 comments: triage 6575406743, claim 6576911211, dev reports 6577038603 and 6581571774, block 6577287839, seat rulings 6581612131 and 6582252856, claim amendment 6584075217), maintainer ruling B on #29346 (6579813296), the PR body, its 17-file list, and the net diff origin/main...pr29464 (merge base 0e0ddacd; origin/main is 399df5bb, and git merge-tree merges the two cleanly; the only main-side change to a touched file is #29241's string-arm hunk in record-validator.ts, in a different region).

Check-runs on the head: green includes Lint and Repo Gates, Lint (ESLint, PM dispatch-gates, docs and code guards, repo and tooling gates, CI/release/engine gates, the scoped lints), Type Check (debt ledger, source gates), PR-body guards, Governed Surface Queue Guard, Dogfood Regression Gate, Check PR Size, Plan Test Core shards. Red: Check Changeset, two runs, both only on check-empty-changeset (judged in ②). Still in progress or queued: Build Core, Test Core 1/8 to 8/8, Type Check (workspace, consumer gates, UI), Build and E2E UI, Live E2E UI, Test UI 1/16 to 16/16, and a re-queued Lint and Repo Gates. This review does not stand in for any of them.

① Derived judgments

  1. The three new @objectstack/core exports: right, each the one spelling. packages/core/src/utils/blank-storage-form.ts is re-exported from packages/core/src/index.ts by export *. It adds three runtime exports (blankAsNullColumns, blankStorageForm, ENGINE_LIFECYCLE_COLUMNS) and two types (BlankAsNullReach, BlankAsNullFieldDef). ONE_STRING_STORED_TYPES and UNIQUE_BLANK_TEXT_TYPES stay module-private. Core already depends on @objectstack/spec, and metadata-protocol already depends on core, so no edge is added. record-validator.ts keeps no arm logic of its own. optionalUniqueTextColumns and UNIQUE_BLANK_TEXT_TYPES are deleted, along with the NON_TEXT_STORED_VALUE_TYPES, STRING_VALUE_TYPES and isUniqueDeclared imports. normalizeBlankTypedRow keeps only the isMissing pre-filter, which is a fast path both reaches share. It then asks blankStorageForm(value, reachOf(name)), with the map built lazily once per call as before. SKIP_FIELDS is now new Set(ENGINE_LIFECYCLE_COLUMNS), the same five names.
  2. The accept set is unchanged on the two older arms, and the objectql: a cleared optional unique lookup or select still stores '' and the second cleared record is refused 409 UNIQUE_VIOLATION #29277 arm is added as ruled: right. Read line by line against origin/main:
  3. Seed replay: right. isNoOpReplay now compares blankStorageForm(seedValue, comparands.blankAsNull.get(key)) with the stored value. Only the seed side is put in stored form, and the stored value is read as is. blankAsNull is a third ReplayComparands entry, built once per dataset by the same blankAsNullColumns.
    • No blanket equality. A column outside the map keeps the raw comparison, so on plain text a seed '' against a stored null is still a change. seed-blank-as-null-replay.integration.test.ts pins that as its control: totalUpdated: 1, and the row then stores ''.
    • No false no-op. The comparand is exactly "what the door would store". A seed '' reaches null only where the door writes null, so it can equal only a stored null. A legacy stored '' on these columns compares as null against '', which counts as changed, and that is true. The 'empty' reach leaves whitespace as written. Credential masking runs before it on the raw value, unchanged.
    • Pins and ablation. All three arms are pinned (number, date, unique text, select: replay 0 updated, 4 skipped). The CI red from 375469fa (seed-blank-reference-real-driver line 161) is fixed without touching its expectations. Ablation L5 turns both runtime files red.
  4. The read-shape move is stated: right. A cleared optional select/radio/lookup/master_detail/user/tree/file-reference field now reads null and no longer matches field = ''. The objectql: a cleared optional unique lookup or select still stores '' and the second cleared record is refused 409 UNIQUE_VIOLATION #29277 changeset says this under "Read shape" and "Rows already stored", and so do the PR body's Acceptance notes and its 维护者速读 risk line. The REST $empty pin shows the table's own answers unchanged, with legacy '' still a value.
  5. Forced test edits: right, nothing loosened.
    • federated-injected-column-readers.test.ts: four VOCABULARY rows deleted (created_at, created_by, updated_at, updated_by keyed on record-validator.ts at module level). The census fails on a row whose seam use is gone ("delete or re-key the row"). The names left the scanned objectql source for core's ENGINE_LIFECYCLE_COLUMNS, and their disposition said they read no row, so no reader leaves the census's sight. A forced deletion, not a loosening.
    • seed-blank-reference-real-driver.integration.test.ts: comment only, "it does not null one" corrected. totalUpdated: 0 and totalSkipped: 6 are unchanged.
    • The five round-1 edits reverse '' to null on select/lookup as the ruling requires. That is stricter, not looser: pick/ref move into the covered set and the controls expect null. The text controls still expect '', and the "not typed" assertion is kept for every type moved.

② Semver level

Clause-②: yes (widening)

③ Boundary flags

Implemented-by: claude/project-thread-uzstmh
Reviewed-by: session_01ADHkmQJdgarUjPLNgFBY67

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

席位核对 · ACCEPT · PR #29464 · 2026-11-02T17:52Z

Head-sha: 22a779351bf40b62e478721343b78d8360b53b7d

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main,Fixes #29277。17 个文件,在认领及其修订 6584075217 的申报面内。被迫测试改动:清空的 select/lookup 期望由 '' 改为 null(裁决要求,更严,不是放宽)、federated-injected-column-readers 删 4 行(生命周期列名搬到 core,普查自身提示删除)、一处过时注释更正。
  2. 前提实测:声明面契约复审 PASS(本 PR 上 ## Contract review 记录,TIER_DEFAULT,独立会话)。复审逐条核对:两条旧规则覆盖的列与主干一致,新规则按裁决加入;重放只把种子一侧换成存储形态,纯文本列 '' 对 null 仍算变更,不会把真变更误判为无变更。
  3. 门禁:dev 第三轮(head 22a77935):core 3021、metadata-protocol 22161、objectql 10320、runtime 9678、rest 7329 全绿,各 typecheck exit 0;消融 L5(关掉比较项)两条 runtime 钉子转红,复原哈希一致;dispatch-gates 63/63,0 未测;pnpm lint 0。入队前席位读本 head 全部 check-run。
  4. 接受的偏差:生命周期列不纳入、owner_id 纳入(复审对源码核过,鉴权中间件把 null 与 '' 同等看待)。Check Changeset 红(check-empty-changeset)= 更正 data: clearing an optional unique text field stores an empty string, so the second record whose field is cleared is refused 409 UNIQUE_VIOLATION #29176 待发 changeset 的 DELIBERATE CORRECTION,已在 6581612131 确认,不是必需检查。第三轮读数写在 PR 正文,没有另贴 os-dev-report,接受。
  5. 开放问题裁定:文件类型按存储形态并入(6581612131);重放比较按存储形态(6582252856),四项均已落实。复审提的后续:migrate: an os migrate value-shapes step stores null for '' already stored on optional single-value select/radio/lookup/master_detail/user/tree columns (#29346 ruling B, data half) #29431 的列范围已补到 11 种类型(本卡注记);records: a free-form select field (no options) admits a plain object or an array on create and stores it as-is, although a select's stored value is a scalar option value #29244(席 1)在本 PR 合并后落地;带 defaultValue 的列种子写 '' 会多重写一次后稳定,属插入/更新默认值的既有不对称,不在本卡范围。
  6. 落地条款:Clause-②: yes (widening)(core 新增三个导出),Metadata-protocol: none,PR 正文与 changeset 都有。semver:@objectstack/objectql、@objectstack/core、@objectstack/metadata-protocol 均 patch。check-governed-merges --pr 29464:NOT governed。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

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

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

分类:failure —— 按下面的日志分诊。

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

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

    @objectstack/spec:test:  FAIL   local  scripts/dropped-refinements.test.ts > the committed ledger > names at least one site per entry, and its header totals match its body
      ↳ 失败原因: @objectstack/spec:test: AssertionError: expected 993 to be 998 // Object.is equality
    

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

分诊清单:

  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 bd68248 Nov 2, 2026
66 of 68 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-uzstmh branch November 2, 2026 18:45
os-steve pushed a commit that referenced this pull request Nov 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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectql: a cleared optional unique lookup or select still stores '' and the second cleared record is refused 409 UNIQUE_VIOLATION

1 participant