Repository navigation
Commit 7107bde
docs(adr-0120): the per-object batch upsert by id joins the create doors; pin the edit-without-read class (#28797)
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
## 维护者速读
- 改了什么:ADR-0120 的 2026-11-01 修订注记末尾再加一条同日注记,并在其「新建门」清单第一条补一句:带 `id` 的
`POST /data/:object/batch` `upsert`,对调用方读不到的行就是一扇新建门——已被占用但读不到的 `id`
只得到撞键(`409 UNIQUE_VIOLATION`)而不是匹配,没有新建权则得到新建的拒绝;同时记下 driver-memory
开启持久化时重复行会写入开发库这一已接受的残余。dogfood 用例的 `HIDDEN`
表新增「有编辑、无读取」这一类主体(无新建权、有新建权各一);`protocol-batch-data.ts` 的分叉注释补一句。零运行时改动。
- 为什么改:QA 第三轮发现的安全加固(细节按披露裁决不公开)
- 风险与回滚:只动 ADR、一处代码注释和一个测试文件,运行时行为不变;回滚即 revert 本
PR。今后若有人改动该分叉的选臂规则,新用例会转红,这是有意的。
-
先看:`docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md`(风险
hunk:修订注记末尾新增的日期注记,以及清单第一条的补句,核对它与 2026-11-01 修订的裁决范围一致)→
`packages/qa/dogfood/test/upsert-by-id-caller-read.dogfood.test.ts`(`HIDDEN`
表新增的两行,及「不存在的 id」用例里无新建权的分支)→
`packages/metadata-protocol/src/protocol-batch-data.ts`(分叉注释第一条理由的补句)。
- 两分钟验证:`pnpm --filter @objectstack/dogfood exec vitest run
--maxWorkers=1 test/upsert-by-id-caller-read.dogfood.test.ts`,预期 1
个文件、36 个用例全绿(原有 28 个加新增 8 个)。
- 需要你做的:本 PR 触及 `docs/adr/**`(Tier H),请审阅新增注记的措辞后亲手合并或入队。
## What changes
A docs + pin + comment PR. **No runtime change.**
- **ADR-0120, the 2026-11-01 amendment.** A dated note is appended to
the amendment in the record's own amendment form, and the amendment's
first fact (the create-door list) gains one clause pointing at it. The
note records that `POST /data/:object/batch` with `operation: 'upsert'`
is a create door for a row naming an `id` the caller cannot read: the
door's match key is the `id`, read as the caller; a readable row takes
the update arm, an unreadable one takes the create arm with the create's
answer. Contracts it states:
- a held `id` the caller cannot read meets the collision, never a match:
`409 UNIQUE_VIOLATION` on a store that enforces the primary key, the
same row error a batch create naming that `id` carries, identical for a
row hidden inside the caller's organization and one held by another;
import `upsert` matches on the mapping's declared key fields, this door
on the `id` itself;
- a caller without the create grant gets the create's refusal, the same
for a held `id` and for one that names no row;
- on `driver-memory` the create arm lands a second row with that `id`,
and with that driver's persistence configured the duplicate is written
to the development store; recorded as the same accepted residual (the
development and test driver declares no primary key).
Provenance in the note: ruling record `5985885287`, applied to this door
by commit `7b62f0a77`. The status line is unchanged; its existing
2026-11-01 entry already points at the amendment this note extends.
- **Dogfood pin**, `upsert-by-id-caller-read.dogfood.test.ts` (stock
showcase): the `HIDDEN` table gains the edit-without-read principal
class on `showcase_semantic_zoo` (`public_read_write`, so the platform's
`update` ownership floor does not bind it, and no baseline set grants
anything on it), once without and once with `allowCreate`. Each table
row now carries the create's expected answer (`403 PERMISSION_DENIED`
without the create grant, `409 UNIQUE_VIOLATION` with it); the held-id
case asserts the upsert's body equals a batch create naming that id and
the stored row is byte-identical afterwards; the unknown-id case answers
as the create does (created with the grant; without it, the create's
refusal, nothing stored, and the same answer a held id gets). The guard
case reads each new principal's grants through `/security/explain`
(`object_crud` verdict `grants` on update, `denies` on read), its by-id
read (`403`) and its create (`201` / `403`).
- **Fork comment**,
`packages/metadata-protocol/src/protocol-batch-data.ts`: one clause in
the first reason of the reversal paragraph naming the earlier fork's
third outcome, stated as the rule. Comment only.
## Tests
All at head `751c15af6`, exit codes captured before any pipe.
- Dogfood file, under the verify lock, `--maxWorkers=1`: `Test Files 1
passed (1)`, `Tests 36 passed (36)`, lock `VERDICT command-exit 0`. 28
before plus 8 new (two classes, atomic and not, held id and unknown id).
- `@objectstack/metadata-protocol` unit tests that drive `batchData` (9
files: `protocol.upsert-existence`, `protocol.batch-atomic`,
`protocol.batch-row-driver-text`, `protocol.batch-row-http-status`,
`protocol.batch-row-sandbox-refusal`, `protocol.bulk-record-not-found`,
`protocol.readonly-insert`,
`protocol.write-response-internal-fields.tripwire`,
`protocol.bracketed-refusal-opener-absence`), under the verify lock:
`Test Files 9 passed (9)`, `Tests 150 passed (150)`, lock `VERDICT
command-exit 0`.
- Type check: `tsc --noEmit --listFiles` over the dogfood package's own
`tsconfig.json` (what its `typecheck` script runs), under the verify
lock: exit 0, 0 errors; the edited test is in the program (2261 files
listed, 435 of them under `packages/qa/dogfood/test/`).
<details>
<summary>One-shot ablation of the new pin (reverted fork, measured,
restored with proof)</summary>
Driven by `scripts/ablation-replace.mjs` in wrap mode (trap-armed
restore, absolute path), the whole sequence under the verify lock
(`VERDICT command-exit 0`).
- Mutate leg: the fork's caller read replaced by a system-context
existence probe. Anchor `1 -> 0`, replacement `0 -> 1`, blob `f26864308`
to `8037e64df`. `pnpm --filter @objectstack/metadata-protocol build`
exit 0. `ablation-dist-preflight` exit 0: marker present in 2 built
files.
- Run: `Tests 16 failed | 20 passed (36)`. The six new cases are among
them: the held-id case for both new classes, atomic and not (4), and the
unknown-id case of the no-create class, atomic and not (2). The other
ten are the existing hidden-row cases. The with-create class's
unknown-id case stays green, as it should: an unknown id takes the
create arm under either fork.
- Restore leg: blob back to the HEAD blob `f26864308`, `git diff HEAD`
empty for the path, whole-tree `git status --porcelain` empty. Rebuilt
(exit 0); `ablation-dist-preflight --absent` exit 0: marker absent from
all 24 built files, tree clean.
Nothing of the ablation is in the tree or the branch.
</details>
## Acceptance notes
- The import contrast in the note: the import door also matches as the
caller and creates on a miss, so what separates the two doors is the
match key, the mapping's declared key fields there and the `id` itself
here. That is what makes this door's answer to a held-but-unreadable
`id` the collision rather than a match, and the note says it that way so
the sentence holds for both doors.
- Not run locally, by design: `test:repo` (this diff edits none of its
tests and the derivation names none of it; `Test Core` runs it), and the
repo-wide scans CI owns.
- `pnpm check:dual-build-cjs-loads` first answered `PREREQUISITE NOT
MET` (exit 3: seven packages outside the dogfood closure had no
`dist/`). Built those seven with turbo (all cache hits) and re-ran it:
exit 0. Both readings are recorded here; the list below carries the
measured one.
- No changeset. The diff touches `docs/adr/**`, the private
`@objectstack/dogfood` package, and one code comment in
`packages/metadata-protocol/src`. Measured: the comment's text has 0
hits in the built `dist/` (the package's `files[]` ships `dist`),
against the positive control `callerMayReadObject` with 2 hits.
`skip-changeset` is declared at creation; `pnpm check:empty-changeset`
exit 0.
## Verification
Head `1e7ced88b`. It adds one commit on top of `751c15af6`, from
contract review 6563439682: two wording corrections in the ADR-0120
note, prose only and in that one file. The driver-memory bullet now
names the driver's public `persistence` option ("a custom adapter
included") in place of a private field name. The facts-carry sentence
now reads "holds for that arm, which no readable row enters". Re-run at
`1e7ced88b`, exit codes captured before any pipe:
- `node scripts/check-adr-links.mjs` :: exit 0 (967 relative link
destinations resolve)
- `pnpm check:adr-index` :: exit 0 (`docs/adr/README.md is current`)
- `pnpm check:doc-anchors` :: exit 0 (857 internal fragment links
resolve)
- `pnpm check:doc-authoring` :: exit 0
The dogfood run, the unit tests, the type check, the ablation and the
derived gate families below were taken at `751c15af6`. The commit after
it touches neither test file nor any code path.
At `751c15af6`:
- `node scripts/check-adr-links.mjs` :: exit 0 (967 relative link
destinations resolve)
- `pnpm check:adr-index` :: exit 0 (`docs/adr/README.md is current`)
- `pnpm check:doc-anchors` :: exit 0 (857 internal fragment links
resolve)
- `pnpm check:doc-authoring` :: exit 0
- `pnpm check:nul-bytes` :: exit 0, and a control-byte scan of the three
changed files: no hits
- `node scripts/pm/dispatch-gates.mjs --ran` :: exit 0, `63 derived, 63
run, 0 NOT-MEASURED, 0 UNRUN`, every line carrying its exit code
<details>
<summary>Derived gate families (dispatch-gates --commands, reconciled
with --ran)</summary>
Each line is `command :: exit code`, captured before any pipe, at
`751c15af6`.
- `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-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/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/spec run check:duration-unit-keys` :: 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:strictness-ledger` :: exit
0
- `pnpm --filter @objectstack/spec run check:variant-docs` :: 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:cross-package-test-inputs` :: exit 0
- `pnpm check:dispatcher-error-vocabulary` :: exit 0
- `pnpm check:doc-authoring` :: exit 0
- `pnpm check:driver-memory-census` :: exit 0
- `pnpm check:dts-closure` :: exit 0
- `pnpm check:dual-build-cjs-loads` :: exit 0
- `pnpm check:durability-log-level` :: exit 0
- `pnpm check:engine-double-contract` :: exit 0
- `pnpm check:error-status-conformance` :: 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:query-options-erasure` :: 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: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 0
- `pnpm check:undeclared-dep-imports` :: exit 0
- `pnpm check:watch-hint-literal` :: exit 0
- `pnpm check:where-matcher` :: exit 0
- `pnpm check:doc-anchors` :: exit 0
- `pnpm check:empty-changeset` :: exit 0
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 00918c8 commit 7107bde
3 files changed
Lines changed: 127 additions & 22 deletions
File tree
- docs/adr
- packages
- metadata-protocol/src
- qa/dogfood/test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
183 | 183 | | |
184 | 184 | | |
185 | 185 | | |
186 | | - | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
187 | 190 | | |
188 | 191 | | |
189 | 192 | | |
| |||
244 | 247 | | |
245 | 248 | | |
246 | 249 | | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
247 | 281 | | |
248 | 282 | | |
249 | 283 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
404 | 404 | | |
405 | 405 | | |
406 | 406 | | |
407 | | - | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
408 | 410 | | |
409 | 411 | | |
410 | 412 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
22 | 25 | | |
23 | 26 | | |
24 | 27 | | |
| |||
32 | 35 | | |
33 | 36 | | |
34 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
35 | 44 | | |
36 | 45 | | |
37 | 46 | | |
| |||
100 | 109 | | |
101 | 110 | | |
102 | 111 | | |
| 112 | + | |
| 113 | + | |
103 | 114 | | |
104 | 115 | | |
105 | 116 | | |
106 | 117 | | |
107 | 118 | | |
108 | 119 | | |
109 | 120 | | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
110 | 128 | | |
111 | 129 | | |
112 | 130 | | |
| |||
117 | 135 | | |
118 | 136 | | |
119 | 137 | | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
120 | 159 | | |
121 | 160 | | |
122 | 161 | | |
| |||
137 | 176 | | |
138 | 177 | | |
139 | 178 | | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
140 | 183 | | |
141 | 184 | | |
142 | 185 | | |
143 | 186 | | |
144 | | - | |
145 | | - | |
| 187 | + | |
| 188 | + | |
146 | 189 | | |
147 | 190 | | |
148 | 191 | | |
| |||
171 | 214 | | |
172 | 215 | | |
173 | 216 | | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
174 | 228 | | |
175 | 229 | | |
176 | | - | |
177 | | - | |
178 | | - | |
179 | | - | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
180 | 240 | | |
181 | 241 | | |
182 | | - | |
| 242 | + | |
183 | 243 | | |
184 | 244 | | |
185 | 245 | | |
| |||
192 | 252 | | |
193 | 253 | | |
194 | 254 | | |
195 | | - | |
196 | | - | |
| 255 | + | |
| 256 | + | |
197 | 257 | | |
198 | 258 | | |
199 | 259 | | |
200 | 260 | | |
201 | 261 | | |
202 | 262 | | |
203 | | - | |
| 263 | + | |
204 | 264 | | |
205 | 265 | | |
206 | | - | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | 266 | | |
212 | 267 | | |
213 | | - | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
214 | 283 | | |
215 | 284 | | |
216 | 285 | | |
| |||
0 commit comments