Skip to content

Commit 38e8452

Browse files
committed
feat(spec)!: retire datasource retryPolicy / healthCheck / external inert keys (#4583 B/C/D)
The last nine dead properties on `datasource`, all authorWarn'd, none bridgeable — each already had a DIFFERENT live mechanism doing the job it appeared to configure: - `retryPolicy` (4 keys): no connect or query path retried on it. Connection failure is the boot policy in the datasource connection service (degraded boot / bootCritical fail-fast), which does not retry on a schedule. - `healthCheck` (3 keys): nothing scheduled a probe, so `enabled` enabled nothing. Liveness is probed ON DEMAND via the driver handle's ping() / checkHealth(). Not to be confused with external.validation.checkIntervalMs, the one recurring datasource timer, which checks SCHEMA DRIFT. - `external.label`: the federation block never had a display name of its own; Setup renders the top-level `label`. - `external.requirePermission`: no authorization check consulted it. Federated access is governed by ordinary object permission sets + RLS — naming a permission that is never required is the false-compliance shape ADR-0049 removes. The retryPolicy rejection deliberately does NOT offer a rename. hook.retryPolicy and job.retryPolicy ARE enforced, but they are a different key on a different type and spell the delay `backoffMs`, not `baseDelayMs` — and that inconsistency is itself the evidence nothing read the datasource one, since no code reads both spellings. #4488 named this the sharpest trap in the type; the prescription and a test both pin it. Committed as one change rather than three: B/C/D share a single ADR-0087 conversion (house style for multi-key removals — `surface` carries all four clauses), one ledger, one README row and one set of regenerated artifacts, so split commits would each fail their own gates. The three changesets keep the release-note granularity the issue asked for. Also fixes a test that used `external: { label: … }` only to make the block non-empty — `external: {}` says what it meant. datasource liveness ledger: 9 dead -> 0. It was seeded with 20, the highest dead ratio of any governed type. Strictness-ledger site count 8 -> 6, data/ 164 -> 162. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5CYr5SDwe85gH2Jr5KSgu
1 parent e533b0b commit 38e8452

16 files changed

Lines changed: 304 additions & 144 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/example-showcase": patch
4+
---
5+
6+
feat(spec)!: retire `external.label` and `external.requirePermission` (#4583 batch D)
7+
8+
Two keys on the federation block, both read by nothing.
9+
10+
**`external.label`** — nothing rendered the federation block's own label. Setup →
11+
Datasources renders the datasource's **top-level** `label`, which every datasource already
12+
has, so this was a second display name that never displayed. The showcase example declared
13+
both; it now declares only the one that shows.
14+
15+
**`external.requirePermission`** — no authorization check ever consulted it. A permission
16+
named here gated nothing: access to a federated datasource's data is governed by the
17+
ordinary object permission sets and RLS, exactly as for a managed datasource. Naming a
18+
permission that is never required is the false-compliance shape ADR-0049 exists to remove
19+
— it reads like an access control and is one only in the author's head.
20+
21+
FROM → TO: delete `external.label` (use the top-level `label`); delete
22+
`external.requirePermission` and grant or withhold the object permissions instead.
23+
`os migrate meta --from 16` removes both automatically (conversion
24+
`datasource-inert-blocks-removed`).
25+
26+
With these, the `datasource` liveness ledger reaches **zero dead properties** — down from
27+
the 20 it was seeded with in #4487, the highest dead ratio of any governed type.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: retire `datasource.healthCheck` — no probe loop ever existed (#4583 batch C)
6+
7+
Three keys — `enabled`, `intervalMs`, `timeoutMs` — declared, strict-guarded, read by
8+
nothing. No health-check loop was ever scheduled, so `enabled: true` enabled nothing and
9+
the two timeouts bounded nothing.
10+
11+
Connection liveness is probed **on demand** through the driver handle's `ping()` /
12+
`checkHealth()`, which the datasource admin service calls for "Test connection". That is
13+
the mechanism — it needs no configuration here and never read this block.
14+
15+
Note what it is NOT to be confused with: `external.validation.checkIntervalMs` is the one
16+
recurring datasource timer, and it checks **schema drift** on a federated datasource, not
17+
connection liveness. It is unaffected.
18+
19+
FROM → TO: delete the block. `os migrate meta --from 16` removes it automatically
20+
(conversion `datasource-inert-blocks-removed`).
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: retire `datasource.retryPolicy` — nothing ever retried on it (#4583 batch B)
6+
7+
Four keys — `maxRetries`, `baseDelayMs`, `maxDelayMs`, `backoffMultiplier` — declared,
8+
strict-guarded, and read by no connect or query path. Connection failure is handled by
9+
the boot policy in the datasource connection service (degraded boot, or `bootCritical`
10+
fail-fast); nothing retries on a schedule, so setting `maxRetries: 5` changed nothing.
11+
12+
**Do not "fix" this by renaming keys.** `hook.retryPolicy` and `job.retryPolicy` ARE
13+
enforced — but they are a different key on a different type, and they spell the delay
14+
`backoffMs`, not `baseDelayMs`. That very inconsistency is the evidence nothing read the
15+
datasource one: no code in the repo reads both spellings. Moving these values onto a hook
16+
or a job only makes sense if you actually want that hook or job retried.
17+
18+
FROM → TO: delete the block. `os migrate meta --from 16` removes it automatically
19+
(conversion `datasource-inert-blocks-removed`). `DatasourceSchema` is `.strict()`, so a
20+
leftover `retryPolicy` is a loud rejection carrying this prescription — never a silent
21+
strip.

content/docs/references/data/datasource.mdx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,12 @@ const result = DatasourceSchema.parse(data);
3636
| **driver** | `string` || Underlying driver type |
3737
| **config** | `Record<string, any>` || Driver specific configuration |
3838
| **pool** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool settings |
39-
| **healthCheck** | `{ enabled: boolean; intervalMs: number; timeoutMs: number }` | optional | Datasource health check configuration |
4039
| **ssl** | `{ enabled: boolean; rejectUnauthorized: boolean; ca?: string; cert?: string; … }` | optional | SSL/TLS configuration for secure database connections |
41-
| **retryPolicy** | `{ maxRetries: number; baseDelayMs: number; maxDelayMs: number; backoffMultiplier: number }` | optional | Connection retry policy for transient failures |
4240
| **description** | `string` | optional | Internal description |
4341
| **active** | `boolean` || Is datasource enabled |
4442
| **autoConnect** | `boolean` || Force a live driver connection at boot even when managed + unrouted (ADR-0062 D2). |
4543
| **schemaMode** | `Enum<'managed' \| 'external' \| 'validate-only'>` || Schema ownership mode |
46-
| **external** | `{ label?: string; allowedSchemas?: string[]; allowWrites: boolean; validation: object; … }` | optional | External datasource federation settings (schemaMode != "managed") |
44+
| **external** | `{ allowedSchemas?: string[]; allowWrites: boolean; validation: object; credentialsRef?: string; … }` | optional | External datasource federation settings (schemaMode != "managed") |
4745
| **origin** | `Enum<'code' \| 'runtime'>` || Datasource provenance (server-managed, read-only) |
4846
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
4947
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
@@ -82,13 +80,11 @@ External datasource federation settings (schemaMode != "managed")
8280

8381
| Property | Type | Required | Description |
8482
| :--- | :--- | :--- | :--- |
85-
| **label** | `string` | optional | Display label, e.g. "Snowflake — ANALYTICS / PROD" |
8683
| **allowedSchemas** | `string[]` | optional | Whitelist of remote schemas/databases that may be exposed. |
8784
| **allowWrites** | `boolean` || Global write gate. Individual objects must also opt in via object.external.writable. |
8885
| **validation** | `{ onMismatch: Enum<'fail' \| 'warn' \| 'ignore'>; checkOnBoot: boolean; checkIntervalMs?: number }` || Boot/drift validation policy |
8986
| **credentialsRef** | `string` | optional | Reference into the secrets store; never inline credentials. |
9087
| **queryTimeoutMs** | `number` || Hard cap on per-query execution time. |
91-
| **requirePermission** | `string` | optional | Optional convenience: gate the entire datasource behind a single role. |
9288

9389

9490
---

docs/audits/2026-07-unknown-key-strictness-ledger.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts).
454454
| `notification.zod.ts` / `offline.zod.ts` / `report.zod.ts` | 3 ea | authorable (p) | |
455455
| `sharing.zod.ts` | 2 | authorable (p) | public-sharing config |
456456

457-
### `data/`164 sites
457+
### `data/`162 sites
458458

459459
| File | Sites | Class | Note |
460460
|---|---|---|---|
@@ -465,7 +465,7 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts).
465465
| `field.zod.ts` | 11 | authorable | partially strict |
466466
| `filter.zod.ts` / `query.zod.ts` | 11+5 | open | query dialect — user data flows through; validated semantically elsewhere. `query.zod.ts` dropped one site in #4196: `FieldNodeSchema`'s nested-select object form was declared-but-inert and narrowed to `z.string()`, so the union's second member is gone. Four more left in #4286 with the `joins`/`windowFunctions` removals: `JoinNodeBaseSchema`, `WindowFunctionNodeSchema`, and `WindowSpecSchema`'s two blocks (outer + `frame`) were deleted with their clusters. Class unchanged |
467467
| `driver-nosql.zod.ts` / `driver.zod.ts` / `driver-sql.zod.ts` | 10+9+2 | wire | driver capability contracts |
468-
| `datasource.zod.ts` | 8 | authorable | **strict as of #4001 data step** — all 8: `DatasourceSchema` (+ `pool` / `healthCheck` / `ssl` / `retryPolicy`), `ExternalDatasourceSettingsSchema` (+ `validation`), `DriverDefinitionSchema`. `config` stays `z.record` **at this level** by construction (per-driver shapes), but is no longer unchecked: **#4410** made `DatasourceSchema`'s refinement parse it against the contract for the declared driver (`driver/config-registry.zod.ts`), so the openness here is a shape this level cannot express rather than the absence of one. This row used to add "the driver's own `configSchema` validates them", which was false until #4410 landed the parse site it names. #4410 extended the same parse to each `readReplicas` entry; **#4468 retired that key** — no driver ever opened a replica connection and no query path splits reads from writes, so the entries were being checked against a contract nothing would apply. Strictness makes a dropped key loud; it cannot make a slot live, and a *precisely validated* dead slot is the more convincing lie | **#4583 dropped the ninth site**: `DatasourceCapabilities` is gone — eleven flags no code read, on a block whose strictness was the clearest case of this row's own closing sentence. `readOnly` in particular was *precisely validated* and completely inert, and had been relocated twice (#4410, #4465) toward somewhere it might be enforced; the shipped CRM example called a datasource a read replica on the strength of it while writes went through. Class unchanged
468+
| `datasource.zod.ts` | 6 | authorable | **strict as of #4001 data step** — all 6: `DatasourceSchema` (+ `pool` / `ssl`), `ExternalDatasourceSettingsSchema` (+ `validation`), `DriverDefinitionSchema`. **#4583 B/C dropped two more sites**: the `healthCheck` and `retryPolicy` blocks are gone — nothing scheduled a probe and nothing retried, so their strictness was validating a shape no code consumed. `config` stays `z.record` **at this level** by construction (per-driver shapes), but is no longer unchecked: **#4410** made `DatasourceSchema`'s refinement parse it against the contract for the declared driver (`driver/config-registry.zod.ts`), so the openness here is a shape this level cannot express rather than the absence of one. This row used to add "the driver's own `configSchema` validates them", which was false until #4410 landed the parse site it names. #4410 extended the same parse to each `readReplicas` entry; **#4468 retired that key** — no driver ever opened a replica connection and no query path splits reads from writes, so the entries were being checked against a contract nothing would apply. Strictness makes a dropped key loud; it cannot make a slot live, and a *precisely validated* dead slot is the more convincing lie | **#4583 dropped the ninth site**: `DatasourceCapabilities` is gone — eleven flags no code read, on a block whose strictness was the clearest case of this row's own closing sentence. `readOnly` in particular was *precisely validated* and completely inert, and had been relocated twice (#4410, #4465) toward somewhere it might be enforced; the shipped CRM example called a datasource a read replica on the strength of it while writes went through. Class unchanged
469469
| `driver/memory.zod.ts` / `driver/mongo.zod.ts` / `driver/postgres.zod.ts` | 6+1+1 | authorable | The per-driver shapes for the `config` slot — what an author actually writes under `datasource.config` (`host`, `port`, `filename`). **Undeclared here until the coverage walk went recursive** (see below): a subdirectory was invisible to the gate, so these sites sat outside the map while the map reported full coverage. **Strict as of #4410**, which is also what unblocked them: this row previously read "strictness here would enforce nothing" because nothing parsed `datasource.config` against these schemas and both `*DriverSpec.configSchema` literals were `{}`. Now `DatasourceSchema` parses `config` against them, and the same schemas project onto `configSchema` and onto the Studio connection form. (#4410 also ran the parse over each `readReplicas` entry; #4468 retired that key outright — see the row above.) `postgres.zod.ts` drops a site: its `ssl` was a `boolean | {ca, cert, key, …}` union, and the object arm is gone — certificates now live in the datasource-level `ssl` block (declared, strict, and until #4410 read by nobody), leaving `config.ssl` as the on/off shorthand. That narrowing is forced by the same projection: the Studio form renders anything that is not boolean/enum/number as a TEXT INPUT, so a union here would have produced a wizard whose every `ssl` value the new gate rejects. `memory.zod.ts` keeps 6 but loses two KEYS — `indexes` / `maxRecordsPerObject`, which `InMemoryDriverConfig` has no field for, removed under ADR-0049 rather than blessed by the new gate |
470470
| `driver/mysql.zod.ts` / `driver/sqlite.zod.ts` | 1+2 | authorable | The rest of the `config` contract, added by #4410. `mysql.zod.ts` and `sqlite.zod.ts` (sqlite + sqlite-wasm) are shapes that **never existed** — both driver ids were offered by the connection form and buildable by the shared factory, with no config contract anywhere, so `driver: 'sqlite'` + a misspelled `filename` was an ephemeral `:memory:` database reported as configured. All three sites strict, same error factory as the rest of the campaign. (Their sibling `driver/common.zod.ts` holds shared enums and prescription strings and has no `z.object(` site, so the coverage gate skips it) |
471471
| `analytics.zod.ts` | 8 | mixed (p) | |

docs/protocol-upgrade-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ The `script` flow node converges on its one real path (#4343). It had four ways
189189
| `flow-node-wait-timeout-keys-removed` | `flow.node.waitEventConfig` | waitEventConfig keys 'timeoutMs' (→ 'timerDuration', stringified — its only reader used it as the duration) and 'onTimeout' (removed — zero readers, so no timeout ever fired) (#4158) | retired — `migrate meta` only |
190190
| `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 |
191191
| `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 |
192+
| `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 |
192193
| `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 |
193194

194195
### Semantic (delegated to you, with acceptance criteria)

examples/app-showcase/src/system/datasources/showcase-external.datasource.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export const ShowcaseExternalDatasource = defineDatasource({
3434
// same place the fixture writes it. Sits next to the managed standalone.db.
3535
config: { filename: '.objectstack/data/showcase_external.db' },
3636
external: {
37-
label: 'External Analytics DB — read-only federation demo (ADR-0015)',
37+
// `external.label` was removed in #4583 — nothing read the federation
38+
// block's own label; the top-level `label` above is what Setup renders.
3839
allowWrites: false,
3940
validation: { onMismatch: 'warn', checkOnBoot: true },
4041
},

packages/cli/src/utils/lint-liveness-properties.test.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -195,28 +195,27 @@ describe('lintLivenessProperties', () => {
195195
// the type that most needed it: 20 of its 43 props have no runtime consumer,
196196
// and until #4487 nothing told an author so.
197197

198-
it('warns on the dead datasource blocks that remain — healthCheck / retryPolicy (#4487)', () => {
199-
// `capabilities` left this list in #4583: the block was REMOVED from the
200-
// schema, so an author who writes it now gets a hard parse rejection with a
201-
// prescription — a stronger signal than a lint warning, and the reason its
202-
// ledger rows are gone rather than flipped. healthCheck / retryPolicy are
203-
// still authorable and still dead (batches B and C of #4583).
198+
it('no longer warns on ANY datasource block — the whole dead surface is gone (#4583)', () => {
199+
// This assertion has now inverted twice, and the direction of travel is the
200+
// point. It began (#4487) asserting warnings on capabilities/healthCheck/
201+
// retryPolicy; batch A removed `capabilities`, so it narrowed to the other
202+
// two; batches B/C/D removed those as well. Every one of the twenty dead
203+
// datasource properties is now a hard parse rejection carrying its own
204+
// prescription — strictly stronger than an advisory lint warning, which is
205+
// why their ledger rows are deleted rather than flipped.
206+
//
207+
// Kept (rather than deleted) as a REGRESSION GUARD: it runs against the
208+
// real shipped ledger, so re-introducing a dead+authorWarn datasource
209+
// property fails here rather than shipping quietly.
204210
const findings = lintLivenessProperties({
205211
datasources: [{
206212
name: 'warehouse',
213+
label: 'Warehouse',
207214
driver: 'postgres',
208215
config: { host: 'db.internal', database: 'analytics' },
209-
healthCheck: { enabled: true, intervalMs: 30000 },
210-
retryPolicy: { maxRetries: 5, baseDelayMs: 1000 },
211216
}],
212217
});
213-
const msgs = paths(findings);
214-
expect(msgs.some((m) => m.includes('healthCheck.enabled'))).toBe(true);
215-
expect(msgs.some((m) => m.includes('healthCheck.intervalMs'))).toBe(true);
216-
expect(msgs.some((m) => m.includes('retryPolicy.maxRetries'))).toBe(true);
217-
expect(msgs.some((m) => m.includes('retryPolicy.baseDelayMs'))).toBe(true);
218-
// The removed block must no longer be reported by the lint at all.
219-
expect(msgs.some((m) => m.includes('capabilities'))).toBe(false);
218+
expect(findings).toEqual([]);
220219
});
221220

222221
// The entry the whole audit was worth doing for. `capabilities.readOnly` read

packages/spec/authorable-surface.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3238,12 +3238,10 @@
32383238
"data/Datasource:description",
32393239
"data/Datasource:driver",
32403240
"data/Datasource:external",
3241-
"data/Datasource:healthCheck",
32423241
"data/Datasource:label",
32433242
"data/Datasource:name",
32443243
"data/Datasource:origin",
32453244
"data/Datasource:pool",
3246-
"data/Datasource:retryPolicy",
32473245
"data/Datasource:schemaMode",
32483246
"data/Datasource:ssl",
32493247
"data/Dimension:description",
@@ -3392,9 +3390,7 @@
33923390
"data/ExternalDatasourceSettings:allowWrites",
33933391
"data/ExternalDatasourceSettings:allowedSchemas",
33943392
"data/ExternalDatasourceSettings:credentialsRef",
3395-
"data/ExternalDatasourceSettings:label",
33963393
"data/ExternalDatasourceSettings:queryTimeoutMs",
3397-
"data/ExternalDatasourceSettings:requirePermission",
33983394
"data/ExternalDatasourceSettings:validation",
33993395
"data/ExternalFieldMapping:defaultValue",
34003396
"data/ExternalFieldMapping:readonly",

0 commit comments

Comments
 (0)