Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@objectstack/spec': patch
---

fix(spec): `os migrate meta` guidance for the `driver-*`, `kernel-*` and `system-*` migration entries states each lesson in words instead of citing tracker numbers

Clause-②: no

The ADR-0087 semantic entries of the `driver-*` family (the driver query-argument
narrowings, the inert capability bits, the SQL driver's unresolvable-column and
cross-row upsert refusals, and the retired Turso config keys), the `kernel-*` family
(preview mode, and the kernel duration keys that now carry their unit in the key name)
and the `system-*` family (the system duration keys renamed under the same rule) are
printed by `os migrate meta` as the header, `why:` and `verify:` lines of a manual
change. Their text sent the reader to issue-tracker and decision-batch numbers — some
of which no longer resolve — for what a ruling, measurement or fix had decided; it now
says what was decided, in the sentence being read. ADR ids are kept.

Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic changes,
and the chain rewrites exactly what it rewrote before. The generated migration registry,
`spec-changes.json` and the protocol upgrade guide carry the same text.
6 changes: 3 additions & 3 deletions docs/protocol-upgrade-guide.md

Large diffs are not rendered by default.

33 changes: 30 additions & 3 deletions packages/cli/test/migrate-meta-engine-guidance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

/**
* `os migrate meta` — the guidance it prints for the ADR-0087 semantic entries
* of the COVERED families (`engine-*`, `ui-*`, `plugin-*`) states each lesson
* in words and carries no tracker number.
* of the COVERED families (`engine-*`, `ui-*`, `plugin-*`, `driver-*`,
* `kernel-*`, `system-*`) states each lesson in words and carries no tracker
* number.
*
* ## What this pins
*
Expand Down Expand Up @@ -72,19 +73,35 @@ const TSX = resolve(HERE, '../../../node_modules/.bin/tsx');
const TRACKER_ID = /#\d{4,5}\b/;

/** The families this pin holds, selected by entry-id prefix. */
const COVERED_PREFIXES = ['engine-', 'ui-', 'plugin-'];
const COVERED_PREFIXES = ['engine-', 'ui-', 'plugin-', 'driver-', 'kernel-', 'system-'];

/**
* The entries rewritten when each family was brought to this line — the
* anti-vacuity floor. A covered entry that carried no tracker id to begin with
* is held by its prefix and needs no row here.
*/
const REWRITTEN = [
'driver-aggregate-undeclared-key-aliases-removed',
'driver-capabilities-inert-bits-removed',
'driver-options-timeout-to-timeout-ms',
'driver-sql-distinct-bare-filter-typed',
'driver-sql-unresolvable-where-column-refused',
'driver-sql-upsert-cross-row-identity-merge-refused',
'driver-turso-config-local-path-wasm-retired',
'engine-dotted-filter-refused',
'engine-dotted-projection-refused',
'engine-find-formula-filter-refused',
'engine-find-formula-order-by-refused',
'engine-update-upsert-retired',
'kernel-compatibility-matrix-estimated-migration-time-unit-in-key',
'kernel-context-preview-mode-retired',
'kernel-event-bus-retention-unit-in-key',
'kernel-health-check-and-hot-reload-durations-unit-in-key',
'kernel-package-lifecycle-durations-unit-in-key',
'kernel-plugin-health-report-durations-unit-in-key',
'kernel-plugin-security-durations-unit-in-key',
'kernel-runtime-config-timeout-unit-in-key',
'kernel-startup-orchestrator-durations-unit-in-key',
'plugin-activation-events-retired',
'plugin-auto-restart-never-reinitialised',
'plugin-manifest-contributes-dead-members-retired',
Expand All @@ -95,6 +112,16 @@ const REWRITTEN = [
'plugin-runtime-family-retired',
'plugin-security-scan-result-surface-retired',
'plugin-security-scanner-retired',
'system-cache-durations-unit-in-key',
'system-collaboration-durations-unit-in-key',
'system-failover-health-check-interval-unit-in-key',
'system-metrics-jsdoc-durations-unit-in-key',
'system-metrics-window-durations-unit-in-key',
'system-object-storage-durations-unit-in-key',
'system-registry-config-durations-unit-in-key',
'system-tracing-otel-exporter-durations-unit-in-key',
'system-tracing-span-duration-unit-in-key',
'system-worker-queue-rate-limit-duration-unit-in-key',
'ui-cloud-connection-widgets-unknown-keys-refused',
'ui-form-field-length-malformed-refused',
'ui-form-field-precision-scale-integer-refused',
Expand Down
12 changes: 6 additions & 6 deletions packages/spec/spec-changes.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ export const entry: SemanticMigration = {
+ 'upgrade guide or release note. So this entry does not record a declared surface '
+ 'being withdrawn; it records a LENIENCY being withdrawn, which is why it is here '
+ 'rather than behind a tombstone. The only writers in this repository were the two '
+ 'driver packages\' own fixtures — #4984\'s family, where a fixture spelling the alias '
+ 'keeps the tolerant limb green forever and no test in existence can go red on its '
+ 'deletion — so ADR-0049 enforce-or-remove applies once those are re-spelt. ⚠️ Do '
+ 'driver packages\' own fixtures — the family of the org-axis red-line gate that read '
+ 'only rejected aliases while its own fixtures spelt them, so its tests stayed green '
+ 'and the rule stayed dead: a fixture spelling the alias keeps the tolerant limb green '
+ 'forever and no test in existence can go red on its deletion — so ADR-0049 '
+ 'enforce-or-remove applies once those are re-spelt. ⚠️ Do '
+ 'NOT read this across to `dashboard`/`page` measures: `aggregate` IS the canonical '
+ 'key there and `func` IS a declared, loudly-suggesting alias (`DatasetMeasureSchema`, '
+ 'ui/dataset.zod.ts). That neighbouring vocabulary is untouched, and it is the most '
Expand All @@ -35,8 +37,14 @@ export const entry: SemanticMigration = {
+ 'no enforced channel at all, which is exactly why this ledger entry has to exist: '
+ 'the generated upgrade guide is the only way such a reader learns of the rename. '
+ 'Same disposition, and the same reason, as `data-driver-find-stream-retired` '
+ '(#4484), `storage-service-list-retired` (#5540) and `actor-user-roles-to-positions` '
+ '(#6011). ADR-0049 / ADR-0087, #6321 (PR #6404).',
+ '(`IDataDriver.findStream`, removed with no tombstone because nothing parses a driver '
+ 'object), `storage-service-list-retired` (the zero-consumer `IStorageService.list`, '
+ 'whose two adapters answered differently and both incompletely) and '
+ '`actor-user-roles-to-positions` (the `ctx.user` `roles` alias, closed at once on the '
+ 'maintainer\'s word rather than given a window). The removal ran in a fixed order — '
+ 'the fixtures re-spelt first, the two alias branches deleted second, the parameter '
+ 'narrowed to `DriverQuery` last — because the reverse order yields red nobody can '
+ 'explain. ADR-0049 / ADR-0087.',
acceptanceCriteria:
'No caller passes `aggregate:` to a driver\'s `aggregate()`, and no aggregation entry '
+ 'spells its function `func:`; both are written `aggregations:` / `function:`. An '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ export const entry: SemanticMigration = {
+ 'presence cannot carry the signal are `queryDateGranularity`, `autonumber` and '
+ '`batchSchemaSync`)',
reason:
'The #4484 findStream close-out found `DriverCapabilities.streaming` pointing at a '
+ 'capability the contract no longer declares, and the follow-up audit (#4634) checked '
'Retiring `IDataDriver.findStream` (it had no production caller, and two of its three '
+ 'implementations read the whole result set into memory before yielding a row) left '
+ '`DriverCapabilities.streaming` pointing at a capability the contract no longer '
+ 'declares, and the follow-up audit checked '
+ 'every bit in the record the same way, across objectstack and cloud (objectui '
+ 'confirmed clean): of 34 declared bits, THREE have a decision-making reader — '
+ '`queryDateGranularity` (engine aggregate dispatch + checkDateBucketParity), '
Expand All @@ -47,14 +49,15 @@ export const entry: SemanticMigration = {
+ 'stack metadata — `supports` literals live in driver classes and '
+ '`DriverConfig.capabilities` is plugin TS configuration, neither ever a '
+ '`sys_metadata` shape (the stack-tree neighbour, `datasource.capabilities`, was '
+ 'retired separately in #4583) — so there is no source for the D2 chain to rewrite '
+ 'retired separately, as a whole block nothing read) — so there is no source for the '
+ 'D2 chain to rewrite '
+ 'and this entry is the D3 record. The keys are tombstoned rather than deleted '
+ 'because `DriverCapabilitiesSchema` is not `.strict()` and IS parsed '
+ '(DriverConfigSchema / SQLDriverConfigSchema / NoSQLDriverConfigSchema embed it): '
+ 'a plain delete would silently strip a vendor\'s authored bit, replacing one '
+ 'silent no-op with another. `batchSchemaSync` also drops its `.default(false)` '
+ 'for `.optional()` — absence already meant false at both readers, and the default '
+ 'forced every capability object to spell out 30+ bits. ADR-0049 / ADR-0078, #4634.',
+ 'forced every capability object to spell out 30+ bits. ADR-0049 / ADR-0078.',
acceptanceCriteria:
'No `supports` literal or `DriverConfig.capabilities` object authors any of the 31 '
+ 'retired bits — a driver class that still writes one fails tsc against '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export const entry: SemanticMigration = {
reason:
'This entry records a TYPE being added, not a surface being withdrawn, and it says '
+ 'so up front because the distinction decides who has to do anything. `distinct` is '
+ 'not declared on `IDataDriver`, so #5181 / #6075 never reached it and it kept '
+ 'not declared on `IDataDriver`, so neither the narrowing of `IDataDriver`\'s query '
+ 'parameters to `DriverQuery` nor the follow-through that brought five drivers\' '
+ 'implementations in line ever reached it, and it kept '
+ '`filters?: any` while its body said something far more specific — '
+ '`applyFilters(builder, filters)` is handed the ARGUMENT ITSELF, never a `.where` '
+ 'off it. ⚠️ RUNTIME BEHAVIOUR IS UNCHANGED by this entry\'s change: not one '
Expand All @@ -26,9 +28,9 @@ export const entry: SemanticMigration = {
+ 'orders" answered with EVERY product, silently. That spelling is now TS2345 at the '
+ 'call site. This is a driver CALL ARGUMENT — code, never stack metadata — so there '
+ 'is no source for the D2 chain to rewrite and deliberately no schema tombstone, the '
+ 'disposition `data-driver-find-stream-retired` (#4484), `storage-service-list-retired` '
+ '(#5540), `actor-user-roles-to-positions` (#6011) and '
+ '`driver-aggregate-undeclared-key-aliases-removed` (#6321) already carry. ⚠️ It '
+ 'disposition `data-driver-find-stream-retired`, `storage-service-list-retired`, '
+ '`actor-user-roles-to-positions` and '
+ '`driver-aggregate-undeclared-key-aliases-removed` already carry. ⚠️ It '
+ 'differs from those four in ONE measured way a reader should not have to infer: '
+ 'because nothing changed at run time, an untyped JS caller is not affected BY THE '
+ 'UPGRADE at all. The entry is here for a different reason — such a caller is exactly '
Expand All @@ -41,9 +43,11 @@ export const entry: SemanticMigration = {
+ 'valid filter — one constraining columns named `object` and `where` — and so is a '
+ 'FilterArray. Both reach `distinct` type-checked and are refused at run time, '
+ 'loudly, with INVALID_FILTER / 400. `driver-memory`\'s opposite half — where the '
+ 'BARE spelling returns the unfiltered set in silence — stayed open under the #5499 '
+ 'freeze, which was lifted on 2026-08-11; it is still open, now unexcused rather than '
+ 'deferred (#6320). ADR-0087, #6320.',
+ 'BARE spelling returns the unfiltered set in silence — stayed open under the '
+ 'maintainer\'s 2026-08-05 investment freeze on driver-memory, which was lifted on '
+ '2026-08-11; it is still open, now unexcused rather than deferred (the measurement '
+ 'that found the two drivers reading this argument differently split the fix: the sql '
+ 'half is this entry, and the memory half was held back by that freeze). ADR-0087.',
acceptanceCriteria:
'No caller passes a non-object to `distinct()`\'s third argument. A scalar there is '
+ 'now a compile error (`TS2345: Argument of type \'string\' is not assignable to '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const entry: SemanticMigration = {
surface: '`DriverOptions.timeout` (data/driver.zod.ts) — the per-call options argument of every `IDataDriver` method',
replacement: '`DriverOptions.timeoutMs` (milliseconds) — rename the key; the value is unchanged',
reason:
'Maintainer ruling 2026-09-02 on #14478 (ruled B — no grandfathered baseline): the unit of a '
'Maintainer ruling 2026-09-02 on duration units (ruled B — no grandfathered baseline): the unit of a '
+ 'duration-shaped `z.number()` key lives in the key NAME, never only in the description. '
+ '`timeout` said "Timeout in ms" in prose and nothing else. Tombstoned with retiredKey '
+ '(`DriverOptionsSchema` is not strict, so a bare deletion would strip the old key in '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export const entry: SemanticMigration = {
+ 'is no spelling of an unresolvable column that means "match nothing", which is '
+ 'exactly what the old empty list was mistaken for',
reason:
'One predicate had two answers. `SqlDriver.findRows()` carries the #3821 unknown-'
+ 'column recovery ladder, whose rungs are all built from `buildBase()` — and '
'One predicate had two answers. `SqlDriver.findRows()` carries the unknown-column '
+ 'recovery ladder (an unsortable query loses its ORDER BY, not its rows), whose rungs '
+ 'are all built from `buildBase()` — and '
+ '`buildBase()` always re-applies `query.where`. So the ladder can drop a projection '
+ 'and can drop an ORDER BY, but it can never drop the clause that failed when the '
+ 'unresolvable column is in the WHERE: both rungs raise the same error and the method '
Expand All @@ -33,25 +34,27 @@ export const entry: SemanticMigration = {
+ 'agent reads "no matching records" and writes its next query on that belief. The '
+ "thrown half was no better — the dialect's own `code`, no `status` (an unclassified "
+ '5xx at the REST boundary rather than a caller mistake), and the statement\'s bound '
+ 'literals inlined in the message, the same predicate-text disclosure shape #7929 '
+ 'redacted elsewhere.\n\n'
+ 'Ruled 2026-08-15 on #8790: refuse BOTH halves with `INVALID_FILTER` / 400, naming '
+ 'the column. The envelope is not minted here — it is what every sibling refusal on '
+ 'literals inlined in the message, the same predicate-text disclosure shape the '
+ 'driver\'s field-reference filter refusals had already been made to stop echoing '
+ '(the full diagnostic goes to the server log, never the response).\n\n'
+ 'Ruled by the maintainer on 2026-08-15: refuse BOTH halves with `INVALID_FILTER` / '
+ '400, naming the column. The envelope is not minted here — it is what every sibling '
+ 'refusal on '
+ 'this path already answers, required on both SQL drivers by '
+ '`cross-field-conformance-cases.ts` and pinned by `sql-driver-boolean-identity.test.ts` '
+ 'and `sql-driver-cross-field-conformance.test.ts` — so what closes is a declared-vs-'
+ 'enforced gap, not a new posture. Recover-both was excluded by the card\'s own '
+ 'argument: dropping a WHERE returns rows the caller explicitly excluded, and #3821\'s '
+ '"rows matter more than their order" is an argument about how rows are PRESENTED, '
+ 'which does not transfer to a predicate. The ladder KEEPS both of its recoveries — '
+ 'enforced gap, not a new posture. Recover-both was excluded by the ruling\'s own '
+ 'argument: dropping a WHERE returns rows the caller explicitly excluded, and the '
+ 'ladder\'s own premise — rows matter more than their order — is an argument about '
+ 'how rows are PRESENTED, which does not transfer to a predicate. The ladder KEEPS both of its recoveries — '
+ 'only the WHERE-failure terminal became a refusal.\n\n'
+ 'Reach, stated rather than assumed: the refusal fires on the wordings the ladder has '
+ 'always recognised — SQLite (`no such column: x`) and Postgres (`column "x" does not '
+ "exist`). MySQL spells it `Unknown column 'x' in 'where clause'`, which neither arm "
+ 'matches, so on MySQL this condition still travels out as the raw dialect error; '
+ 'widening that predicate would also hand MySQL the #3821 recoveries it has never had, '
+ 'widening that predicate would also hand MySQL the ladder\'s recoveries it has never had, '
+ 'which is an accept-set change in the opposite direction and is filed separately.\n\n'
+ 'Addendum 2026-08-16 (#8926, landed by PR #9061). The paragraph above is kept as the '
+ 'Addendum 2026-08-16. The paragraph above is kept as the '
+ 'state at registration; this amends it. MySQL joined the one shared predicate, so the '
+ 'reach is now all three dialects this driver speaks, and a MySQL reader must NOT '
+ 'conclude the migration does not apply — it applies exactly as it does on SQLite and '
Expand All @@ -61,9 +64,9 @@ export const entry: SemanticMigration = {
+ 'recoveries — was considered and refused). (1) THE ENVELOPE: an unresolvable WHERE '
+ 'column now refuses with the same `INVALID_FILTER` / 400 naming the column, instead of '
+ "travelling out as the raw `ER_BAD_FIELD_ERROR` with the statement's bound literals "
+ 'inlined — the #7929 disclosure shape closed on the last dialect that still had it. '
+ '(2) THE RECOVERIES: MySQL also gained the #3821 projection and ORDER-BY recoveries it '
+ 'had never had, so an unresolvable column in a projection or an ORDER BY now returns '
+ 'inlined — that disclosure shape closed on the last dialect that still had it. '
+ '(2) THE RECOVERIES: MySQL also gained the ladder\'s projection and ORDER-BY '
+ 'recoveries it had never had, so an unresolvable column in a projection or an ORDER BY now returns '
+ 'recovered rows where it used to throw. The two halves arrive together because '
+ '`ER_BAD_FIELD_ERROR` spells every clause position with one sentence — '
+ "`Unknown column 'x' in 'where clause'` / `'field list'` / `'order clause'` — so all "
Expand All @@ -72,15 +75,17 @@ export const entry: SemanticMigration = {
+ 'The widening can never drop a predicate: every ladder rung is rebuilt from '
+ '`buildBase()`, which unconditionally re-applies `query.where`. Unchanged by the '
+ 'ruling: a DOTTED filter key is still classified per dialect (Postgres raises '
+ 'undefined_table, which neither arm matches), the axis #8371 owns. The entry id, '
+ 'undefined_table, which neither arm matches), the axis owned by the dotted-filter '
+ 'verdict, which refuses a dotted key whose head is a relation, a formula or a plain '
+ 'column at the protocol and engine doors. The entry id, '
+ 'surface and prescription are unchanged — this is a text amendment, not a new '
+ 'migration.\n\n'
+ 'This is a CODE-path API, not stored metadata, so — like '
+ '`engine-dotted-projection-refused` and `engine-find-formula-filter-refused` — there '
+ 'is no `sys_metadata` row for the D2 chain to rewrite and this entry is the '
+ 'notification channel. No mechanical rewrite exists: the platform cannot know which '
+ 'real column a mistyped filter key meant, and guessing one would answer with rows the '
+ 'caller never asked for. #8790, #3821, #7929, #8371, ADR-0112.',
+ 'caller never asked for. ADR-0112.',
acceptanceCriteria:
'No saved report `query.filter`, flow condition, sharing/permission rule or hook '
+ 'filters on a name the queried object has no column for. Reads and counts complete '
Expand Down
Loading
Loading