You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Merge origin/main into claude/issue-4661-retry-policy-dual-source
#4664 (spec key retirements + doc.tags) landed on main and touched the same
five files. Resolution:
- authorable-surface.json / spec-changes.json / docs/protocol-upgrade-guide.md
REGENERATED from source (gen:schema / gen:spec-changes / gen:upgrade-guide),
never hand-merged — hand-editing the authorable surface is forbidden (#4650).
The regenerated surface differs from main by exactly this branch's delta:
`automation/RetryPolicy:backoffMs` added, `:retryDelayMs` relabelled
[RETIRED], and system/RetryPolicy gaining jitter / maxRetryDelayMs / the
tombstone.
- conversions/registry.ts auto-merged; verified 40 entries, zero duplicate ids,
every declared conversion grouped, both `mappingInertKeysRemoved` (#4664) and
`retryPolicyConverged` (#4661) present in the major-17 block.
- migrations/registry.ts hand-resolved: both sides appended a paragraph to
step17's `rationale` and an entry to `conversionIds`. Kept both. #4664's
paragraph ended the string literal, so the concatenation was repaired and this
branch's opener reworded ("Finally" -> "The same window") to avoid two
"Finally"s in one rationale.
Re-verified after the merge that exactly ONE conversion clause still ends in
`.retryDelayMs` and it is `retry-policy-converged` — #4664 added five retired
leaves (extractQuery / errorPolicy / batchSize / includeAll / placement), none
of which collide with this cluster under the #4659 leaf-name match.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL
feat(spec)!: retire the five keys the advisory lint could never have warned about — mapping `extractQuery`/`errorPolicy`/`batchSize`, contextSelector `includeAll`/`placement` (#4509)
7
+
8
+
Five authorable keys parsed, stored, and controlled nothing. What groups them is
9
+
not the type they sit on but **why they had to go out in a major rather than
10
+
after a deprecation cycle**: four of the five carry schema DEFAULTS, and a
11
+
default materialises at parse time — so the liveness advisory lint cannot tell a
12
+
value the author wrote from one the schema supplied. Marking them would have
13
+
warned on every mapping and every selector in existence, which is why the ledger
14
+
recorded them as `_authorWarnSkipped` instead. For a key in that state, removal
15
+
is not the escalation after a warning. It is the only channel that ever reaches
16
+
the author.
17
+
18
+
**The retirement kit:**
19
+
20
+
| FROM | TO | Fix |
21
+
|---|---|---|
22
+
|`mapping.extractQuery`|*(removed)*| Delete the key. Exports run through the ordinary query API (`POST /api/v1/data/:object/query`) — no exporter has ever read a mapping artifact. |
23
+
|`mapping.errorPolicy`|*(removed)*| Delete the key. Error handling on the import path belongs to the import REQUEST's own options, not the stored mapping. |
24
+
|`mapping.batchSize`|*(removed)*| Delete the key. The write path sizes its own batches. **Do not relocate the value** — see below. |
25
+
|`app.contextSelectors[].includeAll`|*(removed)*| Delete the key. Selectors are mandatory-scope; widen `optionsSource.filter` to widen the choices. |
26
+
|`app.contextSelectors[].placement`|*(removed)*| Delete the key. Selectors always render in the sidebar header; `'topbar'` placed nothing. |
27
+
28
+
Run `os migrate meta --from 16` to rewrite existing sources automatically.
29
+
30
+
**`includeAll` is the one worth reading twice.** It was not unread — it was
31
+
deliberately *disobeyed*, and for a security reason. A context selector is a
32
+
mandatory scope, so an "All" row would clear the scope on a surface that exists
33
+
to be scoped; on Studio's package selector that means listing the platform's own
34
+
system/cloud kernel packages to a developer who scoped to their own package. The
35
+
renderer never offered an All row regardless of the flag, so `includeAll: false`
36
+
hardened nothing and `includeAll: true` unlocked nothing. `STUDIO_APP` shipped
37
+
authoring `includeAll: true` against a renderer that ignored it — that authoring
38
+
site goes with the key in this change.
39
+
40
+
**`batchSize` deliberately offers no rename.**`bulkActionDef.batchSize`,
41
+
`connector.batchSize`, `sync.batchSize`, `offline.batchSize`, the seed loader's
42
+
and the NoSQL driver cursor's are all LIVE and enforced — but each is a
43
+
different key on a different type sizing its own path, and none of them sizes a
44
+
mapping import. The rejection says so explicitly, because "removed" plus a
45
+
familiar name one line away is exactly how a dead setting gets laundered into a
46
+
live-looking one. Same trap `datasource.retryPolicy` had to defuse against
47
+
`hook`/`job``retryPolicy` (which spell the delay `backoffMs`) one issue
48
+
earlier.
49
+
50
+
Both schemas are `.strict()`, so the keys are deleted from the shape and
51
+
rejected with a `guidance` prescription rather than tombstoned; their liveness
52
+
rows are deleted rather than kept. The retired ALIAS spellings (`query`,
Copy file name to clipboardExpand all lines: docs/protocol-upgrade-guide.md
+5-2Lines changed: 5 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -164,7 +164,9 @@ The `script` flow node converges on its one real path (#4343). It had four ways
164
164
165
165
The same audit reaches the driver contract itself: `IDataDriver.findStream` is removed (#4484). It was REQUIRED — every driver and every test double had to implement it — and documented as the read "optimized for large datasets to avoid memory overflow", while two of its three implementations awaited `find()` for the whole result set and then yielded it row by row, reaching exactly the peak it promised to avoid; the third streamed for real but was the one read in that driver that skipped `buildFindOptions`, so it dropped `query.fields`. Nothing anywhere called it, which is why a contract method could carry an inverted guarantee for this long and why ~20 test doubles could satisfy it by throwing `not implemented`. Paged `find()` is the read that exists and is enforced (its total-order guarantee is checked by the shared pagination-conformance cases); a cursor-based read is worth building when a caller asks for one, which is the honest order. A TS/API surface, never stored — one semantic TODO for driver authors, no source rewrite, and no tombstone: `DriverInterfaceSchema` describes a contract that code IMPLEMENTS and nothing ever `.parse()`d a driver, so tsc is the only channel that could carry the prescription, and it carries it where it matters — at a call site.
166
166
167
-
Finally it converges the retry policy (#4661). `@objectstack/spec/automation` and `@objectstack/spec/system` each exported a `RetryPolicy`/`RetryPolicySchema` resolving to a DIFFERENT declaration, so which shape a consumer got depended only on the import path (#4411) — yet both computed `delay = base * multiplier^(retry-1)` and both executors implemented that same formula. One declaration now serves both entries with the union of their capabilities, so `job.retryPolicy` gains the `maxRetryDelayMs` ceiling and `jitter` (both enforced in `runWithPolicy`, not merely declared — jitter is what stops a fleet of jobs that failed on one outage from retrying in lockstep). The single authorable casualty is the automation spelling of the base delay: `retryDelayMs` → `backoffMs`, a pure rename that replays losslessly and is what the already-enforced retry policies (`job.retryPolicy`, `hook.retryPolicy`) call it.
167
+
Finally, five keys retire because the advisory lint could never have warned about them (#4509): mapping `extractQuery` / `errorPolicy` / `batchSize`, and app `contextSelectors[].includeAll` / `.placement`. Four of the five carry schema DEFAULTS, and a default materialises at parse time — so the liveness lint cannot tell a value the author wrote from one the schema supplied, and marking them would have warned on every mapping and every selector in existence. For a key in that state removal is not the escalation after a warning; it is the only channel that ever reaches the author, which is why they ship inside the 17.0.0 window rather than after a deprecation cycle. What they claimed: `extractQuery` promised an export path no exporter implements (exports go through the ordinary query API); `errorPolicy` offered skip/abort/retry where error handling belongs to the import REQUEST; `batchSize` sized batches the write path sizes itself; `placement` offered a topbar that places nothing. `includeAll` is the one worth reading twice — it was not unread but deliberately DISOBEYED, because context selectors are mandatory-scope and an "All" row would clear the scope: on Studio's package selector that means listing the platform's own system/cloud kernel packages to a developer who scoped to their package. `STUDIO_APP` authored `includeAll: true` against a renderer that ignored it. The mapping prescription for `batchSize` deliberately offers no rename: bulk-action, connector, sync, offline, seed-loader and NoSQL-cursor `batchSize` are all live, but each is a different key sizing its own path — the same trap `datasource.retryPolicy` vs `hook`/`job` `retryPolicy` had to defuse one issue earlier.
168
+
169
+
The same window converges the retry policy (#4661). `@objectstack/spec/automation` and `@objectstack/spec/system` each exported a `RetryPolicy`/`RetryPolicySchema` resolving to a DIFFERENT declaration, so which shape a consumer got depended only on the import path (#4411) — yet both computed `delay = base * multiplier^(retry-1)` and both executors implemented that same formula. One declaration now serves both entries with the union of their capabilities, so `job.retryPolicy` gains the `maxRetryDelayMs` ceiling and `jitter` (both enforced in `runWithPolicy`, not merely declared — jitter is what stops a fleet of jobs that failed on one outage from retrying in lockstep). The single authorable casualty is the automation spelling of the base delay: `retryDelayMs` → `backoffMs`, a pure rename that replays losslessly and is what the already-enforced retry policies (`job.retryPolicy`, `hook.retryPolicy`) call it.
168
170
169
171
The subtle half is the defaults, and it is worth stating because no gate can see it: `job.retryPolicy` defaulted `maxRetries: 3` / `backoffMultiplier: 2` while the automation shape defaulted 0 / 1, and the authorable-surface gate compares KEY SETS — a changed default is invisible to it, to the tombstone mechanism and to `spec_changes` alike. The merged declaration takes 0 / 1 (retry replays side effects, so it is opt-in — the same reading already recorded in `flow-retry-max-retries-required`), and the conversion writes the pre-17 numbers into every existing `job.retryPolicy` that omitted them. Deployed stacks therefore keep their exact behaviour; what changes is only what a NEWLY authored omission means.
170
172
@@ -185,7 +187,7 @@ The subtle half is the defaults, and it is worth stating because no gate can see
185
187
|`flow-node-script-config-aliases`|`flow.node.script.config`| script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (#3796) | live — protocol 17 loader accepts the old shape |
186
188
|`permission-rls-priority-removed`|`permission.rowLevelSecurity.priority`| RLS-policy key 'priority' removed (#3896 audit — policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome) | retired — `migrate meta` only |
|`app-dead-authoring-keys-removed`|`app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / app.mobileNavigation`| app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation' removed (2026-06 liveness audit — never read; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented) | retired — `migrate meta` only |
190
+
|`app-dead-authoring-keys-removed`|`app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / app.mobileNavigation / app.contextSelectors.includeAll / app.contextSelectors.placement`| app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation' plus contextSelectors 'includeAll'/'placement' removed (liveness audits #4001, #4509— never read; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, and includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope) | retired — `migrate meta` only |
189
191
|`field-required-notnull-explicit`|`object.fields.*.required / object.fields.*.storage.notNull`| required fields gain explicit 'storage.notNull: true' (ADR-0113 — pre-17 'required' implied the column constraint; post-17 it is only the write contract) | retired — `migrate meta` only |
190
192
|`action-inert-keys-removed`|`action.shortcut / action.bulkEnabled`| action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | retired — `migrate meta` only |
191
193
|`flow-inert-keys-removed`|`flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId`| flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle) | retired — `migrate meta` only |
@@ -198,6 +200,7 @@ The subtle half is the defaults, and it is worth stating because no gate can see
198
200
|`datasource-read-replicas-removed`|`datasource.readReplicas`| datasource key 'readReplicas' removed (#4468 — no driver opened a replica connection and no query path splits reads from writes; front replicas behind one endpoint and point `config` at it) | retired — `migrate meta` only |
199
201
|`datasource-capabilities-removed`|`datasource.capabilities`| datasource key 'capabilities' removed (#4583 — eleven flags no code read; pushdown comes from the driver's own supports.*, and `readOnly` never made anything read-only) | retired — `migrate meta` only |
200
202
|`datasource-inert-blocks-removed`|`datasource.retryPolicy / datasource.healthCheck / datasource.external.label / datasource.external.requirePermission`| datasource keys 'retryPolicy'/'healthCheck' and external 'label'/'requirePermission' removed (#4583 — nothing retried, nothing probed on a schedule, and the federation label/permission were read by nobody) | retired — `migrate meta` only |
203
+
|`mapping-inert-keys-removed`|`mapping.extractQuery / mapping.errorPolicy / mapping.batchSize`| mapping keys 'extractQuery'/'errorPolicy'/'batchSize' removed (#4509 — no exporter reads a mapping, error handling belongs to the import request, and the write path sizes its own batches) | retired — `migrate meta` only |
|`flow-node-script-branch-keys-removed`|`flow.node.script.config.actionType / flow.node.script.config.template / flow.node.script.config.recipients / flow.node.script.config.variables / flow.node.script.config.script`| script flow-node config keys 'actionType' (→ 'function' when it was shorthand for one; otherwise removed — 'email'/'slack' were logger-backed stubs that delivered nothing), plus 'template' / 'recipients' / 'variables' (fed those stubs) and 'script' (inline JS the runtime never executed) (#4343) | retired — `migrate meta` only |
203
206
|`retry-policy-converged`|`flow.node.config.retry.retryDelayMs / job.retryPolicy.maxRetries / job.retryPolicy.backoffMultiplier`| retry policy unified across job.retryPolicy and try_catch retry: base delay 'retryDelayMs' → 'backoffMs', and the pre-17 job defaults (maxRetries 3, backoffMultiplier 2) written out explicitly now that the merged default is 0 / 1 (#4661) | live — protocol 17 loader accepts the old shape |
0 commit comments