@objectstack/metadata 17.2.0 → 17.4.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,1972 @@
1
1
  # @objectstack/metadata
2
2
 
3
+ ## 17.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a56baa2: feat(metadata,objectql): a keyed plural read on `MetadataManager`, `listNames` fault parity, and an action audit that answers from the same identity and sources as the router
8
+
9
+ Two plural reads of one metadata plane could disagree with a by-name read of
10
+ that same plane, and the ADR-0110 D5 action-governance audit stood on the
11
+ disagreement — reporting `registered handler with NO declaration … REFUSED at
12
+ dispatch` about a route the router was resolving and dispatching in the same
13
+ boot.
14
+
15
+ **`MetadataManager.listNames` gains the per-loader `try`/`catch` that
16
+ `loadMany` and `list()` have carried since #5108.** One loader fault used to
17
+ produce two different facts depending only on which plural read a caller
18
+ reached for: `loadMany` swallowed it and answered short, `listNames` threw. It
19
+ now degrades the same way, through the same `reportLoaderReadFailure` /
20
+ `reportLoaderReadRecovered` helpers — one outage, one line, one vocabulary.
21
+ Callers that relied on `listNames` throwing to detect an outage should read
22
+ `listDiagnosed()`, which reports `degraded` explicitly.
23
+
24
+ **New: `MetadataManager.loadManyKeyed(type, options?)`** — `loadMany` read under
25
+ the identity the STORE holds each item by, returning `{ name, data }` pairs. It
26
+ delegates to a loader's own `loadManyKeyed` where one is offered (on
27
+ `DatabaseLoader` that shares `loadMany`'s single query, so it costs nothing
28
+ extra) and otherwise falls back to that loader's `list()` + per-name `load()`.
29
+ ⛔ **`loadMany`'s published return shape does not change**, and no existing
30
+ consumer is touched: the key travels *beside* the body, never inside it, so a
31
+ body that deliberately carries no `name` stays byte-identical to what was
32
+ stored (#14205).
33
+
34
+ **The action-governance audit now mirrors the router on both halves of the D5
35
+ bijection.** The declaration half enumerates the plane keyed
36
+ (`loadStandaloneActionsKeyed`), so a row whose body does not name itself — a
37
+ `sys_metadata` row keyed by its `name` column, or a `FilesystemLoader` file
38
+ whose identity is its path — is a declaration to the audit exactly as it is to
39
+ the router; the handler half also probes the plane BY NAME
40
+ (`lookupMetadataAction`, `loadDiagnosed`/`load`, injected like the existing
41
+ registry rung), so a loader fault a plural read swallows can no longer turn a
42
+ dispatchable handler into an accusation. Both probes stay conservative in one
43
+ direction only: a source that throws leaves the handler on the list.
44
+
45
+ Additive on every published signature. `runActionGovernanceInventory` and
46
+ `collectEngineActionDeclarations` gain optional parameters and keep their old
47
+ ones working unchanged; declaration rows gain an optional `storeKey` (the new
48
+ exported `ActionDeclarationRow`).
49
+
50
+ **Population change, reported:** `unboundDeclarations` now sees declarations
51
+ whose identity is the store key. Its BEFORE was **0, structurally rather than
52
+ by sampling** — a nameless row was dropped before reconciliation ran, so it
53
+ could never be reported however many a plane held. Its one deliberate
54
+ subtraction: a row with neither an own `name` nor a store key is no longer
55
+ reported as `actionName: undefined`, which read as a parse failure in the
56
+ warning rather than as a finding.
57
+
58
+ Known boundary, stated in the audit's docblock rather than left to be
59
+ rediscovered: a boot-time audit runs outside any request scope, so if a
60
+ composition ever registered `metadata` as `SCOPED` the audit could not reach
61
+ that instance at all — before any read method runs. No shipped composition does
62
+ (`packages/metadata/src/plugin.ts` registers a static instance), and reaching a
63
+ request-scoped service from a boot-time audit is a separate change.
64
+ - c1d274d: fix(metadata): two files sharing one stem are refused with both paths named, instead of one being listed twice and served by extension precedence (#14921)
65
+
66
+ **BREAKING** accept-set narrowing on `FilesystemLoader`, shipped as `minor`
67
+ under the repo's launch-window convention for breaking changes. Ruled on
68
+ #14921 (2026-09-05, option 1 of three).
69
+
70
+ **Remedy: delete or rename the duplicate file.** The refusal names every
71
+ colliding path and the metadata type, so the fix is visible at the point of
72
+ failure.
73
+
74
+ `FilesystemLoader` derives a metadata name by stripping a flat file's
75
+ extension, and resolves a name back to a file under a FIXED extension
76
+ precedence (`.json` → `.yaml` → `.yml` → `.ts` → `.js`). Two files sharing a
77
+ stem therefore produced one name **twice** in `list()` while only the
78
+ first-precedence file was reachable through any name at all. With
79
+ `object/twin.json` and `object/twin.yaml` both present, `list()` answered
80
+ `['twin', 'twin']`, `twin.yaml` was addressable through nothing, and
81
+ `loadMany()` returned both bodies. `MetadataManager.listNames()` unions loader
82
+ output into a `Set`, which collapsed the duplicate and took the count
83
+ discrepancy with it — the file stayed unreachable either way, so a clean
84
+ `listNames()` was never evidence the collision had been absorbed.
85
+
86
+ The invariant that broke: **what is listed is what is loadable.** The listed
87
+ set and the addressable set stopped being the same set. The failure was silent
88
+ in the direction that matters for authoring — convert `twin.json` to
89
+ `twin.yaml` and leave the old file behind, or land one from each of two
90
+ packages, and the JSON one is served forever with no diagnostic anywhere,
91
+ while `admitLoaderItems()`'s documented "keep the first and say nothing"
92
+ absorbs the collision a second time.
93
+
94
+ `FilesystemLoader.list()` now throws `AmbiguousMetadataStemError`
95
+ (`AMBIGUOUS_METADATA_STEM`, HTTP 500) naming both paths and the type, and the
96
+ same refusal fronts the shared `loadMany()` / `loadManyKeyed()` walk, so the
97
+ two-body answer is gone rather than de-duplicated. `MetadataManager.listNames()`
98
+ and `list()` **propagate** it rather than absorbing it into their per-loader
99
+ degradation: an ambiguous stem is an authoring error no retry fixes, and
100
+ degrading it would drop every item the loader holds into a short-but-served
101
+ list while the server keeps reporting healthy. A real storage outage still
102
+ degrades exactly as before — the seams discriminate on a branded predicate,
103
+ `isAmbiguousMetadataStemError`, not on a blanket rethrow.
104
+
105
+ **Refused shape**, precisely: two or more files **directly under
106
+ `ROOT/TYPE/`** whose basenames differ only by an extension belonging to one of
107
+ **this instance's registered serializers**. Register `javascript` and
108
+ `dual.json` + `dual.js` becomes ambiguous; under the manager's default format
109
+ set (`typescript` / `json` / `yaml`) it is not, because `.js` derives no name.
110
+ Nested files are untouched — they are neither listed nor resolvable (#14486),
111
+ so `crm/solo.json` beside a flat `solo.json` is not a collision. The refusal is
112
+ scoped to the type directory that holds it: a clean `view/` still lists while
113
+ `object/` refuses.
114
+
115
+ New exports from the package root entry: `AmbiguousMetadataStemError`,
116
+ `isAmbiguousMetadataStemError`, `AMBIGUOUS_METADATA_STEM_CODE`,
117
+ `AMBIGUOUS_METADATA_STEM_STATUS`.
118
+
119
+ Measured migration cost, which is what makes this narrowing cheap: **no tree in
120
+ this repository carries the shape.** A walk of all 7,770 tracked files across
121
+ 526 directories found zero stem collisions among `.json` / `.yaml` / `.yml` /
122
+ `.ts` / `.js`, confirmed independently by a `git ls-files` pass, and the repo
123
+ holds no `.yaml`/`.yml` metadata file at all outside CI and workspace config.
124
+ No existing tree goes red.
125
+
126
+ <!-- adr-0087: not-required (no-migration-prescription) An accept-set narrowing on a LOADER, not on any authorable key: no property of any spec schema is removed, renamed or re-shaped, so there is no tombstone and nothing for `objectstack migrate meta` to rewrite in a stored document. The affected artifact is a FILESYSTEM LAYOUT — two sibling files — which the ledger cannot address at all: a migration entry rewrites metadata bodies, and neither of the colliding files is wrong on its own. Which one an author wants kept is intent no entry can decide: they may have meant the conversion to `.yaml` to land and forgotten to delete the `.json`, or may have meant the opposite, and the two files carry no evidence of which. The refusal is the channel that reaches them, at the load site, naming both paths, the type, and the remedy. Measured in-repo population of affected trees is zero: a walk of 7,770 tracked files over 526 directories found no directory holding two files with one stem among the registered extensions, and there are no `.yaml`/`.yml` metadata files outside CI and workspace config. -->
127
+ - e9fcd6b: feat(metadata)!: `DatabaseLoaderOptions.cache.ttl` → `cache.ttlMs` — the read-through cache TTL carries its unit in the key name (#14478)
128
+
129
+ <!-- adr-0087: registered metadata-manager-config-cache-ttl-unit-in-key -->
130
+
131
+ **BREAKING** rename on the exported `DatabaseLoaderOptions.cache` shape
132
+ (`DatabaseLoaderCacheOptions.ttl` → `ttlMs`), shipped as `minor` under the
133
+ launch-window convention. `MetadataManager` hands `config.cache.databaseLoader`
134
+ straight to `new DatabaseLoader({ cache })`, so this option is the spec key
135
+ `cache.databaseLoader.ttlMs` one layer down and renames with it: a loader
136
+ configured with `ttlMs: 60_000` expires entries after 60 seconds exactly as
137
+ `ttl: 60_000` did. The README example and the kernel metadata-service docs page
138
+ spell the new key.
139
+
140
+ ```ts
141
+ // before
142
+ new DatabaseLoader({ driver, cache: { enabled: true, maxSize: 500, ttl: 60_000 } });
143
+ // after
144
+ new DatabaseLoader({ driver, cache: { enabled: true, maxSize: 500, ttlMs: 60_000 } });
145
+ ```
146
+ - 3bd9b34: feat(metadata): `deriveViewContainerObject` gets a leaf `/view-container` subpath, so objectql's lean ADR-0076 entry stops loading the manager, chokidar, glob and js-yaml for a six-line pure function
147
+
148
+ `packages/objectql/src/engine.ts` reached `deriveViewContainerObject` through
149
+ `@objectstack/metadata`'s ROOT entry. `core.ts` — the ADR-0076 lean entry —
150
+ re-exports `engine.ts`, so `@objectstack/objectql/core`'s module-init closure
151
+ inherited the whole root entry: `MetadataPlugin` -> `NodeMetadataManager` ->
152
+ `chokidar`, plus `glob`, `js-yaml` and `readdirp`.
153
+
154
+ The same file already carried the answer 79 lines above, at its
155
+ `@objectstack/metadata/errors` import: that leaf subpath exists "precisely so a
156
+ cross-package consumer gets the predicate without the manager, the loaders or
157
+ the YAML/filesystem machinery behind the root entry". This is that pattern,
158
+ taken a second time.
159
+
160
+ **Measured on the built artifacts, not asserted** — every module Node actually
161
+ evaluates when `@objectstack/objectql/core` is loaded in a fresh process,
162
+ recorded through a `module.registerHooks` load hook (ESM and CJS) plus
163
+ `require.cache`, byte sizes from `statSync`:
164
+
165
+ | `@objectstack/objectql/core` | modules | bytes |
166
+ |:---|---:|---:|
167
+ | before (ESM `dist/core.mjs`) | 190 | 12,348,424 |
168
+ | after (ESM `dist/core.mjs`) | 185 | 11,849,808 |
169
+ | **delta** | **-5** | **-498,616 (-486.9 KiB)** |
170
+ | before (CJS `dist/core.js`) | 188 | 12,654,238 |
171
+ | after (CJS `dist/core.js`) | 183 | 12,141,034 |
172
+ | **delta** | **-5** | **-513,204 (-501.2 KiB)** |
173
+
174
+ Six modules stop loading — `packages/metadata/dist/index.js` (237,747 B),
175
+ `js-yaml` (114,610 B), `glob` (82,749 B), `chokidar` (2 files, 54,220 B) and
176
+ `readdirp` (9,836 B) — and one 469-byte module takes their place. Marginal
177
+ module-init time for that root entry, measured on a warm lean closure, was
178
+ ~22 ms (median of 7; 20.4-27.5 ms) out of ~630 ms.
179
+
180
+ ⚠️ The figure the finding was argued on — "~3.6 KB to ~450 KB" — is right about
181
+ the delta and wrong about the baseline: the lean entry's closure was already
182
+ ~11.5 MiB before this import existed, dominated by `@objectstack/spec`
183
+ (9,587,914 B) and `zod` (567,918 B), neither of which the metadata root entry
184
+ contributes. What the root import cost was ~487 KiB *on top of* that, not a
185
+ closure of 450 KB.
186
+
187
+ The derivation itself moves to `packages/metadata/src/view-container.ts`, a
188
+ module with **no imports at all**, and `view-container-expansion.ts` imports
189
+ and re-exports it, so `index.ts`'s root export and `plugin.ts` keep their
190
+ spelling and the symbol stays on the root entry — this subpath is an additional
191
+ door, not a relocation. A re-export shim onto `view-container-expansion.ts` was
192
+ tried first and rejected on measurement: esbuild tree-shakes the unused
193
+ `expandRuntimeViewContainer` but keeps its two `@objectstack/spec` import
194
+ statements, so that shim's own closure was 84 modules / 3,035 KiB. The real
195
+ leaf's is 1 module / 469 B.
196
+
197
+ `expandRuntimeViewContainer` is deliberately not exported from the new subpath:
198
+ `metadata-manager.ts` is its only caller, the root entry does not export it
199
+ either, and it is the half that carries the spec machinery.
200
+ - 8647c87: A completed run of the ADR-0030 notification cut-over now records itself in the `sys_migration` deployment ledger, per the ruled claim matrix.
201
+
202
+ `migrateSysNotificationToEvent` reports `migrated` / `already_done` / `not_applicable` / `error` to its caller and — until now — recorded nothing anywhere. Once that line had scrolled, "did this cut-over run here, and when" had no answer in the deployment even in principle. The ledger row is what answers it, and what a run of this migration may claim under `NOTIFICATION_EVENT_MIGRATION_ID` is stated on that constant in `@objectstack/spec/system`:
203
+
204
+ - `last_run_at` — stamped on every completed non-`error` run (`migrated`, `already_done`, `not_applicable` alike).
205
+ - `applied_at` — stamped only on `migrated`. Never cleared: a later `already_done` leaves an earlier backfill's stamp alone, because the backfill really did happen.
206
+ - `verified_at` — never written, in either direction. This migration has no self-check, and `verified_at` means a self-check passed. On a store created after the cut-over the row already exists and `attestFreshDatastore` set `verified_at` at birth; that certificate survives a run untouched, because the column is omitted from the update rather than sent as `null`.
207
+ - `blocking: 0`, and `details` carrying `{ outcome }` verbatim.
208
+ - An `error` run writes no claim at all — it does not know what it did, so it does not say.
209
+
210
+ **Receipt, not gate.** Nothing reads a row under this id as a precondition and nothing may: a gate would need the self-check that does not exist. The row is what an operator reads, in the shape `sys_migration` already documents for the seed-tenancy repair.
211
+
212
+ Two additions to the published surface of `@objectstack/metadata/migrations`, both driven by that: a new `SysNotificationMigrationReceipt` type, and a new `receipt` member on `SysNotificationMigrationResult` reporting what became of the claim (`inserted` / `updated` / `not-claimed` / `no-ledger` / `failed`, with a reason on the last two). This directory takes no logger and reports to its caller, so the claim's own fate is reported the same way the migration's is — a receipt that could not be written is never swallowed. Reading a result is unaffected; code that CONSTRUCTS a `SysNotificationMigrationResult` by hand (a test double) now supplies `receipt`.
213
+
214
+ ### Patch Changes
215
+
216
+ - 0c5d035: fix(metadata): a `HistoryCleanupManager` run that loses deletes now says so, at both of `start()`'s triggers
217
+
218
+ A failing history cleanup was completely silent. Three things composed: every inner `catch` on the delete path is a bare `catch {`, so the error object is discarded; the only `console.error` in `runCleanup()` sits in its OUTER catch, which those inner catches prevent execution from reaching; and `start()` invoked the run as `void this.runCleanup()`, throwing away the `{ deleted, errors }` the run returns — at BOTH call sites, the immediate run and every interval tick. A driver whose deletes failed on every scheduled run therefore produced zero output and no reachable error count, while the history table grew past its retention policy with nothing to find.
219
+
220
+ The repair reads the envelope instead of replacing it. `runCleanup()`'s contract, its inner catches and its counting are unchanged: reporting a failure to the CALLER is the third answer AGENTS.md → "Degradation log levels" allows a durability seam, and that same section names a log per failed write as the mirror-image failure. What was missing was a reader — `start()` is where the chain ends, since it returns `void` and an interval tick has no caller at all. Both call sites now go through one shared pass that reads the returned counts and, when a run lost deletes, prints one `error` line naming the consequence (rows past the retention policy are still in the table, nothing retries them, and the system keeps reporting healthy) and where to look. A run that loses nothing stays quiet, and a direct caller of `runCleanup()` sees exactly the same `{ deleted, errors }` as before.
221
+ - 281bf0d: `HistoryCleanupManager` computes its retention cutoff on one calendar, not two.
222
+
223
+ Both call sites — the age-based delete in `runCleanup()` and the preview count in `getCleanupStats()` — built the cutoff with `cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays)` and then rendered it with `toISOString()`. `setDate`/`getDate` read and write the **local** calendar; `toISOString()` renders **UTC**. They now use `setUTCDate`/`getUTCDate`, so the arithmetic and the rendering agree.
224
+
225
+ `setDate` preserves wall-clock time, so shifting the local calendar back `n` days moves the *instant* by exactly `n × 24h` only while every local day in the window is 24 hours long. When the window straddles a DST transition it is 23 hours (spring-forward) or 25 (fall-back), and the cutoff instant that goes into the `recorded_at: { $lt: … }` **delete** filter is off by the size of that transition — one hour in most zones, thirty minutes on Lord Howe Island. History rows within that slip of the retention boundary were deleted early, or retained too long.
226
+
227
+ The exposure is not limited to the two transition days: the window only has to *straddle* a transition, so it grows with `maxAgeDays`. Measured over a 12-zone × 366-day × 48-half-hour sweep of 2026, in `America/New_York` the old spelling produced a wrong cutoff for 0.6% of instants at `maxAgeDays: 1`, 16.4% at 30, 49.7% at 90 and 69.4% at 180. In zones that do not observe DST (`UTC`, `Asia/Shanghai`, `Asia/Kolkata`, `Australia/Perth`) the rate is 0.0% at every `maxAgeDays` — which is why no test had ever gone red on this.
228
+
229
+ This does **not** make retention timezone-aware, and does not change what `maxAgeDays` means. The cutoff was already intended to be `now − maxAgeDays × 24h`; it is now that in every zone rather than only in zones without DST. Nothing else in either filter moved: the `organization_id` scoping, the ADR-0009 `executionPinned` exclusion and the `maxVersions` path are untouched.
230
+ - 3e7ef9c: Serve an Invalid `Date` from a driver instead of raising `RangeError` in `DatabaseLoader.stat`.
231
+
232
+ `canonicalIsoInstant` reached `value.toISOString()` for any `Date`, and that call raises `RangeError: Invalid time value` for the one `Date` whose time value is `NaN`. `stat()` is a hot read path — REST `/meta/*`, ObjectQL plan resolution, runtime overlay merges — so one legacy `sys_metadata` row answered **500** where the spelling this repair replaced had served a visibly-wrong value.
233
+
234
+ The shape is measured: mysql2 3.23.1 hands back a constant literally named `INVALID_DATE` for a zero `DATETIME`, and postgres-date 1.0.7 builds `new Date(NaN)` for every year in 275760..294276, which Postgres itself stores.
235
+
236
+ The `Date` arm now guards on `Number.isNaN(value.getTime())` and answers `undefined`, so `stat()`'s own `?? new Date().toISOString()` — the branch an absent column already takes — publishes a parseable `MetadataStats.mtime`. `undefined` rather than visible text is deliberate here: `mtime` is declared `z.string().datetime()`, so the text `"Invalid Date"` would not produce a readable cell, it would produce a zod refusal at the consumer, moving the failure instead of removing it. A blank is excluded for the opposite reason — it hides the producer's bug.
237
+ - 09c1d91: docs(metadata): the `@objectstack/metadata/errors` header records what the leaf entry actually loads — 4 packages, 83 modules, 2.2 MB — instead of leaving the reader to infer it is light (#15346)
238
+
239
+ `packages/metadata/src/errors.ts` is the leaf subpath `@objectstack/metadata/errors`, and its header explains why the subpath exists: the root entry pulls the manager, every loader and the YAML/filesystem machinery behind them, and "a consumer that wants a 40-line predicate should not have to load any of that". Measured, that promise half holds — and the header did not say which half.
240
+
241
+ **The half that holds, and stays:** MEASURED 2026-09-08 on `origin/main` `8c1515e847`, with `scripts/check-lean-entry-closure.mjs`'s exported `measure()`/`byPackage()` (one fresh child per published condition; module set collected through `module.registerHooks` unioned with `require.cache`). Both published conditions load exactly four packages, and the manager, every loader, `chokidar`, `glob`, `js-yaml` and `readdirp` are absent from all of them. That is the claim the header actually wrote, and it is accurate.
242
+
243
+ **The half that had drifted:** the general conclusion a reader takes away. `import` loads 83 modules / 2,201,709 bytes; `require` loads 83 / 2,355,405. Since the maintainer's 2026-08-30 ruling the file re-exports from `@objectstack/types`, which builds to one bundled module importing **both** `@objectstack/spec/api` (1,401,253 B) and `@objectstack/spec/security` (195,367 B) at module top, as values — `api` is the dominant edge at 7x `security`, and the two spec entries are independent of each other.
244
+
245
+ The header now separates the two costs the way the finding did, because conflating them is the available mistake: **in-repo the marginal contribution is 143 bytes** — the only current consumer of the subpath, `@objectstack/metadata-protocol`, declares `@objectstack/spec` itself and pays for that closure anyway — while the megabytes are what an **out-of-repo** consumer pays, which nothing here can measure. The figures are labelled PROVENANCE, dated and tree-pinned, in the manner the neighbouring gate keeps its own: nothing derives from them, nothing compares against them, and the header says to re-take them rather than quote them. They rot fast — the same measurement four days earlier at `6e67b86c0` found the same 83 modules but 2,487,842 bytes, 286,133 more, without this file changing at all.
246
+
247
+ **Why `patch` and not `skip-changeset`, measured rather than assumed.** `@objectstack/metadata` is released (17.3.0) and `errors.ts` is a shipped source file, so "a comment publishes nothing" needed checking rather than asserting. Building the package before and after the edit: `dist/errors.js`, `dist/errors.cjs`, `dist/errors.d.ts` and `dist/errors.d.cts` are **byte-identical** (esbuild strips the comment, and there is no `sourcesContent`), but `dist/errors.js.map` and `dist/errors.cjs.map` **change** — the export statement moved from source line 65 to line 113, so the `mappings` VLQ moves with it (`;AAgEA,…` → `;AAgHA,…`), at identical file size. `npm pack --dry-run` lists both maps in the tarball. So this diff does publish bytes from a released package, which is exactly what `skip-changeset` is not for. Nothing executable and no type changes: a consumer's stack traces resolve to the right source line, and that is the whole of it.
248
+ - 7629f4d: feat(spec)!: retire the three inert outer keys of `MetadataManagerConfig.cache` — `enabled`, `ttlSeconds` (formerly `ttl`) and `maxSize` — read by nothing; `cache.databaseLoader` is the only live half (#15624, ADR-0049)
249
+
250
+ <!-- adr-0087: registered metadata-manager-config-inert-cache-keys-retired -->
251
+
252
+ **BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
253
+ launch-window convention ships it as `minor`; the migration prescription is
254
+ registered under protocol major 18, where `os migrate meta` users will look).
255
+ ADR-0049 enforce-or-remove decides it: a declared-but-unenforced key with zero
256
+ measured readers comes off, and the published reference page stops teaching it.
257
+
258
+ `MetadataManagerConfig.cache` declared three outer knobs — `enabled` (default
259
+ `true`), `ttlSeconds` (default 3600; spelled `ttl` until #14478) and `maxSize`
260
+ ("Max cache size in bytes") — beside the nested `databaseLoader` block, and
261
+ **nothing read the outer three**. The only runtime consumer of the block is
262
+ `MetadataManager` (`packages/metadata`), which hands `cache.databaseLoader` and
263
+ nothing else to `new DatabaseLoader({ cache })`; a reader census over
264
+ `packages/**` (tests and changelogs excluded) found no runtime reader of any
265
+ outer key, while the same grep shape found the nested `cache?.databaseLoader`
266
+ read twice — the control that makes the zero a measurement. An author writing
267
+ `cache: { enabled: false }` or `cache: { ttlSeconds: 60 }` got a clean parse
268
+ and a cache that behaved exactly as before, with no error and no warning, and
269
+ the published reference page (`references/kernel/metadata-loader`) documented
270
+ all three as if they configured something.
271
+
272
+ **What is refused:** authoring `cache.enabled`, `cache.ttlSeconds`, `cache.ttl`
273
+ or `cache.maxSize` on `MetadataManagerConfig`, with any value — directly, through
274
+ `MetadataManagerOptions`, or through `MetadataPluginConfig.storage`. The nested
275
+ object is not `.strict()`, so each key is a `retiredKey()` tombstone rather than
276
+ a bare deletion (a deletion would have stripped it in silence — the same no-op
277
+ one layer down): authoring it is a `tsc` error (`never`) and a parse error
278
+ carrying the prescription, which names the live nested knob.
279
+
280
+ **What stays, byte-identical:** the DatabaseLoader read-through cache under
281
+ `cache.databaseLoader` — `enabled` (default `true`), `maxSize` (an entry count,
282
+ default 500) and `ttlMs` (milliseconds, default 60000) — and every runtime
283
+ path. Parsed configs no longer carry the two former defaults (`enabled: true`,
284
+ `ttlSeconds: 3600`) that were materialized and never consulted.
285
+
286
+ **The #14478 rename is folded in.** `cache.ttl` → `cache.ttlSeconds` was
287
+ registered under this same unreleased major and never reached a published
288
+ release, so it is absorbed by the removal: `cache.ttl`'s tombstone now
289
+ prescribes deletion (naming `cache.databaseLoader.ttlMs`) instead of a rename to
290
+ a key that is itself retired — an author upgrading from a published 17.x sees
291
+ one hop. The nested `cache.databaseLoader.ttl` → `ttlMs` half of that rename is
292
+ unchanged.
293
+
294
+ ## FROM → TO
295
+
296
+ ```ts
297
+ // before — parsed green; no runtime ever read the three outer numbers
298
+ new MetadataManager({
299
+ datasource: 'default',
300
+ cache: { enabled: true, ttlSeconds: 3600, maxSize: 10_485_760, databaseLoader: { ttlMs: 60_000 } },
301
+ });
302
+
303
+ // after — delete the outer keys; the nested block is the cache that runs
304
+ new MetadataManager({
305
+ datasource: 'default',
306
+ cache: { databaseLoader: { enabled: true, maxSize: 500, ttlMs: 60_000 } },
307
+ });
308
+ ```
309
+
310
+ **Migration.** Delete `cache.enabled`, `cache.ttlSeconds` / `cache.ttl` and
311
+ `cache.maxSize`; nothing replaces them, because nothing ever consumed them. If
312
+ you meant to switch the cache off, cap it or set its TTL, write
313
+ `cache.databaseLoader.enabled` / `.maxSize` (entries) / `.ttlMs` (milliseconds)
314
+ — those are honoured. No `os migrate meta` conversion runs on this surface: a
315
+ `MetadataManager` config is not a stack collection member and never a stored
316
+ row, so the chain has no seam for it; the D3 semantic entry
317
+ `metadata-manager-config-inert-cache-keys-retired` carries the prescription
318
+ into `spec-changes.json`, the upgrade guide and the `spec_changes` MCP tool.
319
+
320
+ The retirement kit: `retiredKey()` tombstones on all three (and the absorbed
321
+ `ttl`), `RETIRED_KEYS_BY_MAJOR[18]` entries for each, the D3 semantic entry
322
+ above (the #14478 entry's outer half is re-worded from a rename to a deletion),
323
+ negative pins asserting each prescription and a positive pin asserting the
324
+ parse output no longer materializes the retired defaults, the published
325
+ reference pages regenerated, and the hand-written docs page and this package's
326
+ README (`@objectstack/metadata` ships `README.md`, hence its `patch`) no longer
327
+ authoring `cache.enabled`.
328
+ - 6f23f0e: fix(metadata): keep the original notification instant when migrating `sys_notification` to the event model (#16312)
329
+
330
+ `migrateSysNotificationToEvent` materializes each legacy inbox row into a
331
+ `sys_inbox_message` and a `sys_notification_receipt`, back-dating both to the
332
+ notification's own `created_at`. Both writes passed no options bag, so they
333
+ relied on the audit binder's create-side `record.created_at ?? now` — the
334
+ laundering #15964 removed, on the maintainer ruling of 2026-09-06. Without
335
+ that accident, every migrated inbox row and receipt is stamped with the moment
336
+ the migration RAN: a user's whole bell history collapses to "all arrived
337
+ today".
338
+
339
+ The two writes now declare `{ context: { preserveAudit: true } }`, the explicit
340
+ historical-import channel the same ruling deliberately kept (#3493; it is what
341
+ REST import's `treatAsHistorical` sets). This is not a bypass of audit — it is
342
+ the door audit left open for a historical import. No exported symbol, schema or
343
+ config key moves.
344
+
345
+ **Release ordering.** `@objectstack/objectql`'s side of #15964 is itself still
346
+ an unreleased changeset, so no published version of this migration has ever
347
+ written the flattened timeline. Releasing the two together keeps it that way.
348
+
349
+ **If a deployment did run it from a build that has both halves**, the original
350
+ timeline is recoverable rather than lost: the source `sys_notification` rows
351
+ are rewritten in place, never deleted or archived, and `created_at` is not
352
+ among the legacy columns the run clears — so the notification's own instant is
353
+ still on the event row and reachable from both new rows through
354
+ `notification_id`.
355
+ - efc5447: `RemoteLoader.list()` no longer reports a nameless remote body as a literal `undefined`.
356
+
357
+ The method declares `Promise<string[]>` and read the collection as `loadMany<{ name: string }>(type)` before mapping `items.map(i => i.name)`. That type argument is an **assertion** about bodies that arrived over HTTP, and nothing checked it: a body with no top-level `name` yielded `undefined`, which went into an array the signature declares as `string[]`. `MetadataManager.listNames()` unions loader `list()` output unfiltered, so the violation reached consumers — measured on this fixture, `listNames()` answered `[ 'account', undefined, 42 ]`.
358
+
359
+ The guard is `DatabaseLoader.list()`'s, one file away: the same cast-then-map spelling with `.filter(name => typeof name === 'string')` behind it. `RemoteLoader` was the only one of the four loaders in that directory with no guard at all — `MemoryLoader` answers with its store keys, and `FilesystemLoader` reports only names `findFile()` resolves. Dropping silently rather than throwing is the direction those siblings already carry: a name in the list that the door answers `null` for is the silent failure an author reads as their own typo, so the list is narrowed to agree with the door.
360
+
361
+ Nothing that was validly returned before stops being returned: the only entries that disappear are the ones whose type the signature already ruled out. A caller that previously received `[undefined]` now receives `[]`. `loadMany()` is deliberately untouched — it keys nothing, so a body carrying no `name` is still served there; this loader reads over HTTP and holds no store key, so `body.name` is the only identity it has and the family's "identity is the store key" rule cannot be satisfied for it.
362
+ - c5d6803: Published `.js.map` files no longer embed the complete original source text (`sourcesContent`) — comments included. `sourcemap: true` was esbuild shorthand, and esbuild's own default for `sourcesContent` is `true`; nobody had decided to publish every package's full source (including `@internal`/test-only comments) to npm inside its source maps, it fell out of a default nobody had looked at. Measured before this change: 55 of 57 publishable packages shipped embedded source text, and maps were roughly half of `@objectstack/spec`'s published bytes.
363
+
364
+ `sourcesContent: false` is now set at one shared place (`scripts/tsup-drop-sources-content.mjs`, wired into every `tsup.config.ts` via tsup's `esbuildOptions` hook — most packages build through the repo-root config directly and pick this up with no config change of their own). `mappings` are untouched, so stack-trace positions still resolve correctly to the original file/line/column; only the embedded source text is gone.
365
+
366
+ `@objectstack/cli` (built with `tsc`, not `tsup`) never embedded source text to begin with — its maps' `sources` entries point at `src/**` paths that are not part of the published tarball either way. That is not a defect unique to `cli`: every `tsup`-built package's `sources` entries are `../src/**`-relative paths that are equally outside `files: ["dist", …]`, and were merely masked by the embedded content that just stopped shipping. Shipping `src/**` in `files[]` to make `sources` resolve was rejected — it would put most of the removed bytes straight back. So `cli`'s maps are left exactly as `tsc` emits them: this is now the fleet-consistent shape (accurate `mappings`, non-resolving-but-honest `sources` labels, no embedded text), not an outlier.
367
+
368
+ A new gate, `pnpm check:sourcemap-no-sources-content`, sweeps every built, non-private package's `dist/**/*.map` and fails if any of them carries a non-empty `sourcesContent` array — so a future `tsup.config.ts` that skips the shared hook, or a toolchain upgrade that changes esbuild's default back, is caught rather than silently re-publishing source text.
369
+ - Updated dependencies [fe0d9a4]
370
+ - Updated dependencies [ecd2158]
371
+ - Updated dependencies [f2b5e46]
372
+ - Updated dependencies [2ed6be6]
373
+ - Updated dependencies [ed7243d]
374
+ - Updated dependencies [6ba0db4]
375
+ - Updated dependencies [625b0c3]
376
+ - Updated dependencies [233222e]
377
+ - Updated dependencies [07f40e5]
378
+ - Updated dependencies [ceb4877]
379
+ - Updated dependencies [e9fcd6b]
380
+ - Updated dependencies [90e7e6d]
381
+ - Updated dependencies [2bdabe6]
382
+ - Updated dependencies [ca326b5]
383
+ - Updated dependencies [8f404a5]
384
+ - Updated dependencies [159dbad]
385
+ - Updated dependencies [68437d4]
386
+ - Updated dependencies [abb140c]
387
+ - Updated dependencies [8333a6c]
388
+ - Updated dependencies [3e3ecb0]
389
+ - Updated dependencies [3030369]
390
+ - Updated dependencies [d5d8d50]
391
+ - Updated dependencies [e08892d]
392
+ - Updated dependencies [ae05f2e]
393
+ - Updated dependencies [b548e43]
394
+ - Updated dependencies [c463d03]
395
+ - Updated dependencies [64bd6a3]
396
+ - Updated dependencies [13c48c2]
397
+ - Updated dependencies [b0529e1]
398
+ - Updated dependencies [66dc6ab]
399
+ - Updated dependencies [6f94458]
400
+ - Updated dependencies [6e67b86]
401
+ - Updated dependencies [132742f]
402
+ - Updated dependencies [85a2459]
403
+ - Updated dependencies [50dc214]
404
+ - Updated dependencies [e89fa92]
405
+ - Updated dependencies [e9fcd6b]
406
+ - Updated dependencies [8976ea1]
407
+ - Updated dependencies [56fe8c2]
408
+ - Updated dependencies [acabd24]
409
+ - Updated dependencies [ab50c8f]
410
+ - Updated dependencies [6491463]
411
+ - Updated dependencies [89cf4d6]
412
+ - Updated dependencies [21c5dcb]
413
+ - Updated dependencies [6d4d5d3]
414
+ - Updated dependencies [ed5d557]
415
+ - Updated dependencies [bca21f7]
416
+ - Updated dependencies [e9fcd6b]
417
+ - Updated dependencies [2025b1f]
418
+ - Updated dependencies [1a7a7c9]
419
+ - Updated dependencies [e9fcd6b]
420
+ - Updated dependencies [cbca47d]
421
+ - Updated dependencies [acf4d38]
422
+ - Updated dependencies [ef3a138]
423
+ - Updated dependencies [68d5dfd]
424
+ - Updated dependencies [3e21cf0]
425
+ - Updated dependencies [4cfc93b]
426
+ - Updated dependencies [efd6b43]
427
+ - Updated dependencies [859ded3]
428
+ - Updated dependencies [fa125f3]
429
+ - Updated dependencies [74628d9]
430
+ - Updated dependencies [a646120]
431
+ - Updated dependencies [6f1ce7d]
432
+ - Updated dependencies [7778115]
433
+ - Updated dependencies [2c753fe]
434
+ - Updated dependencies [52804cd]
435
+ - Updated dependencies [3f89967]
436
+ - Updated dependencies [53cf263]
437
+ - Updated dependencies [21aabbc]
438
+ - Updated dependencies [9c270bb]
439
+ - Updated dependencies [76c8c5a]
440
+ - Updated dependencies [088f761]
441
+ - Updated dependencies [a84e1ce]
442
+ - Updated dependencies [bf1054a]
443
+ - Updated dependencies [d8d2776]
444
+ - Updated dependencies [222dc0f]
445
+ - Updated dependencies [e9fcd6b]
446
+ - Updated dependencies [32c917d]
447
+ - Updated dependencies [f9a3c32]
448
+ - Updated dependencies [f502898]
449
+ - Updated dependencies [51ae731]
450
+ - Updated dependencies [af7edfe]
451
+ - Updated dependencies [b60f48b]
452
+ - Updated dependencies [c78c918]
453
+ - Updated dependencies [4ca358d]
454
+ - Updated dependencies [cf9bda4]
455
+ - Updated dependencies [784cb92]
456
+ - Updated dependencies [7629f4d]
457
+ - Updated dependencies [51df9fd]
458
+ - Updated dependencies [a7da4de]
459
+ - Updated dependencies [de0bcdd]
460
+ - Updated dependencies [70f7d6d]
461
+ - Updated dependencies [c677cda]
462
+ - Updated dependencies [6acb37e]
463
+ - Updated dependencies [7797102]
464
+ - Updated dependencies [554a160]
465
+ - Updated dependencies [f7da71e]
466
+ - Updated dependencies [7f745c3]
467
+ - Updated dependencies [0a038cc]
468
+ - Updated dependencies [e9fcd6b]
469
+ - Updated dependencies [97adce2]
470
+ - Updated dependencies [a83482c]
471
+ - Updated dependencies [5eb24f8]
472
+ - Updated dependencies [2a3decc]
473
+ - Updated dependencies [cc00df2]
474
+ - Updated dependencies [cc00df2]
475
+ - Updated dependencies [f4e6adf]
476
+ - Updated dependencies [ee4a59b]
477
+ - Updated dependencies [4db3c61]
478
+ - Updated dependencies [5ca314a]
479
+ - Updated dependencies [e0af1a8]
480
+ - Updated dependencies [4771bd9]
481
+ - Updated dependencies [414c1fc]
482
+ - Updated dependencies [22c0279]
483
+ - Updated dependencies [c930f85]
484
+ - Updated dependencies [0db2947]
485
+ - Updated dependencies [92b5d7f]
486
+ - Updated dependencies [613bfbd]
487
+ - Updated dependencies [abae16a]
488
+ - Updated dependencies [094b8fd]
489
+ - Updated dependencies [c7aca0d]
490
+ - Updated dependencies [c1d8f98]
491
+ - Updated dependencies [8e0b297]
492
+ - Updated dependencies [d4f9b2a]
493
+ - Updated dependencies [5f7fa1d]
494
+ - Updated dependencies [87f0ccc]
495
+ - Updated dependencies [aedbaef]
496
+ - Updated dependencies [a727043]
497
+ - Updated dependencies [c5d6803]
498
+ - Updated dependencies [10d05bb]
499
+ - Updated dependencies [69602e5]
500
+ - Updated dependencies [c3ce76c]
501
+ - Updated dependencies [7936b29]
502
+ - Updated dependencies [46803fa]
503
+ - Updated dependencies [c2a336c]
504
+ - Updated dependencies [9f890d3]
505
+ - Updated dependencies [0bb2318]
506
+ - Updated dependencies [f7db8f4]
507
+ - Updated dependencies [1ecee3e]
508
+ - Updated dependencies [9408b7f]
509
+ - Updated dependencies [2bb0614]
510
+ - Updated dependencies [b3820c3]
511
+ - Updated dependencies [e9fcd6b]
512
+ - Updated dependencies [4df2a98]
513
+ - Updated dependencies [9bcd9be]
514
+ - Updated dependencies [b398ad2]
515
+ - Updated dependencies [99261a7]
516
+ - Updated dependencies [81b426f]
517
+ - Updated dependencies [001af1c]
518
+ - Updated dependencies [fb77aa5]
519
+ - Updated dependencies [3d3f60e]
520
+ - Updated dependencies [581d8f8]
521
+ - Updated dependencies [f81afe3]
522
+ - Updated dependencies [40a44b9]
523
+ - Updated dependencies [f89812e]
524
+ - Updated dependencies [7a7fb03]
525
+ - Updated dependencies [8fd246d]
526
+ - Updated dependencies [021a735]
527
+ - Updated dependencies [7bdb163]
528
+ - @objectstack/spec@17.4.0
529
+ - @objectstack/core@17.4.0
530
+ - @objectstack/platform-objects@17.4.0
531
+ - @objectstack/types@17.4.0
532
+ - @objectstack/metadata-core@17.4.0
533
+ - @objectstack/metadata-fs@17.4.0
534
+
535
+ ## 17.3.0
536
+
537
+ ### Minor Changes
538
+
539
+ - 32448d4: feat(metadata): the artifact door registers stack-declared `capabilities` (#12892 step 1)
540
+
541
+ `ARTIFACT_FIELD_TO_TYPE` — the map that decides which collections of a compiled
542
+ artifact reach `MetadataManager` — now carries `capabilities: 'capability'`.
543
+ This is step 1 of the maintainer's 2026-08-29 ruling on #12892 (option 1: *the
544
+ door owns the registration route* for the five artifact security collections).
545
+
546
+ **FROM.** `capabilities` is an authorable top-level stack collection (ADR-0066
547
+ D1), but the door did not map it while `AppPlugin`'s ADR-0057 `SECURITY_FIELDS`
548
+ block did — making that block the collection's **sole registrar on an artifact
549
+ boot**, and it registers the raw bundle bytes with no strict parse, no schema
550
+ default and no ADR-0010 provenance. On a `bootstrap: 'artifact-only'` runtime
551
+ where `AppPlugin` does not run, a package's declared capabilities reached no
552
+ registry at all: `GET /meta/capability` answered **empty**, and
553
+ `bootstrapDeclaredCapabilities` seeded **no `sys_capability` row** for them.
554
+
555
+ **TO.** The door registers them like every other mapped collection: strict
556
+ parse, schema defaults, ADR-0010 provenance. Measured on a real artifact-only
557
+ kernel boot with no `AppPlugin`, over a package declaring
558
+ `{ name: 'crm.export', label: 'Export CRM data' }`:
559
+
560
+ - `GET /meta/capability` went from `[]` to one item carrying `scope:'platform'`
561
+ (the `CapabilitySchema` default) plus `_packageId` / `_packageVersion` /
562
+ `_provenance`;
563
+ - `sys_capability` went from 9 rows (platform-curated only) to 10 — the
564
+ declaration now materializes with `managed_by:'package'` and its `package_id`.
565
+
566
+ **What this does NOT change, deliberately.** On the ordinary artifact boot
567
+ `AppPlugin` still registers `capabilities` and still runs last, so its unparsed
568
+ copy still wins the registry — measured byte-identical before and after this
569
+ change. Two registrars on one route is the interim state the ruling explicitly
570
+ permits while step 2 (that block stops registering the five on the **artifact**
571
+ path, after a census of the non-artifact boots that depend on it) lands. No
572
+ authoring surface moves, and no artifact that parses today stops parsing.
573
+ - 4b4d5a3: fix(metadata): `FilesystemLoader.list()` reports only names `findFile()` / `load()` / `exists()` can resolve
574
+
575
+ **BREAKING (list output narrows).** Two shapes stop appearing in
576
+ `FilesystemLoader.list()`, and therefore in `MetadataManager.listNames()`:
577
+
578
+ - **nested files** — `ROOT/TYPE/crm/account.json` was listed as `account`, a
579
+ name that resolves against `ROOT/TYPE/account.json` and finds nothing;
580
+ - **extension-less files** — `ROOT/TYPE/noext` was listed as `noext`, which
581
+ resolves under no appended extension at all.
582
+
583
+ A third shape follows from the same rule rather than from a rule of its own:
584
+ the extensions a name can be resolved under are now the ones belonging to the
585
+ loader's **registered serializers**, so under the manager's default format set
586
+ (`typescript` / `json` / `yaml`) a `.js` file leaves `list()` too. It was
587
+ previously listed and resolvable while `loadMany()` could never return it and
588
+ `load()` threw `No serializer found for format: javascript`. Register the
589
+ `javascript` serializer and it is listed, resolvable and loadable together.
590
+
591
+ `list()`, `findFile()` and `loadManyKeyed()` now share one name-to-path
592
+ derivation, so `listNames()` and `get()` give the same answer. Previously a name
593
+ could sit in the list while `get()` answered `null` for it — a silent failure an
594
+ author reads as their own typo.
595
+
596
+ Nothing changes for a tree whose metadata is laid out as `ROOT/TYPE/NAME.json`
597
+ (or `.yaml` / `.yml` / `.ts`), which is the layout ADR-0008 §10 already
598
+ prescribes and `metadata-fs`'s `parseItemPath()` already enforces. `.yaml`,
599
+ `.yml` and `.ts` are unaffected: the extension set follows the registered
600
+ serializers, not §10's `.json`-only rule, which governs the `metadata-fs` store.
601
+
602
+ `loadMany()` is unchanged and still returns bodies for nested and
603
+ extension-less files; `findFile()` still resolves an explicitly path-shaped
604
+ name such as `crm/account`, which nothing lists.
605
+
606
+ <!-- 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. -->
607
+ - f4e7ae5: fix(security): gate the metadata HMR door on an explicit development posture (#12140)
608
+
609
+ **BREAKING** surface narrowing — `GET`/`POST /api/v1/dev/metadata-events` are no
610
+ longer mounted unless `NODE_ENV` is exactly `development`. Shipped as `minor`
611
+ under the repo's launch-window convention for breaking changes.
612
+
613
+ `MetadataPlugin` mounted both HMR routes whenever a raw-app-capable HTTP server
614
+ was present, with no authentication and no environment condition of its own. The
615
+ dev-only posture the path (`/api/v1/dev/…`) advertises lived only in prose —
616
+ "production deployments simply won't have a CLI POSTing to this endpoint" — which
617
+ is a claim about who is on the network, not a gate that stops them, and the same
618
+ structural shape #9391 closed for the `datasource-admin` family: both mounts take
619
+ the host's framework-native app handle, which is outside REST's `enforceAuth`
620
+ seam by construction.
621
+
622
+ The distributions were enumerated rather than assumed, because "a dev-only
623
+ surface lacking a gate that says so" and "an unauthenticated door on a real
624
+ deployment" want different repairs. The official container image runs `os start`
625
+ under `NODE_ENV=production`; that boot reaches `createStandaloneStack`, which
626
+ composes `MetadataPlugin` unconditionally (only `artifactWatch` was
627
+ NODE_ENV-gated) onto a kernel that registers the Hono server whenever it serves.
628
+ So a production-shaped boot did mount both routes and did answer them — `POST`
629
+ re-reading the compiled artifact from disk and broadcasting a reload frame to
630
+ every connected client, unauthenticated.
631
+
632
+ `registerMetadataHmrRoutes` now refuses at its first statement, ahead of every
633
+ side effect it performs, and returns `null` so "nothing was mounted" is a fact
634
+ the compiler forces its caller to handle. Unset `NODE_ENV` is closed, per the
635
+ maintainer's 2026-08-06 ruling that an absent value reads as `production`; `test`
636
+ and unrecognised spellings (`staging`, `preview`, `qa`) are closed too — a gate
637
+ must not treat a spelling nobody recognises as a key.
638
+
639
+ Who is affected, in both directions:
640
+
641
+ - `os dev` is unchanged. It spawns `os serve --dev`, which sets
642
+ `NODE_ENV='development'` before any plugin starts, so the watch-recompile loop
643
+ still gets its `200` and Studio still gets its reload frames.
644
+ - A deployment that was reaching this endpoint on a production-posture boot now
645
+ gets its host app's `404`. That door was never advertised, never in the SDK
646
+ (`@objectstack/client` builds no such URL) and is a build-tool loopback; if you
647
+ need a supported production reload trigger, that is a product decision, not
648
+ this endpoint.
649
+ - The server-side artifact-file watcher is untouched on every boot shape that had
650
+ it: the reload still happens, only the broadcast to (now absent) SSE clients is
651
+ skipped.
652
+
653
+ <!-- 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 -->
654
+ - 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)
655
+
656
+ `MetadataManager.readListUncached()` — and its no-catch sibling
657
+ `listForIndex()`, which builds the endpoint index — merged each loader's answer
658
+ into the result set keyed by `body.name`, and admitted an item ONLY when the
659
+ stored body carried a string `name`.
660
+
661
+ A metadata body is not required to name itself. `register(type, name, data)`
662
+ takes the key as its ARGUMENT, and `assertMetadataRegisterContract` says so in
663
+ as many words: "A document with NO `name` of its own is fine — the argument is
664
+ the key". An aggregated `defineView` container is exactly that shape — no own
665
+ `name` by design, its identity being the target object, carried in the row's
666
+ `name` COLUMN — and `DatabaseLoader.rowToData()` returns the stored body
667
+ without folding the column into it.
668
+
669
+ So a container written by `register('view', OBJECT, container)` lived in the
670
+ registry for the life of the process and was written to `sys_metadata`, and
671
+ then **disappeared at the next restart**: cold registry, only the loader
672
+ answering, and `list('view')` refused the row. `listDiagnosed()` reported that
673
+ short answer as complete (`degraded: false`) because no loader had thrown. Not
674
+ scoped to views — any loader-held body with no top-level `name` was invisible.
675
+
676
+ **The repair.** A loader-held item's identity is the key its store holds it
677
+ under, so the manager now asks the loader for that key rather than guessing it
678
+ from the body: `MetadataLoader` gains an OPTIONAL `loadManyKeyed()` returning
679
+ `(name, body)` pairs, implemented by `DatabaseLoader` (from the row's `name`
680
+ column) and `MemoryLoader` (from its storage map key). The key travels BESIDE
681
+ the body and is never folded into it, so nothing synthesises a `name` into a
682
+ body that deliberately has none and the register contract's refusal of a
683
+ disagreeing `data.name` keeps meaning what it says.
684
+
685
+ **Nothing consumers see today changes shape.** For any item that went through
686
+ `register()`, a `data.name` that exists is required to equal the key, so the
687
+ keyed merge produces the identical entry; what is new is only the items the old
688
+ gate refused. `loadManyKeyed()` is optional, and a loader without it (a
689
+ `RemoteLoader`, whose wire format carries bodies only) falls back to the
690
+ previous `body.name` keying unchanged — so no implementor of the published
691
+ `MetadataLoader` interface needs to change.
692
+
693
+ `MetadataManager.loadMany()` is deliberately untouched: its `body.name` test is
694
+ a de-duplication guard, not an admission gate — a nameless item already fell
695
+ past it and was returned — so it never carried this defect.
696
+ - 9e0ba21: feat(spec): retire the paper metadata-customization protocol with its full coupling set (#13135, re-charter of #12057; ADR-0049, ADR-0126)
697
+
698
+ <!-- adr-0087: registered metadata-customization-protocol-retired -->
699
+
700
+ **BREAKING** export removal + authorable-key retirement, landing after the
701
+ v17.0.0 cut (the lockstep launch-window convention ships it as `minor`; the
702
+ prescriptions are registered under protocol major 18 —
703
+ `RETIRED_DEFS_BY_MAJOR[18]`, `RETIRED_KEYS_BY_MAJOR[18]` and the D3 semantic
704
+ entry `metadata-customization-protocol-retired` — where `os migrate meta`
705
+ users will look).
706
+
707
+ `kernel/metadata-customization.zod.ts` declared a three-layer platform/user
708
+ patch-overlay protocol (field-level change tracking, customization policies, a
709
+ 3-way-merge story) that nothing reachable implemented: no route ever served
710
+ the paper `…/overlay` / `…/effective` endpoints, the only implementation
711
+ (`packages/metadata`'s manager limb) was called solely by its own unit tests,
712
+ no merge engine ever existed, and no code read a `CustomizationPolicy`.
713
+ ADR-0126 §6 wall 4 supersedes the protocol as a matter of record ("nothing may
714
+ build against it"); the maintainer adopted retirement on #12057 (2026-08-29,
715
+ 「同意」), and #13135 charters the full coupling set the fork report measured.
716
+
717
+ FROM → TO:
718
+
719
+ - `MetadataOverlaySchema` / `FieldChangeSchema` / `CustomizationOriginSchema` /
720
+ `MergeConflictSchema` / `MergeStrategyConfigSchema` / `MergeResultSchema` /
721
+ `CustomizationPolicySchema` and their `…`/`…Parsed` types
722
+ (`@objectstack/spec/kernel`) → *(removed — no replacement protocol)*. The
723
+ customization that actually ships: ADR-0005's org-scoped overlay
724
+ (`allowOrgOverride` on `DEFAULT_METADATA_TYPE_REGISTRY`, `sys_metadata` org
725
+ rows, layered read `code`/`overlay`/`effective`) and ADR-0126's
726
+ packaged-metadata model (clone + ledger disable).
727
+ - `MetadataOverlayResponseSchema` / `MetadataOverlaySaveRequestSchema` /
728
+ `MetadataEffectiveResponseSchema` (`@objectstack/spec/api` §5) →
729
+ *(removed)* — contracts for endpoints no adapter ever served; the layered
730
+ read's contracts (`getMetaItemLayered`) are the live API.
731
+ - `IMetadataService.getOverlay` / `.saveOverlay` / `.removeOverlay` /
732
+ `.getEffective` optional members (`@objectstack/spec/contracts`) →
733
+ *(removed)*, together with `packages/metadata`'s in-memory limb and its
734
+ `'overlay'` feature log entry.
735
+ - `MetadataPluginConfig.customizationPolicies` / `.mergeStrategy` and
736
+ `MetadataManagerConfig.persistence.overlayWritable` → *(removed — retiredKey
737
+ tombstones)*: authoring one is now a `tsc` error and a parse error carrying
738
+ the prescription. Delete the keys; nothing replaces them (`persistence.writable`
739
+ remains the base write gate).
740
+
741
+ One-line fix: delete the keys and any code building against the removed
742
+ exports — they configured and described nothing that ever ran; org-level
743
+ customization keeps riding the ADR-0005 overlay unchanged.
744
+
745
+ The retirement kit: whole-module deletion + kernel barrel line; 10
746
+ `RETIRED_DEFS_BY_MAJOR[18]` entries (7 kernel defs + 3 api §5 contracts); 3
747
+ `RETIRED_KEYS_BY_MAJOR[18]` tombstone entries (no D2 conversion —
748
+ plugin/manager configs are not stack collection members, the
749
+ `kernel/MetadataPluginConfig:additionalTypes` precedent); D3 semantic entry
750
+ `metadata-customization-protocol-retired`; retirement pin test
751
+ (`kernel/metadata-customization-retirement.test.ts`); type-alias pin rows
752
+ Iso408-411 vacated; api-surface / export-origins / json-schema manifest /
753
+ authorable-surface / reference docs regenerated (the
754
+ `kernel/metadata-customization` reference page disappears with the module).
755
+ - 4cda78c: fix(metadata,objectql,metadata-protocol): require a missing-table error to name the table that was READ (#13324)
756
+
757
+ `isMissingTableError` answers the one question that licenses a fail-soft caller
758
+ to treat an empty result as the truth: "did this read fail because the table has
759
+ not been provisioned yet?". It matched the *shape* of the dialect phrase and
760
+ never asked WHICH table the phrase names.
761
+
762
+ Measured on a real libsql database: a view whose base table is gone fails with
763
+ `no such table: main.<base>` when the view itself is read. The phrase matches,
764
+ so a read of a relation that **exists and may be backed by rows** was classified
765
+ benign, and every fail-soft consumer on that path — `probeInstallOrganizations`,
766
+ `resolveFileReferences`, `seedAutonumber`, the cascade-delete dependents probe,
767
+ `DatabaseLoader`, `SeedLoaderService`, the `sys_metadata` overlay reads —
768
+ computed its answer from data it never read. That is a false "benign", the
769
+ direction the module's own docblock calls far more expensive than a false
770
+ "real".
771
+
772
+ The predicate now takes the object the caller was reading and refuses the
773
+ benign verdict when the phrase names a different relation. Shape alone cannot
774
+ separate the two cases: measured, a view over a missing base table and a
775
+ genuine missing table the caller qualified produce byte-identical messages, so
776
+ the read's name is a parameter rather than another regex.
777
+
778
+ The parameter is **optional** — omitting it reproduces the previous behaviour
779
+ exactly, so no external caller of `@objectstack/metadata/errors` changes. Every
780
+ in-repo call site now passes it. The comparison folds away schema/database
781
+ qualifiers, the legacy `namespace__short` prefix and case, so every shape
782
+ recognised before for a genuine missing table (sqlite `no such table: X`,
783
+ Postgres `relation "x" does not exist`, MySQL `table "x" doesn't exist`,
784
+ `unknown table`, the SQLSTATE and errno limbs) still answers benign.
785
+ - 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)
786
+
787
+ Three sites derive "which object does an aggregated `defineView` container bind
788
+ to". After #13407 / #13913 / #13912 all three read the container's own top-level
789
+ `object` before the `list.data.object` chain, but they still disagreed about the
790
+ row's own `name`:
791
+
792
+ - `packages/objectql/src/engine.ts` `resolveMetadataItemName('views', item)` —
793
+ the boot-loop SOURCE registrar — read `name` FIRST, before `object`;
794
+ - `deriveViewContainerObject` (`@objectstack/metadata`, used by the artifact/HMR
795
+ SOURCE registrar and by `getViewsByObject()`) and `expandRuntimeViewContainer`
796
+ (`@objectstack/metadata-protocol`, the runtime door) both read `name` LAST.
797
+
798
+ A container written as `{ name: 'lead_views', object: 'crm_lead', list: { … } }`
799
+ therefore registered under `lead_views` through the boot loop and under
800
+ `crm_lead` everywhere else, with the whole expansion (`<object>.<key>`) carried
801
+ along — and since `getViewsByObject()` / `GET /meta/view?object=` filter the
802
+ expanded items by their `object`, which registrar loaded the document decided
803
+ whether the views were addressable under the object at all. No error, no
804
+ diagnostic.
805
+
806
+ The boot loop's container branch now calls `deriveViewContainerObject` — by
807
+ import, not by re-spelling: a fourth hand-copy of the chain was the defect, not
808
+ the repair. The direction is the 2026-08-07 meta-rule rather than taste (one
809
+ operation, two inconsistent implementations, the side bound by a DECLARATION
810
+ wins): `ViewSchema.object`'s own `.describe()` names its readers, while the boot
811
+ loop's order argued from item identity, which declares nothing about the
812
+ binding. The two sites that already held the winning order are untouched.
813
+
814
+ **`@objectstack/metadata` — new public export (`minor`).**
815
+ `deriveViewContainerObject` was module-local; it is now on the package's root
816
+ entry, because `packages/objectql` is a SOURCE registrar for the same containers
817
+ and has to mint the same key. `packages/objectql` already declares
818
+ `@objectstack/metadata` as a dependency and nothing in `packages/metadata`
819
+ depends on `objectql`, so the import adds no cycle.
820
+
821
+ **Scope of the behaviour change.** Only the `views` CONTAINER branch moves, gated
822
+ on `isAggregatedViewContainer`: the assembled `viewItems:` channel (standalone
823
+ ViewItems and flattened overlays, every member of `AssembledViewArtifactSchema`
824
+ requiring `viewKind`) still keys by its own `name` first, which is its identity
825
+ and not a binding. `item.id` is untouched and cannot fire for a container —
826
+ `ViewSchema` is a `strictObject` declaring `name` and `object` and no `id`.
827
+
828
+ **No migration surface.** Measured on this tree: of the 54 non-test sources that
829
+ author or carry view containers, ZERO declare a `name` that differs from the
830
+ object they bind to, so every in-tree container derived identically at all three
831
+ sites before this change and does after it. What moves is the latent shape only.
832
+
833
+ ⚠️ One card premise was measured false and is recorded in the new pin rather
834
+ than quietly dropped: the artifact/HMR registrar does not silently mint a second
835
+ key for a divergent container. It derives `crm_lead` correctly and then refuses
836
+ the whole artifact load — `assertMetadataRegisterContract` (#7378 row 1),
837
+ `VALIDATION_ERROR` / 400 — because the document's own `data.name` still reads
838
+ `lead_views`. The boot loop reconciles that field and the artifact door does
839
+ not; that residual asymmetry is a separate defect at a separate site and is
840
+ filed as its own card.
841
+
842
+ ### Patch Changes
843
+
844
+ - 655b106: fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)
845
+
846
+ A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
847
+ door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
848
+ iterated the **flattened top level** and stamped every item with the artifact's
849
+ own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
850
+ 'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
851
+ pick — so a two-package artifact registered the **module's** object under the
852
+ **App** package's identity, while the ObjectQL load path, reading the same
853
+ artifact's `packages[]`, owned it under the module's.
854
+
855
+ The platform then held two answers to "who owns this object", and which one a
856
+ consumer saw depended on the door it went through. Measured on a real boot of
857
+ `examples/app-multi-package`:
858
+
859
+ - `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
860
+ slots by `${packageId}${name}`, so the two differently-attributed copies
861
+ landed in two slots;
862
+ - `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
863
+ object, because the App-stamped copy was re-ingested into the registry as that
864
+ package's contribution;
865
+ - the layers door named the App package while the item door and
866
+ `GET /api/v1/packages` named the module;
867
+ - Studio's Data pillar for the App package listed the module's object — ADR-0130
868
+ Consequences §1.3a ("Studio's scope is the package") did not hold.
869
+
870
+ **The door now reads both shapes, and attributes every item to the body it was
871
+ found in.** `packages` present → each assembled package body's collections are
872
+ registered stamped with **that body's** id; `packages` absent → the single
873
+ `manifest` branch runs exactly as before (D7). The owner is read off the body an
874
+ item was found in — never reverse-derived by matching a top-level item's name
875
+ against a name-to-package index, which would be the second metadata-identity
876
+ resolution path #14512's triage rejected by name.
877
+
878
+ **Ordering and the entry gate are reused, not re-derived (D5).** The door calls
879
+ the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
880
+ readers of one `packages[]` cannot disagree about the registration order **or**
881
+ about which artifacts are loadable at all.
882
+
883
+ ⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
884
+ `@objectstack/core`** — hence the `minor` there. They were in
885
+ `@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
886
+ metadata door could not import them from where they lived; `@objectstack/core`
887
+ already owns `resolvePluginOrder` and is already a dependency of both readers,
888
+ so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
889
+ re-exports both under their existing names — its published surface is unchanged,
890
+ which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
891
+ dispatcher error vocabulary's `file:` anchors, repointed at the new path.
892
+
893
+ **Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
894
+ than asserted: the whole `manager.register` sequence for a single-`manifest`
895
+ artifact — every call, in order, with the id and version each item was stamped
896
+ with — is pinned as a literal in
897
+ `packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
898
+ recorded identically on both legs of the ablation. A real boot of
899
+ `examples/app-todo` answers every door identically before and after.
900
+
901
+ **Nothing a booted instance can see today disappears.** Every live
902
+ `ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
903
+ (measured, not assumed), so iterating bodies loses no collection; and because
904
+ `packages` composes by `concat`, an artifact whose top level carries a
905
+ definition no package body repeats keeps it — registered once, attributed to the
906
+ artifact's own identity, and logged, because it means the artifact's two halves
907
+ disagree about what it ships.
908
+
909
+ ⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
910
+ emitting the flattened top level alongside `packages[]`. Whether they should is
911
+ #14512's decision, not this door's.
912
+ - 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).
913
+
914
+ 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).
915
+
916
+ 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`.
917
+ - 7ef0268: fix(metadata,runtime): retire the `policies` dead pointer in both artifact registrars, and pin the map that carried it (#12894)
918
+
919
+ Zero behaviour change, by construction. Both readers of an artifact boot carried
920
+ a `policies` -> `policy` entry — the artifact door's `ARTIFACT_FIELD_TO_TYPE`
921
+ (`packages/metadata/src/plugin.ts`) and `AppPlugin`'s ADR-0057 `SECURITY_FIELDS`
922
+ list (`packages/runtime/src/app-plugin.ts`) — and **neither could ever match**.
923
+ `ObjectStackDefinitionSchema` is a `strictObject` that declares no top-level
924
+ `policies` key, so a definition carrying a `policies` array is refused outright
925
+ by the door's strict parse and reaches neither registry. The word is real but
926
+ lives one level down: on a permission set `policies` is an alias for
927
+ `rowLevelSecurity` (`PERMISSION_SET_KEY_ALIASES`) — a key on an **item**, never a
928
+ collection. Both entries are removed, each leaving in place the note the map
929
+ already writes for a retirement: what it pointed at, and why it could not match.
930
+
931
+ That was the third entry retired from `ARTIFACT_FIELD_TO_TYPE` for exactly this
932
+ reason (`themes`, then `roles` -> `positions`, which "matched nothing and
933
+ silently dropped compiled positions"). So the deletion ships with the thing the
934
+ two predecessors did not have — a check that fails when the pattern recurs:
935
+
936
+ - `check:stack-collection-maps` now reconciles **eight** hand-maintained
937
+ enumerations against the schema, not seven. `SECURITY_FIELDS` is the new
938
+ eighth, and how it was missing is the finding rather than a footnote: it is
939
+ the only one of the eight that pairs its keys as `[collection, kind]` tuples,
940
+ which neither existing extractor could read, so the site was skipped rather
941
+ than reported. Re-adding `policies` — or any other key the schema does not
942
+ declare — to **either** registrar now fails the gate with the site named.
943
+ - A new `tupleFirstItems` extractor reads that shape, with a self-test case
944
+ (13 assertions, up from 12) covering the comment/nesting cases the flat
945
+ string-array extractor already pins.
946
+
947
+ The mirror-image half of the same measurement is **carried, not shipped**:
948
+ `capabilities` is a declared top-level collection that `SECURITY_FIELDS`
949
+ registers and the door's map does not, making `AppPlugin` its sole registrar on
950
+ an artifact boot. Adding it to the door changes what an artifact boot registers,
951
+ so it is measured and handed to the route-ownership decision (#12892) instead of
952
+ being smuggled in here. The new waiver row records the asymmetry in place.
953
+ - b745157: feat(metadata-core,metadata): warn the operator when a pre-current-era artifact carries form-view predicates that fault open (#12915)
954
+
955
+ A form-view predicate binds `record` (+ `previous`, `parent`) in runtime record
956
+ forms, or `data` in metadata-editing forms. The contract states the failure mode
957
+ beside the vocabulary: **a bare identifier is unbound, the predicate faults, and
958
+ `visibleWhen`'s fault fallback is `true`** — so a field the predicate was
959
+ authored to hide renders for everyone.
960
+
961
+ That is quiet alone and lethal in combination with the authoring pattern it
962
+ serves. Measured on a real deployment: an artifact built by released
963
+ `@objectstack/cli` 17.1.0 authors
964
+ `{ field: 'disqualification_reason', required: true, visibleWhen: 'status == "unqualified"' }`
965
+ — the era's working spelling. On a 17.2 runtime the predicate faults open, the
966
+ conditionally hidden field renders, and its unconditional `required: true`
967
+ blocks **every** record creation through the console, while the same payload
968
+ POSTs 201 through REST. Nothing refused and nothing logged, so the operator —
969
+ the only person who can rebuild the artifact — had no signal at all.
970
+
971
+ The framework artifact door now emits **one deduped `warn` line per artifact**
972
+ naming the authored `engines.protocol` floor and the runtime spec version, how
973
+ many predicates on which views (with the first path as an anchor), the
974
+ fault-open consequence, and the remedy (`os build`). It rides the same funnel
975
+ that already carries the forward-conversion summaries, so both SaaS shapes are
976
+ covered: a single-DB multi-org runtime warns once at boot, and per-tenant-DB
977
+ kernels each warn at their own.
978
+
979
+ **No behaviour change.** No refusal, no rewrite, no schema or contract edit —
980
+ the predicate keeps faulting open exactly as before, and the artifact bytes are
981
+ untouched. Rewriting a bare root to `record.` is a separate, deferred ADR-0087
982
+ conversion.
983
+
984
+ **Scoped to legacy artifacts by construction.** The notice fires only inside the
985
+ versioned window the forward conversion already opens (declared floor below the
986
+ running spec, or undeclared), read off that pass's own verdict rather than
987
+ recomputed. An artifact declaring the current or a newer floor gets zero notices
988
+ from this feature even when it carries bare roots — the boundary that keeps a
989
+ notice about legacy artifacts out of contract territory.
990
+
991
+ Detection is exported from `@objectstack/metadata-core` as
992
+ `detectUnboundFormViewPredicateRoots` (with `BOUND_FORM_VIEW_PREDICATE_ROOTS`)
993
+ so other composed artifact doors can reuse one policy rather than fork it. It is
994
+ pure, read-only, and tuned to prefer silence over a false accusation: string
995
+ literals are stripped before the scan, only root position counts, call targets
996
+ are not roots, comprehension macros (whose iteration variable is locally bound)
997
+ are skipped whole, and AST-only envelopes pass.
998
+ - 6a180e4: fix(core,rest,services)!: a permission-store read failure now fails LOUD instead of resolving as an authenticated caller holding zero capabilities (#13279)
999
+
1000
+ **BREAKING** runtime behaviour change on the shared authorization resolver,
1001
+ shipped as `minor` under the repo's launch-window convention.
1002
+
1003
+ `resolveAuthzContext`'s per-read helper `tryFind` answered a THROWN read exactly
1004
+ the way it answered an EMPTY one: `[]`. So an outage of the permission store
1005
+ resolved as a well-formed context for an authenticated principal holding no
1006
+ capabilities, and the package-management door answered
1007
+ `403 FORBIDDEN` — "Reading packages requires the `studio.access` or
1008
+ `setup.access` capability." That answer was measured byte-identical
1009
+ (`JSON.stringify` equal, against a control that separates two answers which do
1010
+ differ) to what a caller who genuinely holds nothing receives. An administrator
1011
+ was told they lack a capability, during an outage of the store that holds the
1012
+ capability.
1013
+
1014
+ Maintainer ruling 2026-08-30, verbatim 「第一批其余同意」: `tryFind` 区分「无行」
1015
+ 与「读失败」,读失败 fail-loud —— 权限库不可达时不再解析为「已认证零能力」,而是
1016
+ 响亮拒绝(与真实能力拒绝的 403 可区分)。
1017
+
1018
+ Second maintainer ruling the same day (第 5 场总监席决裁批 #9, verbatim 「同意」),
1019
+ after implementing the first one showed that "the read failed" is two facts:
1020
+ 采**选项 A** —— 把 `isMissingTableError` 从 `@objectstack/metadata` 迁至
1021
+ `@objectstack/types`(core 已依赖),metadata 保留 re-export 兼容;`tryFind` 仅对
1022
+ **未被判定为「表未 provision」**的读失败抛 `AuthzStoreUnavailableError`。
1023
+
1024
+ **What changed.** A permission-store read that is issued and throws now raises
1025
+ `AuthzStoreUnavailableError`, which carries the EXISTING ADR-0112 wire code
1026
+ `SERVICE_UNAVAILABLE` and status `503`. No code is added to the closed wire
1027
+ vocabulary and no response envelope gains or loses a key — only which declared
1028
+ code an outage selects. Doors that map thrown errors through
1029
+ `resolveThrownHttpError` answer 503 with no per-door change.
1030
+
1031
+ **What did NOT change**, and is pinned:
1032
+
1033
+ - A reachable, genuinely EMPTY store (reads return no rows) still resolves to
1034
+ zero capabilities.
1035
+ - A genuine capability denial still answers `403 FORBIDDEN` with its message.
1036
+ - An ABSENT engine (`ql` unwired, so no read is ever issued) still resolves to
1037
+ an empty-but-valid envelope.
1038
+ - Anonymous requests never reach the store, so an outage cannot make them loud.
1039
+ - A REAL engine whose `sys_*` tables were never provisioned resolves to zero
1040
+ capabilities, quietly — pinned to be byte-identical to the empty-store
1041
+ envelope, in every dialect spelling and in the production wrapper shape where
1042
+ the driver's phrase is on `cause` rather than the outer message.
1043
+
1044
+ **The boundary between the two kinds of read failure.** An earlier revision of
1045
+ this changeset claimed "embedders without a data plane are unaffected". That
1046
+ claim was too broad; it is retracted here, and the gap it named is now closed
1047
+ rather than merely disclosed. A read also throws when the table was never
1048
+ PROVISIONED — a real engine, wired and reachable, whose `sys_*` tables were
1049
+ never created — and that is a supported deployment shape, not an outage. There
1050
+ "zero capabilities" is the TRUE answer rather than a fabrication: nothing is
1051
+ provisioned, so nothing was withheld. Only an UNREACHABLE store — the ruling's
1052
+ own word 不可达 — leaves the capability set unknown, and only an unknown answer
1053
+ may not be reported as a denial.
1054
+
1055
+ Treating the two alike was measured, not theorised: it turned four CI suites
1056
+ red, all from `no such table` on `sys_user` / `sys_member` /
1057
+ `sys_user_position` / `sys_user_permission_set`. Ordinary CRUD in
1058
+ `@objectstack/client` answered `503`; batch validation errors that owe `400`
1059
+ answered `503`, because authorization refused before validation ran; runtime
1060
+ notifications answered `401` where authenticated callers must be served `200`;
1061
+ and two `.integration.test.ts` noise guards reported that the driver and engine
1062
+ diagnostics for `sys_position` stopped being emitted — the eager throw aborted
1063
+ the resolution before that later read was ever issued, so a change made to stop
1064
+ a failed read being silent had made two other channels silent.
1065
+
1066
+ `tryFind` therefore raises `AuthzStoreUnavailableError` only for a read failure
1067
+ that is NOT positively identified as an unprovisioned table.
1068
+
1069
+ **`isMissingTableError` moved to `@objectstack/types`.** The classifier that
1070
+ draws that boundary already existed and was already right — driver-code based
1071
+ rather than prose-sniffing, documented so that "cannot say" never means "be
1072
+ loud". It lived in `@objectstack/metadata`, which DEPENDS ON `@objectstack/core`,
1073
+ so the resolver could not import it. Rather than keep a second copy of a
1074
+ security-relevant predicate, the ruling relocated the one classifier to
1075
+ `@objectstack/types` — the package core already depends on, and the repo's own
1076
+ stated Home rule for a cross-package error predicate ("every consumer of the
1077
+ question already depends on it, so adopting the predicate never adds an edge",
1078
+ `packages/types/src/unique-violation.ts`). `@objectstack/metadata/errors` still
1079
+ exports `isMissingTableError`, re-exported from the new home, so no consumer of
1080
+ that published subpath changes.
1081
+
1082
+ Its sibling `isSchemaAlreadyExistsError` moved with it — the two are not two
1083
+ modules but two signatures over one matcher, and separating them would have
1084
+ meant re-rolling the matcher, which is the duplication the module exists to
1085
+ prevent. Both are now exported from `@objectstack/types`; the metadata subpath
1086
+ deliberately still publishes only `isMissingTableError`, which is the only one
1087
+ anything imports through it.
1088
+
1089
+ ⚠️ **Signed-off risk, recorded because it is load-bearing.** Gating loudness on
1090
+ a driver-error predicate was approved with its false-positive direction stated:
1091
+ mis-reading a genuine outage as "table not provisioned" silently restores the
1092
+ quiet 403 this change removes, with no thrown error and no other failing test.
1093
+ That direction is accepted, not overlooked — the predicate keys on driver codes,
1094
+ SQLSTATEs and errnos first, excludes the known superstring traps up front, and
1095
+ returns `false` for anything it does not positively recognise, so an
1096
+ unrecognised outage stays loud by default. The risk is written beside the
1097
+ predicate in `resolve-authz-context.ts` and both directions are pinned by name
1098
+ in `authz-store-unavailable.test.ts`. ⛔ Do not widen `isMissingTableError` to
1099
+ make a first boot quieter: every widening moves outages into the quiet branch.
1100
+
1101
+ **All-transport, not just REST.** Every transport authorizing through
1102
+ `resolveAuthzContext` inherits this. Six of the eight production transports
1103
+ wrapped the call in a fail-closed `catch` that would have re-silenced the
1104
+ outage — measured, not assumed: with the resolver loud but the nets untouched,
1105
+ the package door answered `401`, i.e. the outage merely changed disguises. Those
1106
+ `catch` blocks now re-raise via `isAuthzStoreUnavailableError` and keep their
1107
+ previous behaviour for every other fault. The transport set is rebuilt from
1108
+ source and audited for set equality on every test run, so a transport added
1109
+ later cannot inherit the old silence unnoticed.
1110
+
1111
+ Callers that treat any throw from `resolveAuthzContext` as "anonymous" should
1112
+ re-raise `isAuthzStoreUnavailableError(err)` instead: degrading it restores the
1113
+ disguise this removes.
1114
+
1115
+ <!-- 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. -->
1116
+ - e7191ce: fix(build): give each `exports` condition its own `types` target in the 28 dual-build packages (#13112)
1117
+
1118
+ **Published-surface change, zero runtime change.** No emitted byte moves; what
1119
+ moves is which declaration file a resolver READS. Maintainer ruling 2026-08-29
1120
+ (decision batch #3, verbatim 「同意」) chose declaring the files over deleting
1121
+ them.
1122
+
1123
+ ## What was wrong
1124
+
1125
+ These 28 packages are `"type": "module"` and dual-built, and each spelled one
1126
+ `types` condition as a **sibling** of `import`/`require`:
1127
+
1128
+ ```json
1129
+ "exports": { ".": {
1130
+ "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs"
1131
+ } }
1132
+ ```
1133
+
1134
+ A sibling `types` answers for **both** conditions, so a CommonJS consumer was
1135
+ handed `dist/index.d.ts` — an ES-module declaration, because the package is
1136
+ `"type": "module"` — for an entry point it reaches with `require`. Measured with
1137
+ `tsc --traceResolution` on a `"type": "commonjs"` fixture at `moduleResolution:
1138
+ node16`:
1139
+
1140
+ ```
1141
+ error TS1479: The current file is a CommonJS module whose imports will produce
1142
+ 'require' calls; however, the referenced file is an ECMAScript module and cannot
1143
+ be imported with 'require'.
1144
+ ```
1145
+
1146
+ The JavaScript at `dist/index.cjs` loads perfectly (`check:dual-build-cjs-loads`
1147
+ has asserted that for months). It is the **types** that told the consumer the
1148
+ supported `require` entry point could not be required. The `dist/index.d.cts`
1149
+ twin tsup emits beside it — 36 files, 5,517,701 B on this build — was named by
1150
+ no condition at all and shipped in every tarball unreachable.
1151
+
1152
+ ## What changed
1153
+
1154
+ Each condition now names its own declaration, the shape TypeScript documents:
1155
+
1156
+ ```json
1157
+ "exports": { ".": {
1158
+ "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
1159
+ "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
1160
+ } }
1161
+ ```
1162
+
1163
+ 33 entry points across 27 packages, subpaths included. The root `types` field is
1164
+ untouched, so `node10` resolvers are unaffected; the `import` condition resolves
1165
+ exactly what it resolved before, measured as an unchanged control in the same
1166
+ run.
1167
+
1168
+ ## `@objectstack/core` is deliberately NOT changed
1169
+
1170
+ Splitting a declaration in two makes TypeScript compare it nominally, and
1171
+ `ObjectKernel` carries a `private plugins` member that reaches every plugin
1172
+ through `PluginContext.getKernel()`. With core split, whole-repo `pnpm build`
1173
+ fails in `@objectstack/verify` with 5 × TS2345 ("Types have separate
1174
+ declarations of a private property 'plugins'"); with core held back and the
1175
+ other 27 split, 71/71 tasks pass. So core keeps the sibling-`types` shape and
1176
+ its two `.d.cts` files (220,854 B) stay unreachable, declared as such in
1177
+ `check:dual-build-cjs-loads`. Splitting it needs a decision about core's public
1178
+ types, not about an exports map.
1179
+
1180
+ ## For consumers
1181
+
1182
+ - **ESM consumers: nothing changes.** Same declaration file, byte for byte.
1183
+ - **CJS consumers under `node16`/`nodenext`: TS1479 goes away** and the
1184
+ declarations they get are the ones built for CommonJS.
1185
+ - **`node10` / `moduleResolution: node` consumers: nothing changes** — they never
1186
+ read `exports`.
1187
+ - Nothing is removed: every path that resolved before still resolves.
1188
+
1189
+ Packages that are CJS-first (`require` → `./dist/index.js`, no `"type": "module"`)
1190
+ were already correct and are untouched — their `dist/index.d.ts` really is the
1191
+ CommonJS declaration. Their ESM mirror (an unreachable `.d.mts` under the
1192
+ `import` condition) is a separate, larger population and is filed separately per
1193
+ the ruling, not fixed here.
1194
+
1195
+ `check:dual-build-cjs-loads` grew a fourth invariant (TYPED) that reds on the old
1196
+ shape, so the drift cannot return silently.
1197
+ - 2e471e3: `FilesystemLoader` now implements `loadManyKeyed()`, so a metadata file whose
1198
+ body has no top-level `name` is no longer invisible to `MetadataManager.list()`.
1199
+ `loadMany()` globbed files and pushed bodies, discarding the path it had just
1200
+ read; the manager then fell back to keying by `body.name`, which drops every
1201
+ nameless body — an aggregated `defineView` container has none by design. That is
1202
+ the #14205 defect, unrepaired for this loader until now.
1203
+
1204
+ The key is this loader's own name-to-path derivation — the basename minus
1205
+ extension, the same one `list()` reports — but only where that derivation is a
1206
+ bijection for the file: it sits directly under `ROOT/TYPE/` and carries an
1207
+ extension `findFile()` tries, so `findFile(type, key)` resolves back to that same
1208
+ file. Every other shape (a nested path, an extension-less file) keeps the
1209
+ previous behaviour verbatim: keyed by `body.name` when it has one, dropped when
1210
+ it has none. `list()` and `findFile()` disagree outside the flat shape — `list()`
1211
+ reports the bare basename for a nested file and `findFile()` cannot resolve it —
1212
+ so keying those by the basename would mint names `get()` and `exists()` cannot
1213
+ open, and two directories holding one basename would collide silently. Repairing
1214
+ the derivation itself is tracked separately.
1215
+
1216
+ One deliberate consequence: a flat file whose `body.name` disagrees with its
1217
+ basename is now keyed by the basename. That is #14205's rule (identity is the
1218
+ key the store holds an item under, not `body.name`) applied to this loader, and
1219
+ it aligns `list()` with `listNames()` for that shape. `loadMany()`'s own
1220
+ signature and answer are unchanged; both methods now share one file walk so
1221
+ their bodies cannot drift.
1222
+ - 200d255: fix(metadata-core,metadata): a form SECTION binds `current_user` too, so the unbound-root notice stops flagging one (#13072)
1223
+
1224
+ Second correction to the unbound-root boot notice, and the same defect as the
1225
+ first one a surface later. The notice judged a SECTION-level predicate against
1226
+ `record` / `previous` / `parent` / `data`, sourced faithfully from the section
1227
+ contract prose — which was stale.
1228
+
1229
+ `current_user` and its ADR-0068 alias roots (`user`, `ctx.user`, `os.user`)
1230
+ **resolve on a section-level `visibleWhen`**: objectui#6110 threads the host
1231
+ shell's predicate scope into `isSectionVisible` where it used to pass
1232
+ `undefined`, and objectui#6111 copies the authored `visibleWhen` onto the
1233
+ `section-divider` pseudo-field whose predicate the SDUI form renderer evaluates
1234
+ with that scope bound. #12914 re-measured the contract text accordingly. Until
1235
+ this change, a legacy artifact carrying a legitimate section-level
1236
+ `current_user.role == "admin"` predicate was reported at boot as an unbound root
1237
+ that faults open — a notice about a predicate that resolves, which is the
1238
+ cry-wolf failure the module's own doc forbids and the one that trains operators
1239
+ to ignore the channel.
1240
+
1241
+ **What changes:** one vocabulary now serves both form-view predicate surfaces —
1242
+ `record`, `previous`, `parent`, `data`, `current_user`, `user`, `ctx`, `os`. A
1243
+ section predicate rooted at the `current_user` family is silent; a section
1244
+ predicate rooted at a bare field identifier is still reported, and the operator
1245
+ line still prints the rule per surface for the surfaces the findings implicate.
1246
+
1247
+ **Blast radius, stated without inflation:** this is a **notice**, not a refusal
1248
+ — no parse change, no gate, no behaviour change, and it only runs inside the
1249
+ versioned window `applyArtifactForwardConversions` opens. The cost it removes is
1250
+ a false operator signal on legacy artifacts, not a broken runtime.
1251
+
1252
+ **Removed export, with its migration:** `FIELD_ONLY_BOUND_PREDICATE_ROOTS` is
1253
+ gone from `@objectstack/metadata-core`. The section binding empties it, and an
1254
+ exported constant named `FIELD_ONLY_…` holding `[]` asserts a per-surface
1255
+ difference that no renderer makes. FROM → TO: read
1256
+ `BOUND_FORM_VIEW_PREDICATE_ROOTS` (every root bound on any form-view predicate)
1257
+ or `BOUND_FORM_FIELD_PREDICATE_ROOTS` (the field question, the same list today).
1258
+ No consumer can be carrying it: the notice has never shipped — the two
1259
+ changesets that introduce it are still pending in `.changeset/`, the newest
1260
+ published `@objectstack/metadata-core` is 17.2.0, and the commit that added
1261
+ `form-predicate-root-policy.ts` is in no release tag. This was the last moment
1262
+ at which the removal cost nothing.
1263
+
1264
+ **Why the vocabulary is no longer justified by quoting the contract.** Both
1265
+ times this list has been wrong, it was wrong by transcribing a correct-looking
1266
+ sentence that the renderer had already moved past. The prose is a transcription
1267
+ of a renderer and can only lag one, so membership is now stated as the mechanism
1268
+ — *a root is bound on a surface iff some renderer threads a scope carrying it
1269
+ into that surface's evaluator* — with the threading site named per entry, and
1270
+ the module's test reads the LIVE `.describe()` text of
1271
+ `FormFieldSchema.visibleWhen` / `FormSectionSchema.visibleWhen` out of
1272
+ `@objectstack/spec` instead of copying it into a comment. A comment quoting that
1273
+ sentence goes stale in silence, twice now; an assertion that fetches it cannot.
1274
+
1275
+ <!-- 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. -->
1276
+ - 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)
1277
+
1278
+ Same-day correction to the unbound-root boot notice. The notice judged **every**
1279
+ form-view predicate against one vocabulary (`record` / `previous` / `parent` /
1280
+ `data`), sourced faithfully from the contract prose — which, for the field-level
1281
+ slot, was stale.
1282
+
1283
+ `current_user` and its ADR-0068 alias roots (`user`, `ctx.user`, `os.user`)
1284
+ **resolve on a field-level `visibleWhen`** since objectui#6010; three spec text
1285
+ sites still said otherwise until #12930 re-measured them, and one of those sites
1286
+ was the sentence this policy was written against. A legacy artifact carrying a
1287
+ legitimate `current_user.role == "admin"` field predicate was therefore reported
1288
+ as faulting open — the cry-wolf failure the notice is explicitly built to avoid,
1289
+ and the one that trains operators to ignore the channel.
1290
+
1291
+ The vocabulary is now per surface, which is what the contract actually says:
1292
+
1293
+ - **Field-level** (`BOUND_FORM_FIELD_PREDICATE_ROOTS`): the shared base plus
1294
+ `current_user`, `user`, `ctx`, `os`. Silent on all of them.
1295
+ - **Section-level** (`BOUND_FORM_VIEW_PREDICATE_ROOTS`, unchanged in name and
1296
+ value): the base alone. `current_user` is still flagged there — the section
1297
+ docblock states it is unbound at that level and faults open.
1298
+
1299
+ Two limits of the field binding deliberately do **not** change the answer: it is
1300
+ a rendering rule rather than authorization (an authoring hazard, not a
1301
+ version-drift one), and the scope is empty on the console's public `/f/:slug`
1302
+ route (equally true of a freshly built current artifact, so it says nothing
1303
+ about the artifact's era — the only thing this notice claims to detect).
1304
+
1305
+ The emitted warn line now prints the bound roots **per surface, and only for the
1306
+ surfaces the findings implicate**, so an operator is never shown a rule their
1307
+ artifact has no instance of. Findings carry a `surface` field.
1308
+
1309
+ `unboundRootsInCelSource` takes the vocabulary as an optional second argument;
1310
+ its default is unchanged (the stricter base), so existing callers behave exactly
1311
+ as before.
1312
+ - f7be03f: fix(metadata): `getViewsByObject()` expands aggregated view containers instead of answering empty (#13913)
1313
+
1314
+ `MetadataManager.getViewsByObject()` reads `this.list('view')` — the manager's
1315
+ own registry + loader store, which is a completely different store from the
1316
+ `sys_metadata` rows the REST route (`GET /meta/view?object=`) reads through
1317
+ `getMetaItems`. #13407 taught that route to expand a runtime-authored aggregated
1318
+ `defineView` container inline; this exit never called it and had no equivalent
1319
+ step, so a container the REST route now serves still answered **empty** here —
1320
+ for every internal/SDK caller that uses this entry point rather than the route.
1321
+
1322
+ Getting the container into the store was never enough on its own: the filter
1323
+ also requires `viewKind`, and a container has none. Relaxing that requirement
1324
+ would answer with the container itself as a view — the behaviour #7163 ruled
1325
+ wrong — so the repair adds the container's **expansion**, whose items each carry
1326
+ the `viewKind` + `object` pair this filter has always tested. The filter is
1327
+ untouched; it reads the top-level `object` exactly as `ViewSchema.object`
1328
+ declares.
1329
+
1330
+ The expansion is registry-free and per-read, mirroring #13407's choice at the
1331
+ other exit and for the same reason: the registry is process-wide, so a read must
1332
+ not graft rows into it. Already-present names win, so a container whose expanded
1333
+ ViewItems were registered by a source registrar (the ObjectQL boot loop, the
1334
+ artifact/HMR loader) still answers with those registered, fully-enriched items
1335
+ and gains nothing new.
1336
+
1337
+ The object-derivation chain (`object` → `list.data.object` → `form.data.object`
1338
+ → the row's own `name`) now has one spelling for this package, in the new
1339
+ `view-container-expansion.ts`, rather than a third private copy to fall behind.
1340
+ - e7f56d6: Make the settings engine facade and the metadata database loader bind the row
1341
+ they resolved, not a row the payload names
1342
+
1343
+ Two ingresses resolved an authoritative row id and then folded it into the
1344
+ write payload with the **losing** spread order — `{ id, ...data }` — so a
1345
+ caller-supplied `data.id` spread over the id the ingress had just resolved and
1346
+ silently retargeted the write:
1347
+
1348
+ - `wrapEngineAsSettingsEngine`'s by-id `update` branch
1349
+ (`@objectstack/service-settings`), whose id comes from the caller's
1350
+ `where.id`.
1351
+ - `DatabaseLoader._update` (`@objectstack/metadata`), whose id arrives as a
1352
+ separate parameter every caller resolves first (`existing.id`, from the read
1353
+ immediately above).
1354
+
1355
+ Both now spell it `{ ...data, id }` — the operation's id **after** the spread,
1356
+ so it wins. That is the convention the repo's other two ingresses already
1357
+ document: `rest-server.ts`'s batch update arm ("the operation's id AFTER the
1358
+ spread, so it wins") and `protocol.updateData`'s #6479 fix
1359
+ (`{ ...request.data, id: request.id }`).
1360
+
1361
+ **No wrong write is known to have been reachable.** Both sites' current callers
1362
+ build fresh field literals and never put an `id` inside `data`, so this is
1363
+ hardening a fragile pattern rather than repairing a measured defect. What makes
1364
+ it worth the three characters is that neither site can be caught downstream:
1365
+ both pass **no `where`** to the engine, so the payload is the only id the engine
1366
+ ever sees, and the engine's conflicting-id refusal (`UPDATE_ID_MISMATCH`, 400)
1367
+ needs two disagreeing declarations before it can fire. The fold is the entire
1368
+ trust boundary at both sites, and it is one refactor — a caller handing back a
1369
+ row copy, and rows carry `id` — from the #6479 shape.
1370
+
1371
+ Both are pinned with a payload whose `id` names a **different** row than the
1372
+ one the ingress resolved, asserting the resolved row is still the row bound. A
1373
+ pin exercising a payload without an `id` would have passed against both
1374
+ spellings. The doubles answer "which row does this bind?" with the producer's
1375
+ own `assertEngineUpdateDispatch`, so they cannot be kinder about it than a
1376
+ running server.
1377
+ - 41aa979: Artifact/HMR loader: read a view container's own top-level `object` field
1378
+
1379
+ `MetadataPlugin`'s artifact/HMR registrar derived which object a `defineView`
1380
+ container binds to by walking `list.data.object` then `form.data.object`, and
1381
+ never consulted the container's own top-level `object` — the field
1382
+ `ViewSchema.object` documents as "how a stack-level `views: [...]` entry says
1383
+ which object its views belong to; read by `getViewsByObject()` /
1384
+ `GET /meta/view?object=`".
1385
+
1386
+ A package-shipped `defineView({ object: 'crm_lead', list: { columns: [...] } })`
1387
+ therefore registered nothing at all through this path: the container was dropped
1388
+ before registration, so no expanded `crm_lead.<key>` ViewItems were produced and
1389
+ `getViewsByObject('crm_lead')` / `GET /meta/view?object=crm_lead` answered empty
1390
+ for it. Both derivation sites now call the package's single spelling of that
1391
+ derivation (`deriveViewContainerObject`), which consults the container's own
1392
+ `object` first and keeps the existing `list.data.object` → `form.data.object` →
1393
+ row-name fallback unchanged for every container written before that field was
1394
+ read here. This is the same order #13407 settled at the runtime door.
1395
+ - a7002ce: Canonicalise driver-materialised timestamps at the metadata adapter boundaries
1396
+
1397
+ `MetadataItem.authoredAt` is declared `z.string()` ('ISO-8601 timestamp') and
1398
+ `MetadataStats.mtime` is declared `z.string().datetime()`, but three producers
1399
+ adapted a driver row into those declared types without converting the value.
1400
+ `created_at` / `updated_at` are builtin audit columns and `recorded_at` is a
1401
+ declared `Field.datetime`; `SqlDriver#formatOutput` repairs both only inside its
1402
+ `if (this.isSqlite)` arm, so on Postgres and MySQL a JS `Date` landed in a field
1403
+ every consumer reads as a `string`.
1404
+
1405
+ `SysMetadataRepository#get` / `#getByHash` and `DatabaseLoader#stat` now emit
1406
+ canonical ISO-8601 text on every dialect, matching the sibling producers that
1407
+ already spelled it correctly. Values that were already canonical (SQLite) pass
1408
+ through byte-identically.
1409
+ - 77a532d: Five metadata adapter boundaries now emit the ISO-8601 string their declared type promises
1410
+ when the driver hands them a JS `Date`, instead of asserting `as string` over it
1411
+
1412
+ `MetadataRecord.createdAt` / `.updatedAt` and `MetadataHistoryRecord.recordedAt` are
1413
+ declared `z.string().datetime()`, and `MetadataEvent.ts` is declared `z.string()`. Four
1414
+ producers in `DatabaseLoader` (`rowToRecord`, `getHistoryRecord`, `queryHistory`) and one
1415
+ in `SysMetadataRepository` (`rowToEvent`) reached those fields through an unchecked
1416
+ `row.<column> as string` cast, which is an assertion about a driver row rather than a
1417
+ measurement of one — so nothing type-checked and nothing reported it.
1418
+
1419
+ On Postgres and MySQL the assertion is false for both column classes involved.
1420
+ `SqlDriver#formatOutput` repairs the builtin audit columns and folds declared
1421
+ `Field.datetime` columns only inside its `if (this.isSqlite)` arm, and
1422
+ `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp` deliberately untouched
1423
+ because those are instants. A column being declared `Field.datetime` therefore does **not**
1424
+ protect it: on the production default driver both classes come out of the record read door
1425
+ as a `Date`, and `.datetime()` is a refinement a `Date` fails outright. Nothing has failed
1426
+ yet only because no production path parses these values today.
1427
+
1428
+ The repair is producer-side, at the adapter boundary that asserts the declared type — not
1429
+ a tolerant fallback in a consumer, and not a change at the driver's read door, which would
1430
+ reverse a deliberate driver decision. Callers keep their existing behaviour for every other
1431
+ shape: an already-canonical SQLite string passes through byte-identically, an absent column
1432
+ still yields `undefined` so each caller's `?? <default>` chain means what it meant, and an
1433
+ Invalid `Date` is handed through unchanged rather than converted, because what the shared
1434
+ canonical-ISO spelling should do with that one input is still being decided.
1435
+ - 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.
1436
+ - 8225248: fix(metadata): drop the leftover boot-time debug probe from `MetadataPlugin.init` (#14527)
1437
+
1438
+ `MetadataPlugin.init()` printed a bare `console.log` immediately after
1439
+ `ctx.registerService('metadata', this.manager)`, reporting
1440
+ `typeof this.manager.getRegisteredTypes`. Nothing gated it on `NODE_ENV`, a debug
1441
+ flag or a logger level, so every kernel boot that installs `MetadataPlugin` wrote
1442
+ it to stdout: the CLI, the dev server and any embedding host alike.
1443
+
1444
+ What it printed was a probe, not information. `getRegisteredTypes` is a method the
1445
+ class declares statically, so the `typeof` it reported cannot vary in a way an
1446
+ operator could act on, and a repo-wide search finds no reader of the string. The
1447
+ `ctx.logger.info('Initializing Metadata Manager', ...)` call three lines above
1448
+ already announces the same lifecycle step through the plugin's own logger, carrying
1449
+ the fields that are actually actionable (`root`, `watch`, `artifactSource`). Deleted
1450
+ rather than demoted to `ctx.logger.debug` on that reading: once the reported fact is
1451
+ statically known and unread, there is no shape check left worth keeping.
1452
+
1453
+ Observable change: one fewer line on stdout at boot. No API, no types, no behaviour
1454
+ beyond the removed print. On the two paths where a stray stdout line would break a
1455
+ parser — `--json` payloads and the `os serve` protocol channel — the CLI already
1456
+ reserves stdout by redirecting to stderr, so nothing downstream was relying on this
1457
+ line's presence or its absence.
1458
+
1459
+ The file's five other `console.*` calls (the dev HMR path) are deliberately
1460
+ untouched: each carries its own marker, and the one that prints on every
1461
+ non-development boot carries a written rationale for doing so.
1462
+ - 0ae9e1e: fix(metadata): `migrateProjectIdToEnvironmentId`'s raw-driver guard stated its instruction sentence twice (#13219)
1463
+
1464
+ An operator who called `migrateProjectIdToEnvironmentId` with a driver that has
1465
+ no `raw()` was refused correctly, but read the same remedy twice in one message:
1466
+
1467
+ ```
1468
+ 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.
1469
+ ```
1470
+
1471
+ The sentence was concatenated twice, a copy-paste artifact — the sibling
1472
+ `migrateEnvIdToProjectId` carries the correct single-sentence form of the
1473
+ identical guard. Cosmetic and operator-facing only: the guard fires on exactly
1474
+ the same condition, the remedy it names is unchanged, and nothing parses the
1475
+ message. The duplicate line is deleted; the surviving sentence keeps the
1476
+ trailing space that separates it from the one naming the conforming drivers.
1477
+
1478
+ The refusal case in the package's tests now pins the properties — the
1479
+ instruction appears exactly once, no sentence runs into the next, and the
1480
+ supporting sentence is still present — rather than substring-matching the
1481
+ message, which could not see a second copy and so passed either way.
1482
+ - ef76342: fix(metadata): stop `migrateProjectIdToEnvironmentId` renaming into a column no declaration knows about (#13205)
1483
+
1484
+ `AFFECTED_TABLES` was a hand-written list, and it outlived the declarations it
1485
+ described. The branch/project-removal amendment (M1) took `environment_id` out
1486
+ of `sys_metadata_history`'s declaration in `@objectstack/metadata-core`; the
1487
+ migration kept naming that table. Its only guard asks whether `project_id` is
1488
+ present **physically** (`_columnExists`) — which says nothing about the target
1489
+ column being **declared** — so against any database whose physical
1490
+ `sys_metadata_history` still carried the pre-v5 column, the migration renamed it
1491
+ to `environment_id`: a fresh orphan column that no declaration, no `syncSchema`
1492
+ and no reader knows about.
1493
+
1494
+ The list is now **derived from the declarations** rather than restated beside
1495
+ them. A candidate table is migrated only if its current declaration carries the
1496
+ target column, so the two cannot drift apart again — the derivation and the
1497
+ declaration are the same fact. `@objectstack/metadata-core` was already a
1498
+ dependency of this package, so this adds no dependency edge.
1499
+
1500
+ A candidate that does not declare the target column is now **reported** as
1501
+ `status: 'skipped_not_declared'` (an additive member of the result union)
1502
+ instead of silently vanishing from the result array: an operator reading the
1503
+ results can tell "considered and deliberately skipped" from "forgotten again",
1504
+ which is the state this defect started in.
1505
+
1506
+ The sibling `migrateEnvIdToProjectId` is deliberately left alone: its target
1507
+ (`project_id`) is an intermediate column that no current declaration carries by
1508
+ design, so the "target must be declared" rule is sound only for the terminal
1509
+ migration in the chain.
1510
+
1511
+ No behaviour changes for `sys_metadata`, whose declaration does carry
1512
+ `environment_id`: it is renamed exactly as before.
1513
+ - 2a18117: fix(metadata): every migration in `@objectstack/metadata/migrations` refused every driver this repo ships (#14023)
1514
+
1515
+ All four helpers exported from `@objectstack/metadata/migrations` guarded on —
1516
+ and drove through — `driver.raw(sql, bindings?)`. **No data driver in this repo
1517
+ defines `raw`.** `SqlDriver` keeps its knex handle `protected` and declares no
1518
+ `raw` member, and `SqliteWasmDriver` inherits that; the only `raw(` member
1519
+ anywhere outside a test double is an HTTP harness in `packages/verify` whose
1520
+ signature is `(path, init)`. So an operator who passed their platform driver was
1521
+ refused by all four:
1522
+
1523
+ ```
1524
+ migrateSysNotificationToEvent({ driver, data }) -> { status: 'error', migrated: 0 }
1525
+ ```
1526
+
1527
+ The failure was quiet in the shape that matters. `migrateSysNotificationToEvent`
1528
+ *returns* `{ status: 'error' }` rather than throwing, and the message blamed the
1529
+ caller's driver for lacking a method instead of saying the migration had not
1530
+ run — so someone following the ADR-0030 cut-over runbook, which names this call
1531
+ as the supported way to preserve users' existing bell notifications, would read
1532
+ it as a problem with their own driver.
1533
+
1534
+ It was not only an operator-facing path. `DatabaseLoader` calls
1535
+ `migrateProjectIdToEnvironmentId(driver)` on bootstrap with a real driver, at
1536
+ two call sites, each wrapped in a catch — so the v5.0 `project_id` ->
1537
+ `environment_id` forward migration threw and was swallowed on every boot.
1538
+
1539
+ The four helpers now resolve their raw-SQL entry point through one shared
1540
+ resolver (`src/migrations/driver-exec.ts`) that tries `execute` first and falls
1541
+ back to `raw`. `execute` goes first because it is the surface the contract
1542
+ declares: `IDataDriver` (`@objectstack/spec/contracts`) declares
1543
+ `execute(command, parameters?, options?)` **non-optionally**, with bound
1544
+ parameters as the second positional argument — exactly the shape `raw(sql,
1545
+ bindings?)` was being called in — and has never declared `raw`. `raw` is kept as
1546
+ a fallback so a host or third-party driver that does define it keeps working;
1547
+ nothing that worked before stops working, and the refusal now fires only for a
1548
+ driver offering neither surface.
1549
+
1550
+ Two sibling directories already resolved both surfaces instead of assuming one,
1551
+ in opposite orders (`metadata-protocol`'s `partial-index-probe` tries `raw`
1552
+ first, its `seed-tenancy-backfill` tries `execute` first, and `protocol.ts`'s
1553
+ `ensureOverlayIndex` is a third). One operation with three implementations and
1554
+ two behaviours resolves to the declaration-bound side, which is why this
1555
+ directory adopts `execute`-first uniformly rather than copying either precedent.
1556
+
1557
+ The refusal message now names both surfaces. It keeps the properties pinned
1558
+ after the doubled-sentence defect: the remedy is stated exactly once, the
1559
+ sentences stay separated, and a conforming driver is still named.
1560
+
1561
+ Tests: every pre-existing case in this directory built its own double carrying a
1562
+ `raw` method — including the case asserting the guard fires — so the suite
1563
+ pinned the guard's wording while never exercising a driver the platform ships.
1564
+ Swapping `raw` for `execute` in the helpers and in the doubles would have moved
1565
+ that hole rather than closed it. A new `real-driver-exec-surface.test.ts` drives
1566
+ all four migrations through a real `SqliteWasmDriver` against real in-process
1567
+ SQLite, asserting the physical schema rather than the returned status, and pins
1568
+ the surface reality the file exists for: the real driver has no `raw` and does
1569
+ have `execute`.
1570
+ - c9d4de3: fix(metadata): `migrateSysNotificationToEvent` writes canonical ISO timestamps, not `Date.prototype.toString` (#13998)
1571
+
1572
+ `selectLegacyRows` reads the legacy `sys_notification` table through
1573
+ `driver.raw`/`execute` — a door that does not run `formatOutput`, so none of its
1574
+ repairs apply. On SQLite the legacy stamps come back as canonical ISO text and
1575
+ `String(row.created_at)` is the identity. On Postgres and MySQL an instant
1576
+ column materialises as a JS `Date`, so the migration wrote
1577
+
1578
+ ```
1579
+ Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)
1580
+ ```
1581
+
1582
+ into `created_at` on the new `sys_inbox_message` row and into `created_at` / `at`
1583
+ on the new `sys_notification_receipt` row — whole seconds in the **migrating
1584
+ host's** zone with the milliseconds dropped, or a value no dialect's timestamp
1585
+ grammar accepts at all. The migration is one-way, so that spelling is what the
1586
+ platform would carry afterwards.
1587
+
1588
+ Both stamps are now canonicalised at the migration, matching the repo's existing
1589
+ correct form: a `Date` is rendered with `toISOString()`, ISO text passes through
1590
+ untouched. Neither column could be repaired further upstream — `created_at` is a
1591
+ builtin audit column that `formatOutput` repairs only in its `if (this.isSqlite)`
1592
+ arm, and `read_at` is a legacy column ADR-0030 removed from the object, so it is
1593
+ not a declared `Field.datetime` either and no coercion could ever reach it.
1594
+
1595
+ Pinned with a hand-made `Date` driven through the migration's read path under a
1596
+ forced process zone, which is what breaks the SQLite identity that kept the
1597
+ existing cases green while the defect was live.
1598
+ - a23603e: fix(runtime,metadata): the default local environment id is `env_local`, not `proj_local` (#13366)
1599
+
1600
+ The v5.0 `project` to `environment` rename changed the default local environment
1601
+ id and shipped that change on the surfaces most people meet: `packages/cli`'s
1602
+ `CHANGELOG.md` records "Default local env id: `proj_local` -> `env_local`", the
1603
+ `os dev` / `os start` / `os serve` commands emit `env_local`, and
1604
+ `content/docs/deployment/cli.mdx` documents `env_local` as the default. Two
1605
+ sites never received it and kept stamping `proj_local`.
1606
+
1607
+ FROM: `createStandaloneStack()` — with no `environmentId` in its config and no
1608
+ `OS_ENVIRONMENT_ID` in the environment — stamped `proj_local` on the kernel it
1609
+ composed, and `MetadataPlugin` used `proj_local` to fill the environment-artifact
1610
+ validation envelope for a bare definition.
1611
+
1612
+ TO: both stamp `env_local`.
1613
+
1614
+ WHO SEES IT. Two audiences, both on the DEFAULT path — no `environmentId` in
1615
+ the config and no `OS_ENVIRONMENT_ID` in the environment:
1616
+
1617
+ 1. a host that calls `createStandaloneStack` / `createDefaultHostConfig`
1618
+ **directly**;
1619
+ 2. a **bare `os serve`** — one not spawned by `os dev` / `os start`. Those two
1620
+ commands export `OS_ENVIRONMENT_ID=env_local` into the child process, which
1621
+ the fallback yields to, so a boot they start never reached the changed line.
1622
+ `os serve` sets no such variable for its own boot: it only READS one to name
1623
+ the runtime state file. So a bare `os serve` used to run a kernel stamped
1624
+ `proj_local` while publishing `runtime.env_local.json` beside it; the two now
1625
+ agree.
1626
+
1627
+ Where the id is observable — row scoping in `ObjectQLPlugin`, the
1628
+ `X-Environment-Id` header, `sys_metadata.environment_id` — such an embedder now
1629
+ sees `env_local` where it saw `proj_local`, so an install with rows already
1630
+ written under the old id should set `environmentId: 'proj_local'` (or
1631
+ `OS_ENVIRONMENT_ID=proj_local`) explicitly to keep them addressed. That escape
1632
+ hatch is unchanged and still wins over the default.
1633
+
1634
+ NOT CHANGED, deliberately: `@objectstack/cloud-connection` still treats BOTH
1635
+ spellings as the local sentinel, so a persisted `OS_ENVIRONMENT_ID=proj_local`
1636
+ config keeps being recognised as local rather than presented to the control
1637
+ plane as a cloud environment id; and `package-state-store`'s separate `'default'`
1638
+ fallback keeps its own spelling, because renaming it would re-key persisted
1639
+ package-disable state files.
1640
+ - Updated dependencies [809d417]
1641
+ - Updated dependencies [387e231]
1642
+ - Updated dependencies [f794e4e]
1643
+ - Updated dependencies [cae2169]
1644
+ - Updated dependencies [b812a54]
1645
+ - Updated dependencies [2d4fa75]
1646
+ - Updated dependencies [0e4e51b]
1647
+ - Updated dependencies [e84bbf6]
1648
+ - Updated dependencies [effae80]
1649
+ - Updated dependencies [efb3513]
1650
+ - Updated dependencies [d62f990]
1651
+ - Updated dependencies [c45d8e6]
1652
+ - Updated dependencies [2e3e8c7]
1653
+ - Updated dependencies [e621291]
1654
+ - Updated dependencies [655b106]
1655
+ - Updated dependencies [40a93b5]
1656
+ - Updated dependencies [101ad2c]
1657
+ - Updated dependencies [d5b330d]
1658
+ - Updated dependencies [dda969c]
1659
+ - Updated dependencies [1f45690]
1660
+ - Updated dependencies [277948f]
1661
+ - Updated dependencies [8bdd955]
1662
+ - Updated dependencies [54e2d36]
1663
+ - Updated dependencies [b745157]
1664
+ - Updated dependencies [f3bbbef]
1665
+ - Updated dependencies [4f24e9d]
1666
+ - Updated dependencies [e27583e]
1667
+ - Updated dependencies [4bd6faa]
1668
+ - Updated dependencies [86cbe37]
1669
+ - Updated dependencies [6a180e4]
1670
+ - Updated dependencies [474242f]
1671
+ - Updated dependencies [63cd487]
1672
+ - Updated dependencies [bd4aa4e]
1673
+ - Updated dependencies [803eaab]
1674
+ - Updated dependencies [f8e8f03]
1675
+ - Updated dependencies [983edf1]
1676
+ - Updated dependencies [eae824e]
1677
+ - Updated dependencies [f6fa22c]
1678
+ - Updated dependencies [8a483b3]
1679
+ - Updated dependencies [3bc2e38]
1680
+ - Updated dependencies [97bcd99]
1681
+ - Updated dependencies [df59de0]
1682
+ - Updated dependencies [96e25a8]
1683
+ - Updated dependencies [f75a38a]
1684
+ - Updated dependencies [7a25e7d]
1685
+ - Updated dependencies [1fa05a6]
1686
+ - Updated dependencies [c85a265]
1687
+ - Updated dependencies [dcb10a5]
1688
+ - Updated dependencies [773a999]
1689
+ - Updated dependencies [35dffea]
1690
+ - Updated dependencies [d8024f0]
1691
+ - Updated dependencies [8120808]
1692
+ - Updated dependencies [776a098]
1693
+ - Updated dependencies [5060877]
1694
+ - Updated dependencies [4f6325d]
1695
+ - Updated dependencies [52954c0]
1696
+ - Updated dependencies [2aa8456]
1697
+ - Updated dependencies [d23ebb9]
1698
+ - Updated dependencies [93809a3]
1699
+ - Updated dependencies [7c0d0c3]
1700
+ - Updated dependencies [daae7aa]
1701
+ - Updated dependencies [8dc22d6]
1702
+ - Updated dependencies [fa5d137]
1703
+ - Updated dependencies [a392dbf]
1704
+ - Updated dependencies [279431e]
1705
+ - Updated dependencies [948dd6b]
1706
+ - Updated dependencies [3b4c56c]
1707
+ - Updated dependencies [ae8edd2]
1708
+ - Updated dependencies [e25403c]
1709
+ - Updated dependencies [a81aa9d]
1710
+ - Updated dependencies [64baa68]
1711
+ - Updated dependencies [9fa70d7]
1712
+ - Updated dependencies [09db64a]
1713
+ - Updated dependencies [92916e7]
1714
+ - Updated dependencies [a84f3ea]
1715
+ - Updated dependencies [f2eaae8]
1716
+ - Updated dependencies [56c093c]
1717
+ - Updated dependencies [c09451b]
1718
+ - Updated dependencies [ba64877]
1719
+ - Updated dependencies [e7191ce]
1720
+ - Updated dependencies [7345308]
1721
+ - Updated dependencies [79b6a22]
1722
+ - Updated dependencies [30d96ab]
1723
+ - Updated dependencies [f658793]
1724
+ - Updated dependencies [0fd4899]
1725
+ - Updated dependencies [c95ad19]
1726
+ - Updated dependencies [e58ea8b]
1727
+ - Updated dependencies [4a17645]
1728
+ - Updated dependencies [3795c5f]
1729
+ - Updated dependencies [8ab926b]
1730
+ - Updated dependencies [7317cf2]
1731
+ - Updated dependencies [e25e839]
1732
+ - Updated dependencies [5997207]
1733
+ - Updated dependencies [8b13cc8]
1734
+ - Updated dependencies [00d8f65]
1735
+ - Updated dependencies [4a4a35d]
1736
+ - Updated dependencies [86e765a]
1737
+ - Updated dependencies [1d7e76a]
1738
+ - Updated dependencies [53dc739]
1739
+ - Updated dependencies [fd289be]
1740
+ - Updated dependencies [03bf7b1]
1741
+ - Updated dependencies [f90e820]
1742
+ - Updated dependencies [18d816a]
1743
+ - Updated dependencies [e8bd715]
1744
+ - Updated dependencies [b91c351]
1745
+ - Updated dependencies [a28a3c0]
1746
+ - Updated dependencies [200d255]
1747
+ - Updated dependencies [2852acc]
1748
+ - Updated dependencies [daeaaf9]
1749
+ - Updated dependencies [c459da6]
1750
+ - Updated dependencies [e914733]
1751
+ - Updated dependencies [f887e52]
1752
+ - Updated dependencies [881f8d8]
1753
+ - Updated dependencies [3bfa1e6]
1754
+ - Updated dependencies [0a8ebf3]
1755
+ - Updated dependencies [901355c]
1756
+ - Updated dependencies [34ce8e7]
1757
+ - Updated dependencies [33681ea]
1758
+ - Updated dependencies [bfe13c8]
1759
+ - Updated dependencies [0fb3044]
1760
+ - Updated dependencies [4635f3e]
1761
+ - Updated dependencies [fd289be]
1762
+ - Updated dependencies [ee3595c]
1763
+ - Updated dependencies [09b4f4e]
1764
+ - Updated dependencies [b2eab95]
1765
+ - Updated dependencies [93940d4]
1766
+ - Updated dependencies [3a04b01]
1767
+ - Updated dependencies [45b9051]
1768
+ - Updated dependencies [3954fb7]
1769
+ - Updated dependencies [4805b56]
1770
+ - Updated dependencies [b9e9227]
1771
+ - Updated dependencies [d395692]
1772
+ - Updated dependencies [5894d30]
1773
+ - Updated dependencies [a3765f6]
1774
+ - Updated dependencies [2d5cee3]
1775
+ - Updated dependencies [e22158f]
1776
+ - Updated dependencies [7404925]
1777
+ - Updated dependencies [0c2334f]
1778
+ - Updated dependencies [778c59f]
1779
+ - Updated dependencies [d2619fd]
1780
+ - Updated dependencies [af56546]
1781
+ - Updated dependencies [6acb11a]
1782
+ - Updated dependencies [33c5fd3]
1783
+ - Updated dependencies [20b0fdb]
1784
+ - Updated dependencies [905019b]
1785
+ - Updated dependencies [a286411]
1786
+ - Updated dependencies [98c0d33]
1787
+ - Updated dependencies [368a82e]
1788
+ - Updated dependencies [a3d5724]
1789
+ - Updated dependencies [93ea19b]
1790
+ - Updated dependencies [9ee2dcf]
1791
+ - Updated dependencies [8cb96ec]
1792
+ - Updated dependencies [8f10a79]
1793
+ - Updated dependencies [6269a55]
1794
+ - Updated dependencies [a17da05]
1795
+ - Updated dependencies [a8c00e2]
1796
+ - Updated dependencies [22e5236]
1797
+ - Updated dependencies [0fb8760]
1798
+ - Updated dependencies [e5ce2ed]
1799
+ - Updated dependencies [be21955]
1800
+ - Updated dependencies [bc56e18]
1801
+ - Updated dependencies [be21955]
1802
+ - Updated dependencies [a9ee989]
1803
+ - Updated dependencies [4d0d944]
1804
+ - Updated dependencies [15d58db]
1805
+ - Updated dependencies [d63b014]
1806
+ - Updated dependencies [9abe4e4]
1807
+ - Updated dependencies [2cc7122]
1808
+ - Updated dependencies [50d6c92]
1809
+ - Updated dependencies [15d55fb]
1810
+ - Updated dependencies [9e0ba21]
1811
+ - Updated dependencies [a07a831]
1812
+ - Updated dependencies [311433f]
1813
+ - Updated dependencies [3e5ad08]
1814
+ - Updated dependencies [9abe4e4]
1815
+ - Updated dependencies [b7131f3]
1816
+ - Updated dependencies [e5812fa]
1817
+ - Updated dependencies [7085f90]
1818
+ - Updated dependencies [dee4dd4]
1819
+ - Updated dependencies [ce7e497]
1820
+ - Updated dependencies [51ecb2f]
1821
+ - Updated dependencies [9086761]
1822
+ - Updated dependencies [f6344e7]
1823
+ - Updated dependencies [42a117b]
1824
+ - Updated dependencies [1401ae7]
1825
+ - Updated dependencies [4297fe7]
1826
+ - Updated dependencies [e398863]
1827
+ - Updated dependencies [d16df74]
1828
+ - Updated dependencies [d79c602]
1829
+ - Updated dependencies [f11fc61]
1830
+ - Updated dependencies [e808890]
1831
+ - Updated dependencies [8f79379]
1832
+ - Updated dependencies [e6ca40e]
1833
+ - Updated dependencies [0c77ea4]
1834
+ - Updated dependencies [52954c0]
1835
+ - Updated dependencies [89eb997]
1836
+ - Updated dependencies [7131f12]
1837
+ - Updated dependencies [aa5994e]
1838
+ - Updated dependencies [be93457]
1839
+ - Updated dependencies [a65db76]
1840
+ - Updated dependencies [2cf5a96]
1841
+ - Updated dependencies [15eb2c9]
1842
+ - Updated dependencies [5691b07]
1843
+ - Updated dependencies [2a6122b]
1844
+ - Updated dependencies [225e769]
1845
+ - Updated dependencies [8af88dd]
1846
+ - Updated dependencies [fb5fbb8]
1847
+ - Updated dependencies [d7b3963]
1848
+ - Updated dependencies [33184fd]
1849
+ - Updated dependencies [7c41693]
1850
+ - Updated dependencies [b72db01]
1851
+ - Updated dependencies [dce5cd4]
1852
+ - Updated dependencies [9688f58]
1853
+ - Updated dependencies [556ebc1]
1854
+ - Updated dependencies [177ebdc]
1855
+ - Updated dependencies [8d237b4]
1856
+ - Updated dependencies [2d2e6f0]
1857
+ - Updated dependencies [2d8dd8d]
1858
+ - Updated dependencies [22d573e]
1859
+ - Updated dependencies [b5a2398]
1860
+ - Updated dependencies [348860c]
1861
+ - Updated dependencies [5383fa6]
1862
+ - Updated dependencies [5b3ff63]
1863
+ - Updated dependencies [1a6a19c]
1864
+ - Updated dependencies [064d484]
1865
+ - Updated dependencies [527e050]
1866
+ - Updated dependencies [dd33bf9]
1867
+ - Updated dependencies [4cb2a90]
1868
+ - Updated dependencies [74a7804]
1869
+ - Updated dependencies [53d3689]
1870
+ - Updated dependencies [b3a63d3]
1871
+ - Updated dependencies [49f0dcf]
1872
+ - Updated dependencies [033a34c]
1873
+ - Updated dependencies [4d25d22]
1874
+ - Updated dependencies [1ffee51]
1875
+ - Updated dependencies [5ae4303]
1876
+ - Updated dependencies [ece4dad]
1877
+ - Updated dependencies [e9b377e]
1878
+ - Updated dependencies [146f448]
1879
+ - Updated dependencies [735f5c7]
1880
+ - Updated dependencies [a7e18de]
1881
+ - Updated dependencies [366f895]
1882
+ - Updated dependencies [dc75ba8]
1883
+ - Updated dependencies [cce0aa9]
1884
+ - Updated dependencies [e764507]
1885
+ - Updated dependencies [cff17af]
1886
+ - Updated dependencies [39404f3]
1887
+ - Updated dependencies [ca1965f]
1888
+ - Updated dependencies [8619f95]
1889
+ - Updated dependencies [b706af9]
1890
+ - Updated dependencies [db8c288]
1891
+ - Updated dependencies [0e5fe7f]
1892
+ - Updated dependencies [add4360]
1893
+ - Updated dependencies [e0abc38]
1894
+ - Updated dependencies [fc9ba76]
1895
+ - Updated dependencies [1272f0a]
1896
+ - Updated dependencies [0f94cc7]
1897
+ - Updated dependencies [a11c1a5]
1898
+ - Updated dependencies [71f9cd1]
1899
+ - Updated dependencies [ee17d86]
1900
+ - Updated dependencies [cdbd920]
1901
+ - Updated dependencies [18c432e]
1902
+ - Updated dependencies [3c418c4]
1903
+ - Updated dependencies [fa8715a]
1904
+ - Updated dependencies [a933ed7]
1905
+ - Updated dependencies [b3ca463]
1906
+ - Updated dependencies [a933ed7]
1907
+ - Updated dependencies [0d4a6a8]
1908
+ - Updated dependencies [518d5e5]
1909
+ - Updated dependencies [6643ba1]
1910
+ - Updated dependencies [eeba2ef]
1911
+ - Updated dependencies [ec4c4d2]
1912
+ - Updated dependencies [424f73c]
1913
+ - Updated dependencies [cccbe51]
1914
+ - Updated dependencies [a8d6b1d]
1915
+ - Updated dependencies [e4a7695]
1916
+ - Updated dependencies [87075b1]
1917
+ - Updated dependencies [fc58a99]
1918
+ - Updated dependencies [14cfc00]
1919
+ - Updated dependencies [1c6f7b4]
1920
+ - Updated dependencies [e854a53]
1921
+ - Updated dependencies [dfebfc8]
1922
+ - Updated dependencies [598b7ec]
1923
+ - Updated dependencies [d028b37]
1924
+ - Updated dependencies [f7b25c5]
1925
+ - Updated dependencies [122ef38]
1926
+ - Updated dependencies [4a37870]
1927
+ - Updated dependencies [428f9b2]
1928
+ - Updated dependencies [aa7ff56]
1929
+ - Updated dependencies [811a3c2]
1930
+ - Updated dependencies [1401ae7]
1931
+ - Updated dependencies [2fd3f1c]
1932
+ - Updated dependencies [c41b42e]
1933
+ - Updated dependencies [d41d166]
1934
+ - Updated dependencies [c4db311]
1935
+ - Updated dependencies [750fff5]
1936
+ - Updated dependencies [c19035e]
1937
+ - Updated dependencies [ececf7a]
1938
+ - Updated dependencies [d173125]
1939
+ - Updated dependencies [8eeca27]
1940
+ - Updated dependencies [8425c17]
1941
+ - Updated dependencies [a5ef1d8]
1942
+ - Updated dependencies [87ad30c]
1943
+ - Updated dependencies [772d5de]
1944
+ - Updated dependencies [ce80ec2]
1945
+ - Updated dependencies [b372318]
1946
+ - Updated dependencies [97a2263]
1947
+ - Updated dependencies [29d0676]
1948
+ - Updated dependencies [0169d49]
1949
+ - Updated dependencies [6bd3231]
1950
+ - Updated dependencies [d2b5ba8]
1951
+ - Updated dependencies [b799ac5]
1952
+ - Updated dependencies [8f74307]
1953
+ - Updated dependencies [d23dc08]
1954
+ - Updated dependencies [644ad50]
1955
+ - Updated dependencies [9735662]
1956
+ - Updated dependencies [4d5b4f8]
1957
+ - Updated dependencies [3e8f5b0]
1958
+ - Updated dependencies [5d16379]
1959
+ - Updated dependencies [0da7cd2]
1960
+ - Updated dependencies [28a5c3e]
1961
+ - Updated dependencies [4bc18e5]
1962
+ - Updated dependencies [9f57f1e]
1963
+ - @objectstack/spec@17.3.0
1964
+ - @objectstack/platform-objects@17.3.0
1965
+ - @objectstack/core@17.3.0
1966
+ - @objectstack/types@17.3.0
1967
+ - @objectstack/metadata-core@17.3.0
1968
+ - @objectstack/metadata-fs@17.3.0
1969
+
3
1970
  ## 17.2.0
4
1971
 
5
1972
  ### Patch Changes