Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions examples/app-multi-package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
19 changes: 15 additions & 4 deletions examples/app-multi-package/src/packages/orders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 25 additions & 5 deletions packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading