@objectstack/core 17.0.0-rc.4 → 17.0.0-rc.6

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,526 @@
1
1
  # @objectstack/core
2
2
 
3
+ ## 17.0.0-rc.6
4
+
5
+ ### Minor Changes
6
+
7
+ - 82da264: feat: declare `ExecutionContext.authGate`, so the ADR-0069 gate sits inside the closed field set (#7280)
8
+
9
+ The ADR-0069 authentication-policy gate (expired password, enforced MFA) rode
10
+ the execution context **undeclared**: REST's `computeExecCtx` spread it onto the
11
+ assembled envelope with `...(authGate ? { authGate } : {})` behind an `as any`,
12
+ and its `enforceAuth` read it back ten lines later. Nothing was broken — but the
13
+ closed entry field set shipped in #6216 is derived from `keyof ExecutionContext`,
14
+ so a field that exists only inside an `as any` is **outside every closure gate by
15
+ construction**: `ENTRY_EXECUTION_CONTEXT_FIELDS` could not list it,
16
+ `ExecutionContextEntryFields` could not demand it, and the runtime pin that
17
+ reconciles the closed set against `ExecutionContextSchema.shape` could not see
18
+ it. It was the exact blind spot that gate exists to remove, sitting one `as any`
19
+ outside it.
20
+
21
+ **@objectstack/spec** declares the field:
22
+
23
+ ```ts
24
+ authGate: z.object({ code: z.string(), message: z.string() }).optional();
25
+ ```
26
+
27
+ Both inner keys are required, matching the sole producer
28
+ (`AuthManager.computeAuthGate`, which sets both on every return branch) — `code`
29
+ is the stable machine code a client branches on, `message` is what the blocked
30
+ user reads, and the transport seam renders both as the `403` body.
31
+
32
+ **@objectstack/core** picks it up as an ENTRY-decided field — it is resolved from
33
+ the request's own session at the transport entry point, never written mid-request
34
+ — so `ExecutionContextAssemblyInput` gains a **required** `authGate` input on the
35
+ same footing as `accessToken`: every face states its decision instead of omitting
36
+ it. A guest principal never carries one (no authenticated session for a policy
37
+ gate to attach to). Also exported: `normalizeAuthGate`, which completes a session
38
+ user's loose `authGate` into the declared shape at the one producer rather than
39
+ tolerating a partial shape downstream — a gate naming a `code` but no `message`
40
+ no longer renders a `403` body with `message: undefined`. `AuthGate` is now
41
+ derived from the schema instead of being a second hand-written declaration.
42
+
43
+ **@objectstack/rest** passes the resolved gate as an assembler input and drops the
44
+ post-assembly spread; the remaining `as any` covers `__kernel` alone.
45
+ **@objectstack/runtime** (the runtime / MCP dispatcher) passes `authGate:
46
+ undefined` on the record: it enforces the same gate at its own seam
47
+ (`HttpDispatcher.enforceAuthGate` re-reads the session and calls
48
+ `evaluateAuthGate`) and never reads `context.authGate`, so carrying it there
49
+ would be a second copy no consumer reads.
50
+
51
+ **No runtime behaviour change on either surface.** The shared assembler omits
52
+ `undefined`-valued keys, so the key is present exactly when it was before. The one
53
+ new behaviour is the normalization above, on a shape the sole producer never
54
+ emits today.
55
+
56
+ - f586f1a: refactor: one shared `ExecutionContext` assembler, two named anonymous entries (#6216)
57
+
58
+ `resolveAuthzContext` already made AUTHORIZATION resolution single-sourced; the
59
+ step after it — turning the resolved envelope into the `ExecutionContext` that
60
+ reaches enforcement — was still one hand-written copy per transport, and the
61
+ copies drifted twice for real: **#6071** (the REST copy never set
62
+ `principalKind`, so every enforcement judgment reading it was silently
63
+ never-true on that face) and **#6206 / #6551** (a dropped `accessible_org_ids`
64
+ produced real 403s on the share-link faces).
65
+
66
+ **@objectstack/core** gains the single assembly, with the anonymous divergence
67
+ as named API rather than drift (maintainer ruling 2026-08-08 on #6216, Option
68
+ A):
69
+
70
+ - `assembleExecutionContext(input)` — the **fail-closed default** entry. No
71
+ resolved principal → `undefined`, and the surface answers 401.
72
+ - `assembleExecutionContextOrGuest(input)` — the **explicit guest** entry. No
73
+ resolved principal → a first-class guest envelope (`principalKind: 'guest'`,
74
+ `positions: ['guest']`), whose consumers are live (`explain-engine`'s
75
+ guest ⇒ `EXTERNAL` posture floor). Adopted only by a surface whose product
76
+ semantics serve anonymous principals.
77
+ - The field set is **closed by type**: `ExecutionContextEntryFields` requires a
78
+ decision for every `ExecutionContext` field that is not explicitly declared
79
+ non-entry-resolved, so a new field cannot reach one transport and miss
80
+ another. Also exported: `ENTRY_EXECUTION_CONTEXT_FIELDS`,
81
+ `EntryExecutionContextField`, `ExecutionContextAssemblyInput`,
82
+ `OAuthTokenProvenance`, `EntryLocalization`.
83
+
84
+ **@objectstack/runtime** (`resolveExecutionContext`, the runtime / MCP
85
+ dispatcher) and **@objectstack/rest** (`computeExecCtx`) now assemble through
86
+ that module — the dispatcher via the guest entry, REST via the fail-closed
87
+ default.
88
+
89
+ **No runtime behaviour change on either surface.** The remaining per-face
90
+ divergences are required inputs rather than silent omissions: REST passes
91
+ `accessToken: undefined` (it has never carried the session bearer on the
92
+ envelope, and `session.accessToken` is a published hook surface) and
93
+ `oauth: undefined` (OAuth bearers are honoured on the `/mcp` door alone). The
94
+ one measurable difference is that a key whose value was `undefined` is now
95
+ omitted rather than spelled — invisible to `ctx.x` reads, to `JSON.stringify`
96
+ and to spreading the envelope.
97
+
98
+ - 28d1eb7: fix(core): the QA `contains` assertion fails loudly instead of silently passing on a non-array/non-string actual (#7256)
99
+
100
+ `TestRunner.assert`'s `case 'contains':` handled the two shapes it can evaluate —
101
+ an array (membership) and a string (substring) — and had **no `else`**. Every
102
+ other shape fell straight out of the switch throwing nothing, so the assertion
103
+ reported **PASSED**. A scenario asserting
104
+ `{ field: "body.data.items", operator: "contains", expectedValue: "acme" }`
105
+ against a response that has no `body.data.items` at all reported ✅. The
106
+ overwhelmingly common way to reach that branch is the one that matters most: a
107
+ typo'd `field` path, or a response shape that moved under a suite nobody
108
+ re-read. The assertion that was supposed to _be_ the test is the thing that
109
+ silently disappears, and CI believes the green.
110
+
111
+ `contains` was the only path in this engine that could decide "no comparison
112
+ applies here" and report success. Every other unhandled shape already fails
113
+ loud — an operator with no branch throws `Unknown assertion operator`, an action
114
+ type with no adapter branch throws `Unsupported action type in HttpAdapter`,
115
+ and `equals`/`not_equals`/`is_null`/`not_null` all compare unconditionally. This
116
+ closes the asymmetry rather than adding a new posture: an assertion the engine
117
+ **cannot evaluate** is a **failed** assertion.
118
+
119
+ The message is written for the author who has to act on it, so it names the
120
+ field, the operator and the runtime type of what the path actually resolved to
121
+ (`null` and arrays get their own names, not `typeof`'s `object`), and then says
122
+ which of the two things is wrong:
123
+
124
+ ```
125
+ Assertion failed: body.data.items cannot be evaluated by 'contains' — expected an
126
+ array or a string at that path, got undefined. The path resolved to nothing — the
127
+ field is absent from the result, or the path is misspelled. Use 'is_null' if
128
+ asserting absence is what you meant.
129
+ ```
130
+
131
+ `undefined`/`null` point at the **fixture** (the path did not resolve, so the
132
+ field path or the response shape it was written against is the suspect);
133
+ a number, boolean or object points at the **assertion** (the path resolved
134
+ fine and `contains` is the wrong operator for what it found).
135
+
136
+ **Behaviour change, and its measured blast radius.** Suites that today pass a
137
+ `contains` against a non-array/non-string will start failing — which is the
138
+ point; each such assertion was asserting nothing. The in-tree radius was
139
+ measured on the loud build and is **zero**: `os test` is the runner's only
140
+ consumer, and the repository contains no Quality Protocol suite documents at
141
+ all (no `qa/*.test.json` anywhere; the three example apps run `vitest`, and
142
+ `packages/qa/*` are vitest suites that never touch `TestRunner`). No CI workflow
143
+ invokes `os test`. So no in-repo case was passing vacuously and none needed
144
+ repair. Downstream suites are the ones that will see red, and every case they
145
+ see is a test that was never running.
146
+
147
+ The two evaluable shapes are untouched in both directions: a matching array or
148
+ string still passes, a non-matching one still fails with its existing message.
149
+ `not_contains`, `gt`, `gte`, `lt`, `lte` and `error` are declared in
150
+ `TestAssertionTypeSchema` and still have no branch in the runner — they were
151
+ already refused loudly at `default:` rather than silently passed, so they do not
152
+ carry this defect; that gap is recorded separately and is pinned here so a later
153
+ implementation is a deliberate change rather than an accident.
154
+
155
+ ### Patch Changes
156
+
157
+ - b127c8b: fix(spec,core): a filter placeholder is recognised by INTENT — `{TODAY()}` refuses loudly instead of comparing as a literal (#5586)
158
+
159
+ `UnknownFilterTokenError` had a hole exactly where authors fall in. Recognition
160
+ used the token-NAME grammar `/^\$?\{([a-zA-Z0-9_]+)\}$/`, so any placeholder
161
+ carrying a **non-word character** classified as "not a placeholder at all" and
162
+ was handed to the driver verbatim, to be compared as a literal string — the
163
+ silent-wrong-result failure the diagnostic exists to abolish.
164
+
165
+ The failure was inverted against the author. Measured on 17.0.0-rc.2 against a
166
+ four-row fixture:
167
+
168
+ | filter value | before | |
169
+ | ------------------------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
170
+ | `due_date < '{today}'` | 2 rows | correct — the two overdue rows |
171
+ | `due_date < '{TODAY}'` | throws `UnknownFilterTokenError` | diagnostic working |
172
+ | `due_date < '{TODAY()}'` | **4 rows** | diagnostic bypassed — literal string compare, and `'2026-…' < '{'` in lexicographic order swallowed a row due a week later |
173
+
174
+ So misspelling `{today}` as `{TODAY}` was reported by name, while misspelling it
175
+ as `{TODAY()}` returned the wrong rows in silence — and the parenthesised,
176
+ kebab-case, natural-language and dotted spellings (`{TODAY()}`,
177
+ `{current-user-id}`, `{30 days ago}`, `{user.id}`) are precisely what an author
178
+ migrating from another system's macro syntax writes first.
179
+
180
+ **Both directions of the behaviour change:**
181
+
182
+ - **Previously silent, now refuses loudly** — a filter value that is entirely
183
+ brace-wrapped and outside the vocabulary now throws `UnknownFilterTokenError`
184
+ (`code: FILTER_TOKEN_UNKNOWN`, `status: 400`) on the ObjectQL read and write
185
+ paths and the analytics dataset executor, and is reported as
186
+ `filter-token-unknown` by `objectstack build` / `validate` / `lint`. Before,
187
+ it reached the data engine and compared as text.
188
+ - **Unchanged** — `{today}` / `{current_user_id}` still resolve; `{TODAY}` still
189
+ refuses with the same identity; a value that merely _contains_ braces
190
+ (`'acme {x} deal'`), or is not ONE pair around the whole value (`{a}{b}`,
191
+ `{{x}}`, `{}`), is still an ordinary literal and still reaches the driver
192
+ untouched.
193
+
194
+ Recognition and vocabulary are now two named grammars rather than one:
195
+ `FILTER_TOKEN_WRAPPED_RE` (`/^\$?\{([^{}]+)\}$/`) answers "did the author mean a
196
+ placeholder", and `isContextToken` / `isDateMacroToken` answer "is it in the
197
+ vocabulary". Wide in, strict out. No escape hatch for a literal `{…}` comparand
198
+ ships with this: a repo-wide measurement across structured metadata, examples,
199
+ seed data and fixtures found zero legitimate consumers comparing a
200
+ brace-wrapped literal, and an escape syntax is a public micro-contract that can
201
+ be added the day one shows up.
202
+
203
+ Flow templates are unaffected. `interpolateFilter` in
204
+ `@objectstack/service-automation` already recognised the same wide shape and
205
+ resolves `{record.id}` / `{TODAY() + 30}` from flow variables **before** the
206
+ filter reaches ObjectQL; its hand-off to the engine is keyed on the token
207
+ vocabulary (`isKnownFilterToken`), which this change does not touch.
208
+
209
+ - d6d1a50: refactor(core): one implementation per hook-dispatch flavour, plus a paired-pin gate (#5282)
210
+
211
+ `ObjectKernel` does not extend `ObjectKernelBase` — it is a standalone
212
+ production kernel with its own `hooks` map, and only `LiteKernel` extends the
213
+ base. Lifecycle-hook dispatch therefore existed **twice**, with no shared code
214
+ path: the base's `triggerHook` (isolating) / `triggerHookOrThrow` (propagating) /
215
+ `context.trigger` on one side, and `ObjectKernel`'s private
216
+ `triggerShutdownHookIsolating` / `context.trigger` on the other. The two
217
+ isolating loops printed the same `Hook handler failed: kernel:shutdown` line
218
+ because someone typed it twice.
219
+
220
+ That seam produced three consecutive bugs, each the same shape — one hook name
221
+ meaning opposite things on the two kernels: `kernel:ready` (#5170),
222
+ `kernel:bootstrapped` / `kernel:listening` (#5257, where a swallowed
223
+ `server.listen()` failure let a process print "✅ Bootstrap complete" with
224
+ nothing listening), and `kernel:shutdown` in the other direction (#5274, where
225
+ one bad handler skipped every `destroy()`).
226
+
227
+ **No behaviour change.** The two dispatch flavours move verbatim into an
228
+ internal module, `packages/core/src/hook-dispatch.ts`, which both kernels now
229
+ call:
230
+
231
+ - `dispatchHookIsolating` — a failing handler is logged as
232
+ `Hook handler failed: <name>` and the remaining handlers still run.
233
+ - `dispatchHookPropagating` — the first failure escapes unwrapped and the
234
+ handlers behind it are skipped.
235
+
236
+ Every call path keeps the flavour, the log wording and the trace line it had
237
+ before, including the one asymmetry inside the propagating flavour:
238
+ `PluginContext.trigger` has never emitted the `Triggering hook: <name>` trace on
239
+ either kernel, so it still does not. The kernels' two `hooks` maps are
240
+ deliberately **not** unified, and `ObjectKernel` deliberately does **not** gain a
241
+ base class — both were considered and ruled out of scope.
242
+
243
+ How "no behaviour change" was proved: the paired kernel pins from #5170 / #5257 /
244
+ #5274 pass untouched, and deleting the shared dispatcher's error log now turns
245
+ **both** kernels' test files red from a single edit — a property the hand-mirrored
246
+ copies could not have (editing `ObjectKernel`'s private loop could never turn
247
+ `lite-kernel.test.ts` red).
248
+
249
+ Shared dispatch cannot cover the residual two-maps seam, so the pairing of the
250
+ tests is now a gate rather than a convention: `pnpm check:kernel-hook-pairs`
251
+ (`scripts/check-kernel-hook-pairs.mjs`, wired into the ESLint job) requires every
252
+ `kernel:*` hook dispatched in `packages/core/src` to be named in a test title in
253
+ **both** `kernel.test.ts` and `lite-kernel.test.ts`, and fails naming the hook
254
+ and the side that lacks it. A fifth lifecycle hook can no longer arrive paired on
255
+ one kernel only.
256
+
257
+ Also pinned, deliberately unchanged: `kernel:shutdown` has two dispatch paths
258
+ with different flavours on both kernels — the kernel's own teardown isolates,
259
+ while a plugin calling `ctx.trigger('kernel:shutdown')` by hand propagates.
260
+ Nothing in the repo triggers it by hand today, so this is dormant; it is now a
261
+ documented fact with a named test on each side rather than a surprise found at
262
+ teardown.
263
+
264
+ - d0d5205: refactor(core,plugin-audit,service-storage,plugin-reports): give the `__` operation-private-key convention a single owner (#7284)
265
+
266
+ `withoutOperationPrivateKeys` — the rule that a consumer forwarding a caller's
267
+ execution envelope to a question about a DIFFERENT object must first drop the
268
+ `__`-prefixed keys plugin-security stamped for the operation in flight — had been
269
+ hand-copied into three packages: `plugin-audit`'s comment access hooks (#7141),
270
+ `service-storage`'s attachment access hooks (#7145) and `plugin-reports`' report
271
+ service (#7204). Each carried its own `OPERATION_PRIVATE_KEY_PREFIX` and its own
272
+ doc block, and the prose had already diverged while the code still agreed — the
273
+ shape that makes a later divergence in behaviour hard to notice.
274
+
275
+ The helper now lives once, in `@objectstack/core`
276
+ (`security/operation-private-keys.ts`), exported from the package root. Core is
277
+ the only candidate all three consumers already depend on: `plugin-security` is
278
+ the producer of the convention and the most honest owner, but none of the three
279
+ depends on it and a string-prefix filter does not justify three new dependency
280
+ edges onto a plugin; `@objectstack/spec` is fenced off by Prime Directive #2. The
281
+ new home sits beside `assemble-execution-context.ts`, which owns the other end of
282
+ the same lifecycle — that file is where an `ExecutionContext` is built at a
283
+ transport entry point, this one is where it is stripped back down before being
284
+ forwarded.
285
+
286
+ The full reasoning moved with the code rather than being thinned: which keys the
287
+ middleware stamps and why each is a widening input, why they are dropped by
288
+ PREFIX and never by a name list, and why the fresh copy is load-bearing in both
289
+ directions. Each consumer keeps only its own local half — which object _its_
290
+ gates actually ask about — and points at the shared home.
291
+
292
+ No behaviour change: the three copies were byte-equivalent, and all three
293
+ packages' suites pass unchanged. Two new pins at the home cover it — the rule's
294
+ own behaviour, which no package-level test had ever asserted directly, and a
295
+ repository-shape pin that turns red if a fourth file declares its own copy.
296
+
297
+ - Updated dependencies [3d5c090]
298
+ - Updated dependencies [e5bd768]
299
+ - Updated dependencies [e027b3e]
300
+ - Updated dependencies [c2429b0]
301
+ - Updated dependencies [445a0c2]
302
+ - Updated dependencies [f6609e6]
303
+ - Updated dependencies [a70358a]
304
+ - Updated dependencies [97e7e3c]
305
+ - Updated dependencies [8828b9e]
306
+ - Updated dependencies [53068c1]
307
+ - Updated dependencies [ee58392]
308
+ - Updated dependencies [f16e54e]
309
+ - Updated dependencies [06be54e]
310
+ - Updated dependencies [259459d]
311
+ - Updated dependencies [3f7f14e]
312
+ - Updated dependencies [6968885]
313
+ - Updated dependencies [eaed61f]
314
+ - Updated dependencies [debe2f6]
315
+ - Updated dependencies [97b0798]
316
+ - Updated dependencies [43a7a8d]
317
+ - Updated dependencies [73f69dc]
318
+ - Updated dependencies [04c56aa]
319
+ - Updated dependencies [b3efeb7]
320
+ - Updated dependencies [ddd075a]
321
+ - Updated dependencies [88154be]
322
+ - Updated dependencies [e8dc61e]
323
+ - Updated dependencies [2f3e793]
324
+ - Updated dependencies [d8e8d9c]
325
+ - Updated dependencies [94e749b]
326
+ - Updated dependencies [ea1d916]
327
+ - Updated dependencies [ae31a19]
328
+ - Updated dependencies [e0f300b]
329
+ - Updated dependencies [62b6a2f]
330
+ - Updated dependencies [5b4780b]
331
+ - Updated dependencies [a933452]
332
+ - Updated dependencies [8140915]
333
+ - Updated dependencies [7b48cf9]
334
+ - Updated dependencies [b5404f4]
335
+ - Updated dependencies [f764691]
336
+ - Updated dependencies [e120a5a]
337
+ - Updated dependencies [e650d67]
338
+ - Updated dependencies [04476e7]
339
+ - Updated dependencies [79228cd]
340
+ - Updated dependencies [b3363e9]
341
+ - Updated dependencies [2ef1807]
342
+ - Updated dependencies [d03fe25]
343
+ - Updated dependencies [2672f85]
344
+ - Updated dependencies [11066f6]
345
+ - Updated dependencies [916af17]
346
+ - Updated dependencies [84c86fb]
347
+ - Updated dependencies [2a2a9fb]
348
+ - Updated dependencies [a2e157c]
349
+ - Updated dependencies [95c4227]
350
+ - Updated dependencies [2a61116]
351
+ - Updated dependencies [d4df105]
352
+ - Updated dependencies [e2798fa]
353
+ - Updated dependencies [0fd8556]
354
+ - Updated dependencies [74155c7]
355
+ - Updated dependencies [6908830]
356
+ - Updated dependencies [8b06bba]
357
+ - Updated dependencies [4c54037]
358
+ - Updated dependencies [0f7157b]
359
+ - Updated dependencies [d9bef45]
360
+ - Updated dependencies [f549a0d]
361
+ - Updated dependencies [82da264]
362
+ - Updated dependencies [9b9b70f]
363
+ - Updated dependencies [f5a9bc2]
364
+ - Updated dependencies [881a3cc]
365
+ - Updated dependencies [ad6317b]
366
+ - Updated dependencies [8a88885]
367
+ - Updated dependencies [5f7669e]
368
+ - Updated dependencies [becbe53]
369
+ - Updated dependencies [b127c8b]
370
+ - Updated dependencies [a80302a]
371
+ - Updated dependencies [474f131]
372
+ - Updated dependencies [050cd82]
373
+ - Updated dependencies [4d552af]
374
+ - Updated dependencies [44d677c]
375
+ - Updated dependencies [c32944d]
376
+ - Updated dependencies [1dd780f]
377
+ - Updated dependencies [c8d6f6e]
378
+ - Updated dependencies [92a67f2]
379
+ - Updated dependencies [9136327]
380
+ - Updated dependencies [bf0ae99]
381
+ - Updated dependencies [cb3b6cd]
382
+ - Updated dependencies [73b7234]
383
+ - Updated dependencies [d2b97c3]
384
+ - Updated dependencies [59b794f]
385
+ - Updated dependencies [fc3a36a]
386
+ - Updated dependencies [69787f0]
387
+ - Updated dependencies [5d022a1]
388
+ - Updated dependencies [042b9ee]
389
+ - Updated dependencies [f549a0d]
390
+ - Updated dependencies [a36db28]
391
+ - Updated dependencies [3f8817a]
392
+ - Updated dependencies [a2443e3]
393
+ - Updated dependencies [e1554b1]
394
+ - Updated dependencies [4856789]
395
+ - Updated dependencies [c3f4916]
396
+ - Updated dependencies [33e0385]
397
+ - Updated dependencies [2205363]
398
+ - Updated dependencies [09fe58d]
399
+ - Updated dependencies [d0a5ceb]
400
+ - Updated dependencies [e18a162]
401
+ - Updated dependencies [d127ff0]
402
+ - Updated dependencies [9b86cf6]
403
+ - Updated dependencies [8825a06]
404
+ - Updated dependencies [5087ac6]
405
+ - Updated dependencies [2d1ddf0]
406
+ - Updated dependencies [354b00f]
407
+ - Updated dependencies [3de535b]
408
+ - Updated dependencies [fe2e15a]
409
+ - Updated dependencies [c6b6bb4]
410
+ - Updated dependencies [2f59da0]
411
+ - Updated dependencies [8ad609c]
412
+ - Updated dependencies [bbee302]
413
+ - Updated dependencies [08863dd]
414
+ - Updated dependencies [56664f5]
415
+ - Updated dependencies [31cbe90]
416
+ - Updated dependencies [90bbf25]
417
+ - Updated dependencies [eb91eba]
418
+ - Updated dependencies [42da73d]
419
+ - Updated dependencies [643b7c7]
420
+ - Updated dependencies [1a15893]
421
+ - Updated dependencies [b70e534]
422
+ - Updated dependencies [2233a85]
423
+ - Updated dependencies [62dd69a]
424
+ - Updated dependencies [e15e679]
425
+ - Updated dependencies [2ab1257]
426
+ - Updated dependencies [4cc4fb7]
427
+ - Updated dependencies [2c26040]
428
+ - Updated dependencies [f758cec]
429
+ - Updated dependencies [78f0be8]
430
+ - Updated dependencies [35f7fb4]
431
+ - Updated dependencies [a5302c7]
432
+ - Updated dependencies [7084313]
433
+ - Updated dependencies [0e043d8]
434
+ - Updated dependencies [dadd1ad]
435
+ - Updated dependencies [2f2e63c]
436
+ - Updated dependencies [486d526]
437
+ - Updated dependencies [89d7b35]
438
+ - Updated dependencies [85ec26d]
439
+ - Updated dependencies [f6476fc]
440
+ - Updated dependencies [4ac12ef]
441
+ - Updated dependencies [b88f5e8]
442
+ - Updated dependencies [42cc219]
443
+ - Updated dependencies [d7e0b42]
444
+ - Updated dependencies [3510e4a]
445
+ - Updated dependencies [aa4b90d]
446
+ - Updated dependencies [54299ca]
447
+ - Updated dependencies [dc61def]
448
+ - Updated dependencies [251e888]
449
+ - Updated dependencies [183b4c4]
450
+ - Updated dependencies [2fdb36e]
451
+ - Updated dependencies [20526f5]
452
+ - Updated dependencies [c5eef1d]
453
+ - Updated dependencies [e0f300b]
454
+ - Updated dependencies [761a0ba]
455
+ - Updated dependencies [be87153]
456
+ - Updated dependencies [60f0dd8]
457
+ - Updated dependencies [a87c5cd]
458
+ - Updated dependencies [a47f338]
459
+ - Updated dependencies [2598216]
460
+ - Updated dependencies [2c7e62d]
461
+ - Updated dependencies [eb7613c]
462
+ - Updated dependencies [ecc9110]
463
+ - Updated dependencies [f7bd4e2]
464
+ - Updated dependencies [361bd5b]
465
+ - Updated dependencies [1818998]
466
+ - Updated dependencies [09ee21c]
467
+ - Updated dependencies [f549a0d]
468
+ - Updated dependencies [3fc2e48]
469
+ - Updated dependencies [e8f435c]
470
+ - Updated dependencies [41610f6]
471
+ - @objectstack/spec@17.0.0-rc.6
472
+
473
+ ## 17.0.0-rc.5
474
+
475
+ ### Minor Changes
476
+
477
+ - 1363084: feat(spec,objectql): `engine.transaction` 契约收紧第一批 —— `opts.require` fail-closed 与 `owned` 信号 (#5696)
478
+
479
+ `IObjectQLEngine.transaction` 的声明面(`packages/spec/src/contracts/objectql-engine.ts`,
480
+ ADR-0119 D1)此前把「默认驱动之外的对象写在事务外」与「驱动没有 `beginTransaction`
481
+ 时静默降级」写成**声明语义**的一部分。#4619 把这两条降级变得可观测(PR #5724),本次
482
+ 把其中两条收紧为调用方可选的契约,并同步修订 TSDoc 的事实性偏差。
483
+
484
+ **新增(可选,默认行为完全不变):**
485
+
486
+ - `transaction(cb, base, { require: true })` —— 驱动没有 `beginTransaction` 时
487
+ **抛 `TransactionUnsupportedError`(`code: 'ERR_TRANSACTION_UNSUPPORTED'`)**,
488
+ 而不是静默降级成「无事务、无回滚」。在回调运行**之前**拒绝,所以调用方收到错误时
489
+ 一行都还没写。这是把 `batchData` 的 atomic 门(ADR-0119 D4)泛化成通用能力:
490
+ 只为「开事务的唯一理由就是回滚」的调用方而设,不传 `require` 的行为一字未变
491
+ (仍然降级 + warn-once)。
492
+ - 回调的**第二个参数** `{ owned: boolean }` —— `true` 表示本次调用开启了事务并拥有
493
+ 提交/回滚,`false` 表示它 **join** 了外层已开的 ambient 事务(ADR-0067 D2),
494
+ 或者处在降级路径上(那里根本没有事务可拥有)。join 语义本身正确且保留;缺的是
495
+ 调用方**无从分辨**,而「整体一起回滚」这类担保只在 owned 时成立。单参数回调不受影响。
496
+
497
+ 两点在 `ctx.api.transaction`(`ScopedContext.transaction`,沙箱 hook/action 体)上
498
+ 同样生效 —— 同一个原语的第二份实现不该变成第二种方言。
499
+
500
+ **契约文本修订:** transaction 的 TSDoc 原先写「路由到别处的对象在事务**外**写入」,
501
+ 实测不符 —— 引擎无条件把 ambient 事务句柄穿给了目标驱动,语句在**错误的连接**上执行
502
+ (#5351 在真 SQL driver 上实测为 `no such table`)。TSDoc 已按实测改写,并声明了随后
503
+ 落地的两条语义:业务写跨驱动**响亮拒绝**、系统账本(`lifecycle.class` 为
504
+ `audit`/`telemetry`/`event`)**移出事务执行**。
505
+
506
+ **类型面:** `@objectstack/core` 的 `EngineWithTransaction` 从「手抄签名」改为
507
+ `transaction: IObjectQLEngine['transaction']`,窄接口可以窄,但不能与真签名漂移。
508
+ 新导出 `EngineTransactionOptions` / `EngineTransactionInfo`(spec `contracts` 命名空间,
509
+ 经 `@objectstack/core` 转出)。
510
+
511
+ 升级须知:无破坏性变更。既有调用点全部保持原行为;要 fail-closed 的调用方显式传
512
+ `{ require: true }`。
513
+
514
+ ### Patch Changes
515
+
516
+ - Updated dependencies [e8f8f6c]
517
+ - Updated dependencies [7f713b6]
518
+ - Updated dependencies [c960170]
519
+ - Updated dependencies [def5919]
520
+ - Updated dependencies [ce0cfe9]
521
+ - Updated dependencies [1363084]
522
+ - @objectstack/spec@17.0.0-rc.5
523
+
3
524
  ## 17.0.0-rc.4
4
525
 
5
526
  ### Major Changes