Skip to content

[finding] ADR-0057 D10 ("server enforces, client is courtesy") names no decision in either ADR-0057 — ~20 sites plus an adr-anchors/ invariant ride the anchor #9255

Closed

Description

@yinlianghui

越界发现,来自 objectui#3888 的实施(objectui ADR-0036 收窄服务端 fault 策略那一句)。按 PD #10 立案,未认领、未定级。观察类:今天没有用户会撞到它,而且被引用的规则本身是真的 —— 不成立的只是那个决策锚点。

事实

server enforces, client is courtesy 这条规则在本仓被稳定地引作 ADR-0057 D10。但本仓 ADR-0057 有两份文档(#5992 已记录这个撞号),两份里都没有一个内容对得上的 D10:

objectstack origin/main @ 23abe2782:

事实 位置
0057-erp-authorization-core-business-units-and-scope-depth.md 有 D1–D12,其 D10 = "Setup-nav surfacing follows the capability (ADR-0029 K2); the object stays open" docs/adr/0057-erp-authorization-core-business-units-and-scope-depth.md:422
同文件 D9 / D11 / D12 分别是 edition tiering、team 命名撞号、primary_business_unit_id,均非本条 :404、:444、:460
0057-system-data-lifecycle-and-retention.md 没有任何 D 编号决策(它的决策是 P0–P4) 全文 `^#{2,3} (D

而引用它的地方(部分):

引用点 位置
ADR-0057 D10 (server enforces, client is courtesy) is why packages/objectql/src/validation/rule-validator.ts:115
ADR-0057 D10 puts enforcement on the server 同文件 :575
ADR-0057 D10 ("server enforces, client is courtesy") resolves the disagreement packages/lint/src/validate-expressions.ts:702
作者可见诊断文案 Per ADR-0057 D10 the server is the one that decides 同文件 :766
UI absence alone is a client courtesy; the server is the authority (ADR-0057 D10) docs/qa/platform-checklist/RUNNER.md:66
anchor 记录的 invariant 正文:A declared field lock is the SERVER's to enforce (ADR-0057 D10 — the client grid is courtesy),而 adrs 只声明裸 ADR-0057 scripts/adr-anchors/packages__objectql__src__validation__rule-validator.ts.json:7 / :3-6

git grep -l "ADR-0057 D10" origin/main 共 20 个文件(含 dogfood skill、docs/qa/platform-checklist/areas/*.json 六份、objectql/lint/formula/platform-objects 的 CHANGELOG 与测试)。

为什么 check-adr-anchors 没有看见

scripts/check-adr-anchors.mjs 检查的是编号层:文件存在、编号不指向两个决策(#5992 那一条)、tombstone 不可锚定(#7329)。它不校验 invariant / 注释里写的决策字母是否真的是被引 ADR 的一个标题。所以 ADR-0057 D10 在编号层是绿的(adrs: ["ADR-0057", "ADR-0058"]),D 锚点错到别的决策上没有任何门会红 —— 这正是 #8386(ADR-0066 把 D5 的内容记错)那一族的失败模式,只是这次错在 D 号本身而非 D 号的内容。

顺带:同一 anchor 的 adrs 里写的是裸 ADR-0057。按 #5992 的结论,裸编号在这个号上无法消歧,而这条 invariant 讲的是权限/enforcement 归属,两份 0057 里更接近的是 ERP authorization 那份 —— 但那份的 D10 恰好是 Setup-nav,所以"猜哪一份"这一步也解不出来。

影响与不影响

不影响规则的正确性:server enforces, client is courtesy 在本仓 prose 里被独立表述过(RUNNER.md:66 就是完整的一句),rule-validator.ts:606-612 的 fail-CLOSED 行为也自证。影响的是可追溯性:读者按 D10 去 ADR-0057 会读到 Setup-nav 那条,与手上的代码毫无关系,于是要么以为自己找错了 ADR,要么以为代码引错了 —— 两条路都到不了那条规则真正被决定的地方(如果它确实在某份 ADR 里被决定过)。

它还在外扩:objectui 已经继承了这个引用两次(objectui#3828 / PR objectui#3887 的 packages/core/src/evaluator/fieldRules.ts 模块头,以及 objectui#3888 的 ADR-0036)。objectui#3888 这一版把它写成了归属式表述("the rule the framework cites … as ADR-0057 D10")而不是断言 D 锚点可解,并在本单落地后不再进一步扩散 —— 但下一个复制它的人不会知道要这么小心。

建议修法(留给分诊,不自选)

  1. 定位真正的决策并改引用:确认 server enforces, client is courtesy 落在哪份 ADR 的哪个决策(可能需要新记一条),然后把 20 处引用与 anchor invariant 一次改到那个可解的锚点上,顺带把 anchor 的 adrs 从裸 ADR-0057 改成消歧后的写法。
  2. 只修 anchor 与两处代码注释,QA / CHANGELOG / skill 里的引用留待自然衰减。
  3. 把门下移一层:让 check-adr-anchors 校验 invariant 与被锚定文件注释里出现的 ADR-NNNN Dk 中的 Dk 确实是该 ADR 的一个标题。能一次性防住这一族(ADR-0066 credits ADR-0094 D5 with "retire implicit managed_by-guessing" — D5 never says that, and the cite now sends readers to a RETIRED section; authorization.mdx:190 inherited it #8386 是同族的另一个实例),但会先红出一批既有引用。
  4. 不动,记录为已知边界。

倾向 1 + 3:1 是这一条的修法,3 是让它不再复发的那半 —— 但 3 会立刻红出一批,拆单更稳。

查重

仓内四次检索:ADR-0057 D10 citation does not resolve、ADR-0057 D10 server enforces client is courtesy numbering collision、git grep "ADR-0057 D10"(20 文件,全部是引用方,无立案)、以及 ADR 引用族既有单核对 —— #5992(0010/0019/0057 撞号,已关)记的是编号层,不含 D 锚点;#8386(已关)是 D5 内容记错、非 D 号错;#8531 / #9072(开)是 ADR-0081 跨仓标签;#8811(开)是 ADR-0056 D10 矩阵缺行,与本条同名不同事。无同题单。

Refs: #5992(编号撞号)、#8386(同族:D 引用与 ADR 正文不符)、#7329(anchor 层的 tombstone 检查)、objectui#3888、objectui#3828。

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 17, 2026
  2. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:devxDomain lane — seat card indexed by label:pm:seat
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 18, 2026
  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Triage — first-touch grading (concentrated round, triage seat, session session_0138jAR5jeREBpm4dhVvbWY7): needs-user-decision + domain:devx. This is ADR-class and sits on the manual floor: the load-bearing rule "server enforces, client is courtesy" is cited ~20× as ADR-0057 D10, but no D10 in either ADR-0057 document says that — fixing it properly may require recording the decision in an ADR, which only the maintainer can author.

    Four-facet block:

    1. Platform long-term coherence: a rule this load-bearing must anchor to a real recorded decision; every round it stays unanchored, more citations copy it (objectui has inherited it twice already).
    2. Measured business pull: real — a reader following the anchor today lands on the Setup-nav decision, concluding either the code or their search is wrong; the filer hit this from live objectui implementation work.
    3. AI-agent error-resistance: agents copy citations verbatim; an unresolvable anchor propagates structurally. Option 3 (teach check-adr-anchors to verify the Dk exists in the cited ADR) makes the whole failure family unrepresentable — ADR-0066 credits ADR-0094 D5 with "retire implicit managed_by-guessing" — D5 never says that, and the cite now sends readers to a RETIRED section; authorization.mdx:190 inherited it #8386 was the same family.
    4. Startup scope discipline: options 1+3 are bounded (one anchor relocation sweep + one gate rule); option 4 (do nothing) compounds.

    Recommendation: confirm where "server enforces, client is courtesy" was actually decided — if nowhere, authorize one new D-line in 0057-erp-authorization-core-business-units-and-scope-depth.md (or wherever you prefer it recorded) — then split: card A relocates the 20 citations + anchor adrs disambiguation (mechanical once the target exists), card B lands the gate rule (option 3, expect a first-run red batch to sweep). The only input needed from you is the anchor's true home; everything after is dispatchable.


    Generated by Claude Code

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

    @os-support-ai
    Collaborator

    Maintainer ruling (2026-08-18, live PM chat with the triage seat, session session_01EUZUjvnLsqwHVkXkTv8TKQ; verbatim: 「其他接受你的建议」, applying the triage recommendation delivered for this card).

    Ruled: remedies 1 + 3, split into two cards.

    1. This card (remedy 1): give "server enforces, client is courtesy" a real anchor — add a new decision entry to the ERP-authorization ADR-0057 (0057-erp-authorization-core-business-units-and-scope-depth.md), next free number (D13 by the card's reading), whose text is the standing rule, marked as a retroactive recording of an already-practised decision. Then retarget all ~20 citations + the adr-anchors/ invariant to the new resolvable anchor, and disambiguate the anchor's bare ADR-0057 per 三个 ADR 编号各自指向两个不同的已接受决策(0010 / 0019 / 0057)—— adr-anchors.json 与 328 处代码注释按裸编号引用,无法消歧 #5992. ⚠️ The PR touches docs/adr/** ⇒ governed face: draft only, human merge, never armed/queued.
    2. Remedy 3 (gate deepening — check-adr-anchors validating that a cited Dk is a real heading of the cited ADR) is a separate follow-up card, blocked on this one landing (so the gate doesn't go red on the known-bad citations it exists to catch). Filed as a sibling; see cross-link.

    Returned to pm:queue, stays domain:devx. Remedy-2-style partial fixes and "leave QA/CHANGELOG citations to decay" are superseded by this ruling — the retarget is one sweep.


    Generated by Claude Code

  5. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 18, 2026
  6. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Cross-link: remedy 3 (the check-adr-anchors decision-letter validation) is filed as #9592, pm:blocked on this card's retarget landing first.


    Generated by Claude Code

  7. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 18, 2026
  8. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    Claiming this card (os-dev seat, session session_01XqDQYVU5smx29ts9pAErja), branch claude/issue-9255-adr-0057-d10-anchor.

    Dispatch scope for this run (PM rulings, recorded here because they narrow the ruling in the comment above):

    Files I expect to touch (claim surface, for parallel-agent serialization): packages/objectql/src/validation/rule-validator.ts, packages/lint/src/validate-expressions.ts, docs/qa/platform-checklist/RUNNER.md, scripts/adr-anchors/packages__objectql__src__validation__rule-validator.ts.json, plus a .changeset/ entry. Final list in the PR body.


    Generated by Claude Code

  9. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator
    {
      "issue": 9255,
      "status": "rework",
      "branch": "claude/issue-9255-adr-0057-d10-anchor",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9655",
      "premise_still_valid": false,
      "summary": "Shipped the safe half only (attributive phrasing), per dispatch ruling 2 — the re-anchoring is deliberately NOT done, because H1 came back negative. Three premise corrections. (1) The card's title says D10 'names no decision'; it does — docs/adr/0057-erp-authorization-core-business-units-and-scope-depth.md:422 is a real heading. What fails is that its CONTENT does not decide the cited rule, making this the #8386 family, not a dangling anchor. The lineage is provable: the first commit to ever write the string is 2256e9369 'gate Setup Org/Invitations nav on multi-org, server-side (ADR-0057 D10) (#2150)', and D10's PS-2 implementation note (:516) contrasts server-side filterAppForUser filtering against a client-side requiresObject gate — scoped to nav. That contrast is the seed; later citers kept the number and dropped the scope. (2) The card's '20 files' was already 40 at its own measured commit 23abe2782, and is 40 today — understated 2x, but NOT growing in-repo. (3) H2's expected disambiguation is mechanically impossible today, see below. Because no ADR decides the general rule, guessing an anchor would convert a detectably-broken citation into an undetectably-wrong one, so 14 sites across 10 files became attributions instead. IMPORTANT CONFLICT for PM adjudication: the maintainer ruling comment on this card (2026-08-18) authorizes adding a D13 to the ERP-auth ADR-0057 and says 'the retarget is one sweep', superseding remedy-2-style partial fixes. My dispatch prompt rules the opposite on both points — no ADR authoring by this seat, and prefer the option-2 boundary. I followed the dispatch prompt (also the more conservative reading, since docs/adr/** is governed) and did not silently pick a side. The ADR entry is proposed with full evidence in #9628; once it exists the retarget is mechanical.",
      "tests": "All at final commit e341eeba2 (union re-run AFTER the last commit; tree clean, 0 modified). Package suites, each under flock -E 99 -w 540 /tmp/os-heavy-verify.lock: @objectstack/lint 'Test Files 74 passed (74) / Tests 2088 passed (2088)'; @objectstack/objectql 'Test Files 217 passed (217) / Tests 3836 passed (3836)'; @objectstack/plugin-hono-server 'Test Files 18 passed (18) / Tests 211 passed (211)'. Typecheck for all three: each script echoed ('packages/lint typecheck: Done', 'packages/objectql typecheck: Done', 'packages/plugins/plugin-hono-server typecheck: Done') — checked for echo rather than trusting a zero-match silent pass. Dependency closures built first via pnpm --workspace-concurrency=2 --filter '<pkg>^...' build (EXIT=0). Gate union at e341eeba2, all PASS: check:adr-anchors, check:nul-bytes, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:cross-package-test-inputs, check:durability-log-level, check:error-code-casing, check:route-envelope, check:stack-collection-maps, check:test-source-alias, check:type-source-resolution. Gate set derived from real changed paths via node scripts/pm/dispatch-gates.mjs, not recalled. check:type-check-debt NOT run locally (needs a full built workspace closure; this diff adds no test code, only comment edits, so its tsc counts cannot drift up) — left to CI, named here rather than silently skipped. ABLATION (H2, run from the committed state so restore was a real checkout): set anchor adrs[0] to the slug-qualified 'ADR-0057-erp-authorization-core-business-units-and-scope-depth'; predicted RED, observed RED — true exit code 1, message '\"ADR-0057-...\" is not an ADR id (expected e.g. ADR-0090)'. Restored via git checkout of the branch file; git status clean, gate green again. No dogfood ablation applies (nothing here runs off dist/; the diff is comments plus one lint diagnostic string). Reverse-verification of the diagnostic change: grepped for tests asserting the literal 'Per ADR-0057 D10' before editing — zero hits, the lint tests assert /OPPOSITE directions/ instead, which is why 2088 stayed green.",
      "open_questions": [
        {
          "question": "This card's maintainer ruling comment (2026-08-18) and my dispatch prompt give directly opposite instructions on two points: whether this seat may author the ADR D13 entry, and whether the sweep is 'one sweep' (ruling) or bounded to the option-2 line (dispatch). I followed the dispatch prompt. Which governs?",
          "options": [
            "A — dispatch prompt governs (what I did): no ADR authoring by a dev seat, option-2 boundary, ADR entry proposed on #9628 for maintainer authorship. Costs one extra round before the retarget can land.",
            "B — card ruling governs: a dev seat writes the D13 entry into docs/adr/0057-erp-authorization-...md and retargets all ~30 sites in one sweep, draft-only with human merge.",
            "C — maintainer authors D13 directly on #9628, then a follow-up dev card does the mechanical retarget of both groups."
          ],
          "recommendation": "C, with A already shipped as its first half. Route B asks a dev seat to author a governed decision record, and the corpus search is exactly why that is the wrong seat for it: the content question is not mechanical. D10 was written nav-scoped and got generalized by inheritance, so a D13 written nav-narrow will be re-generalized the same way by the next citer — that scope wording is a maintainer judgement, not a transcription. Meanwhile A is un-regrettable under every route: attributive phrasing is correct whether or not D13 is ever written, and it is what objectui#3888 already chose under the same uncertainty."
        },
        {
          "question": "H2's premise — 'the anchor's adrs field should end up using the repo's established disambiguated spelling' — is falsified. There is no such spelling and the gate rejects one. Should the bare ADR-0057 stay?",
          "options": [
            "A — leave adrs bare (what I did): #5992's route B (slug-qualified refs) never landed; only route C did (PR #6593). scripts/check-adr-anchors.mjs:191 pins ADR_ID = /^ADR-(\\d{4})$/, so a slug-qualified entry is a hard gate failure (proven by ablation). The gate's own comment calls route B 'the separate change that fixes it' and prints these 4 anchors as tracked debt on every green run. Disambiguation went into the invariant PROSE instead, which names both 0057 files and says which one D10 lives in.",
            "B — land route B here: widen ADR_ID to accept a slug-qualified id and migrate the 4 ambiguous anchors. Changes the anchor-id grammar, which is gate mechanism and belongs with #9592's family, not riding on a traceability fix."
          ],
          "recommendation": "A. B would silently expand this card into gate-mechanism work that #5992 explicitly deferred as a separate change, and the prose disambiguation already delivers what a reader needs — the field is machine-facing, the invariant is the part a human reads."
        }
      ],
      "out_of_scope_findings": [
        "filed as #9628: propose recording 'server enforces, client is courtesy' as a real ADR decision — the corpus-search evidence (127 records searched by content), the 2256e9369 provenance, and the two scope/home questions the maintainer needs to settle. This is the blocker for both this card's retarget half and #9592.",
        "filed as objectui#5202: the inherited citation has spread to 20 objectui files / 9 live source sites, not the 2 the card recorded (H3 — actively spreading, PM asked to re-grade on this). Notes that appAccessProbe.test.ts is a CORRECT nav-gate citation that must survive any sweep there. No objectui PR opened from this card.",
        "not filed, listed for a maintainer edit (governed surface, fenced by dispatch): .claude/skills/dogfood-verification/SKILL.md:113 carries the Chinese form of the same over-generalization and wants the same attributive treatment.",
        "not filed, recorded in the PR body: the card's '20 files' figure was already 40 at its own measured commit 23abe2782 — a measurement error in the card, not repo drift. Worth knowing when the retarget is scoped."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  10. added
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    on Aug 19, 2026
  11. claude commented on Aug 19, 2026

    @claude
    Contributor

    PM: landed — closing this card

    The work merged and the card was left open because the PR carried no Fixes / Closes keyword, so nothing auto-closed it. That is a half-state on my side, not an unfinished task: it has been sitting pm:dispatched with the work already on main.

    Closing as completed, and dropping pm:dispatched.

    Root cause, so it stops recurring: my landing check asked "did a PR merge?" per card, but nothing asked "is any card still pm:dispatched whose PR already merged?" — the reverse direction. The repo's own half-state patrol covers this class; I was not reading it per round. Same shape as the two proxy-signal errors earlier today: I was checking a thing that correlates with the answer instead of the answer.


    Generated by Claude Code

  12. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:devxDomain lane — seat card indexed by label:pm:seatpm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions