Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/adr-0057-d10-citation-attributive.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
---
"@objectstack/lint": patch
---

docs(lint): the `readonlyWhen` field-rule diagnostic no longer cites `ADR-0057 D10` (#9255)

The author-visible consequence text for a faulting `readonlyWhen` predicate said
"Per ADR-0057 D10 the server is the one that decides". The rule it states is
correct and unchanged — the server locks the field while the form still renders
it editable — but the citation does not resolve: `D10` of the ERP-authorization
`ADR-0057` decides Setup-nav capability surfacing, and the other `ADR-0057`
(system data lifecycle) carries no D-numbered decisions at all. An author who
followed the anchor landed on an unrelated decision and had no way to tell
whether the code or their search was wrong.

The diagnostic now states the rule on its own authority, which is where it
always rested. No behaviour, no message semantics and no rule changed — only
the traceability claim. Recording the rule as an actual decision is tracked
separately in #9628.
7 changes: 5 additions & 2 deletions docs/qa/platform-checklist/RUNNER.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,8 +74,11 @@ test-run output the clause's `evidence` field names.
this" was the actual bug.
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
(ADR-0057 D10) — where feasible, prove denial with a direct forged request.
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.)
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
written down (the golden rule of the dogfood skill).
Expand Down
9 changes: 6 additions & 3 deletions packages/lint/src/validate-expressions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1143,8 +1143,9 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => {
* whose trigger IS the unbound-root case) and
* `stripReadonlyWhenFields` deletes the value from the
* payload; client `fallback: false` ⇒ editable. The two ends
* fault in OPPOSITE directions and ADR-0057 D10 gives it to
* the server. ⇒ the old sentence was BACKWARDS here.
* 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.
* 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
Expand DownExpand Up@@ -1183,7 +1184,9 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => {
});

it('`readonlyWhen` — names the client/server disagreement, not just the server verdict', () => {
// ADR-0057 D10: the form renders the field editable (`fallback: false`)
// Server enforces, client is courtesy (cited as ADR-0057 D10, an
// attribution — #9628): 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.
const m = messageFor('readonlyWhen');
Expand Down
8 changes: 5 additions & 3 deletions packages/lint/src/validate-expressions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -699,8 +699,10 @@ export function validateStackExpressions(stack: AnyRec): ExprIssue[] {
* — and `stripReadonlyWhenFields` then DELETES the field from the incoming
* payload and lets the rest of the write through. Client:
* `resolveFieldRuleState` passes `fallback: false`, so the form renders the
* field editable. ADR-0057 D10 ("server enforces, client is courtesy")
* resolves the disagreement: the author edits the field, the save reports
* 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
* 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
* the opposite troubleshooting direction.
Expand DownExpand Up@@ -763,7 +765,7 @@ export function validateStackExpressions(stack: AnyRec): ExprIssue[] {
'the predicate faults — and the two ends fault in OPPOSITE directions. The server treats ' +
'the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not ' +
'evaluate, #4889) and drops your value from the payload, while the form still renders the ' +
'field editable (`fallback: false`). Per ADR-0057 D10 the server is the one that decides: ' +
'field editable (`fallback: false`). The server is the one that decides: ' +
'the field looks writable, the save reports success, and the value silently never lands',
requiredWhen:
'the predicate faults and the requirement is never enforced anywhere — the server logs it ' +
Expand Down
3 changes: 2 additions & 1 deletion packages/objectql/src/engine-readonly-when-parent.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,8 @@
// 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. ADR-0057 D10 puts
// 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
// 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 —
Expand Down
5 changes: 3 additions & 2 deletions packages/objectql/src/engine.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4970,8 +4970,9 @@ export class ObjectQL implements IObjectQLEngine {
* `readonlyWhen` reads, or `null` when this write cannot resolve one.
*
* `readonlyWhen: parent.status == 'paid'` is a documented **server**
* guarantee (ADR-0057 D10 puts enforcement here; the client grid is
* courtesy), but the strip is a pure function over the payload and the prior
* 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
* row — it has no driver and cannot fetch a header. So the engine resolves it
* and passes it in.
*
Expand Down
13 changes: 9 additions & 4 deletions packages/objectql/src/validation/rule-validator.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,7 +112,11 @@
* line, recorded as an addendum on that ADR alongside the same narrowing #4649
* 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 ADR-0057 D10 (server enforces, client is courtesy) is why.
* 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.
*
* ## `requiredWhen`: the SCOPE is bound, the SEMANTICS are not changed (#4977)
*
Expand DownExpand Up@@ -657,9 +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. ADR-0057 D10 puts enforcement on
* the server; a lock that fails open leaves enforcement in the courtesy
* layer. So an unbound root resolves to LOCKED — conservative toward the
* 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
* author's declared intent, and the direction #4649 (validation predicates)
* and #4775 (hook conditions) already took for their own unevaluable case.
*
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,7 +237,8 @@ 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). ADR-0057 D10 makes the
* (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.
*
* The super-user grant covers private/managed objects on the server, so folding
Expand DownExpand Up@@ -864,7 +865,8 @@ export function registerCurrentUserEndpoints(
}
// Make the client's per-object FLS reflect the server's ACTUAL
// effective enforcement = permission-set grant ∩ identity write
// guard (ADR-0057 D10). (1) Fold the `'*'` super-user grant into
// guard (server enforces, client is courtesy — cited as ADR-0057
// D10, an attribution, #9628). (1) Fold the `'*'` super-user grant into
// every object so an admin's wildcard is not shadowed by another
// set's explicit deny; (2) re-clamp `better-auth` managed objects
// by their write affordance, since the guard (ADR-0092 D2) blocks
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,9 @@ import { describe, it, expect } from 'vitest';
import { foldWildcardSuperUser, clampManagedObjectWrites, type ManagedSchemaLike } from './current-user-endpoints.js';

/**
* ADR-0057 D10 / ADR-0092 D5 — the `/me/permissions` per-object FLS map must
* Server enforces, client is courtesy (cited as ADR-0057 D10 — an attribution,
* not a resolvable anchor, #9628) / 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
* deny (most-permissive merge, no deny-wins).
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,10 @@
// `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. ADR-0057 D10 makes the
// SERVER the enforcement point and the client courtesy; here it was inverted.
// 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.
//
// 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
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,5 +4,5 @@
"ADR-0057",
"ADR-0058"
],
"invariant": "A declared field lock is the SERVER's to enforce (ADR-0057 D10the client grid is courtesy). 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. 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."
}
Loading