From 584cff6b9ed948441cbd81cb472071ab6e477908 Mon Sep 17 00:00:00 2001 From: os-steve Date: Mon, 24 Aug 2026 16:12:03 +0000 Subject: [PATCH] docs: retarget the general-rule ADR-0057 D10 citations to ADR-0124 D1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0124 recorded the general enforcement rule ("the server is the enforcement point; client-side gating is a usability courtesy") that ~30 sites had been citing as `ADR-0057 D10` — a number that actually decides Setup-nav capability surfacing. This moves the general-rule citations and leaves the capability-gate citations where they are. Classified per citation, not rewritten mechanically: 31 of 98 occurrences moved; 33 live citing sites that cite D10 for its own subject stay; 34 occurrences in shipped CHANGELOGs and in the two ADRs are records and are not rewritten. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx --- .claude/skills/dogfood-verification/SKILL.md | 2 +- .../2026-07-studio-package-create-ux-dogfood.md | 2 +- docs/qa/platform-checklist/RUNNER.md | 6 +++--- .../areas/access-security.json | 12 ++++++------ docs/qa/platform-checklist/areas/approvals.json | 4 ++-- .../areas/attachments-storage.json | 2 +- .../platform-checklist/areas/identity-auth.json | 6 +++--- .../platform-checklist/areas/platform-core.json | 2 +- .../platform-checklist/areas/records-forms.json | 4 ++-- .../areas/studio-authoring.json | 4 ++-- packages/lint/src/validate-expressions.test.ts | 8 ++++---- packages/lint/src/validate-expressions.ts | 3 +-- .../src/engine-readonly-when-parent.test.ts | 3 +-- packages/objectql/src/engine.ts | 5 ++--- .../objectql/src/validation/rule-validator.ts | 16 ++++++++-------- .../src/current-user-endpoints.ts | 7 ++++--- .../src/fold-wildcard-superuser.test.ts | 4 ++-- ...showcase-readonly-when-parent.dogfood.test.ts | 6 ++---- ...ctql__src__validation__rule-validator.ts.json | 6 +++--- 19 files changed, 49 insertions(+), 53 deletions(-) diff --git a/.claude/skills/dogfood-verification/SKILL.md b/.claude/skills/dogfood-verification/SKILL.md index 016d700704..79b3c55d0a 100644 --- a/.claude/skills/dogfood-verification/SKILL.md +++ b/.claude/skills/dogfood-verification/SKILL.md @@ -110,7 +110,7 @@ dev 工作树、dev-server 端口、preview 浏览器全是**共享的**:并行 - [ ] 确认页面渲染完成*之后*(先截图,再查 selector),DOM dump 没问题。 - [ ] **门的两侧都要测**:`requiresService`/`requiresObject`/权限门要在依赖存在*与* 缺席两种状态下各验一次。 -- [ ] 服务端是权威可见性门(ADR-0057 D10)—— 客户端过滤只是「礼貌」。元数据开关不改 +- [ ] 服务端是权威可见性门(ADR-0124 D1)—— 客户端过滤只是「礼貌」。元数据开关不改 UI 时,先查强制在服务端(框架,可在本仓修)还是客户端(objectui console,另一 个仓)。 - [ ] 用 `preview_screenshot`(API 改动用 `preview_network`)向用户证明;loading diff --git a/docs/audits/2026-07-studio-package-create-ux-dogfood.md b/docs/audits/2026-07-studio-package-create-ux-dogfood.md index 14a9bd1cda..5943716b83 100644 --- a/docs/audits/2026-07-studio-package-create-ux-dogfood.md +++ b/docs/audits/2026-07-studio-package-create-ux-dogfood.md @@ -6,7 +6,7 @@ object with fields, enter a record, create an app with navigation, publish, and use the result as an end user — driving a real Chromium against `/_console` (vendored console build `7782698`, matching the `.objectui-sha` pin, so findings are not stale-bundle artifacts). Both sides of the read-only gate were exercised -per ADR-0057 D10. +per ADR-0124 D5. Run: `objectstack dev --ui --seed-admin` on the showcase example, fresh SQLite DB, `admin@objectos.ai` seeded admin, headless Chromium via CDP with screenshots at diff --git a/docs/qa/platform-checklist/RUNNER.md b/docs/qa/platform-checklist/RUNNER.md index d237f796d3..8a925e68e5 100644 --- a/docs/qa/platform-checklist/RUNNER.md +++ b/docs/qa/platform-checklist/RUNNER.md @@ -75,9 +75,9 @@ test-run output the clause's `evidence` field names. 4. **Both sides of every gate.** For any permission/visibility/feature gate, verify presence for the entitled persona AND absence (or server-side rejection) for the unentitled one. UI absence alone is a client courtesy; the server is the authority. - (That rule is cited across the framework as `ADR-0057 D10`; treat it as an - attribution — D10 itself decides Setup-nav capability surfacing, and recording the - general rule is tracked in #9628. The rule is not in doubt, only its anchor.) + (That rule is `ADR-0124 D1`, and this rule is its verification half — `ADR-0124 D5` + states it directly: a test that asserts only that the interface hides something has + tested the courtesy layer and left the enforcement point unobserved.) Where feasible, prove denial with a direct forged request. 5. **Severe findings are hypotheses.** "The whole surface is unreachable" gets disproven-or-confirmed via screenshot + the server's own metadata before it is diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 2fef0814ee..da0105a773 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -51,7 +51,7 @@ "evidence": "PATCH response + admin re-read" }, { - "clause": "admin (platform posture) reads the full set — the entitled side of the same gate (both sides, RUNNER rule 4 / ADR-0057 D10)", + "clause": "admin (platform posture) reads the full set — the entitled side of the same gate (both sides, RUNNER rule 4 / ADR-0124 D5)", "oracle": "api", "verify": "admin GET list contains every id created in this run", "evidence": "admin listing" @@ -83,7 +83,7 @@ }, "source": [ "packages/verify/src/rls.ts", - "ADR-0057 D10", + "ADR-0124 D1", "packages/qa/dogfood/test/showcase-private-owd.dogfood.test.ts", "authz-conformance.matrix.ts rows rls-read / rls-by-id-write", "#7637 (run record) — the by-id spot-check on a skipped object is where the private D11 defect surfaced", @@ -335,7 +335,7 @@ "evidence": "screenshot + disabled-state DOM read" }, { - "clause": "the SERVER refuses the same write: direct PUT /api/v1/meta/object/ on the read-only package answers 4xx with a ledgered metadata-protocol code (WRITABLE_PACKAGE_REQUIRED, or ITEM_LOCKED for _lock'd items) — UI absence never suffices (ADR-0057 D10)", + "clause": "the SERVER refuses the same write: direct PUT /api/v1/meta/object/ on the read-only package answers 4xx with a ledgered metadata-protocol code (WRITABLE_PACKAGE_REQUIRED, or ITEM_LOCKED for _lock'd items) — UI absence never suffices (ADR-0124 D1)", "oracle": "api", "verify": "PUT response status >=400 and error.code ∈ {WRITABLE_PACKAGE_REQUIRED, ITEM_LOCKED} (packages/spec/src/api/error-code-ledger.zod.ts, @objectstack/metadata-protocol entry)", "evidence": "PUT trace" @@ -421,7 +421,7 @@ "evidence": "the verdict matrix + spot re-reads" }, { - "clause": "every withheld cell is DENIED SERVER-SIDE with the ledgered code: verbs marked false answer 403 with error.code PERMISSION_DENIED (rest-server maps explicit security denials to 403 PERMISSION_DENIED) — UI absence never counts (ADR-0057 D10)", + "clause": "every withheld cell is DENIED SERVER-SIDE with the ledgered code: verbs marked false answer 403 with error.code PERMISSION_DENIED (rest-server maps explicit security denials to 403 PERMISSION_DENIED) — UI absence never counts (ADR-0124 D1)", "oracle": "api", "verify": "per-cell status 403 and body code PERMISSION_DENIED; capture any cell answering a different code for triage", "evidence": "the verdict matrix" @@ -1293,7 +1293,7 @@ } ], "negative": [ - "a Studio panel that greys the save while a direct meta PUT widens is a FAIL — the server gate is the authority (ADR-0057 D10 both-sides)", + "a Studio panel that greys the save while a direct meta PUT widens is a FAIL — the server gate is the authority (ADR-0124 D1 both-sides)", "a stock-deploy packaged-object widening PUT that answers 2xx (or leaves the object widened) is a FAIL", "do NOT tick external-principal read/write enforcement: that evaluation is liveness `planned` (#2696) — this item is the save/authoring gate only" ], @@ -1310,7 +1310,7 @@ "packages/lint/src/validate-security-posture.ts (OWD_WIDTH + SECURITY_EXTERNAL_WIDER lint parity)", "objectui packages/app-shell/src/views/studio-design/owd-sharing.ts (isExternalWider) + PackageOwdOverviewPanel.tsx + ObjectSettingsPanel.tsx", "examples/app-showcase/src/data/objects/announcement.object.ts + account.object.ts (externalSharingModel declarations)", - "ADR-0090 D11, ADR-0086 D1, ADR-0057 D10, #3050, objectui#2508" + "ADR-0090 D11, ADR-0086 D1, ADR-0124 D1, #3050, objectui#2508" ], "history": [ { diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 53278e8fc8..fcb6371c86 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -280,7 +280,7 @@ "evidence": "request read" }, { - "clause": "the gate is server-side: a forged direct POST of the decision route as the submitter is rejected (ADR-0057 D10 — UI absence alone is a client courtesy)", + "clause": "the gate is server-side: a forged direct POST of the decision route as the submitter is rejected (ADR-0124 D1 — UI absence alone is a client courtesy)", "oracle": "api", "verify": "the forged approve answers FORBIDDEN (403-mapped); test BOTH sides — the entitled approver's decision on the same request succeeds", "evidence": "the rejected call + the entitled approver's accepted call" @@ -298,7 +298,7 @@ "traps": ["hydration-race", "wrong-persona"], "source": [ "#3358 §1", - "ADR-0057 D10 (server is the authoritative visibility gate)", + "ADR-0124 D1 (server is the authoritative visibility gate)", "examples/app-showcase/src/security/seed-approval-demo.ts (Mei Phone: 'a clean submitter — a requester who is never also one of her own approvers')" ], "history": [ diff --git a/docs/qa/platform-checklist/areas/attachments-storage.json b/docs/qa/platform-checklist/areas/attachments-storage.json index 32441f9e7c..f694612fd0 100644 --- a/docs/qa/platform-checklist/areas/attachments-storage.json +++ b/docs/qa/platform-checklist/areas/attachments-storage.json @@ -233,7 +233,7 @@ } ], "negative": [ - "a gated file downloadable anonymously (silent success) is a FAIL — UI absence of a download button is a client courtesy; the route is the authority (ADR-0057 D10)", + "a gated file downloadable anonymously (silent success) is a FAIL — UI absence of a download button is a client courtesy; the route is the authority (ADR-0124 D1)", "a deny that surfaces in the panel as 'Download failed (403)' instead of the mapped copy means the error-envelope dialect broke (#3689 note in the panel) — file it against objectui, not storage" ], "traps": ["wrong-persona", "dispatcher-vs-hono-route", "stale-console-bundle"], diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index 78e5344fd6..8add2dc10c 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -211,7 +211,7 @@ "evidence": "the screenshot set" }, { - "clause": "a disabled method is refused SERVER-SIDE, not merely hidden — UI absence is a client courtesy; the server is the authority (ADR-0057 D10)", + "clause": "a disabled method is refused SERVER-SIDE, not merely hidden — UI absence is a client courtesy; the server is the authority (ADR-0124 D1)", "oracle": "api", "verify": "firing each disabled method's endpoint returns a non-2xx (the plugin's routes are absent or refuse)", "evidence": "the forged-request responses" @@ -347,7 +347,7 @@ ], "negative": [ "an admin-role invitation that returns success, or that leaves ANY row behind, is a FAIL of privilege-escalation severity — file immediately, P0-verify per RUNNER rule 7", - "UI-only enforcement (affordance hidden but the forged request succeeds) is a FAIL — the server is the authority (ADR-0057 D10)", + "UI-only enforcement (affordance hidden but the forged request succeeds) is a FAIL — the server is the authority (ADR-0124 D1)", "my-delegable-scope OVER-reporting is a FAIL — a position, permission set or BU subtree the caller cannot actually delegate, or (were the shape ever to grow one) a role the caller cannot mint: a client that trusts the scope would offer what the endpoint then refuses. UNDER-reporting is NOT a FAIL and must not be filed as one: the endpoint answering an empty scope while the delegate can still mint a member invitation is the safe direction and the expected state on stock fixtures (run #7663 — DelegableScope has no invitation-role field at all)" ], "automated": { "kind": "e2e", "ref": "packages/qa/dogfood/test/delegated-admin-invite.dogfood.test.ts" }, @@ -807,7 +807,7 @@ } ], "negative": [ - "an org management surface where the affordance is hidden but the forged endpoint succeeds for a non-admin is a FAIL — the server is the authority (ADR-0057 D10)", + "an org management surface where the affordance is hidden but the forged endpoint succeeds for a non-admin is a FAIL — the server is the authority (ADR-0124 D1)", "remove-member that drops the roster row but leaves the ex-member's org-scoped access intact is a FAIL — removal must change authorization", "a role written outside the {owner, admin, delegated_admin, member} vocabulary is a FAIL — including a stored 'guest': the closed list is the write-side guardrail that makes an ungoverned capability grant unrepresentable (ADR-0108), so a 2xx that persists 'guest' is a regression of the closure, not a vocabulary difference. A role change that does not flip any gate is equally a FAIL", "the Organization nav landing on the raw sys_organization list because {current_org_id} did not resolve (when an active org exists) is a FAIL of the ADR-0081 wiring" diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index bb2590c407..25f0e09e03 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -518,7 +518,7 @@ "evidence": "three screenshots" }, { - "clause": "the app-level gate is server-side, not a client courtesy: a forged member GET /api/v1/meta/app?id=setup is denied/empty at the server, not merely hidden in the launcher (ADR-0057 D10 both-sides)", + "clause": "the app-level gate is server-side, not a client courtesy: a forged member GET /api/v1/meta/app?id=setup is denied/empty at the server, not merely hidden in the launcher (ADR-0124 D1 both-sides)", "oracle": "api", "verify": "the forged request's status/body proves server-side denial", "evidence": "the forged response" diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index 071e720e17..fca81d296d 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -1120,7 +1120,7 @@ { "clause": "related lists are READ-gated on BOTH ends: a persona WITHOUT read on the child object (showcase_contributor lacks showcase_contact read) sees NO Contacts section on the account detail (UI courtesy — deriveRelatedLists drops children the user cannot read, objectui#2359) AND a direct child query is refused server-side (403); a persona WITH child read (showcase_manager) sees the section AND the query 200s", "oracle": "api", - "verify": "as showcase_contributor: screenshot confirms the Contacts tab is absent, and the forged GET /api/v1/data/showcase_contact?$filter=[[\"account\",\"=\",\"\"]] returns 403; as showcase_manager: the tab renders and the identical query returns 200 with rows — the server is the authority (ADR-0057 D10, RUNNER rule 4), the UI drop is courtesy", + "verify": "as showcase_contributor: screenshot confirms the Contacts tab is absent, and the forged GET /api/v1/data/showcase_contact?$filter=[[\"account\",\"=\",\"\"]] returns 403; as showcase_manager: the tab renders and the identical query returns 200 with rows — the server is the authority (ADR-0124 D1, RUNNER rule 4), the UI drop is courtesy", "evidence": "both personas' detail screenshots + the 403 and the 200 child queries" } ], @@ -2489,7 +2489,7 @@ { "clause": "the SERVER is the boundary: a forged PUT /api/v1/meta/view/ by the non-admin is refused (4xx) — UI absence alone is courtesy", "oracle": "api", - "verify": "the direct non-admin PUT returns a 403-class refusal and no overlay view is created (RUNNER rule 4, ADR-0057 D10)", + "verify": "the direct non-admin PUT returns a 403-class refusal and no overlay view is created (RUNNER rule 4, ADR-0124 D5)", "evidence": "the forged-request refusal" } ], diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index 28c54f49e6..d95fe56e49 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -73,14 +73,14 @@ } ], "negative": [ - "silent acceptance of an authoring write into a read-only package is a FAIL (ADR-0057 D10 — the server is the authoritative gate; the client lock is courtesy)", + "silent acceptance of an authoring write into a read-only package is a FAIL (ADR-0124 D1 — the server is the authoritative gate; the client lock is courtesy)", "a 'published' app absent from the Home launcher, or an end-user list rendering raw picklist values instead of labels, is a FAIL" ], "traps": ["stale-console-bundle", "automation-input", "hydration-race"], "source": [ "docs/audits/2026-07-studio-package-create-ux-dogfood.md ('The loop closes' — the canonical walk; findings 1/3/4/6 carried as knownGaps)", "ADR-0016 §9 (the MVP loop this proves)", - "ADR-0057 D10 (server-side gate authority)", + "ADR-0124 D1 (server-side gate authority)", "access-security.readonly-package-locks-studio (client-side lock — cross-referenced, not duplicated)" ], "history": [ diff --git a/packages/lint/src/validate-expressions.test.ts b/packages/lint/src/validate-expressions.test.ts index 12096e1f1b..3331a9c0e9 100644 --- a/packages/lint/src/validate-expressions.test.ts +++ b/packages/lint/src/validate-expressions.test.ts @@ -1146,8 +1146,8 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => { * `stripReadonlyWhenFields` deletes the value from the * payload; client `fallback: false` ⇒ editable. The two ends * fault in OPPOSITE directions and the "server enforces, - * client is courtesy" rule (cited as ADR-0057 D10; an - * attribution, #9628) gives it to the server. ⇒ the old sentence was BACKWARDS here. + * client is courtesy" rule (ADR-0124 D1) gives it to the + * server. ⇒ the old sentence was BACKWARDS here. * requiredWhen server logs the unbound root and `continue`s (#4977 did not * copy the carve-out); client `fallback: false`. Both ends * fail open and neither is about visibility. ⇒ the old @@ -1186,8 +1186,8 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => { }); it('`readonlyWhen` — names the client/server disagreement, not just the server verdict', () => { - // Server enforces, client is courtesy (cited as ADR-0057 D10, an - // attribution — #9628): the form renders the field editable + // Server enforces, client is courtesy (ADR-0124 D1): the form renders + // the field editable // (`fallback: false`) // while the server locks it. An author who only reads "LOCKED" cannot // reconcile that with the editable input in front of them. diff --git a/packages/lint/src/validate-expressions.ts b/packages/lint/src/validate-expressions.ts index c97d94875d..4bbde99a50 100644 --- a/packages/lint/src/validate-expressions.ts +++ b/packages/lint/src/validate-expressions.ts @@ -563,8 +563,7 @@ function rulePredicates(rule: AnyRec, path: string): Array<{ label: string; raw: * payload and lets the rest of the write through. Client: * `resolveFieldRuleState` passes `fallback: false`, so the form renders the * field editable. The standing "server enforces, client is courtesy" rule - * — cited in this repo as ADR-0057 D10, an attribution rather than a - * resolvable anchor (#9628) — resolves the disagreement: the author edits + * — ADR-0124 D1 — resolves the disagreement: the author edits * the field, the save reports * success, and the value silently never lands. The old sentence told this * author the field would be VISIBLE TO EVERYONE — the opposite failure, and diff --git a/packages/objectql/src/engine-readonly-when-parent.test.ts b/packages/objectql/src/engine-readonly-when-parent.test.ts index 2dec8e4248..f5c5f5d839 100644 --- a/packages/objectql/src/engine-readonly-when-parent.test.ts +++ b/packages/objectql/src/engine-readonly-when-parent.test.ts @@ -7,8 +7,7 @@ // Paid, its lines are frozen". It was enforced only in the client grid: the // server-side strip bound `record` and `previous` and nothing else, so every // `parent.*` predicate faulted, took the fail-OPEN branch, and the write landed -// with a 200 while the UI still drew the cell locked. The rule this repo cites -// as ADR-0057 D10 (an attribution, not a resolvable anchor — #9628) puts +// with a 200 while the UI still drew the cell locked. ADR-0124 D1 puts // enforcement on the SERVER and makes the client courtesy; this suite pins that // direction end-to-end through the real engine + a real driver, not through the // strip function in isolation (PD #10: a `case` label is not enforcement — diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index f02cf6cff3..ce95c8aaf3 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -5391,9 +5391,8 @@ export class ObjectQL implements IObjectQLEngine { * `readonlyWhen` reads, or `null` when this write cannot resolve one. * * `readonlyWhen: parent.status == 'paid'` is a documented **server** - * guarantee (the rule this repo cites as ADR-0057 D10 puts enforcement here; - * the client grid is courtesy — an attribution, not a resolvable anchor, - * #9628), but the strip is a pure function over the payload and the prior + * guarantee (ADR-0124 D1 puts enforcement here; the client grid is + * courtesy), but the strip is a pure function over the payload and the prior * row — it has no driver and cannot fetch a header. So the engine resolves it * and passes it in. * diff --git a/packages/objectql/src/validation/rule-validator.ts b/packages/objectql/src/validation/rule-validator.ts index 6cc42e03e8..52b08b30fe 100644 --- a/packages/objectql/src/validation/rule-validator.ts +++ b/packages/objectql/src/validation/rule-validator.ts @@ -113,10 +113,10 @@ * and #4775 made at the two neighbouring write gates. Do not widen it back on * the grounds that it reads inconsistent with D5's table — the table is what * was amended, and the standing "server enforces, client is courtesy" rule is - * why. This repo cites that rule as ADR-0057 D10 — an ATTRIBUTION, not a - * resolvable anchor: D10 decides Setup-nav capability surfacing, and no ADR - * records the general rule yet (#9628). The rule stands on its own; what is - * missing is the decision record, not the discipline. + * why. That rule is ADR-0124 D1, recorded in its own right by #9628 after a + * corpus search found it universally practised and decided by no ADR at all. + * ADR-0124 D2 makes the scope general by construction: it may not be + * re-narrowed to a single surface by a later citer. * * ## `requiredWhen`: the SCOPE is bound, the SEMANTICS are not changed (#4977) * @@ -661,10 +661,10 @@ export function stripReadonlyWhenFields( * is unlocked — we simply could not ask. Waving it through inverts the * guarantee: a field the author declared locked is written, the API answers * 200, and the client grid still draws the cell as read-only, so the UI and - * the database disagree with nobody told. The rule this repo cites as - * ADR-0057 D10 puts enforcement on the server; a lock that fails open leaves - * enforcement in the courtesy layer. (Attribution, not a resolvable anchor — - * see the module header and #9628.) So an unbound root resolves to LOCKED — conservative toward the + * the database disagree with nobody told. ADR-0124 D1 puts enforcement on + * the server; a lock that fails open leaves enforcement in the courtesy + * layer, which ADR-0124 D3 names outright: the server may not hand an + * undecided case to the client. So an unbound root resolves to LOCKED — conservative toward the * author's declared intent, and the direction #4649 (validation predicates) * and #4775 (hook conditions) already took for their own unevaluable case. * diff --git a/packages/plugins/plugin-hono-server/src/current-user-endpoints.ts b/packages/plugins/plugin-hono-server/src/current-user-endpoints.ts index 207d2a7ccf..b6ae1346c0 100644 --- a/packages/plugins/plugin-hono-server/src/current-user-endpoints.ts +++ b/packages/plugins/plugin-hono-server/src/current-user-endpoints.ts @@ -245,9 +245,10 @@ function allPathsMounted(rawApp: any, paths: readonly string[]): boolean { * (`admin_full_access` `'*': {modifyAllRecords}`) who ALSO holds * `organization_admin` (which denies writes on identity tables): the client * would see `sys_user.allowEdit:false` and disable a form the server accepts - * (verified: `PATCH /data/sys_user {name}` → 200). The rule this repo cites as - * ADR-0057 D10 — an attribution, not a resolvable anchor (#9628) — makes the - * server the authoritative gate; the client must mirror it, never diverge. + * (verified: `PATCH /data/sys_user {name}` → 200). ADR-0124 D1 makes the + * server the authoritative gate, and D4 makes this direction explicit: what + * the client is told must be derived from the server's actual effective + * enforcement, never from an independent reading of the declarations. * * The super-user grant covers private/managed objects on the server, so folding * it here is exactly as broad as real enforcement — never broader. diff --git a/packages/plugins/plugin-hono-server/src/fold-wildcard-superuser.test.ts b/packages/plugins/plugin-hono-server/src/fold-wildcard-superuser.test.ts index 1bfc53e31a..2a13553659 100644 --- a/packages/plugins/plugin-hono-server/src/fold-wildcard-superuser.test.ts +++ b/packages/plugins/plugin-hono-server/src/fold-wildcard-superuser.test.ts @@ -4,8 +4,8 @@ import { describe, it, expect } from 'vitest'; import { foldWildcardSuperUser, clampManagedObjectWrites, type ManagedSchemaLike } from './current-user-endpoints.js'; /** - * Server enforces, client is courtesy (cited as ADR-0057 D10 — an attribution, - * not a resolvable anchor, #9628) / ADR-0092 D5 — the `/me/permissions` + * Server enforces, client is courtesy (ADR-0124 D1; D4 for this direction + * specifically) / ADR-0092 D5 — the `/me/permissions` * per-object FLS map must * mirror the server's actual enforcement, which grants writes via a `'*'` * modifyAll super-user bypass regardless of another set's explicit per-object diff --git a/packages/qa/dogfood/test/showcase-readonly-when-parent.dogfood.test.ts b/packages/qa/dogfood/test/showcase-readonly-when-parent.dogfood.test.ts index 8c0b007aff..bfeed64091 100644 --- a/packages/qa/dogfood/test/showcase-readonly-when-parent.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-readonly-when-parent.dogfood.test.ts @@ -11,10 +11,8 @@ // `previous` and had no `parent`, so every one of those predicates faulted, the // fail-OPEN branch let the write through, and a single PATCH rewrote the // quantity and unit price of a settled invoice's line — HTTP 200, value -// persisted, while the grid still drew the cell locked. The rule this repo -// cites as ADR-0057 D10 (an attribution, not a resolvable anchor — #9628) -// makes the SERVER the enforcement point and the client courtesy; here it was -// inverted. +// persisted, while the grid still drew the cell locked. ADR-0124 D1 makes the +// SERVER the enforcement point and the client courtesy; here it was inverted. // // This is the issue's own repro, run against the shipped metadata rather than a // hand-built fixture: the unit + engine suites in `@objectstack/objectql` pin diff --git a/scripts/adr-anchors/packages__objectql__src__validation__rule-validator.ts.json b/scripts/adr-anchors/packages__objectql__src__validation__rule-validator.ts.json index 0dce84888a..ffe6388a82 100644 --- a/scripts/adr-anchors/packages__objectql__src__validation__rule-validator.ts.json +++ b/scripts/adr-anchors/packages__objectql__src__validation__rule-validator.ts.json @@ -1,8 +1,8 @@ { "file": "packages/objectql/src/validation/rule-validator.ts", "adrs": [ - "ADR-0057", - "ADR-0058" + "ADR-0058", + "ADR-0124" ], - "invariant": "A declared field lock is the SERVER's to enforce — the client grid is courtesy. This repo cites that rule as ADR-0057 D10; read it as an ATTRIBUTION, not as a resolvable anchor. D10 of `0057-erp-authorization-core-business-units-and-scope-depth.md` decides Setup-nav capability surfacing, and only its PS-2 implementation note carries the server-enforcement half — scoped to nav. The other ADR-0057 (system data lifecycle) has no D-numbered decisions at all. The rule itself is true and unchanged here; recording it as a decision is #9628. A `readonlyWhen` whose predicate names a scope root the write path could not bind resolves to LOCKED, not to \"not locked\": \"could not check\" must never read as \"allowed\" on a field the author declared frozen. This narrows ADR-0058 D5's fail-soft tier deliberately and only for that case — a merely BROKEN predicate (undeclared key, null overload, parse fault, throw) still fails open, and requiredWhen / option visibleWhen are untouched." + "invariant": "A declared field lock is the SERVER's to enforce — the client grid is courtesy. That rule is ADR-0124 D1, and it is general by construction: ADR-0124 D2 forbids re-narrowing it to one surface, and D3 forbids the specific move this file exists to prevent — resolving an undecided case permissively on the expectation that the courtesy layer will hide the result. This anchor previously carried the rule as an ATTRIBUTION rather than a resolvable anchor, because no ADR had recorded it; #9628 recorded it and the citation was retargeted. A `readonlyWhen` whose predicate names a scope root the write path could not bind resolves to LOCKED, not to \"not locked\": \"could not check\" must never read as \"allowed\" on a field the author declared frozen. This narrows ADR-0058 D5's fail-soft tier deliberately and only for that case — a merely BROKEN predicate (undeclared key, null overload, parse fault, throw) still fails open, and requiredWhen / option visibleWhen are untouched." }