From 79081f0c1fc8f8f59838b44ca87184fd989c4bc3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 04:55:04 +0000 Subject: [PATCH 1/2] docs(ui): state the one-app-per-package rule where an author reads it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `content/docs/ui/audience-based-interfaces.mdx` is the page about serving several audiences, and its worked example is already the correct shape — one `crm` app whose navigation is gated by `requiredPermissions`. It never said the shape was mandatory, so a reader designing for three business audiences saw a page about gating, not a page about a cap, and reasonably reached for one app per audience. `defineStack` then refuses the second app at load, after the architecture and part of the implementation are written. Say it: a package of `type: 'app'` exposes exactly one App; more audiences are navigation groups inside that one app. `ADR-0019` names two records in this repo — `0019-app-as-consumer-unit` (D3 = the one-app rule) and `0019-approval-as-flow-node` (D3 = deprecating `ApprovalProcessSchema`) — and `stack.zod.ts` cites the bare number for both, so an author following the throw's own citation is as likely to land on the wrong record as the right one. The three citations of the app-cap rule in that file now name the record: the throw string, the `validateSingleApp` docblock and the `StackSingleAppViolationError` docblock. The approvals citations are left bare; repo-wide disambiguation is a separate scope. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU --- content/docs/ui/audience-based-interfaces.mdx | 4 +++- packages/spec/src/stack.zod.ts | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/content/docs/ui/audience-based-interfaces.mdx b/content/docs/ui/audience-based-interfaces.mdx index a8bdee9672..acdfdfe235 100644 --- a/content/docs/ui/audience-based-interfaces.mdx +++ b/content/docs/ui/audience-based-interfaces.mdx @@ -18,6 +18,8 @@ description: The same data serves different audiences. Give end users a curated The built-in permission sets already encode this split: `member_default` and `viewer_readonly` do **not** carry `studio.access` / `manage_metadata`, so Studio and schema-design surfaces are invisible to them; `admin_full_access` carries both, while `organization_admin` gets `setup.access` only — the Setup shell is reachable but Studio and schema design stay hidden from org-scoped admins (see [`default-permission-sets.ts`](https://github.com/objectstack-ai/objectstack/blob/main/packages/plugins/plugin-security/src/objects/default-permission-sets.ts)). +**One package, one app.** A package of `type: 'app'` exposes **exactly one** App — `defineStack` refuses a second at load, per ADR-0019 (app-as-consumer-unit) D3. More audiences therefore never mean more apps: they are navigation groups **inside** that one app, gated by `requiredPermissions` exactly as below. + ### 1. Gate the app and its navigation ```ts @@ -68,6 +70,6 @@ This is the lesson from Airtable's surfaces: the **Automations tab is visible to ## See also -- Decision records: **ADR-0047** (run modes), **ADR-0066** (unified authorization, dual-surface gates). +- Decision records: **ADR-0019** (app-as-consumer-unit — one app per package), **ADR-0047** (run modes), **ADR-0066** (unified authorization, dual-surface gates). - [Who can see data / automation / interface](/docs/permissions/access-recipes). - Guide: [Security](/docs/permissions); reference: [App schema](/docs/references/ui/app), [Permission schema](/docs/references/security/permission). diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index 34043ef7ce..9a0875337d 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -1460,7 +1460,7 @@ function validateNamespacePrefix(config: ObjectStackDefinition): string[] { } /** - * Validate the "at most one App per package" rule — ADR-0019 (D1/D3). + * Validate the "at most one App per package" rule — ADR-0019 (app-as-consumer-unit) D1/D3. * * A consumer package (`manifest.type === 'app'`) must not define **more than * one** app — that is the banned "suite contains apps" shape. Fold the apps @@ -1479,7 +1479,7 @@ function validateSingleApp(config: ObjectStackDefinition): string[] { const names = apps.map((a) => a.name).join(', '); return [ `An 'app' package must define at most one app, but found ${apps.length} (${names}). ` + - `Fold them into one app with multiple tabs, or split into separate packages (ADR-0019 D3).`, + `Fold them into one app with multiple tabs, or split into separate packages — ADR-0019 (app-as-consumer-unit) D3.`, ]; } @@ -1943,7 +1943,8 @@ class StackNamespacePrefixInvalidError extends StackRefusalError { /** * [ADR-0112 · #15963] An `app` package declares more than one app — the banned - * "suite contains apps" shape, ADR-0019 D3 — {@link validateSingleApp}. + * "suite contains apps" shape, ADR-0019 (app-as-consumer-unit) D3 — + * {@link validateSingleApp}. * Spelled `_VIOLATION` like the ledger's other rule-violation refusals * (`UNIQUE_VIOLATION`, `EXTERNAL_SCHEMA_MODE_VIOLATION`). */ From 7a135e875b3f8a1a7a7fd331e768559110a9f7a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 05:27:45 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore(changeset):=20patch=20@objectstack/sp?= =?UTF-8?q?ec=20=E2=80=94=20the=20refusal=20string=20ships?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured, not assumed: `packages/spec/package.json` `files[]` publishes both `dist` and `src/**/*.zod.ts`, and after a build the new citation appears in four published bundles (`dist/index.js`, `dist/index.mjs`, `dist/browser/index.js`, `dist/browser/index.mjs`) with the old spelling at zero. Published bytes move, so `skip-changeset` does not apply. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU --- .changeset/one-app-rule-adr-0019-citation.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changeset/one-app-rule-adr-0019-citation.md diff --git a/.changeset/one-app-rule-adr-0019-citation.md b/.changeset/one-app-rule-adr-0019-citation.md new file mode 100644 index 0000000000..233a4f248b --- /dev/null +++ b/.changeset/one-app-rule-adr-0019-citation.md @@ -0,0 +1,17 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): the one-app-per-package refusal cites the record it means, `ADR-0019 (app-as-consumer-unit) D3` + +`ADR-0019` names **two** records in this repository — `0019-app-as-consumer-unit` (D3 = a `type: 'app'` package defines at most one app) and `0019-approval-as-flow-node` (D3 = deprecating `ApprovalProcessSchema`). Both have a D3, and `stack.zod.ts` cited the bare number for both, so an author following the refusal's own citation was as likely to reach the wrong decision record as the right one. + +The three citations of the app-cap rule now name the record: + +- the `STACK_SINGLE_APP_VIOLATION` message — the only one an app author ever sees; +- the `validateSingleApp` docblock; +- the `StackSingleAppViolationError` docblock. + +Only the message tail changed: `An 'app' package must define at most one app, but found N (…)` is untouched, so any consumer matching on that prefix is unaffected. The rule, the refusal's condition and `defineStack`'s behaviour are unchanged. + +The approvals-side citations are deliberately left bare — repo-wide ADR-number disambiguation is tracked separately.