Skip to content

fix(objectql): droppedFields on a create names only keys the caller sent, never a middleware fill (#21682) - #21701

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-21682-dropped-fields-platform-stamp
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-21682-dropped-fields-platform-stamp

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21682
Clause-②: no

What changes

This applies on a walled posture with the real @objectstack/organizations runtime mounted:

  • Before: a create that named no organization_id answered 201 with droppedFields: [{ object, fields: ['organization_id'], reason: 'readonly' }]. The caller never sent that key.
  • After: the same create answers 201 with no droppedFields. The row is still stored in the active organization.

The fix sits where triage routed it: ObjectQL.insert's caller-payload snapshot in packages/objectql/src/engine.ts.

  • At insert's entry, before the middleware chain runs, the engine records the keys each row carries. The record is keyed by the row object (callerKeySets, line 2094; called at line 12879).
  • The snapshot suppliedPerRow (line 12983) now keeps only those keys (callerSuppliedRow, line 2113).
  • Values are unchanged, so every key the caller does send is judged as before.
  • No field is named anywhere. A key counts as the platform's because of WHEN it appeared on the payload, not because of its name.

The console reading (H4), measured first: the console warns, so this is the p2 raise reading

objectui at its pin ab187972159583b595facdcae3c73b50f6f312e9 (.objectui-sha), read from a shallow clone that has since been deleted:

  • Every adapter create reports. packages/data-objectstack/src/index.ts: ObjectStackAdapter.create calls notifyDroppedFields('create', resource, result, id, data) on every create.
  • The filter keeps a field the caller never sent. notifyDroppedFields (line 3767) runs the entries through withoutNoOpDrops(valid, sent, stored) (line 2794). That filter keeps such a field: if (!Object.prototype.hasOwnProperty.call(sent, f)) return true;. It drops only a field whose sent value equals the stored value.
  • The shell turns it into a toast. packages/app-shell/src/providers/AdapterProvider.tsx line 80 subscribes onWriteWarning and passes sonner's toast to emitWriteWarning. For any non-empty list, emitWriteWarning (writeWarningToast.ts) calls sink.warning with the title "Saved — but some fields did not take effect" and the line "Read-only, so it did not take effect: " followed by the field's label.
  • The record forms go through that adapter. These call dataSource.create: plugin-form's ObjectForm.tsx:1281, ModalForm.tsx:660, DrawerForm.tsx:574, SplitForm.tsx:399, TabbedForm.tsx:501 and WizardForm.tsx:978, plus ObjectCalendar.tsx:1190 and the grid's ImportWizard.tsx:1931.
  • Measured as well as read. I extracted the pinned withoutNoOpDrops, sameWireValue and stableStringify verbatim and ran them on the card's wire shape:
    • entries: [{ object: 'qa_ledger', fields: ['organization_id'], reason: 'readonly' }];
    • sent: { name };
    • stored: a record carrying organization_id.
    • Output: the entry is kept (toast fires: true).
  • One console path does not toast. apps/console/src/components/FormPage.tsx posts its internal submit with raw fetch (line 1375) and never reads droppedFields.

So an ordinary walled create from the console's record forms shows the amber "Saved — but some fields did not take effect" toast, naming Organization. That is #8093's symptom, on create. The seat raises the grade on this reading.

Mechanism, measured (Partition 2)

  • H1, confirmed by measurement.
    • At 251a7dd4b4, insert took suppliedPerRow (line 12905) inside the callback it hands executeWithMiddleware (line 12828). By then every middleware had already run.
    • Middleware A (organizations-plugin.ts:344) fills an absent or empty organization_id in place, so the snapshot recorded the fill as a caller key.
    • The static-readonly strip took it, because the injected column is readonly: true, and reported it. Further down, the driver's injectTenantOnInsert filled the column again from the context. That is why no stored data was wrong.
    • The new pins, run against the unmodified engine, returned [{ fields: ['organization_id'], reason: 'readonly' }] for both a member and a platform administrator.
  • H2, the precedent applied.
  • H3, explained.
    • Middleware A fills only a non-array payload (!Array.isArray(opCtx.data)). So a createMany row carries no fill and never reported one. The driver fills the column for it.
    • Without the runtime (single) there is no fill at all.
    • Both controls are pinned. They are green before and after.
  • H5, measured.
    • On insert, the in-tree middlewares that write a value into opCtx.data are two:
      • @objectstack/organizations Middleware A, for organization_id (single row only);
      • @objectstack/plugin-security step 3.5, for owner_id (every row, security-plugin.ts:3230).
    • The rest filter reads, act after next(), or (the anonymous public-form branch) delete keys rather than fill them.
    • owner_id is not readonly, so it never reached droppedFields. It did reach the referential-integrity check (see Behaviour notes).
    • The walled pins assert droppedFields is empty, which covers both fills. The engine pins use an arbitrary author column, so no name is involved.

Pins

These are triage's three, on a walled boot built from the real @objectstack/organizations runtime, the real SecurityPlugin, a real ObjectQL and SqliteWasmDriver. The block is added to #21666's harness in packages/plugins/organizations/src/create-explicit-organization-wall.test.ts. The drops are collected through onFieldsDropped, the listener the REST create doors (createData, createManyData) answer droppedFields from.

  1. A walled create with no organization_id reports no dropped field and is stored in the active organization. Run for a member and for a platform administrator.
  2. A caller that sends a readonly key still sees it reported:
    • organization_id itself, sent explicitly, is reported;
    • created_by, sent beside the fill, is reported alone.
  3. createMany and the single posture are unchanged:
    • an array row naming no organization reports nothing;
    • under single, booted without the runtime, a create naming no organization reports nothing, and one naming an organization still reports it.

There are three engine pins in packages/objectql/src/engine-insert-static-readonly-strip.test.ts, beside the existing exemption "a beforeInsert hook's OWN stamp survives":

  • a middleware fill on an author readonly column lands and is not reported;
  • the same holds per row on a batch, beside a key the caller did send, which is still taken and reported;
  • a key the caller sent is judged as before even when a middleware rewrote its value.

Reverse verification

Run from committed state, with HEAD 81ad21efc1 carrying the fix:

  • The mutation. scripts/ablation-replace.mjs changed callerSuppliedRow(row, callerKeysPerRow[i]) to callerSuppliedRow(row, undefined). That restores the full post-middleware copy, which is the pre-fix snapshot.
  • It landed on disk. The anchor count went 1 to 0 and the replacement count 0 to 1. The blob went from 16159cd42cbf to 9fae7a2c1837.
  • No build was needed. Both suites resolve the subject from source: the organizations config aliases @objectstack/objectql to src, and the engine test imports ./engine.js.
  • Predicted, then observed:
    • organizations file: 3 failed, 18 passed. The failures are pin 1 for both callers, plus pin 2's "beside the fill" case, which also asserts pin 1's absence. Pin 2's explicit case and every pin 3 case stayed green.
    • engine file: 2 failed, 19 passed. The failures are the two fill cases (expected undefined to be 'stamp'; expected [ undefined, undefined ] to deeply equal [ 'stamp', 'stamp' ]). The rewritten-value case stayed green.
  • Restored. git checkout HEAD -- packages/objectql/src/engine.ts, run by the tool's trap. Blob after restore 16159cd42cbf equals HEAD, and git diff HEAD is empty.

Behaviour notes

  • The referential-integrity check moves with the snapshot.
    • assertReferencesResolve reads suppliedPerRow to decide what the caller sent. Its docblock already scopes it to caller-supplied keys and names owner_id and organization_id as stamps it must not judge.
    • A middleware fill used to be judged anyway. I measured this once with a scratch probe (deleted, not committed): a middleware fills a lookup with a dangling id on a create that did not name it.
    • Before: refused VALIDATION_FAILED. After: created.
    • The in-tree fill this reaches is plugin-security's owner_id fill: its stamp was checked as if the caller had sent it. I did not measure a principal that the check actually refused for it.
  • Values are unchanged for keys the caller sent. That includes a caller-sent empty organization_id that Middleware A fills. It is still reported, as before. The update path's snapshot also captures values before its chain; this change aligns the KEY set only.

Tests (at 47cbb883b3)

The test runs were taken at 47cbb883b3, whose code equals the fix commit 81ad21efc1. The merge of origin/main (994ec65025) brought in only a service-analytics test and scripts/pm/git-history.mjs, neither in objectql nor organizations, so the suites were not rerun after it. Every run went through scripts/pm/os-verify-lock.sh with --maxWorkers=2.

  • Before the fix, with the pins at e95548845c over the unmodified engine: pnpm --filter @objectstack/organizations exec vitest run src/create-explicit-organization-wall.test.ts gave 3 failed and 18 passed. The received value was [{ fields: ['organization_id'], object: 'qa_ledger', reason: 'readonly' }].

  • After the fix:

    • the same file: 21 passed;
    • pnpm --filter @objectstack/objectql exec vitest run src/engine-insert-static-readonly-strip.test.ts: 21 passed (18 existing, 3 new);
    • pnpm --filter @objectstack/objectql exec vitest run --project local (the package's test script): 371 files, 7450 tests passed;
    • pnpm --filter @objectstack/organizations test: 9 files, 130 passed (123 existing, 7 new);
    • pnpm --filter @objectstack/objectql typecheck: exit 0. check:test-typecheck reports OK, with 40 files, 234 errors and 65 signatures held in the ledger, unchanged;
    • pnpm --filter @objectstack/organizations typecheck: exit 0, test layer 0 errors.
  • Builds:

    • the dependency closure, turbo run build --filter='@objectstack/organizations^...': 27 of 27 tasks;
    • objectql and organizations, rebuilt for the gates that read dist/ (the fix is present in dist/index.mjs);
    • a full --filter='!@objectstack/docs' build, 72 of 72 tasks (71 cached), for check:dual-build-cjs-loads.
  • Lint, a declared narrowing. I ran eslint --no-inline-config --format json on the 3 TypeScript files this diff touches: 3 files linted, 0 errors, 0 warnings, none ignored (--print-config exits 0 for each). Why the narrowing excludes nothing:

    • eslint.config.mjs enables no type-aware linting (its own note at lines 327-328: no parserOptions.project, no typed rules);
    • its plugins are local per-file AST rules;
    • its only external reads are scripts/slot-lookup-baseline.json and scripts/query-options-erasure-baseline.json, which this diff does not touch.

    So this diff cannot move a verdict on an untouched file. The repo-wide pnpm lint stays with CI.

Gates

The final run was at 994ec65025, after the merge. node scripts/pm/dispatch-gates.mjs --commands was run with no paths. It derives 70 commands from the 4-path change set against merge base 38bef8cf9, the same 70 as before the merge.

  • The 70 derived: all exit 0. dispatch-gates --ran reports "70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN".
    • check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET, 39 packages without dist) in the first run at 47cbb883b3. After the full build it exited 0, and it exited 0 again at 994ec65025.
    • Some verdict lines: check:nul-bytes OK (10060 files, no control bytes); check:engine-double-contract OK (936 pinned); check:test-source-alias OK; check:cross-package-test-inputs OK; check-adr-0087-registration (1 non-breaking changeset); check-changeset-no-major (no major); check-empty-changeset OK; check:type-check-coverage OK.
  • The artifact-roster block printed outside the total: 54 families, 53 of them not in the derived set. 51 exit 0.
    • 3 exit 2 NOT WIRED, because they need pull-request context: check-closing-target-claim, check-partof-closing-keyword and check-single-claim-paths.
    • check-partof-closing-keyword was also run on this body with PR_BODY set, and exited 0.
    • The other two can only run once this PR exists. Their readings are in the os-dev report on the card.

Acceptance notes

These are noted, not filed: none is a reproducible defect, a contract violation or an authoring trap that this card measured.

  • Values still differ from the update path. Insert now matches update for the caller KEY set. Update also captures VALUES before its chain; insert keeps the value the chain handed on. So a caller-sent key whose value a middleware rewrote is judged on the middleware's value on insert, and on the caller's value on update. No in-tree insert middleware rewrites a non-empty caller value since [finding] POST /data on a walled posture silently replaces a platform admin's explicit organization_id with the admin's active organization (201), while the equivalent PATCH refuses loudly #21666: Middleware A only fills an absent or empty value, and plugin-security refuses a forged owner rather than rewriting it. This is unexercised drift, and no PR or person will carry it.
  • Middleware A's fill now reaches the driver. On a single-row create, Middleware A's fill and the driver's injectTenantOnInsert give the same value. Before, the fill was stripped and the driver filled the column again. Now the fill is kept.
  • objectui's FormPage.tsx drops the report. Its internal submit posts with raw fetch and never reads droppedFields, so a strip on that path is silent. That is at objectui pin ab187972; no PR or person will carry it.
  • objectui keeps never-sent fields. objectui's withoutNoOpDrops keeps a reported field the caller never sent. With this change the server no longer reports a middleware fill, so that branch no longer fires for one.

Generated by Claude Code

claude added 4 commits October 4, 2026 06:01
…no organization_id (#21682)

The pins come first so the red on unmodified main is measured. The fix follows.

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
…ent, never a middleware fill (#21682)

ObjectQL.insert took its caller snapshot (suppliedPerRow) inside the
middleware chain's innermost step, after a write middleware had already
filled the payload. On a walled posture @objectstack/organizations fills an
absent organization_id, so the static-readonly strip took the platform's own
value and droppedFields reported it on every create that named no
organization. The console shows every non-empty droppedFields as a warning
toast.

The keys each row carries are now recorded at insert's entry, before the
chain runs, keyed by the row object. The snapshot keeps only those keys. The
values stay as the chain handed them on, so every key the caller did send is
judged as before. There is no name list.

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 17 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 16d241a6af00be4acce9883190fc333a5f560825 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 16d241a6af00be4acce9883190fc333a5f560825

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT — PR #21701 at head 994ec65025

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-04T07:01Z. The os-dev report is on #21682. Judged against GitHub and the branch, not against the report.

  • Shape: draft, base main, assignee os-project-manager.

    • The first lines are Fixes #21682 and Clause-②: no.
    • The closing-keyword scan finds #21682 only. #21666 and #8093 appear in the body with no verb next to them.
  • Scope: 4 files, +256/-11:

    NOT governed. No packages/spec file is touched. A local git merge-tree against origin/main is clean.

  • The diff, read:

    • callerKeySets records each row's key set at insert's entry, after the two normalizers and before executeWithMiddleware, keyed by row identity in a WeakMap.
    • Inside the chain, callerKeysPerRow is read on the rows the chain handed on, BEFORE the computed-field door can replace a row with a copy.
    • callerSuppliedRow narrows only WHICH keys the snapshot keeps. Values are the chain's, as before.
    • A row a middleware replaced wholesale has no record and keeps every key. That is the over-reporting direction, never the under-stripping one.
    • ⛔ No field name appears anywhere.
  • The three snapshot consumers, checked:

  • Clause-②: no — accepted.

    • What a caller can send, and how the public door answers it, is unchanged except that droppedFields stops naming a key the caller never sent.
    • The referential check no longer judges a middleware-filled reference. The dev measured that with a scratch middleware. No in-tree fill can dangle: organizations fills the active organization and plugin-security the acting user. So no public-door accept or reject moves, and no contract review is owed.
  • H4, measured first: the console warns.

  • Changeset, checked sentence by sentence:

    • @objectstack/objectql patch.
    • The before and after POST /api/v1/data/<object> statements match pin 1.
    • "No field is exempted by name … owner_id" matches the mechanism.
    • The referential-check sentence matches the consumer read above.
    • The "unchanged" bullets match pin 2 (a caller-sent organization_id is still reported), the array-insert case and the single-posture cases.
    • "The stored row is the same" matches the driver's injectTenantOnInsert refill of the same value.
  • Evidence:

    • Pins run on a walled boot built from the real @objectstack/organizations, SecurityPlugin, ObjectQL and SqliteWasmDriver. They are RED before the fix (the wall file: 3 failed, with the received droppedFields naming organization_id) and GREEN after.
    • Reverse verification ablated the one call to the pre-fix full copy. Exactly the predicted cases went red, in both files, and the restore was proved by blob equality and an empty git diff HEAD.
    • objectql: 7450 tests pass. organizations: 130 pass. Both typechecks exit 0, with the ledger held.
    • No refusal pin exists, so the ADR-0112 code/status check has no subject.
  • Gates:

    • dispatch-gates --ran: 70 derived, 70 run, every one exit 0, at the post-merge head.
    • The artifact-roster block: 51 exit 0, and the 3 PR-context guards exit 0 after pr_create.
  • Deviations — accepted:

  • CI: read by the seat at landing. The seat lands only once every check is green or an expected skip.

Out-of-scope findings — Acceptance notes, not filed: none has a measured public-door reach.

  • Insert now matches update on the caller KEY set, but update also captures VALUES before its chain. No in-tree middleware rewrites a caller-sent value.
  • The organizations fill duplicates injectTenantOnInsert with the same value.
  • objectui FormPage's raw-fetch submit never reads droppedFields.
  • objectui withoutNoOpDrops keeps a never-sent field; this fix removes the server report it would have fired on.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 4, 2026 07:26
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 4, 2026 07:26
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

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

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

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

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

    @objectstack/objectql:test:  FAIL   local  src/engine-insert-static-readonly-strip.test.ts > #14147 — the exemptions, each one load-bearing > [#21682] a write middleware’s fill is not caller-supplied 
      ↳ 失败原因: @objectstack/objectql:test: AssertionError: the platform’s value reaches the driver: expected undefined to be 'stamp' // Object.is equality
    
  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Built console still carries the PUBLISHED @objectstack/spec.
    

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

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

历史信号:

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

分诊清单:

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

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

…h a valid instant (#21682)

The readonly value shape check that landed on main judges every readonly
value left on an insert row. The pin's fill of a readonly datetime was the
string 'stamp', which that check refuses with invalid_date. It now fills a
valid ISO-8601 instant. What the pin proves is unchanged: a middleware fill
lands, and it is not reported.

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT addendum — PR #21701, patch round 1, head b7d2799c8d

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-04T09:05Z. This carries ACCEPT 5977530316 from 994ec65025 to this head. The dev's addendum is 5978353371 on #21682.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 5259a35 Oct 4, 2026
35 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21682-dropped-fields-platform-stamp branch October 4, 2026 09:33
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

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

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

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

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Built console still carries the PUBLISHED @objectstack/spec.
    

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

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

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

分诊清单:

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

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

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