Skip to content

docs(adr-0120): record the caller-supplied id collision on the create doors as an accepted residual, document it, pin it - #28694

Merged
os-zhuang merged 3 commits into
mainfrom
claude/docs-create-caller-id-collision
Nov 1, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/docs-create-caller-id-collision

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk

维护者速读

  • 改了什么:ADR-0120 加一条带日期的修订注记(S10 之下),记录「记录 id 是全部署唯一键,新建撞上已有 id 一律同一种拒绝」这一已接受的残余;HTTP 协议文档 Create Record 一节补上调用方自带 id 与撞键的回答;新增两个 dogfood 用例钉住今天的行为。零产品代码改动。
  • 为什么改:QA 第三轮发现的安全加固(细节按披露裁决不公开)
  • 风险与回滚:只动文档、ADR 与测试,不改运行时;回滚即 revert 本 PR。风险在于今后有人改动新建门的拒绝形状时,新用例会转红,这是有意的。
  • 先看:docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md(修订注记全文,对照你 2026-11-01 的「同意」)→ content/docs/protocol/kernel/http-protocol.mdx(Create Record 新增段)→ packages/qa/dogfood/test/create-caller-id-collision-walled.dogfood.test.ts(跨组织一侧的断言)。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/create-caller-id-collision.dogfood.test.ts test/create-caller-id-collision-walled.dogfood.test.ts,预期 2 个文件、30 个用例全绿。
  • 需要你做的:本 PR 触及 docs/adr/**(Tier H),请审阅修订注记措辞后亲手合并或入队。

What changes

A docs + pin PR that records a maintainer ruling. No product code changes.

  • ADR-0120 gains a dated amendment note under its business-requirement matrix (and a pointer on the status line and in the S10 row), in the record's own amendment form. It states that S10's no-oracle promise covers 'organization' uniqueness only; that the record id is the one deployment-global key; and that a create colliding on it answers one refusal, identical for every visibility of the row that holds the id, naming no organization or owner. It records the ruling chain (ruling record 5934879010 rule 2, extended by the maintainer on 2026-11-01 from upsert to every create door and to the same-organization half) and the options considered and rejected.
  • content/docs/protocol/kernel/http-protocol.mdx, Create Record: a caller may supply id; the duplicate-id answer (409 UNIQUE_VIOLATION, nothing written, no owner or organization detail, the same for every visibility, on every create door).
  • Two dogfood pins at the HTTP door, on POST /data/:object, per-object batch create and the cross-object POST /batch:
    • create-caller-id-collision.dogfood.test.ts — same organization, stock showcase (added to the shared-showcase project: no boot options, no metadata or organization writes, assertions scoped to its own rows).
    • create-caller-id-collision-walled.dogfood.test.ts — another organization, walled two-organization boot under isolated and group; the wall itself is measured in a precondition case rather than assumed.
    • Each door: a fresh caller-supplied id lands as given (control); the hidden-row and visible-row collisions return the same status and the same body, whole; both rows are byte-identical after; the body names no organization, owner, or user.

The pins confirm today's behaviour matches the ruling: no answer differed by visibility and none named an organization, so there was nothing to stop and report.

Tests

  • pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=1 on the two new files, under the verify lock: Test Files 2 passed (2), Tests 30 passed (30), lock VERDICT command-exit 0.
  • NOT MEASURED locally: the whole shared-showcase project, which the same-organization file joins. Reason: five verify-lock attempts answered 99 (queue-timeout) over about 45 minutes behind a sibling dispatch's full-suite hold; CI's dogfood gate runs the project. The new file itself ran inside that project (on its own boot of the shared stack) and passed in the run above.
  • Type check, narrowed and declared: tsc -p over a temporary config that extends the package's tsconfig.json with files set to the two new tests (deleted afterwards, tree clean): exit 0, both files in the program (--listFiles, 1113 files), zero errors. The package-wide pnpm --filter @objectstack/dogfood typecheck is NOT MEASURED locally, for the same lock-queue reason; CI's TypeScript Type Check runs it.
  • No ablation: the diff changes no tested behaviour; it pins today's. The precondition cases are what keep the identity assertions non-vacuous (the hidden row is measured hidden; the fresh-id control is measured honoured).

Acceptance notes

  • The walled half uses bootStack(..., { multiTenant: 'posture-only' }). That option's docblock in packages/verify/src/harness.ts says the stand-in "stamps no organization_id and scopes no query"; on this boot the row is stamped with the creating organization and another organization's member reads 404 for it, both asserted in the precondition case. Noted, not filed: a docblock reading, outside this PR's scope.
  • Doors outside the three pinned here (createMany, import insert and upsert, clone overrides.id) are named in the ADR note and the doc; they are not pinned by this PR.
  • No changeset: the diff touches docs/adr/**, content/docs/** and the private @objectstack/dogfood package, none of which ships in any published package's files[]. Declared with skip-changeset at creation.

Verification

Head 393827105. It adds two commits on top of c2c7d9183, both from the contract-tier review, both prose-only in the same two files:

  • 41e8ea516: the ADR-0120 note's second fact and the Create Record paragraph in http-protocol.mdx now name the store precondition. The 409 holds on a store that enforces the primary key (the SQL-family drivers); driver-memory declares none.
  • 393827105: the same two hunks call driver-memory the development and test driver, not a production store. This replaces a descriptor that its README does not support.

Re-run at 393827105, exit codes captured before any pipe:

  • node scripts/check-adr-links.mjs :: exit 0 (965 relative link destinations resolve)
  • pnpm check:adr-index :: exit 0 (docs/adr/README.md is current)
  • pnpm check:doc-anchors :: exit 0 (855 internal fragment links resolve)
  • pnpm check:doc-authoring :: exit 0

The dogfood run, the narrowed type check and the derived gate families below were taken at c2c7d9183. The two commits after it touch neither test file and no gate-read path outside those two prose files. Not run locally by design: test:repo (this diff edits none of its tests, and the derivation names none of it) and the repo-wide scans CI owns.

Derived gate families (dispatch-gates --commands, reconciled with --ran)

Each line is command :: exit code, captured before any pipe.

  • node scripts/check-adr-links.mjs :: exit 0
  • node scripts/check-adr-links.mjs --self-test :: exit 0
  • node scripts/check-closing-keyword-parity.mjs :: exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
  • node scripts/check-comment-mask-adoption.mjs :: exit 0
  • node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
  • node scripts/check-comment-mask-corpus.mjs :: exit 0
  • node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
  • node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
  • node scripts/check-docs-section-name.mjs :: exit 0
  • node scripts/check-docs-section-name.mjs --self-test :: exit 0
  • node scripts/check-dts-emitted.mjs --self-test :: exit 0
  • node scripts/check-issue-citations.mjs :: exit 0
  • node scripts/check-plugin-teardown-shape.mjs :: exit 0
  • node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
  • node scripts/check-section-landing-index.mjs :: exit 0
  • node scripts/check-section-landing-index.mjs --self-test :: exit 0
  • node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
  • node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
  • node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
  • node scripts/docs-audit/check-affected-docs.mjs :: exit 0
  • node scripts/docs-audit/check-drift-comment.mjs :: exit 0
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
  • pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
  • pnpm --filter @objectstack/spec run check:docs :: exit 0
  • pnpm --filter @objectstack/spec run check:empty-state :: exit 0
  • pnpm --filter @objectstack/spec run check:liveness :: exit 0
  • pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 — NOT MEASURED, PREREQUISITE NOT MET: reads packages/client-react/dist declarations, outside the dogfood build closure built here; CI runs it in Lint & Repo Gates / TypeScript Type Check
  • pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
  • pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
  • pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
  • pnpm check:adr-anchors :: exit 0
  • pnpm check:adr-index :: exit 0
  • pnpm check:adr-symbol-anchors :: exit 0
  • pnpm check:ci-filter-parity :: exit 0
  • pnpm check:closed-ceremony-text :: exit 0
  • pnpm check:corpus-claim-drift :: exit 0
  • pnpm check:cross-package-test-inputs :: exit 0
  • pnpm check:doc-anchors :: exit 0
  • pnpm check:doc-authoring :: exit 0
  • pnpm check:doc-frontmatter :: exit 0
  • pnpm check:docs-audit-scope :: exit 0
  • pnpm check:docs-redirects :: exit 0
  • pnpm check:docs-single-h1 :: exit 0
  • pnpm check:docs-spec-enumerations :: exit 0
  • pnpm check:docs-transcript-drift :: exit 0
  • pnpm check:driver-memory-census :: exit 0
  • pnpm check:dts-closure :: exit 0
  • pnpm check:dual-build-cjs-loads :: exit 3 — NOT MEASURED, PREREQUISITE NOT MET: reads the dist of seven packages outside the dogfood build closure built here; CI runs it in Lint & Repo Gates / TypeScript Type Check
  • pnpm check:engine-double-contract :: exit 0
  • pnpm check:gitlink-declared :: exit 0
  • pnpm check:issue-citations :: exit 0
  • pnpm check:keyed-text-bounds :: exit 0
  • pnpm check:lean-entry-closure :: exit 0
  • pnpm check:logger-receiver-detach :: exit 0
  • pnpm check:nul-bytes :: exit 0
  • pnpm check:objectql-double-limit :: exit 0
  • pnpm check:org-identifier :: exit 0
  • pnpm check:page-declaration-shape :: exit 0
  • pnpm check:platform-object-tenancy-census :: exit 0
  • pnpm check:pm-governed-merges :: exit 0
  • pnpm check:pm-prior-rulings :: exit 0
  • pnpm check:published-files :: exit 0
  • pnpm check:published-readme-links :: exit 0
  • pnpm check:query-options-erasure :: exit 0
  • pnpm check:react-page-adapter-contract :: exit 0
  • pnpm check:refd-timer-probe :: exit 0
  • pnpm check:registry-log-declared :: exit 0
  • pnpm check:rest-log-spy-declared :: exit 0
  • pnpm check:role-word :: exit 0
  • pnpm check:skill-identifier-liveness :: exit 0
  • pnpm check:slot-lookup :: exit 0
  • pnpm check:sourcemap-no-sources-content :: exit 0
  • pnpm check:system-context-census :: exit 0
  • pnpm check:test-source-alias :: exit 0
  • pnpm check:tier-file-adoption :: exit 0
  • pnpm check:type-check-coverage :: exit 0
  • pnpm check:type-check-debt :: exit 3 — NOT MEASURED, PREREQUISITE NOT MET: its own closure build never got a turn on the shared verify lock; CI runs it in Lint & Repo Gates / TypeScript Type Check
  • pnpm check:undeclared-dep-imports :: exit 0
  • pnpm check:vendor-version-stamps :: exit 0
  • pnpm check:watch-hint-literal :: exit 0
  • pnpm check:where-matcher :: exit 0

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE (FILE = the lines above):

✓ dispatch-gates --ran: 82 derived famil(ies) accounted for — 79 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3).

Generated by Claude Code

…d residual, document it, and pin it at the HTTP door

ADR-0120 gains a dated amendment under its business-requirement matrix:
S10's no-oracle promise covers 'organization' uniqueness only; the record
id is the one deployment-global key, and a create colliding on it answers
409 UNIQUE_VIOLATION, writes nothing, names no organization or owner, and
is the same for every visibility of the holding row. It extends ruling
record 5934879010 rule 2 from upsert to every create door and to the
same-organization half, and lists the options considered and rejected.

http-protocol.mdx Create Record documents the caller-supplied id and the
collision answer. Two dogfood files pin today's behaviour on three create
doors: same organization on the stock showcase, another organization on
a walled two-organization boot. No product code changes.

Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 1, 2026
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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

Coarse fallback — 2 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 da6f0ce35da60bf81e79890b227815a989f9b165 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json da6f0ce35da60bf81e79890b227815a989f9b165

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c2c7d918393152c508ea3f988a20d274516843ce
Local-runs: none
Scope: docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md + content/docs/protocol/kernel/http-protocol.mdx, packages/qa/dogfood/test/create-caller-id-collision.dogfood.test.ts, packages/qa/dogfood/test/create-caller-id-collision-walled.dogfood.test.ts, packages/qa/dogfood/vitest.config.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-memory/src/memory-driver.ts, packages/types/src/data-error-classification.ts, packages/types/src/unique-violation.ts, packages/objectql/src/engine.ts, packages/objectql/src/duplicate-record-error.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol-batch-data.ts, packages/core/src/utils/import-runner.ts, packages/core/src/security/tenant-layer0-verdict.ts, packages/verify/src/harness.ts, scripts/adr-index.mjs, .github/workflows/ci.yml

Read-only, from the net diff origin/main...c2c7d9183, the PR body, the two ruling records (5934879010 on #21185, 5985885287 on #21771), the sibling branch claude/harden-upsert-by-id-visibility (its changeset and protocol-batch-data.ts hunk), and the head's check-runs as read at 2026-11-01T14:21Z. Per the 2026-10-29 security-hardening disclosure ruling this record names positions and contracts only.

① Derived judgments

Face: governed rules text (docs/adr/**). No published schema moves; no product code moves. Each accept-set or public-surface claim the diff makes, judged:

  1. Status line — right. **Status**: Accepted (…) · **Amended** (2026-11-01, …) keeps Accepted as the parsed keyword (scripts/adr-index.mjs takes the EARLIEST keyword in the value, statusKeywordOf), which is the register's form for an in-force record with an amendment (ADR-0005, ADR-0009 use the same · Amended suffix). The index cell truncates at 64 characters before the new text, so docs/adr/README.md is not stale; the dev's check:adr-index exit 0 agrees.
  2. Amendment form — right. A dated blockquote > **Amendment (date, ref) — …** under the business-requirement matrix is this ADR's own existing form (the 2026-08-22 amendment at its line 356) and the register's (ADR-0131 line 248, ADR-0090 lines 677/738 use the same > **Amended (date, ruling of record …)** shape). AGENTS.md PD 13 asks for a new ADR or an amended status line; the PR does the latter plus the pointer in the S10 row. Sanctioned.
  3. Ruling fidelity — right, and no more. The note states exactly: S10's no-oracle promise is scoped to 'organization' uniqueness (and field-level true, its synonym); id is the one deployment-global key; a create colliding on it answers one refusal, identical for visible, in-organization-hidden and cross-organization holders, naming no organization or owner; the signal is bounded by id unguessability. It cites rule 2 of 5934879010 verbatim where it binds (no new code, no detail naming the holder's organization) and records the 2026-11-01 extension from upsert to every create door and to the same-organization half. Ruling 5985885287 is left untouched with the correct reason (a held id has no "nonexistent" answer to give). Rejected options a–e are recorded as rejected, not promised.
  4. S10 scoping claim — right. id is not a scoped unique declaration; the primary key is materialized verbatim under every posture and no COALESCE(organization_id, …) key part reaches it (D3 governs declared 'organization' indexes and field-level unique, not the PK). The amendment's cross-reference to §Posture portability ("becomes a cross-customer existence oracle (S10)" for a 'global' unique, ADR line 206) is the same oracle, accurately cited.
  5. Entropy claim — right. packages/drivers/driver-sql/src/sql-driver.ts:266 DEFAULT_ID_LENGTH = 16, minted by nanoid(DEFAULT_ID_LENGTH) at its three mint sites (6463, 8216, 8629); nanoid's default alphabet is 64 symbols, so 16 characters = 96 bits. The importer pre-assigns randomUUID() when the row carries none (packages/core/src/utils/import-runner.ts:1243, "An id the user supplied explicitly is respected"). The pins' freshId (12 random bytes, base64url) reproduces the minted shape.
  6. Door inventory — each door's answer read in code:
    • POST /data/:object: engine.insert honours a caller id; the driver's primary-key violation leaves the engine as the An application told to rely on a unique index cannot recognise the violation: ObjectQL rethrows the raw driver error and isUniqueViolationError is not reachable from an app #14095 DuplicateRecordError envelope (engine.ts ~15993) and the HTTP mapping answers 409 { error: 'A record with this value already exists', code: 'UNIQUE_VIOLATION', developerMessage, field?, object } (data-error-classification.ts:1058-1075). developerMessage is buildDuplicateMessage(objectLabel, fieldLabel) — object and field only, never a value, owner or organization — so the body is identical across holders. Right.
    • POST /data/:object/batch operation: 'create': the row's errors[0].code is UNIQUE_VIOLATION and httpStatus reads the declared 409 (protocol.ts 3533 docblock). Right.
    • cross-object POST /batch: one engine transaction, rolled back on the first failing operation (http-protocol.mdx Batch section, unchanged). Right.
    • createMany (POST /data/:object/createMany, rest-server.zod.ts:601): createManyData is one engine.insert(object, records[]); on driver-sql the batch is one multi-row bulkCreate statement (sql-driver.ts 8616 docblock: "the batch is ALREADY all-or-nothing"), so a collision answers a whole-request 409 and writes nothing there. Right on the persistent SQL drivers; see 7.
    • import insert: no existence lookup (writeMode !== 'insert' gates findExisting), the supplied id is respected and a collision is the row's UNIQUE_VIOLATION (import-runner.ts:539). import upsert: findExisting runs through findArgsBase, which spreads the caller's context (830-834), by matchFields; a miss creates. "An upsert whose match, run as the caller, finds no row creates" — right.
    • clone overrides.id: CLONE_STRIP_FIELDS strips the source id, overrides are assigned after the strip (protocol.ts 14255-14300), and the same engine.insert honours it. Right.
  7. Wrong by omission (should-fix, not must-fix): the store precondition. The note's facts — "answers 409 UNIQUE_VIOLATION and writes nothing" — are stated for every door without naming the store. driver-memory enforces no primary key (memory-driver.ts:476 docblock; create() stores data.id || generateId, line 845): a create naming a held id lands a second row there, on every door. Ruling 5934879010 itself put driver-memory out of scope. The binding sentence (no visibility-shaped answer, no holder named) still holds on that store, so this is a qualifier the record owes, not a false promise about a production door: one clause, "on a store that enforces the primary key — every persistent driver; driver-memory declares none". The same clause covers the engine's own note that a driver without a statement-level transaction may apply a createMany batch partially (engine.ts ~15985).
  8. Sibling consistency (should-fix on whichever lands second). The note lists the create doors and names import's upsert arm as a create door; it does not name POST /data/:object/batch operation: 'upsert'. On main today that door forks on system-context existence (probeRecord, protocol-batch-data.ts 353), so a hidden held id takes the UPDATE arm and never creates — the note is accurate for main. claude/harden-upsert-by-id-visibility (changeset batch-upsert-by-id-caller-read.md, patch, Clause-②: no) makes an unreadable row take the INSERT arm, "so the row answers exactly as a create naming that id answers", and its code comment cites this 2026-11-01 extension as its record. After it lands the note is incomplete, not contradicted. The door belongs in the list under the same parenthetical as import's upsert; adding it now would describe main falsely, so: the later of the two PRs adds the sentence.
  9. http-protocol.mdx Create Record paragraph + 409 example — right. Status, code, body keys and the unnamed sentence match the mapping in 6. field: "id" is best-effort as the paragraph says (SQLite's constraint failed: table.id and Postgres's Key (id)= parse to the column; MySQL's for key 'PRIMARY' does not) and the paragraph defers to the Delete section's Constraint Violations note on that, consistently. "A create never overwrites — change an existing record with Update Record" is consistent with the Update paragraph's security: 服务端 readonly 字段在 UPDATE 时未强制(可被用户上下文覆盖) #2948 rule (id is read-only on update, honoured on create). The 405/400 sections are untouched and not contradicted. Anchors #update-record, #delete-record, #batch-operations resolve to existing headings; Check Documentation Links is green. The example omits developerMessage, as every existing 409 example on the page does — a convention, not drift.
  10. Pins — non-vacuous, and they prove what they claim. Each file: a PRECONDITION case measures the hiding (the caller / B reads the held row as 404, the owner / A and the operator read it as 200, both rows in one organization or in two distinct ones with the stamped organization_id asserted); a CONTROL per door proves a fresh caller id lands as given with the expected owner / organization; the identity case asserts refusalCode(onOwn) === 'UNIQUE_VIOLATION' FIRST (so two undefined bodies cannot pass), then same status, toEqual on the whole body, and byte-identical rows after via JSON.stringify of system-context reads; the naming case asserts the body contains none of org ids, slugs, user ids, emails, nor the four key spellings. Per-object batch asserts httpStatus: 409 and succeeded: 0.
  11. Walled boot — the wall is real on this boot; the dev's docblock flag is confirmed. multiTenant: 'posture-only' registers only the org-scoping stand-in (harness.ts 1090-1102). The stamp is the engine's injectTenantOnInsert ("keeps the active organization as tenantId in EVERY posture", engine.ts ~7341) and the wall is the Layer-0 verdict off tenancy.posture (postureEnforcesWall, tenant-layer0-verdict.ts:131), which the stand-in makes non-degraded isolated — neither lives in the service the docblock says is absent. So the harness docblock (harness.ts 77-80, 248-257: "stamps no organization_id and scopes no query … would assert nothing and pass") is stale since ADR-0131 D8/D11. The pin does not lean on it: the precondition measures the 404 and would FAIL on an unwalled boot. See ③.
  12. vitest.config — right. The same-organization file joins the shared-showcase exact list; the walled file falls to the isolated glob project (derived population, no second list to keep in step).
  13. Disclosure — right. ADR, page and pin prose name doors, codes and contracts; the only "recipe" is the public API's documented behaviour.

② Semver level

skip-changeset, no changeset — correct. The diff touches docs/adr/**, content/docs/** and packages/qa/dogfood/** ("private": true); nothing publishes from a released package, which is exactly what AGENTS.md Post-Task step 3 reserves the label for. No Clause-②: line is owed by a diff that declares no published change; Check Changeset and PR-body guards are green on the head. Check-runs at reading: no failure; Governed Surface Queue Guard, Dogfood Regression Gate, Check Changeset, Check Documentation Links, Build Core, Test Core 5-8, Type Check · source gates / debt ledger, Lint scoped families green; Type Check · workspace / consumer gates, Test Core 1-4, Lint · repo & tooling / docs & code guards / CI, release & engine gates, Build Docs still in progress — landing waits for green per the seat's ② gate, this record does not.

③ Boundary flags

  • Dev flag — harness docblock drift: confirmed (①.11). Not this PR's scope (packages/verify is a published package; a docblock correction is its own patch). Card it: BootOptions.multiTenant and SimulatedOrgScopingPlugin docblocks in packages/verify/src/harness.ts describe a pre-ADR-0131 world; the honest sentence is that the stand-in flips the posture and the engine's stamp plus the Layer-0 verdict then wall by posture — the proof the docblock says does not exist here now does.
  • Dev flag — unpinned doors (createMany, import insert/upsert, clone overrides.id): answered. Read in code (①.6); each answers the collision as the ADR states on the persistent drivers. They stay unpinned; a low-priority follow-up pin card is reasonable, not owed before landing.
  • Dev open question — fleet-write pr_create read-back prints no labels line: out of scope. PM tooling, not this PR. The labels are on the PR as read over REST (documentation, size/m, tests, skip-changeset); nothing on this PR depends on the read-back.
  • CI shape the seat should know: the dogfood shards are skipped on pull_request by design (ci.yml dogfood job if: excludes pull_request and merge_group; dogfood-gate is the always-run aggregator). The pins' only evidence at this head is the dev's local run (2 files, 30 cases, verify-lock VERDICT command-exit 0); CI runs them on the push to main. Repo-standard, not a defect of this PR.
  • Sibling ordering (①.8): claude/harden-upsert-by-id-visibility is in flight on the same ruling; whichever lands second adds the per-object batch upsert door to the ADR note's list. The seat holds the ordering.
  • Author is objectstack-fleet[bot] — no login note owed.

④ Design judgment

docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md, against its first consumer and its execution plan:

  • First consumer: the Create Record paragraph in http-protocol.mdx and the two dogfood pins. The paragraph restates the note's contract without widening it (same doors, same code, same no-naming promise, same deference to best-effort field); the pins assert the identity of the answers and the untouched rows at the three HTTP doors, under private OWD in one organization and under isolated and group across two. Consumer and record agree.
  • Execution plan: none is owed — the amendment accepts a residual and changes no code; "What this binds" names the bar a reversal must clear (import round-trips without an exemption that re-opens the door), which is the bar options a–e failed in code (import-runner.ts:1243 respects a supplied id; an unheld id answers 201, so no refusal shape can close the signal — option e). The reasoning holds against the tree.
  • Tried to break it: (a) a store without a primary key — found, ①.7, qualifier owed, binding sentence survives; (b) a create door that forks on visibility — the per-object batch upsert on main does, but it is not a create door for a held id there, and the sibling that makes it one answers this contract — ①.8; (c) a body that differs by holder — developerMessage carries object and field labels only; (d) a vacuous wall — the precondition measures it, ①.11.

Must-fix: none.
Should-fix: (1) ①.7 — one clause naming the store precondition ("on a store that enforces the primary key — every persistent driver; driver-memory declares none") in the note's second fact, so the governed text does not state a fact false on a shipped non-production driver; (2) ①.8 — the later of this PR and claude/harden-upsert-by-id-visibility names the per-object batch upsert arm among the doors, beside import's upsert.

Implemented-by: claude/docs-create-caller-id-collision
Reviewed-by: session_017xRBcVy4VXmkQjn2MGeaWk

VERDICT: PASS


Generated by Claude Code

The 409 UNIQUE_VIOLATION answer to a create colliding on id holds on a
store that enforces the primary key: the SQL-family drivers do, while
driver-memory (the dev and edge fallback) declares no primary key and
lands a duplicate id. The ADR-0120 amendment note's second fact and the
http-protocol.mdx Create Record paragraph now say so, so neither states
anything false on a shipped driver.

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

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 41e8ea51640fad01226cf3abf25b9fc8db067bde
Local-runs: none
Scope: docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md + content/docs/protocol/kernel/http-protocol.mdx, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts, packages/drivers/driver-turso/src/turso-driver.ts, packages/drivers/driver-turso/src/remote-transport.ts, packages/types/src/unique-violation.ts, packages/drivers/driver-memory/src/memory-driver.ts, packages/drivers/driver-memory/README.md

Delta review by the same reviewer, per contract-review.md: one findings-only commit 41e8ea516 on top of the reviewed head c2c7d9183 (record 6559209282, PASS), applying that record's should-fix 1 (①.7). Read-only: the range c2c7d9183..41e8ea516 (two files, two hunks), the cited driver code on the live tree, and the head's check-runs as read at 2026-11-01T14:26Z. Everything outside the range stands as judged in 6559209282.

① Derived judgments

The delta adds one qualifier to the ADR note's second fact and to the page's Create Record paragraph: "on a store that enforces the primary key". Each sentence it introduces, judged:

  1. "the SQL-family drivers do (driver-sql, and driver-sqlite-wasm and driver-turso, which extend it)" — right. SqliteWasmDriver extends SqlDriver (sqlite-wasm-driver.ts:67) and TursoDriver extends SqlDriver (turso-driver.ts:1815). driver-sql emits the id column as the primary key in its DDL (sql-driver.ts:10550, 11091, 12426 — table.string('id').primary()), so every table the family creates refuses a duplicate id at the store. driver-turso overrides create (3026) and bulkCreate (3751) for its remote face, but both faces write the same libsql table whose DDL carries that key, and the duplicate's refusal — SQLite's UNIQUE constraint failed: t.id, the same sentence a primary-key collision raises — is classified by message (unique-violation.ts:210), so the remote face answers the same 409 UNIQUE_VIOLATION. Held on both faces.
  2. "driver-memory … declares no primary key, so a duplicate id lands there (its docblock in packages/drivers/driver-memory/src/memory-driver.ts)" — right. The docblock at 470-484: "there is no primary key … Two rows with the same id still land — through create and bulkCreate alike, unless id itself declares unique or a declared index lists it". Cited accurately, and the citation is a position, not a recipe.
  3. The scoping of the claim — right, and no more. The qualifier names the family that enforces the key and the one store that does not; it says nothing of driver-mongodb or driver-odata, which is correct silence (mongodb's duplicate-key mapping unmeasured, odata delegating to the remote), and exactly the narrowing the PM seat describes rather than "every persistent driver". The binding sentence ("no create door answers differently by the holder's visibility; none names the holder") is unchanged and still holds on every store, driver-memory included.
  4. Wrong by one appositive (should-fix): "the dev and edge fallback". Both hunks call driver-memory "the dev and edge fallback" (ADR) / "a dev and edge fallback" (page). The repo does not support either word: its README names the driver for "development, unit tests, Storybook, and browser MSW mocks" and marks production ❌ (README lines 3, 83, 87), and the one place "edge" appears there attributes edge deployments to driver-turso (line 93). Nothing serves driver-memory as a fallback today — serve.ts:3349 records that it used to build the in-memory driver by default, past tense. The contract clause is accurate; the descriptor is not. One edit: "the development and test driver, not a production store (its README)" — or drop the appositive, since the docblock citation already carries the fact.
  5. Nothing else moved. Status line, S10 row, the door list, the ruling paragraph, the options, "What this binds", the pins and the vitest entry are byte-identical to the reviewed head; the page's 409 example and the paragraph's remaining sentences are unchanged. Anchors untouched; Check Documentation Links green on the head.

② Semver level

Unchanged: skip-changeset, no changeset — still correct; the delta touches docs/adr/** and content/docs/** only. Check-runs at reading: no failure; Governed Surface Queue Guard, Dogfood Regression Gate (aggregator), Check Documentation Links, Check PR Size, PR-body guards, Flag docs affected, Spec property liveness, Type Check · debt ledger, Lint · PM dispatch-gates and Lint · scoped · tooling self-tests green; Build Core, Build Docs, Test Core 1-8, Type Check · workspace / source gates / consumer gates, the remaining Lint families and Live E2E UI in progress. Landing waits for green per the seat's ② gate.

③ Boundary flags

  • The flags of 6559209282 stand (harness docblock card; unpinned doors; fleet-write read-back out of scope; dogfood shards skipped on pull_request by design; sibling claude/harden-upsert-by-id-visibility adds the per-object batch upsert door to the list if it lands second — should-fix 2 of that record, not applied by this delta and not owed by it).
  • Author objectstack-fleet[bot] — no login note.

④ Design judgment

The qualifier makes the ADR's recorded fact true on the tree: it names the stores on which the contract is a store-level refusal and the one shipped store on which it is not, without widening the ruling or adding a store the code does not prove. The page restates the same scoping in one clause, consistently with the ADR. The only sentence the delta introduces that the tree does not support is the driver-memory descriptor in ①.4, a characterisation rather than a contract: a should-fix, not a false promise about any door.

Must-fix: none.
Should-fix: ①.4 — replace "the dev and edge fallback" / "a dev and edge fallback" with what the driver's README says of it (development and tests, not production), in both hunks.

Implemented-by: claude/docs-create-caller-id-collision
Reviewed-by: session_017xRBcVy4VXmkQjn2MGeaWk

VERDICT: PASS


Generated by Claude Code

…iver

The precondition clause called driver-memory a dev and edge fallback.
Its README lists development, unit tests, Storybook and MSW mocks and
marks production unsupported, and edge is driver-turso's role. Both the
ADR-0120 amendment note and the http-protocol.mdx Create Record
paragraph now call it the development and test driver, not a
production store.

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

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 393827105956c4e4040116fe0a3471b899a0556d
Local-runs: none
Scope: docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md + content/docs/protocol/kernel/http-protocol.mdx, packages/drivers/driver-memory/README.md, packages/drivers/driver-memory/src/memory-driver.ts

Delta review by the same reviewer, per contract-review.md: one findings-only commit 393827105 on top of 41e8ea516 (delta record 6559270010, PASS; full record 6559209282 on c2c7d9183, PASS), applying the one should-fix of 6559270010 (its ①.4). Read-only: the range 41e8ea516..393827105 (two files, one hunk each), the driver's README and docblock on the live tree, and the head's check-runs as read at 2026-11-01T14:29Z. Everything outside the range stands as judged in the two records above.

① Derived judgments

  1. The appositive — right now. "the dev and edge fallback" (ADR) and "a dev and edge fallback" (page) are replaced by "the development and test driver, not a production store", which is what the driver's README states (line 3: development, unit tests, Storybook, browser MSW mocks; line 83 ✅ development, unit tests, CI, Storybook; line 87 ❌ production). No word the tree does not support remains in either sentence; "edge" and "fallback" are gone.
  2. The contract clause — unchanged and still right. "on a store that enforces the primary key — the SQL-family drivers do (driver-sql, and driver-sqlite-wasm and driver-turso, which extend it); driver-memory … declares no primary key, so a duplicate id lands there (its docblock …)" is byte-for-byte the clause judged right in 6559270010 ①.1–①.3, re-wrapped only. The docblock citation (memory-driver.ts:470-484) still says exactly that.
  3. The re-wrap — content-neutral. The page hunk re-joins the wrapped line so the paragraph reads as one sentence; the ADR hunk re-breaks three lines. No sentence, code span, link or anchor is added or removed; the 409 example and the rest of the paragraph are untouched.
  4. Nothing else moved. Status line, S10 row, door list, ruling text, options, "What this binds", pins and the vitest entry are byte-identical to the reviewed heads.

② Semver level

Unchanged: skip-changeset, no changeset — still correct; the delta touches docs/adr/** and content/docs/** only. Check-runs at reading: no failure; Governed Surface Queue Guard, Check Documentation Links, Check PR Size, PR-body guards, Spec property liveness, Auto Label green; the Type Check, Lint, Build and Test Core families still queued or in progress on this head. Landing waits for green per the seat's ② gate.

③ Boundary flags

  • The flags of 6559209282 and 6559270010 stand (harness docblock card; unpinned doors; fleet-write read-back out of scope; dogfood shards skipped on pull_request by design; sibling claude/harden-upsert-by-id-visibility adds the per-object batch upsert door to the ADR's list if it lands second — should-fix 2 of the first record, not owed by this delta).
  • Author objectstack-fleet[bot] — no login note.

④ Design judgment

The delta changes a descriptor, not a contract: the store precondition the amendment records is as judged in 6559270010, and the one sentence that record found unsupported now says what the driver's own README says. No should-fix remains open on the governed text from this reviewer's records other than the sibling-ordering item in ③, which is the sibling's to carry.

Must-fix: none.
Should-fix: none in this range.

Implemented-by: claude/docs-create-caller-id-collision
Reviewed-by: session_017xRBcVy4VXmkQjn2MGeaWk

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk

ACCEPT → head 393827105956c4e4040116fe0a3471b899a0556d · 2026-11-01T14:46Z

维护者速读

  • 改了什么:在 ADR-0120 的 S10 之下加一条带日期的修订注记,记录一个已接受的残余:记录的 id 是全部署唯一的键;用已存在的 id 新建时,所有新建入口都返回 409 UNIQUE_VIOLATION,不写入,也不点出所属组织或所有者。这个回答对可见的行、同组织内被隐藏的行、别的组织的行都一样,前提是存储会强制主键(SQL 系驱动是这样)。http-protocol.mdx 的 Create Record 一节同步补充了这一点。新增两个 dogfood 用例钉住这个行为。不改产品代码。
  • 为什么改:QA 第三轮发现的安全加固(细节按披露裁决不公开)。
  • 风险与回滚:只改文档、ADR 和测试,运行时不变。回滚即 revert 本 PR。
  • 先看:docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md 的 Amendment 段,以及 content/docs/protocol/kernel/http-protocol.mdx 的 Create Record 段。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/create-caller-id-collision.dogfood.test.ts test/create-caller-id-collision-walled.dogfood.test.ts,预期 30/30 通过。
  • 需要你做的:本 PR 碰受管文本(docs/adr/**,Tier H),需要你在 GitHub 批准(APPROVED review)后由席位落地。

席位意见:收。本 PR 落实维护者 2026-11-01 在 PM 会话里对 C1 的裁定(选 f,原话「同意」):把裁决 5934879010 细则 2 延伸到所有新建入口,并覆盖同组织内被隐藏的那一半。

席位核对

  1. 形态:draft,目标 main。按披露裁决不建卡,正文没有关闭关键词。正文首行是签名,正文由席位按 dev 暂存稿写入。
  2. 前提实测:两个 pin 共 30 个用例,在验证锁下跑出 exit 0。前置用例实测了"行确实被隐藏",对照组证明调用方自带的 id 被采纳;验证内容包括整个应答体相同、行逐字节不变、应答体不点出组织或所有者。
  3. 门禁:本地派生的门禁共 82 项,79 项 exit 0,3 项因缺前置条件未测(已在正文申报)。本 head CI 共 54 项:38 success、16 skipped、0 失败;Lint & Repo Gates、TypeScript Type Check、Test Core、Dogfood Regression Gate、Governed Surface Queue Guard 都是 success。
  4. 契约面:碰到受管文本 → 按 CONTRACT_REVIEW_TIER 做了完整复核 6559209282(PASS),随后同一复核者对增量又复核两轮:6559270010 和 6559308871,都是 PASS,当前 head 上没有遗留问题。两条 should-fix 都已应用:存储前提的限定语,以及 driver-memory 的描述用词。
  5. 范围外去向:
  6. 落地:受管面,Tier H(人合),490 行 → 席位执行 pr_ready;维护者批准后再 automerge_enable。

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 14:47
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit e1fede6 Nov 1, 2026
55 checks passed
@os-zhuang
os-zhuang deleted the claude/docs-create-caller-id-collision branch November 1, 2026 15:55
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 skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants