diff --git a/examples/app-multi-package/README.md b/examples/app-multi-package/README.md index f930c1187f..721be701dc 100644 --- a/examples/app-multi-package/README.md +++ b/examples/app-multi-package/README.md @@ -19,6 +19,12 @@ is one package ASSEMBLED (manifest fields plus the collections that package owns), declared by `AssembledPackageBodySchema`. `GET /api/v1/packages` on a booted instance lists both rows. -`orders` carries **no `scope` key** on purpose; the App's navigation lives with -the App package because a package's own navigation may not point at a foreign -object, while cross-package lookups (which `crm_order.account` is) are accepted. +Both rows are served with **`scope: "project"`**. `defineStack` parses every +`packages[]` entry through `ManifestSchema`, whose `scope` defaults to +`project`, so no package of a compiled artifact is ever scope-less — what marks +these two read-only is the server's own **`writable: false`** verdict (ADR-0070 +D2), which reads `engine.manifests` before it reads any scope. + +The App's navigation lives with the App package because a package's own +navigation may not point at a foreign object, while cross-package lookups (which +`crm_order.account` is) are accepted. diff --git a/examples/app-multi-package/src/packages/orders/index.ts b/examples/app-multi-package/src/packages/orders/index.ts index a53b70dfc7..096c8a18cd 100644 --- a/examples/app-multi-package/src/packages/orders/index.ts +++ b/examples/app-multi-package/src/packages/orders/index.ts @@ -12,10 +12,21 @@ import { defineStack } from '@objectstack/spec'; * - It declares the **same namespace** as the App package. That is what * ADR-0130 D1 buys: co-ownership of one namespace inside one artifact, so * `crm_order` keeps its name instead of becoming `orders_order`. - * - It carries **no `scope` key**. `ManifestSchema.scope` defaults to - * `'project'`, so a scope-less module is the row that separates the server's - * writability verdict from a client-side `scope !== 'project'` heuristic - * (ADR-0070 D2 / ADR-0130 Consequences row 6). + * - Its served row carries **`writable: false`** — the server's OWN verdict + * (ADR-0070 D2 / ADR-0130 Consequences row 6). `isWritablePackage` reads + * `engine.manifests` FIRST, so a package booted from an artifact is + * read-only whatever its `scope` says. + * + * ⛔ This module is NOT a scope-less row, and no package of a compiled artifact + * can be. It authors no `scope` key, but `defineStack` parses every `packages[]` + * entry through `ManifestSchema` (`spec/src/stack.zod.ts`, + * `ArtifactPackageEntrySchema`), whose `scope` is `.default('project')` — so + * `dist/objectstack.json` and every served row carry `scope: 'project'`. A + * genuinely scope-less row exists only where a manifest reaches the registry + * WITHOUT that parse: a marketplace / offline-imported package (booted, hence + * read-only) or a Studio-created base via `POST /api/v1/packages` (writable). + * That discriminating pair is pinned in + * `packages/runtime/src/domains/packages-writable-verdict.test.ts`, not here. * * `crm_order.account` looks up an object this package does NOT own. That is * legal and is the whole point of the split: cross-package lookups are accepted diff --git a/packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts b/packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts index d83ea1a5f2..b6a7adf5f2 100644 --- a/packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts +++ b/packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts @@ -94,14 +94,34 @@ describe('dogfood: one artifact, two co-owning packages (ADR-0130 D4)', () => { expect(orders?.manifest?.namespace).toBe('crm'); }); + it('both rows carry the schema default `scope: "project"` — nothing here is scope-less', () => { + // [#14597] This file used to document `orders` as being SERVED with no + // `scope` key. It authors none, but `defineStack` parses every `packages[]` + // entry through `ManifestSchema` (`ArtifactPackageEntrySchema`), whose + // `scope` is `.default('project')` — so the default is materialised at + // compile time, into `dist/objectstack.json` and into both served rows. + // Pinned on a real boot because that is the only place the old claim could + // ever have been checked, and it never was: every unit pin around it + // asserted a hand-built scope-less manifest instead of this artifact's. + const core = rows.find((r) => r.manifest?.id === CORE); + const orders = rows.find((r) => r.manifest?.id === ORDERS); + + expect(core?.manifest?.scope).toBe('project'); + expect(orders?.manifest?.scope).toBe('project'); + }); + it('both rows are read-only — the server\'s own verdict, not a scope heuristic', () => { // ADR-0070 D2 / ADR-0130 Consequences row 6: a package booted from an // artifact through `registerApp` is read-only whatever its scope says, - // because `isWritablePackage` reads `engine.manifests` FIRST. The module is - // the row that separates that verdict from Studio's client-side - // `scope !== 'project'` heuristic — it is authored with no `scope` key at - // all, and a client rule reading the row alone cannot tell it from a - // Studio-created writable base. + // because `isWritablePackage` reads `engine.manifests` FIRST. That is the + // whole content of the verdict here — and it is NOT reproducible from these + // rows, which carry `scope: 'project'` (pinned above). ⛔ This fixture is + // therefore not the row that separates the server rule from a client-side + // `scope !== 'project'` one: the scope-less pair that does (a booted + // marketplace import, read-only, vs a Studio-created base, writable) only + // arises where a manifest reaches the registry without a `ManifestSchema` + // parse, and is pinned in + // `packages/runtime/src/domains/packages-writable-verdict.test.ts` (#14597). const core = rows.find((r) => r.manifest?.id === CORE); const orders = rows.find((r) => r.manifest?.id === ORDERS);