Skip to content

Commit a839020

Browse files
docs(spec): Seed.locale says which loads filter on it, and the ownership-queue contract says the registered service honours onFailedRead refuse (#30604)
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa <!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuKB8Sg6c6sVpL96ruVvdxaf)_ Fixes #30347 Fixes #30389 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 只改 `@objectstack/spec` 的两处说明文字,不改任何代码或 schema 形状。① `Seed.locale` 的说明不再说"安装不按语种挑":现在写明启动种子按应用的 `i18n.defaultLocale`;市场安装、重灌示例数据、重启自愈、按组织回放按"组织设置 → 安装/创建人语言 → 应用默认"挑;包应用和草稿发布仍不挑。② 队列服务契约不再说"注册的服务还不支持 `'refuse'`",写明激活台账读失败仍按"开启"是已定的决定,并写明 `isMember` 在 `'refuse'` 下何时答 `true`、何时拒绝。 - **为什么改:** PR #30346 和 PR #30386 合并后,这几句已发布的说明与运行时行为不符,作者照着读会得出错误结论。 - **风险与回滚:** 只动 TSDoc、`.describe()` 文案和由它生成的四个参考页,运行时零变化。回滚即撤 PR。 - **先看:** `packages/spec/src/data/seed.zod.ts`(`locale` 的说明,三级取值那一段);`packages/spec/src/contracts/ownership-queue-service.ts`(`isMember` 文档新增的四句)。 - **两分钟验证:** `git grep -n "do not filter by locale" content/docs/references packages/spec/src`,预期每处命中都是"package apply and draft publish do not filter by locale",不再有"The publish and install paths"。 - **需要你做的:** 无,席位落地。 ## What changes Wording only, in one package (`@objectstack/spec`, `patch`). Each new sentence below names the line on `origin/main` (`ffeda137b`, merged into this branch in round 2) it rests on. ### #30347: `SeedSchema.locale` (`packages/spec/src/data/seed.zod.ts`) **Before (docblock):** "Publishing and installing are locale-neutral acts — this axis is scoped to the boot path, by decision. … `SeedLoaderConfig.locale`, which only the boot path supplies … Package apply, draft publish and marketplace install each build their own load request and pass no locale …" **Before (`.describe()`):** "The publish and install paths do not filter by locale — they load every dataset and warn" **After:** the docblock paragraph becomes three bullets, and the `.describe()` says the same in one sentence. Round 1 (contract review should-fix 1 and 2) names the reseeding user in the `.describe()` and says what rung 1 reads for a load that names no organization. | New sentence | Rests on | |---|---| | A request with no locale loads a `locale`-scoped dataset for every locale, and the loader warns | `packages/runtime/src/seed-locale.ts:30-32` (no rung ⇒ `undefined`, `warnOnUnresolvedLocaleScope`); `marketplace-install-local-plugin.ts:5305-5307` (spread, never `locale: undefined`) | | The boot seed and the dev hot-reload seeder filter on the app's `i18n.defaultLocale` | `packages/runtime/src/app-plugin.ts:1937` (boot) and `:2367` (hot reload), both `this.resolveSeedLocale()`, which reads `i18n.defaultLocale` at `:2299-2303` | | Install, reseed, heal and per-organization replay filter on what `resolveSeedLocaleForLoad` resolves | its four callers: `packages/cloud-connection/src/marketplace-install-local-plugin.ts:5239-5242` (`applySideEffects`, reached by install `:2537` and reseed `:4376`), `:1465` (restart heal), `:1728` (that plugin's own replayer); `packages/runtime/src/app-plugin.ts:1825-1830` (the AppPlugin per-organization replayer). All four go through `runInlineSeed` (`:5287-5292`) or call the producer directly | | Rung 1: the `localization.locale` setting for the organization, only when `source` is not `'default'` | `seed-locale.ts:135-140` | | Rung 1, a load that names no organization (a single-tenant install or reseed, the restart heal) still reads the setting: 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 | `seed-locale.ts:137` (`settings.get('localization', 'locale', {})` when no organization is named); `packages/services/service-settings/src/settings-service.ts:1951-1966` (the env override answers before any row is read), `:2119` and `:2175` (the cascade order global, tenant, default, first non-null wins), `:2137-2149` (the tenant rung, read for a tenant-scoped key: `localization` is `scope: 'tenant'`, `manifests/localization.manifest.ts:39`); `settings-service.ts:2221-2227` (`organizationReachOf` hands an organization-less caller to `resolveOrganizationReach`), `packages/core/src/security/organization-reach.ts:87-95` (no wall ⇒ the derived organization becomes the reach), `packages/core/src/security/resolve-authz-context.ts:1676-1696` (`resolveGuestOrganization`) and `packages/core/src/security/system-write-organization.ts:115-124` (`single` posture and exactly one `sys_organization` ⇒ `derived`); the heal passes no organization (`marketplace-install-local-plugin.ts:1465`) | | Rung 2: `sys_user.locale` of the user who installs, reseeds or founds, when one started the load (the `.describe()` now says "the installing, reseeding or founding user") | `seed-locale.ts:142-153`; install passes `admission.userId` (`marketplace-install-local-plugin.ts:1744` to `:2537` to `:5239-5241`), reseed passes the reseeding user's `admission.userId` (`:4376` to `:5239-5241`), the heal passes no user (`:1462-1465`) | | Rung 3: the app's `i18n.defaultLocale`, when the caller names one | `seed-locale.ts:128-129, 155`; the marketplace plugin's own replayer passes none (`marketplace-install-local-plugin.ts:1723-1728`) | | A setting or user read that fails seeds nothing rather than another language | `seed-locale.ts:34-39` (the throw reaches the caller, which writes nothing) | | Package apply and draft publish pass no locale | `packages/runtime/src/domains/packages.ts:4000-4007` and `packages/metadata-protocol/src/protocol-publish-batch.ts:115-125`: neither request carries `locale` | | The #17011 bound is narrowed to those two paths by the maintainer's ruling (2026-11-05) | `seed-locale.ts:7-9` ("maintainer ruling A, 2026-11-05 … reversing the install-path bound recorded on `SeedSchema.locale`") | The `.describe()` change regenerates four references pages, all committed: `content/docs/references/data/seed.mdx`, `data/seed-loader.mdx`, `api/package-api.mdx`, `kernel/manifest.mdx`. ### #30389: `packages/spec/src/contracts/ownership-queue-service.ts` 1. **Before:** "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'`." **After:** "the registered one, `@objectstack/plugin-sharing`'s, honours `'refuse'`." Rests on `packages/plugins/plugin-sharing/src/ownership-queue-service.ts:270-279` (`expandMembers`) and `:290-297` (`isMember`), which take `onFailedRead` and wrap a failure in `queueReadRefusal` (`:168`). 2. **Before:** "whether that posture moves is #29604's code half to decide." **After:** "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." Rests on `ownership-queue-service.ts:670-688` (`switchedOff` catches the ledger read and returns nothing switched off, under either answer) and `packages/plugins/plugin-sharing/src/sharing-rule-service.ts`, `readPositionActive` (a failed ledger read answers active). 3. **Added to `isMember`'s doc:** "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." Rests on `ownership-queue-service.ts:232-261` (`resolve` reads `authoredQueueRow` and `authoredMemberSpecs` with `refuse` before any specification; a declared queue's specifications come from `declaredMemberSpecs` at `:243`, its authored additions from `authoredMemberSpecs` at `:237`) and `:476-496` (`specsReachUser`: returns `true` on the first reaching spec, keeps the first failure, throws it only after the walk). Round 1 (contract review should-fix 3) replaced the earlier reason, "since without them the call does not know what to ask", which overstated it for a declared queue. Left as it was, because it is true: the `'nobody'` bullet still says the admission consumers (D3, D4, D5) take `'nobody'`. Measured: `sharing-service.ts:952`, `security-plugin-authz-middleware.ts:1610` and `objectql/src/assignment-rules.ts:293` call without the option; only D6 (`approval-service.ts:3541`) and D7 (`sharing-rule-service.ts:1752`, `:2198`) pass `'refuse'`. ## Tests No behaviour changed, so no new test. Round 2 readings were taken on head `d0863ab93` (after merging `origin/main` `ffeda137b`). - `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/data/seed.test.ts src/data/seed-loader.test.ts src/data/seed-file.zod.test.ts src/data/define-seed-record-keys.test.ts`: 4 files, 111 tests passed (the spec tests that touch `seed.zod.ts`; round 2 changes no other file's source). - `pnpm --filter @objectstack/spec build`, then `gen:docs`, then `check:generated`: "All 14 generated artifacts are up to date". - `pnpm --filter @objectstack/spec typecheck`: exit 0, 0 errors. - eslint on `seed.zod.ts`: exit 0. - `node scripts/check-changeset-no-major.mjs --base origin/main --event` (this body as a `pull_request` event): exit 0, "a non-protocol change at `patch`". - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 96 commands, run after a full `turbo run build` (119/119), all exit 0, exit codes recorded before any pipe. `--ran`: "96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN". ## Acceptance notes - **The dispatch's reading of #30389 sentence 2 was wider than the code.** "Whether that posture moves" names the activation-ledger read posture, not the admission consumers' answer. PR #30386 kept the ledger posture ("switched on" on a failed read). The new sentence says that. The admission consumers' `'nobody'` was already stated and still holds (see above). - **`isMember` under `'refuse'` does not answer `true` beside every failed read.** A failed `sys_queue` or `sys_queue_member` read rejects before any specification is read, even for a declared queue whose positions might reach the user (PR #30386's acceptance notes say the same). The doc says so, so that "rejects only when no successful read reaches the user" is not read as covering the queue's own rows. - **One more published sentence turned false by PR #30346, outside this PR's file surface.** The `locale` row note in `packages/spec/liveness/seed.json` still says the marketplace install passes no locale and calls publish and install locale-neutral. Same defect class; not touched here because the claim covers only `seed.zod.ts`, `ownership-queue-service.ts`, the regenerated pages and one changeset, and the note is a long ruling record, not a mechanical edit. Reported to the seat. - **Card #30347 has two commits, not one.** The regenerated references pages and the changeset went in a second commit (`9ba263492`) after the source commit (`3bbc3eb9b`), because a force-push to fold them in was refused in this session. #30389 is the third commit (`9d61fa4aa`). - **Round 1.** The contract review (PASS, no must-fix) listed three should-fix wording items; all three are in this PR: `d56c29454` (#30347, rung 1 and the reseeding user), `d0db2c6a9` (#30389, the `isMember` reason), `8629666d9` (the four regenerated references pages). The liveness-note finding is filed separately as #30607. `origin/main` was merged in first (`871582d63`); the moved citations (`packages.ts`, `approval-service.ts`) are updated above. - **Round 2.** The delta review at `8629666d9` (PASS, one should-fix) found rung 1's organization-less parenthetical read as exhaustive while missing a third source, the deployment's one organization's own row. `d0863ab93` adds it to the docblock; the `.describe()` and the changeset are unchanged (their wording stays true). `origin/main` `ffeda137b` was merged first (`e2f2e2175`), and `15456e098` regenerates `package-api.mdx` after that merge (its only difference from `origin/main` is the two `locale` rows). The marketplace plugin moved in that merge, so its line citations above are re-measured. - `packages/spec/CHANGELOG.md` still carries the released "publish and install paths do not filter by locale" entry. It is release-owned and correct for the release that shipped it; not edited. --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgTqqeHSwZYJmApbDCa)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgTqqeHSwZYJmApbDCa --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ca37605 commit a839020

7 files changed

Lines changed: 61 additions & 21 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `Seed.locale` says which loads filter on it, and the ownership-queue contract says the registered service honours `onFailedRead: 'refuse'`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Wording only: no schema shape, no type and no behaviour changed.
11+
12+
- **`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.
13+
- **`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.

‎content/docs/references/api/package-api.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ Package manifest installed through POST /api/v1/packages (AUTHORING stage; scope
246246
| **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. |
247247
| **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. |
248248
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
249-
| **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 |
249+
| **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 |
250250
| **records** | `Record<string, any>[]` | ✅ | Data records |
251251
| **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. |
252252
| **_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
354354
| **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. |
355355
| **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. |
356356
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
357-
| **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 |
357+
| **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 |
358358
| **records** | `Record<string, any>[]` | ✅ | Data records |
359359
| **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. |
360360
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |

‎content/docs/references/data/seed-loader.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ Seed loader request with datasets and configuration
233233
| **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. |
234234
| **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. |
235235
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
236-
| **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 |
236+
| **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 |
237237
| **records** | `Record<string, any>[]` | ✅ | Data records |
238238
| **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. |
239239
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |

‎content/docs/references/data/seed.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const result = SeedSchema.parse(data);
3333
| **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. |
3434
| **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. |
3535
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
36-
| **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 |
36+
| **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 |
3737
| **records** | `Record<string, any>[]` | ✅ | Data records |
3838
| **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. |
3939
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |

‎content/docs/references/kernel/manifest.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Install mode: managed (registered as code, upgradeable, sealed) | unmanaged (cop
100100
| **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. |
101101
| **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. |
102102
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
103-
| **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 |
103+
| **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 |
104104
| **records** | `Record<string, any>[]` | ✅ | Data records |
105105
| **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. |
106106
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |

‎packages/spec/src/contracts/ownership-queue-service.ts‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ export type QueueExpansionPurpose = 'access' | 'addressing';
6262
* does not carry is absence, not failure. The activation-ledger read
6363
* (`sys_metadata_activation`, which says whether a declared queue or a position
6464
* is switched off) is NOT a failed read in this sense: under either answer its
65-
* failure reads "switched on", as it did before the key existed; whether that
66-
* posture moves is #29604's code half to decide.
65+
* failure reads "switched on", as it did before the key existed. That posture
66+
* stays by decision: it is the one the sharing-rule service takes for the same
67+
* ledger on its own `position` recipients, so a queue's position and a rule's
68+
* position cannot disagree during an outage.
6769
*
6870
* - `'nobody'` — the default and the answer every caller had before the key
6971
* existed. A failed read is logged at `warn`, and what sat behind it
@@ -76,8 +78,7 @@ export type QueueExpansionPurpose = 'access' | 'addressing';
7678
* grants and "nobody" there revokes: the approver slate (D6) and the
7779
* sharing-recipient grant recompute (D7). An implementation written before
7880
* this key existed ignores it and answers `'nobody'`; the registered one,
79-
* `@objectstack/plugin-sharing`'s, honours `'refuse'` from #29604's code half
80-
* on, so until then a caller passing `'refuse'` still gets `'nobody'`.
81+
* `@objectstack/plugin-sharing`'s, honours `'refuse'`.
8182
*
8283
* Under either answer an EMPTY queue — every read succeeded and reached
8384
* nobody — is still `[]`, and a name that resolves nowhere is still `[]` /
@@ -157,6 +158,14 @@ export interface IOwnershipQueueService {
157158
* under `'nobody'`, the default, a failed read counts as not reaching
158159
* `userId`; under `'refuse'`, `false` is answered only when every read
159160
* behind it succeeded, and a failed read rejects with a failure naming it.
161+
* Under `'refuse'` the registered service reads the queue's own rows
162+
* (`sys_queue`, `sys_queue_member`) first, and a failure there rejects at
163+
* once, since without them the queue's member set would be incomplete (a
164+
* declared queue's own specifications come from the registry, but its
165+
* authored additions come from `sys_queue_member`). Past them it
166+
* answers `true` as soon as a read that succeeded reaches `userId`, even
167+
* beside a failed read, and rejects only when no successful read reaches
168+
* `userId` and one failed.
160169
* The options argument is optional, so a three-argument call is today's.
161170
*/
162171
isMember(

‎packages/spec/src/data/seed.zod.ts‎

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -261,20 +261,38 @@ export const SeedSchema = lazySchema(() => strictObject({
261261
* The platform translates nothing. This is the axis that SELECTS between
262262
* record sets the app authored itself.
263263
*
264-
* **Publishing and installing are locale-neutral acts — this axis is scoped
265-
* to the boot path, by decision.** The loader evaluates it against
266-
* `SeedLoaderConfig.locale`, which only the boot path supplies (AppPlugin
267-
* reads the app's declared `i18n.defaultLocale`). Package apply, draft
268-
* publish and marketplace install each build their own load request and pass
269-
* no locale, so a `locale`-scoped dataset reaching one of them is loaded for
270-
* **every** locale and the loader warns naming each dataset it let through.
271-
* That bound is the contract, not an unfinished path: a package is assembled
272-
* once for every market, and selecting a market is what happens when it is
273-
* opened. Ruled on #17011 (2026-09-10), which also kept the warning rather
274-
* than turning those paths into a refusal.
264+
* **Which loads filter on it.** The loader evaluates this axis against
265+
* `SeedLoaderConfig.locale`; a load request that carries none loads a
266+
* `locale`-scoped dataset for **every** locale, and the loader warns naming
267+
* each dataset it let through.
268+
*
269+
* - **Boot.** The boot seed and the dev hot-reload seeder filter on the
270+
* app's declared `i18n.defaultLocale` (AppPlugin).
271+
* - **Install, reseed, heal and per-organization replay.** The marketplace
272+
* install, the sample-data reseed, the restart heal and the
273+
* per-organization replay filter on the locale resolved for that load by
274+
* `resolveSeedLocaleForLoad` (`@objectstack/runtime`), first answer wins:
275+
* the `localization.locale` setting for the organization the rows land in
276+
* (a load that names no organization, such as a single-tenant install or
277+
* reseed or the restart heal, still reads it: the environment override
278+
* answers first, then the `global` setting, then, where no wall stands and
279+
* the deployment holds exactly one organization, that organization's own
280+
* setting), counted only when somebody set it (a
281+
* value whose `source` is `'default'` is the platform's placeholder, not a
282+
* choice); then the `sys_user.locale`
283+
* of the user who installs, reseeds or founds the organization, when one
284+
* started the load; then the app's declared `i18n.defaultLocale`, when the
285+
* caller names one. No answer is a load with no locale, as above. A
286+
* setting or user read that fails seeds nothing rather than another
287+
* language.
288+
* - **Package apply and draft publish** pass no locale: they stay
289+
* locale-neutral, because a package is assembled once for every market.
290+
* That is the bound ruled on #17011 (2026-09-10), narrowed to these two
291+
* paths when the maintainer ruled that the install paths pick by locale
292+
* (2026-11-05).
275293
*/
276294
locale: z.array(LocaleSchema).min(1).optional()
277-
.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'),
295+
.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'),
278296

279297
/**
280298
* The Payload

0 commit comments

Comments
 (0)