Repository navigation
docs(adr): ADR-0066 ⑧'s when note names the record-scoped field projection as a third, row-aware reader (#26594, ruling B) - #27152
Conversation
…ojection as the third, row-aware reader of a field permission's `when` One note under ⑧, directly below the 2026-10-26 note: GET /api/v1/data/:object/:id/field-access (SecurityPlugin.getRecordFieldAccess) judges each field permission's `when` on the stored row with the write gate's own computeForbiddenFieldWrites; the row-blind readers are unchanged and still apply a conditioned entry as its lock; no reading is wider than the gate's. No other line of the ADR moves. Claude-Session: https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read at 2026-10-28T08:29Z by the ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
维护者速读 — 席位终稿 · PR #27152 · head
|
Fixes #27075
Clause-②: no
Metadata-protocol: none
维护者速读
docs/adr/0066-unified-authorization-model.md⑧ 下新增一条 2026-10-28 的注记(全文 +2 行,其一为空行),点名「记录级字段投影」GET /api/v1/data/:object/:id/field-access是字段权限when的第三个读者、且是按存储行判定的那一个;ADR 其余每一行一字未动。whenlock on the stored row #27063(card Record form: a row-conditioned field lock (when) disables the field on every row, though the write gate admits it on rows the condition does not name #26594,裁决记录 6457467702 字母 B)已把这个按行判定的读者落在 main 上,但 ADR 的 2026-10-26 注记只点名了写门与不看行的读者;读 ADR 的人会以为没有任何读者看得到行。现在三类读者在 ADR 里都有名字。docs/adr/**不发布,打skip-changeset);回滚即 revert 本 PR 的单个 commit。docs/adr/0066-unified-authorization-model.mdL135–L136(新注记);核对其三句话与packages/plugins/plugin-security/src/security-plugin-field-readers.tsL233–L248、packages/rest/src/rest-route-ledger.tsL364–L366 一致。git diff origin/main...HEAD --numstat,预期恰好一行2 0 docs/adr/0066-unified-authorization-model.md。docs/adr/**受管面)—— 一次GOVERNED_APPROVERS账号的 APPROVED review;批准后由席位落地,不需要你点合并。What changes
One dated note under ⑧ of ADR-0066, placed directly after the 2026-10-26 note (ruling of record 6408558941) and before ⑨, in the file's own form: a blank line, the indented blockquote, a bold dated lead-in, one paragraph on one line (1116 characters against the existing note's 1457). Nothing else in the ADR moves — the 2026-10-26 note, the header and
**Status**:line, every other item, anddocs/adr/README.md(generated byscripts/adr-index.mjsfrom**Status**:lines only;check:adr-indexreports it current) are byte-identical toorigin/main.The note, verbatim:
Where each clause was read from on
origin/main(24351cfe, the merge base):whenon the STORED row with the write gate's own function. Route:GET /api/v1/data/:object/:id/field-accessaspackages/rest/src/rest-route-ledger.tsL364 spells it, registered atpackages/rest/src/data-action-routes.tsL356 as${dataPath}/:object/:id/field-access. Judge:SecurityPlugin.getRecordFieldAccess(security-plugin.tsL3013) reads the row as the caller reads it and asksresolveProjectionFieldMask'sreadRowForbiddenWrites(security-plugin-field-readers.tsL241), which callscomputeForbiddenFieldWrites— the exported symbolsecurity-plugin.tsL57 imports fromsecurity-plugin-object-inputs.ts— withoperation: 'update'and the stored row aspreImage, naming every field. The record form reads it:client.data.fieldAccess(ledger L364;content/docs/permissions/field-level-security.mdxL60–67). "Field-level verdict" is deliberate: the object grant and row-level walls are judged elsewhere, as the schema text and the PR feat(spec,plugin-security,rest,client): a record-scoped field projection judges awhenlock on the stored row #27063 contract review (record 6460514485) state.whenlock on the stored row #27063 contract review ("Unchanged without the record — holds":/auth/me/permissions,getWritableFields,getReadableFields,getMetadataReadableFields,getQueryableFields,os explainuntouched) andfield-level-security.mdxL55–59.readablestays row-blind:permission.zod.tsL923 and thegetRecordFieldAccessdocblock.getDatafirst with the same gates and mapped refusals asGET /data/:object/:id, so an absent or unreadable row answers that read's refusal (data-action-routes.tsL349–352; ledger note L366).when) disables the field on every row, though the write gate admits it on rows the condition does not name #26594 and the ruling of record 6457467702, letter B (read in full this act: "the server judges each field permission'swhenagainst the stored row exactly as the write gate does …/auth/me/permissionsand the other row-less readers keep reporting a conditioned entry as its lock").Premise verified before writing:
git log --oneline -1 --grep='#27063' origin/main→f9f27a06 feat(spec,plugin-security,rest,client): a record-scoped field projection judges a \when` lock on the stored row (#27063)— merged and onorigin/main; its file list carried nodocs/adr/**`, and L134 of the ADR still named only the write gate and the row-blind readers.Tests
No test changes: a
docs/adr/**amendment touches no package, so no ① dependency closure and no ② packagetest/typecheckis owed (the file surface isdocs/adr/0066-unified-authorization-model.mdalone), and documentation is not ablated. Head measured:5ccadf23(git rev-parse --short HEADafter the final commit; the gate union below ran on that tree).On-disk proof of the diff shape, head 5ccadf2
Acceptance notes
docs/adr/README.mdis generated from**Status**:lines only, so an in-place note owes it nothing; left untouched and reported current. (2) The derived list is the ADR/docs prose families plus the whole-tree families, nothing from the test tiers; butcheck:pm-governed-prose, the family the order named, does not derive for this path (it readsAGENTS.md's governed-surface paragraph), and no markdown-lint or link-check family exists beyondcheck:adr-links. The ADR siblings that derived:check:adr-links,check:adr-anchors,check:adr-index,check:adr-symbol-anchors,check:doc-authoring,check:doc-formula-expressions. (3) The ledger's spelling of the route is the one cited. (4)computeForbiddenFieldWritesis the exported symbol; the plugin's private method of the same name wraps it. (5)check:pm-skill-id-lintscans.claude/skills/pm-dispatch,skills/,.claude/agents/os-dev.mdandAGENTS.mdonly./auth/me/permissions, the field projections,os explain)" now reads beside the new note and is left verbatim by the card's scope (⑧ only, one dated note). Not a finding.content/docs/protocol/objectql/security.mdxL264 spells the routeGET /data/{object}/{id}/field-access,field-level-security.mdxL61 spellsGET /api/v1/data/{object}/{id}/field-access, and the ledger spells:object/:id. The ADR cites the ledger's. Noted, not filed.pnpm --filter @objectstack/lint run check:doc-formula-expressionsfirst exited 3 — PREREQUISITE NOT MET:@objectstack/formulaand@objectstack/lintare not built in a fresh worktree. Built both under the verify lock (VERDICT command-exit 0 · held the lock 89s · waited 0s) and re-ran: exit 0. A local-environment fact, not a finding; nothing was measured by the first run.Verification
Local gate subset, derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths; change set off merge base24351cfe: 1 path, +2/-0; 21 commands — 13 matched by path, 8 declared whole-tree) and reconciled with--ranon a record carrying every exit code:21 derived, 21 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 21 recorded an exit code and none of them is 3). Every exit captured before any pipe (cmd > LOG 2>&1; EXIT=$?).Gate readings on head 5ccadf2 — 21 commands, all exit 0
Every family CI runs on this PR (the artifact-roster, wide-population and always-runs families the derivation lists as outside the derived total) is read from the PR's check-runs by the seat and was not re-run locally.
check:dev-prereqs/check:skill-frame-freshnessare not owed (nopackages/specbuild, no frame file touched). Dogfood battery: n/a (no!in the title, Clause-② no).test:repois CI's.Generated by Claude Code