Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@objectstack/spec': patch
---

fix(spec): `os migrate meta` guidance for the `datasource-*`, `filter-*`, `action-*`, `data-*` and `element-*` migration entries states each lesson in words instead of citing tracker numbers

Clause-②: no

The ADR-0087 semantic entries of the `datasource-*` family (the publish-time credential,
placeholder and URL refusals, and the bound-secret pairs a mongo datasource cannot use),
the `filter-*` family (the retired `$regex`, the `$between` endpoint refusals and the
comparand shapes the save door now refuses), the `action-*` family (the retired
descriptor key, the `resumeAuthority` default flip, the action-session rename, the bulk
dispatch contract and the engine facade's query envelope), the `data-*` family (the
retired driver and engine contract members, the retired field-changed event and two
duration keys renamed with their unit) and the `element-*` family (the filter rule array
at the page binding and the element and block doors) 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, decision-batch and ruling-record numbers — some of which no longer
resolve, and some in other repositories — 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.
24 changes: 12 additions & 12 deletions docs/protocol-upgrade-guide.md

Large diffs are not rendered by default.

42 changes: 39 additions & 3 deletions packages/cli/test/migrate-meta-engine-guidance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
/**
* `os migrate meta` — the guidance it prints for the ADR-0087 semantic entries
* of the COVERED families (`engine-*`, `ui-*`, `plugin-*`, `driver-*`,
* `kernel-*`, `system-*`) states each lesson in words and carries no tracker
* number.
* `kernel-*`, `system-*`, `datasource-*`, `filter-*`, `action-*`, `data-*`,
* `element-*`) states each lesson in words and carries no tracker number.
*
* ## What this pins
*
Expand Down Expand Up @@ -73,26 +73,62 @@ 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-', 'driver-', 'kernel-', 'system-'];
const COVERED_PREFIXES = [
'engine-', 'ui-', 'plugin-', 'driver-', 'kernel-', 'system-',
'datasource-', 'filter-', 'action-', 'data-', 'element-',
];

/**
* 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 = [
'action-bulk-dispatch-contract-undeclared',
'action-descriptor-is-async-retired',
'action-descriptor-resume-authority-default-flip',
'action-engine-facade-find-query-envelope',
'action-session-roles-to-positions',
'data-driver-find-stream-retired',
'data-driver-query-omit-object',
'data-engine-batch-retired',
'data-field-changed-event-retired',
'data-file-value-duration-unit-in-key',
'data-nosql-query-options-timeout-unit-in-key',
'datasource-config-inline-credential-refused',
'datasource-config-mongo-options-credential-refused',
'datasource-config-placeholder-refused',
'datasource-config-postgres-url-unparseable-refused',
'datasource-config-url-query-credential-refused',
'datasource-config-url-userinfo-refused',
'datasource-credentialsref-mongo-composed-no-username-refused',
'datasource-credentialsref-mongo-url-no-user-refused',
'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',
'element-data-source-and-object-block-filter-rule-array',
'element-number-filter-rule-array',
'element-record-picker-filter-rule-array',
'engine-dotted-filter-refused',
'engine-dotted-projection-refused',
'engine-find-formula-filter-refused',
'engine-find-formula-order-by-refused',
'engine-update-upsert-retired',
'filter-between-blank-endpoint-refused',
'filter-between-field-reference-endpoint-refused',
'filter-comparand-types-and-widget-nested-slots-refused-at-save',
'filter-equality-array-comparand-refused',
'filter-equality-array-comparand-refused-at-save',
'filter-icontains-comparand-refused-at-parse',
'filter-ne-array-comparand-refused',
'filter-preset-ordering-comparand-refused',
'filter-query-face-comparands-refused-at-save',
'filter-regex-options-retired',
'filter-text-operator-declared-type-refused',
'kernel-compatibility-matrix-estimated-migration-time-unit-in-key',
'kernel-context-preview-mode-retired',
'kernel-event-bus-retention-unit-in-key',
Expand Down
44 changes: 22 additions & 22 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 @@ -12,16 +12,16 @@ export const entry: SemanticMigration = {
reason:
'ADR-0049 enforce-or-remove. `isAsync` declared "this action suspends the flow '
+ 'awaiting an external reply" and NOTHING read it: a fresh three-repo measurement '
+ '(#6748, re-run at pickup) found zero property reads across objectstack, objectui '
+ 'and cloud — every hit was the declaration itself, a generated baseline, one of '
+ '(taken when the key was filed for retirement, and re-run at pickup) found zero '
+ 'property reads across objectstack, objectui and cloud — every hit was the declaration itself, a generated baseline, one of '
+ 'five shipped descriptors WRITING it, a test fixture pinning the shape, or prose. '
+ 'So declaring it never made a node suspend and omitting it never stopped one, '
+ 'which is the silently-inert declaration ADR-0049 exists to end. It was always a '
+ 'second, weaker spelling of the capability `supportsPause` states, and the two '
+ 'diverged in exactly the way a duplicated declaration does: `screen` declared '
+ 'both, `map` and `wait` declared `isAsync` alongside `supportsPause`, and nothing '
+ 'anywhere reconciled them. The sibling took the ENFORCE leg of the same ruling in '
+ '#6667 — `AutomationEngine` now refuses a suspension whose type does not declare '
+ 'anywhere reconciled them. The sibling took the ENFORCE leg of the same ruling — '
+ '`AutomationEngine` now refuses a suspension whose type does not declare '
+ '`supportsPause: true` — so the capability this key gestured at is now a real, '
+ 'enforced fact under one name. This one had no consumer to grow into and takes '
+ 'the remove leg. '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ export const entry: SemanticMigration = {
'A SECURE-DEFAULT FLIP with no metadata shape to rewrite — the same category as '
+ "protocol 12's `rest-requireauth-default-flip`, and it is registered here for the "
+ 'same reason: whether a given pause is genuinely open to the generic route is a '
+ 'trust judgment no transform can make. The #3801 resume gate keys on the SUSPENDED '
+ 'trust judgment no transform can make. The generic resume route\'s authorization gate '
+ 'keys on the SUSPENDED '
+ "NODE, and `ActionDescriptor.resumeAuthority` used to default to `'any'`, so a "
+ 'pausing node type shipped raw-resumable unless its author remembered the field. '
+ "It now resolves to `'service'` when absent: an unclaimed pause is refused on the "
+ 'generic route with `PERMISSION_DENIED` / 403 until its descriptor states who may '
+ 'continue it. #3823 is the incident that decided the direction — ADR-0044 pointed '
+ 'continue it. The revise-window incident decided the direction — ADR-0044 pointed '
+ "an approval's revise edge at a generic `wait`, `wait` is legitimately `'any'`, and "
+ 'the pause standing in a service-owned position inherited a fail-open value nobody '
+ 'chose; the demonstrated cost was an unaudited resubmit plus a destroyed remote '
Expand All @@ -36,8 +37,8 @@ export const entry: SemanticMigration = {
+ "guessing `'service'` returns a refusal naming the missing field. ⚠️ The surface "
+ 'is a DESCRIPTOR FIELD set in plugin CODE, never stack metadata, so there is no '
+ 'source for a D2 conversion to rewrite and deliberately no schema tombstone — the '
+ 'disposition `data-driver-find-stream-retired` (#4484), `storage-service-list-retired` '
+ '(#5540) and `actor-user-roles-to-positions` (#6011) already carry. It differs from '
+ 'disposition `data-driver-find-stream-retired`, `storage-service-list-retired` '
+ 'and `actor-user-roles-to-positions` already carry. It differs from '
+ 'those in one way a reader should not have to infer: nothing is REMOVED, so tsc '
+ 'reports nothing at all — the field was already optional after step one and an '
+ 'omission still compiles. The enforced channels are all run-time: a registration '
Expand All @@ -47,7 +48,8 @@ export const entry: SemanticMigration = {
+ 'channel that arrives BEFORE a user hits a run that will not continue. In-tree the '
+ 'flip moves nothing: all six shipped pausing types (screen, wait, subflow, map, '
+ 'approval, approval_revise) declare their authority explicitly. ADR-0044 amendment '
+ '(2026-07-28) and its 2026-08-08 landing section, ADR-0019 #3801 addendum, #5561.',
+ '(2026-07-28) and its 2026-08-08 landing section, and the 2026-07-28 resume-seam '
+ 'addendum to ADR-0019 (approval as a flow node).',
acceptanceCriteria:
'Every action descriptor your plugin registers for a node type that can suspend '
+ 'declares `resumeAuthority`. Booting the stack logs no `declares supportsPause but '
Expand All @@ -56,12 +58,12 @@ export const entry: SemanticMigration = {
+ "through the generic route succeeds for the ones you declared `'any'`, and answers "
+ "403 (`PERMISSION_DENIED`) for the ones you declared `'service'`, which continue "
+ 'through your own service API instead. ⚠️ `supportsPause` is no longer the '
+ 'declaration nothing enforced (#5703, closed by #6667): an executor whose '
+ 'declaration nothing enforced: an executor whose '
+ '`execute()` returns `suspend: true` while leaving `supportsPause` false is still '
+ 'warned about by neither warning channel, but '
+ '`AutomationEngine.refuseUndeclaredSuspension` now refuses that suspension at the '
+ 'one seam every suspension passes through — a guard-class failure no `fault` edge '
+ 'routes — so it needs no hand-check. The residue that does: an executor registering '
+ 'NO descriptor declares nothing for either warning or the refusal to read, so its '
+ 'pauses are still created and refused only later, on the resume route (#5561).',
+ 'pauses are still created and refused only later, on the resume route.',
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,25 @@ export const entry: SemanticMigration = {
reason:
'The MIRROR-IMAGE sibling of `actor-user-roles-to-positions`, and the reason both are in this '
+ 'step: the hook `ctx.session` carried `roles` declared-and-never-produced (removed '
+ 'outright, #5050), while the ACTION body\'s `ctx.session` carries it '
+ 'outright), while the ACTION body\'s `ctx.session` carries it '
+ 'produced-and-really-populated. `buildActionSession()` '
+ '(`packages/runtime/src/action-execution.ts`) copies `ExecutionContext.positions` '
+ 'into a key spelled `roles` — the ADR-0090 D3 vocabulary handed to the author under '
+ 'the one spelling that ADR bans — so a body author met two different answers to one '
+ 'key name on one platform: rejected in a hook, live and full of values in an action. '
+ '#5613 ruled contract-first (maintainer, 2026-08-06: "C skeleton + A semantics"): '
+ 'phase 1 (#5697) declared the previously undeclared shape as `ActionSessionSchema`, '
+ 'and phase 2 renames the key. `positions` is now the canonical key on that schema '
+ 'and `roles` a deprecated alias of it (#5779); the producer emits both for one '
+ 'deprecation window (#5613 runtime half), after which `roles` is removed on the path '
+ 'the v16 session-alias removal already walked (#3280 deprecated → #3290 removed). '
+ 'The maintainer ruled contract-first on 2026-08-06 ("C skeleton + A semantics": declare '
+ 'the shape as it stands first, then rename on the typed face): phase 1 declared the '
+ 'previously undeclared shape as `ActionSessionSchema`, and phase 2 renames the key. '
+ '`positions` is now the canonical key on that schema and `roles` a deprecated alias of '
+ 'it; the producer emits both for one deprecation window (the runtime half of the same '
+ 'ruling), after which `roles` is removed on the path the v16 session-alias removal '
+ 'already walked (the hook session\'s `tenantId` alias: deprecated first, removed in the '
+ 'next major). '
+ 'Why this is a D3 semantic TODO and not a D2 conversion, on two independent grounds: '
+ 'FIRST, there is no source to convert — an action `ctx.session` is constructed per '
+ 'dispatch and never persisted, so no `sys_metadata` row, example or template can '
+ 'carry the key — the `openApi31` (#4579) / `activationEvents` (#4657) / '
+ '`hook-context-session-roles-retired` (#5050) shape. SECOND, the only place the key '
+ 'carry the key — the `openApi31` / `activationEvents` / '
+ '`hook-context-session-roles-retired` shape. SECOND, the only place the key '
+ 'is ever SPELLED is inside an action body: author-written JS/TS, or a sandboxed '
+ 'script whose `ScriptContext.session` is still `unknown`. A declarative transform '
+ 'cannot safely rewrite an identifier inside free-form code — exactly the reason the '
Expand All @@ -38,7 +40,7 @@ export const entry: SemanticMigration = {
+ 'authorable-surface ratchet adjudicates) belongs to the release that closes the '
+ 'window. Until then this entry IS the channel: `spec-changes.json` and the generated '
+ 'upgrade guide are how a reader learns the rename before the removal reaches them. '
+ 'ADR-0090 D3, ADR-0087, #5613 / #5779.',
+ 'ADR-0090 D3, ADR-0087.',
acceptanceCriteria:
'No action body reads `ctx.session.roles`; every such read is `ctx.session.positions` '
+ 'and observes the same array (the rename is a rename — the VALUE is '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const entry: SemanticMigration = {
+ 'no schema tombstone either: nothing ever ran a driver object through '
+ '`DriverInterfaceSchema.parse()`, so a prescription there would have no one to '
+ 'reach. The enforced channel is tsc, and it points at callers. ADR-0049 / '
+ 'ADR-0078, #4484.',
+ 'ADR-0078.',
acceptanceCriteria:
'No code calls `driver.findStream(...)`; large reads page through `find()` with '
+ '`limit`/`offset` (which guarantees a total order across the whole walk) or go '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ export const entry: SemanticMigration = {
+ 'layer spends a named 400 (`QUERY_OBJECT_MISMATCH`) refusing the inconsistency. The '
+ 'driver side paid in blanket casts: a direct caller holding only a `where` could not '
+ 'name the type, wrote `as any`, and switched off checking for `where` / `orderBy` / '
+ '`fields` along with it — 20 such sites measured in cloud#1053, and cloud#1030\'s '
+ '`$like` reached runtime through exactly that hole. This is a TS contract surface with '
+ '`fields` along with it — 20 such sites were measured in the downstream cloud codebase, '
+ 'and a `$like` the type layer would have caught reached runtime there through exactly '
+ 'that hole. This is a TS contract surface with '
+ 'no authored source for the chain to rewrite, which is why it is a semantic entry and '
+ 'not a D2 conversion; for a typed caller the compiler names every site (TS2353 '
+ '`\'object\' does not exist in type \'DriverQuery\'`), and for an untyped JS caller '
Expand All @@ -29,14 +30,15 @@ export const entry: SemanticMigration = {
+ 'unchanged (excess properties are only rejected on fresh literals), and an '
+ 'implementation still declaring `query: QueryAST` keeps compiling under parameter '
+ 'bivariance. What an implementation may no longer do is READ `query.object` — callers '
+ 'are now entitled to omit it. Registered by the #6350 stock reconciliation. #5181 was '
+ 'the audit\'s CONTROL sample, drawn to show that not every flagged candidate is an '
+ 'are now entitled to omit it. Registered by the stock reconciliation that compared the '
+ 'breaking changesets already on the v17 release train against this ledger. This change '
+ 'was that audit\'s CONTROL sample, drawn to show that not every flagged candidate is an '
+ 'omission, and it was one: the seven `IDataDriver` hits in the ledger are all prose '
+ 'inside other entries, and the only subject-level hit on this interface is '
+ '`data-driver-find-stream-retired` — a DIFFERENT member. Two later, smaller driver '
+ 'call-parameter changes (#6321, #6083) both registered, and both cite #5181 as '
+ 'background; the larger sibling they derive from never got its own entry. ADR-0087, '
+ '#5181 (backfilled #6350).',
+ 'call-parameter changes both registered, and both cite this narrowing as '
+ 'background; the larger sibling they derive from never got its own entry. ADR-0087 '
+ '(backfilled by that reconciliation).',
acceptanceCriteria:
'No `IDataDriver` call site passes an inline literal carrying `object:` — `driver.find('
+ '"account", { object: "account", where: … })` becomes `driver.find("account", { where: '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const entry: SemanticMigration = {
+ '`DataEngineBatchRequestSchema`, so a `retiredKey()` prescription would have no one '
+ 'to reach; its three `authorable-surface.json` baseline lines and its '
+ '`json-schema.manifest.json` entry are dropped in the same change, deliberately. '
+ 'The enforced channel is tsc. ADR-0049 / ADR-0078, #4618.',
+ 'The enforced channel is tsc. ADR-0049 / ADR-0078.',
acceptanceCriteria:
'No code calls `engine.batch(...)` and no type references `DataEngineBatchRequest`; '
+ 'in-process multi-write atomicity goes through `IObjectQLEngine.transaction(cb)`, a '
Expand Down
Loading
Loading