Repository navigation
feat(runtime,cloud-connection,organizations): the marketplace install, sample-data reseed, restart heal and per-organization replay pick seed datasets by the organization's language - #30346
Conversation
… on install, reseed, heal and replay (WIP) The marketplace install, the sample-data reseed, the rehydrate heal and the per-org seed replay now pass a Seed.locale filter: the organization's Localization setting when one is set, else the installing or founding user's language, else the app's declared i18n.defaultLocale. Claude-Session: https://claude.ai/code/session_01Utp6CafJkuNKWHNo7WLtiP Co-authored-by: Claude <noreply@anthropic.com>
…eplay Claude-Session: https://claude.ai/code/session_01Utp6CafJkuNKWHNo7WLtiP Co-authored-by: Claude <noreply@anthropic.com>
…g suites stub it Claude-Session: https://claude.ai/code/session_01Utp6CafJkuNKWHNo7WLtiP Co-authored-by: Claude <noreply@anthropic.com>
…tion's language; changeset Claude-Session: https://claude.ai/code/session_01Utp6CafJkuNKWHNo7WLtiP Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 43 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 36 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5f46ef6c606f6bdf41f7c947c681d0729df68636 && git checkout 5f46ef6c606f6bdf41f7c947c681d0729df68636
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cfb602c4595795c696dd07763abca3104dd4cf80 6d3a814a442b0c3a9d101a3a6e67d5b75e89ff2b && git checkout -B drift-repro cfb602c4595795c696dd07763abca3104dd4cf80 && git merge --no-ff 6d3a814a442b0c3a9d101a3a6e67d5b75e89ff2b
node scripts/docs-audit/affected-docs.mjs --json cfb602c4595795c696dd07763abca3104dd4cf80
|
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF 席位意见:ACCEPT(#30281)。市场安装、重灌示例数据、重启自愈和按组织回放,四条路径都按组织的语言挑 席位核对Head-sha:
|
Requested by Bill · project thread
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
维护者速读
Seed.locale数据集:先看组织的「设置 → 本地化 → 默认语言」(有人设过才算,内置的 en-US 默认值不算),再看安装人/创建人的个人语言,最后看应用声明的i18n.defaultLocale。locale的数据集;没有locale的照旧全装,整包都没有locale时连设置和用户都不读。读设置或用户时出错就不灌,按原路径报出(安装摘要、重灌 400、回放告警),不会拿别的语种顶上。启动种子、热重载、包应用、草稿发布不变。回滚即撤 PR。packages/runtime/src/seed-locale.ts(三级取值与"默认值不算"那一句);packages/runtime/src/app-plugin.ts的 replayer(改成每次回放按组织取);packages/plugins/organizations/src/organizations-plugin.ts中间件 B 把创建人传给 replayer 的那几行。pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/app-plugin.seed-locale-producer.test.ts,预期 22 个测试全绿,其中"两个组织不同语种在同一进程拿到不同数据集"和"建组织时按创建人语言"两条。packages/spec里Seed.locale的说明仍写"安装不按语种挑",归 spec 车道另改(follow-up owed)。Fixes #30281
Clause-②: yes (widening) —
@objectstack/runtimeexports the seed-locale producer (resolveSeedLocaleForLoad,appDefaultSeedLocale,seedsDeclareLocale,describeSeedLocaleand three types), and theseed-replayerservice callable takes an optional second argument(organizationId, { userId }); a one-argument caller works as before.Metadata-protocol: semantic — an existing
Seed.localekey now selects datasets on the marketplace install, the sample-data reseed, the restart heal and the per-organization replay, where it loaded every dataset and warned.Skills-docs: updated here —
content/docs/data-modeling/seed-data.mdx(the two Locale Scoping callouts: where the loading locale comes from on the install paths, and "installing is locale-neutral" narrowed to apply and publish);skills/**teaches noSeed.locale(git grep -n locale skillsnames no seed or dataset line), so nothing there turns false. follow-up owed —packages/spec/src/data/seed.zod.tslocale docblock and.describe()(install paths now pick by the organization's locale, ruling A 2026-11-05), whose.describe()regeneratescontent/docs/references/data/seed.mdx.What changes
The producer (
@objectstack/runtime, newseed-locale.ts).resolveSeedLocaleForLoad(host, { seeds, organizationId, userId, appDefaultLocale })answers the locale one seed load filtersSeed.localeon. First answer wins:localization.localeread from thesettingsservice fororganizationId({ tenantId }), so the service's own cascade decides (env override, the organization's row, theglobalrow). ⛔ A value whosesourceis'default'does not count. The manifest declaresdefault: 'en-US'(service-settings/src/manifests/localization.manifest.ts), so taken literally the card's "effective value (cascade platform → global → tenant), or else the user's locale, or elsei18n.defaultLocale" always stops at the first rung and the other two are unreachable. The repo already reads it this way in two places: the auth email language takeslocalization.locale"only when it was EXPLICITLY set; a manifest default leaves the app's build-timei18n.defaultLocalestanding" (plugin-auth/src/auth-plugin.ts,applyLocalizationLocale), and the new-organization currency writes only while the key "still resolves from the default (source: 'default')" (plugin-auth/src/organization-default-currency.ts). The seat accepted this reading before code.sys_user.localeofuserId(system-context read,SystemObjectName.USER).appDefaultLocale.Nothing answers ⇒
undefined, and the request carries nolocalekey (the loader's unrestricted spelling, with its existing warning). Whenseedsis given and no dataset declareslocale, rungs 1 and 2 are not read at all. Nosettingsservice ⇒ rung 1 is skipped. A settings or user read that throws is thrown on: the caller's seed reports it and writes nothing. Also exported:appDefaultSeedLocale(body)(top-leveli18n.defaultLocale, then a nestedmanifest; a legacyi18narray declares none),seedsDeclareLocale,describeSeedLocale(log line), and the three types.Per-organization replay (
AppPlugin,packages/runtime/src/app-plugin.ts). The replayer used to captureresolveSeedLocale()(the boot bundle'si18n.defaultLocale) once at boot. It now resolves at replay time per organization, with the boot default as rung 3, and takes(organizationId, options?: { userId }). Oneinfoline names the locale and its rung. The boot inline seed and the dev hot-reload seeder still useresolveSeedLocale()alone (outside the card's ask; see Acceptance notes).The founder (
@objectstack/organizations, Middleware B). On asys_organizationinsert the middleware passesopCtx.context.attributedUserId(elseuserId) to the replayer as{ userId }, and keeps the one-argument call when the insert names no person. better-auth's create runs through the adapter as system with the signed-in creator asattributedUserId(plugin-auth/src/objectql-adapter.ts,withSystemContext).Item 3, new-organization founding: lands on (a), with (c) behind it, by construction. At the insert the organization has no
tenantrow yet (plugin-auth writes its currency and timezone inafterCreateOrganization, after the insert, and writes no locale at all), so rung 1 answers only an env orglobalvalue. Rung 2 is then the founder'ssys_user.locale, which is option (a); no founder (the boot's default organization, a cloud system insert) falls to the app's default, which is option (c). Option (b) would need a deferral mechanism the cascade does not have. Noneeds_decision; both paths are pinned (Tests).Install paths (
@objectstack/cloud-connection,runInlineSeedand its three call sites).runInlineSeed(ctx, datasets, organizationId?, localeFrom = {})resolves through the runtime producer and spreadslocaleinto the loader config. Measured call sites onorigin/main4be9734 (the dispatch's mapping was off by one role each; the seat confirmed):applySideEffectsstep 3 (reached by the install,handleInstall, and by the reseed,handleReseed): the active organization, the admitted user (opts.userId, new, passed from both handlers'admission.userId), and the package body for itsi18n.defaultLocale.maybeHealSampleData, single-tenant only): no organization, no user, the converted body.seed-replayer(registered only when the host has none): the organization and the founder, ⛔ no app default, because it replays the union of every installed package and no one package's default speaks for it.The constructor and the rehydrate section (seat 3's #30217) are untouched.
Docs.
content/docs/data-modeling/seed-data.mdx, Locale Scoping: the info callout gains the install-path rungs; the warn callout's "publishing and installing are locale-neutral, by decision" is narrowed to apply and publish, citing this ruling as reversing the install half of #17011.Changeset.
30281-seed-locale-per-organization.md:@objectstack/runtimeminor,@objectstack/cloud-connectionminor,@objectstack/organizationspatch (Metadata-protocol: semantic⇒ at least one moved packageminor), both declaration lines.Tests
packages/runtime/src/app-plugin.seed-locale-producer.test.ts(theSeed.localeproducer harness), two new top-leveldescribes, rows reaching the engine as the assertion through the realSeedLoaderService:tenantId; aglobalvalue counts; at founding the founder's language decides (option (a)); the manifest'sen-USdefault never outranks the founder; the organization's setting outranks the founder; no founder and no setting ⇒ the app's default (option (c)); a founder with a blank language falls through; nosettingsservice still honours the founder; nothing answers ⇒ every dataset loads and the loader warns; an unscoped dataset loads for both organizations (pin 4); no locale-scoped dataset ⇒ neither the setting norsys_useris read; a settings read that throws rejects the replay and writes no row.appDefaultSeedLocale, 3 cases (top level, nestedmanifest, legacy array, blank).packages/cloud-connection/src/marketplace-install-local-seed-replayer.test.ts(the install-path seeding harness), new top-leveldescribe, 7 cases with recording stand-ins for the producer (its rungs are pinned in runtime): the install asks for the active organization, the admitted user and the package'si18n.defaultLocale, and the loader receives the answer (pin 1); the reseed asks again and loads the answer as it stands now; the rehydrate heal asks with no organization and no user; the replayer asks per organization (two organizations, two locales, pin 2) and passes the founder with no app default; no answer ⇒ nolocalekey; a failing read seeds nothing and the install summary names the error.packages/plugins/organizations/src/organizations-plugin.test.ts, new top-leveldescribe, 2 cases: Middleware B hands the replayer{ userId }from the insert'sattributedUserId, and keeps the one-argument call for a system insert with no person.@objectstack/runtimemock carries the loader alone gained the three producer exports answering "no locale" (their subject is not the locale; vitest throws on a mocked module's missing export, which turned their seeds intoseed-errorskips).scripts/ablation-replace.mjs, anchor hit 1 → 0 on disk, restore proven by an emptygit diff HEADon the file). All subjects are imported by relative path fromsrc/, so nodist/leg applies.global; setting outranks founder)source: 'default'countundefinedorganizationId: undefineduserId: undefinedrunInlineSeeddropslocalefrom the loader configAcceptance notes
source !== 'default'. See What changes, rung 1. A deployment that wants every organization seededen-USregardless of its founders sets theglobalvalue (orOS_LOCALIZATION_LOCALE), which counts.Seed.localecase-insensitively and otherwise exactly (['zh']does not matchzh-CN). A user whose storedsys_user.localeis a barezhdoes not select azh-CNdataset; the Localization setting offers only the four region-qualified tags.i18n.defaultLocale; package apply (service-package) and draft publish still pass no locale. The ruling text names the install paths; apply and publish are left alone as the dispatch says.packages/spec/src/data/seed.zod.ts'slocaledocblock and.describe()still say the install paths do not filter by locale (and the generatedcontent/docs/references/data/seed.mdxrepeats it). Not edited here (follow-up owed, Skills-docs line); the hand-written docs page is corrected in this PR. The code ships honestly against it: the behaviour is the ruling's, and the stale sentence is a description to correct, not a contract a caller relies on.marketplace-install-local-plugin.tsedits are limited torunInlineSeed, its three call sites,applySideEffects' options type and the two handler calls that feed it. Whoever merges second mergesorigin/mainfirst.Verification
Head
6d3a814a4, the merge oforigin/mainc1240a38ainto the branch. The merge touches none of this PR's 16 files.app-plugin.seed-locale-producer.test.ts22 passed,marketplace-install-local-seed-replayer.test.ts16 passed,organizations-plugin.test.ts20 passed.c6795fe0a, the last code commit (these packages' files are byte-identical at head;a8fb28cd5adds the docs page and the changeset):@objectstack/runtime541 files, 8635 passed, 19 skipped;@objectstack/cloud-connection70 files, 853 passed;@objectstack/organizations18 files, 250 passed. Typecheck of the same three packages: exit 0.pnpm install --frozen-lockfileexit 0;turbo run build --filter='!@objectstack/docs'119/119.node scripts/pm/dispatch-gates.mjs --commandsderived 95 families. Each ran at head with its exit recorded: 95 of 95exit 0.--ranreconciliation (it re-derives on the head tree): 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.pnpm lintexit 0.Not measured: a live boot that installs a marketplace package into a
zh-CNorganization over HTTP. The install, reseed, heal and replay seams are pinned at the plugin with the producer stood in, and the producer's three rungs are pinned in runtime against the realSeedLoaderService, so the join between the two is covered by types and the shared export, not by one end-to-end run.Expect 22, 16 and 20 passed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CafJkuNKWHNo7WLtiP