Skip to content

Commit 98d70c6

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16896-automation-run-lifecycle-doors-docs
2 parents 916a53f + de1a611 commit 98d70c6

14 files changed

Lines changed: 475 additions & 36 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/objectql": patch
3+
"@objectstack/rest": patch
4+
---
5+
6+
Documentation only: seven in-source prose sites that still stated the superseded readonly-on-INSERT contract as live now state the ruled one.
7+
8+
The 2026-09-03 maintainer ruling (option C, #14147) put the static `readonly` strip inside `engine.insert` under the same `isSystem` gate as `engine.update`, and deleted the metadata-protocol create-ingress copy. Comments and test headers written before that ruling still said, in the present tense, that a non-system INSERT is exempt from the static strip, or that the strip lives at the DataProtocol create ingress. Each now states the ruled contract, and the superseded sentence is kept only as history, marked as superseded.
9+
10+
No behaviour changes and no test was deleted, skipped or re-scoped — the diff is comments only. It is a `patch` rather than `skip-changeset` because it was measured to publish: `@objectstack/objectql`'s comment edit moves source line numbers, so `dist/{index,core}.{js,mjs}.map` change, and `@objectstack/rest` inlines that same objectql source into its bundle, so `dist/index.{js,cjs}.map` change with it. Every emitted `.js` / `.mjs` / `.cjs` and every `.d.ts` / `.d.mts` / `.d.cts` is byte-identical before and after, and all six maps ship inside the published tarballs.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/runtime": minor
3+
"@objectstack/spec": patch
4+
---
5+
6+
`AppPlugin` now supplies `SeedLoaderConfig.locale`, so the `Seed.locale` axis takes effect on the default boot path.
7+
8+
The locale filter axis landed complete on the consumer side: the loader reads `Seed.locale`, composes it with `env` by conjunction, and names every dataset it drops. What it never had was a **producer** — no first-party call site passed `config.locale`, so `filterByLocale` returned its input on its first line and `dataset.locale` was never read at all. Authoring the key changed nothing. That is the same shape `Seed.env` spent releases in before framework#4704.
9+
10+
- **The locale is resolved from the app's own `i18n.defaultLocale`** — the same envelope key, read the same way `loadTranslations` already reads it for `setDefaultLocale` — and threaded into all three `SeedLoaderRequest`s `AppPlugin` builds: the inline boot seed, the per-org replayer registered for tenant provisioning, and the dev hot-reload seeder.
11+
- **An app that declares no locale sends no `locale` key at all**, rather than an `'en'` default. Absence is the loader's unrestricted spelling, so a stack that never opted in keeps loading every dataset exactly as before; defaulting would have turned a wiring change into a data change, silently dropping a `locale: ['zh-CN']` dataset on every stack without an `i18n` block. A blank or non-string `defaultLocale` is treated as absence for the same reason.
12+
- **Resolved at the call sites, not inside `load()`.** The sibling `env` axis resolves itself in the loader off an ambient `NODE_ENV`; a locale has no ambient source, and the only layer that knows which locale a stack runs in is the app config the loader is never handed. So this axis needs a real producer, which is what this change is.
13+
14+
`SeedLoaderService#warnOnUnresolvedLocaleScope` **stays**. It is not a signpost for an unwired state that has now gone away: three of this repo's six seed-request builders are publish/install-time paths that are handed no stack config and still pass no locale, embedding hosts build their own requests, and a stack may declare no `i18n` block at all. Every one of those still reaches `load()` with locale-scoped datasets and no `config.locale`, and the warning is what keeps that loud instead of silently inert.
15+
16+
The liveness ledger row `seed.locale` moves `experimental``live` with a `producer` pointer naming this wiring, and records which call sites supply the locale and which do not rather than claiming the frontier away.
17+
18+
⚠️ **Release-note reconciliation, for whoever compiles this release.** The sibling changeset `seed-locale-axis.md` (from the PR that landed the consumer half) states in the present tense that no first-party call site supplies `config.locale`, that the axis is inert on the default boot path, and that the liveness ledger records `seed.locale` as `experimental`. All three sentences describe the state that changeset shipped into, and **this change ends all three**. If both land in one release, the notes must read them in order — or fold them into one entry — rather than publishing the earlier state as current. ⛔ That sibling changeset is deliberately not edited here: it accurately records what its own PR did, and release notes are compiled centrally.
19+
20+
⛔ Out of scope, unchanged: rows already written under a different locale stay resident. Every seed is an `upsert` and the loader only writes, so switching a stack's locale on a non-empty database does not remove the other market's rows.

content/docs/data-modeling/seed-data.mdx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,21 @@ is made when the seeds load rather than when your config is assembled — so
215215
switching markets does not mean rebuilding, and the axis is evaluated in the one
216216
layer that could ever reconcile rows already written for another market.
217217

218+
<Callout type="info">
219+
**Where the loading locale comes from.** On the normal boot path your stack
220+
supplies it: the runtime reads your app's `i18n.defaultLocale` and passes it to
221+
the seed loader, so declaring a locale on a dataset takes effect with no extra
222+
wiring. Declare no `i18n` block and no locale is sent at all — which is the
223+
unrestricted setting, not a filter.
224+
</Callout>
225+
218226
<Callout type="warn">
219227
The axis is evaluated against the seed loader's `config.locale`. A host that
220-
supplies no locale gets **every** dataset, and the loader warns naming each
221-
locale-scoped dataset it let through — so a scope that is not taking effect is
222-
one log line to diagnose rather than a silent no-op.
228+
supplies no locale — an app with no declared `i18n.defaultLocale`, or an
229+
embedding host that builds its own seed-load request — gets **every** dataset,
230+
and the loader warns naming each locale-scoped dataset it let through, so a
231+
scope that is not taking effect is one log line to diagnose rather than a
232+
silent no-op.
223233
</Callout>
224234

225235
---

packages/objectql/src/readonly-strict-errors.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ import type { DroppedFieldsEvent } from '@objectstack/spec/data';
1818
* static `readonly` (#2948), a TRUE `readonlyWhen` predicate (#3042), the
1919
* implicitly-readonly runtime-owned types (#5503), and the `primary_key` strip
2020
* of a payload `id` the update dispatch ruled is not an identifier (#6437); on
21-
* INSERT only the runtime-owned ones, because a create is deliberately exempt
22-
* from the author-declared strips (#3413). One error names everything wrong
23-
* with the payload instead of forcing a round-trip per field. `drops`
24-
* keeps the per-reason breakdown (the same `DroppedFieldsEvent` shape
25-
* `onFieldsDropped` would have received, had the write been allowed to
26-
* complete), so a caller can tell a schema-level lock from a state-dependent
27-
* one without parsing the message.
21+
* INSERT the runtime-owned ones and, since the 2026-09-03 ruling (#14147)
22+
* superseded the create-side exemption #3413 had granted, static `readonly`
23+
* too — judged over `staticReadonlyInsertSubject`, which leaves a
24+
* `sys_`-prefixed or `managedBy` object to its own guards (#15719), while
25+
* `readonlyWhen` still locks nothing on a create (a conditional lock needs a
26+
* prior record). One error names everything wrong with the payload instead of
27+
* forcing a round-trip per field. `drops` keeps the per-reason breakdown (the
28+
* same `DroppedFieldsEvent` shape `onFieldsDropped` would have received, had
29+
* the write been allowed to complete), so a caller can tell a schema-level
30+
* lock from a state-dependent one without parsing the message.
2831
*
2932
* ## The message is composed from `drops`, not from the error's name (#6437)
3033
*

packages/rest/src/rest-batch-endpoint.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,17 @@ describe('POST {basePath}/batch — cross-object transactional batch', () => {
305305

306306
// ── create ingress parity (#3835) ─────────────────────────────────────────
307307
//
308-
// The engine's INSERT path is static-`readonly`-exempt by design (#3413), so
309-
// the #3043 strip that stops a non-system caller from seeding a read-only
310-
// column lives at the protocol's create ingress. This route used to call
311-
// `ql.insert` directly and skip it, so `readonly` meant two different things
312-
// depending on which create endpoint you used.
308+
// When this was written the #3043 strip that stops a non-system caller from
309+
// seeding a read-only column lived at the protocol's create ingress, because
310+
// the engine's INSERT path was static-`readonly`-exempt (#3413). This route
311+
// used to call `ql.insert` directly and skip that ingress, so `readonly`
312+
// meant two different things depending on which create endpoint you used.
313+
// Since the maintainer ruling of 2026-09-03 (option C, #14147) the strip
314+
// runs inside `engine.insert` for every non-system caller and the ingress
315+
// copy is deleted, so both create routes are stripped identically. The
316+
// routing pinned below stands on what the ingress still owns: the #3770
317+
// object-existence gate, the #7823 `internal: true` response strip and the
318+
// `droppedFields` relay — one create ingress, one response contract.
313319

314320
it('routes create ops through the protocol create ingress, not ql.insert', async () => {
315321
const ql = makeQl();

0 commit comments

Comments
 (0)