Skip to content

Commit 93e9e42

Browse files
fix(spec): the etl-pipeline and unresolvable-where-column migration entries state what the tree does (#20838)
Fixes #20773 Fixes #20467 Clause-②: no Two ADR-0087 D3 semantic entries no longer said what the tree does. This PR corrects the text only. It has one commit per card and one shared `patch` changeset for `@objectstack/spec`. Each entry was edited by hand at its source, and every generated copy was written by its own generator. **Owed before enqueue:** the at-tier contract review by the owning seat. The review face is `packages/spec/src/**` (non-test). This PR does not carry that record. ## Card 20773: `etl-pipeline-layer-retired` (protocol 17), commit `27070673` The `replacement` text said `syncConfig.schedule` was retired "in 18 under ADR-0049". Measured on base `810d42b6`: - `packages/spec/src/integration/connector.zod.ts:296` reads: "`syncConfig.schedule` was DELETED here in @objectstack/spec 17 (ADR-0049 enforce-or-remove ...". - The deleting commit is `929d9e3f20` ("delete the seven cron-typed positions outright"). `packages/spec/CHANGELOG.md` lists it as `929d9e3` at line 8699, under `## 17.5.0` (line 3; the next heading is `## 17.4.0` at line 14893). The REST compare agrees: the `@objectstack/spec@17.5.0` tag is 1549 commits ahead of it and 0 behind, and `@objectstack/spec@17.4.0` is 172 commits behind it, so 17.4.0 does not contain it. - No protocol-18 entry or conversion names `syncConfig`: `git grep syncConfig` over `src/migrations/entries` and `src/conversions` has 0 hits outside this entry. The sentence now reads "the same measurement that deleted `syncConfig.schedule` in @objectstack/spec 17 under ADR-0049", which matches the wording of `connector.zod.ts`. The rest of that `replacement` paragraph (the `ConnectorSchema.syncConfig` routing) is untouched, because that rewrite belongs to #20281. The released `CHANGELOG.md` sentence (line 11485) is left as history. ## Card 20467: `driver-sql-unresolvable-where-column-refused` (protocol 18), commit `f45b0968` The entry named only a `where` column on `find()` / `findOne()` / `count()`, and only `INVALID_FILTER` / 400. Measured on base `810d42b6`, which contains `3e8b492d` (`git merge-base --is-ancestor 3e8b492 HEAD` exit 0): - `TursoDriver.remoteReadFault` (`packages/drivers/driver-turso/src/turso-driver.ts`) classifies every remote read exit, `aggregate` included, with the local face's own `SqlDriver.aggregateBackendFault` (`packages/drivers/driver-sql/src/sql-driver.ts:10768`). - `packages/drivers/driver-turso/src/turso-local-remote-missing-table-column-parity.test.ts` pins both faces to one answer. An aggregate over an absent table gets `DATABASE_ERROR` / 500. An aggregate grouped by, or summing, a declared field whose column is absent gets `INVALID_FIELD` / 400. An aggregate whose `where` names that field gets `INVALID_FILTER` / 400. Before, the remote face answered `[]` to all three. What changed in the entry: - **`surface`** names the `aggregate()` door of the remote face and its three codes. - **`replacement`** (the remedy) adds grouping and aggregating, and "so the object's table exists". - **`acceptanceCriteria`** adds reports and dashboards that group by, or aggregate over, a missing column. It quotes the driver's `INVALID_FIELD` message fragment "has no column for, so the aggregate never ran" (`unresolvableAggregateColumnRefusal`) and names `DATABASE_ERROR` / 500 for an absent table. - **`reason`** is not touched. **Generated copies for this card: `registry.ts` only.** `spec-changes.json` and `docs/protocol-upgrade-guide.md` project the chain up to the current protocol (`spec-changes.json` has `"from": 16, "to": 17`). Neither carries any step-18 entry: `grep -c driver-sql-unresolvable-where-column-refused` answers 0 in each. The generators ran and left both files unchanged. The claim's file surface named them, so this is stated here as measured, not skipped. ## Proof that the generated diff is only the entries' text `git diff --stat 810d42b f45b096`: ``` .changeset/20773-etl-entry-syncconfig-schedule-deleted-in-17.md | 28 ++++++++++++++++++++++++++++ docs/protocol-upgrade-guide.md | 2 +- packages/spec/spec-changes.json | 4 ++-- .../spec/src/migrations/entries/semantic/17.etl-pipeline-layer-retired.ts | 4 ++-- .../entries/semantic/18.driver-sql-unresolvable-where-column-refused.ts | 16 ++++++++++++---- packages/spec/src/migrations/registry.ts | 20 ++++++++++++++------ 6 files changed, 59 insertions(+), 15 deletions(-) ``` - **Commit `27070673`.** A token diff (`git diff --word-diff=porcelain --word-diff-regex='[^[:space:]]+'`) shows exactly two swaps, `retired` to `deleted` and `18` to `@objectstack/spec 17`. They appear in 5 places: the entry, `registry.ts`, `spec-changes.json` twice, and the upgrade guide. No other token changed. - **Commit `f45b0968`.** The 16 changed lines of `registry.ts` are byte-identical, with indentation normalised, to the 16 changed lines of the entry file (`cmp` exit 0). They sit in 4 hunks at lines 9156 to 9244, inside the entry block that starts at `registry.ts:9151`. - **Control, one-shot.** With the entry carrying the new sentence, each generated copy was restored to `810d42b6`, and each gate went red: `check:migration-registry` exit 1, `check:spec-changes` exit 1, `check:upgrade-guide` exit 1. `git checkout HEAD --` then restored each copy. Its blob hash equals HEAD's, and `git diff HEAD` is empty. So every copy is generator-owned and gated. ## Verification at the final head `f45b0968` - `pnpm --filter @objectstack/spec build`: exit 0. Spec has no workspace dependencies, so its dependency closure is empty. - `pnpm --filter @objectstack/spec exec vitest run --project local`: 578 files, 17065 passed, 1 todo. The existing pins over both entries (`migrations.test.ts`, `cron-typed-positions-retirement.test.ts`) pass 172 of 172. - `pnpm --filter @objectstack/spec run typecheck` (tsc, scripts typecheck, test typecheck): exit 0. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 83 commands. All 83 were run, each with its exit code captured to a file before any pipe. The `--ran` reconciliation reads: 83 derived, 81 run, 2 NOT-MEASURED, 0 UNRUN. The named families all exit 0: `check-adr-0087-registration --base origin/main` ("this PR adds no declared-breaking changeset"), `check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`, `check:generated` ("All 15 generated artifacts are up to date"), `check:doc-authoring`, `check:changeset-no-major`, `check-empty-changeset` and `check:nul-bytes`. - **NOT MEASURED, 2 gates. CI runs both.** - `node scripts/check-plugin-teardown-shape.mjs --self-test` exits 3. Its positive control is pinned to commit `621a4876`, which this shallow clone cannot reach. It only checks the checker's own health. - `pnpm check:dual-build-cjs-loads` exits 3. It needs every package's `dist/`, which a text-only spec diff does not build. - Two other gates first exited 3 and were cleared by building their prerequisites (`@objectstack/formula`, `@objectstack/lint`, `@objectstack/objectql`). Both then passed: `check:doc-formula-expressions` 0 and `check:lean-entry-closure` 0. - **eslint, narrowed to the changed files.** `eslint --no-inline-config --format json` over the 6 changed paths at `f45b0968` returns 6 results. eslint's own config lints the 3 `.ts` files, with 0 errors and 0 warnings. It reports the `.md` and `.json` files as "File ignored because no matching configuration was supplied". `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, as it states at lines 326 to 328), so this diff cannot change a verdict on a file it did not touch. The full `pnpm lint` run belongs to CI. - **No new test.** The changed text is prose, and no consumer parses the version phrase or the code list out of it. ## Acceptance notes - `origin/main` moved 4 commits past the base, to `4b45afae`. None of those commits touches these six paths (`git diff --name-only 810d42b origin/main` has no overlap). The branch was not merged, because the queue rebuilds it on current main. - The `reason` of `driver-sql-unresolvable-where-column-refused` keeps its 2026-08-16 addendum sentence "The entry id, surface and prescription are unchanged — this is a text amendment". That sentence describes that addendum, while this PR does change `surface`. It is left as history, because the claim's file surface excludes `reason`. Nobody else is set to edit that text. - The docblock table in `turso-driver.ts` (the "remote []" column in `remoteReadFault`'s doc) is labelled "Measured at base `6e3e5462c`": it is the before-state of the refusal change, not a stale claim. This is not a finding. - Card 20467's body names the upgrade guide as a reader of this entry. At protocol 17 the guide renders no step-18 entry, so today the entry reaches authors through `os migrate meta` and `registry.ts` only. --- _Generated by [Claude Code](https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5cd403e commit 93e9e42

6 files changed

Lines changed: 59 additions & 15 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): two ADR-0087 migration entries state what the tree does — `etl-pipeline-layer-retired` dates the `syncConfig.schedule` deletion to `@objectstack/spec` 17, and `driver-sql-unresolvable-where-column-refused` names the remote `aggregate()` refusals
6+
7+
Clause-②: no
8+
9+
**`etl-pipeline-layer-retired` (protocol 17).** The entry explains that connector-attached
10+
`syncConfig` has no reader outside `packages/spec`, and cites the same measurement that removed
11+
`syncConfig.schedule`. Its `replacement` text said that key was retired "in 18". It was deleted
12+
in `@objectstack/spec` 17 under ADR-0049 (first released in 17.5.0), as the note at the deleted
13+
position in `integration/connector.zod.ts` already says. The sentence now reads "the same
14+
measurement that deleted `syncConfig.schedule` in @objectstack/spec 17 under ADR-0049".
15+
16+
**`driver-sql-unresolvable-where-column-refused` (protocol 18).** The entry named only a `where`
17+
column on `find()` / `findOne()` / `count()` and `INVALID_FILTER` / 400. On the remote face of
18+
`TursoDriver`, the `aggregate()` door answered `[]` for a missing column or a missing table. It
19+
now refuses as the local face does: `INVALID_FILTER` / 400 for a `where` column the table lacks, `INVALID_FIELD` / 400 for a
20+
`groupBy` or aggregation column the table lacks, and `DATABASE_ERROR` / 500 for an object whose
21+
table is absent. The entry's `surface` now names that door and those codes. Its remedy adds
22+
grouping and aggregating, and running schema sync so the object's table exists. Its acceptance
23+
criterion now also covers a report or dashboard that groups by, or aggregates over, a name the
24+
object has no column for.
25+
26+
Text only: no entry id, conversion or matching logic changes, and `os migrate meta` rewrites
27+
exactly what it rewrote before. The generated migration registry, `spec-changes.json` and the
28+
protocol upgrade guide carry the corrected text.

‎docs/protocol-upgrade-guide.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ ONE AUTHOR-REACHABLE SURFACE reaches this indirectly and is why it is not purely
343343
- **`enhanced-api-error-field-errors-renamed`** — `api.enhancedApiError.fieldErrors` → fields
344344
- Why not automatic: The wire has always carried `fields` — the validators, import coercion, validation-failure.ts, @objectstack/client and the console's field-error extractor all say `fields`, and nothing ever emitted `fieldErrors`, so a reader keying on it was reading a field no server sent (ADR-0078's silently-inert declaration, on the error envelope). This is a RESPONSE surface: no stack, example or template carries the key, so there is no source for the chain to rewrite — the schema tombstones it via retiredKey() and consumers move their read themselves. ADR-0114 D4 (the field-level error code catalog).
345345
- Done when: No consumer reads `error.fieldErrors`; per-field validation detail is read from `error.fields`, and constructing an EnhancedApiError with `fieldErrors` fails to parse with the rename prescription instead of silently losing the array.
346-
- **`etl-pipeline-layer-retired`** — `automation.etlPipeline / automation.etlPipelineRun / automation.etlSource / automation.etlDestination / automation.etlTransformation (the whole L2 layer of automation/etl.zod.ts, its four enums and the `ETL` factory — 9 defs, 27 exported names)` → (removed — no protocol surface replaces it, deliberately. Layer by layer: connector-attached synchronisation is `ConnectorSchema.syncConfig` (`integration/connector.zod.ts`), which is PARSED AND VALIDATED but NOT EXECUTED — a declared shape, not a running sync. `AutomationEngine.registerConnector` runs `ConnectorSchema.parse` and stores the parsed definition; nothing reads `syncConfig` back off it, and the key has no reader outside `packages/spec` at all — the same measurement that retired `syncConfig.schedule` in 18 under ADR-0049, with the other cron-typed positions nothing reads. What the platform DOES execute on a connector is its `actions`: a flow's `connector_action` node resolves the registered handler and awaits it, so an author who needs data actually moved drives it from there. Per-field value transformation on import is `shared/mapping.zod.ts`, whose `transform` is applied row by row by the REST import path and recorded key by key in `packages/spec/liveness/mapping.json`; scheduling is `system/job.zod.ts`. What has NO replacement is multi-source, multi-stage movement with joins and aggregations — because it never had an implementation either. It returns through the ENFORCE route: the engine first, the vocabulary second)
346+
- **`etl-pipeline-layer-retired`** — `automation.etlPipeline / automation.etlPipelineRun / automation.etlSource / automation.etlDestination / automation.etlTransformation (the whole L2 layer of automation/etl.zod.ts, its four enums and the `ETL` factory — 9 defs, 27 exported names)` → (removed — no protocol surface replaces it, deliberately. Layer by layer: connector-attached synchronisation is `ConnectorSchema.syncConfig` (`integration/connector.zod.ts`), which is PARSED AND VALIDATED but NOT EXECUTED — a declared shape, not a running sync. `AutomationEngine.registerConnector` runs `ConnectorSchema.parse` and stores the parsed definition; nothing reads `syncConfig` back off it, and the key has no reader outside `packages/spec` at all — the same measurement that deleted `syncConfig.schedule` in @objectstack/spec 17 under ADR-0049, with the other cron-typed positions nothing reads. What the platform DOES execute on a connector is its `actions`: a flow's `connector_action` node resolves the registered handler and awaits it, so an author who needs data actually moved drives it from there. Per-field value transformation on import is `shared/mapping.zod.ts`, whose `transform` is applied row by row by the REST import path and recorded key by key in `packages/spec/liveness/mapping.json`; scheduling is `system/job.zod.ts`. What has NO replacement is multi-source, multi-stage movement with joins and aggregations — because it never had an implementation either. It returns through the ENFORCE route: the engine first, the vocabulary second)
347347
- Why not automatic: The reading the spec dual-source cleanup used to retire L1 `DataSyncConfig` (its automation copy deleted as dead), re-measured one layer up and identical: narrative-only. No engine ever parsed, scheduled or executed an `ETLPipeline`. Measured on origin/main immediately before the removal: the only non-spec references in this repo are two fumadocs-generated documentation sources (`apps/docs/.source/*.ts`), not executors; objectui has no reference at all; there is no `liveness/etl.json` or `pipeline.json`, so no ADR-0049 gate ever had a reading on it — while the same file family's EXECUTED half does have one (`liveness/mapping.json`), which is the contrast that makes the absence meaningful rather than an oversight. The `etl` string in this registry was the one untested link the finding named, and it is not a loader path: it was the id of the retry-vocabulary entry for `ETLPipeline.retry` (a third retry-policy vocabulary the retry convergence had not covered), absorbed here. The layer was ADR-0078's asymmetry in its purest form — an author could write a complete ten-stage pipeline, get no error, and get no execution. It was also advertised: `packages/spec/docs/SYNC_ARCHITECTURE.md` named `ETLPipeline` as the recommended destination for authors displaced by the L1 retirement and listed ten transformation types with copyable examples down to `script | Custom JavaScript/Python`. That document is rewritten in the same change; a retirement whose own doc still recommends the retired layer is self-contradictory, and forwarding L1's authors to a second layer with no executor was the defect compounding rather than closing. ⚠️ `etl-retry-converged-onto-retry-policy` is SUBSUMED here, the way the `activationEvents`, dynamic plugin-loading and widget / i18n retirements each let an earlier tombstone go with the shape that carried it: both land in the unreleased protocol 17, so composed, a rename of `retry.maxAttempts` on a shape that does not survive the major has no observable effect — and keeping both would tell an upgrader to rewrite a key on a schema the same upgrade deletes. The `maxAttempts` `retiredKey()` tombstone goes with the shape that carried it, which is strictly stronger than the tombstone: there is no longer a `retry` block to author the key into. Route 3 — no carrier key, no parse site, so no D2 conversion and no tombstone; RETIRED_DEFS_BY_MAJOR plus this entry are the declaration. ADR-0049, ADR-0078.
348348
- Done when: No source imports `ETLPipeline`, `ETLPipelineParsed`, `ETLPipelineSchema`, `ETLPipelineRun(Schema)`, `ETLSource(Schema)`, `ETLDestination(Schema)`, `ETLTransformation(Schema)`, `ETLEndpointType(Schema)`, `ETLTransformationType(Schema)`, `ETLSyncMode(Schema)`, `ETLRunStatus(Schema)` or the `ETL` factory from `@objectstack/spec/automation`; `tsc` reports TS2724/TS2305 on any that survives. Every author who was pointed at L2 has been re-pointed by name: SYNC_ARCHITECTURE.md no longer lists an L2 row, no longer recommends `ETLPipeline` as L1's destination and no longer advertises a transformation-type table. The surviving layers still parse unchanged — a connector declaring `syncConfig` and an import declaring `mapping.transform` both behave exactly as they did in 16.x.
349349
- **`export-axis-opt-in`** — `security.permissionSet.objects[].allowExport (ABSENT — a permission set that never declared the key)` → an explicit `allowExport: true` on the object entry (or the `*` wildcard) of every permission set whose holders are meant to keep exporting

0 commit comments

Comments
 (0)