Skip to content

Record "server enforces, client is courtesy" as a real ADR decision — ~30 in-repo sites plus 9 in objectui cite it, and no ADR decides it #9628

Closed

Description

@os-steve

Split out of #9255 (that card's retarget half). This card is the part a dev seat may not do: authoring an ADR entry. docs/adr/** is maintainer-authored, so the #9255 implementation stopped at the traceability half and left the decision itself unrecorded.

What the corpus search found (measured at 14935ab71, whole docs/adr/ corpus, 127 records)

The rule is real and universally practised; no ADR decides it. Searched by content, not by number — courtesy, server enforces, server is the authority, client is a courtesy, enforcement point, fail-closed, UI absence, plus every decision heading matching server|client|enforc|authorit.

Candidate What it actually decides Is it the rule?
0057-erp-authorization-core-business-units-and-scope-depth.md D10 (:422) Setup-nav surfacing follows the capability (ADR-0029 K2); the object stays open No — nav-entry tiering
same file, PS-2 implementation note (2026-06-22, :516) — "D10 realized via server-enforced requiresService" The Setup nav is filtered server-side in filterAppForUser; requiresObject is "a client-side (objectui) gate, not enforced in this repo" Closest ancestor — but scoped to nav/app-metadata visibility, not a general enforcement-location rule
0057-system-data-lifecycle-and-retention.md Lifecycle/retention; uses §3.1–3.6, zero D<n> headings No
ADR-0049 (no unenforced security properties) Whether a declared property is enforced (declared = enforced, no silent fail-open) No — different axis (whether, not where)
ADR-0104 addendum, "the enforcement-point principle" (:398) Build/validate time vs runtime No — different axis (when, not where)
ADR-0111 D2 / D6 Management authority enforced in the service, not just the route Narrow instance, not the general rule

Provenance. The first commit to ever write the string is 2256e9369 — "feat: gate Setup Org/Invitations nav on multi-org, server-side (ADR-0057 D10) (#2150)". So the citation began correct and nav-scoped, and the general principle was back-formed from the PS-2 note's server-vs-client contrast. Every later site inherited the number without the scope.

Why this needs a decision record, not a comment fix

The rule is load-bearing across five packages and both repos, and it is the reason for at least three deliberate behaviours: rule-validator's unbound-root-resolves-to-LOCKED narrowing of ADR-0058 D5, the /me/permissions FLS fold in current-user-endpoints.ts, and RUNNER rule 4 ("both sides of every gate"). Those all rest on a sentence with no recorded decision behind it. #9255's PR converts the assertive citations to attributive phrasing so no reader is sent to a decision that does not say this — that removes the false trail but leaves the rule unanchored.

Proposed disposition (maintainer's to make)

The 2026-08-18 ruling on #9255 already authorized this shape: add a decision entry to 0057-erp-authorization-core-business-units-and-scope-depth.md at the next free number (D13), marked as a retroactive recording of an already-practised rule, whose text is the standing sentence and whose scope is explicitly general (not nav-only). Once it exists, the attributive phrasing landed by #9255 becomes a mechanical retarget to a resolvable anchor.

Two things worth deciding at the same time:

  1. Scope wording. The rule as practised covers visibility gates, field locks, FLS maps and route authority. If D13 is written nav-narrow it will be re-generalized by the next citer, which is exactly how this happened.
  2. Home. ERP-authorization ADR-0057 is where the lineage sits, but the rule is not ERP-specific. A general enforcement-authority ADR of its own may age better, and would sidestep the 三个 ADR 编号各自指向两个不同的已接受决策(0010 / 0019 / 0057)—— adr-anchors.json 与 328 处代码注释按裸编号引用,无法消歧 #5992 bare-ADR-0057 ambiguity permanently.

Blocking relationship

#9592 (teaching check-adr-anchors to validate that a cited Dk is a real heading of the cited ADR) is pm:blocked on the retarget. That gate cannot go green on these sites until this decision exists — attributive phrasing removes the false anchor but does not give the sites a resolvable one.

Refs: #9255, #9592, #5992, #8386, objectui#3888.


Generated by Claude Code

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Triage: promoted to pm:queue, domain:devx, type Task — this is an execution card, not a new decision. The 2026-08-18 ruling on #9255 already fixed the two things this card floats as open: home = a new decision entry in the ERP-auth ADR (0057-erp-authorization-core-business-units-and-scope-depth.md, next free number D13) and scope = explicitly general (the standing sentence, not nav-narrow). The "standalone ADR may age better" alternative re-opens a ruled point and is not commissioned; the #5992 bare-number ambiguity is mitigated by writing the entry into the ERP-auth file explicitly.

    Dispatch constraints:


    Generated by Claude Code

  2. claude commented on Aug 18, 2026

    @claude
    Contributor

    ⛔ PM escalation — the corpus search is done, and it changes the question the maintainer already answered

    Maintainer input needed. Not to re-litigate the 2026-08-18 ruling on #9255, but because that ruling rests on an assumption the search has now falsified.

    What the ruling assumed

    The ruling authorized a new decision entry on 0057-erp-authorization-core-business-units-and-scope-depth.md, described as:

    …whose text is the standing rule, marked as a retroactive recording of an already-practised decision.

    That framing makes it a transcription: the decision already exists in practice, write it down, retarget the citations, done. Under that reading the triage seat's summary was right — "the only input needed from you is the anchor's true home; everything after is dispatchable."

    What the search found

    The dev searched the whole corpus — 127 records, by content rather than by number (courtesy, server enforces, server is the authority, enforcement point, fail-closed, UI absence, plus every decision heading matching server|client|enforc|authorit). No ADR decides the general rule. The nearest candidates and why each falls short:

    Candidate Decides Is it the rule?
    ERP-auth ADR-0057 D10 + PS-2 note Setup-nav capability gating, realized server-side Ancestor, nav-scoped
    0057-system-data-lifecycle-and-retention.md Lifecycle/retention; §3.1–3.6, zero D-numbered headings No
    ADR-0049 Whether a declared property is enforced No — whether, not where
    ADR-0104 addendum, "enforcement-point principle" Build/validate time vs runtime No — when, not where
    ADR-0111 D2/D6 Management authority enforced in the service Narrow instance

    And the lineage is provable, which is the part that matters:

    • the first commit ever to write the string is 2256e9369 — "gate Setup Org/Invitations nav on multi-org, server-side (ADR-0057 D10) (feat: gate Setup Org/Invitations nav on multi-org, server-side (ADR-0057 D10) #2150)";
    • D10's own PS-2 implementation note (:516) contrasts server-side filterAppForUser filtering against a client-side requiresObject gate, described as "a client-side (objectui) gate, not enforced in this repo";
    • that contrast is the seed. Later citers kept the number and dropped the scope.

    Why this is not a transcription

    The rule as practised is broader than anything ever decided. So writing D13 is not recording an existing decision — it is making one, and the only substantive content is how broadly it is scoped.

    That is not a detail. It is the entire failure mode this card exists to fix, and it will repeat: D10 was written nav-scoped and got generalized by inheritance over ~40 in-repo sites. A D13 written nav-narrow will be re-generalized the same way by the next citer. Written too broadly, it retroactively blesses claims nobody evaluated — including in objectui, where 9 live source sites already cite it.

    So the input needed is not "the anchor's true home". It is the scope wording, which is a judgement about how much of the platform's enforcement posture you are willing to declare in one line.

    The two questions

    1. Scope. Is "server enforces, client is courtesy" a general platform invariant (every enforcement decision belongs to the server; any client-side check is advisory), or is it bounded to a named surface set (app/nav metadata visibility, field locks, validation rules) with everything else out of scope until separately decided?
    2. Home. The ruling proposed the ERP-authorization ADR-0057. If the answer to (1) is "general platform invariant", that file may be the wrong home — it is an ERP-authorization record, and a general invariant filed there inherits a scope it does not want. A different or new ADR may be right.

    Recommended route

    C — you author the D-entry directly (it is docs/adr/**, governed, and the content question above is a judgement, not a transcription); a follow-up dev card then does the mechanical retarget of both groups once a resolvable anchor exists.

    Route B — a dev seat writes the entry as a draft PR for human merge — is authorized by your ruling and remains available. I am recommending against it only because the corpus search moved this from transcription to authorship. If you would rather a seat draft it for you to edit and merge, say so and I will dispatch it that way; the evidence above is everything a drafter would need.

    Already shipped and un-regrettable either way

    PR #9655 converted 14 sites across 10 files to attributive phrasing — "the rule the framework cites as ADR-0057 D10" rather than asserting the anchor resolves. That is correct whether or not D13 is ever written, and it follows objectui#3888's precedent under the same uncertainty. It removes the false trail without inventing a destination.

    Blocked on this: #9255's retarget half, and #9592 (the check-adr-anchors decision-letter validation) — which cannot land until the known-bad citations have a real anchor, or it goes red on exactly the citations it exists to catch.

    Related: #9255 · PR #9655 · #9592 · #5992 · #8386 · objectui#5202


    Generated by Claude Code

  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Maintainer ruling recorded (2026-08-18, live chat with the triage session session_014v3dpZ2224xgf98dfY4rw4; authorization verbatim: 「其他接受你的建议」, accepting the recommendation presented in that session, restated as the operative ruling).

    Ruling: record it as an INDEPENDENT general ADR (new number), not as ADR-0057 D13. Reasons on record: this card's own provenance measurement shows nav-narrow wording gets re-generalized by the next citer — so the scope must be written general (visibility gates, field locks, FLS maps, route authority) from the start; and the bare-ADR-0057 ambiguity (#5992) has already cost once, so the general rule gets a clean home. Content: the standing sentence — the server enforces; client-side gating is a usability courtesy — marked explicitly as a retroactive recording of an already-practised rule, with the lineage note (ADR-0057 PS-2 as the closest ancestor). ADR-0057 gets a one-line pointer at the PS-2 note to the new ADR.

    Process: dev drafts the ADR + pointer as a draft-only PR; docs/adr/** is a governed surface — ⛔ human merge only, the merge is the maintainer's review act. After it lands, the #9255 attributive phrasings retarget mechanically and #9592's anchor gate can go green on these sites.

    Label: needs-user-decision → pm:queue, domain:devx.


    Generated by Claude Code

  4. os-steve commented on Aug 19, 2026

    @os-steve
    CollaboratorAuthor

    Claim — dev seat starting work on this card.

    • Session ID: session_01XqDQYVU5smx29ts9pAErja
    • Branch: claude/issue-9628-server-enforces-adr (pushed, empty, as the write-path probe)
    • Container & model: L, mode:subagent, model: opus

    Working the 2026-08-18 maintainer ruling as recorded above: an independent general ADR at a newly allocated number (not ADR-0057 D13), scope written general from the start (visibility gates, field locks, FLS maps, route authority), marked as a retroactive recording of an already-practised rule with the ADR-0057 PS-2 lineage note; plus a one-line pointer at ADR-0057's PS-2 note.

    Scope is the ADR + the pointer only. No citation retargeting in this PR (that is #9255's half, sequenced after this lands). The PR will be opened draft and left draft — docs/adr/** is a governed surface and the maintainer's hand-merge is the review act.


    Generated by Claude Code

  5. os-steve commented on Aug 19, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 9628,
      "status": "done",
      "branch": "claude/issue-9628-server-enforces-adr",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9856",
      "premise_still_valid": true,
      "summary": "PR #9856 is DRAFT and UNARMED (draft:true, merged:false, no auto-merge request, not enqueued) and must stay that way -- the maintainer's hand-merge is the review act per the 2026-08-18 ruling. It adds docs/adr/0124-server-enforces-client-is-courtesy.md, an independent GENERAL ADR at a newly allocated number (not ADR-0057 D13), and a one-line pointer blockquote at ADR-0057's PS-2 note. H1 (structure): ADRs here carry NO front-matter (zero of 127 records do -- check-adr-anchors' header rejects it explicitly), so the shape is a `# ADR-NNNN: sentence` title plus bold Status/Deciders/Builds on/Consumers/Surfaced by lines, `---`, then TL;DR / Context / Decision / Consequences; there is no ADR registry to register in (PRIORITIZATION.md self-declares STALE and is a historical snapshot, not an index) and no allocator script -- the real constraint is check-adr-anchors' number-uniqueness audit, whose header says take the next free number rather than add to KNOWN_NUMBER_COLLISIONS. Highest existing stem is 0123, ADR-0124/0125 are cited nowhere in the tree, no open PR adds an ADR, so 0124 is the allocated number and the gate now reports 122 decision numbers with no new collision. Scope is written general by a TEST rather than a surface list (D2), which is the anti-re-narrowing requirement the ruling turns on; D3 bounds it to 'where, not which way' so ADR-0058 D5's failure tiers are not amended by implication, and D6 blocks the opposite over-read (it does not license deleting client-side gating -- 9 live objectui sites depend on that layer existing). The ~40 citations are deliberately NOT retargeted here; that is #9255's half, sequenced after.",
      "tests": "Gate set DERIVED, not recalled: `node scripts/pm/dispatch-gates.mjs docs/adr/0124-... docs/adr/0057-...` -> exactly 3 families (check:adr-anchors, check:pm-governed-merges, check-adr-links), plus check:nul-bytes for the any-edit clause. Full union re-run AFTER the final commit, at `git rev-parse --short HEAD` = 7dd683446, all green: `check-adr-anchors --self-test: 74 assertions over the real auditAdrDirectory() / auditCitedNumbers() / assembleAnchors() paths.` / `check-adr-anchors: OK (51 anchored file(s), every governing ADR still referenced; 122 decision number(s), each naming one decision or an allowlisted pair; 26448 citation(s) across 3361 file(s) resolve).` / `check-adr-links: 551 relative link destination(s) under docs/adr/ resolve` (self-test: discrimination, census, ADR-0046 pin and baseline staleness all verified) / `check-nul-bytes: OK (scanned 6282 text file(s) -- 6282 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).` / `check-governed-merges --self-test: 77 assertions`. Manual control-char scan of both edited files (grep -naP over the C0 range + DEL): no hits. No build/test needed -- docs-only, no package source touched, so the shared heavy-verify lock was never taken. No ablation applies. H2 sample-citation check (7 sites across the named classes -- rule-validator.ts fail-CLOSED, validate-expressions.ts author diagnostic, RUNNER.md rule 4, current-user-endpoints.ts FLS fold, app.zod.ts visibility, rest-server.ts route authority, the adr-anchors invariant shard): all 7 would be TRUE citations of the text as written, so no widening was needed after drafting. GitHub sanitizer: the first PR body lost `<n>` from the H4 measurement sentence (`### D<n>` -> `### D`); caught by reading the body back, respelled `<`-free, and re-verified by a second readback. Label union written additively after a re-read that caught a bot adding `documentation` between reads; readback confirms [documentation, size/m, skip-changeset].",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    H4 verdict, stated separately because it was a near-miss design constraint

    #9592 is genuinely unblocked by this — but only because ADR-0124's decisions are ### markdown headings rather than bold paragraph leads.

    Measured across the corpus: 46 records write decisions as headings (### D1 — …), 12 as bold leads (**D1 — …**). The newest record, ADR-0123, is in the bold-lead group — so copying the most recent house style would have been the natural move and the wrong one. #9592's gate verifies a cited ADR-NNNN Dk resolves to a real decision heading; ADR-0057, the record that gate must handle first, is in the heading group, so a heading-blind parser cannot exist. Had ADR-0124 used bold leads, a citation of ADR-0124 D1 would still not anchor and #9592 would stay blocked on exactly the sites it exists to fix — discovered only after a governed human merge.

    One residual dependency #9592 still has, which this card does not remove: the anchor gate reads ADR-NNNN Dk citations in anchor invariants and anchored-file comments. Those sites still say ADR-0057 D10 today. They become resolvable-and-correct only once #9255's retarget points them at ADR-0124 Dk. So the ordering in the ruling holds exactly as written: this ADR lands → #9255 retargets → #9592 can go green. This card removes the missing-anchor blocker, not the retarget itself.

    H3 note — which ADR-0057 was edited

    The pointer went into 0057-erp-authorization-core-business-units-and-scope-depth.md, verified in both directions: it carries ### PS-2 implementation note (2026-06-22), and 0057-system-data-lifecycle-and-retention.md has no PS-2 note and zero D-numbered headings. The pointer is written for a reader who arrived from a wrong citation — it names the general rule, says D10 does not decide it, and sends them to ADR-0124.

    ⛔ For the PM / maintainer

    PR #9856 is draft and unarmed and must not be flipped ready, auto-merged or enqueued by any seat. docs/adr/** is governed; the maintainer's hand-merge is the review of the ADR-0124 wording.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions