Skip to content

docs(adr): ADR-0123 — layered master data, template/override linkage and the resolution organization (#4585) - #6668

Closed
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-4585-adr-d10-layered-master-data
Closed

docs(adr): ADR-0123 — layered master data, template/override linkage and the resolution organization (#4585)#6668
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-4585-adr-d10-layered-master-data

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4585

Discharges the ADR-0105 D10 reservation ("Mechanics … to be detailed in a follow-up ADR; this ADR reserves the concept and its place in Phase 2"). Per the 2026-08-06 maintainer ruling the first deliverable is the ADR, not code — so this PR is one new file and nothing else: docs/adr/0123-layered-master-data-template-override-and-the-resolution-organization.md. Status is Proposed, deliberately not Accepted.

Docs-only ⇒ the PM will apply skip-changeset at acceptance.

Three measurements that shaped the draft

None of these existed when D10 was reserved; all were taken against main.

  1. The group wall has no NULL arm, and the driver's does. Layer 0 under group is { organization_id: { $in: accessible_org_ids } } (plugin-security/src/tenant-layer.ts:139-143), lowering to SQL IN (…) — NULL-false. The SQL driver's own tenant chokepoint carries the opposite carve-out on both arms (sql-driver.ts:6369, :6382, the Fresh objectstack dev boot: tenant admin sees ZERO rows in sys_position / sys_permission_set / sys_business_unit over REST (Setup Access Control renders empty) #2734 fix). The two AND together, so an org-less "platform-global, read-shared" template row is actively excluded today, and W1/W2 mean no Layer 1 mechanism (sharing, OWD public_read, sys_record_share, the superuser bit) can restore it.
  2. group posture deleted the resolution organization. D2 demoted the active organization to "default write target, UI context" and made membership the read bound. But "org override wins" is only defined relative to one organization, and a union reader spanning root + plant A + plant B has several valid answers. The reserved text's resolution rule needs a resolution organization before it means anything.
  3. Walking the org tree in an authored rule is lint-forbidden. D6 red line ① is error severity (packages/lint/src/validate-org-axis-red-lines.ts), so "just author a policy that reaches up to the group org" is closed, not overlooked. Resolution has to be engine machinery.

The five decisions #4585 asked for

# Issue point Decision Recommendation
D2 ① linkage Declared natural key (layered: { key: 'code' }) riding ADR-0120's unique: 'organization' / COALESCE(organization_id, '__global__') Natural key over a self-lookup: a row id is minted per deployment (the same argument D9 used to reject ids in flows), it survives per-org seed replay and package export unchanged, and it avoids a lookup that points across the wall and would need a privileged read on an ordinary write path. Cost stated: no DB-level referential integrity; key renames need a real discipline, not a promise. The ADR-0092 whitelist route named in the issue is a category error — that guard is registry-driven off managedBy: 'better-auth' and has no jurisdiction over a business master object.
D4 ② resolution point Read path, explicit, naming its organization No implicit collapse. Default = raw rows (wall semantics untouched); resolution is asked for, and a union reader gets one resolved row per organization ("the master as each plant sees it") rather than one globally-collapsed row. A silent-collapse default makes count(*) disagree with the grid with no way to tell which row won — fatal for the consolidated reporting the union wall exists for, and the worst possible read for an AI author. Two mechanical constraints: resolution must not ride DriverOptions.tenantIds (that channel's fail-toward-isolation fallback becomes fail-toward-wrong-answer for a projection), and it cannot be authored metadata. A view layer is a consumer of resolution, not the seam — a view cannot bind the read path a flow, action, export or AI tool call takes.
D3 / D5 ③ policies + write authority Org-less template + one narrow declared Layer 0 opening; group_controlled / graded / free declared per object Template row is org-less, with Layer 0 widened to $or: [{ organization_id: { $in: … } }, { organization_id: null }] only for layered objects — narrow, declared, and read-only by construction (the existing post-image check already denies any tenant write carrying a foreign org, NULL included). This reconciles a disagreement the platform already has rather than inventing a rule. Policies are enforced on WRITE: a graded divergence on a non-overridable field is refused, never quietly resolved back to the template on read (ADR-0049 — a read-time overwrite means the table and the screen disagree forever). No new permission axis: template writes are already narrowed to platform-admin/system contexts, which is exactly the publish authority the commercial tooling runs as; override rows keep ordinary object permissions and FLS.
D7 ④ open/commercial Stated per cloud ADR-0016 铁律 and D12's code-vs-activation amendment Open: the layered declaration and schema, the policy enforcement, the resolution algorithm, the linkage/uniqueness gates, the D3 Layer 0 term. Commercial: publish/sync tooling, distribution policy UI, per-org replay, divergence reporting. Activation is entitled and fail-fast: a deployment declaring layered without the org-scoping probe refuses to boot, on the ADR-0093 D5 guard. Written out explicitly because the alternative reading — "the declaration silently does nothing in the open edition" — is exactly the silent-degradation hole #3570 closed, and it would land here by default if nobody said otherwise.
D6 ⑤ grouping vocabulary Reuse D6/D9 verbatim $root / $parent (APPROVER_ORG_SYMBOLS) for the publish target, "shares a parent_organization_id root" for the group boundary, refuse-don't-ignore for posture mismatch. Nothing new minted — no layered-specific hierarchy, no distribution tree, no publisher role.

Plus D1 (layered is an object-level declaration that refuses outside the group posture rather than idling — D9's amendment point 4 and ADR-0078 applied to the one key whose inertness would be a governance incident) and D8 (named non-goals: no template versioning/effectivity dating, no sub-field merge, no cross-group distribution, no Layer 0 change for non-layered objects).

The one thing left open on purpose

D3 has a fork the draft does not take, because it isn't a developer's to take: an org-less template is installation-global, not group-global. Making it group-scoped requires resolving "the root of my group" at authorization time — the substance of D6 red line ①, even if an engine-owned Layer 0 computation escapes its letter.

  • T2-flat (recommended): installation-global templates, no tree walk, multi-group-per-environment declared out of scope (a second group is a second environment, as it already is for physical isolation).
  • T2-rooted: buys multi-group-per-environment; costs an engine-owned exception to the org-axis red line and a tree read on the authorization hot path.

Both are cheap now and neither is cheap after templates are published on live data — which is why it's put to the maintainer rather than defaulted.

cloud-side inputs

The objectstack-ai/cloud repo is not reachable from the drafting session. Appendix A records cloud#874 (demand side + the 2026-07-27 founder ruling on the iron rule), cloud#2937 (cross-org mirroring — if it already defines a reconciliation vocabulary, D4 should reuse it rather than mint one) and cloud#881/#884 (per-org seed/config replay, which D2's portability argument leans on) as inputs to verify, not as established facts.

Gates

Every check:* step in .github/workflows/lint.yml applicable to a docs-only change, enumerated and run one by one in the worktree — all green:

Gate Result
check:nul-bytes ✅ 6220 tracked text files, no raw control bytes
check:doc-authoring ✅ 366 files clean
check:docs-audit-scope ✅ in sync, 178 hand-written docs
check:role-word ✅ 44 baselined files, no new occurrences
check:quick-reference-counts ✅ 13 sections match their tables
check:adr-anchors ✅ 118 decision numbers, each naming one decision — 0123 is unique
check:org-identifier ✅ 1703 files, no removed alias

Also verified by hand: all 8 relative ADR links resolve (the check-links.yml lychee job runs offline over relative paths). Not applicable: docs-drift-check.yml (triggers on packages/** only); every typecheck/spec/build gate (no code, spec or generated artifact touched). No content/docs/releases/ edit.

On acceptance

ADR-0105's D10 entry, its "D10 mechanics — reserved, follow-up ADR" non-goal line, and its Status line's "D10 stays reserved pending its follow-up ADR" should be updated to point here — a docs-only follow-up, deliberately not bundled into this record.

🤖 Generated with Claude Code

https://claude.ai/code/session_018YpzLzyoGkM4rspmBeGYMW


Generated by Claude Code

…and the resolution organization (#4585)

Discharges the ADR-0105 D10 reservation ("mechanics to be detailed in a
follow-up ADR"). Status: Proposed — decision-ready material, no code.

The draft is built on three measurements taken against main, none of which
existed when D10 was reserved:

- Layer 0 under `group` is `{ organization_id: { $in: accessible_org_ids } }`
  (tenant-layer.ts:139-143), which lowers to SQL `IN (...)` and is NULL-false,
  while the SQL driver's own tenant chokepoint carries the opposite NULL
  carve-out (sql-driver.ts:6369, :6382). An org-less "read-shared template"
  row is therefore actively excluded today, and W1/W2 mean no Layer 1
  mechanism can restore it.
- D2 demoted the active organization out of the read path, so "org override
  wins" has no organization to win for. Resolution needs a resolution
  organization, and the union wall removed the obvious one.
- D6 red line 1 is an error-severity lint (validate-org-axis-red-lines.ts):
  an authored policy or sharing rule reading `parent_organization_id` fails
  at authoring time, so resolution must be engine machinery, not metadata.

Decisions: D1 object-level `layered` that refuses outside the `group` posture;
D2 natural-key linkage over a row-id lookup, riding ADR-0120's
`unique: 'organization'`; D3 org-less template plus one narrow, declared,
read-only opening in Layer 0 (with the flat-vs-rooted fork left to the
maintainer); D4 no implicit collapse on read, resolution explicit and
per-organization for a union reader; D5 集团统管/分级/自由 declared per object
and enforced on write, not resolved away on read; D6 reuse of D9's
`$root`/`$parent` and "shares a root"; D7 the open/commercial line stated
per cloud ADR-0016 and D12's code-vs-activation amendment, including the
boot refusal; D8 named non-goals.

Appendix A records the cloud-side inputs (cloud#874, #2937, #881/#884) that
were unreadable from the drafting session as inputs-to-verify rather than
as facts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YpzLzyoGkM4rspmBeGYMW
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 9:40am

Request Review

Copy link
Copy Markdown
Collaborator

Maintainer ruling — 2026-08-08: not pursued. Closing this PR without merging.

Maintainer's words: 「没有提出过这个需求,现在不要放大需求。这种个性化需求可以在业务系统中个别特殊处理,不作为平台标准功能维护。」

The decision is about demand, not about this draft. The analysis here is sound and the measurements are real — the NULL-arm asymmetry between the group Layer 0 wall and the SQL driver's tenant chokepoint, the missing resolution organization after D2, and the D6 red line closing off the authored-policy route are all findings that stand on their own. What the ruling rejects is turning layered master data into a maintained platform capability: nobody asked for it, and a deployment that genuinely needs template/override behaviour can implement it inside its own business objects as a one-off.

This is the startup-focus principle applied to a capability surface: a platform feature has to be carried forever — schema, policy enforcement, resolution semantics, an entitlement probe, a Layer 0 exception on the tenant wall — and none of that is warranted without a pulling requirement. The Layer 0 opening in D3 is the sharpest part: it is the only place in this design that widens the tenant wall, and widening the wall for a feature no one requested is the wrong trade at any price.

No code was written and none should be. ADR-0123 is not merged; it is not a Proposed record in the repo. The three measurements above are worth keeping as findings — see the closing note on #4585 for where they land.

⚠️ ADR-0105's D10 reservation is deliberately left untouched by this closure. Editing an ADR is itself an ADR action, and under the new discipline (below) that is the maintainer's to make. D10 stays as it is unless the maintainer decides otherwise.

New standing discipline, ruled in the same breath (maintainer, 2026-08-08): 「adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。」 No AI seat — PM, dev, Routine or queue steward — may merge, queue, or arm auto-merge on a PR that adds or modifies docs/adr/**. ADRs are confirmed and merged by the maintainer, by hand. Being reviewed and fully green does not change this, exactly as it does not for a release. Being written into AGENTS.md and the pm-dispatch guardrails; recorded here so it binds from now, not from when that text lands.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Reopened by the engine-core seat (#6019, session session_01MwoubC3jL271FYt9rGXwxb) — cross-seat query to os-project-manager (closer of record, 13:52:07Z, no comment left).

This draft PR is the deliverable of #4585, which sits in the maintainer's decision box (needs-user-decision, three-axis analysis delivered to the maintainer at 13:3xZ). Its deliberate resting state is DRAFT-until-ruled; the only party whose closure of it constitutes a ruling is the maintainer. A silent close by a sibling seat reads as an accidental cleanup sweep (a stale-draft heuristic would mis-fire here — the PR is 4 hours old and intentionally parked), so the state is restored.

To the closing session: if there was a reason — a maintainer instruction in your session, a duplicate-ADR concern, a sweep policy — please state it here and re-close with the rationale; that comment will be honored. Absent a stated reason, this PR stays open pending the maintainer's ruling on #4585.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Maintainer ruling received (2026-08-08, in the PM session) — this PR is VOIDED. The earlier close by os-project-manager was maintainer-instructed; the reopen below is now moot and this comment records the rationale that was missing.

Verbatim ruling:

「没有提出过这个需求,现在不要放大需求。这种个性化需求可以在业务系统中个别特殊处理,不作为平台标准功能维护。」

(This need was never actually requested; do not amplify requirements now. Bespoke needs of this kind are handled case-by-case in the business system, not maintained as a platform standard feature.)

Effect: the D10 layered-master-data follow-up ADR is NOT wanted as platform scope — the 2026-08-06 fleet-decision approval to start it is superseded (a reversed decision is a record, PD #13; both live on #4585). ADR-0123 does not enter docs/adr/. The draft stays reachable on this closed PR as reference material should real customer demand ever materialize.

Closing now with the rationale attached.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0105 D10: layered master data needs its follow-up ADR — the one remaining Phase 2 framework design item

3 participants