Repository navigation
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
Description
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:
- 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.
- 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-0057ambiguity 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
os-support-ai commentedon Aug 18, 2026
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:
docs/adr/**): draft-only PR, never ready/queued/auto-merge — the maintainer's hand-merge is the approval of the D13 wording. The PR body must quote the [finding]ADR-0057 D10("server enforces, client is courtesy") names no decision in either ADR-0057 — ~20 sites plus anadr-anchors/invariant ride the anchor #9255 ruling as provenance.ADR-0057 D10("server enforces, client is courtesy") names no decision in either ADR-0057 — ~20 sites plus anadr-anchors/invariant ride the anchor #9255's retarget PR is in flight (assignee os-steve) — the D13 entry and the retarget touch disjoint files by design, but confirm before claiming; the retarget's attributive phrasing becomes a mechanical re-anchor once D13 exists.check-adr-anchorsshould verify that a citedADR-NNNN Dkdecision letter is a real heading of the cited ADR #9592 stayspm:blockeduntil this lands; objectui#5202 has been queued independently on the attributive form and does not wait for this.Generated by Claude Code
docs(adr-anchors): make the `ADR-0057 D10` citation attributive where…
claude commentedon Aug 18, 2026
⛔ 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: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 matchingserver|client|enforc|authorit). No ADR decides the general rule. The nearest candidates and why each falls short:ADR-0057D10 + PS-2 note0057-system-data-lifecycle-and-retention.mdAnd the lineage is provable, which is the part that matters:
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)";:516) contrasts server-sidefilterAppForUserfiltering against a client-siderequiresObjectgate, described as "a client-side (objectui) gate, not enforced in this repo";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
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-anchorsdecision-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
os-support-ai commentedon Aug 18, 2026
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-0057ambiguity (#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
os-steve commentedon Aug 19, 2026
Claim — dev seat starting work on this card.
session_01XqDQYVU5smx29ts9pAErjaclaude/issue-9628-server-enforces-adr(pushed, empty, as the write-path probe)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
docs(adr): record "server enforces, client is courtesy" as ADR-0124, …
os-steve commentedon Aug 19, 2026
{ "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 citedADR-NNNN Dkresolves 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 ofADR-0124 D1would 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 Dkcitations in anchor invariants and anchored-file comments. Those sites still sayADR-0057 D10today. They become resolvable-and-correct only once #9255's retarget points them atADR-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), and0057-system-data-lifecycle-and-retention.mdhas 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
docs(adr-anchors): make the ADR-0057 D10 citation attributive where i…
docs(adr): record "server enforces, client is courtesy" as ADR-0124, …