diff --git a/.changeset/30347-spec-seed-locale-and-queue-refuse-wording.md b/.changeset/30347-spec-seed-locale-and-queue-refuse-wording.md new file mode 100644 index 00000000000..052929cbe9d --- /dev/null +++ b/.changeset/30347-spec-seed-locale-and-queue-refuse-wording.md @@ -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. diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index 9e74602ead9..932782050e6 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -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 }>` | 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[]` | ✅ | Data records | | **files** | `Record` | 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). | @@ -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 }>` | 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[]` | ✅ | Data records | | **files** | `Record` | 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). | diff --git a/content/docs/references/data/seed-loader.mdx b/content/docs/references/data/seed-loader.mdx index 231395ac6d1..c335792fcd9 100644 --- a/content/docs/references/data/seed-loader.mdx +++ b/content/docs/references/data/seed-loader.mdx @@ -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 }>` | 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[]` | ✅ | Data records | | **files** | `Record` | 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). | diff --git a/content/docs/references/data/seed.mdx b/content/docs/references/data/seed.mdx index ad186a9c641..646ed74badc 100644 --- a/content/docs/references/data/seed.mdx +++ b/content/docs/references/data/seed.mdx @@ -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 }>` | 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[]` | ✅ | Data records | | **files** | `Record` | 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). | diff --git a/content/docs/references/kernel/manifest.mdx b/content/docs/references/kernel/manifest.mdx index 64aba9b6b5d..fd124280ae1 100644 --- a/content/docs/references/kernel/manifest.mdx +++ b/content/docs/references/kernel/manifest.mdx @@ -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 }>` | 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[]` | ✅ | Data records | | **files** | `Record` | 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). | diff --git a/packages/spec/src/contracts/ownership-queue-service.ts b/packages/spec/src/contracts/ownership-queue-service.ts index aa3f13980d6..19ce0a87d8a 100644 --- a/packages/spec/src/contracts/ownership-queue-service.ts +++ b/packages/spec/src/contracts/ownership-queue-service.ts @@ -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 @@ -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 `[]` / @@ -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( diff --git a/packages/spec/src/data/seed.zod.ts b/packages/spec/src/data/seed.zod.ts index dc7831d364a..346ba8dfc8d 100644 --- a/packages/spec/src/data/seed.zod.ts +++ b/packages/spec/src/data/seed.zod.ts @@ -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