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
13 changes: 13 additions & 0 deletions .changeset/30347-spec-seed-locale-and-queue-refuse-wording.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@objectstack/spec': patch
---

docs(spec): `Seed.locale` says which loads filter on it, and the ownership-queue contract says the registered service honours `onFailedRead: 'refuse'`

Clause-②: no
Metadata-protocol: none

Wording only: no schema shape, no type and no behaviour changed.

- **`Seed.locale`** (docblock and `.describe()`, which the references pages render). It no longer says that installing is locale-neutral. The boot seed and the dev hot-reload seeder filter on the app's `i18n.defaultLocale`. The marketplace install, the sample-data reseed, the restart heal and the per-organization replay filter on the locale `resolveSeedLocaleForLoad` (`@objectstack/runtime`) resolves for the load, first answer wins: the `localization.locale` setting, counted only when somebody set it (not `source: 'default'`); then the `sys_user.locale` of the user who installs, reseeds or founds the organization; then the app's `i18n.defaultLocale` where the load names one. Package apply and draft publish still pass no locale: they load every dataset and warn.
- **`OwnershipQueueFailedReadAnswer` and `IOwnershipQueueService.isMember`** (`@objectstack/spec/contracts`). The registered service, `@objectstack/plugin-sharing`'s, honours `'refuse'`; the "until then a caller passing `'refuse'` still gets `'nobody'`" clause is gone. The activation-ledger read keeps reading "switched on" when it fails, under both answers, and the contract now states that as decided. `isMember` under `'refuse'` reads the queue's own rows (`sys_queue`, `sys_queue_member`) first and rejects at once when one fails; past them it answers `true` as soon as a successful read reaches the user, even beside a failed read, and rejects only when no successful read reaches the user and one failed.
4 changes: 2 additions & 2 deletions content/docs/references/api/package-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ Package manifest installed through POST /api/v1/packages (AUTHORING stage; scope
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, and `replace` inserts into a table the caller cleared. |
| **columns** | `Record<string, { applyOn?: Enum<'create' \| 'always'> }>` | optional | Per-column write policy, keyed by field name: `{ status: { applyOn: 'create' } }` writes `status` only when the seed creates the row. Omitted, every column is written on create and update. |
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The publish and install paths do not filter by locale — they load every dataset and warn |
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app's i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app's i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn |
| **records** | `Record<string, any>[]` | ✅ | Data records |
| **files** | `Record<string, { contentType: string; size: integer; sha256: string; data: string }>` | optional | Bytes of the package files the records name with `seedFile(path)`, keyed by path. Written by `os build`, which reads each path relative to the package root; the loader stores each one as a file the written record owns. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
Expand Down Expand Up @@ -354,7 +354,7 @@ Package manifest installed through POST /api/v1/packages (AUTHORING stage; scope
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, and `replace` inserts into a table the caller cleared. |
| **columns** | `Record<string, { applyOn?: Enum<'create' \| 'always'> }>` | optional | Per-column write policy, keyed by field name: `{ status: { applyOn: 'create' } }` writes `status` only when the seed creates the row. Omitted, every column is written on create and update. |
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The publish and install paths do not filter by locale — they load every dataset and warn |
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app's i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app's i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn |
| **records** | `Record<string, any>[]` | ✅ | Data records |
| **files** | `Record<string, { contentType: string; size: integer; sha256: string; data: string }>` | optional | Bytes of the package files the records name with `seedFile(path)`, keyed by path. Written by `os build`, which reads each path relative to the package root; the loader stores each one as a file the written record owns. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/data/seed-loader.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ Seed loader request with datasets and configuration
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, and `replace` inserts into a table the caller cleared. |
| **columns** | `Record<string, { applyOn?: Enum<'create' \| 'always'> }>` | optional | Per-column write policy, keyed by field name: `{ status: { applyOn: 'create' } }` writes `status` only when the seed creates the row. Omitted, every column is written on create and update. |
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The publish and install paths do not filter by locale — they load every dataset and warn |
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app's i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app's i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn |
| **records** | `Record<string, any>[]` | ✅ | Data records |
| **files** | `Record<string, { contentType: string; size: integer; sha256: string; data: string }>` | optional | Bytes of the package files the records name with `seedFile(path)`, keyed by path. Written by `os build`, which reads each path relative to the package root; the loader stores each one as a file the written record owns. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/data/seed.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const result = SeedSchema.parse(data);
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, and `replace` inserts into a table the caller cleared. |
| **columns** | `Record<string, { applyOn?: Enum<'create' \| 'always'> }>` | optional | Per-column write policy, keyed by field name: `{ status: { applyOn: 'create' } }` writes `status` only when the seed creates the row. Omitted, every column is written on create and update. |
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The publish and install paths do not filter by locale — they load every dataset and warn |
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app's i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app's i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn |
| **records** | `Record<string, any>[]` | ✅ | Data records |
| **files** | `Record<string, { contentType: string; size: integer; sha256: string; data: string }>` | optional | Bytes of the package files the records name with `seedFile(path)`, keyed by path. Written by `os build`, which reads each path relative to the package root; the loader stores each one as a file the written record owns. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/kernel/manifest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Install mode: managed (registered as code, upgradeable, sealed) | unmanaged (cop
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, and `replace` inserts into a table the caller cleared. |
| **columns** | `Record<string, { applyOn?: Enum<'create' \| 'always'> }>` | optional | Per-column write policy, keyed by field name: `{ status: { applyOn: 'create' } }` writes `status` only when the seed creates the row. Omitted, every column is written on create and update. |
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The publish and install paths do not filter by locale — they load every dataset and warn |
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app's i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app's i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn |
| **records** | `Record<string, any>[]` | ✅ | Data records |
| **files** | `Record<string, { contentType: string; size: integer; sha256: string; data: string }>` | optional | Bytes of the package files the records name with `seedFile(path)`, keyed by path. Written by `os build`, which reads each path relative to the package root; the loader stores each one as a file the written record owns. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
Expand Down
17 changes: 13 additions & 4 deletions packages/spec/src/contracts/ownership-queue-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ export type QueueExpansionPurpose = 'access' | 'addressing';
* does not carry is absence, not failure. The activation-ledger read
* (`sys_metadata_activation`, which says whether a declared queue or a position
* is switched off) is NOT a failed read in this sense: under either answer its
* failure reads "switched on", as it did before the key existed; whether that
* posture moves is #29604's code half to decide.
* failure reads "switched on", as it did before the key existed. That posture
* stays by decision: it is the one the sharing-rule service takes for the same
* ledger on its own `position` recipients, so a queue's position and a rule's
* position cannot disagree during an outage.
*
* - `'nobody'` — the default and the answer every caller had before the key
* existed. A failed read is logged at `warn`, and what sat behind it
Expand All @@ -76,8 +78,7 @@ export type QueueExpansionPurpose = 'access' | 'addressing';
* grants and "nobody" there revokes: the approver slate (D6) and the
* sharing-recipient grant recompute (D7). An implementation written before
* this key existed ignores it and answers `'nobody'`; the registered one,
* `@objectstack/plugin-sharing`'s, honours `'refuse'` from #29604's code half
* on, so until then a caller passing `'refuse'` still gets `'nobody'`.
* `@objectstack/plugin-sharing`'s, honours `'refuse'`.
*
* Under either answer an EMPTY queue — every read succeeded and reached
* nobody — is still `[]`, and a name that resolves nowhere is still `[]` /
Expand Down Expand Up @@ -157,6 +158,14 @@ export interface IOwnershipQueueService {
* under `'nobody'`, the default, a failed read counts as not reaching
* `userId`; under `'refuse'`, `false` is answered only when every read
* behind it succeeded, and a failed read rejects with a failure naming it.
* Under `'refuse'` the registered service reads the queue's own rows
* (`sys_queue`, `sys_queue_member`) first, and a failure there rejects at
* once, since without them the queue's member set would be incomplete (a
* declared queue's own specifications come from the registry, but its
* authored additions come from `sys_queue_member`). Past them it
* answers `true` as soon as a read that succeeded reaches `userId`, even
* beside a failed read, and rejects only when no successful read reaches
* `userId` and one failed.
* The options argument is optional, so a three-argument call is today's.
*/
isMember(
Expand Down
42 changes: 30 additions & 12 deletions packages/spec/src/data/seed.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,20 +261,38 @@ export const SeedSchema = lazySchema(() => strictObject({
* The platform translates nothing. This is the axis that SELECTS between
* record sets the app authored itself.
*
* **Publishing and installing are locale-neutral acts — this axis is scoped
* to the boot path, by decision.** The loader evaluates it against
* `SeedLoaderConfig.locale`, which only the boot path supplies (AppPlugin
* reads the app's declared `i18n.defaultLocale`). Package apply, draft
* publish and marketplace install each build their own load request and pass
* no locale, so a `locale`-scoped dataset reaching one of them is loaded for
* **every** locale and the loader warns naming each dataset it let through.
* That bound is the contract, not an unfinished path: a package is assembled
* once for every market, and selecting a market is what happens when it is
* opened. Ruled on #17011 (2026-09-10), which also kept the warning rather
* than turning those paths into a refusal.
* **Which loads filter on it.** The loader evaluates this axis against
* `SeedLoaderConfig.locale`; a load request that carries none loads a
* `locale`-scoped dataset for **every** locale, and the loader warns naming
* each dataset it let through.
*
* - **Boot.** The boot seed and the dev hot-reload seeder filter on the
* app's declared `i18n.defaultLocale` (AppPlugin).
* - **Install, reseed, heal and per-organization replay.** The marketplace
* install, the sample-data reseed, the restart heal and the
* per-organization replay filter on the locale resolved for that load by
* `resolveSeedLocaleForLoad` (`@objectstack/runtime`), first answer wins:
* the `localization.locale` setting for the organization the rows land in
* (a load that names no organization, such as a single-tenant install or
* reseed or the restart heal, still reads it: the environment override
* answers first, then the `global` setting, then, where no wall stands and
* the deployment holds exactly one organization, that organization's own
* setting), counted only when somebody set it (a
* value whose `source` is `'default'` is the platform's placeholder, not a
* choice); then the `sys_user.locale`
* of the user who installs, reseeds or founds the organization, when one
* started the load; then the app's declared `i18n.defaultLocale`, when the
* caller names one. No answer is a load with no locale, as above. A
* setting or user read that fails seeds nothing rather than another
* language.
* - **Package apply and draft publish** pass no locale: they stay
* locale-neutral, because a package is assembled once for every market.
* That is the bound ruled on #17011 (2026-09-10), narrowed to these two
* paths when the maintainer ruled that the install paths pick by locale
* (2026-11-05).
*/
locale: z.array(LocaleSchema).min(1).optional()
.describe('Applicable locales (BCP-47 tags); omitted applies to every locale. The publish and install paths do not filter by locale — they load every dataset and warn'),
.describe('Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app\'s i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app\'s i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn'),

/**
* The Payload
Expand Down
Loading