@objectstack/types 17.0.0-rc.6 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,2345 @@
1
1
  # @objectstack/types
2
2
 
3
+ ## 17.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2f65b1b: `error.code` is a closed vocabulary at every door (#9106, maintainer ruling
8
+ 2026-08-16): the runtime dispatcher's thrown-error exits
9
+ (`HttpDispatcher.errorFromThrown`, `dispatcher-plugin`'s `errorResponseBase`,
10
+ `endpoint-executor`'s `endpointErrorAnswer` — the actions door among them) now
11
+ serve the narrowed `code` the shared resolver (`resolveThrownHttpError`,
12
+ `@objectstack/types`) has always computed, exactly as the REST door has since
13
+ #8016. A thrown code that is not a member of `StandardErrorCode ∪
14
+ ERROR_CODE_LEDGER` no longer reaches `error.code`.
15
+
16
+ It is not dropped: `ApiErrorSchema` declares a new optional `declaredCode`
17
+ field — the open, author-authored channel — and the demoted spelling rides
18
+ there. Presence means demotion: the field is absent whenever the producer's
19
+ code is a vocabulary member (it is already in `error.code`) or the producer
20
+ declared none. The #7867 sandbox passthrough capability is preserved — a
21
+ metadata app's own thrown `.code` still crosses the QuickJS boundary and still
22
+ reaches the wire.
23
+
24
+ For a metadata app that throws its own code (e.g.
25
+ `Object.assign(new Error('pick another'), { code: 'DUPLICATE' })` in an action
26
+ body) and reads it back from an actions-door failure:
27
+
28
+ - FROM: `error.code === 'DUPLICATE'`
29
+ - TO: `error.code` is the closed member the status derives (e.g.
30
+ `VALIDATION_ERROR` on a 400) and `error.declaredCode === 'DUPLICATE'`.
31
+ One-line fix: branch on `error.declaredCode` for app-specific spellings;
32
+ branch on `error.code` for platform conditions.
33
+
34
+ Platform producers are unaffected: every registered code reaches `error.code`
35
+ verbatim, as before (post-#8846 the dispatcher-vocabulary gate holds that set
36
+ registered). Measured before landing (the ruling's binding precondition): no
37
+ existing consumer of the actions door branches on author-authored strings in
38
+ `error.code`.
39
+
40
+ `@objectstack/types` adds `demotedDeclaredCode(thrown)` — the one definition of
41
+ "which spelling a boundary surfaces beside the closed `code`".
42
+ - 79c46da: feat(contract): a hook refusal can mark its message user-facing — `userMessage`, the producer-side opt-in channel (#9934, producer half of objectui#5210)
43
+
44
+ <!-- adr-0087: not-required (no-migration-prescription) Purely additive: one
45
+ new OPTIONAL field on the two error-envelope schemas, a new shared reader in
46
+ @objectstack/types, and passthrough plumbing at the boundaries. Nothing
47
+ authorable is renamed, retired, aliased or tombstoned, so there is no
48
+ conversion to register. Unmarked errors produce byte-identical wire bodies. -->
49
+
50
+ The console form deliberately discards the server `message` on 403 and
51
+ substitutes a generic string — the recorded #3821 fix for platform diagnostics
52
+ leaking to end users. That substitution also suppressed every deliberate,
53
+ localized refusal an application hook author wrote (11 real hook guards in the
54
+ objectui#5210 report), and incentivized misusing 400 for permission refusals.
55
+ The maintainer-accepted ruling (2026-08-19, option 1): give the AUTHOR a
56
+ producer-side way to mark a refusal message user-facing, once, at the contract
57
+ level — status-agnostic, with #3821 preserved by construction for everything
58
+ unmarked.
59
+
60
+ **The marking**: set `userMessage` (non-empty string) on the thrown error at
61
+ throw time. It is a text-carrying field, not a boolean beside `message` — the
62
+ mark and the marked text are one value, so no boundary that rewraps or
63
+ substitutes `message` can promote platform prose into the marked channel, and
64
+ platform/driver code never sets it.
65
+
66
+ - `@objectstack/spec`: `ApiErrorSchema.userMessage` and
67
+ `EnhancedApiErrorSchema.userMessage` (optional, additive).
68
+ - `@objectstack/types`: `declaredUserMessage(error)` — the ONE "is this
69
+ marked?" read (non-empty string, nothing invented) — and
70
+ `ThrownHttpError.userMessage` on `resolveThrownHttpError`.
71
+ - `@objectstack/rest`: `mapDataError` / `resolveErrorResponse` ride a declared
72
+ marking onto whatever envelope classification chose (flat body top-level
73
+ `userMessage`, truncated at the same #5423 bound as the 4xx message).
74
+ - `@objectstack/runtime`: the QuickJS side-channel carries `userMessage`
75
+ across the sandbox boundary (both directions, joining `code`/`fields`/
76
+ `status`), and the dispatcher door emits it as a declared sibling in the
77
+ nested envelope.
78
+ - `@objectstack/client`: the SDK attaches `err.userMessage` from both wire
79
+ dialects, so a UI renders it verbatim when present and keeps its generic
80
+ substitution when absent.
81
+
82
+ The consumer half — the console form rendering a marked message instead of the
83
+ generic `form.noPermissionToSave` — is objectui#5210.
84
+
85
+ ### Patch Changes
86
+
87
+ - 2d0af57: fix(tests): give two default-vitest-timeout cases real margin instead of a bare default (#9311)
88
+
89
+ Two cases only passed `pnpm test` when they were not competing for CPU — the
90
+ same defect class as the already-closed precedents #3662, #4186, #4485,
91
+ #5421, #6329: a test running under vitest's **default** `testTimeout` /
92
+ `hookTimeout` with no margin for anything heavier than an idle box.
93
+
94
+ **`packages/types/src/node.test.ts`** — `"falls back to the importing
95
+ package's own resolution when the host does not declare"` is the only case
96
+ in the file that performs a real dynamic `import()` of `@objectstack/spec` (a
97
+ multi-megabyte package); every sibling in the same `describe` block resolves
98
+ a small on-disk fixture or fails fast, all under 10ms. Measured on this box:
99
+ ~0.9-1.1s unloaded, already observed failing at 5061ms against the 5000ms
100
+ default under nothing heavier than `turbo run test --concurrency=2` (#9311's
101
+ own isolation runs). Gave that one case an explicit 30s `testTimeout` — the
102
+ same order of magnitude the repo already uses for subprocess/real-load cases
103
+ (`#3662` precedent) — and left every sub-10ms sibling alone.
104
+
105
+ **`packages/qa/dogfood/test/semantic-roles.dogfood.test.ts`** — its
106
+ `beforeAll` boots the full showcase stack (ObjectQL + ~45 plugins) through
107
+ `@objectstack/verify`'s `bootStack`, which does not fit vitest's 10s
108
+ `hookTimeout` default with any margin at all: observed failing at 10027ms
109
+ against the 10000ms budget, and this file's own isolated run measured 18.3s
110
+ (vitest `Duration`) / 19.5s wall clock for the whole file even with the box
111
+ otherwise idle. Gave the hook an explicit 180s timeout, matching this
112
+ package's own existing house pattern for the identical
113
+ `bootStack(showcaseStack, …)` call
114
+ (`admin-identity-audit-trail.dogfood.test.ts`'s `beforeAll(…, 180_000)`)
115
+ rather than inventing a new number for the same operation.
116
+
117
+ **No behaviour change** — both suites already pass; this only gives the two
118
+ timeout-sensitive cases room to finish on a loaded box. The repo's full test
119
+ suite is confirmed green at low concurrency (#9311), so this is margin
120
+ repair, not a product fix. `turbo.json`'s default concurrency is out of scope
121
+ for this change (a maintainer-level default, per #9311's own filing).
122
+ - 27a567d: fix(types): teach the internal-leak predicate MySQL's three error templates (#8739)
123
+
124
+ `looksLikeInternalErrorLeak` decides whether a message is a driver dump that
125
+ must not reach an API client. It is applied at three HTTP boundaries
126
+ (`@objectstack/rest`'s `mapDataError`, `@objectstack/runtime`'s
127
+ dispatcher-plugin and endpoint-executor, the hono adapter) and by
128
+ `@objectstack/objectql`'s log redactor. Its dialect list covered the SQLite
129
+ family and Postgres; on a MySQL deployment it returned `false` for every one of
130
+ these conditions — **silent, not clearing**.
131
+
132
+ Under the maintainer's 2026-08-15 ruling on #8739, **MySQL is a supported
133
+ deployment target**, not merely a tested dialect — the answer already implied by
134
+ what is published (`OS_DATABASE_DRIVER=mysql` as a documented deployment knob,
135
+ `MysqlConfig` as authorable datasource config, per-field MySQL DDL in
136
+ `types.mdx`) and by a required CI check that stands up a live `mysql:8.0`. A
137
+ supported target's driver text reaches those boundaries in production, so its
138
+ templates belong in the list.
139
+
140
+ **Now recognised** — one per condition the other two dialects were already
141
+ covered for, each anchored on MySQL's own errmsg template rather than on a bare
142
+ substring:
143
+
144
+ - `Table 'app.t' doesn't exist` (ER_NO_SUCH_TABLE 1146). MySQL's contracted
145
+ spelling quotes `db.table` as one identifier, so the Postgres
146
+ `relation "t" does not exist` limb could never reach it.
147
+ - `Unknown column 'c' in 'field list'` (ER_BAD_FIELD_ERROR 1054). Both quoted
148
+ parts are required; the second is MySQL's clause name (`field list`,
149
+ `where clause`, `order clause`, `on clause`), and it is what distinguishes the
150
+ driver's template from a sentence that merely calls a column unknown.
151
+ - `Duplicate entry 'x' for key 'i'` (ER_DUP_ENTRY 1062). The `for key` tail plus
152
+ a quoted index is the anchor. This is the one MySQL template whose text embeds
153
+ a **caller's value** rather than an identifier — SQLite's
154
+ `UNIQUE constraint failed: t.c` and Postgres' `violates unique constraint "…"`
155
+ both name only an index — which is why closing this gap was worth a behaviour
156
+ change rather than another comment.
157
+
158
+ **Deliberately still NOT recognised**, so the boundary of the change is on the
159
+ record rather than inferred:
160
+
161
+ - **MySQL's ACL family** — `Access denied for user 'u'@'h' to database 'd'`
162
+ (1044), `SELECT command denied to user … for table 't'` (1142) — the
163
+ counterpart of the Postgres `permission denied for table` limb. Nothing in
164
+ this repo has raised one off a live server, and the standing rule in this
165
+ neighbourhood (`unique-violation.ts`) is that a dialect's spelling is added
166
+ once it has been MEASURED off a thrown error, never from a reading of the
167
+ manual. `Access denied` also collides with this platform's own security prose
168
+ (`[Security] Access denied: …`), so a guessed pattern here would over-match —
169
+ and over-matching suppresses diagnostics an operator needs.
170
+ - **MSSQL and Oracle** — `Invalid object name 'sys_metadata'.`,
171
+ `ORA-00942: table or view does not exist` still return `false`.
172
+ - **Prose that shares the keywords without the driver's anchoring** — an import
173
+ summary saying `duplicate entry in the uploaded file`, a mapping message
174
+ saying `Unknown column in the uploaded CSV header`, `The table you selected
175
+ does not exist`. Pinned as negative cases, because a phrasing list that says
176
+ "leak" too often replaces real answers with `Internal server error`.
177
+
178
+ **The `false`-means-UNCOVERED rule survives the change and keeps a live
179
+ subject.** A `false` here has never meant the text is safe, only that the
180
+ predicate never learned that dialect — the reading a reviewer on PR #8737 got
181
+ wrong while sizing a disclosure residual, which is what produced this card. The
182
+ four `toBe(false)` pins PR #8824 planted as a tripwire for this exact moment
183
+ went red as designed and are rewritten, not deleted: the same three measured
184
+ messages now assert `true`, so a future change that silently drops MySQL
185
+ coverage fails there, and a second block keeps the original `false`-means-
186
+ uncovered shape pointed at MSSQL and Oracle. `declaresServerFault` remains the
187
+ phrasing-independent answer.
188
+
189
+ **No status mapping moves.** `@objectstack/rest` answers the 409 conflict
190
+ question with `isUniqueViolationError`, above and independently of this
191
+ predicate (#6250), so a MySQL duplicate-entry error is still `409
192
+ UNIQUE_VIOLATION` and a MySQL unknown-column error is still `400 INVALID_FIELD`
193
+ — both decided before the leak branch is reached. The log redactor is unchanged
194
+ too: a bare MySQL diagnostic carries no knex ` - ` separator, so there is no
195
+ statement to cut. Measured across the predicate's full consumer set — types,
196
+ objectql, rest, runtime, metadata-protocol, hono, service-package,
197
+ service-analytics — the only verdicts that moved are the two that measure this
198
+ predicate directly.
199
+
200
+ No live MySQL deployment leaking through these boundaries was measured; this
201
+ closes a gap in what the boundary recognises, and the card is explicit that no
202
+ leak was demonstrated.
203
+ - bbbfcfc: fix(types): `isUniqueViolationError` stops claiming the sentences that say a unique constraint is ABSENT (#8590)
204
+
205
+ The shared predicate's message limb was a bare `unique constraint`, and a word
206
+ pair is not a condition. Every dialect that can say "this row violated a unique
207
+ constraint" can also say "there is no unique constraint here", and the same two
208
+ words sit adjacent in both — so the predicate answered **true** for errors
209
+ meaning the exact opposite of what it detects. `rest-server.ts` maps that
210
+ verdict to `409 UNIQUE_VIOLATION`, which tells a client to change a value when
211
+ nothing was ever compared, on a status an SDK will not retry.
212
+
213
+ **Measured on live servers for this fix, all three supported dialect families**
214
+ — SQLite via better-sqlite3, PostgreSQL 16.13 via `pg` 8.22.0, MariaDB 10.11.14
215
+ via `mysql2` 3.23.1, all through knex 3.3.0 — driving each dialect through both
216
+ conditions plus the NOT NULL / FOREIGN KEY near misses:
217
+
218
+ ```
219
+ sqlite ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint
220
+ -> was true, WRONG (the reported defect, #8590)
221
+ postgres there is no unique constraint matching given keys for referenced table "t"
222
+ -> was true, WRONG (42830 — found by this fix's dialect sweep)
223
+ postgres there is no unique or exclusion constraint matching the ON CONFLICT specification
224
+ -> false (the pair is not adjacent here)
225
+ mysql the condition cannot arise: knex compiles to ON DUPLICATE KEY UPDATE,
226
+ which carries no conflict target (confirmed against a live server)
227
+ ```
228
+
229
+ **Postgres was not clean either, and that chose the fix.** #8590 was filed
230
+ reading the collision as SQLite-only, with Postgres escaping "by luck of word
231
+ order". The sweep raised **42830** — a `FOREIGN KEY` referencing a non-unique
232
+ column — where Postgres puts `unique constraint` adjacent in its own absence
233
+ sentence. The card offered two candidate fixes; only one survives 42830. A
234
+ negative lookahead on SQLite's missing-index sentence is a blocklist that can
235
+ only enumerate absence sentences somebody already tripped over, and it answers
236
+ `true` on 42830. So the limb now requires a **violation phrasing** —
237
+ `unique constraint failed` (SQLite) or `violates unique constraint` (Postgres) —
238
+ which restores the module's own stated default, *unrecognised is `false`*, to
239
+ the message channel.
240
+
241
+ **Both spellings the retired limb covered are preserved exactly**, which was the
242
+ constraint on the fix: the limb was inherited verbatim from the REST branch
243
+ #6250 replaced and covered SQLite's `UNIQUE constraint failed: t.c` *and*
244
+ Postgres' `... violates unique constraint "..."`. The `unique violation`,
245
+ `duplicate key` and `duplicate entry` limbs are untouched, as are the `code` and
246
+ `errno` channels — MySQL's `Duplicate entry` path never went through the
247
+ narrowed limb at all.
248
+
249
+ **No user-visible behaviour changes today; this closes a latent inversion.** The
250
+ one site compiling a caller-supplied conflict target (`SqlDriver.upsert`)
251
+ recognises the unbacked target *first* in its catch and throws a refusal
252
+ declaring `status: 400`, and `mapDataError` reads `declaredHttpStatus` before it
253
+ reaches the unique-violation branch — so the 409 was gated off the wire by
254
+ ordering, not by the verdict. That ordering was the only thing standing between
255
+ this and a wrong status, which is why the verdict is now pinned rather than left
256
+ to it. A repo-wide scan of every string literal whose verdict moves found no
257
+ consumer relying on the old answer: all of them are prose, a different
258
+ predicate's vocabulary (`looksLikeInternalErrorLeak` keeps its own list), or
259
+ fixtures asserted through the status-passthrough path.
260
+
261
+ `unbacked-conflict-target.test.ts`'s pin — written by #8567 to point at itself
262
+ rather than go quietly green — is **inverted, not deleted**, and
263
+ `unique-violation-absence-sentences.test.ts` pins the absence sentences per
264
+ dialect in both directions, including the code channel, so re-reading `code`
265
+ cannot undo the message-side fix from the other side.
266
+ - Updated dependencies [56656aa]
267
+ - Updated dependencies [07e630e]
268
+ - Updated dependencies [2f65b1b]
269
+ - Updated dependencies [720ee95]
270
+ - Updated dependencies [f287435]
271
+ - Updated dependencies [9aa8890]
272
+ - Updated dependencies [7c9c1dd]
273
+ - Updated dependencies [75b7c24]
274
+ - Updated dependencies [d5552ca]
275
+ - Updated dependencies [d9813a9]
276
+ - Updated dependencies [8640fb2]
277
+ - Updated dependencies [2420641]
278
+ - Updated dependencies [2ad91c3]
279
+ - Updated dependencies [f57fb38]
280
+ - Updated dependencies [00777a0]
281
+ - Updated dependencies [d491625]
282
+ - Updated dependencies [420804d]
283
+ - Updated dependencies [716ac9b]
284
+ - Updated dependencies [62b1427]
285
+ - Updated dependencies [7ea1372]
286
+ - Updated dependencies [23abe27]
287
+ - Updated dependencies [985a9cd]
288
+ - Updated dependencies [a8189ae]
289
+ - Updated dependencies [26e70fb]
290
+ - Updated dependencies [42b05af]
291
+ - Updated dependencies [2b292ce]
292
+ - Updated dependencies [abcf853]
293
+ - Updated dependencies [8b9eba5]
294
+ - Updated dependencies [d575779]
295
+ - Updated dependencies [94f7ef8]
296
+ - Updated dependencies [c5ac5e4]
297
+ - Updated dependencies [a777944]
298
+ - Updated dependencies [dd88e1c]
299
+ - Updated dependencies [856527c]
300
+ - Updated dependencies [870f710]
301
+ - Updated dependencies [79c46da]
302
+ - Updated dependencies [7ff3975]
303
+ - Updated dependencies [29d055b]
304
+ - Updated dependencies [65589d6]
305
+ - Updated dependencies [2c86fe3]
306
+ - Updated dependencies [e196c6a]
307
+ - Updated dependencies [4ab7523]
308
+ - Updated dependencies [19539b4]
309
+ - Updated dependencies [11b779e]
310
+ - Updated dependencies [739fe5b]
311
+ - Updated dependencies [4bfe1a5]
312
+ - Updated dependencies [2065e31]
313
+ - Updated dependencies [b69d0f5]
314
+ - Updated dependencies [4d47afe]
315
+ - Updated dependencies [e4e5c6e]
316
+ - Updated dependencies [9a56784]
317
+ - Updated dependencies [d00d2f6]
318
+ - Updated dependencies [df0c12d]
319
+ - Updated dependencies [d31785f]
320
+ - Updated dependencies [c308a4f]
321
+ - Updated dependencies [e2899f6]
322
+ - Updated dependencies [3851f87]
323
+ - Updated dependencies [2a29caa]
324
+ - Updated dependencies [09a6eee]
325
+ - Updated dependencies [1a7f907]
326
+ - Updated dependencies [cd455c8]
327
+ - Updated dependencies [30d3752]
328
+ - Updated dependencies [c80e7ae]
329
+ - Updated dependencies [09a9a8a]
330
+ - Updated dependencies [07026cf]
331
+ - Updated dependencies [5d4f3d5]
332
+ - Updated dependencies [4d80e8b]
333
+ - Updated dependencies [30b1c63]
334
+ - Updated dependencies [079b457]
335
+ - Updated dependencies [e43b211]
336
+ - Updated dependencies [890b38f]
337
+ - Updated dependencies [8bee54b]
338
+ - Updated dependencies [7a537ce]
339
+ - Updated dependencies [593c4bf]
340
+ - Updated dependencies [ff08691]
341
+ - Updated dependencies [60e0f90]
342
+ - Updated dependencies [90c5285]
343
+ - Updated dependencies [7901b2d]
344
+ - Updated dependencies [56bca91]
345
+ - Updated dependencies [79394d7]
346
+ - Updated dependencies [730fd9a]
347
+ - Updated dependencies [44bc51d]
348
+ - Updated dependencies [73cfddf]
349
+ - Updated dependencies [d634e66]
350
+ - @objectstack/spec@17.1.0
351
+
352
+ ## 17.0.0
353
+
354
+ ### Minor Changes
355
+
356
+ - 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a
357
+ first-class authorization dimension.
358
+
359
+ > This release carries BREAKING spec removals (see "Enforce-or-remove" below)
360
+ > but is recorded as `minor`: every publishable package is in the Changesets
361
+ > lockstep group, so one `major` would promote the whole monorepo. Breaking
362
+ > changes ship as `minor` during the launch window — the migration notes below
363
+ > are what reach consumers in `CHANGELOG.md`.
364
+
365
+ ## Tenancy is now a spectrum (D1)
366
+
367
+ `single | group | isolated`, resolved by the `tenancy` service and selected with
368
+ the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged:
369
+ `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED`
370
+ (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot
371
+ rather than silently landing in a posture with no organization wall.
372
+
373
+ - `single` — no wall (unchanged).
374
+ - `group` — **new.** Organizations are membership boundaries over one shared
375
+ dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC
376
+ semantics). Enforced by the OPEN engine.
377
+ - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping`
378
+ probe and degraded-boot handling all unchanged.
379
+
380
+ ## Organization scope is a first-class context field (D2)
381
+
382
+ `ExecutionContext.accessible_org_ids` — every organization the caller holds a
383
+ currently-valid membership in (ADR-0091 validity windows) — is resolved once by
384
+ `resolveAuthzContext` and carried by every transport. The `group` wall reads it
385
+ directly; RLS policies may reference it as
386
+ `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set
387
+ fails the wall closed.
388
+
389
+ Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still
390
+ computed independently of the RLS compiler, AND-composed outermost, and
391
+ crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so
392
+ ADR-0095's W1/W2 invariants hold in every posture.
393
+
394
+ ## Two P0 correctness fixes (D3, D4) — behavior changes
395
+
396
+ **D3 — app-authored org-scoped RLS policies are no longer silently dropped**
397
+ (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose
398
+ `using` contained the substring `current_user.organization_id` when isolation was
399
+ inactive, which swallowed app-authored policies as well as the platform's own.
400
+ Stripping is now decided by PROVENANCE (identity against the shipped
401
+ declaration). **Upgrade impact:** in a deployment with no organization wall, an
402
+ app-authored policy referencing the active organization is now RETAINED and
403
+ fails closed (zero rows) with a one-time warning, where it previously vanished
404
+ and the object read unscoped. `getReadFilter` shared the defect, so analytics and
405
+ raw-SQL consumers were affected too. If a policy was only ever meant for
406
+ multi-org, delete it or install `@objectstack/organizations`.
407
+
408
+ **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization
409
+ boundary** (finding F2, framework#3540). Under a wall-less posture nothing
410
+ bounded the wildcard superuser bits `organization_admin` carries, so a
411
+ deployment that accumulated organizations (personal orgs on signup) made every
412
+ owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a
413
+ de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and
414
+ revokes the superseded variant whenever the posture changes. **Upgrade impact:**
415
+ in `single` posture an org owner/admin keeps full CRUD but loses the blanket
416
+ ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains
417
+ available through `admin_full_access` or an explicitly authored permission set —
418
+ it just stops being a side effect of a better-auth membership role.
419
+
420
+ ## Engine-owned organization stamping (D5)
421
+
422
+ Under any wall-enforcing posture the engine stamps `organization_id` from the
423
+ caller's active organization on an insert that omits it, and validates every
424
+ supplied value against the wall. Idempotent with the enterprise auto-stamp
425
+ (neither overwrites a supplied value). This also closes a real hole: the
426
+ pre-existing post-image check required a non-array payload, so a BULK insert
427
+ could carry a forged `organization_id` per row. One forged row now denies the
428
+ whole write.
429
+
430
+ ## Group structure, extension fields and red-line lints (D6, D7)
431
+
432
+ - `sys_organization` gains `parent_organization_id` and `sort_order` — a
433
+ **reporting dimension only**.
434
+ - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`,
435
+ `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` /
436
+ `os validate`: an RLS policy or sharing rule that walks the org tree is an
437
+ error, as is a business-unit grant on a platform-global object.
438
+ - Extension fields on better-auth-managed objects ride the existing ADR-0092
439
+ whitelist. A new guard derives better-auth's real field surface from
440
+ `getAuthTables()` at the pinned version and fails the build on any name
441
+ collision, so a library upgrade cannot silently take ownership of a column.
442
+
443
+ ## Enforce-or-remove (D11) — BREAKING
444
+
445
+ Both removals are of surface that had **zero runtime consumers**, so no
446
+ behavior changes; authoring them is now a no-op instead of a lint warning.
447
+
448
+ - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read
449
+ it. FROM → TO: a set a policy needs as `field IN (current_user.<key>)` is now
450
+ supplied by a registered membership resolver (below); a constant belongs in
451
+ the policy itself as a literal (`status = 'published'`).
452
+ - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`)
453
+ — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO:
454
+ matrix requirements are served by multi-position × business-unit anchoring; a
455
+ generalized dimension-security module will arrive with its own ADR.
456
+ - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has
457
+ merged since ADR-0056 finally has a producer: register an
458
+ `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the
459
+ `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by
460
+ construction — an unresolved key makes its policies drop out. Kernel-owned
461
+ keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be
462
+ overwritten from this seam.
463
+
464
+ ## Edition boundary (D12)
465
+
466
+ The `group` posture's enforcement primitives ship OPEN — the union wall,
467
+ `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness
468
+ fixes and the D6 lints — because the correctness of a wall is never a paid
469
+ feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing
470
+ enterprise `org-scoping` probe, so the current commercial boundary for
471
+ legal-entity isolation is unchanged by this release.
472
+
473
+ - 28ad90e: feat(types,cloud-connection,lint,cli): ADR-0120 17.x 收尾 —— `isolated` 安装期姿态硬门(D5e)、D5c 重拼写 advisory、成文契约扫荡与三姿态 conformance (#5081)
474
+
475
+ ADR-0120 17.x 波的第三块,也是最后一块。前两块已在 main 上:#5212(driver 侧
476
+ D3+D4 —— `COALESCE(organization_id, '__global__')` 物化、drift 两侧同步、重复预检)
477
+ 与 #5208(spec 词汇 `'organization'` + D5a/D5b lint)。本次补齐三件事:安装期的
478
+ 姿态决策点、剩余的成文契约、以及把「一个 app 包跑遍三种姿态」从假设变成测试。
479
+
480
+ **D5e —— 装进 `isolated` 环境时的硬门。** 词汇本身是姿态无关的:作者说的是业务
481
+ 边界(`'organization'` 一个组织一份 / `'global'` 整个安装一份),没有任何索引形状
482
+ 读姿态。唯一的残留在一个方向上:`isolated` 下组织就是**不同客户**,此时 app 业务
483
+ 对象上的 `'global'` 唯一既跨客户过度约束,又变成跨客户的存在性预言机(S10/S14)。
484
+ 维护者裁定这是**硬门而非 advisory**:把带 `'global'` 唯一(非 `sys` 对象)的 app
485
+ 装进 `isolated` 环境会**停下来并逐索引列出**,安装者(通常是 AI agent)要么确认它
486
+ 确实是平台级的,要么改写为 `'organization'`;确认按 ADR-0104 attestation 风格
487
+ 留痕在安装清单里(`InstalledManifestEntry.globalUniqueAttestation` —— 确认了什么、
488
+ 谁确认的、何时、在哪个姿态下问的),**之后不复问**。
489
+
490
+ - 停下的安装**什么都不留**:先于 hot-register 和任何 ledger 写入,所以作者改完
491
+ 元数据可以直接重试,不需要先卸载。
492
+ - 逐索引确认是有牙齿的:`confirmGlobalUniques` 收 `true` 或明确的 id 数组,只确认
493
+ 其中一条仍会在剩下的那条上停住。
494
+ - 升级引入的**新**约束会被问,老的答案继续算数。
495
+ - 另一个姿态下给出的确认**不算同意** —— `isolated` 那个问题在 `single` 下从未被
496
+ 问过,所以按「未确认」处理(唯一不会静默放行跨客户约束的方向)。
497
+ - ⛔ **永不做成启动期告警**(#4884 纪律)。boot 时的 rehydrate 不评估此门;门够不到
498
+ 的两类存量 —— 门禁上线前的安装、装后姿态变更的环境 —— 由 `os doctor` 与
499
+ `os migrate plan` 的 advisory 形态覆盖。
500
+
501
+ 判定里有三条是承重的,别「简化」掉:声明索引上的裸 `unique: true` **算**(D1 说它
502
+ 就是 `'global'` 的位置式拼写,排除它等于让整个 17.x 可以靠拼写绕过);字段级
503
+ `true` **不算**(它是 `'organization'`,永久合法);`sys_`/`base_` 对象**不算**
504
+ (S5 那批引擎幂等键天然就是平台级的,每次安装都问一遍就是 #4884 的误报类)。
505
+
506
+ CLI: `os package install` 新增 `--confirm-global-uniques`,并把 409 渲染成可读的
507
+ 逐条清单而不是一句 "Install failed (409)"。
508
+
509
+ **D5c —— 遗留手写组织复合索引的 advisory。** 新规则
510
+ `unique/legacy-organization-composite`:声明的唯一索引自己列出了组织列
511
+ (`{ fields: ['name','organization_id'], unique: true }`)—— 这是词汇出现之前手写
512
+ per-organization 的写法。它读起来像「每组织唯一」,物化出来却是普通复合索引,而
513
+ SQL UNIQUE 是 NULL-distinct 的:组织列为 NULL 的行上它**什么都不约束**(#5030),
514
+ 在单组织部署上那就是每一行。改写成 `unique: 'organization'`(`fields` 原样保留,
515
+ driver 会把已列出的组织列**就地**变成 NULL-safe 形式)正是补上这个洞的动作。
516
+ **永远只是 advisory,永远不自动修**:老拼写永久合法、零强制 drift,而 opt-in 是
517
+ 真实的物理收紧,要走 D4 的 `recreate_index` + 重复预检。
518
+
519
+ **D6 —— 成文契约扫荡。** `content/docs/data-modeling/indexing.mdx` 的
520
+ §Two ways to say "unique" 全节按新词汇重写(含 `os:check` 代码块);
521
+ `content/docs/protocol/objectql/schema.mdx` 的 §Uniqueness and tenancy 重写为
522
+ §Uniqueness and scope —— 其中那句「单租户部署不受影响,租户列是常量,复合索引
523
+ 退化为单列索引」是 #5030 **证伪过的原话**,现已替换为 D3 的 NULL-safe 事实;
524
+ `content/docs/deployment/cli.mdx` 的 `replace_unique_index` / `recreate_index`
525
+ 条目补上 NULL-safe 形状与重复预检;`content/docs/references/**` 经
526
+ `gen:schema && gen:docs` 再生成,未手改。
527
+
528
+ 按 ADR-0120 Resolved #2 的非规范性引导(官方示例/脚手架/生成器在新代码中输出
529
+ 显式拼写),`skills/objectstack-data/**` 的索引与校验规则整体扫过:声明索引一律
530
+ 说清 scope,并新增一节完整讲 `'organization'` 的 NULL-safe 语义与「永远不写姿态」。
531
+ 顺带修掉那里长期使用的 `tenant_id` —— 平台的列叫 `organization_id`。
532
+ `examples/**`、`create-objectstack` 模板与 `os generate` 经核查**根本没有声明任何
533
+ 唯一约束**,故无可扫;这是核查结论,不是遗漏。
534
+
535
+ **三姿态 conformance(ADR §Acceptance tests)。** 同一个 fixture app 在
536
+ `single | group | isolated` 三姿态下启动,逐 S 行用**真实的违规插入**断言 enforcement
537
+ (S1/S2/S3/S4/S5/S6/S7/S8/S9/S11/S12),并逐姿态捕获物化出的索引键,断言三者
538
+ **逐字节相同** —— 「没有任何索引形状读姿态」这句话一旦有两者不同就是假的。相同性
539
+ 断言配了一条正向断言(对着期望的键形状),这样「三次都什么都没建」不会读成「一致」。
540
+ 外加 ADR 只要的那一条 transition smoke:在 `single` 下建库、`isolated` 下重新打开,
541
+ drift op 为零。
542
+
543
+ 对既有部署的影响:除新增的安装期确认外,本次不改变任何已有物化行为。字段级
544
+ `unique: true` 一如既往合法。
545
+
546
+ - 840ee4b: fix(analytics,runtime,types): gate cube auto-inference on object existence; stop the dispatcher boundary returning raw SQL (#3867)
547
+
548
+ Two independent defects on the `/analytics` surface, found while verifying #3770
549
+ against a real server. On an authenticated CRM dev server, before this change:
550
+
551
+ ```
552
+ POST /api/v1/analytics/query {"cube":"sqlite_master","measures":["count"],"dimensions":["type"]}
553
+ → 200 {"rows":[{"type":"index","count":262},{"type":"table","count":71},{"type":"view","count":1}],
554
+ "sql":"SELECT type AS \"type\", COUNT(*) AS \"count\" FROM \"sqlite_master\" GROUP BY type"}
555
+ ```
556
+
557
+ That is SQLite's internal schema table — never a registered object — read
558
+ successfully through the analytics endpoint. Not merely "the name reaches the
559
+ driver and errors": **any table the connection can see was readable.**
560
+
561
+ **① The cube name reached the driver as a table name.** `AnalyticsService.ensureCube`
562
+ auto-infers a minimal Cube when none is registered, with `cube.sql = <the queried
563
+ name>`. That is the intended "metric over an object" path — an `object-metric` KPI
564
+ widget queries `crm_account` with no authored Cube — but it accepted _any_ string,
565
+ so the endpoint could aggregate over an arbitrary physical table. The
566
+ analytics-side twin of the data-path gap #3770 closed, and it was not covered by
567
+ that fix: #3770 gated the protocol's `analyticsQuery`, which is the _degraded
568
+ fallback_; a deployment with `@objectstack/service-analytics` installed runs the
569
+ real engine instead (`ctx.replaceService`).
570
+
571
+ Inference is now gated on the same schema registry the data path consults, via a
572
+ new optional `AnalyticsServiceConfig.isRegisteredObject` that `plugin.ts` wires
573
+ from the `data` engine's `getObject`. Three-way rule: a registered Cube runs
574
+ untouched (its `sql` is whatever it declares); an unregistered name that IS an
575
+ object still auto-infers exactly as before; neither → `CUBE_NOT_FOUND` / 404
576
+ raised before any SQL exists, naming both ways to make the request valid. With no
577
+ probe configured the gate stands down and warns once — the same tiering #3770
578
+ took for a missing registry. `generateSql` (`/analytics/sql`) is gated too.
579
+
580
+ **② The dispatcher boundary returned `err.message` verbatim.** `errorResponseBase`
581
+ is the single error exit for _every_ route the dispatcher plugin mounts —
582
+ `/analytics`, `/packages`, `/i18n`, `/storage`, `/automation`, `/auth`,
583
+ `/notifications`, `/mcp`. `@objectstack/rest` has guarded its data routes against
584
+ driver dumps forever (`mapDataError`); this boundary guarded nothing, so any
585
+ driver error on any of those routes shipped its SQL to the client. Unlike ①, this
586
+ half is unconditional — it does not depend on the cube being invalid.
587
+
588
+ The leak heuristic moved out of `rest-server.ts` into `@objectstack/types` as
589
+ `looksLikeInternalErrorLeak` (both packages already depend on it) and is now
590
+ applied at both boundaries — one predicate, one place to widen when a new
591
+ dialect's phrasing shows up. `mapDataError`'s behaviour is unchanged. At the
592
+ dispatcher it applies **only to 5xx**: a 4xx message is a deliberate
593
+ business/validation answer and must reach the caller intact. Sanitising costs no
594
+ diagnostics — the untouched error still reaches `errorReporter` through the
595
+ existing `__obsRecordedError` side-channel.
596
+
597
+ **Also fixed in the same function:** `errorResponseBase` read only
598
+ `err.statusCode`, while domain errors across this codebase carry `status` (and
599
+ `HttpDispatcher.errorFromThrown` already reads `status` first). Every deliberate
600
+ 4xx thrown through a dispatcher route — including #3770's `OBJECT_NOT_FOUND` on
601
+ the analytics fallback path — was rendered as a **500**. It now reads `status`
602
+ then `statusCode`.
603
+
604
+ **Behaviour change.** `/analytics/query` and `/analytics/sql` return 404
605
+ `CUBE_NOT_FOUND` for a cube that is neither registered nor a registered object;
606
+ previously the name was passed to the driver. Dashboards and KPI widgets pointed
607
+ at real objects or authored cubes are unaffected. A 5xx on a dispatcher route
608
+ whose message looks like a driver dump now reads `Internal server error` — check
609
+ server logs or your error reporter for the original.
610
+
611
+ - 64cd010: fix(runtime,types)!: `/analytics/query` no longer echoes RLS policy field names — the declared-server-fault withhold is shared by both HTTP boundaries (#5811)
612
+
613
+ **Observable behaviour change — read this if you read, log, or assert on
614
+ `error.message` from a dispatcher-plugin route.** An error that **declares a
615
+ server fault** in the ADR-0112 envelope (`status >= 500` _and_ a non-empty
616
+ `code`) now leaves `dispatcher-plugin.errorResponseBase` with its message
617
+ replaced by `"Internal server error"`. It previously reached the caller verbatim
618
+ unless it happened to _sound_ like a SQL/driver dump. This applies to every route
619
+ that plugin mounts — `/analytics`, `/packages`, `/i18n`, `/automation`, `/auth`,
620
+ `/notifications`, `/mcp`, … — not only the one that motivated it. Nothing a
621
+ machine reads changed: the producer's `code` still arrives in the response
622
+ (`error.code`, promoted there from `details` by the shared envelope builder,
623
+ #3842), the status is untouched, and the full original text still goes to the
624
+ server log and `errorReporter` via `__obsRecordedError`.
625
+
626
+ ## What was wrong
627
+
628
+ #5367 (maintainer ruling 2026-08-06) made `read-scope-sql.ts`'s ten fail-closed
629
+ RLS lowering refusals `READ_SCOPE_COMPILE_FAILED` / 500 and taught
630
+ `POST /analytics/dataset/query` to withhold their message, because those messages
631
+ name the field names and comparands of an **administrator's** sharing rule:
632
+
633
+ ```
634
+ [read-scope-sql] unsafe field identifier "secret_policy_field" — refusing to
635
+ build read scope (fail-closed).
636
+ ```
637
+
638
+ The caller never wrote that field name and must not be able to read it out of an
639
+ error body. But the **sibling** analytics face was never closed.
640
+ `compileScopedFilterToSql` runs on both `NativeSQLStrategy.applyReadScope` and
641
+ `ObjectQLStrategy`'s echoed SQL, both of which serve `POST /analytics/query`,
642
+ which exits through `dispatcher-plugin.errorResponseBase`. That exit's only
643
+ message guard was `looksLikeInternalErrorLeak` — a heuristic over SQL/driver
644
+ _phrasing_ — and all eleven read-scope message shapes return `false` from it.
645
+ Measured at that boundary: **11 of 11 echoed verbatim**, at 500, with the policy
646
+ content in `error.message`. A real reachable disclosure, not a theoretical one.
647
+
648
+ ## What changed
649
+
650
+ - **`@objectstack/types` gains `declaresServerFault(err)`**, exported from
651
+ `error-leak.ts` beside `looksLikeInternalErrorLeak`. The heuristic asks whether
652
+ a message _sounds_ internal; the declaration asks whether the producer _said
653
+ so_. `error-leak.ts`'s own file header already states the principle — "do not
654
+ ship driver internals to clients" is a property of the HTTP boundary, not of
655
+ one router — and this is the second predicate that principle asks for.
656
+ - **Both boundaries read it.** `dispatcher-plugin.errorResponseBase` gains the
657
+ withhold (the fix); `rest-server.ts`'s `/analytics/dataset/query` catch drops
658
+ its in-line copy of the same test in favour of the shared one. #5808 wrote that
659
+ rule in-line on purpose — promoting a rule with one consumer is a speculative
660
+ surface — and this is the second consumer, so it was promoted rather than
661
+ duplicated (`#3843`/`#3867` paid for the two-implementations shape twice).
662
+ The REST face's verdict is unchanged in every case: same `status >= 500` plus
663
+ non-empty `code` test, over the same two fields.
664
+
665
+ ## What deliberately did NOT change
666
+
667
+ - ⛔ **This is not "withhold every 5xx".** #5667 kept **undeclared** 5xx errors
668
+ legible on purpose: a bare `Error` from our own code ("no strategy can handle
669
+ query …") is the operator's own bug report, names nothing tenant-sensitive, and
670
+ still falls to `looksLikeInternalErrorLeak` alone. A 5xx carrying only half an
671
+ envelope (a status with no code) is likewise still readable — inventing the
672
+ withhold for it would be the consumer-side leniency Prime Directive #12 removes.
673
+ - **4xx is untouched.** `declaresServerFault` requires `status >= 500`, so a
674
+ deliberate business/validation answer can never be swallowed by it.
675
+ - **`statusCode` is not accepted as a substitute for `status`.** `status` is the
676
+ channel ADR-0112 declares; making a disclosure rule depend on which spelling a
677
+ producer reached for would be the same leniency in a different place.
678
+ - **The heuristic was not taught to recognise `[read-scope-sql]`.** That would be
679
+ more prose sniffing — the mechanism #5352/#5367 exist to remove — and would only
680
+ ever cover the family someone remembered to add.
681
+
682
+ Coverage: `analytics-query-read-scope-withhold.test.ts` (runtime) drives six RLS
683
+ policy shapes end-to-end through a **real** `AnalyticsService` on the real
684
+ native-SQL path and the real mounted route, asserting the 500, that the whole
685
+ serialized body contains no policy detail, that `error.code` still carries
686
+ `READ_SCOPE_COMPILE_FAILED`, and that the full text is still on the
687
+ `__obsRecordedError` side-channel — plus a positive control and both sides of the
688
+ declared-vs-undeclared tiering. `error-leak.test.ts` (types) pins the predicate
689
+ directly, including that all eleven read-scope shapes stay invisible to the
690
+ heuristic. The REST face's existing `analytics-read-scope-refusal-envelope.test.ts`
691
+ is green before and after, unchanged, which is the pin on the refactor.
692
+
693
+ - 9f5cc79: A bulk write's per-row `errors[].httpStatus` carries the status its producer declared, in any spelling (#8570)
694
+
695
+ `toRowApiError` set the limb from `err.status` alone, so two well-defined client
696
+ refusals shipped a batch row with no status at all: objectql's `ValidationError`
697
+ (a 400 recognisable by shape, which deliberately declares no `status`) and
698
+ `plugin-approvals`' record lock (a 409 spelled `statusCode`). Sibling rows in the
699
+ same response did carry one — `rowRequiredIdError` → 400,
700
+ `recordNotFoundError` → 404 — so a caller branching on `httpStatus` to tell "fix
701
+ your input" from "the server broke" got an answer for some failure rows and
702
+ silence for others, with nothing saying which. Same single-spelling defect #7525
703
+ fixed at the HTTP door, one layer down.
704
+
705
+ The limb now asks `resolveThrownHttpError` — the resolver the HTTP doors and the
706
+ row's `message` limb already answer with — so a refusal declaring `.status`,
707
+ `.statusCode` or the `VALIDATION_FAILED` shape reaches the row as the status it
708
+ always meant. Rows whose throw declared nothing (a driver fault, a hook throwing
709
+ a bare `Error`) still carry no `httpStatus`: the resolver's 500 there is the
710
+ caller's fallback, not a producer's claim, and stamping it would add a field to
711
+ the wire for those populations rather than restore a declared one. `code` reads
712
+ the same resolution, so a row can no longer contradict itself.
713
+
714
+ `ThrownHttpError` gains `declaredStatus` — the resolved status minus the
715
+ fallback, absent when the throw declared none. `status` is unchanged, and every
716
+ boundary that answers with the status itself keeps reading it; the new field is
717
+ for sinks that mirror a status onto response DATA, where a fallback would be an
718
+ invention.
719
+
720
+ - 2a37694: fix(plugin-dev,types): the production escape hatch stops being silent (#3900)
721
+
722
+ `DevPlugin.init()` refuses to run under `NODE_ENV=production` (ADR-0115 D6), and
723
+ `OS_ALLOW_DEV_PLUGIN` overrides that refusal. As shipped, the override returned
724
+ early with **no output at all**: the process ran the development assembly while
725
+ every log line and the ready banner read like an ordinary production start.
726
+
727
+ That reproduces, one level up, the defect the guard exists to close. The guard's
728
+ own precedent says so — `OS_ALLOW_DEGRADED_TENANCY` boots degraded _and brands
729
+ it everywhere an operator looks_, and `OS_ALLOW_DRIVER_CONNECT_FAILURE`'s
730
+ contract is "logged loudly at startup". An escape hatch that says nothing leaves
731
+ the operator's only evidence of a degraded state in an env var they may not have
732
+ set themselves.
733
+
734
+ **The override now brands itself, twice.** A warning at `init()` — emitted
735
+ before any assembly work, so it survives an assembly step that later throws —
736
+ and a repeat on the ready banner, which is the surface an operator actually
737
+ reads:
738
+
739
+ ```
740
+ ⚠ DEV ASSEMBLY UNDER NODE_ENV=production (OS_ALLOW_DEV_PLUGIN is set) — the boot
741
+ guard was explicitly overridden. This process is running the DEVELOPMENT
742
+ assembly, which is not hardened for production traffic (ADR-0115 D6).
743
+ • Auth secret is the default published inside @objectstack/plugin-dev. It is
744
+ public, so anyone can mint a session this stack accepts. Pass `authSecret`
745
+ explicitly.
746
+ • Data goes to the in-memory driver with persistence disabled — every record
747
+ is lost when this process exits.
748
+ ```
749
+
750
+ Only hazards that are live for _that_ configuration are named: the secret line
751
+ is suppressed when the operator passed their own `authSecret`, and the driver
752
+ line when the `driver` toggle is off. The dev-admin seed is deliberately absent
753
+ — `plugin-auth`'s `maybeSeedDevAdmin` is hard-gated to
754
+ `NODE_ENV === 'development'` and cannot fire on this path, so warning about it
755
+ would spend the attention the real hazards need.
756
+
757
+ **New export — `resolveAllowDevPlugin()` (`@objectstack/types`).** The flag moves
758
+ off a bare `process.env['OS_ALLOW_DEV_PLUGIN'] === '1'` and joins the
759
+ `OS_ALLOW_*` family's shared truthy vocabulary, next to
760
+ `resolveAllowDegradedTenancy` / `resolveAllowDriverConnectFailure`.
761
+
762
+ FROM → TO for operators: `OS_ALLOW_DEV_PLUGIN=1` keeps working unchanged.
763
+ `OS_ALLOW_DEV_PLUGIN=true` (and `on` / `yes`, case-insensitive, surrounding
764
+ whitespace ignored) **now takes effect** where the strict comparison previously
765
+ ignored it and failed the boot. That is a widening, in the direction an operator
766
+ setting the flag already intended; falsy and unrecognised values still refuse to
767
+ boot, and unset still means "fail fast". If you were relying on
768
+ `OS_ALLOW_DEV_PLUGIN=true` being inert as a way to keep the guard armed, unset
769
+ the variable instead.
770
+
771
+ No change to the refusal path, which this issue re-verified end to end:
772
+ `kernel.use()` only registers, `initPluginWithTimeout` does not catch,
773
+ `bootstrap()` rethrows, and `os serve`'s outer handler prints the message and
774
+ exits `1`. The `throw` is genuinely fatal here, so it needs none of the
775
+ `process.exit(1)` the tenancy guard required for sitting inside a broad `catch`.
776
+
777
+ - b25a116: fix(verify): resolve the enterprise organizations package from the HOST APP (#4700)
778
+
779
+ `bootStack(app, { multiTenant: true })` — and therefore `objectstack verify
780
+ --multi-tenant` — could never load `@objectstack/organizations`. Node ESM
781
+ resolves a bare `import()` against the **importer's own realpath**, which for
782
+ `packages/verify` is inside the framework workspace, while the enterprise
783
+ package is cloud-private and only ever lives in the verified app's
784
+ `node_modules`. Every real host app fell into the catch and was told to
785
+ "Install/link it in this workspace" — about a package it had already installed.
786
+ Same defect class as cloud#1013, which fixed `objectstack serve`; #4699 fixed
787
+ that one call site and this issue tracked the two the sweep left behind.
788
+
789
+ **New: `@objectstack/types/node`.** The host-app resolver (`createHostRequire` /
790
+ `createHostImporter`) moved out of `packages/cli/src/utils/import-from-host.ts`
791
+ — where `@objectstack/verify` and the dogfood suite could not import it without
792
+ inverting the dependency direction — into a **node-only subpath export** of
793
+ `@objectstack/types`. One behaviour, one source; the CLI now consumes it and its
794
+ private copy is deleted.
795
+
796
+ It is a subpath and **not** the root export because `@objectstack/types` is a
797
+ dependency of `@objectstack/hono` ("edge-compatible REST API server for
798
+ Cloudflare Workers, Deno, Bun, and Node") and of the plugin layer a `LiteKernel`
799
+ boots on Workers. The root entry reaches zero `node:` builtins, and a Workers
800
+ bundle breaks on `node:module` even when nothing calls it. `tsup` emits the two
801
+ entries as separate self-contained bundles (`splitting: false`), and a test
802
+ walks the root's import graph and fails on the first reachable `node:`
803
+ specifier, so the isolation is enforced rather than merely intended. Same
804
+ arrangement `@objectstack/metadata` already ships for its `./node` subpath.
805
+
806
+ **New: `BootOptions.hostRoot`** (optional, defaults to `process.cwd()`) names
807
+ the app whose `node_modules` supplies those optional packages — for a harness
808
+ booting an app that is not the working directory.
809
+
810
+ **The dogfood multi-org gates had never run.** Two suites probed availability
811
+ with the same bare `import()` and so were **constant-false** — not "false
812
+ because absent" but false by construction, in every environment including the
813
+ cloud CI whose comment claimed it ran them. The #1994 cross-tenant RLS proof and
814
+ the attachments cross-tenant isolation block had therefore never executed while
815
+ the suite reported green (Prime Directive #10, test-suite edition). They now
816
+ resolve like the runtime does, and `OS_TEST_MULTI_ORG_ENABLED=1` declares that a
817
+ run is expected to ship the package — turning a silent skip into a loud failure,
818
+ so a run can no longer pass by quietly not running the gates it exists for.
819
+
820
+ - 02dc076: feat(types,cli,verify)!: 只解析 host app 声明过的包 —— `NODE_PATH` 不再算数,ADR-0093 D5 那道墙从此与启动方式无关 (#4719)
821
+
822
+ **问题:契约写下了,但从没被检查过。** `@objectstack/types/node` 的
823
+ `createHostRequire` 返回一个 CJS `createRequire`,而 CJS 解析认 `NODE_PATH`
824
+ (`Module.globalPaths`)。pnpm 生成的 bin shim 第一件事就是
825
+ `export NODE_PATH=<workspace>/node_modules/.pnpm/node_modules`,于是任何被工作区里
826
+ **任意一个包**传递依赖到的包都能"从 host app 解析成功" —— 跟这个 app 声明了什么毫无关系。
827
+
828
+ 实测(cloud `apps/objectos-ee`,当时未声明 `@objectstack/organizations`):
829
+ `pnpm start`(经 shim)boot 成功、插件表里有 `Organizations`、ADR-0093 D5 一声不吭;
830
+ `node node_modules/@objectstack/cli/bin/run.js serve`(不经 shim)则
831
+ `✖ FATAL: tenancy posture 'isolated' was requested…` 并 exit 1。同一个 app、同一份
832
+ `package.json`、同一个 posture,**只因为进程是怎么被拉起来的**,走出两种结果。
833
+ 而 D5 的报错一直在教 operator "declare it in the app's package.json" —— 那正是
834
+ CLI 从来没检查过的那件事。
835
+
836
+ **改法:声明即执行。** 解析前先读 `<hostRoot>/package.json`;只有包名出现在
837
+ `dependencies` / `devDependencies` / `optionalDependencies` / `peerDependencies`
838
+ 的 **键**里,才去 host 的 `node_modules` 里查它。仅仅"能被解析到"不再算数 ——
839
+ 那正是让契约失效的那个偶然。未声明的包退回到 importing package 自身的解析
840
+ (ESM,不认 `NODE_PATH`),框架自有的包加载路径不受影响。
841
+
842
+ **两种失败从此分开报。** 今天它们都塌成同一条 `MODULE_NOT_FOUND`,补救办法却相反:
843
+
844
+ - **未声明** —— 指向"在 app 的 `package.json` 里声明并安装",并说明为什么
845
+ hoisting / `NODE_PATH` 不被接受;
846
+ - **声明了但解析不到** —— 明确说这是**安装**问题(`pnpm install`、生产 prune
847
+ 砍掉了它、dist 没构建),别再让人回去重看那份已经写对的 `package.json`。
848
+
849
+ 分类经新导出的 `hostImportFailureKind(err)` 暴露给调用方;两种错误都仍带
850
+ `code: 'MODULE_NOT_FOUND'`,`isModuleNotFoundError` 的既有判定不变。
851
+
852
+ **BREAKING — 哪类部署会从假绿变红,以及怎么修。**
853
+
854
+ 1. **靠 hoisting 苟着的部署。** 一个 app 请求了 walled tenancy posture
855
+ (`OS_TENANCY_POSTURE=group` / `isolated` 或 `OS_MULTI_ORG_ENABLED=1`)、
856
+ 却没在自己的 `package.json` 里声明 `@objectstack/organizations`,过去经 pnpm
857
+ shim 启动能正常 boot —— 现在会命中 ADR-0093 D5 并 exit 1。
858
+ **修法:在那个 app 的 `package.json` 里声明该依赖并安装。**
859
+ 这些部署本来就在未声明状态下运行,红的是一直存在的事实,不是新引入的故障:
860
+ 同一个 app 不经 shim 启动今天就已经是 exit 1。
861
+ (同样适用于 `@objectstack/service-ai` / `@objectstack/service-ai-studio`,以及
862
+ `bootStack({ multiTenant: true })`、dogfood 的 enterprise 门。)
863
+
864
+ 2. **`createHostImporter` 的签名变了**,因为它现在需要 host 的**根目录**才能读到
865
+ 那份 manifest,而一个 `NodeRequire` 无法被问出它锚在哪里:
866
+
867
+ ```diff
868
+ - createHostImporter(createHostRequire(hostRoot))
869
+ + createHostImporter(hostRoot) // 省略参数 = process.cwd(),同旧默认
870
+ ```
871
+
872
+ `createHostRequire` 本身保持不变,仍然导出。
873
+
874
+ 新增导出(`@objectstack/types/node`):`HOST_DECLARATION_FIELDS`、
875
+ `HostDeclarationField`、`HostDeclaration`、`readHostDeclaration`、
876
+ `isDeclaredByHost`、`packageNameFromSpecifier`、`HostImportFailureKind`、
877
+ `HOST_IMPORT_FAILURE_KIND`、`hostImportFailureKind`。
878
+
879
+ - 030125b: feat(objectql)!: `init()` refuses to boot when a data driver fails to connect (#3741)
880
+
881
+ `ObjectQLEngine.init()` wrapped every driver's `connect()` in a try/catch, logged
882
+ one error line, and carried on. A server whose database was unreachable therefore
883
+ "started successfully" — health endpoints could even stay green — and then failed
884
+ every request with an error that reads nothing like _the database is down_. The
885
+ warning it printed (`Operations may recover via lazy reconnection or fail at query
886
+ time`) was half fiction: grep the repo and no reconnection exists in `driver-sql`
887
+ or `driver-mongodb`, so only the "fail at query time" half was ever real. The
888
+ caller made it worse — `ObjectQLPlugin.start()` runs `syncRegisteredSchemas()`
889
+ immediately after `init()`, issuing DDL against a driver that isn't there.
890
+
891
+ The structural half of the bug was worse than the operational one: the catch
892
+ removed a driver's ability to **refuse startup at all**. Any fatal startup check —
893
+ licence, server version, incompatible configuration, missing capability, not just
894
+ an unreachable socket — is expressed by throwing from `connect()`, and every one
895
+ of them was silently downgraded to a runtime error. That is why driver-mongodb's
896
+ multi-tenancy guard (#3724 / #3734) had to be hoisted into its constructor.
897
+
898
+ - `init()` now **throws** `DriverConnectError` (`code: 'ERR_DRIVER_CONNECT'`)
899
+ when any boot-registered driver's `connect()` rejects, aborting kernel
900
+ bootstrap. It still attempts every driver first, so one failed boot names all
901
+ of them. The message is self-contained — each failed driver and its cause —
902
+ because the CLI prints `error.message` alone; the first cause is also attached
903
+ as `error.cause`. Exported from both `@objectstack/objectql` and
904
+ `@objectstack/objectql/core`.
905
+ - `connect()` is now a supported place for a driver to veto boot. Startup
906
+ validation that needs a live connection (server version, capability probes)
907
+ no longer has to be forced into a constructor.
908
+ - The misleading "lazy reconnection" warning is gone.
909
+ - New escape hatch `OS_ALLOW_DRIVER_CONNECT_FAILURE=1`
910
+ (`resolveAllowDriverConnectFailure()` in `@objectstack/types`) restores the old
911
+ lenient boot, but loudly: a `DEGRADED BOOT` banner names the failed drivers and
912
+ states that they are never retried or reconnected and that every query and
913
+ schema sync routed to them will fail for the process lifetime. The banner goes
914
+ to stderr as well as the logger, because `os serve` swallows all of stdout
915
+ during boot and `Logger` routes `warn` there — logger-only, the one message
916
+ that matters would be invisible in exactly the deployment the flag is for.
917
+ Defaults off.
918
+
919
+ **Migration.** No code or config change is needed for a correctly configured
920
+ deployment — a driver that connected before still connects. A deployment that was
921
+ _silently_ booting without its database now fails the boot instead, with the
922
+ driver name and cause in the error; fix the datasource configuration (typically
923
+ `OS_DATABASE_URL`, credentials, or network reachability). To keep booting without
924
+ it — deliberately, and knowing every request that touches it will fail — set
925
+ `OS_ALLOW_DRIVER_CONNECT_FAILURE=1`.
926
+
927
+ - b4b2c7d: fix(rest,runtime,types): the direct-mount package door answers a coded refusal with its own status and code (#8016)
928
+
929
+ **This changes HTTP status codes on a live surface.** Requests to
930
+ `/api/v1/packages` that today come back `500 INTERNAL_ERROR` will come back as
931
+ the refusal they always were — `409 DESTRUCTIVE_CHANGE` for an uninstall that
932
+ would drop data, `400`/`403` for a coded refusal thrown from below. A client
933
+ that keys on `500` to decide "the platform is down, retry later" for these
934
+ routes must key on the `code` instead. No route, path, verb or success body
935
+ changes.
936
+
937
+ `/api/v1/packages` has two HTTP transports. The runtime dispatcher's
938
+ (`packages/runtime/src/domains/packages.ts`) reads a thrown error's own
939
+ `.status` and `.code` and answers with them. The direct-mount REST registrar
940
+ (`packages/rest/src/package-routes.ts`) had **four** catch-alls that answered
941
+ `sendError(res, 500, 'INTERNAL_ERROR', …)` regardless — and that registrar
942
+ mounts _first_ in the production stack, so the status-blind answer was the one
943
+ production actually returned. `packageService.publish`, `packageService.delete`
944
+ and `protocol.deletePackage` all execute inside those blocks, and
945
+ `@objectstack/metadata-protocol` throws coded, status-carrying refusals from
946
+ that call path. So a caller who was **refused** was told the platform had
947
+ **broken**: the wrong class of answer, a retry that cannot succeed, and the one
948
+ field a client can branch on dropped.
949
+
950
+ The four sites now leave through one shared exit. The mapping is not
951
+ reimplemented here — that is how the two doors diverged in the first place. It
952
+ moved to `resolveThrownHttpError` in **`@objectstack/types`** (alongside the
953
+ `sendOk`/`sendError` envelope writer and `looksLikeInternalErrorLeak`, for the
954
+ same reason: it is a property of the HTTP boundary, not of one router), and the
955
+ dispatcher's `HttpDispatcher.errorFromThrown` is now its other caller. It could
956
+ not live in `@objectstack/runtime`: that package depends on `@objectstack/rest`,
957
+ so the import can only point one way.
958
+
959
+ The rule, unchanged from what the dispatcher always applied:
960
+
961
+ - **status** — the producer's `.status`, then `.statusCode` (both spellings are
962
+ produced in this repo), then `400` for a record-validation failure, then the
963
+ caller's fallback.
964
+ - **code** — `VALIDATION_FAILED` for a validation failure, then the thrown
965
+ `.code` **when it is a member of the declared ADR-0112 vocabulary**
966
+ (`StandardErrorCode ∪ ERROR_CODE_LEDGER`), then the code the status derives.
967
+ An unregistered code no longer reaches `error.code` on the dispatcher door
968
+ either; it would have failed envelope parse, which is ADR-0112's closed
969
+ vocabulary working rather than a dialect leaking onto the wire.
970
+ - **the 500 survives** — a throw declaring neither status nor a registered code
971
+ is a genuine fault and still answers `500 INTERNAL_ERROR`.
972
+
973
+ `validation-failure.ts` moved from `@objectstack/runtime` to
974
+ `@objectstack/types` for reachability and is re-exported from its old module
975
+ path; every existing import site is unchanged.
976
+
977
+ Unchanged and deliberately so: this REST door still ships a 5xx message
978
+ verbatim, where the dispatcher withholds one that looks like an internal leak
979
+ (`looksLikeInternalErrorLeak`, #3867). That asymmetry predates this fix and is
980
+ filed separately.
981
+
982
+ - 91cefb8: refactor(types,rest,metadata,analytics): Postgres 的 `"x" of relation "y"` 短语收归一处,三个包不再各修一遍同一个超串洞(#6615)
983
+
984
+ Postgres 把「关系内部某个子对象」的失败写成 `column "label" of relation "sys_team" does not exist`——里面**逐字包含**一句合法的「表不存在」短语 `relation "sys_team" does not exist`,含义却相反:关系正因为存在才被点名。任何对「这句话是不是在说表没了」的正则收紧都消不掉这个匹配,短语确实在里面;唯一的修法是**先问更具体的问题**。所以修的是**顺序**,不是模式。
985
+
986
+ 正因为如此,这个短语被分三次教给了这个仓库,分属三个包、三个 PR,其中两次是在别处已经踩过同一个洞之后:`@objectstack/rest` 的 `mapDataError`(#5352)、`@objectstack/service-analytics` 的缺列扣除(#6035 / PR #6346)、`@objectstack/metadata` 的 `MISSING_TABLE.excludes`(#6347 / PR #6613)。本次把它收进 `@objectstack/types`,与 `isUniqueViolationError`(#6250)和 `isModuleNotFoundError`(framework#3265)同一个理由与同一个位置。
987
+
988
+ **两种宽度,故意保留成两个导出。** 三个消费者要的并不是同一条正则,差别也不是随手写的,而是**每个站点哪个方向的误差是安全的**:
989
+
990
+ - `matchMissingColumnOfRelation(message)` —— 严格提取器,锚定 Postgres 的 errmsg 模板 `column "%s" of relation "%s" does not exist`,返回列名。`rest` 用它把 42703 答成 `400 INVALID_FIELD` 而不是 `404`;`service-analytics` 用它在分类前扣除缺列。这两处**过宽**会把真正缺失的表变成硬失败、回退 #5033 刻意保留的宽容,**漏匹配**只是让消息含糊一点——所以必须严格。
991
+ - `isRelationSubObjectPhrase(message)` —— 宽检测器,丢掉 `column` / `[a-z0-9_]+` / `does not exist` 三个锚点:任意子对象、任意带引号标识符、任意判词。`metadata` 用它做排除。这一处**过宽**只会把良性判定变成响亮判定,**漏匹配**却会让 `event_seq` 从 1 重新开始、撞进一张已有行的历史表——方向正好相反。
992
+
993
+ 把两者合并成一条正则,无论哪种宽度胜出都会对其中一个调用方是错的;这是卡片记录在案的风险,两个导出即为此而设,理由是承重的而非风格的。仓库里第四份拷贝(`service-analytics` 测试内用于守护 fixture 的那条正则)同时收编:它本是为「两张面孔别对不上」而写,却把断言打在其中一面的私有复述上,因而正是它要防的漂移。
994
+
995
+ 行为逐字保持不变:搬进来的两条模式与原站点逐字节相同。`@objectstack/service-analytics` 因此新增一条对 `@objectstack/types` 的依赖边——这是本次唯一的依赖变化,构造上无环(`@objectstack/types` 只依赖 `@objectstack/spec`,后者无仓内依赖),且仓库 73 个包中已有 25 个、16 个 service 中已有 5 个携带同一条边。
996
+
997
+ - d5749d7: refactor(types,rest,services,plugin-sharing): one shared writer for the response envelope, and `error.code` is enforced at compile time (#3973)
998
+
999
+ `BaseResponseSchema` declares one envelope for every REST body the platform
1000
+ emits. It declared it once; the code that _wrote_ it was copied per route
1001
+ module. After #3843 and #3983 converted the last drifting one, seven modules
1002
+ each carried their own two-line `sendOk` / `sendError` pair — so the envelope's
1003
+ shape lived in fourteen places rather than one.
1004
+
1005
+ `pnpm check:route-envelope` proved those seven copies agreed, which is why this
1006
+ is a cleanup rather than a bug fix. But a guard proves agreement; it does not
1007
+ create it. An eighth module starts by copying the pair again — not
1008
+ hypothetically: `share-link-routes.ts` was found already drifting by the
1009
+ repo-wide scan, and its drift had broken `client.shareLinks.create()` and
1010
+ `.list()` through `unwrapResponse` (#3983).
1011
+
1012
+ ## What moved
1013
+
1014
+ `sendOk` / `sendError` now live once, in `@objectstack/types`
1015
+ (`response-envelope.ts`), and all seven modules import them:
1016
+
1017
+ | Module |
1018
+ | ------------------------------------- |
1019
+ | `service-storage/storage-routes.ts` |
1020
+ | `service-settings/settings-routes.ts` |
1021
+ | `service-datasource/admin-routes.ts` |
1022
+ | `rest/external-datasource-routes.ts` |
1023
+ | `rest/package-routes.ts` |
1024
+ | `service-i18n/i18n-service-plugin.ts` |
1025
+ | `plugin-sharing/share-link-routes.ts` |
1026
+
1027
+ Placement was the open question in #3973, not design. `packages/spec` is
1028
+ schemas-only (Prime Directive #2), and the callers span `rest`, four
1029
+ `services/*` and one `plugins/*`, which rules out anything depending on them.
1030
+ `@objectstack/types` depends on nothing but `@objectstack/spec`, so every caller
1031
+ can reach it, and it is already where the repo puts a helper the HTTP boundaries
1032
+ share — `looksLikeInternalErrorLeak` (#3867) sits one file over and made the
1033
+ same argument first.
1034
+
1035
+ The builders take a structural `{ status(n), json(body) }`, so the package
1036
+ imports no HTTP contract at all: `IHttpResponse` satisfies it, and so does the
1037
+ `any`-typed `res` the older modules carry.
1038
+
1039
+ ## `error.code` is now checked by the compiler
1040
+
1041
+ All seven copies typed the parameter `code: string`. ADR-0112 (#3841) closed the
1042
+ vocabulary — `ErrorCode` is `StandardErrorCode ∪ ERROR_CODE_LEDGER` — but an
1043
+ invented code was still caught only at runtime, by a conformance suite parsing a
1044
+ driven body, i.e. only on routes some test happened to drive.
1045
+
1046
+ The shared `sendError` types `code` as `ErrorCode`, so an unregistered code now
1047
+ fails to compile, at every call site at once:
1048
+
1049
+ ```ts
1050
+ sendError(res, 400, "NOT_A_REGISTERED_CODE", "invented");
1051
+ // Argument of type '"NOT_A_REGISTERED_CODE"' is not assignable to parameter of type 'ErrorCode'.
1052
+ ```
1053
+
1054
+ This cost no call-site churn: every code the seven modules emit was already
1055
+ registered.
1056
+
1057
+ ## `extra` is closed at the same place
1058
+
1059
+ `sendError`'s last parameter is `Pick<ApiError, 'category' | 'httpStatus' |
1060
+ 'details' | 'requestId'>` — exactly what `ApiErrorSchema` declares beside `code`
1061
+ and `message`.
1062
+
1063
+ It was `Record<string, unknown>` while `settings-routes` still hung `namespace` /
1064
+ `key` / `reason` / `fields` beside `code`. Those bodies passed every gate anyway:
1065
+ `ApiErrorSchema` is a plain `z.object`, so unknown keys were STRIPPED rather than
1066
+ rejected, and `envelopeViolations` inspects only the body's top level —
1067
+ conformant _by stripping_ rather than by declaration. #4224 moved that module
1068
+ onto `details`, which is what lets the parameter close here. Closing it at the
1069
+ shared builder is the part that lasts: an undeclared sibling is now a compile
1070
+ error in every module at once, rather than a key that quietly evaporates in
1071
+ whichever module reintroduces it.
1072
+
1073
+ ## Nothing changes on the wire
1074
+
1075
+ The seven pairs were identical modulo the optional `status` and `extra`
1076
+ parameters this one unions, and each module's driven conformance suite still
1077
+ parses its real bodies against the real spec schemas. One internal call site was
1078
+ rewritten: `package-routes` passed `details` positionally and now passes
1079
+ `{ details }`, producing the same `error.details` it always did.
1080
+
1081
+ ## The guard got stronger
1082
+
1083
+ `scripts/check-route-envelope.mjs` counts response write sites per module. A
1084
+ module that routes everything through the shared pair builds **none** itself, so
1085
+ the seven now declare `0 / 0 / 0` where they used to declare `2 / 1 / 1`, and the
1086
+ shared pair is pinned separately at `2 / 1 / 1` so the invariant stays total for
1087
+ the surface rather than per-module. What the count asserts is no longer "your two
1088
+ builders are the enveloped ones" but "you have no builders" — and a new route
1089
+ that hand-rolls a body still moves it off zero and fails.
1090
+
1091
+ - 129b378: fix(types,rest): one named answer for "which column conflicted" — an index name is never returned as one (#6544)
1092
+
1093
+ #6250 retired four private "is this a unique violation?" vocabularies into
1094
+ `isUniqueViolationError`. It left the harder half of the question behind: the
1095
+ import runner's `sanitizeRowError` still carried its own three-dialect regex
1096
+ chain, because it does **more** than answer yes/no — it names the offending
1097
+ column so the importer can say _"A record with this `email` already exists."_
1098
+ This lands that second answer as a shared export and migrates the last private
1099
+ copy onto it.
1100
+
1101
+ **New — `uniqueViolationColumn(error)` in `@objectstack/types`** (`string |
1102
+ undefined`), sibling to `isUniqueViolationError` and gated on it, reading the
1103
+ same channels one step down the same bounded `cause` chain, plus
1104
+ node-postgres' `detail` field.
1105
+
1106
+ **Its contract, per the maintainer's 2026-08-08 ruling: a value comes back only
1107
+ when the identifier the driver printed is determinably a COLUMN.** When a
1108
+ dialect names an _index_ instead — MySQL's `Duplicate entry … for key
1109
+ 'idx_email_unique'`, Postgres' `violates unique constraint "sys_user_email_key"`,
1110
+ SQLite's `UNIQUE constraint failed: index 'x'` — the answer is `undefined`,
1111
+ never the index name. Callers render this into a form field, and an index name
1112
+ mistaken for a column points the user at a field that does not exist, whereas
1113
+ `undefined` degrades to generic copy. A **composite** key (`Key (tenant_id,
1114
+ email)=(…)`) is `undefined` for the same reason: there is no single offending
1115
+ column, and naming the first is the same class of wrong answer.
1116
+
1117
+ **⚠️ User-visible change on MySQL imports.** MySQL's duplicate-entry message
1118
+ names the index and never the column, so the importer no longer names a column
1119
+ there: rows that used to read _"A record with this `idx_email_unique` already
1120
+ exists."_ — or, on MySQL 8's table-qualified `for key 'sys_user.email'`, a
1121
+ plausible-looking _`email`_ that was still an index name — now read **"A record
1122
+ with this value already exists."** That is deliberate and is the accepted cost
1123
+ of the ruling. The conflict is still recognised as a conflict; only the naming
1124
+ narrowed.
1125
+
1126
+ Three smaller import messages improve in the same move, all previously wrong
1127
+ rather than merely vague:
1128
+
1129
+ - SQLite's expression/partial-index form used to render as _"A record with this
1130
+ **index** already exists."_
1131
+ - Postgres' expression index used to render the truncated fragment _"A record
1132
+ with this **lower(email** already exists."_
1133
+ - A Postgres conflict with no `DETAIL:` line used to fall through to the SQL
1134
+ backstop and echo the driver's own sentence — index name included — at the
1135
+ importer. It now gets the same generic conflict copy, which is also the exact
1136
+ wording `mapDataError` puts in the 409 `UNIQUE_VIOLATION` body, so the
1137
+ importer and the API say one thing about one condition.
1138
+
1139
+ Not changed: the NOT NULL branch, the raw-SQL backstop, and every non-conflict
1140
+ message, which pass through exactly as before.
1141
+
1142
+ ### Patch Changes
1143
+
1144
+ - a8940e4: The ADR-0114 D3 mapper (Zod issue codes → the closed `FieldErrorCode` catalog) is now
1145
+ `zodIssuesToFields`, exported from `@objectstack/spec` (`@objectstack/spec/api`), and it is
1146
+ the ONE implementation of D3's table in the repo (#8124).
1147
+
1148
+ Why: `fields[].code` is declared as a closed catalog (`FieldErrorCode`, ADR-0114 D2), but
1149
+ `@objectstack/types`' `fieldsFromZodIssues` — the helper the runtime `/analytics`,
1150
+ `/notifications` and `/automation` entry refusals emit through — passed Zod's own issue
1151
+ codes through verbatim. A refusal carrying `unrecognized_keys` / `too_small` did not parse
1152
+ against the schema the protocol declares for it, and the same wire slot spoke two
1153
+ vocabularies depending on which route served it.
1154
+
1155
+ What changed on the wire (all three runtime domain routes):
1156
+
1157
+ - `fields[].code` values are now catalog members: `unrecognized_keys` → `unknown_field`,
1158
+ `too_small` → `min_length`/`min_value`/`min_items` (by origin), `too_big` → the `max_*`
1159
+ mirrors, enum misses → `invalid_option`, `custom` and any unmapped Zod code →
1160
+ `invalid_value`.
1161
+ - A rejection behind a `z.union` is expanded per #5014: the union's own entry is followed
1162
+ by the branch entries that explain it, so entry count is no longer issue count.
1163
+ - Two hand-spelled `unrecognized_keys` literals (the analytics `filters` hint and the
1164
+ automation toggle unknown-key refusal) now say `unknown_field`, the catalog member.
1165
+
1166
+ `@objectstack/rest` re-exports the shared implementation from `rest-server.ts` and its
1167
+ behavior is unchanged (its own mapper tests pin that); `fieldsFromZodIssues` keeps its
1168
+ signature (plus an optional trailing `input` that upgrades a missing required property from
1169
+ `invalid_type` to `required`, per the D3 table) and keeps the `'(body)'` spelling for
1170
+ root-level failures.
1171
+
1172
+ - 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
1173
+
1174
+ The AGENTS.md post-task checklist requires breaking changesets to carry their
1175
+ FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
1176
+ inside the npm package and is what an upgrading agent greps after the tombstone
1177
+ error." That delivery path was severed for 68 of the 69 publishable packages:
1178
+ npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
1179
+ older npm versions — not `CHANGELOG.md`, and the canonical
1180
+ `"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
1181
+ 10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
1182
+ 70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
1183
+ explicitly.
1184
+
1185
+ The tombstone-error scenario is precisely the one where the repo is out of
1186
+ reach — the upgrading agent has `node_modules` and nothing else — so the
1187
+ migration text has to ride in the tarball. Every publishable package now
1188
+ declares `CHANGELOG.md` in `files`, and the canonical whitelist is
1189
+ `["dist", "README.md", "CHANGELOG.md"]`.
1190
+
1191
+ The other half is the gate: `check:published-files` gains a fifth invariant,
1192
+ COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
1193
+ always-required lint job, so the next package cannot silently sever the path
1194
+ again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
1195
+ into the canonical set.
1196
+
1197
+ Consumer-visible change: one more file per install (the package's changelog,
1198
+ e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
1199
+ node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
1200
+ promised.
1201
+
1202
+ - 87aca93: fix(datasource)!: a declared datasource that objects bind to must connect, or the boot fails (#3758)
1203
+
1204
+ `DatasourceConnectionService.handleFailure()` fail-fasted only for an `external`
1205
+ datasource with `validation.onMismatch: 'fail'`. Everything else degraded to one
1206
+ `warn` line — including the case the D2 auto-connect gate itself flags as having
1207
+ **no fallback path**: a datasource that objects bind to explicitly via
1208
+ `object.datasource`. Those objects never fall through to the `default` driver;
1209
+ `engine.getDriver` throws `Datasource 'x' is not registered` for them.
1210
+
1211
+ So an app declaring `datasource: 'analytics'` with 20 objects bound to it, booted
1212
+ against a wrong `ANALYTICS_URL`, started clean and exited zero — and then failed
1213
+ every read and write of those 20 objects with an error that reads nothing like
1214
+ _the analytics database is unreachable_. The rest of the app worked, which made it
1215
+ **harder** to locate than a total outage: it looks like "some pages are broken",
1216
+ not like a misconfigured datasource. This is the same decision #3741/#3751 fixed
1217
+ one layer up in `ObjectQLEngine.init()`; the boundary here was still drawn in the
1218
+ old place.
1219
+
1220
+ - **Fail-fast is now keyed on "no fallback path", not on `onMismatch` alone.** At
1221
+ the `declared-auto` (boot) trigger, a connect failure aborts the boot when the
1222
+ datasource is `external` + `onMismatch: 'fail'` **or** when ≥1 object binds to
1223
+ it explicitly. `autoConnect: true` with nothing bound stays lenient — that is
1224
+ "connect it if you can", and nothing declares a dependency on it. The
1225
+ runtime-admin create/update and boot-rehydration triggers are unchanged and
1226
+ still always degrade: a UI action must never brick a running server.
1227
+ - **Every failure mode counts**, not just an unreachable socket: an unresolvable
1228
+ `external.credentialsRef` (D3) and an unsupported `driver` leave the bound
1229
+ objects exactly as dead, so they take the same verdict.
1230
+ - **The error names the bound objects** (up to 10, then `+N more`) alongside the
1231
+ underlying cause, so the message points at the real problem instead of just the
1232
+ datasource name. The service already receives the list for post-connect
1233
+ `syncObjectSchema`.
1234
+ - **`connectDeclared()` attempts every gated datasource before throwing**, and
1235
+ aggregates, so one failed boot reports all the misconfigured ones rather than
1236
+ one per restart — the same shape as `ObjectQLEngine.init()`'s
1237
+ `DriverConnectError`.
1238
+ - **The escape hatch is shared with the engine guard**:
1239
+ `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` now also covers this path (and covers
1240
+ `onMismatch: 'fail'`, which previously had no opt-out). The operator intent is
1241
+ identical — "I know the database is unreachable, boot anyway" — and two flags
1242
+ would only guarantee one of them gets missed. When set, boot continues and a
1243
+ `DEGRADED BOOT` banner goes to stderr as well as the logger, because `os serve`
1244
+ swallows stdout during boot. `emitDegradedBootBanner` moved to
1245
+ `@objectstack/types` so both call sites share one implementation;
1246
+ `@objectstack/objectql` re-exports it unchanged.
1247
+
1248
+ ADR-0062 D5 is amended with the new criterion and the shared flag.
1249
+
1250
+ **Migration.** No change for a correctly configured deployment — a datasource that
1251
+ connected before still connects. A deployment that was _silently_ booting with a
1252
+ dead, explicitly-bound datasource now fails the boot instead, naming the
1253
+ datasource, the cause, and the objects that depend on it; fix the datasource
1254
+ configuration. To keep booting without it — deliberately, knowing every request
1255
+ touching those objects will fail — set `OS_ALLOW_DRIVER_CONNECT_FAILURE=1`.
1256
+
1257
+ - c20b875: **Correct the stale premise left behind by #4012: the degraded-boot stderr copy
1258
+ survives the operator's LOG LEVEL, not `os serve`'s boot-quiet window.**
1259
+
1260
+ `emitDegradedBootBanner` writes the `OS_ALLOW_DRIVER_CONNECT_FAILURE` banner to
1261
+ stderr in addition to `logger.warn`, and every comment and test name explaining
1262
+ why cited the same reason: `os serve` swallowed all of stdout while the kernel
1263
+ booted, and `Logger` routes `warn` to stdout. #4012 fixed that — the boot window
1264
+ now buffers and replays `warn`-and-above — which retires the _stated_
1265
+ justification for a duplicate that is nonetheless still load-bearing:
1266
+
1267
+ `Logger.write()` returns before touching a stream when the record is below
1268
+ `config.level`, so at `--log-level error`, `fatal` or `silent` the banner's
1269
+ `logger.warn` reaches **no** stream at all. A production host at `error` is
1270
+ exactly the deployment this escape hatch exists for, and exactly where a
1271
+ logger-only banner would vanish. Removing the stderr copy on the strength of
1272
+ #4012 would therefore have been a regression — so this documents the reason that
1273
+ is still true, in the places someone would read before deleting it:
1274
+ `degraded-boot.ts`, the engine's emit site, and all three parity tests
1275
+ (objectql, runtime, service-datasource), which are renamed off "which `os serve`
1276
+ boot-quiet cannot swallow" to "which the operator log level cannot filter away".
1277
+
1278
+ The objectql parity test now proves the claim instead of asserting around it: it
1279
+ drives a **real** `ObjectLogger` at `level: 'error'` and requires the banner on
1280
+ stderr _and_ nothing on stdout. Set the level to `warn` and it fails — so the
1281
+ test is pinned to the level filter rather than passing for any reason.
1282
+
1283
+ Also corrected in the same sweep, all comment-only, all previously overstating
1284
+ what #4012 had not yet fixed:
1285
+
1286
+ - the automation wiring summary (`format.ts`, `serve.ts`, its test) claimed the
1287
+ boot window swallowed the engine's binding warnings. Its real justification is
1288
+ stronger and unchanged: a flow that silently fails to arm emits **no** log line
1289
+ at any level, so binding state has to be read off the live engine — absence of
1290
+ a warning was never evidence of a bound flow.
1291
+ - the seed summary (`seed-summary.ts`, `format.ts`, its test) and `AppPlugin`'s
1292
+ seed-outcome note attributed the silence to the boot window; the operative
1293
+ gate is that `SeedLoader`'s result logs are `info`, under the default `warn`.
1294
+
1295
+ No behavior changes.
1296
+
1297
+ - 32d3800: fix(driver-sql): bound a connection attempt at 10s, and correct the "no reconnection" claim (#3769, #3759)
1298
+
1299
+ Two related corrections, both from measuring what #3741/#3751/#3765 had only asserted.
1300
+
1301
+ **The claim was wrong.** #3751 and #3765 shipped several statements that drivers
1302
+ never reconnect — "there is no lazy reconnection", "NOT retried and NOT
1303
+ reconnected", "stays disconnected for the process lifetime". Measured, both
1304
+ drivers recover on their own:
1305
+
1306
+ - driver-mongodb: killing a real `mongod` and restarting it on the same port,
1307
+ the _same_ driver instance served the next write successfully (13ms), with no
1308
+ reconnect call from us — the official driver's topology monitor handles it.
1309
+ - driver-sql: a knex/pg pool is not poisoned by an outage. Its error tracks live
1310
+ server state (`ECONNREFUSED` while down → a handshake error once a listener is
1311
+ back → `ECONNREFUSED` again), i.e. every acquire opens a fresh connection.
1312
+ `storage-driver.ts` also configures `pool.min: 0`, so no stale idle
1313
+ connections are held.
1314
+
1315
+ The original reasoning grepped this repo for `reconnect`, found nothing, and
1316
+ concluded recovery does not happen — but the recovery lives in the client
1317
+ libraries, not in our code. The claims are now corrected in `DriverConnectError`,
1318
+ the `DEGRADED BOOT` banner, `resolveAllowDriverConnectFailure`'s docs, and the
1319
+ drivers / self-hosting pages.
1320
+
1321
+ **Fail-fast at boot is unchanged and still correct** — the reason is just
1322
+ different. It is not that the connection can never return; it is that the _boot
1323
+ sequence_ never re-runs. A driver that missed `init()` also missed
1324
+ `syncRegisteredSchemas()`, so its tables can simply not exist even after the
1325
+ database comes back. The banner now says that.
1326
+
1327
+ **The real defect underneath.** `SqlDriver` passed its config to knex untouched,
1328
+ so a database endpoint that accepts TCP but never completes the handshake — an
1329
+ overloaded instance, a half-open firewall, a load balancer mid-failover — made
1330
+ every query wait out tarn's 30s default, then fail with `Timeout acquiring a
1331
+ connection. The pool is probably full`, pointing an operator at pool sizing
1332
+ instead of the network. With a small `pool.max` a few such queries saturate the
1333
+ pool and everything else queues.
1334
+
1335
+ `SqlDriver` now defaults `pool.createTimeoutMillis` to **10s**, matching
1336
+ driver-mongodb's existing `connectTimeoutMS ?? 10_000` so both drivers give up on
1337
+ an unreachable server at the same point. A host that sets its own
1338
+ `createTimeoutMillis` is left alone.
1339
+
1340
+ **Migration.** None for a healthy datasource. A deployment that deliberately
1341
+ relies on connection establishment taking longer than 10s (a slow cross-region
1342
+ replica) should set `pool.createTimeoutMillis` explicitly on its `SqlDriver`
1343
+ config.
1344
+
1345
+ Not fixed here, tracked in #3769: knex still reports the bounded wait as "the
1346
+ pool is probably full". An accurate message needs a dialect-specific connect
1347
+ timeout (pg's `connectionTimeoutMillis`), which changes the shape of `connection`
1348
+ and would regress the startup banner's URL display.
1349
+
1350
+ - f598aa8: Withhold the Postgres and bare-SQLite phrasings of a driver failure from HTTP error bodies
1351
+
1352
+ `looksLikeInternalErrorLeak` recognised SQLite's `SQLITE_ERROR: no such table: sys_metadata`
1353
+ but not the Postgres phrasing of the same condition, `relation "sys_metadata" does not exist`.
1354
+ The result was that one failure disclosed a physical table name or not depending on which
1355
+ engine was underneath, from every boundary that applies the predicate — `HttpDispatcher.error`,
1356
+ the declarative endpoint executor, the dispatcher plugin, the direct-mount package door and the
1357
+ Hono auth-config route.
1358
+
1359
+ The predicate now also recognises, for the engines this repo actually runs:
1360
+
1361
+ - Postgres `relation "…" does not exist` and `column "…" does not exist` (42P01/42703), which
1362
+ covers the `… of relation "…"` sub-object family as a superstring;
1363
+ - Postgres `permission denied for table|relation|sequence|database …` (42501);
1364
+ - SQLite/libsql `no such table:` / `no such column:` in their bare, un-prefixed form.
1365
+
1366
+ Each phrasing is anchored on the driver's own template — a quoted identifier, or the trailing
1367
+ colon — never on the bare tail, so ordinary business messages such as "user does not exist" are
1368
+ still returned to the caller unchanged. The predicate is applied only where the outcome is
1369
+ already a 5xx, and the full text still reaches the server log and the error reporter.
1370
+
1371
+ - 08f93bc: fix(auth): `organization/create` gates on the authoritative `OS_TENANCY_POSTURE`, not the demoted `OS_MULTI_ORG_ENABLED` (#5233)
1372
+
1373
+ A deployment configured the documented way — `OS_TENANCY_POSTURE=isolated` (or
1374
+ `group`), legacy boolean unset — mounted the entire organization wall and still
1375
+ answered `403 Creating additional organizations is disabled on this deployment.`
1376
+ to `POST /api/v1/auth/organization/create`. Org-less users had no way to create
1377
+ their workspace, so the guided "Create your workspace" path was a dead end.
1378
+
1379
+ ADR-0105 D1 made `OS_TENANCY_POSTURE` the canonical knob and demoted
1380
+ `OS_MULTI_ORG_ENABLED` to a back-compat _input_ of `resolveTenancyPosture()`.
1381
+ Two sites in `AuthManager` kept reading the demoted boolean directly, so both
1382
+ reported "single-org" on a deployment that had asked for a wall and got one:
1383
+
1384
+ - `organizationHooks.beforeCreateOrganization` — the 403 above. It now judges
1385
+ `postureEnforcesWall(resolveTenancyPosture())`, matching the knob `serve.ts`'s
1386
+ own ADR-0093 D5 boot guard keys on. Intent is unchanged (single-org still
1387
+ refuses); only the knob is corrected.
1388
+ - `/auth/config`'s `features.multiOrgEnabled` — its no-tenancy-service fallback
1389
+ read the same boolean. It now falls back to the resolved posture, so a lean
1390
+ embedding advertises the capability its own gate allows.
1391
+
1392
+ **No configuration change is needed anywhere.** Deployments that set only
1393
+ `OS_MULTI_ORG_ENABLED=true` keep working unchanged — `resolveTenancyPosture()`
1394
+ falls back to it — and the `OS_TENANCY_POSTURE=isolated` + `OS_MULTI_ORG_ENABLED=true`
1395
+ workaround people used to unblock themselves stays valid. Deployments that set
1396
+ only `OS_TENANCY_POSTURE` can now drop the redundant boolean.
1397
+
1398
+ `resolveMultiOrgEnabled()`'s doc comment in `@objectstack/types` — which still
1399
+ instructed "the auth manager's `/auth/config` feature flag and org-create guard
1400
+ … MUST call this", written before the demotion — now says the opposite: ask the
1401
+ posture, and never gate on this boolean. Its behaviour is unchanged.
1402
+
1403
+ - 9881074: fix(batch): the background walks seek instead of counting, so they stop skipping rows (#4363)
1404
+
1405
+ #4363 made a single paged read a partition of its result set. It could not make
1406
+ a _walk_ one: seven background scans paged with a growing `offset` while writing
1407
+ to the very rows they were reading, and an offset counts into a set those writes
1408
+ are changing. Rows slide past the cursor and are never visited.
1409
+
1410
+ That is not a slow page in any of these — it is a wrong answer wearing the shape
1411
+ of a clean run:
1412
+
1413
+ - **`rebuildApproverIndex`** built its desired state by walking
1414
+ `sys_approval_request WHERE status = 'pending'` with no `orderBy` at all, then
1415
+ **deleted** every index row that state did not explain. A skipped request
1416
+ meant an approver silently dropped from someone's queue. (The loop beside it
1417
+ ordered by `created_at` — not unique, so its pages were never a partition
1418
+ either.)
1419
+ - **`verifyFileReferences`** decides which files nothing references. A record it
1420
+ never visits is reported as an unreferenced file.
1421
+ - **`backfillFileReferences`** and the **pinyin companion backfill** rewrite
1422
+ each row they read, so their own writes were shifting the set out from under
1423
+ the cursor. Records were left unconverted and unsearchable by a run that
1424
+ reported success.
1425
+ - **`scanValueShapes`** exists to vouch that no stored value is off-shape, and
1426
+ it opens a migration gate on that evidence.
1427
+
1428
+ All of them now go through `keysetWalk` (`@objectstack/types`): order by a
1429
+ unique key, and seek past the last one instead of counting from the start. A
1430
+ row's key does not move when the row is updated, and cannot be shifted when
1431
+ another is deleted, so the walk is stable under exactly the mutation these
1432
+ functions perform. It is also O(n) rather than O(n²/page) — measured on
1433
+ Postgres over 2M rows, deep pages cost ~1.1 s by offset against ~0.09 s by seek.
1434
+
1435
+ One deliberate non-conversion: the REST **export** stream keeps its offset. It
1436
+ honors a caller-chosen sort, and a keyset walk would have to re-order the export
1437
+ by `id` to seek — changing what the user asked for to fix a cost. Its pages are
1438
+ already a partition since #4363; only the depth cost remains.
1439
+
1440
+ `keysetWalk` merges the cursor with `$and` rather than spreading it into the
1441
+ caller's filter, so a walk whose own `where` constrains the key column
1442
+ (`{ id: { $in: [...] } }`) keeps that constraint instead of having it silently
1443
+ overwritten. When a `max` cap is set it reads one row beyond the cap to tell
1444
+ "the cap stopped us" from "the source ended exactly there" — without that, a
1445
+ walk that read everything still reports `truncated`, and a caller acting on it
1446
+ goes looking for rows that were never withheld.
1447
+
1448
+ The storage suites' fake engines now **throw** on an `offset` instead of serving
1449
+ one, so the conversion is pinned rather than merely passing.
1450
+
1451
+ - 39eb01b: fix(runtime,cli,types): `os migrate` and the dev runtime now share one `__search` companion schema view (#3955)
1452
+
1453
+ On a zh-locale deployment the dev runtime provisions the hidden `__search`
1454
+ pinyin companion column (ADR-0098) on every eligible object, but the
1455
+ `os migrate plan`/`apply` boot went through `createStandaloneStack`, which
1456
+ never derived the locale-gated pinyin decision from the compiled artifact.
1457
+ Its metadata therefore lacked every companion column, and `migrate plan`
1458
+ reported each live `__search` column of a dev-created database as a
1459
+ destructive orphan — with `--allow-destructive` as the printed remediation,
1460
+ which would have dropped live feature columns.
1461
+
1462
+ - `@objectstack/types`: new `collectConfiguredLocales(i18n)` and
1463
+ `stampSearchPinyinEnabled(i18n)` — the single resolve-and-stamp helper for
1464
+ `OS_SEARCH_PINYIN_ENABLED`. An explicit env value still wins; only a
1465
+ positive locale-derived decision is stamped.
1466
+ - `@objectstack/runtime`: `createStandaloneStack` stamps the decision from
1467
+ the artifact's `i18n` before any plugin constructs a `SchemaRegistry`, and
1468
+ surfaces `i18n` on its result like `requires`/`objects`/`manifest`.
1469
+ - `@objectstack/cli`: the `serve`/`dev` boot now stamps through the same
1470
+ shared helper (behaviour unchanged), so create/serve and plan/apply cannot
1471
+ compute different schema views of the same source tree.
1472
+
1473
+ A fresh CLI-created database is now also born with the same `__search`
1474
+ columns the dev runtime would provision, instead of acquiring them on the
1475
+ next dev boot.
1476
+
1477
+ - 694c350: fix(drivers): a `conflictKeys` upsert with no backing unique index refuses legibly on **Postgres** too, not only SQLite (#8567)
1478
+
1479
+ `SqlDriver.upsert` recognised "the `ON CONFLICT` target is not backed by a
1480
+ PRIMARY KEY or UNIQUE index" on **SQLite only** (#8445). `driver-sql` serves
1481
+ three dialects, so on Postgres the raw driver error still escaped: `mapDataError`
1482
+ fell through to its default branch and served the thrown message as the whole
1483
+ response body — and that message is the **statement**, with no `code` for any
1484
+ client to branch on.
1485
+
1486
+ **What a Postgres caller got, and now gets.** Measured against a real
1487
+ PostgreSQL 16.13 through the same knex + `pg` path the driver uses:
1488
+
1489
+ ```
1490
+ before: code=42P10 status=undefined
1491
+ message=insert into "plain" ("email", "id", "title") values ($1, $2, $3)
1492
+ on conflict ("email") do update set "title" = excluded."title"
1493
+ - there is no unique or exclusion constraint matching the ON CONFLICT specification
1494
+
1495
+ after: code=VALIDATION_ERROR status=400
1496
+ message=Cannot upsert into "plain" on conflict keys ("email"): no PRIMARY KEY or
1497
+ UNIQUE index backs them, … Fix by declaring the column(s) "unique: true" …
1498
+ ```
1499
+
1500
+ The accept/reject set does not move: the same upserts fail, they fail legibly.
1501
+ The server's own sentence is preserved on the error's `cause`, which no error
1502
+ mapper puts on the wire, so an operator debugging the table keeps the ground
1503
+ truth while the caller stops receiving SQL text.
1504
+
1505
+ **One clause of the refusal wording changed, on both faces.** "…and SQLite
1506
+ refuses the statement" is now "…and **the database** refuses the statement".
1507
+ Once recognition covers Postgres, naming SQLite points a Postgres operator at
1508
+ the wrong engine. `driver-turso`'s remote-face copy moved in the same commit —
1509
+ the two are held word-for-word identical (#5240) by #8568's cross-face parity
1510
+ pin. No other sentence of the refusal changed.
1511
+
1512
+ **Recognition is now a named, shared predicate.**
1513
+ `isUnbackedConflictTargetError` is exported from `@objectstack/types` beside
1514
+ `isUniqueViolationError`, carrying one measured message limb per dialect that
1515
+ can raise the condition. ⚠️ It is deliberately a **separate** predicate:
1516
+ `isUniqueViolationError` answers the opposite condition (an index exists and the
1517
+ row violated it), and a merged one would report a working constraint as a
1518
+ missing one.
1519
+
1520
+ **MySQL is unaffected, by measurement rather than omission.** knex compiles
1521
+ `onConflict(...).merge(...)` on that dialect to `ON DUPLICATE KEY UPDATE`, which
1522
+ takes no conflict target — the named keys never leave the process, so the server
1523
+ is never asked to find an index for them and the condition cannot arise. The
1524
+ compiled statement is pinned; the live MySQL cell is reported as un-run rather
1525
+ than passing vacuously.
1526
+
1527
+ - 88f9d94: fix(types,rest): one named unique-violation predicate — a MySQL conflict is 409 UNIQUE_VIOLATION, not 500 (#6250)
1528
+
1529
+ **On MySQL, every unique-constraint conflict came back as `500 INTERNAL_ERROR`.**
1530
+ The API contract registers `UNIQUE_VIOLATION` as a 409 code
1531
+ (`packages/spec/src/api/error-code-ledger.zod.ts`), so a front end had no way to
1532
+ tell "this email is already taken" from "the server fell over" — no retry advice,
1533
+ no field to point at, and a 5xx in the operator's dashboards for what is an
1534
+ ordinary client outcome. SQLite and Postgres deployments never saw it, which is
1535
+ why it survived: their conflict prose happens to contain the words the mapping
1536
+ looked for.
1537
+
1538
+ **Cause: the conflict verdict was nested inside a leak heuristic.** REST's 409
1539
+ branch lived inside the true-branch of `looksLikeInternalErrorLeak()`, keyed on
1540
+ the substrings `unique constraint` / `unique violation`. MySQL says
1541
+ `ER_DUP_ENTRY: Duplicate entry '…' for key '…'`, which matches no limb of that
1542
+ heuristic, so the conflict never reached the `if` at all and fell out of the
1543
+ terminal `UNCLASSIFIED_FAULT`. Two unrelated questions — "is this a conflict?"
1544
+ and "would echoing this text leak internals?" — had been fused into one, and
1545
+ MySQL is where they disagree.
1546
+
1547
+ Measured on the previous release, through the real error mapper:
1548
+
1549
+ ```
1550
+ mysql, bare message 500 INTERNAL_ERROR → 409 UNIQUE_VIOLATION
1551
+ mysql, knex-wrapped SQL 500 DATABASE_ERROR → 409 UNIQUE_VIOLATION
1552
+ postgres, SQLSTATE only 500 INTERNAL_ERROR → 409 UNIQUE_VIOLATION
1553
+ sqlite, message 409 UNIQUE_VIOLATION (unchanged)
1554
+ postgres, message 409 UNIQUE_VIOLATION (unchanged)
1555
+ ```
1556
+
1557
+ So the hole was never MySQL-only: the mapping read one of the two channels
1558
+ drivers use. A Postgres error carrying SQLSTATE `23505` with unremarkable prose
1559
+ was a 500 as well.
1560
+
1561
+ **New: `isUniqueViolationError(error)`, exported from `@objectstack/types`.** One
1562
+ named predicate replaces the substring test, reading every channel a driver
1563
+ uses — `code` (`23505` / `ER_DUP_ENTRY` / `SQLITE_CONSTRAINT_UNIQUE`), `errno`
1564
+ (`1062`), the message, and one step down the `cause` chain that pool and
1565
+ query-builder layers wrap with. Its vocabulary is the union of the four
1566
+ hand-written copies the repo already carried, so routing REST through it cannot
1567
+ narrow any verdict clients rely on today; an unrecognised error is never a
1568
+ conflict, because a false 409 tells an SDK not to retry and points the user at a
1569
+ value that is fine.
1570
+
1571
+ **The internal-leak classifier is byte-identical.** The fix hoists the conflict
1572
+ question out of it rather than widening its criteria, so nothing else it guards
1573
+ is reclassified as safe-to-expose. And the 409 body is fixed text: MySQL embeds
1574
+ the offending user data in its message (`Duplicate entry 'a@b.com' …`) and
1575
+ Postgres the index and column names, none of which reaches the client. The full
1576
+ driver text still reaches the server log.
1577
+
1578
+ No action needed. Clients that already handled `409 UNIQUE_VIOLATION` on SQLite
1579
+ and Postgres now receive it on MySQL too.
1580
+
1581
+ - Updated dependencies [50616d9]
1582
+ - Updated dependencies [430dcc2]
1583
+ - Updated dependencies [6a67d7a]
1584
+ - Updated dependencies [333a374]
1585
+ - Updated dependencies [9fe9c1d]
1586
+ - Updated dependencies [3d5c090]
1587
+ - Updated dependencies [e5bd768]
1588
+ - Updated dependencies [08b5a3d]
1589
+ - Updated dependencies [e027b3e]
1590
+ - Updated dependencies [e6ac4bd]
1591
+ - Updated dependencies [c2429b0]
1592
+ - Updated dependencies [445a0c2]
1593
+ - Updated dependencies [d99aeb3]
1594
+ - Updated dependencies [f6609e6]
1595
+ - Updated dependencies [4727eb8]
1596
+ - Updated dependencies [a70358a]
1597
+ - Updated dependencies [0ecc656]
1598
+ - Updated dependencies [06772eb]
1599
+ - Updated dependencies [d4e0809]
1600
+ - Updated dependencies [80334c7]
1601
+ - Updated dependencies [f63cd09]
1602
+ - Updated dependencies [97e7e3c]
1603
+ - Updated dependencies [ce5242c]
1604
+ - Updated dependencies [a7163ea]
1605
+ - Updated dependencies [e6e9379]
1606
+ - Updated dependencies [5823d59]
1607
+ - Updated dependencies [3140f9c]
1608
+ - Updated dependencies [9500ba4]
1609
+ - Updated dependencies [fa3d0cf]
1610
+ - Updated dependencies [af5a224]
1611
+ - Updated dependencies [71f76e1]
1612
+ - Updated dependencies [37b1346]
1613
+ - Updated dependencies [99736a0]
1614
+ - Updated dependencies [fe67e34]
1615
+ - Updated dependencies [fdb4f50]
1616
+ - Updated dependencies [270650f]
1617
+ - Updated dependencies [3aef718]
1618
+ - Updated dependencies [1bd5652]
1619
+ - Updated dependencies [14252d3]
1620
+ - Updated dependencies [7fb436c]
1621
+ - Updated dependencies [879ea13]
1622
+ - Updated dependencies [8828b9e]
1623
+ - Updated dependencies [1ea6bce]
1624
+ - Updated dependencies [c1dcacd]
1625
+ - Updated dependencies [ad303ed]
1626
+ - Updated dependencies [32ccb23]
1627
+ - Updated dependencies [f5a4ef0]
1628
+ - Updated dependencies [2d3e255]
1629
+ - Updated dependencies [a8940e4]
1630
+ - Updated dependencies [7d7521f]
1631
+ - Updated dependencies [5dc4d02]
1632
+ - Updated dependencies [f724f69]
1633
+ - Updated dependencies [98877c9]
1634
+ - Updated dependencies [98877c9]
1635
+ - Updated dependencies [53068c1]
1636
+ - Updated dependencies [ee58392]
1637
+ - Updated dependencies [f16e54e]
1638
+ - Updated dependencies [06be54e]
1639
+ - Updated dependencies [28ad90e]
1640
+ - Updated dependencies [76d74ec]
1641
+ - Updated dependencies [201b31f]
1642
+ - Updated dependencies [e6b1b69]
1643
+ - Updated dependencies [259459d]
1644
+ - Updated dependencies [3f7f14e]
1645
+ - Updated dependencies [e2616e0]
1646
+ - Updated dependencies [6fdc5c6]
1647
+ - Updated dependencies [8b9d71e]
1648
+ - Updated dependencies [05154a1]
1649
+ - Updated dependencies [33f5e23]
1650
+ - Updated dependencies [259af21]
1651
+ - Updated dependencies [f8644c7]
1652
+ - Updated dependencies [306ca50]
1653
+ - Updated dependencies [978fed2]
1654
+ - Updated dependencies [cfc293f]
1655
+ - Updated dependencies [587fc91]
1656
+ - Updated dependencies [de70b42]
1657
+ - Updated dependencies [9b6fe7c]
1658
+ - Updated dependencies [fb3d99b]
1659
+ - Updated dependencies [1986594]
1660
+ - Updated dependencies [6968885]
1661
+ - Updated dependencies [eaed61f]
1662
+ - Updated dependencies [cdfbee2]
1663
+ - Updated dependencies [ad4af62]
1664
+ - Updated dependencies [debe2f6]
1665
+ - Updated dependencies [d44dbfa]
1666
+ - Updated dependencies [29c6c9d]
1667
+ - Updated dependencies [d21c001]
1668
+ - Updated dependencies [ad047d2]
1669
+ - Updated dependencies [8c711fb]
1670
+ - Updated dependencies [f1cc3a3]
1671
+ - Updated dependencies [09e4547]
1672
+ - Updated dependencies [97b0798]
1673
+ - Updated dependencies [474fe39]
1674
+ - Updated dependencies [0bc685a]
1675
+ - Updated dependencies [b949059]
1676
+ - Updated dependencies [2826d1e]
1677
+ - Updated dependencies [be1c52c]
1678
+ - Updated dependencies [c5ff96d]
1679
+ - Updated dependencies [5a84d41]
1680
+ - Updated dependencies [84e7be9]
1681
+ - Updated dependencies [91f4c78]
1682
+ - Updated dependencies [ddc2527]
1683
+ - Updated dependencies [820eff9]
1684
+ - Updated dependencies [a6c3f38]
1685
+ - Updated dependencies [debc23a]
1686
+ - Updated dependencies [0f8ad09]
1687
+ - Updated dependencies [553a47f]
1688
+ - Updated dependencies [43a7a8d]
1689
+ - Updated dependencies [a98085f]
1690
+ - Updated dependencies [20b1a9e]
1691
+ - Updated dependencies [344a22a]
1692
+ - Updated dependencies [4827e91]
1693
+ - Updated dependencies [8d895ff]
1694
+ - Updated dependencies [86f7a20]
1695
+ - Updated dependencies [a3a884d]
1696
+ - Updated dependencies [cfed092]
1697
+ - Updated dependencies [203a449]
1698
+ - Updated dependencies [8f9689f]
1699
+ - Updated dependencies [73f69dc]
1700
+ - Updated dependencies [04c56aa]
1701
+ - Updated dependencies [f6472d7]
1702
+ - Updated dependencies [57a3bb3]
1703
+ - Updated dependencies [b3efeb7]
1704
+ - Updated dependencies [ddd075a]
1705
+ - Updated dependencies [88154be]
1706
+ - Updated dependencies [e8dc61e]
1707
+ - Updated dependencies [9c82146]
1708
+ - Updated dependencies [5f9a987]
1709
+ - Updated dependencies [744b8f5]
1710
+ - Updated dependencies [ac37fc6]
1711
+ - Updated dependencies [2f3e793]
1712
+ - Updated dependencies [4820f55]
1713
+ - Updated dependencies [462d9c4]
1714
+ - Updated dependencies [78caf51]
1715
+ - Updated dependencies [7d21581]
1716
+ - Updated dependencies [37785ed]
1717
+ - Updated dependencies [62a789b]
1718
+ - Updated dependencies [2e284b2]
1719
+ - Updated dependencies [d8e8d9c]
1720
+ - Updated dependencies [789ad63]
1721
+ - Updated dependencies [f2445c9]
1722
+ - Updated dependencies [94e749b]
1723
+ - Updated dependencies [ea1d916]
1724
+ - Updated dependencies [2af1988]
1725
+ - Updated dependencies [1b49eaf]
1726
+ - Updated dependencies [ae31a19]
1727
+ - Updated dependencies [e0f300b]
1728
+ - Updated dependencies [0161c7f]
1729
+ - Updated dependencies [e900015]
1730
+ - Updated dependencies [db02d47]
1731
+ - Updated dependencies [b5bdf48]
1732
+ - Updated dependencies [23338c3]
1733
+ - Updated dependencies [12a19a8]
1734
+ - Updated dependencies [5b843fb]
1735
+ - Updated dependencies [62b6a2f]
1736
+ - Updated dependencies [7e5af5c]
1737
+ - Updated dependencies [5b4780b]
1738
+ - Updated dependencies [a933452]
1739
+ - Updated dependencies [9d1d9c7]
1740
+ - Updated dependencies [8140915]
1741
+ - Updated dependencies [a019e52]
1742
+ - Updated dependencies [e8f8f6c]
1743
+ - Updated dependencies [41dcda3]
1744
+ - Updated dependencies [7b48cf9]
1745
+ - Updated dependencies [b5404f4]
1746
+ - Updated dependencies [64fc6d5]
1747
+ - Updated dependencies [b4487aa]
1748
+ - Updated dependencies [1007379]
1749
+ - Updated dependencies [65ca83a]
1750
+ - Updated dependencies [0bfdf46]
1751
+ - Updated dependencies [947d4f9]
1752
+ - Updated dependencies [f764691]
1753
+ - Updated dependencies [e120a5a]
1754
+ - Updated dependencies [e5bd2f6]
1755
+ - Updated dependencies [e650d67]
1756
+ - Updated dependencies [04476e7]
1757
+ - Updated dependencies [67bf2e2]
1758
+ - Updated dependencies [eaaf03c]
1759
+ - Updated dependencies [d17df80]
1760
+ - Updated dependencies [7d0e7b5]
1761
+ - Updated dependencies [c6d1cb4]
1762
+ - Updated dependencies [6513c17]
1763
+ - Updated dependencies [36030ff]
1764
+ - Updated dependencies [79228cd]
1765
+ - Updated dependencies [6117f7b]
1766
+ - Updated dependencies [e533b0b]
1767
+ - Updated dependencies [cdf4d9a]
1768
+ - Updated dependencies [aee1806]
1769
+ - Updated dependencies [c13350b]
1770
+ - Updated dependencies [c13350b]
1771
+ - Updated dependencies [2c1988c]
1772
+ - Updated dependencies [9ca2d85]
1773
+ - Updated dependencies [c13350b]
1774
+ - Updated dependencies [891d345]
1775
+ - Updated dependencies [c8124e5]
1776
+ - Updated dependencies [a52e2ef]
1777
+ - Updated dependencies [5293114]
1778
+ - Updated dependencies [376a061]
1779
+ - Updated dependencies [c142ced]
1780
+ - Updated dependencies [211abdb]
1781
+ - Updated dependencies [b3363e9]
1782
+ - Updated dependencies [eda599e]
1783
+ - Updated dependencies [a1a4140]
1784
+ - Updated dependencies [7c7e246]
1785
+ - Updated dependencies [2ef1807]
1786
+ - Updated dependencies [f35cdc5]
1787
+ - Updated dependencies [d03fe25]
1788
+ - Updated dependencies [217e2e6]
1789
+ - Updated dependencies [2672f85]
1790
+ - Updated dependencies [20bc357]
1791
+ - Updated dependencies [11066f6]
1792
+ - Updated dependencies [916af17]
1793
+ - Updated dependencies [84c86fb]
1794
+ - Updated dependencies [2a2a9fb]
1795
+ - Updated dependencies [86a71d1]
1796
+ - Updated dependencies [c001422]
1797
+ - Updated dependencies [77022a9]
1798
+ - Updated dependencies [d5c75e2]
1799
+ - Updated dependencies [03d26f7]
1800
+ - Updated dependencies [5966c2a]
1801
+ - Updated dependencies [2382580]
1802
+ - Updated dependencies [9ea2bc5]
1803
+ - Updated dependencies [a2e157c]
1804
+ - Updated dependencies [95c4227]
1805
+ - Updated dependencies [2a61116]
1806
+ - Updated dependencies [52760bf]
1807
+ - Updated dependencies [5543020]
1808
+ - Updated dependencies [880d343]
1809
+ - Updated dependencies [6e82972]
1810
+ - Updated dependencies [d4df105]
1811
+ - Updated dependencies [4615a18]
1812
+ - Updated dependencies [f505689]
1813
+ - Updated dependencies [d9fa683]
1814
+ - Updated dependencies [606d577]
1815
+ - Updated dependencies [4384921]
1816
+ - Updated dependencies [e2798fa]
1817
+ - Updated dependencies [3c628ce]
1818
+ - Updated dependencies [c2d9098]
1819
+ - Updated dependencies [0fd8556]
1820
+ - Updated dependencies [3c7bcc0]
1821
+ - Updated dependencies [4b6cac7]
1822
+ - Updated dependencies [7631964]
1823
+ - Updated dependencies [ac471a0]
1824
+ - Updated dependencies [60ae58e]
1825
+ - Updated dependencies [7f62706]
1826
+ - Updated dependencies [667fa44]
1827
+ - Updated dependencies [37e38d1]
1828
+ - Updated dependencies [e906126]
1829
+ - Updated dependencies [ce92674]
1830
+ - Updated dependencies [08363a0]
1831
+ - Updated dependencies [444de5b]
1832
+ - Updated dependencies [a227ed7]
1833
+ - Updated dependencies [7cb922e]
1834
+ - Updated dependencies [1d22114]
1835
+ - Updated dependencies [1eb13a0]
1836
+ - Updated dependencies [c52e608]
1837
+ - Updated dependencies [9613396]
1838
+ - Updated dependencies [3f7b4ff]
1839
+ - Updated dependencies [74155c7]
1840
+ - Updated dependencies [b5f9397]
1841
+ - Updated dependencies [ed77493]
1842
+ - Updated dependencies [6908830]
1843
+ - Updated dependencies [8b06bba]
1844
+ - Updated dependencies [58a03d2]
1845
+ - Updated dependencies [2bacd1a]
1846
+ - Updated dependencies [e47b342]
1847
+ - Updated dependencies [4c54037]
1848
+ - Updated dependencies [dc530b4]
1849
+ - Updated dependencies [9f601e8]
1850
+ - Updated dependencies [6a9dec6]
1851
+ - Updated dependencies [0f7157b]
1852
+ - Updated dependencies [4dc1c7d]
1853
+ - Updated dependencies [d9bef45]
1854
+ - Updated dependencies [4dfd002]
1855
+ - Updated dependencies [f549a0d]
1856
+ - Updated dependencies [51c5227]
1857
+ - Updated dependencies [82da264]
1858
+ - Updated dependencies [77be690]
1859
+ - Updated dependencies [4ed7ed4]
1860
+ - Updated dependencies [9b9b70f]
1861
+ - Updated dependencies [f5a9bc2]
1862
+ - Updated dependencies [e59786e]
1863
+ - Updated dependencies [2fa4ca1]
1864
+ - Updated dependencies [bcf1112]
1865
+ - Updated dependencies [baeb4f0]
1866
+ - Updated dependencies [29488cc]
1867
+ - Updated dependencies [881a3cc]
1868
+ - Updated dependencies [f5a2320]
1869
+ - Updated dependencies [ad6317b]
1870
+ - Updated dependencies [811c30c]
1871
+ - Updated dependencies [a4a85c8]
1872
+ - Updated dependencies [859cb83]
1873
+ - Updated dependencies [07a4e26]
1874
+ - Updated dependencies [9774b78]
1875
+ - Updated dependencies [8a88885]
1876
+ - Updated dependencies [deb538f]
1877
+ - Updated dependencies [b49ccfd]
1878
+ - Updated dependencies [5b89711]
1879
+ - Updated dependencies [85d95e7]
1880
+ - Updated dependencies [08cd163]
1881
+ - Updated dependencies [0c8a22f]
1882
+ - Updated dependencies [5f7669e]
1883
+ - Updated dependencies [becbe53]
1884
+ - Updated dependencies [b127c8b]
1885
+ - Updated dependencies [763931e]
1886
+ - Updated dependencies [ec975f1]
1887
+ - Updated dependencies [168f60f]
1888
+ - Updated dependencies [b07d829]
1889
+ - Updated dependencies [de9af8a]
1890
+ - Updated dependencies [eb4204b]
1891
+ - Updated dependencies [a80302a]
1892
+ - Updated dependencies [a648e96]
1893
+ - Updated dependencies [a47ac06]
1894
+ - Updated dependencies [e4c61a7]
1895
+ - Updated dependencies [cc60165]
1896
+ - Updated dependencies [474f131]
1897
+ - Updated dependencies [081aa6f]
1898
+ - Updated dependencies [91f4c78]
1899
+ - Updated dependencies [050cd82]
1900
+ - Updated dependencies [4d552af]
1901
+ - Updated dependencies [44d677c]
1902
+ - Updated dependencies [c32944d]
1903
+ - Updated dependencies [1dd780f]
1904
+ - Updated dependencies [e8d0c21]
1905
+ - Updated dependencies [244ca86]
1906
+ - Updated dependencies [546ab3c]
1907
+ - Updated dependencies [c4df271]
1908
+ - Updated dependencies [c8d6f6e]
1909
+ - Updated dependencies [0b51bb6]
1910
+ - Updated dependencies [d9971d3]
1911
+ - Updated dependencies [7dc1067]
1912
+ - Updated dependencies [4f13be2]
1913
+ - Updated dependencies [a41ba5c]
1914
+ - Updated dependencies [189854c]
1915
+ - Updated dependencies [0e3a226]
1916
+ - Updated dependencies [92a67f2]
1917
+ - Updated dependencies [9136327]
1918
+ - Updated dependencies [bf0ae99]
1919
+ - Updated dependencies [abeb375]
1920
+ - Updated dependencies [cb3b6cd]
1921
+ - Updated dependencies [73b7234]
1922
+ - Updated dependencies [d2b97c3]
1923
+ - Updated dependencies [61cc079]
1924
+ - Updated dependencies [0e96e46]
1925
+ - Updated dependencies [c1d44f7]
1926
+ - Updated dependencies [59b794f]
1927
+ - Updated dependencies [ef4efa8]
1928
+ - Updated dependencies [cbb6a5c]
1929
+ - Updated dependencies [fc3a36a]
1930
+ - Updated dependencies [ab9fb5c]
1931
+ - Updated dependencies [69787f0]
1932
+ - Updated dependencies [5d022a1]
1933
+ - Updated dependencies [042b9ee]
1934
+ - Updated dependencies [f985b3f]
1935
+ - Updated dependencies [795b6e1]
1936
+ - Updated dependencies [d52d4fe]
1937
+ - Updated dependencies [742cebb]
1938
+ - Updated dependencies [175d789]
1939
+ - Updated dependencies [f549a0d]
1940
+ - Updated dependencies [427344c]
1941
+ - Updated dependencies [8af76ae]
1942
+ - Updated dependencies [1d4756e]
1943
+ - Updated dependencies [720c5ad]
1944
+ - Updated dependencies [a8d1e24]
1945
+ - Updated dependencies [b85cc54]
1946
+ - Updated dependencies [a36db28]
1947
+ - Updated dependencies [7a8476f]
1948
+ - Updated dependencies [518ca7a]
1949
+ - Updated dependencies [41642b0]
1950
+ - Updated dependencies [4cca74c]
1951
+ - Updated dependencies [88ef03e]
1952
+ - Updated dependencies [9a4932a]
1953
+ - Updated dependencies [3f8817a]
1954
+ - Updated dependencies [a2443e3]
1955
+ - Updated dependencies [e1554b1]
1956
+ - Updated dependencies [9e2caf3]
1957
+ - Updated dependencies [4856789]
1958
+ - Updated dependencies [81ce41a]
1959
+ - Updated dependencies [85e1e4e]
1960
+ - Updated dependencies [c3f4916]
1961
+ - Updated dependencies [55dbbba]
1962
+ - Updated dependencies [33e0385]
1963
+ - Updated dependencies [dac6a08]
1964
+ - Updated dependencies [72c3c86]
1965
+ - Updated dependencies [2d8dba3]
1966
+ - Updated dependencies [7f1a635]
1967
+ - Updated dependencies [2205363]
1968
+ - Updated dependencies [09fe58d]
1969
+ - Updated dependencies [f9fc874]
1970
+ - Updated dependencies [d62f8eb]
1971
+ - Updated dependencies [d0a5ceb]
1972
+ - Updated dependencies [a7586cd]
1973
+ - Updated dependencies [4c5e80e]
1974
+ - Updated dependencies [4b5702a]
1975
+ - Updated dependencies [011b386]
1976
+ - Updated dependencies [e18a162]
1977
+ - Updated dependencies [394b7a1]
1978
+ - Updated dependencies [ce92674]
1979
+ - Updated dependencies [cf2c9b7]
1980
+ - Updated dependencies [d127ff0]
1981
+ - Updated dependencies [36d90fc]
1982
+ - Updated dependencies [7777e8f]
1983
+ - Updated dependencies [9b86cf6]
1984
+ - Updated dependencies [d063a96]
1985
+ - Updated dependencies [8825a06]
1986
+ - Updated dependencies [5087ac6]
1987
+ - Updated dependencies [677b591]
1988
+ - Updated dependencies [cf7c694]
1989
+ - Updated dependencies [ddd0f06]
1990
+ - Updated dependencies [d77d1b7]
1991
+ - Updated dependencies [0f9faa2]
1992
+ - Updated dependencies [2d1ddf0]
1993
+ - Updated dependencies [354b00f]
1994
+ - Updated dependencies [3de535b]
1995
+ - Updated dependencies [fe2e15a]
1996
+ - Updated dependencies [5b79a34]
1997
+ - Updated dependencies [502564d]
1998
+ - Updated dependencies [603cab8]
1999
+ - Updated dependencies [c757854]
2000
+ - Updated dependencies [471839d]
2001
+ - Updated dependencies [507b92a]
2002
+ - Updated dependencies [b508244]
2003
+ - Updated dependencies [df95346]
2004
+ - Updated dependencies [3dede58]
2005
+ - Updated dependencies [c6b6bb4]
2006
+ - Updated dependencies [594508e]
2007
+ - Updated dependencies [7cf42fe]
2008
+ - Updated dependencies [5966c2a]
2009
+ - Updated dependencies [0045682]
2010
+ - Updated dependencies [7309c81]
2011
+ - Updated dependencies [2f59da0]
2012
+ - Updated dependencies [d56012f]
2013
+ - Updated dependencies [f78dd83]
2014
+ - Updated dependencies [a2cd18a]
2015
+ - Updated dependencies [9051802]
2016
+ - Updated dependencies [20bc1ec]
2017
+ - Updated dependencies [1c625ca]
2018
+ - Updated dependencies [2f8328c]
2019
+ - Updated dependencies [2a6c279]
2020
+ - Updated dependencies [8c8f0df]
2021
+ - Updated dependencies [8ad609c]
2022
+ - Updated dependencies [bbee302]
2023
+ - Updated dependencies [90c2b15]
2024
+ - Updated dependencies [4638aaa]
2025
+ - Updated dependencies [0222d3c]
2026
+ - Updated dependencies [08863dd]
2027
+ - Updated dependencies [f293d45]
2028
+ - Updated dependencies [56664f5]
2029
+ - Updated dependencies [71f205d]
2030
+ - Updated dependencies [f067930]
2031
+ - Updated dependencies [414395b]
2032
+ - Updated dependencies [42eeb7d]
2033
+ - Updated dependencies [31cbe90]
2034
+ - Updated dependencies [6b7129a]
2035
+ - Updated dependencies [97ace2a]
2036
+ - Updated dependencies [26e1029]
2037
+ - Updated dependencies [0a936ea]
2038
+ - Updated dependencies [90bbf25]
2039
+ - Updated dependencies [023c00b]
2040
+ - Updated dependencies [eb91eba]
2041
+ - Updated dependencies [42da73d]
2042
+ - Updated dependencies [01e124d]
2043
+ - Updated dependencies [ef7b5ef]
2044
+ - Updated dependencies [9514767]
2045
+ - Updated dependencies [8f20201]
2046
+ - Updated dependencies [155507e]
2047
+ - Updated dependencies [643b7c7]
2048
+ - Updated dependencies [7bba90b]
2049
+ - Updated dependencies [8813b90]
2050
+ - Updated dependencies [108ba8d]
2051
+ - Updated dependencies [2a5f04a]
2052
+ - Updated dependencies [4f740b0]
2053
+ - Updated dependencies [7ce02eb]
2054
+ - Updated dependencies [b4ad984]
2055
+ - Updated dependencies [a9f32df]
2056
+ - Updated dependencies [aeb9b27]
2057
+ - Updated dependencies [7d27da0]
2058
+ - Updated dependencies [1a15893]
2059
+ - Updated dependencies [b70e534]
2060
+ - Updated dependencies [7e05d8e]
2061
+ - Updated dependencies [8f1851e]
2062
+ - Updated dependencies [61ea810]
2063
+ - Updated dependencies [2233a85]
2064
+ - Updated dependencies [67452d1]
2065
+ - Updated dependencies [089767f]
2066
+ - Updated dependencies [a13827e]
2067
+ - Updated dependencies [66d99ec]
2068
+ - Updated dependencies [cb43296]
2069
+ - Updated dependencies [b61afc1]
2070
+ - Updated dependencies [79021fc]
2071
+ - Updated dependencies [7733604]
2072
+ - Updated dependencies [40e420f]
2073
+ - Updated dependencies [62dd69a]
2074
+ - Updated dependencies [d13004a]
2075
+ - Updated dependencies [e15e679]
2076
+ - Updated dependencies [2ab1257]
2077
+ - Updated dependencies [0fc6219]
2078
+ - Updated dependencies [061406d]
2079
+ - Updated dependencies [e4c8b6c]
2080
+ - Updated dependencies [acb10f6]
2081
+ - Updated dependencies [605e190]
2082
+ - Updated dependencies [c6c59f1]
2083
+ - Updated dependencies [b0e78a8]
2084
+ - Updated dependencies [f31cc8d]
2085
+ - Updated dependencies [f343dc4]
2086
+ - Updated dependencies [8269e32]
2087
+ - Updated dependencies [74f7339]
2088
+ - Updated dependencies [a6c35a2]
2089
+ - Updated dependencies [c2f1002]
2090
+ - Updated dependencies [4cc4fb7]
2091
+ - Updated dependencies [97b6658]
2092
+ - Updated dependencies [2c26040]
2093
+ - Updated dependencies [f758cec]
2094
+ - Updated dependencies [5b47ab5]
2095
+ - Updated dependencies [b09d8d9]
2096
+ - Updated dependencies [b09d8d9]
2097
+ - Updated dependencies [8675db6]
2098
+ - Updated dependencies [b09d8d9]
2099
+ - Updated dependencies [27358d5]
2100
+ - Updated dependencies [1c3da1f]
2101
+ - Updated dependencies [c1f344b]
2102
+ - Updated dependencies [3eb1b2b]
2103
+ - Updated dependencies [9c93465]
2104
+ - Updated dependencies [a34fd2e]
2105
+ - Updated dependencies [ebb209c]
2106
+ - Updated dependencies [76bcb83]
2107
+ - Updated dependencies [59b85c0]
2108
+ - Updated dependencies [889ae47]
2109
+ - Updated dependencies [4f4c3fb]
2110
+ - Updated dependencies [78f0be8]
2111
+ - Updated dependencies [6e357ed]
2112
+ - Updated dependencies [d6938bf]
2113
+ - Updated dependencies [35f7fb4]
2114
+ - Updated dependencies [0410522]
2115
+ - Updated dependencies [63b33e6]
2116
+ - Updated dependencies [f163028]
2117
+ - Updated dependencies [814db6d]
2118
+ - Updated dependencies [a5302c7]
2119
+ - Updated dependencies [31e0be9]
2120
+ - Updated dependencies [4bfd455]
2121
+ - Updated dependencies [ffd2ce2]
2122
+ - Updated dependencies [2a44c1d]
2123
+ - Updated dependencies [7084313]
2124
+ - Updated dependencies [f07808c]
2125
+ - Updated dependencies [7ffc3d3]
2126
+ - Updated dependencies [88346ba]
2127
+ - Updated dependencies [4631592]
2128
+ - Updated dependencies [62f8017]
2129
+ - Updated dependencies [32ff033]
2130
+ - Updated dependencies [a831df1]
2131
+ - Updated dependencies [f752ee3]
2132
+ - Updated dependencies [a1b61e0]
2133
+ - Updated dependencies [cd6b9f2]
2134
+ - Updated dependencies [2cb6d3c]
2135
+ - Updated dependencies [af2a095]
2136
+ - Updated dependencies [5ac93d4]
2137
+ - Updated dependencies [695cfbd]
2138
+ - Updated dependencies [0e043d8]
2139
+ - Updated dependencies [93f267f]
2140
+ - Updated dependencies [7445149]
2141
+ - Updated dependencies [ec796d5]
2142
+ - Updated dependencies [071d0dc]
2143
+ - Updated dependencies [0024abf]
2144
+ - Updated dependencies [8dd98bf]
2145
+ - Updated dependencies [e87fea1]
2146
+ - Updated dependencies [c65e529]
2147
+ - Updated dependencies [0848bea]
2148
+ - Updated dependencies [d51bed2]
2149
+ - Updated dependencies [dadd1ad]
2150
+ - Updated dependencies [acbf364]
2151
+ - Updated dependencies [3ca34c1]
2152
+ - Updated dependencies [7adc841]
2153
+ - Updated dependencies [239c3a3]
2154
+ - Updated dependencies [b8b3c64]
2155
+ - Updated dependencies [2f2e63c]
2156
+ - Updated dependencies [4845f85]
2157
+ - Updated dependencies [486d526]
2158
+ - Updated dependencies [94a0bbc]
2159
+ - Updated dependencies [d6bfb3d]
2160
+ - Updated dependencies [8a9c079]
2161
+ - Updated dependencies [7b005b4]
2162
+ - Updated dependencies [cc3555e]
2163
+ - Updated dependencies [a2266a6]
2164
+ - Updated dependencies [d25a0ec]
2165
+ - Updated dependencies [89d7b35]
2166
+ - Updated dependencies [94f7b6a]
2167
+ - Updated dependencies [5c94f83]
2168
+ - Updated dependencies [ea936f3]
2169
+ - Updated dependencies [0c0fbd9]
2170
+ - Updated dependencies [667b83e]
2171
+ - Updated dependencies [f3141d8]
2172
+ - Updated dependencies [7687f7b]
2173
+ - Updated dependencies [5a84d41]
2174
+ - Updated dependencies [fd3013a]
2175
+ - Updated dependencies [85ec26d]
2176
+ - Updated dependencies [73e576f]
2177
+ - Updated dependencies [f6476fc]
2178
+ - Updated dependencies [69ac82c]
2179
+ - Updated dependencies [4ac12ef]
2180
+ - Updated dependencies [833ed84]
2181
+ - Updated dependencies [a18abf3]
2182
+ - Updated dependencies [c6a4eeb]
2183
+ - Updated dependencies [1659072]
2184
+ - Updated dependencies [f450ae7]
2185
+ - Updated dependencies [abceb0d]
2186
+ - Updated dependencies [627b188]
2187
+ - Updated dependencies [8d4eae7]
2188
+ - Updated dependencies [c5a5996]
2189
+ - Updated dependencies [0c302a7]
2190
+ - Updated dependencies [b88f5e8]
2191
+ - Updated dependencies [65a3a84]
2192
+ - Updated dependencies [6633337]
2193
+ - Updated dependencies [21676eb]
2194
+ - Updated dependencies [e9cb9ab]
2195
+ - Updated dependencies [42cc219]
2196
+ - Updated dependencies [d7e0b42]
2197
+ - Updated dependencies [3510e4a]
2198
+ - Updated dependencies [f00d8d4]
2199
+ - Updated dependencies [5326b36]
2200
+ - Updated dependencies [aa4b90d]
2201
+ - Updated dependencies [ccd9397]
2202
+ - Updated dependencies [503be86]
2203
+ - Updated dependencies [54299ca]
2204
+ - Updated dependencies [ae490ef]
2205
+ - Updated dependencies [e124711]
2206
+ - Updated dependencies [dc61def]
2207
+ - Updated dependencies [bca935b]
2208
+ - Updated dependencies [c54c822]
2209
+ - Updated dependencies [8dcc0f5]
2210
+ - Updated dependencies [75b9e51]
2211
+ - Updated dependencies [f61c8cf]
2212
+ - Updated dependencies [e3ef52b]
2213
+ - Updated dependencies [0a2f233]
2214
+ - Updated dependencies [8621cdd]
2215
+ - Updated dependencies [251e888]
2216
+ - Updated dependencies [07f1822]
2217
+ - Updated dependencies [e336549]
2218
+ - Updated dependencies [3bb9340]
2219
+ - Updated dependencies [1e604c4]
2220
+ - Updated dependencies [04fab5e]
2221
+ - Updated dependencies [183b4c4]
2222
+ - Updated dependencies [7f713b6]
2223
+ - Updated dependencies [d40f43a]
2224
+ - Updated dependencies [2fdb36e]
2225
+ - Updated dependencies [6f23667]
2226
+ - Updated dependencies [cde1975]
2227
+ - Updated dependencies [0bc685a]
2228
+ - Updated dependencies [20526f5]
2229
+ - Updated dependencies [efedd28]
2230
+ - Updated dependencies [5d21a48]
2231
+ - Updated dependencies [5278e11]
2232
+ - Updated dependencies [c5eef1d]
2233
+ - Updated dependencies [e5e7ee0]
2234
+ - Updated dependencies [23dba62]
2235
+ - Updated dependencies [e0f300b]
2236
+ - Updated dependencies [761a0ba]
2237
+ - Updated dependencies [c960170]
2238
+ - Updated dependencies [19365b7]
2239
+ - Updated dependencies [ba98e26]
2240
+ - Updated dependencies [b7ed26d]
2241
+ - Updated dependencies [a2ebea2]
2242
+ - Updated dependencies [800bdb0]
2243
+ - Updated dependencies [9d4dfc4]
2244
+ - Updated dependencies [1059965]
2245
+ - Updated dependencies [def5919]
2246
+ - Updated dependencies [60b672e]
2247
+ - Updated dependencies [6b441a8]
2248
+ - Updated dependencies [ce0cfe9]
2249
+ - Updated dependencies [04f1182]
2250
+ - Updated dependencies [be87153]
2251
+ - Updated dependencies [dd0f681]
2252
+ - Updated dependencies [60f0dd8]
2253
+ - Updated dependencies [a87c5cd]
2254
+ - Updated dependencies [a47f338]
2255
+ - Updated dependencies [b3a3d83]
2256
+ - Updated dependencies [7a55913]
2257
+ - Updated dependencies [35accbf]
2258
+ - Updated dependencies [6038de7]
2259
+ - Updated dependencies [fc5f536]
2260
+ - Updated dependencies [5647006]
2261
+ - Updated dependencies [e654bfd]
2262
+ - Updated dependencies [01a7337]
2263
+ - Updated dependencies [b45c71e]
2264
+ - Updated dependencies [f8cfbb4]
2265
+ - Updated dependencies [6e6c872]
2266
+ - Updated dependencies [2598216]
2267
+ - Updated dependencies [11949fc]
2268
+ - Updated dependencies [2c7e62d]
2269
+ - Updated dependencies [eb95d97]
2270
+ - Updated dependencies [b098b0e]
2271
+ - Updated dependencies [4d00b13]
2272
+ - Updated dependencies [1363084]
2273
+ - Updated dependencies [fa5758e]
2274
+ - Updated dependencies [38f7e4f]
2275
+ - Updated dependencies [eb7613c]
2276
+ - Updated dependencies [c57f3cf]
2277
+ - Updated dependencies [ecc9110]
2278
+ - Updated dependencies [e4c2dc8]
2279
+ - Updated dependencies [97faca3]
2280
+ - Updated dependencies [57bab76]
2281
+ - Updated dependencies [c89d18c]
2282
+ - Updated dependencies [1bd2795]
2283
+ - Updated dependencies [f7bd4e2]
2284
+ - Updated dependencies [361bd5b]
2285
+ - Updated dependencies [aac90a5]
2286
+ - Updated dependencies [3da3da5]
2287
+ - Updated dependencies [1e6ab15]
2288
+ - Updated dependencies [b90086a]
2289
+ - Updated dependencies [8186a70]
2290
+ - Updated dependencies [a329cca]
2291
+ - Updated dependencies [c87ef70]
2292
+ - Updated dependencies [3cb0618]
2293
+ - Updated dependencies [32a0874]
2294
+ - Updated dependencies [6eec18c]
2295
+ - Updated dependencies [4d7bebf]
2296
+ - Updated dependencies [821ac7a]
2297
+ - Updated dependencies [8f81731]
2298
+ - Updated dependencies [7055c22]
2299
+ - Updated dependencies [785a748]
2300
+ - Updated dependencies [3af0354]
2301
+ - Updated dependencies [866ff16]
2302
+ - Updated dependencies [5a85e67]
2303
+ - Updated dependencies [8b50cb3]
2304
+ - Updated dependencies [a0fdc56]
2305
+ - Updated dependencies [b95577a]
2306
+ - Updated dependencies [0dcbc11]
2307
+ - Updated dependencies [d88f3e9]
2308
+ - Updated dependencies [ad5fe25]
2309
+ - Updated dependencies [c183a12]
2310
+ - Updated dependencies [83c161f]
2311
+ - Updated dependencies [d8c4957]
2312
+ - Updated dependencies [b9f930b]
2313
+ - Updated dependencies [f24cb83]
2314
+ - Updated dependencies [5dbbb92]
2315
+ - Updated dependencies [ea90179]
2316
+ - Updated dependencies [1818998]
2317
+ - Updated dependencies [ce92674]
2318
+ - Updated dependencies [5ef0b5b]
2319
+ - Updated dependencies [8c2db68]
2320
+ - Updated dependencies [22b5e54]
2321
+ - Updated dependencies [0166bd5]
2322
+ - Updated dependencies [8064b07]
2323
+ - Updated dependencies [09ee21c]
2324
+ - Updated dependencies [4a56dbd]
2325
+ - Updated dependencies [289d04a]
2326
+ - Updated dependencies [f549a0d]
2327
+ - Updated dependencies [48fbacb]
2328
+ - Updated dependencies [06df4fa]
2329
+ - Updated dependencies [3fc2e48]
2330
+ - Updated dependencies [c9b809f]
2331
+ - Updated dependencies [e8f435c]
2332
+ - Updated dependencies [32386f8]
2333
+ - Updated dependencies [9b702dc]
2334
+ - Updated dependencies [ab16331]
2335
+ - Updated dependencies [41610f6]
2336
+ - Updated dependencies [69f1dfd]
2337
+ - Updated dependencies [bbe05de]
2338
+ - Updated dependencies [355e951]
2339
+ - Updated dependencies [a1dd1e4]
2340
+ - Updated dependencies [dadb43f]
2341
+ - @objectstack/spec@17.0.0
2342
+
3
2343
  ## 17.0.0-rc.6
4
2344
 
5
2345
  ### Minor Changes