Skip to content

docs(adr): ADR-0066 ⑧'s when note names the record-scoped field projection as a third, row-aware reader (#26594, ruling B) - #27152

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-27075-adr-0066-when-record-scoped-projection
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-27075-adr-0066-when-record-scoped-projection

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 其余每一行一字未动。
  • 为什么改:PR feat(spec,plugin-security,rest,client): a record-scoped field projection judges a when lock 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 里都有名字。
  • 风险与回滚:零运行时影响(纯 ADR 文本,docs/adr/** 不发布,打 skip-changeset);回滚即 revert 本 PR 的单个 commit。
  • 先看:只有一个文件、一个 hunk —— docs/adr/0066-unified-authorization-model.md L135–L136(新注记);核对其三句话与 packages/plugins/plugin-security/src/security-plugin-field-readers.ts L233–L248、packages/rest/src/rest-route-ledger.ts L364–L366 一致。
  • 两分钟验证:git diff origin/main...HEAD --numstat,预期恰好一行 2 0 docs/adr/0066-unified-authorization-model.md。
  • 需要你做的:Tier H(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, and docs/adr/README.md (generated by scripts/adr-index.mjs from **Status**: lines only; check:adr-index reports it current) are byte-identical to origin/main.

The note, verbatim:

Note (2026-10-28) — a third reader of when holds the row: the record-scoped field projection (#26594, ruling of record 6457467702, letter B). GET /api/v1/data/:object/:id/field-access (SecurityPlugin.getRecordFieldAccess, the answer a record's edit form reads) judges each field permission's when against the stored row with the write gate's own function, computeForbiddenFieldWrites, asked as an update by id of that row naming every field over the same sets, posture and delegator intersection the gate takes — so its editable list is the gate's field-level verdict for that row, a conditioned lock disables a field only on the rows its condition names, and no second evaluator of when exists for it to drift from. The row-blind readers (/auth/me/permissions, the object-level field projections, os explain) are unchanged and still apply a conditioned entry as its lock, and readable stays row-blind. The record is read first exactly as GET /api/v1/data/:object/:id reads it, so an absent or unreadable row answers that read's refusal, and no reading is wider than the gate's.

Where each clause was read from on origin/main (24351cfe, the merge base):

  • (a) The record-scoped projection judges when on the STORED row with the write gate's own function. Route: GET /api/v1/data/:object/:id/field-access as packages/rest/src/rest-route-ledger.ts L364 spells it, registered at packages/rest/src/data-action-routes.ts L356 as ${dataPath}/:object/:id/field-access. Judge: SecurityPlugin.getRecordFieldAccess (security-plugin.ts L3013) reads the row as the caller reads it and asks resolveProjectionFieldMask's readRowForbiddenWrites (security-plugin-field-readers.ts L241), which calls computeForbiddenFieldWrites — the exported symbol security-plugin.ts L57 imports from security-plugin-object-inputs.ts — with operation: 'update' and the stored row as preImage, naming every field. The record form reads it: client.data.fieldAccess (ledger L364; content/docs/permissions/field-level-security.mdx L60–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 a when lock on the stored row #27063 contract review (record 6460514485) state.
  • (b) The row-blind readers are unchanged and still apply a conditioned entry as its lock: the PR feat(spec,plugin-security,rest,client): a record-scoped field projection judges a when lock on the stored row #27063 contract review ("Unchanged without the record — holds": /auth/me/permissions, getWritableFields, getReadableFields, getMetadataReadableFields, getQueryableFields, os explain untouched) and field-level-security.mdx L55–59. readable stays row-blind: permission.zod.ts L923 and the getRecordFieldAccess docblock.
  • (c) No reading is wider than the gate's: the route reads the record through getData first with the same gates and mapped refusals as GET /data/:object/:id, so an absent or unreadable row answers that read's refusal (data-action-routes.ts L349–352; ledger note L366).
  • (d) Cites 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 and the ruling of record 6457467702, letter B (read in full this act: "the server judges each field permission's when against the stored row exactly as the write gate does … /auth/me/permissions and 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 no docs/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 ② package test / typecheck is owed (the file surface is docs/adr/0066-unified-authorization-model.md alone), and documentation is not ablated. Head measured: 5ccadf23 (git rev-parse --short HEAD after the final commit; the gate union below ran on that tree).

On-disk proof of the diff shape, head 5ccadf2
git diff --numstat  →  2  0  docs/adr/0066-unified-authorization-model.md
L134 md5 at HEAD  8a105f47be1fce00f9f2a95fd412c944  ==  L134 md5 in the tree  8a105f47be1fce00f9f2a95fd412c944
L135 = blank · L136 = the new note (1116 chars, begins "  > **Note (2026-10-28) — …") · L137 = "- **⑨ Authoring-time validation …"
grep -naP for the C0 / DEL control-byte class on the file → no match (grep exit 1)
node scripts/check-nul-bytes.mjs → OK (scanned 21863 text file(s) … no raw ASCII control bytes), exit 0

Acceptance notes

  • Dispatch-order hypotheses, measured — all five held, one with a correction: (1) docs/adr/README.md is 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; but check:pm-governed-prose, the family the order named, does not derive for this path (it reads AGENTS.md's governed-surface paragraph), and no markdown-lint or link-check family exists beyond check: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) computeForbiddenFieldWrites is the exported symbol; the plugin's private method of the same name wraps it. (5) check:pm-skill-id-lint scans .claude/skills/pm-dispatch, skills/, .claude/agents/os-dev.md and AGENTS.md only.
  • The 2026-10-26 note's phrase "every row-blind reader (/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.
  • Spelling variance, not a contract violation: content/docs/protocol/objectql/security.mdx L264 spells the route GET /data/{object}/{id}/field-access, field-level-security.mdx L61 spells GET /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-expressions first exited 3 — PREREQUISITE NOT MET: @objectstack/formula and @objectstack/lint are 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.
  • Out-of-scope findings in classes a/b/c: none.
  • Attribution: the commit carries the model-free trailer pair AGENTS.md prescribes; the harness attribution reminder's model-named trailer form and its PR-footer form were not used, by that reminder's own precedence sentence.

Verification

Local gate subset, derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set off merge base 24351cfe: 1 path, +2/-0; 21 commands — 13 matched by path, 8 declared whole-tree) and reconciled with --ran on 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
node scripts/check-adr-links.mjs :: exit 0 — ✅ 925 relative link destination(s) under docs/adr/ resolve
node scripts/check-adr-links.mjs --self-test :: exit 0 — ✅ discrimination, census, ADR-0046 pin and baseline staleness all verified
pnpm check:adr-anchors :: exit 0 — OK (91 anchored file(s), every governing ADR still referenced; 139 decision number(s) …)
pnpm check:adr-index :: exit 0 — OK — docs/adr/README.md is current, every record parsed, self-test 46 assertions green (report-only)
pnpm check:adr-symbol-anchors :: exit 0 — ✅ 2773 anchors across 145 records resolve
pnpm check:doc-authoring :: exit 0 — ✓ 440 files clean · 69 published skill files clean · 19622 customer-facing string(s) clean · sibling-package prose ids hold the baseline
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 (rerun after building @objectstack/formula + @objectstack/lint; first run exit 3 PREREQUISITE NOT MET)
node scripts/check-closing-keyword-parity.mjs :: exit 0 — OK (3 parsers agree on all 9 keywords and both measured separators)
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 — ✓ 40 assertions, 5 mutations driven to red
node scripts/check-comment-mask-corpus.mjs :: exit 0 — ✓ 18694 files, 0 disagree, 0 unparseable
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 — ✓ 23 case(s) across 5 batteries
pnpm check:ci-filter-parity :: exit 0 — OK: all 202 declared cross-package glob(s) covered; all 23 build input(s) and 13 script(s) covered by `core`
pnpm check:closed-ceremony-text :: exit 0 — ✓ 4766 shipped file(s) read; none says the ownership ceremony is closed
pnpm check:gitlink-declared :: exit 0 — OK (21965 index entries, 0 gitlink(s))
pnpm check:cross-package-test-inputs :: exit 0 — OK: 31 package(s) read outside themselves, all declared; 3868 test file(s) … every one declared
pnpm check:driver-memory-census :: exit 0 — OK — every declaration is ledgered, every ledger entry is live
pnpm check:nul-bytes :: exit 0 — OK (scanned 21863 text file(s); no raw ASCII control bytes)
pnpm check:pm-governed-merges :: exit 0 — ✓ self-test 537 assertions; live: the real generator declared 9 output(s) and certified this tree
pnpm check:pm-prior-rulings :: exit 0 — ✓ self-test 155 cases pass
pnpm check:refd-timer-probe :: exit 0 — OK 18684 source file(s) swept; the probe is read in the approved module and nowhere else
pnpm check:watch-hint-literal :: exit 0 — ✓ 74 declaration(s) across 4 rostered name(s), all 187 literals admitted

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-freshness are not owed (no packages/spec build, no frame file touched). Dogfood battery: n/a (no ! in the title, Clause-② no). test:repo is CI's.


Generated by Claude Code

…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>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 28, 2026
@github-actions github-actions Bot added size/xs documentation Improvements or additions to documentation labels Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5ccadf233eb8659b2e971d590fa890b9413e8dd0
Local-runs: none
Scope: docs/adr/0066-unified-authorization-model.md (⑧, the new 2026-10-28 note) + packages/plugins/plugin-security/src/security-plugin-field-readers.ts L140–L248, packages/rest/src/rest-route-ledger.ts L364, packages/rest/src/data-action-routes.ts L343–L356, security-plugin.ts L2098–L2114 (read as the note's referents, not reviewed), ruling of record 6457467702 (letter B)

Read at 2026-10-28T08:29Z by the domain:skills#2 seat on the fetched head (merge-base 24351cfe): one file, +2/−0. Tier H by path (docs/adr/**; check-governed-merges --pr 27152 → H, 2 changed lines). The approval of record is os-zhuang's APPROVED review on this head (2026-10-28T08:26Z); both GOVERNED_APPROVERS were requested by the relay pr_create run and hotlong remains requested; the PR is still draft, nothing flipped. This record is the surface-owed review.

① Derived judgments

  • Shape: one dated note under ⑧ directly after the 2026-10-26 note, in the file's own form (blank line, indented blockquote, bold dated lead-in, one paragraph on one line, 1116 chars against the existing note's 1457); L134 and every other line byte-identical to origin/main (git diff --numstat → 2 0). The card's 「one dated note under ⑧」 and the claim's file surface hold exactly.
  • Claim (a), verified on origin/main: the route is spelled as the ledger spells it (rest-route-ledger.ts L364 GET /api/v1/data/:object/:id/field-access, data-action-routes.ts L356 registers ${dataPath}/:object/:id/field-access); SecurityPlugin.getRecordFieldAccess is a service member (security-plugin.ts L2098/L2114); its edit half is readRowForbiddenWrites(row) → this.computeForbiddenFieldWrites(objectName, every field, secMeta, permissionSets, delegatorSets, { operation: 'update', context, preImage: async () => row }) (security-plugin-field-readers.ts L241–L248, with the docblock 「the step 2.5 gate ITSELF, asked over these sets, this posture and this delegator's sets, as an update by id of row naming every field」). The note's sentence — the gate's own function, update by id, every field, same sets/posture/delegator — is that code in prose; 「no second evaluator of when exists for it to drift from」 is the design's point and true by construction.
  • Claim (b): the row-blind readers (/auth/me/permissions, the object-level field projections, os explain) are named as unchanged and still applying a conditioned entry as its lock — PR feat(spec,plugin-security,rest,client): a record-scoped field projection judges a when lock on the stored row #27063's diff touched none of those readers' judgment (the fieldPerms map above is the row-blind reading, kept beside the new closure), and readable is not judged per row anywhere in the new path. Holds.
  • Claim (c): 「the record is read first exactly as GET /api/v1/data/:object/:id reads it, so an absent or unreadable row answers that read's refusal」 — the docblock's 「the pre-image, read as the caller reads it」 and the route's placement under the data-actions family support it; the seat did not trace the route handler line by line (the ADR note is a pointer, and the contract review on PR feat(spec,plugin-security,rest,client): a record-scoped field projection judges a when lock on the stored row #27063, record 6460514485, covered the handler). Accepted on that record.
  • Citations: 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 and ruling of record 6457467702 letter B, as the card asked; the triage condition 「lands after PR feat(spec,plugin-security,rest,client): a record-scoped field projection judges a when lock on the stored row #27063」 is met (f9f27a06 on origin/main).

② Semver level

  • None owed: an ADR note; no package touched. Clause-②: no, Metadata-protocol: none on the PR body match the claim; no Skills-docs: line owed; skip-changeset correct.

③ Boundary flags

  • Dev's hypothesis correction accepted: check:pm-governed-prose does not derive for docs/adr/**; the ADR family that did derive (check:adr-links, check:adr-anchors, check:adr-index, check:adr-symbol-anchors, check:doc-authoring, check:doc-formula-expressions) ran green, 21/21 derived families exit 0 on the head.
  • docs/adr/README.md is generated from **Status**: lines only and is untouched — check:adr-index reports it current (dev's run). Holds.
  • Two observations the dev left unfiled, agreed: the 2026-10-26 note's 「every row-blind reader (…the field projections…)」 now reads beside the new note that names the record-scoped projection as row-aware — a reader of both gets the full picture, and editing the earlier note was outside the card; route spelling variance across the two hand-written docs pages (/data/{object}/{id}/field-access vs /api/v1/...) — polish, no consumer parses the prose.
  • Author objectstack-fleet[bot] through the relay; mcp_calls: 0; no flip by anyone yet.

④ Design judgment

  • Against the card's scenario (a reader of ADR-0066 asking which readers of a conditioned field lock see the row): the ADR now names all three — the write gate, the row-blind readers, and the one row-aware reader — and says the row-aware one reuses the gate's function rather than re-implementing when, which is the invariant worth recording. Holds. Residual: none beyond the two unfiled observations.

Implemented-by: claude/issue-27075-adr-0066-when-record-scoped-projection
Reviewed-by: session_01QSeKYmhU5KEj1nukYhp1Tg

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 — 席位终稿 · PR #27152 · head 5ccadf23 · domain:skills#2 · 2026-10-28T08:31Z

席位核对 — ACCEPT · domain:skills#2 · session_01QSeKYmhU5KEj1nukYhp1Tg · 2026-10-28T08:31Z

  1. 形态:draft, mergeable_state clean, auto-merge off → main,首行 Fixes #27075,Clause-②: no、Metadata-protocol: none 行首在;一个文件 +2/−0;assignee os-justin;skip-changeset 正确;单 commit。
  2. 前提实测:席位在取回的 head(merge-base 24351cfe)上核 —— 路由拼写同台账 L364;getRecordFieldAccess 为服务成员(security-plugin.ts L2098/L2114);readRowForbiddenWrites → computeForbiddenFieldWrites(…, { operation: 'update', preImage: row })(field-readers L241–248);2026-10-26 注记 L134 逐字节未动;PR feat(spec,plugin-security,rest,client): a record-scoped field projection judges a when lock on the stored row #27063 f9f27a06 在 origin/main(分诊条件满足);docs/adr/README.md 未动且 check:adr-index 报 current。
  3. 门禁 check-runs(head 5ccadf23,本笔读数 2026-10-28T08:31Z):45 check-runs,26 success · 19 skipped · 0 in progress · 0 failure。dev 本地派生 21/21 exit 0 钉 5ccadf23(报告 6464259261;check:doc-formula-expressions 首跑 exit 3 为前置未建,建后 exit 0)。
  4. 接受的偏差:① 机制假设 2 修正(check:pm-governed-prose 不派生于 docs/adr/**,派生的是 ADR 族六项);② worktree 配方用了 os-dev.md 的 --no-track 拼法,结果同;③ 席位未逐行追路由处理器(c 句依 feat(spec,plugin-security,rest,client): a record-scoped field projection judges a when lock on the stored row #27063 的契约复核记录 6460514485)。
  5. 范围外去向:none filed;两条观察记 Acceptance notes(2026-10-26 注记的「field projections」措辞与新注记并读即全貌;两份手写文档页路由拼写不一,无消费者解析)。Skills-docs: 不欠(kind none)。
  6. 落地条款:Tier H;授权批准(os-zhuang APPROVED,head 5ccadf23)在案;契约复核 PASS 评论 6464321368;席位不翻 ready(分类器拦落地笔,已报维护者);MERGED 后关卡摘 pm:dispatched。

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/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants