@objectstack/metadata 17.1.0 → 17.3.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,1546 @@
1
1
  # @objectstack/metadata
2
2
 
3
+ ## 17.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 32448d4: feat(metadata): the artifact door registers stack-declared `capabilities` (#12892 step 1)
8
+
9
+ `ARTIFACT_FIELD_TO_TYPE` — the map that decides which collections of a compiled
10
+ artifact reach `MetadataManager` — now carries `capabilities: 'capability'`.
11
+ This is step 1 of the maintainer's 2026-08-29 ruling on #12892 (option 1: *the
12
+ door owns the registration route* for the five artifact security collections).
13
+
14
+ **FROM.** `capabilities` is an authorable top-level stack collection (ADR-0066
15
+ D1), but the door did not map it while `AppPlugin`'s ADR-0057 `SECURITY_FIELDS`
16
+ block did — making that block the collection's **sole registrar on an artifact
17
+ boot**, and it registers the raw bundle bytes with no strict parse, no schema
18
+ default and no ADR-0010 provenance. On a `bootstrap: 'artifact-only'` runtime
19
+ where `AppPlugin` does not run, a package's declared capabilities reached no
20
+ registry at all: `GET /meta/capability` answered **empty**, and
21
+ `bootstrapDeclaredCapabilities` seeded **no `sys_capability` row** for them.
22
+
23
+ **TO.** The door registers them like every other mapped collection: strict
24
+ parse, schema defaults, ADR-0010 provenance. Measured on a real artifact-only
25
+ kernel boot with no `AppPlugin`, over a package declaring
26
+ `{ name: 'crm.export', label: 'Export CRM data' }`:
27
+
28
+ - `GET /meta/capability` went from `[]` to one item carrying `scope:'platform'`
29
+ (the `CapabilitySchema` default) plus `_packageId` / `_packageVersion` /
30
+ `_provenance`;
31
+ - `sys_capability` went from 9 rows (platform-curated only) to 10 — the
32
+ declaration now materializes with `managed_by:'package'` and its `package_id`.
33
+
34
+ **What this does NOT change, deliberately.** On the ordinary artifact boot
35
+ `AppPlugin` still registers `capabilities` and still runs last, so its unparsed
36
+ copy still wins the registry — measured byte-identical before and after this
37
+ change. Two registrars on one route is the interim state the ruling explicitly
38
+ permits while step 2 (that block stops registering the five on the **artifact**
39
+ path, after a census of the non-artifact boots that depend on it) lands. No
40
+ authoring surface moves, and no artifact that parses today stops parsing.
41
+ - 4b4d5a3: fix(metadata): `FilesystemLoader.list()` reports only names `findFile()` / `load()` / `exists()` can resolve
42
+
43
+ **BREAKING (list output narrows).** Two shapes stop appearing in
44
+ `FilesystemLoader.list()`, and therefore in `MetadataManager.listNames()`:
45
+
46
+ - **nested files** — `ROOT/TYPE/crm/account.json` was listed as `account`, a
47
+ name that resolves against `ROOT/TYPE/account.json` and finds nothing;
48
+ - **extension-less files** — `ROOT/TYPE/noext` was listed as `noext`, which
49
+ resolves under no appended extension at all.
50
+
51
+ A third shape follows from the same rule rather than from a rule of its own:
52
+ the extensions a name can be resolved under are now the ones belonging to the
53
+ loader's **registered serializers**, so under the manager's default format set
54
+ (`typescript` / `json` / `yaml`) a `.js` file leaves `list()` too. It was
55
+ previously listed and resolvable while `loadMany()` could never return it and
56
+ `load()` threw `No serializer found for format: javascript`. Register the
57
+ `javascript` serializer and it is listed, resolvable and loadable together.
58
+
59
+ `list()`, `findFile()` and `loadManyKeyed()` now share one name-to-path
60
+ derivation, so `listNames()` and `get()` give the same answer. Previously a name
61
+ could sit in the list while `get()` answered `null` for it — a silent failure an
62
+ author reads as their own typo.
63
+
64
+ Nothing changes for a tree whose metadata is laid out as `ROOT/TYPE/NAME.json`
65
+ (or `.yaml` / `.yml` / `.ts`), which is the layout ADR-0008 §10 already
66
+ prescribes and `metadata-fs`'s `parseItemPath()` already enforces. `.yaml`,
67
+ `.yml` and `.ts` are unaffected: the extension set follows the registered
68
+ serializers, not §10's `.json`-only rule, which governs the `metadata-fs` store.
69
+
70
+ `loadMany()` is unchanged and still returns bodies for nested and
71
+ extension-less files; `findFile()` still resolves an explicitly path-shaped
72
+ name such as `crm/account`, which nothing lists.
73
+
74
+ <!-- adr-0087: not-required (no-migration-prescription) No authorable key, Zod schema or stored row moves: this narrows one runtime loader's `list()` output. The ledger's artifacts project metadata rewrites, and there is nothing here for `objectstack migrate meta` to rewrite — a tree carrying a nested or extension-less file needs the FILE relocated into the two-segment layout ADR-0008 §10 already prescribes, which no migration prescription can express. -->
75
+ - f4e7ae5: fix(security): gate the metadata HMR door on an explicit development posture (#12140)
76
+
77
+ **BREAKING** surface narrowing — `GET`/`POST /api/v1/dev/metadata-events` are no
78
+ longer mounted unless `NODE_ENV` is exactly `development`. Shipped as `minor`
79
+ under the repo's launch-window convention for breaking changes.
80
+
81
+ `MetadataPlugin` mounted both HMR routes whenever a raw-app-capable HTTP server
82
+ was present, with no authentication and no environment condition of its own. The
83
+ dev-only posture the path (`/api/v1/dev/…`) advertises lived only in prose —
84
+ "production deployments simply won't have a CLI POSTing to this endpoint" — which
85
+ is a claim about who is on the network, not a gate that stops them, and the same
86
+ structural shape #9391 closed for the `datasource-admin` family: both mounts take
87
+ the host's framework-native app handle, which is outside REST's `enforceAuth`
88
+ seam by construction.
89
+
90
+ The distributions were enumerated rather than assumed, because "a dev-only
91
+ surface lacking a gate that says so" and "an unauthenticated door on a real
92
+ deployment" want different repairs. The official container image runs `os start`
93
+ under `NODE_ENV=production`; that boot reaches `createStandaloneStack`, which
94
+ composes `MetadataPlugin` unconditionally (only `artifactWatch` was
95
+ NODE_ENV-gated) onto a kernel that registers the Hono server whenever it serves.
96
+ So a production-shaped boot did mount both routes and did answer them — `POST`
97
+ re-reading the compiled artifact from disk and broadcasting a reload frame to
98
+ every connected client, unauthenticated.
99
+
100
+ `registerMetadataHmrRoutes` now refuses at its first statement, ahead of every
101
+ side effect it performs, and returns `null` so "nothing was mounted" is a fact
102
+ the compiler forces its caller to handle. Unset `NODE_ENV` is closed, per the
103
+ maintainer's 2026-08-06 ruling that an absent value reads as `production`; `test`
104
+ and unrecognised spellings (`staging`, `preview`, `qa`) are closed too — a gate
105
+ must not treat a spelling nobody recognises as a key.
106
+
107
+ Who is affected, in both directions:
108
+
109
+ - `os dev` is unchanged. It spawns `os serve --dev`, which sets
110
+ `NODE_ENV='development'` before any plugin starts, so the watch-recompile loop
111
+ still gets its `200` and Studio still gets its reload frames.
112
+ - A deployment that was reaching this endpoint on a production-posture boot now
113
+ gets its host app's `404`. That door was never advertised, never in the SDK
114
+ (`@objectstack/client` builds no such URL) and is a build-tool loopback; if you
115
+ need a supported production reload trigger, that is a product decision, not
116
+ this endpoint.
117
+ - The server-side artifact-file watcher is untouched on every boot shape that had
118
+ it: the reload still happens, only the broadcast to (now absent) SSE clients is
119
+ skipped.
120
+
121
+ <!-- adr-0087: not-required (no-migration-prescription) a mounted HTTP route stops existing under a non-development posture; no authorable key, no spec declaration, no Zod schema and no stored value moves, so `objectstack migrate meta` has nothing to rewrite and no ledger entry could reach anyone this affects -->
122
+ - ac4fefe: fix(metadata): key loader-held items by the row key they were stored under, so a body with no top-level `name` is no longer dropped from `list()` (#14205)
123
+
124
+ `MetadataManager.readListUncached()` — and its no-catch sibling
125
+ `listForIndex()`, which builds the endpoint index — merged each loader's answer
126
+ into the result set keyed by `body.name`, and admitted an item ONLY when the
127
+ stored body carried a string `name`.
128
+
129
+ A metadata body is not required to name itself. `register(type, name, data)`
130
+ takes the key as its ARGUMENT, and `assertMetadataRegisterContract` says so in
131
+ as many words: "A document with NO `name` of its own is fine — the argument is
132
+ the key". An aggregated `defineView` container is exactly that shape — no own
133
+ `name` by design, its identity being the target object, carried in the row's
134
+ `name` COLUMN — and `DatabaseLoader.rowToData()` returns the stored body
135
+ without folding the column into it.
136
+
137
+ So a container written by `register('view', OBJECT, container)` lived in the
138
+ registry for the life of the process and was written to `sys_metadata`, and
139
+ then **disappeared at the next restart**: cold registry, only the loader
140
+ answering, and `list('view')` refused the row. `listDiagnosed()` reported that
141
+ short answer as complete (`degraded: false`) because no loader had thrown. Not
142
+ scoped to views — any loader-held body with no top-level `name` was invisible.
143
+
144
+ **The repair.** A loader-held item's identity is the key its store holds it
145
+ under, so the manager now asks the loader for that key rather than guessing it
146
+ from the body: `MetadataLoader` gains an OPTIONAL `loadManyKeyed()` returning
147
+ `(name, body)` pairs, implemented by `DatabaseLoader` (from the row's `name`
148
+ column) and `MemoryLoader` (from its storage map key). The key travels BESIDE
149
+ the body and is never folded into it, so nothing synthesises a `name` into a
150
+ body that deliberately has none and the register contract's refusal of a
151
+ disagreeing `data.name` keeps meaning what it says.
152
+
153
+ **Nothing consumers see today changes shape.** For any item that went through
154
+ `register()`, a `data.name` that exists is required to equal the key, so the
155
+ keyed merge produces the identical entry; what is new is only the items the old
156
+ gate refused. `loadManyKeyed()` is optional, and a loader without it (a
157
+ `RemoteLoader`, whose wire format carries bodies only) falls back to the
158
+ previous `body.name` keying unchanged — so no implementor of the published
159
+ `MetadataLoader` interface needs to change.
160
+
161
+ `MetadataManager.loadMany()` is deliberately untouched: its `body.name` test is
162
+ a de-duplication guard, not an admission gate — a nameless item already fell
163
+ past it and was returned — so it never carried this defect.
164
+ - 9e0ba21: feat(spec): retire the paper metadata-customization protocol with its full coupling set (#13135, re-charter of #12057; ADR-0049, ADR-0126)
165
+
166
+ <!-- adr-0087: registered metadata-customization-protocol-retired -->
167
+
168
+ **BREAKING** export removal + authorable-key retirement, landing after the
169
+ v17.0.0 cut (the lockstep launch-window convention ships it as `minor`; the
170
+ prescriptions are registered under protocol major 18 —
171
+ `RETIRED_DEFS_BY_MAJOR[18]`, `RETIRED_KEYS_BY_MAJOR[18]` and the D3 semantic
172
+ entry `metadata-customization-protocol-retired` — where `os migrate meta`
173
+ users will look).
174
+
175
+ `kernel/metadata-customization.zod.ts` declared a three-layer platform/user
176
+ patch-overlay protocol (field-level change tracking, customization policies, a
177
+ 3-way-merge story) that nothing reachable implemented: no route ever served
178
+ the paper `…/overlay` / `…/effective` endpoints, the only implementation
179
+ (`packages/metadata`'s manager limb) was called solely by its own unit tests,
180
+ no merge engine ever existed, and no code read a `CustomizationPolicy`.
181
+ ADR-0126 §6 wall 4 supersedes the protocol as a matter of record ("nothing may
182
+ build against it"); the maintainer adopted retirement on #12057 (2026-08-29,
183
+ 「同意」), and #13135 charters the full coupling set the fork report measured.
184
+
185
+ FROM → TO:
186
+
187
+ - `MetadataOverlaySchema` / `FieldChangeSchema` / `CustomizationOriginSchema` /
188
+ `MergeConflictSchema` / `MergeStrategyConfigSchema` / `MergeResultSchema` /
189
+ `CustomizationPolicySchema` and their `…`/`…Parsed` types
190
+ (`@objectstack/spec/kernel`) → *(removed — no replacement protocol)*. The
191
+ customization that actually ships: ADR-0005's org-scoped overlay
192
+ (`allowOrgOverride` on `DEFAULT_METADATA_TYPE_REGISTRY`, `sys_metadata` org
193
+ rows, layered read `code`/`overlay`/`effective`) and ADR-0126's
194
+ packaged-metadata model (clone + ledger disable).
195
+ - `MetadataOverlayResponseSchema` / `MetadataOverlaySaveRequestSchema` /
196
+ `MetadataEffectiveResponseSchema` (`@objectstack/spec/api` §5) →
197
+ *(removed)* — contracts for endpoints no adapter ever served; the layered
198
+ read's contracts (`getMetaItemLayered`) are the live API.
199
+ - `IMetadataService.getOverlay` / `.saveOverlay` / `.removeOverlay` /
200
+ `.getEffective` optional members (`@objectstack/spec/contracts`) →
201
+ *(removed)*, together with `packages/metadata`'s in-memory limb and its
202
+ `'overlay'` feature log entry.
203
+ - `MetadataPluginConfig.customizationPolicies` / `.mergeStrategy` and
204
+ `MetadataManagerConfig.persistence.overlayWritable` → *(removed — retiredKey
205
+ tombstones)*: authoring one is now a `tsc` error and a parse error carrying
206
+ the prescription. Delete the keys; nothing replaces them (`persistence.writable`
207
+ remains the base write gate).
208
+
209
+ One-line fix: delete the keys and any code building against the removed
210
+ exports — they configured and described nothing that ever ran; org-level
211
+ customization keeps riding the ADR-0005 overlay unchanged.
212
+
213
+ The retirement kit: whole-module deletion + kernel barrel line; 10
214
+ `RETIRED_DEFS_BY_MAJOR[18]` entries (7 kernel defs + 3 api §5 contracts); 3
215
+ `RETIRED_KEYS_BY_MAJOR[18]` tombstone entries (no D2 conversion —
216
+ plugin/manager configs are not stack collection members, the
217
+ `kernel/MetadataPluginConfig:additionalTypes` precedent); D3 semantic entry
218
+ `metadata-customization-protocol-retired`; retirement pin test
219
+ (`kernel/metadata-customization-retirement.test.ts`); type-alias pin rows
220
+ Iso408-411 vacated; api-surface / export-origins / json-schema manifest /
221
+ authorable-surface / reference docs regenerated (the
222
+ `kernel/metadata-customization` reference page disappears with the module).
223
+ - 4cda78c: fix(metadata,objectql,metadata-protocol): require a missing-table error to name the table that was READ (#13324)
224
+
225
+ `isMissingTableError` answers the one question that licenses a fail-soft caller
226
+ to treat an empty result as the truth: "did this read fail because the table has
227
+ not been provisioned yet?". It matched the *shape* of the dialect phrase and
228
+ never asked WHICH table the phrase names.
229
+
230
+ Measured on a real libsql database: a view whose base table is gone fails with
231
+ `no such table: main.<base>` when the view itself is read. The phrase matches,
232
+ so a read of a relation that **exists and may be backed by rows** was classified
233
+ benign, and every fail-soft consumer on that path — `probeInstallOrganizations`,
234
+ `resolveFileReferences`, `seedAutonumber`, the cascade-delete dependents probe,
235
+ `DatabaseLoader`, `SeedLoaderService`, the `sys_metadata` overlay reads —
236
+ computed its answer from data it never read. That is a false "benign", the
237
+ direction the module's own docblock calls far more expensive than a false
238
+ "real".
239
+
240
+ The predicate now takes the object the caller was reading and refuses the
241
+ benign verdict when the phrase names a different relation. Shape alone cannot
242
+ separate the two cases: measured, a view over a missing base table and a
243
+ genuine missing table the caller qualified produce byte-identical messages, so
244
+ the read's name is a parameter rather than another regex.
245
+
246
+ The parameter is **optional** — omitting it reproduces the previous behaviour
247
+ exactly, so no external caller of `@objectstack/metadata/errors` changes. Every
248
+ in-repo call site now passes it. The comparison folds away schema/database
249
+ qualifiers, the legacy `namespace__short` prefix and case, so every shape
250
+ recognised before for a genuine missing table (sqlite `no such table: X`,
251
+ Postgres `relation "x" does not exist`, MySQL `table "x" doesn't exist`,
252
+ `unknown table`, the SQLSTATE and errno limbs) still answers benign.
253
+ - 3c1bbd2: fix(objectql,metadata): the ObjectQL boot loop derives a view container's object through the shared `deriveViewContainerObject`, so the row's own `name` is the LAST term at every SOURCE registrar (#14399)
254
+
255
+ Three sites derive "which object does an aggregated `defineView` container bind
256
+ to". After #13407 / #13913 / #13912 all three read the container's own top-level
257
+ `object` before the `list.data.object` chain, but they still disagreed about the
258
+ row's own `name`:
259
+
260
+ - `packages/objectql/src/engine.ts` `resolveMetadataItemName('views', item)` —
261
+ the boot-loop SOURCE registrar — read `name` FIRST, before `object`;
262
+ - `deriveViewContainerObject` (`@objectstack/metadata`, used by the artifact/HMR
263
+ SOURCE registrar and by `getViewsByObject()`) and `expandRuntimeViewContainer`
264
+ (`@objectstack/metadata-protocol`, the runtime door) both read `name` LAST.
265
+
266
+ A container written as `{ name: 'lead_views', object: 'crm_lead', list: { … } }`
267
+ therefore registered under `lead_views` through the boot loop and under
268
+ `crm_lead` everywhere else, with the whole expansion (`<object>.<key>`) carried
269
+ along — and since `getViewsByObject()` / `GET /meta/view?object=` filter the
270
+ expanded items by their `object`, which registrar loaded the document decided
271
+ whether the views were addressable under the object at all. No error, no
272
+ diagnostic.
273
+
274
+ The boot loop's container branch now calls `deriveViewContainerObject` — by
275
+ import, not by re-spelling: a fourth hand-copy of the chain was the defect, not
276
+ the repair. The direction is the 2026-08-07 meta-rule rather than taste (one
277
+ operation, two inconsistent implementations, the side bound by a DECLARATION
278
+ wins): `ViewSchema.object`'s own `.describe()` names its readers, while the boot
279
+ loop's order argued from item identity, which declares nothing about the
280
+ binding. The two sites that already held the winning order are untouched.
281
+
282
+ **`@objectstack/metadata` — new public export (`minor`).**
283
+ `deriveViewContainerObject` was module-local; it is now on the package's root
284
+ entry, because `packages/objectql` is a SOURCE registrar for the same containers
285
+ and has to mint the same key. `packages/objectql` already declares
286
+ `@objectstack/metadata` as a dependency and nothing in `packages/metadata`
287
+ depends on `objectql`, so the import adds no cycle.
288
+
289
+ **Scope of the behaviour change.** Only the `views` CONTAINER branch moves, gated
290
+ on `isAggregatedViewContainer`: the assembled `viewItems:` channel (standalone
291
+ ViewItems and flattened overlays, every member of `AssembledViewArtifactSchema`
292
+ requiring `viewKind`) still keys by its own `name` first, which is its identity
293
+ and not a binding. `item.id` is untouched and cannot fire for a container —
294
+ `ViewSchema` is a `strictObject` declaring `name` and `object` and no `id`.
295
+
296
+ **No migration surface.** Measured on this tree: of the 54 non-test sources that
297
+ author or carry view containers, ZERO declare a `name` that differs from the
298
+ object they bind to, so every in-tree container derived identically at all three
299
+ sites before this change and does after it. What moves is the latent shape only.
300
+
301
+ ⚠️ One card premise was measured false and is recorded in the new pin rather
302
+ than quietly dropped: the artifact/HMR registrar does not silently mint a second
303
+ key for a divergent container. It derives `crm_lead` correctly and then refuses
304
+ the whole artifact load — `assertMetadataRegisterContract` (#7378 row 1),
305
+ `VALIDATION_ERROR` / 400 — because the document's own `data.name` still reads
306
+ `lead_views`. The boot loop reconciles that field and the artifact door does
307
+ not; that residual asymmetry is a separate defect at a separate site and is
308
+ filed as its own card.
309
+
310
+ ### Patch Changes
311
+
312
+ - 655b106: fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)
313
+
314
+ A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
315
+ door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
316
+ iterated the **flattened top level** and stamped every item with the artifact's
317
+ own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
318
+ 'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
319
+ pick — so a two-package artifact registered the **module's** object under the
320
+ **App** package's identity, while the ObjectQL load path, reading the same
321
+ artifact's `packages[]`, owned it under the module's.
322
+
323
+ The platform then held two answers to "who owns this object", and which one a
324
+ consumer saw depended on the door it went through. Measured on a real boot of
325
+ `examples/app-multi-package`:
326
+
327
+ - `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
328
+ slots by `${packageId}${name}`, so the two differently-attributed copies
329
+ landed in two slots;
330
+ - `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
331
+ object, because the App-stamped copy was re-ingested into the registry as that
332
+ package's contribution;
333
+ - the layers door named the App package while the item door and
334
+ `GET /api/v1/packages` named the module;
335
+ - Studio's Data pillar for the App package listed the module's object — ADR-0130
336
+ Consequences §1.3a ("Studio's scope is the package") did not hold.
337
+
338
+ **The door now reads both shapes, and attributes every item to the body it was
339
+ found in.** `packages` present → each assembled package body's collections are
340
+ registered stamped with **that body's** id; `packages` absent → the single
341
+ `manifest` branch runs exactly as before (D7). The owner is read off the body an
342
+ item was found in — never reverse-derived by matching a top-level item's name
343
+ against a name-to-package index, which would be the second metadata-identity
344
+ resolution path #14512's triage rejected by name.
345
+
346
+ **Ordering and the entry gate are reused, not re-derived (D5).** The door calls
347
+ the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
348
+ readers of one `packages[]` cannot disagree about the registration order **or**
349
+ about which artifacts are loadable at all.
350
+
351
+ ⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
352
+ `@objectstack/core`** — hence the `minor` there. They were in
353
+ `@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
354
+ metadata door could not import them from where they lived; `@objectstack/core`
355
+ already owns `resolvePluginOrder` and is already a dependency of both readers,
356
+ so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
357
+ re-exports both under their existing names — its published surface is unchanged,
358
+ which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
359
+ dispatcher error vocabulary's `file:` anchors, repointed at the new path.
360
+
361
+ **Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
362
+ than asserted: the whole `manager.register` sequence for a single-`manifest`
363
+ artifact — every call, in order, with the id and version each item was stamped
364
+ with — is pinned as a literal in
365
+ `packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
366
+ recorded identically on both legs of the ablation. A real boot of
367
+ `examples/app-todo` answers every door identically before and after.
368
+
369
+ **Nothing a booted instance can see today disappears.** Every live
370
+ `ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
371
+ (measured, not assumed), so iterating bodies loses no collection; and because
372
+ `packages` composes by `concat`, an artifact whose top level carries a
373
+ definition no package body repeats keeps it — registered once, attributed to the
374
+ artifact's own identity, and logged, because it means the artifact's two halves
375
+ disagree about what it ships.
376
+
377
+ ⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
378
+ emitting the flattened top level alongside `packages[]`. Whether they should is
379
+ #14512's decision, not this door's.
380
+ - 54e2d36: Artifacts built by released 17.x tooling boot again on ≥17.2 runtimes: the artifact-ingestion door now runs a versioned ADR-0087 forward conversion before the strict parse (#12772).
381
+
382
+ A compiled artifact whose declared `engines.protocol` floor predates the running `@objectstack/spec` version replays the full conversion chain — retired entries included — before validation, exactly the policy the stored-row read path already applies to `sys_metadata` rows. Measured incident: `dist/objectstack.json` built by `@objectstack/cli` 17.1.0 carries the then-legal `allowRestore`/`allowPurge` permission bits (75 of each, injected by the released builder), and spec 17.2.0's `retiredKey` tombstone refused the boot with no operator remedy (`os migrate meta` targets sources, not built artifacts).
383
+
384
+ The conversion is versioned, not a blanket amnesty: an artifact authored at the current (or a newer) spec version converts nothing and still refuses at the tombstone — the retired keys return with the M2 lifecycle initiative (#1883), and artifacts authored against that surface are never stripped by history. Conversion notices surface operator-visibly and deduped, one summary line per conversion per artifact. New exports from `@objectstack/metadata-core`: `applyArtifactForwardConversions`, `resolveInstalledSpecVersion`, `parseRangeFloor`, `resolveDeclaredRange`.
385
+ - 7ef0268: fix(metadata,runtime): retire the `policies` dead pointer in both artifact registrars, and pin the map that carried it (#12894)
386
+
387
+ Zero behaviour change, by construction. Both readers of an artifact boot carried
388
+ a `policies` -> `policy` entry — the artifact door's `ARTIFACT_FIELD_TO_TYPE`
389
+ (`packages/metadata/src/plugin.ts`) and `AppPlugin`'s ADR-0057 `SECURITY_FIELDS`
390
+ list (`packages/runtime/src/app-plugin.ts`) — and **neither could ever match**.
391
+ `ObjectStackDefinitionSchema` is a `strictObject` that declares no top-level
392
+ `policies` key, so a definition carrying a `policies` array is refused outright
393
+ by the door's strict parse and reaches neither registry. The word is real but
394
+ lives one level down: on a permission set `policies` is an alias for
395
+ `rowLevelSecurity` (`PERMISSION_SET_KEY_ALIASES`) — a key on an **item**, never a
396
+ collection. Both entries are removed, each leaving in place the note the map
397
+ already writes for a retirement: what it pointed at, and why it could not match.
398
+
399
+ That was the third entry retired from `ARTIFACT_FIELD_TO_TYPE` for exactly this
400
+ reason (`themes`, then `roles` -> `positions`, which "matched nothing and
401
+ silently dropped compiled positions"). So the deletion ships with the thing the
402
+ two predecessors did not have — a check that fails when the pattern recurs:
403
+
404
+ - `check:stack-collection-maps` now reconciles **eight** hand-maintained
405
+ enumerations against the schema, not seven. `SECURITY_FIELDS` is the new
406
+ eighth, and how it was missing is the finding rather than a footnote: it is
407
+ the only one of the eight that pairs its keys as `[collection, kind]` tuples,
408
+ which neither existing extractor could read, so the site was skipped rather
409
+ than reported. Re-adding `policies` — or any other key the schema does not
410
+ declare — to **either** registrar now fails the gate with the site named.
411
+ - A new `tupleFirstItems` extractor reads that shape, with a self-test case
412
+ (13 assertions, up from 12) covering the comment/nesting cases the flat
413
+ string-array extractor already pins.
414
+
415
+ The mirror-image half of the same measurement is **carried, not shipped**:
416
+ `capabilities` is a declared top-level collection that `SECURITY_FIELDS`
417
+ registers and the door's map does not, making `AppPlugin` its sole registrar on
418
+ an artifact boot. Adding it to the door changes what an artifact boot registers,
419
+ so it is measured and handed to the route-ownership decision (#12892) instead of
420
+ being smuggled in here. The new waiver row records the asymmetry in place.
421
+ - b745157: feat(metadata-core,metadata): warn the operator when a pre-current-era artifact carries form-view predicates that fault open (#12915)
422
+
423
+ A form-view predicate binds `record` (+ `previous`, `parent`) in runtime record
424
+ forms, or `data` in metadata-editing forms. The contract states the failure mode
425
+ beside the vocabulary: **a bare identifier is unbound, the predicate faults, and
426
+ `visibleWhen`'s fault fallback is `true`** — so a field the predicate was
427
+ authored to hide renders for everyone.
428
+
429
+ That is quiet alone and lethal in combination with the authoring pattern it
430
+ serves. Measured on a real deployment: an artifact built by released
431
+ `@objectstack/cli` 17.1.0 authors
432
+ `{ field: 'disqualification_reason', required: true, visibleWhen: 'status == "unqualified"' }`
433
+ — the era's working spelling. On a 17.2 runtime the predicate faults open, the
434
+ conditionally hidden field renders, and its unconditional `required: true`
435
+ blocks **every** record creation through the console, while the same payload
436
+ POSTs 201 through REST. Nothing refused and nothing logged, so the operator —
437
+ the only person who can rebuild the artifact — had no signal at all.
438
+
439
+ The framework artifact door now emits **one deduped `warn` line per artifact**
440
+ naming the authored `engines.protocol` floor and the runtime spec version, how
441
+ many predicates on which views (with the first path as an anchor), the
442
+ fault-open consequence, and the remedy (`os build`). It rides the same funnel
443
+ that already carries the forward-conversion summaries, so both SaaS shapes are
444
+ covered: a single-DB multi-org runtime warns once at boot, and per-tenant-DB
445
+ kernels each warn at their own.
446
+
447
+ **No behaviour change.** No refusal, no rewrite, no schema or contract edit —
448
+ the predicate keeps faulting open exactly as before, and the artifact bytes are
449
+ untouched. Rewriting a bare root to `record.` is a separate, deferred ADR-0087
450
+ conversion.
451
+
452
+ **Scoped to legacy artifacts by construction.** The notice fires only inside the
453
+ versioned window the forward conversion already opens (declared floor below the
454
+ running spec, or undeclared), read off that pass's own verdict rather than
455
+ recomputed. An artifact declaring the current or a newer floor gets zero notices
456
+ from this feature even when it carries bare roots — the boundary that keeps a
457
+ notice about legacy artifacts out of contract territory.
458
+
459
+ Detection is exported from `@objectstack/metadata-core` as
460
+ `detectUnboundFormViewPredicateRoots` (with `BOUND_FORM_VIEW_PREDICATE_ROOTS`)
461
+ so other composed artifact doors can reuse one policy rather than fork it. It is
462
+ pure, read-only, and tuned to prefer silence over a false accusation: string
463
+ literals are stripped before the scan, only root position counts, call targets
464
+ are not roots, comprehension macros (whose iteration variable is locally bound)
465
+ are skipped whole, and AST-only envelopes pass.
466
+ - 6a180e4: fix(core,rest,services)!: a permission-store read failure now fails LOUD instead of resolving as an authenticated caller holding zero capabilities (#13279)
467
+
468
+ **BREAKING** runtime behaviour change on the shared authorization resolver,
469
+ shipped as `minor` under the repo's launch-window convention.
470
+
471
+ `resolveAuthzContext`'s per-read helper `tryFind` answered a THROWN read exactly
472
+ the way it answered an EMPTY one: `[]`. So an outage of the permission store
473
+ resolved as a well-formed context for an authenticated principal holding no
474
+ capabilities, and the package-management door answered
475
+ `403 FORBIDDEN` — "Reading packages requires the `studio.access` or
476
+ `setup.access` capability." That answer was measured byte-identical
477
+ (`JSON.stringify` equal, against a control that separates two answers which do
478
+ differ) to what a caller who genuinely holds nothing receives. An administrator
479
+ was told they lack a capability, during an outage of the store that holds the
480
+ capability.
481
+
482
+ Maintainer ruling 2026-08-30, verbatim 「第一批其余同意」: `tryFind` 区分「无行」
483
+ 与「读失败」,读失败 fail-loud —— 权限库不可达时不再解析为「已认证零能力」,而是
484
+ 响亮拒绝(与真实能力拒绝的 403 可区分)。
485
+
486
+ Second maintainer ruling the same day (第 5 场总监席决裁批 #9, verbatim 「同意」),
487
+ after implementing the first one showed that "the read failed" is two facts:
488
+ 采**选项 A** —— 把 `isMissingTableError` 从 `@objectstack/metadata` 迁至
489
+ `@objectstack/types`(core 已依赖),metadata 保留 re-export 兼容;`tryFind` 仅对
490
+ **未被判定为「表未 provision」**的读失败抛 `AuthzStoreUnavailableError`。
491
+
492
+ **What changed.** A permission-store read that is issued and throws now raises
493
+ `AuthzStoreUnavailableError`, which carries the EXISTING ADR-0112 wire code
494
+ `SERVICE_UNAVAILABLE` and status `503`. No code is added to the closed wire
495
+ vocabulary and no response envelope gains or loses a key — only which declared
496
+ code an outage selects. Doors that map thrown errors through
497
+ `resolveThrownHttpError` answer 503 with no per-door change.
498
+
499
+ **What did NOT change**, and is pinned:
500
+
501
+ - A reachable, genuinely EMPTY store (reads return no rows) still resolves to
502
+ zero capabilities.
503
+ - A genuine capability denial still answers `403 FORBIDDEN` with its message.
504
+ - An ABSENT engine (`ql` unwired, so no read is ever issued) still resolves to
505
+ an empty-but-valid envelope.
506
+ - Anonymous requests never reach the store, so an outage cannot make them loud.
507
+ - A REAL engine whose `sys_*` tables were never provisioned resolves to zero
508
+ capabilities, quietly — pinned to be byte-identical to the empty-store
509
+ envelope, in every dialect spelling and in the production wrapper shape where
510
+ the driver's phrase is on `cause` rather than the outer message.
511
+
512
+ **The boundary between the two kinds of read failure.** An earlier revision of
513
+ this changeset claimed "embedders without a data plane are unaffected". That
514
+ claim was too broad; it is retracted here, and the gap it named is now closed
515
+ rather than merely disclosed. A read also throws when the table was never
516
+ PROVISIONED — a real engine, wired and reachable, whose `sys_*` tables were
517
+ never created — and that is a supported deployment shape, not an outage. There
518
+ "zero capabilities" is the TRUE answer rather than a fabrication: nothing is
519
+ provisioned, so nothing was withheld. Only an UNREACHABLE store — the ruling's
520
+ own word 不可达 — leaves the capability set unknown, and only an unknown answer
521
+ may not be reported as a denial.
522
+
523
+ Treating the two alike was measured, not theorised: it turned four CI suites
524
+ red, all from `no such table` on `sys_user` / `sys_member` /
525
+ `sys_user_position` / `sys_user_permission_set`. Ordinary CRUD in
526
+ `@objectstack/client` answered `503`; batch validation errors that owe `400`
527
+ answered `503`, because authorization refused before validation ran; runtime
528
+ notifications answered `401` where authenticated callers must be served `200`;
529
+ and two `.integration.test.ts` noise guards reported that the driver and engine
530
+ diagnostics for `sys_position` stopped being emitted — the eager throw aborted
531
+ the resolution before that later read was ever issued, so a change made to stop
532
+ a failed read being silent had made two other channels silent.
533
+
534
+ `tryFind` therefore raises `AuthzStoreUnavailableError` only for a read failure
535
+ that is NOT positively identified as an unprovisioned table.
536
+
537
+ **`isMissingTableError` moved to `@objectstack/types`.** The classifier that
538
+ draws that boundary already existed and was already right — driver-code based
539
+ rather than prose-sniffing, documented so that "cannot say" never means "be
540
+ loud". It lived in `@objectstack/metadata`, which DEPENDS ON `@objectstack/core`,
541
+ so the resolver could not import it. Rather than keep a second copy of a
542
+ security-relevant predicate, the ruling relocated the one classifier to
543
+ `@objectstack/types` — the package core already depends on, and the repo's own
544
+ stated Home rule for a cross-package error predicate ("every consumer of the
545
+ question already depends on it, so adopting the predicate never adds an edge",
546
+ `packages/types/src/unique-violation.ts`). `@objectstack/metadata/errors` still
547
+ exports `isMissingTableError`, re-exported from the new home, so no consumer of
548
+ that published subpath changes.
549
+
550
+ Its sibling `isSchemaAlreadyExistsError` moved with it — the two are not two
551
+ modules but two signatures over one matcher, and separating them would have
552
+ meant re-rolling the matcher, which is the duplication the module exists to
553
+ prevent. Both are now exported from `@objectstack/types`; the metadata subpath
554
+ deliberately still publishes only `isMissingTableError`, which is the only one
555
+ anything imports through it.
556
+
557
+ ⚠️ **Signed-off risk, recorded because it is load-bearing.** Gating loudness on
558
+ a driver-error predicate was approved with its false-positive direction stated:
559
+ mis-reading a genuine outage as "table not provisioned" silently restores the
560
+ quiet 403 this change removes, with no thrown error and no other failing test.
561
+ That direction is accepted, not overlooked — the predicate keys on driver codes,
562
+ SQLSTATEs and errnos first, excludes the known superstring traps up front, and
563
+ returns `false` for anything it does not positively recognise, so an
564
+ unrecognised outage stays loud by default. The risk is written beside the
565
+ predicate in `resolve-authz-context.ts` and both directions are pinned by name
566
+ in `authz-store-unavailable.test.ts`. ⛔ Do not widen `isMissingTableError` to
567
+ make a first boot quieter: every widening moves outages into the quiet branch.
568
+
569
+ **All-transport, not just REST.** Every transport authorizing through
570
+ `resolveAuthzContext` inherits this. Six of the eight production transports
571
+ wrapped the call in a fail-closed `catch` that would have re-silenced the
572
+ outage — measured, not assumed: with the resolver loud but the nets untouched,
573
+ the package door answered `401`, i.e. the outage merely changed disguises. Those
574
+ `catch` blocks now re-raise via `isAuthzStoreUnavailableError` and keep their
575
+ previous behaviour for every other fault. The transport set is rebuilt from
576
+ source and audited for set equality on every test run, so a transport added
577
+ later cannot inherit the old silence unnoticed.
578
+
579
+ Callers that treat any throw from `resolveAuthzContext` as "anonymous" should
580
+ re-raise `isAuthzStoreUnavailableError(err)` instead: degrading it restores the
581
+ disguise this removes.
582
+
583
+ <!-- adr-0087: not-required (runtime-interface-only packages/core/src/security/resolve-authz-context.ts#ResolvedAuthzContext, packages/core/src/security/authz-store-unavailable.ts#AuthzStoreUnavailableError) The breaking surface is runtime TypeScript in `@objectstack/core`'s security module and nothing else: `resolveAuthzContext` stops always-resolving and raises `AuthzStoreUnavailableError` when a permission-store read is issued and throws. NO metadata surface is touched in either direction. No Zod schema changes, no `packages/spec` declaration is added or removed, no authorable key moves, no stored row shape changes, and no object definition is edited — a customer's metadata app is byte-for-byte unaffected, so `objectstack migrate meta` has nothing to visit and there is no tombstone to mint. The wire vocabulary is likewise untouched: `SERVICE_UNAVAILABLE` is an EXISTING `StandardErrorCode` member that `HttpStatusErrorCodeMap` already maps to 503, so this change only selects a different DECLARED code for an outage rather than adding one. Both named symbols resolve at HEAD as exported declarations whose files are not `*.zod.ts`, are not under `packages/spec/src/contracts/`, are not object definitions and are not `z.input` projections; neither is referenced in code by any metadata surface (the `packages/spec` hits for `resolveAuthzContext` are comment prose describing the envelope, which this gate masks). The channel that reaches an affected consumer is therefore code review and this changeset, never the upgrade guide: a ledger entry could not express "your fail-closed catch should re-raise this error", because there is no metadata for a migration to rewrite. -->
584
+ - e7191ce: fix(build): give each `exports` condition its own `types` target in the 28 dual-build packages (#13112)
585
+
586
+ **Published-surface change, zero runtime change.** No emitted byte moves; what
587
+ moves is which declaration file a resolver READS. Maintainer ruling 2026-08-29
588
+ (decision batch #3, verbatim 「同意」) chose declaring the files over deleting
589
+ them.
590
+
591
+ ## What was wrong
592
+
593
+ These 28 packages are `"type": "module"` and dual-built, and each spelled one
594
+ `types` condition as a **sibling** of `import`/`require`:
595
+
596
+ ```json
597
+ "exports": { ".": {
598
+ "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs"
599
+ } }
600
+ ```
601
+
602
+ A sibling `types` answers for **both** conditions, so a CommonJS consumer was
603
+ handed `dist/index.d.ts` — an ES-module declaration, because the package is
604
+ `"type": "module"` — for an entry point it reaches with `require`. Measured with
605
+ `tsc --traceResolution` on a `"type": "commonjs"` fixture at `moduleResolution:
606
+ node16`:
607
+
608
+ ```
609
+ error TS1479: The current file is a CommonJS module whose imports will produce
610
+ 'require' calls; however, the referenced file is an ECMAScript module and cannot
611
+ be imported with 'require'.
612
+ ```
613
+
614
+ The JavaScript at `dist/index.cjs` loads perfectly (`check:dual-build-cjs-loads`
615
+ has asserted that for months). It is the **types** that told the consumer the
616
+ supported `require` entry point could not be required. The `dist/index.d.cts`
617
+ twin tsup emits beside it — 36 files, 5,517,701 B on this build — was named by
618
+ no condition at all and shipped in every tarball unreachable.
619
+
620
+ ## What changed
621
+
622
+ Each condition now names its own declaration, the shape TypeScript documents:
623
+
624
+ ```json
625
+ "exports": { ".": {
626
+ "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
627
+ "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
628
+ } }
629
+ ```
630
+
631
+ 33 entry points across 27 packages, subpaths included. The root `types` field is
632
+ untouched, so `node10` resolvers are unaffected; the `import` condition resolves
633
+ exactly what it resolved before, measured as an unchanged control in the same
634
+ run.
635
+
636
+ ## `@objectstack/core` is deliberately NOT changed
637
+
638
+ Splitting a declaration in two makes TypeScript compare it nominally, and
639
+ `ObjectKernel` carries a `private plugins` member that reaches every plugin
640
+ through `PluginContext.getKernel()`. With core split, whole-repo `pnpm build`
641
+ fails in `@objectstack/verify` with 5 × TS2345 ("Types have separate
642
+ declarations of a private property 'plugins'"); with core held back and the
643
+ other 27 split, 71/71 tasks pass. So core keeps the sibling-`types` shape and
644
+ its two `.d.cts` files (220,854 B) stay unreachable, declared as such in
645
+ `check:dual-build-cjs-loads`. Splitting it needs a decision about core's public
646
+ types, not about an exports map.
647
+
648
+ ## For consumers
649
+
650
+ - **ESM consumers: nothing changes.** Same declaration file, byte for byte.
651
+ - **CJS consumers under `node16`/`nodenext`: TS1479 goes away** and the
652
+ declarations they get are the ones built for CommonJS.
653
+ - **`node10` / `moduleResolution: node` consumers: nothing changes** — they never
654
+ read `exports`.
655
+ - Nothing is removed: every path that resolved before still resolves.
656
+
657
+ Packages that are CJS-first (`require` → `./dist/index.js`, no `"type": "module"`)
658
+ were already correct and are untouched — their `dist/index.d.ts` really is the
659
+ CommonJS declaration. Their ESM mirror (an unreachable `.d.mts` under the
660
+ `import` condition) is a separate, larger population and is filed separately per
661
+ the ruling, not fixed here.
662
+
663
+ `check:dual-build-cjs-loads` grew a fourth invariant (TYPED) that reds on the old
664
+ shape, so the drift cannot return silently.
665
+ - 2e471e3: `FilesystemLoader` now implements `loadManyKeyed()`, so a metadata file whose
666
+ body has no top-level `name` is no longer invisible to `MetadataManager.list()`.
667
+ `loadMany()` globbed files and pushed bodies, discarding the path it had just
668
+ read; the manager then fell back to keying by `body.name`, which drops every
669
+ nameless body — an aggregated `defineView` container has none by design. That is
670
+ the #14205 defect, unrepaired for this loader until now.
671
+
672
+ The key is this loader's own name-to-path derivation — the basename minus
673
+ extension, the same one `list()` reports — but only where that derivation is a
674
+ bijection for the file: it sits directly under `ROOT/TYPE/` and carries an
675
+ extension `findFile()` tries, so `findFile(type, key)` resolves back to that same
676
+ file. Every other shape (a nested path, an extension-less file) keeps the
677
+ previous behaviour verbatim: keyed by `body.name` when it has one, dropped when
678
+ it has none. `list()` and `findFile()` disagree outside the flat shape — `list()`
679
+ reports the bare basename for a nested file and `findFile()` cannot resolve it —
680
+ so keying those by the basename would mint names `get()` and `exists()` cannot
681
+ open, and two directories holding one basename would collide silently. Repairing
682
+ the derivation itself is tracked separately.
683
+
684
+ One deliberate consequence: a flat file whose `body.name` disagrees with its
685
+ basename is now keyed by the basename. That is #14205's rule (identity is the
686
+ key the store holds an item under, not `body.name`) applied to this loader, and
687
+ it aligns `list()` with `listNames()` for that shape. `loadMany()`'s own
688
+ signature and answer are unchanged; both methods now share one file walk so
689
+ their bodies cannot drift.
690
+ - 200d255: fix(metadata-core,metadata): a form SECTION binds `current_user` too, so the unbound-root notice stops flagging one (#13072)
691
+
692
+ Second correction to the unbound-root boot notice, and the same defect as the
693
+ first one a surface later. The notice judged a SECTION-level predicate against
694
+ `record` / `previous` / `parent` / `data`, sourced faithfully from the section
695
+ contract prose — which was stale.
696
+
697
+ `current_user` and its ADR-0068 alias roots (`user`, `ctx.user`, `os.user`)
698
+ **resolve on a section-level `visibleWhen`**: objectui#6110 threads the host
699
+ shell's predicate scope into `isSectionVisible` where it used to pass
700
+ `undefined`, and objectui#6111 copies the authored `visibleWhen` onto the
701
+ `section-divider` pseudo-field whose predicate the SDUI form renderer evaluates
702
+ with that scope bound. #12914 re-measured the contract text accordingly. Until
703
+ this change, a legacy artifact carrying a legitimate section-level
704
+ `current_user.role == "admin"` predicate was reported at boot as an unbound root
705
+ that faults open — a notice about a predicate that resolves, which is the
706
+ cry-wolf failure the module's own doc forbids and the one that trains operators
707
+ to ignore the channel.
708
+
709
+ **What changes:** one vocabulary now serves both form-view predicate surfaces —
710
+ `record`, `previous`, `parent`, `data`, `current_user`, `user`, `ctx`, `os`. A
711
+ section predicate rooted at the `current_user` family is silent; a section
712
+ predicate rooted at a bare field identifier is still reported, and the operator
713
+ line still prints the rule per surface for the surfaces the findings implicate.
714
+
715
+ **Blast radius, stated without inflation:** this is a **notice**, not a refusal
716
+ — no parse change, no gate, no behaviour change, and it only runs inside the
717
+ versioned window `applyArtifactForwardConversions` opens. The cost it removes is
718
+ a false operator signal on legacy artifacts, not a broken runtime.
719
+
720
+ **Removed export, with its migration:** `FIELD_ONLY_BOUND_PREDICATE_ROOTS` is
721
+ gone from `@objectstack/metadata-core`. The section binding empties it, and an
722
+ exported constant named `FIELD_ONLY_…` holding `[]` asserts a per-surface
723
+ difference that no renderer makes. FROM → TO: read
724
+ `BOUND_FORM_VIEW_PREDICATE_ROOTS` (every root bound on any form-view predicate)
725
+ or `BOUND_FORM_FIELD_PREDICATE_ROOTS` (the field question, the same list today).
726
+ No consumer can be carrying it: the notice has never shipped — the two
727
+ changesets that introduce it are still pending in `.changeset/`, the newest
728
+ published `@objectstack/metadata-core` is 17.2.0, and the commit that added
729
+ `form-predicate-root-policy.ts` is in no release tag. This was the last moment
730
+ at which the removal cost nothing.
731
+
732
+ **Why the vocabulary is no longer justified by quoting the contract.** Both
733
+ times this list has been wrong, it was wrong by transcribing a correct-looking
734
+ sentence that the renderer had already moved past. The prose is a transcription
735
+ of a renderer and can only lag one, so membership is now stated as the mechanism
736
+ — *a root is bound on a surface iff some renderer threads a scope carrying it
737
+ into that surface's evaluator* — with the threading site named per entry, and
738
+ the module's test reads the LIVE `.describe()` text of
739
+ `FormFieldSchema.visibleWhen` / `FormSectionSchema.visibleWhen` out of
740
+ `@objectstack/spec` instead of copying it into a comment. A comment quoting that
741
+ sentence goes stale in silence, twice now; an assertion that fetches it cannot.
742
+
743
+ <!-- adr-0087: not-required (unpublished) the removed export FIELD_ONLY_BOUND_PREDICATE_ROOTS was added after 17.2.0 and is in no release tag, so no upgrader can be holding it and there is nothing to migrate from. -->
744
+ - 2852acc: fix(metadata-core,metadata): split the form-view predicate root vocabulary per surface, so a field-level `current_user` test is not false-flagged (#12915)
745
+
746
+ Same-day correction to the unbound-root boot notice. The notice judged **every**
747
+ form-view predicate against one vocabulary (`record` / `previous` / `parent` /
748
+ `data`), sourced faithfully from the contract prose — which, for the field-level
749
+ slot, was stale.
750
+
751
+ `current_user` and its ADR-0068 alias roots (`user`, `ctx.user`, `os.user`)
752
+ **resolve on a field-level `visibleWhen`** since objectui#6010; three spec text
753
+ sites still said otherwise until #12930 re-measured them, and one of those sites
754
+ was the sentence this policy was written against. A legacy artifact carrying a
755
+ legitimate `current_user.role == "admin"` field predicate was therefore reported
756
+ as faulting open — the cry-wolf failure the notice is explicitly built to avoid,
757
+ and the one that trains operators to ignore the channel.
758
+
759
+ The vocabulary is now per surface, which is what the contract actually says:
760
+
761
+ - **Field-level** (`BOUND_FORM_FIELD_PREDICATE_ROOTS`): the shared base plus
762
+ `current_user`, `user`, `ctx`, `os`. Silent on all of them.
763
+ - **Section-level** (`BOUND_FORM_VIEW_PREDICATE_ROOTS`, unchanged in name and
764
+ value): the base alone. `current_user` is still flagged there — the section
765
+ docblock states it is unbound at that level and faults open.
766
+
767
+ Two limits of the field binding deliberately do **not** change the answer: it is
768
+ a rendering rule rather than authorization (an authoring hazard, not a
769
+ version-drift one), and the scope is empty on the console's public `/f/:slug`
770
+ route (equally true of a freshly built current artifact, so it says nothing
771
+ about the artifact's era — the only thing this notice claims to detect).
772
+
773
+ The emitted warn line now prints the bound roots **per surface, and only for the
774
+ surfaces the findings implicate**, so an operator is never shown a rule their
775
+ artifact has no instance of. Findings carry a `surface` field.
776
+
777
+ `unboundRootsInCelSource` takes the vocabulary as an optional second argument;
778
+ its default is unchanged (the stricter base), so existing callers behave exactly
779
+ as before.
780
+ - f7be03f: fix(metadata): `getViewsByObject()` expands aggregated view containers instead of answering empty (#13913)
781
+
782
+ `MetadataManager.getViewsByObject()` reads `this.list('view')` — the manager's
783
+ own registry + loader store, which is a completely different store from the
784
+ `sys_metadata` rows the REST route (`GET /meta/view?object=`) reads through
785
+ `getMetaItems`. #13407 taught that route to expand a runtime-authored aggregated
786
+ `defineView` container inline; this exit never called it and had no equivalent
787
+ step, so a container the REST route now serves still answered **empty** here —
788
+ for every internal/SDK caller that uses this entry point rather than the route.
789
+
790
+ Getting the container into the store was never enough on its own: the filter
791
+ also requires `viewKind`, and a container has none. Relaxing that requirement
792
+ would answer with the container itself as a view — the behaviour #7163 ruled
793
+ wrong — so the repair adds the container's **expansion**, whose items each carry
794
+ the `viewKind` + `object` pair this filter has always tested. The filter is
795
+ untouched; it reads the top-level `object` exactly as `ViewSchema.object`
796
+ declares.
797
+
798
+ The expansion is registry-free and per-read, mirroring #13407's choice at the
799
+ other exit and for the same reason: the registry is process-wide, so a read must
800
+ not graft rows into it. Already-present names win, so a container whose expanded
801
+ ViewItems were registered by a source registrar (the ObjectQL boot loop, the
802
+ artifact/HMR loader) still answers with those registered, fully-enriched items
803
+ and gains nothing new.
804
+
805
+ The object-derivation chain (`object` → `list.data.object` → `form.data.object`
806
+ → the row's own `name`) now has one spelling for this package, in the new
807
+ `view-container-expansion.ts`, rather than a third private copy to fall behind.
808
+ - e7f56d6: Make the settings engine facade and the metadata database loader bind the row
809
+ they resolved, not a row the payload names
810
+
811
+ Two ingresses resolved an authoritative row id and then folded it into the
812
+ write payload with the **losing** spread order — `{ id, ...data }` — so a
813
+ caller-supplied `data.id` spread over the id the ingress had just resolved and
814
+ silently retargeted the write:
815
+
816
+ - `wrapEngineAsSettingsEngine`'s by-id `update` branch
817
+ (`@objectstack/service-settings`), whose id comes from the caller's
818
+ `where.id`.
819
+ - `DatabaseLoader._update` (`@objectstack/metadata`), whose id arrives as a
820
+ separate parameter every caller resolves first (`existing.id`, from the read
821
+ immediately above).
822
+
823
+ Both now spell it `{ ...data, id }` — the operation's id **after** the spread,
824
+ so it wins. That is the convention the repo's other two ingresses already
825
+ document: `rest-server.ts`'s batch update arm ("the operation's id AFTER the
826
+ spread, so it wins") and `protocol.updateData`'s #6479 fix
827
+ (`{ ...request.data, id: request.id }`).
828
+
829
+ **No wrong write is known to have been reachable.** Both sites' current callers
830
+ build fresh field literals and never put an `id` inside `data`, so this is
831
+ hardening a fragile pattern rather than repairing a measured defect. What makes
832
+ it worth the three characters is that neither site can be caught downstream:
833
+ both pass **no `where`** to the engine, so the payload is the only id the engine
834
+ ever sees, and the engine's conflicting-id refusal (`UPDATE_ID_MISMATCH`, 400)
835
+ needs two disagreeing declarations before it can fire. The fold is the entire
836
+ trust boundary at both sites, and it is one refactor — a caller handing back a
837
+ row copy, and rows carry `id` — from the #6479 shape.
838
+
839
+ Both are pinned with a payload whose `id` names a **different** row than the
840
+ one the ingress resolved, asserting the resolved row is still the row bound. A
841
+ pin exercising a payload without an `id` would have passed against both
842
+ spellings. The doubles answer "which row does this bind?" with the producer's
843
+ own `assertEngineUpdateDispatch`, so they cannot be kinder about it than a
844
+ running server.
845
+ - 41aa979: Artifact/HMR loader: read a view container's own top-level `object` field
846
+
847
+ `MetadataPlugin`'s artifact/HMR registrar derived which object a `defineView`
848
+ container binds to by walking `list.data.object` then `form.data.object`, and
849
+ never consulted the container's own top-level `object` — the field
850
+ `ViewSchema.object` documents as "how a stack-level `views: [...]` entry says
851
+ which object its views belong to; read by `getViewsByObject()` /
852
+ `GET /meta/view?object=`".
853
+
854
+ A package-shipped `defineView({ object: 'crm_lead', list: { columns: [...] } })`
855
+ therefore registered nothing at all through this path: the container was dropped
856
+ before registration, so no expanded `crm_lead.<key>` ViewItems were produced and
857
+ `getViewsByObject('crm_lead')` / `GET /meta/view?object=crm_lead` answered empty
858
+ for it. Both derivation sites now call the package's single spelling of that
859
+ derivation (`deriveViewContainerObject`), which consults the container's own
860
+ `object` first and keeps the existing `list.data.object` → `form.data.object` →
861
+ row-name fallback unchanged for every container written before that field was
862
+ read here. This is the same order #13407 settled at the runtime door.
863
+ - a7002ce: Canonicalise driver-materialised timestamps at the metadata adapter boundaries
864
+
865
+ `MetadataItem.authoredAt` is declared `z.string()` ('ISO-8601 timestamp') and
866
+ `MetadataStats.mtime` is declared `z.string().datetime()`, but three producers
867
+ adapted a driver row into those declared types without converting the value.
868
+ `created_at` / `updated_at` are builtin audit columns and `recorded_at` is a
869
+ declared `Field.datetime`; `SqlDriver#formatOutput` repairs both only inside its
870
+ `if (this.isSqlite)` arm, so on Postgres and MySQL a JS `Date` landed in a field
871
+ every consumer reads as a `string`.
872
+
873
+ `SysMetadataRepository#get` / `#getByHash` and `DatabaseLoader#stat` now emit
874
+ canonical ISO-8601 text on every dialect, matching the sibling producers that
875
+ already spelled it correctly. Values that were already canonical (SQLite) pass
876
+ through byte-identically.
877
+ - 77a532d: Five metadata adapter boundaries now emit the ISO-8601 string their declared type promises
878
+ when the driver hands them a JS `Date`, instead of asserting `as string` over it
879
+
880
+ `MetadataRecord.createdAt` / `.updatedAt` and `MetadataHistoryRecord.recordedAt` are
881
+ declared `z.string().datetime()`, and `MetadataEvent.ts` is declared `z.string()`. Four
882
+ producers in `DatabaseLoader` (`rowToRecord`, `getHistoryRecord`, `queryHistory`) and one
883
+ in `SysMetadataRepository` (`rowToEvent`) reached those fields through an unchecked
884
+ `row.<column> as string` cast, which is an assertion about a driver row rather than a
885
+ measurement of one — so nothing type-checked and nothing reported it.
886
+
887
+ On Postgres and MySQL the assertion is false for both column classes involved.
888
+ `SqlDriver#formatOutput` repairs the builtin audit columns and folds declared
889
+ `Field.datetime` columns only inside its `if (this.isSqlite)` arm, and
890
+ `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp` deliberately untouched
891
+ because those are instants. A column being declared `Field.datetime` therefore does **not**
892
+ protect it: on the production default driver both classes come out of the record read door
893
+ as a `Date`, and `.datetime()` is a refinement a `Date` fails outright. Nothing has failed
894
+ yet only because no production path parses these values today.
895
+
896
+ The repair is producer-side, at the adapter boundary that asserts the declared type — not
897
+ a tolerant fallback in a consumer, and not a change at the driver's read door, which would
898
+ reverse a deliberate driver decision. Callers keep their existing behaviour for every other
899
+ shape: an already-canonical SQLite string passes through byte-identically, an absent column
900
+ still yields `undefined` so each caller's `?? <default>` chain means what it meant, and an
901
+ Invalid `Date` is handed through unchanged rather than converted, because what the shared
902
+ canonical-ISO spelling should do with that one input is still being decided.
903
+ - 93940d4: `DatabaseLoader`'s private driver-path update helper is typed with the `null` arm `IDataDriver.update()` now declares; both of its callers discard the result. No runtime behaviour changes.
904
+ - 8225248: fix(metadata): drop the leftover boot-time debug probe from `MetadataPlugin.init` (#14527)
905
+
906
+ `MetadataPlugin.init()` printed a bare `console.log` immediately after
907
+ `ctx.registerService('metadata', this.manager)`, reporting
908
+ `typeof this.manager.getRegisteredTypes`. Nothing gated it on `NODE_ENV`, a debug
909
+ flag or a logger level, so every kernel boot that installs `MetadataPlugin` wrote
910
+ it to stdout: the CLI, the dev server and any embedding host alike.
911
+
912
+ What it printed was a probe, not information. `getRegisteredTypes` is a method the
913
+ class declares statically, so the `typeof` it reported cannot vary in a way an
914
+ operator could act on, and a repo-wide search finds no reader of the string. The
915
+ `ctx.logger.info('Initializing Metadata Manager', ...)` call three lines above
916
+ already announces the same lifecycle step through the plugin's own logger, carrying
917
+ the fields that are actually actionable (`root`, `watch`, `artifactSource`). Deleted
918
+ rather than demoted to `ctx.logger.debug` on that reading: once the reported fact is
919
+ statically known and unread, there is no shape check left worth keeping.
920
+
921
+ Observable change: one fewer line on stdout at boot. No API, no types, no behaviour
922
+ beyond the removed print. On the two paths where a stray stdout line would break a
923
+ parser — `--json` payloads and the `os serve` protocol channel — the CLI already
924
+ reserves stdout by redirecting to stderr, so nothing downstream was relying on this
925
+ line's presence or its absence.
926
+
927
+ The file's five other `console.*` calls (the dev HMR path) are deliberately
928
+ untouched: each carries its own marker, and the one that prints on every
929
+ non-development boot carries a written rationale for doing so.
930
+ - 0ae9e1e: fix(metadata): `migrateProjectIdToEnvironmentId`'s raw-driver guard stated its instruction sentence twice (#13219)
931
+
932
+ An operator who called `migrateProjectIdToEnvironmentId` with a driver that has
933
+ no `raw()` was refused correctly, but read the same remedy twice in one message:
934
+
935
+ ```
936
+ migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms.
937
+ ```
938
+
939
+ The sentence was concatenated twice, a copy-paste artifact — the sibling
940
+ `migrateEnvIdToProjectId` carries the correct single-sentence form of the
941
+ identical guard. Cosmetic and operator-facing only: the guard fires on exactly
942
+ the same condition, the remedy it names is unchanged, and nothing parses the
943
+ message. The duplicate line is deleted; the surviving sentence keeps the
944
+ trailing space that separates it from the one naming the conforming drivers.
945
+
946
+ The refusal case in the package's tests now pins the properties — the
947
+ instruction appears exactly once, no sentence runs into the next, and the
948
+ supporting sentence is still present — rather than substring-matching the
949
+ message, which could not see a second copy and so passed either way.
950
+ - ef76342: fix(metadata): stop `migrateProjectIdToEnvironmentId` renaming into a column no declaration knows about (#13205)
951
+
952
+ `AFFECTED_TABLES` was a hand-written list, and it outlived the declarations it
953
+ described. The branch/project-removal amendment (M1) took `environment_id` out
954
+ of `sys_metadata_history`'s declaration in `@objectstack/metadata-core`; the
955
+ migration kept naming that table. Its only guard asks whether `project_id` is
956
+ present **physically** (`_columnExists`) — which says nothing about the target
957
+ column being **declared** — so against any database whose physical
958
+ `sys_metadata_history` still carried the pre-v5 column, the migration renamed it
959
+ to `environment_id`: a fresh orphan column that no declaration, no `syncSchema`
960
+ and no reader knows about.
961
+
962
+ The list is now **derived from the declarations** rather than restated beside
963
+ them. A candidate table is migrated only if its current declaration carries the
964
+ target column, so the two cannot drift apart again — the derivation and the
965
+ declaration are the same fact. `@objectstack/metadata-core` was already a
966
+ dependency of this package, so this adds no dependency edge.
967
+
968
+ A candidate that does not declare the target column is now **reported** as
969
+ `status: 'skipped_not_declared'` (an additive member of the result union)
970
+ instead of silently vanishing from the result array: an operator reading the
971
+ results can tell "considered and deliberately skipped" from "forgotten again",
972
+ which is the state this defect started in.
973
+
974
+ The sibling `migrateEnvIdToProjectId` is deliberately left alone: its target
975
+ (`project_id`) is an intermediate column that no current declaration carries by
976
+ design, so the "target must be declared" rule is sound only for the terminal
977
+ migration in the chain.
978
+
979
+ No behaviour changes for `sys_metadata`, whose declaration does carry
980
+ `environment_id`: it is renamed exactly as before.
981
+ - 2a18117: fix(metadata): every migration in `@objectstack/metadata/migrations` refused every driver this repo ships (#14023)
982
+
983
+ All four helpers exported from `@objectstack/metadata/migrations` guarded on —
984
+ and drove through — `driver.raw(sql, bindings?)`. **No data driver in this repo
985
+ defines `raw`.** `SqlDriver` keeps its knex handle `protected` and declares no
986
+ `raw` member, and `SqliteWasmDriver` inherits that; the only `raw(` member
987
+ anywhere outside a test double is an HTTP harness in `packages/verify` whose
988
+ signature is `(path, init)`. So an operator who passed their platform driver was
989
+ refused by all four:
990
+
991
+ ```
992
+ migrateSysNotificationToEvent({ driver, data }) -> { status: 'error', migrated: 0 }
993
+ ```
994
+
995
+ The failure was quiet in the shape that matters. `migrateSysNotificationToEvent`
996
+ *returns* `{ status: 'error' }` rather than throwing, and the message blamed the
997
+ caller's driver for lacking a method instead of saying the migration had not
998
+ run — so someone following the ADR-0030 cut-over runbook, which names this call
999
+ as the supported way to preserve users' existing bell notifications, would read
1000
+ it as a problem with their own driver.
1001
+
1002
+ It was not only an operator-facing path. `DatabaseLoader` calls
1003
+ `migrateProjectIdToEnvironmentId(driver)` on bootstrap with a real driver, at
1004
+ two call sites, each wrapped in a catch — so the v5.0 `project_id` ->
1005
+ `environment_id` forward migration threw and was swallowed on every boot.
1006
+
1007
+ The four helpers now resolve their raw-SQL entry point through one shared
1008
+ resolver (`src/migrations/driver-exec.ts`) that tries `execute` first and falls
1009
+ back to `raw`. `execute` goes first because it is the surface the contract
1010
+ declares: `IDataDriver` (`@objectstack/spec/contracts`) declares
1011
+ `execute(command, parameters?, options?)` **non-optionally**, with bound
1012
+ parameters as the second positional argument — exactly the shape `raw(sql,
1013
+ bindings?)` was being called in — and has never declared `raw`. `raw` is kept as
1014
+ a fallback so a host or third-party driver that does define it keeps working;
1015
+ nothing that worked before stops working, and the refusal now fires only for a
1016
+ driver offering neither surface.
1017
+
1018
+ Two sibling directories already resolved both surfaces instead of assuming one,
1019
+ in opposite orders (`metadata-protocol`'s `partial-index-probe` tries `raw`
1020
+ first, its `seed-tenancy-backfill` tries `execute` first, and `protocol.ts`'s
1021
+ `ensureOverlayIndex` is a third). One operation with three implementations and
1022
+ two behaviours resolves to the declaration-bound side, which is why this
1023
+ directory adopts `execute`-first uniformly rather than copying either precedent.
1024
+
1025
+ The refusal message now names both surfaces. It keeps the properties pinned
1026
+ after the doubled-sentence defect: the remedy is stated exactly once, the
1027
+ sentences stay separated, and a conforming driver is still named.
1028
+
1029
+ Tests: every pre-existing case in this directory built its own double carrying a
1030
+ `raw` method — including the case asserting the guard fires — so the suite
1031
+ pinned the guard's wording while never exercising a driver the platform ships.
1032
+ Swapping `raw` for `execute` in the helpers and in the doubles would have moved
1033
+ that hole rather than closed it. A new `real-driver-exec-surface.test.ts` drives
1034
+ all four migrations through a real `SqliteWasmDriver` against real in-process
1035
+ SQLite, asserting the physical schema rather than the returned status, and pins
1036
+ the surface reality the file exists for: the real driver has no `raw` and does
1037
+ have `execute`.
1038
+ - c9d4de3: fix(metadata): `migrateSysNotificationToEvent` writes canonical ISO timestamps, not `Date.prototype.toString` (#13998)
1039
+
1040
+ `selectLegacyRows` reads the legacy `sys_notification` table through
1041
+ `driver.raw`/`execute` — a door that does not run `formatOutput`, so none of its
1042
+ repairs apply. On SQLite the legacy stamps come back as canonical ISO text and
1043
+ `String(row.created_at)` is the identity. On Postgres and MySQL an instant
1044
+ column materialises as a JS `Date`, so the migration wrote
1045
+
1046
+ ```
1047
+ Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)
1048
+ ```
1049
+
1050
+ into `created_at` on the new `sys_inbox_message` row and into `created_at` / `at`
1051
+ on the new `sys_notification_receipt` row — whole seconds in the **migrating
1052
+ host's** zone with the milliseconds dropped, or a value no dialect's timestamp
1053
+ grammar accepts at all. The migration is one-way, so that spelling is what the
1054
+ platform would carry afterwards.
1055
+
1056
+ Both stamps are now canonicalised at the migration, matching the repo's existing
1057
+ correct form: a `Date` is rendered with `toISOString()`, ISO text passes through
1058
+ untouched. Neither column could be repaired further upstream — `created_at` is a
1059
+ builtin audit column that `formatOutput` repairs only in its `if (this.isSqlite)`
1060
+ arm, and `read_at` is a legacy column ADR-0030 removed from the object, so it is
1061
+ not a declared `Field.datetime` either and no coercion could ever reach it.
1062
+
1063
+ Pinned with a hand-made `Date` driven through the migration's read path under a
1064
+ forced process zone, which is what breaks the SQLite identity that kept the
1065
+ existing cases green while the defect was live.
1066
+ - a23603e: fix(runtime,metadata): the default local environment id is `env_local`, not `proj_local` (#13366)
1067
+
1068
+ The v5.0 `project` to `environment` rename changed the default local environment
1069
+ id and shipped that change on the surfaces most people meet: `packages/cli`'s
1070
+ `CHANGELOG.md` records "Default local env id: `proj_local` -> `env_local`", the
1071
+ `os dev` / `os start` / `os serve` commands emit `env_local`, and
1072
+ `content/docs/deployment/cli.mdx` documents `env_local` as the default. Two
1073
+ sites never received it and kept stamping `proj_local`.
1074
+
1075
+ FROM: `createStandaloneStack()` — with no `environmentId` in its config and no
1076
+ `OS_ENVIRONMENT_ID` in the environment — stamped `proj_local` on the kernel it
1077
+ composed, and `MetadataPlugin` used `proj_local` to fill the environment-artifact
1078
+ validation envelope for a bare definition.
1079
+
1080
+ TO: both stamp `env_local`.
1081
+
1082
+ WHO SEES IT. Two audiences, both on the DEFAULT path — no `environmentId` in
1083
+ the config and no `OS_ENVIRONMENT_ID` in the environment:
1084
+
1085
+ 1. a host that calls `createStandaloneStack` / `createDefaultHostConfig`
1086
+ **directly**;
1087
+ 2. a **bare `os serve`** — one not spawned by `os dev` / `os start`. Those two
1088
+ commands export `OS_ENVIRONMENT_ID=env_local` into the child process, which
1089
+ the fallback yields to, so a boot they start never reached the changed line.
1090
+ `os serve` sets no such variable for its own boot: it only READS one to name
1091
+ the runtime state file. So a bare `os serve` used to run a kernel stamped
1092
+ `proj_local` while publishing `runtime.env_local.json` beside it; the two now
1093
+ agree.
1094
+
1095
+ Where the id is observable — row scoping in `ObjectQLPlugin`, the
1096
+ `X-Environment-Id` header, `sys_metadata.environment_id` — such an embedder now
1097
+ sees `env_local` where it saw `proj_local`, so an install with rows already
1098
+ written under the old id should set `environmentId: 'proj_local'` (or
1099
+ `OS_ENVIRONMENT_ID=proj_local`) explicitly to keep them addressed. That escape
1100
+ hatch is unchanged and still wins over the default.
1101
+
1102
+ NOT CHANGED, deliberately: `@objectstack/cloud-connection` still treats BOTH
1103
+ spellings as the local sentinel, so a persisted `OS_ENVIRONMENT_ID=proj_local`
1104
+ config keeps being recognised as local rather than presented to the control
1105
+ plane as a cloud environment id; and `package-state-store`'s separate `'default'`
1106
+ fallback keeps its own spelling, because renaming it would re-key persisted
1107
+ package-disable state files.
1108
+ - Updated dependencies [809d417]
1109
+ - Updated dependencies [387e231]
1110
+ - Updated dependencies [f794e4e]
1111
+ - Updated dependencies [cae2169]
1112
+ - Updated dependencies [b812a54]
1113
+ - Updated dependencies [2d4fa75]
1114
+ - Updated dependencies [0e4e51b]
1115
+ - Updated dependencies [e84bbf6]
1116
+ - Updated dependencies [effae80]
1117
+ - Updated dependencies [efb3513]
1118
+ - Updated dependencies [d62f990]
1119
+ - Updated dependencies [c45d8e6]
1120
+ - Updated dependencies [2e3e8c7]
1121
+ - Updated dependencies [e621291]
1122
+ - Updated dependencies [655b106]
1123
+ - Updated dependencies [40a93b5]
1124
+ - Updated dependencies [101ad2c]
1125
+ - Updated dependencies [d5b330d]
1126
+ - Updated dependencies [dda969c]
1127
+ - Updated dependencies [1f45690]
1128
+ - Updated dependencies [277948f]
1129
+ - Updated dependencies [8bdd955]
1130
+ - Updated dependencies [54e2d36]
1131
+ - Updated dependencies [b745157]
1132
+ - Updated dependencies [f3bbbef]
1133
+ - Updated dependencies [4f24e9d]
1134
+ - Updated dependencies [e27583e]
1135
+ - Updated dependencies [4bd6faa]
1136
+ - Updated dependencies [86cbe37]
1137
+ - Updated dependencies [6a180e4]
1138
+ - Updated dependencies [474242f]
1139
+ - Updated dependencies [63cd487]
1140
+ - Updated dependencies [bd4aa4e]
1141
+ - Updated dependencies [803eaab]
1142
+ - Updated dependencies [f8e8f03]
1143
+ - Updated dependencies [983edf1]
1144
+ - Updated dependencies [eae824e]
1145
+ - Updated dependencies [f6fa22c]
1146
+ - Updated dependencies [8a483b3]
1147
+ - Updated dependencies [3bc2e38]
1148
+ - Updated dependencies [97bcd99]
1149
+ - Updated dependencies [df59de0]
1150
+ - Updated dependencies [96e25a8]
1151
+ - Updated dependencies [f75a38a]
1152
+ - Updated dependencies [7a25e7d]
1153
+ - Updated dependencies [1fa05a6]
1154
+ - Updated dependencies [c85a265]
1155
+ - Updated dependencies [dcb10a5]
1156
+ - Updated dependencies [773a999]
1157
+ - Updated dependencies [35dffea]
1158
+ - Updated dependencies [d8024f0]
1159
+ - Updated dependencies [8120808]
1160
+ - Updated dependencies [776a098]
1161
+ - Updated dependencies [5060877]
1162
+ - Updated dependencies [4f6325d]
1163
+ - Updated dependencies [52954c0]
1164
+ - Updated dependencies [2aa8456]
1165
+ - Updated dependencies [d23ebb9]
1166
+ - Updated dependencies [93809a3]
1167
+ - Updated dependencies [7c0d0c3]
1168
+ - Updated dependencies [daae7aa]
1169
+ - Updated dependencies [8dc22d6]
1170
+ - Updated dependencies [fa5d137]
1171
+ - Updated dependencies [a392dbf]
1172
+ - Updated dependencies [279431e]
1173
+ - Updated dependencies [948dd6b]
1174
+ - Updated dependencies [3b4c56c]
1175
+ - Updated dependencies [ae8edd2]
1176
+ - Updated dependencies [e25403c]
1177
+ - Updated dependencies [a81aa9d]
1178
+ - Updated dependencies [64baa68]
1179
+ - Updated dependencies [9fa70d7]
1180
+ - Updated dependencies [09db64a]
1181
+ - Updated dependencies [92916e7]
1182
+ - Updated dependencies [a84f3ea]
1183
+ - Updated dependencies [f2eaae8]
1184
+ - Updated dependencies [56c093c]
1185
+ - Updated dependencies [c09451b]
1186
+ - Updated dependencies [ba64877]
1187
+ - Updated dependencies [e7191ce]
1188
+ - Updated dependencies [7345308]
1189
+ - Updated dependencies [79b6a22]
1190
+ - Updated dependencies [30d96ab]
1191
+ - Updated dependencies [f658793]
1192
+ - Updated dependencies [0fd4899]
1193
+ - Updated dependencies [c95ad19]
1194
+ - Updated dependencies [e58ea8b]
1195
+ - Updated dependencies [4a17645]
1196
+ - Updated dependencies [3795c5f]
1197
+ - Updated dependencies [8ab926b]
1198
+ - Updated dependencies [7317cf2]
1199
+ - Updated dependencies [e25e839]
1200
+ - Updated dependencies [5997207]
1201
+ - Updated dependencies [8b13cc8]
1202
+ - Updated dependencies [00d8f65]
1203
+ - Updated dependencies [4a4a35d]
1204
+ - Updated dependencies [86e765a]
1205
+ - Updated dependencies [1d7e76a]
1206
+ - Updated dependencies [53dc739]
1207
+ - Updated dependencies [fd289be]
1208
+ - Updated dependencies [03bf7b1]
1209
+ - Updated dependencies [f90e820]
1210
+ - Updated dependencies [18d816a]
1211
+ - Updated dependencies [e8bd715]
1212
+ - Updated dependencies [b91c351]
1213
+ - Updated dependencies [a28a3c0]
1214
+ - Updated dependencies [200d255]
1215
+ - Updated dependencies [2852acc]
1216
+ - Updated dependencies [daeaaf9]
1217
+ - Updated dependencies [c459da6]
1218
+ - Updated dependencies [e914733]
1219
+ - Updated dependencies [f887e52]
1220
+ - Updated dependencies [881f8d8]
1221
+ - Updated dependencies [3bfa1e6]
1222
+ - Updated dependencies [0a8ebf3]
1223
+ - Updated dependencies [901355c]
1224
+ - Updated dependencies [34ce8e7]
1225
+ - Updated dependencies [33681ea]
1226
+ - Updated dependencies [bfe13c8]
1227
+ - Updated dependencies [0fb3044]
1228
+ - Updated dependencies [4635f3e]
1229
+ - Updated dependencies [fd289be]
1230
+ - Updated dependencies [ee3595c]
1231
+ - Updated dependencies [09b4f4e]
1232
+ - Updated dependencies [b2eab95]
1233
+ - Updated dependencies [93940d4]
1234
+ - Updated dependencies [3a04b01]
1235
+ - Updated dependencies [45b9051]
1236
+ - Updated dependencies [3954fb7]
1237
+ - Updated dependencies [4805b56]
1238
+ - Updated dependencies [b9e9227]
1239
+ - Updated dependencies [d395692]
1240
+ - Updated dependencies [5894d30]
1241
+ - Updated dependencies [a3765f6]
1242
+ - Updated dependencies [2d5cee3]
1243
+ - Updated dependencies [e22158f]
1244
+ - Updated dependencies [7404925]
1245
+ - Updated dependencies [0c2334f]
1246
+ - Updated dependencies [778c59f]
1247
+ - Updated dependencies [d2619fd]
1248
+ - Updated dependencies [af56546]
1249
+ - Updated dependencies [6acb11a]
1250
+ - Updated dependencies [33c5fd3]
1251
+ - Updated dependencies [20b0fdb]
1252
+ - Updated dependencies [905019b]
1253
+ - Updated dependencies [a286411]
1254
+ - Updated dependencies [98c0d33]
1255
+ - Updated dependencies [368a82e]
1256
+ - Updated dependencies [a3d5724]
1257
+ - Updated dependencies [93ea19b]
1258
+ - Updated dependencies [9ee2dcf]
1259
+ - Updated dependencies [8cb96ec]
1260
+ - Updated dependencies [8f10a79]
1261
+ - Updated dependencies [6269a55]
1262
+ - Updated dependencies [a17da05]
1263
+ - Updated dependencies [a8c00e2]
1264
+ - Updated dependencies [22e5236]
1265
+ - Updated dependencies [0fb8760]
1266
+ - Updated dependencies [e5ce2ed]
1267
+ - Updated dependencies [be21955]
1268
+ - Updated dependencies [bc56e18]
1269
+ - Updated dependencies [be21955]
1270
+ - Updated dependencies [a9ee989]
1271
+ - Updated dependencies [4d0d944]
1272
+ - Updated dependencies [15d58db]
1273
+ - Updated dependencies [d63b014]
1274
+ - Updated dependencies [9abe4e4]
1275
+ - Updated dependencies [2cc7122]
1276
+ - Updated dependencies [50d6c92]
1277
+ - Updated dependencies [15d55fb]
1278
+ - Updated dependencies [9e0ba21]
1279
+ - Updated dependencies [a07a831]
1280
+ - Updated dependencies [311433f]
1281
+ - Updated dependencies [3e5ad08]
1282
+ - Updated dependencies [9abe4e4]
1283
+ - Updated dependencies [b7131f3]
1284
+ - Updated dependencies [e5812fa]
1285
+ - Updated dependencies [7085f90]
1286
+ - Updated dependencies [dee4dd4]
1287
+ - Updated dependencies [ce7e497]
1288
+ - Updated dependencies [51ecb2f]
1289
+ - Updated dependencies [9086761]
1290
+ - Updated dependencies [f6344e7]
1291
+ - Updated dependencies [42a117b]
1292
+ - Updated dependencies [1401ae7]
1293
+ - Updated dependencies [4297fe7]
1294
+ - Updated dependencies [e398863]
1295
+ - Updated dependencies [d16df74]
1296
+ - Updated dependencies [d79c602]
1297
+ - Updated dependencies [f11fc61]
1298
+ - Updated dependencies [e808890]
1299
+ - Updated dependencies [8f79379]
1300
+ - Updated dependencies [e6ca40e]
1301
+ - Updated dependencies [0c77ea4]
1302
+ - Updated dependencies [52954c0]
1303
+ - Updated dependencies [89eb997]
1304
+ - Updated dependencies [7131f12]
1305
+ - Updated dependencies [aa5994e]
1306
+ - Updated dependencies [be93457]
1307
+ - Updated dependencies [a65db76]
1308
+ - Updated dependencies [2cf5a96]
1309
+ - Updated dependencies [15eb2c9]
1310
+ - Updated dependencies [5691b07]
1311
+ - Updated dependencies [2a6122b]
1312
+ - Updated dependencies [225e769]
1313
+ - Updated dependencies [8af88dd]
1314
+ - Updated dependencies [fb5fbb8]
1315
+ - Updated dependencies [d7b3963]
1316
+ - Updated dependencies [33184fd]
1317
+ - Updated dependencies [7c41693]
1318
+ - Updated dependencies [b72db01]
1319
+ - Updated dependencies [dce5cd4]
1320
+ - Updated dependencies [9688f58]
1321
+ - Updated dependencies [556ebc1]
1322
+ - Updated dependencies [177ebdc]
1323
+ - Updated dependencies [8d237b4]
1324
+ - Updated dependencies [2d2e6f0]
1325
+ - Updated dependencies [2d8dd8d]
1326
+ - Updated dependencies [22d573e]
1327
+ - Updated dependencies [b5a2398]
1328
+ - Updated dependencies [348860c]
1329
+ - Updated dependencies [5383fa6]
1330
+ - Updated dependencies [5b3ff63]
1331
+ - Updated dependencies [1a6a19c]
1332
+ - Updated dependencies [064d484]
1333
+ - Updated dependencies [527e050]
1334
+ - Updated dependencies [dd33bf9]
1335
+ - Updated dependencies [4cb2a90]
1336
+ - Updated dependencies [74a7804]
1337
+ - Updated dependencies [53d3689]
1338
+ - Updated dependencies [b3a63d3]
1339
+ - Updated dependencies [49f0dcf]
1340
+ - Updated dependencies [033a34c]
1341
+ - Updated dependencies [4d25d22]
1342
+ - Updated dependencies [1ffee51]
1343
+ - Updated dependencies [5ae4303]
1344
+ - Updated dependencies [ece4dad]
1345
+ - Updated dependencies [e9b377e]
1346
+ - Updated dependencies [146f448]
1347
+ - Updated dependencies [735f5c7]
1348
+ - Updated dependencies [a7e18de]
1349
+ - Updated dependencies [366f895]
1350
+ - Updated dependencies [dc75ba8]
1351
+ - Updated dependencies [cce0aa9]
1352
+ - Updated dependencies [e764507]
1353
+ - Updated dependencies [cff17af]
1354
+ - Updated dependencies [39404f3]
1355
+ - Updated dependencies [ca1965f]
1356
+ - Updated dependencies [8619f95]
1357
+ - Updated dependencies [b706af9]
1358
+ - Updated dependencies [db8c288]
1359
+ - Updated dependencies [0e5fe7f]
1360
+ - Updated dependencies [add4360]
1361
+ - Updated dependencies [e0abc38]
1362
+ - Updated dependencies [fc9ba76]
1363
+ - Updated dependencies [1272f0a]
1364
+ - Updated dependencies [0f94cc7]
1365
+ - Updated dependencies [a11c1a5]
1366
+ - Updated dependencies [71f9cd1]
1367
+ - Updated dependencies [ee17d86]
1368
+ - Updated dependencies [cdbd920]
1369
+ - Updated dependencies [18c432e]
1370
+ - Updated dependencies [3c418c4]
1371
+ - Updated dependencies [fa8715a]
1372
+ - Updated dependencies [a933ed7]
1373
+ - Updated dependencies [b3ca463]
1374
+ - Updated dependencies [a933ed7]
1375
+ - Updated dependencies [0d4a6a8]
1376
+ - Updated dependencies [518d5e5]
1377
+ - Updated dependencies [6643ba1]
1378
+ - Updated dependencies [eeba2ef]
1379
+ - Updated dependencies [ec4c4d2]
1380
+ - Updated dependencies [424f73c]
1381
+ - Updated dependencies [cccbe51]
1382
+ - Updated dependencies [a8d6b1d]
1383
+ - Updated dependencies [e4a7695]
1384
+ - Updated dependencies [87075b1]
1385
+ - Updated dependencies [fc58a99]
1386
+ - Updated dependencies [14cfc00]
1387
+ - Updated dependencies [1c6f7b4]
1388
+ - Updated dependencies [e854a53]
1389
+ - Updated dependencies [dfebfc8]
1390
+ - Updated dependencies [598b7ec]
1391
+ - Updated dependencies [d028b37]
1392
+ - Updated dependencies [f7b25c5]
1393
+ - Updated dependencies [122ef38]
1394
+ - Updated dependencies [4a37870]
1395
+ - Updated dependencies [428f9b2]
1396
+ - Updated dependencies [aa7ff56]
1397
+ - Updated dependencies [811a3c2]
1398
+ - Updated dependencies [1401ae7]
1399
+ - Updated dependencies [2fd3f1c]
1400
+ - Updated dependencies [c41b42e]
1401
+ - Updated dependencies [d41d166]
1402
+ - Updated dependencies [c4db311]
1403
+ - Updated dependencies [750fff5]
1404
+ - Updated dependencies [c19035e]
1405
+ - Updated dependencies [ececf7a]
1406
+ - Updated dependencies [d173125]
1407
+ - Updated dependencies [8eeca27]
1408
+ - Updated dependencies [8425c17]
1409
+ - Updated dependencies [a5ef1d8]
1410
+ - Updated dependencies [87ad30c]
1411
+ - Updated dependencies [772d5de]
1412
+ - Updated dependencies [ce80ec2]
1413
+ - Updated dependencies [b372318]
1414
+ - Updated dependencies [97a2263]
1415
+ - Updated dependencies [29d0676]
1416
+ - Updated dependencies [0169d49]
1417
+ - Updated dependencies [6bd3231]
1418
+ - Updated dependencies [d2b5ba8]
1419
+ - Updated dependencies [b799ac5]
1420
+ - Updated dependencies [8f74307]
1421
+ - Updated dependencies [d23dc08]
1422
+ - Updated dependencies [644ad50]
1423
+ - Updated dependencies [9735662]
1424
+ - Updated dependencies [4d5b4f8]
1425
+ - Updated dependencies [3e8f5b0]
1426
+ - Updated dependencies [5d16379]
1427
+ - Updated dependencies [0da7cd2]
1428
+ - Updated dependencies [28a5c3e]
1429
+ - Updated dependencies [4bc18e5]
1430
+ - Updated dependencies [9f57f1e]
1431
+ - @objectstack/spec@17.3.0
1432
+ - @objectstack/platform-objects@17.3.0
1433
+ - @objectstack/core@17.3.0
1434
+ - @objectstack/types@17.3.0
1435
+ - @objectstack/metadata-core@17.3.0
1436
+ - @objectstack/metadata-fs@17.3.0
1437
+
1438
+ ## 17.2.0
1439
+
1440
+ ### Patch Changes
1441
+
1442
+ - 047ac86: Five `Plugin` implementations now release their resources from `destroy()`, the
1443
+ only teardown hook the kernel calls (#10772).
1444
+
1445
+ `Plugin` (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)` and
1446
+ `destroy?()`. `ObjectKernel.performShutdown()` and `LiteKernel.destroy()` walk
1447
+ the plugins in reverse calling `plugin.destroy()` — and nothing anywhere calls
1448
+ `stop()`, `dispose()`, `close()` or `shutdown()` on a plugin. Each of these five
1449
+ spelled its teardown with one of those names instead, so what it released was
1450
+ still held after `await kernel.shutdown()` had **resolved**:
1451
+
1452
+ | package | class | was spelled | what outlived shutdown |
1453
+ |:--|:--|:--|:--|
1454
+ | `@objectstack/metadata` | `MetadataPlugin` | `stop` (arrow property) | artifact watcher, `manager.dispose()`, repository handle |
1455
+ | `@objectstack/runtime` | `AppPlugin` | `stop` (arrow property) | the `app:unregistered` catalog event, never emitted |
1456
+ | `@objectstack/runtime` | `ExternalValidationPlugin` | `stop` (arrow property) | every armed drift-check `setInterval` |
1457
+ | `@objectstack/plugin-email` | `EmailServicePlugin` | `dispose` | two metadata subscriptions, the SMTP transport, an engine binding |
1458
+ | `@objectstack/plugin-webhooks` | `WebhookOutboxPlugin` | `dispose` | the auto-enqueuer (2 realtime subscriptions + a refresh interval) and two engine hooks |
1459
+
1460
+ `ExternalValidationPlugin` is the one with teeth: it is one of only two `Plugin`
1461
+ implementations in the tree that own `setInterval` directly, it is mounted on
1462
+ the real `os serve` path, and its `stop()`'s only caller anywhere was the class
1463
+ itself re-arming. Measured against a real kernel, its drift checker performed
1464
+ five further reads in the five intervals after a resolved shutdown — the #9371
1465
+ mechanism verbatim. `WebhookOutboxPlugin.dispose()` had **zero** callers in the
1466
+ entire repo, so its teardown had never run in any process at all.
1467
+
1468
+ **Nothing is removed and no signature narrows.** Each old name is retained as a
1469
+ delegating alias, because it is public API of an exported class and an embedder
1470
+ may have learned to call it directly precisely BECAUSE the kernel never did.
1471
+ `stop` stays an arrow property where it was one (so a detached
1472
+ `const { stop } = plugin` keeps working) and stays synchronous on
1473
+ `ExternalValidationPlugin` (so a non-awaiting call site is unaffected). The two
1474
+ `stop(ctx)` aliases widen their parameter to optional.
1475
+
1476
+ One behavioural note for direct callers, since `destroy()` takes no context:
1477
+ `MetadataPlugin.stop(ctx)` and `AppPlugin.stop(ctx)` now use the context
1478
+ captured in `init()` and ignore the argument. In a real composition these are
1479
+ the same object. The visible difference is confined to a plugin whose `init()`
1480
+ never ran — for `MetadataPlugin` a dropped `warn` line, for `AppPlugin` a
1481
+ catalog event that is no longer emitted for an app that was never registered.
1482
+ - Updated dependencies [8f04d9a]
1483
+ - Updated dependencies [6936d07]
1484
+ - Updated dependencies [59eb04d]
1485
+ - Updated dependencies [9f05b7d]
1486
+ - Updated dependencies [3b2af5e]
1487
+ - Updated dependencies [7d2d112]
1488
+ - Updated dependencies [5fa0d72]
1489
+ - Updated dependencies [8cc8401]
1490
+ - Updated dependencies [02b3b07]
1491
+ - Updated dependencies [46644e2]
1492
+ - Updated dependencies [46d34ab]
1493
+ - Updated dependencies [914c413]
1494
+ - Updated dependencies [55809a0]
1495
+ - Updated dependencies [ee2ff45]
1496
+ - Updated dependencies [47cd3ec]
1497
+ - Updated dependencies [52db1d1]
1498
+ - Updated dependencies [5649efb]
1499
+ - Updated dependencies [9d7d2de]
1500
+ - Updated dependencies [c815c50]
1501
+ - Updated dependencies [795ea05]
1502
+ - Updated dependencies [2306a76]
1503
+ - Updated dependencies [e5ea701]
1504
+ - Updated dependencies [26f3588]
1505
+ - Updated dependencies [a40dcc1]
1506
+ - Updated dependencies [def0d3e]
1507
+ - Updated dependencies [8d0bb79]
1508
+ - Updated dependencies [5acb58d]
1509
+ - Updated dependencies [2e3cf95]
1510
+ - Updated dependencies [4c93387]
1511
+ - Updated dependencies [504c8d5]
1512
+ - Updated dependencies [a037f7c]
1513
+ - Updated dependencies [3ee8ddf]
1514
+ - Updated dependencies [16cef97]
1515
+ - Updated dependencies [a79bd35]
1516
+ - Updated dependencies [6ceaa4b]
1517
+ - Updated dependencies [15ea214]
1518
+ - Updated dependencies [de19489]
1519
+ - Updated dependencies [c684d00]
1520
+ - Updated dependencies [923c424]
1521
+ - Updated dependencies [0ab81d1]
1522
+ - Updated dependencies [1ec36b7]
1523
+ - Updated dependencies [5f2e54c]
1524
+ - Updated dependencies [189373b]
1525
+ - Updated dependencies [35ad101]
1526
+ - Updated dependencies [ceb33a9]
1527
+ - Updated dependencies [dccbcec]
1528
+ - Updated dependencies [05bc692]
1529
+ - Updated dependencies [73d9795]
1530
+ - Updated dependencies [8012960]
1531
+ - Updated dependencies [266654d]
1532
+ - Updated dependencies [f34f56b]
1533
+ - Updated dependencies [f399618]
1534
+ - Updated dependencies [75e9301]
1535
+ - Updated dependencies [f334d66]
1536
+ - Updated dependencies [2810695]
1537
+ - @objectstack/platform-objects@17.2.0
1538
+ - @objectstack/spec@17.2.0
1539
+ - @objectstack/core@17.2.0
1540
+ - @objectstack/metadata-core@17.2.0
1541
+ - @objectstack/metadata-fs@17.2.0
1542
+ - @objectstack/types@17.2.0
1543
+
3
1544
  ## 17.1.0
4
1545
 
5
1546
  ### Patch Changes