Skip to content

Commit 3af470d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20273-connector-resilience-keys-retired
# Conflicts: # packages/spec/src/migrations/registry.ts
2 parents 0d16977 + 21ab410 commit 3af470d

182 files changed

Lines changed: 8005 additions & 1224 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/metadata-protocol": minor
4+
---
5+
6+
feat(spec,metadata-protocol): `os migrate meta --stored` lists every stored page filter the record-filter conversion leaves as stored, as a TODO naming the page, the block and why — the ADR-0087 D3 TODO channel (#17321, ruling B item 2)
7+
8+
**Clause-②: yes** — `@objectstack/spec` gains public exports (`CONVERSION_TODO_CODE`,
9+
`ConversionTodoDetail`, `ConversionTodoNotice`, and the optional `ApplyConversionsOptions.onTodo`
10+
and `ConversionContext.reportTodo`), and `@objectstack/metadata-protocol` gains
11+
`StoredMigrationTodo` and `StoredMigrationRow.todos`. No door's accept set moves, and nothing
12+
that was left as stored before starts converting: every stored body is rewritten exactly as it
13+
was.
14+
15+
**What was silent.** The D2 conversion `page-component-filter-record-to-rule-array` leaves a
16+
stored filter as stored wherever no lossless rule-array spelling exists — above all a record
17+
carrying `$and` / `$or` / `$not`, which is never flattened. It emitted nothing for such a site,
18+
and `os migrate meta --stored` reads conversion notices as its change signal, so a page whose
19+
only legacy filter carried a combinator was reported as **already on protocol**.
20+
21+
**What it says now.** Each such site is a structured TODO (code `OS_METADATA_CONVERSION_TODO`)
22+
carrying its path, the shape left in place, and a reason that names the block (its type, and its
23+
`id` when it has one) and what blocks the rewrite — the combinator by name, the operator
24+
(`$null`, `$exists`, an AST `like`), the null or array value, the rule the door would refuse, or
25+
the inline rows the block renders. The stored pass lists them under their row, whatever the
26+
row's outcome:
27+
28+
```text
29+
⚠ 1 row(s) are outside this pass — each row's reason says why:
30+
• page/pipeline_board [env-wide] — the conversion chain rewrites nothing here: it left 1 site(s) of this row as stored, …
31+
TODO page-component-filter-record-to-rule-array: {"$or":[…]} left as stored at pages[0].regions[0].components[0].properties.filter — On the `object-kanban` block, this filter carries the combinator `$or`: …
32+
☐ TODO: 1 site(s) in 1 row(s) are left as stored — no conversion can rewrite them without changing what they mean, so no run of this pass will. …
33+
```
34+
35+
The same list is `rows[].todos` in `--json` and in the `POST /api/v1/meta/_migrate-stored`
36+
report. A run with no TODO prints exactly what it printed before.
37+
38+
**Outcome and exit code.** A row whose only finding is TODOs has nothing to persist and is now
39+
reported `skipped` (it was `canonical`). Like every other skip class it does not change the
40+
run's exit code: no run of this pass can clear it, because the conversion must not flatten a
41+
combinator — it is the hand rewrite's to decide. A row that also converts something keeps the
42+
outcome its conversion gives it, with its TODOs listed beside its notices. Measured through the
43+
write path: on `--apply`, a row whose leftover sits in a block's `properties.filter` or
44+
`properties.defaultFilters` is rewritten (its lossless filters persist; the metadata API's save
45+
does not refuse block props by component type), while a leftover in `dataSource.filter` fails
46+
the save, and the row's TODO says why.
47+
48+
**For code calling the conversion layer.** `onTodo` and `reportTodo` are optional. Only the
49+
stored-metadata pass passes a sink today; every other seam leaves the site as stored silently,
50+
exactly as before.

‎.changeset/17321-record-filter-d2-conversion.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ are **inline** (`data: { provider: 'value', … }`, a `data` array, or `staticDa
4545
stored: the `object-map`, `object-tree`, `object-calendar` and `object-gantt` renderers match
4646
that filter against their own rows in an in-memory data source that reads the record form but
4747
excludes every row for a rule array, so a rewrite there would empty the block. The same filter
48-
on a block that queries an object converts. Such a page keeps loading and rendering unchanged and is refused at its
49-
`filter` door on its next save — and for a combinator record that refusal no longer renders the
48+
on a block that queries an object converts. Such a page keeps loading and rendering unchanged,
49+
and its `filter` door refuses the form: at `dataSource.filter` on the page's next save; at a
50+
block's `properties.filter` / `properties.defaultFilters` only as the component-props gate's
51+
advisory finding (`os validate`, `os build`, `os lint`) — a re-save through the metadata API is
52+
not refused there, measured — and for a combinator record that refusal no longer renders the
5053
combinator as a field (`{ field: '$or', … }`); it names the combinator and says why no rule
51-
spells it. `os migrate meta --stored` does not list these rows yet: a row the conversion leaves
52-
as stored reports there as already on protocol.
54+
spells it.
55+
`os migrate meta --stored` lists each such filter left as stored as a TODO under its row.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/formula": minor
3+
"@objectstack/plugin-security": minor
4+
"@objectstack/spec": patch
5+
---
6+
7+
A row-level write check that orders a field against a bound (`>`, `>=`, `<`, `<=`) is refused with `INVALID_FILTER` / 400 when that field holds a list or an object on the record being written, instead of comparing the list's string form and admitting the write (#19886).
8+
9+
**BREAKING** — an accept-set narrowing, shipped by `@objectstack/formula` and `@objectstack/plugin-security` as `minor` under the repo's launch-window convention for accept-set narrowings. The hand-migration prescription is registered under protocol major 18 as `rls-predicate-stored-list-ordering-refused`.
10+
11+
Clause-②: no (narrowing)
12+
13+
**Security fix for RLS write checks.** Measured through the real plugin-security on driver-sql and driver-memory: `record.tags > 'a'`, with `tags` a `json` field holding `['m']`, compared `'m' > 'a'` and admitted and stored the write. `record.meta < 'a'` with `meta` holding `{ a: 1 }` compared `'[object Object]' < 'a'` and did the same, and so did a `multiple` lookup. `using` stands in as the check for a policy that declares no `check`, so the same predicate in `using` was enforced the same way on writes. No shipped row-level or sharing-rule predicate orders a field at all.
14+
15+
What changes:
16+
17+
- `@objectstack/formula`: `matchesFilterCondition` refuses `$gt` / `$gte` / `$lt` / `$lte` and `$between` on a field whose value on the record is a list or a plain object, whatever the comparand, with the same `INVALID_FILTER` / 400 and the same message as the stage 2d refusals. The refusal is per record: a record whose `json` field holds one scalar is compared as before. `null` and `Date` values are unchanged, and so is every equality against a stored list (`$eq`, `$ne`, implicit equality, `$in`, `$nin`). `$between` is not produced by the CEL lowering, so it reaches this only through a filter passed to `matchesFilterCondition` directly.
18+
- `@objectstack/plugin-security`: a check insert or by-id update whose post-image holds a list or an object in an ordered field is refused 400 and stores nothing. That includes a by-id update that edits another field of a row whose stored `json` column holds a list, because the post-image merges the stored row.
19+
- `@objectstack/spec`: the migration registry carries the entry. The stage 2a entry `rls-predicate-array-comparand-refused` now ends "Scalar != and ==, null, Date comparands, and { $field } references between single-valued columns evaluate exactly as before", which is true since stage 2d.
20+
21+
Three moves, named:
22+
23+
1. **The write check now matches driver-sql's read.** driver-sql refuses every ordering comparison, and `$between`, on a column it stores as JSON text, by declared type (400, #7398). The in-process check now refuses the same predicate on the same row (400).
24+
2. **driver-memory's read parts from the write check.** driver-memory, a test driver, compares a stored list element by element on a read and keeps returning those rows (`record.tags > 'a'` reads a row holding `['m']`), while the check now refuses writing it. This is declared on #15104, as for stage 2d's `{ $field }` half.
25+
3. **A list written into a scalar field under an ordering check now answers 400.** `status: ['m']` into a `text` field under `record.status > 'a'`, or `amount: [500]` into a `number` field under `record.amount > 10`, was admitted, and driver-sql stored it as the text `'["m"]'` / `'[500]'`. It is now refused before anything is stored.
26+
27+
**The explain answer.** `security/explain` evaluates the business RLS predicate in-process on the fetched record, so it now answers `INVALID_FILTER` / 400 where the record holds a list or an object under an ordering predicate (this stage). It already answered 400 for a `{ $field }` comparison against a list-holding column (stage 2d). For both, per operation:
28+
29+
| explain operation | driver | the enforced operation answers | same as explain's 400? |
30+
|---|---|---|---|
31+
| `read` | driver-sql | 400 `INVALID_FILTER` (the driver's refusal) | yes |
32+
| `update` | driver-memory | 400 `INVALID_FILTER` (the post-image check) | yes |
33+
| `update` | driver-sql | 403 `PERMISSION_DENIED`: the pre-image gate fails closed on the driver's 400 | no — both deny |
34+
| `read` | driver-memory | the rows its element-wise read admits | no — the test driver's read |
35+
36+
Explain itself is unchanged.
37+
38+
**What to change.** Order a single-valued column (`record.priority > 2`), or test membership in the list with `in` (`record.status in ['open', 'pending']`). A `json` or `multiple` field has no ordering.
39+
40+
<!-- adr-0087: registered rls-predicate-stored-list-ordering-refused -->
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
---
5+
6+
fix(spec)!: a `decision` branch with no `expression` — the key absent, or `null` — is refused at authoring (#19961)
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: registered flow-decision-branch-expression-absent-refused -->
11+
12+
**BREAKING** — an accept-set narrowing on one authored flow-node slot, shipped as
13+
`minor` under the launch-window convention (`check-changeset-no-major` refuses
14+
`major` until GA; breaking-ness is carried by this banner and the ADR-0087
15+
disposition above, not by the level).
16+
17+
**What changed.** `DecisionConditionSchema` declares a branch `{ label, expression }`
18+
with `expression` a required `z.string()`. Nothing enforced that: a decision node's
19+
`config` is an open record no schema is parsed against, and the expression ledger's
20+
resolver skipped an absent value as "not authored". So `conditions: [{ label: 'y' }]`
21+
passed `FlowSchema.parse`, `AutomationEngine.registerFlow` and `objectstack validate`,
22+
and the run then failed at that branch — the executor evaluates every branch it
23+
reaches, and a branch with no `expression` is a condition with no `source`, which
24+
`evaluateCondition` refuses. The ledger now marks the slot `required` (reconciled
25+
against the schema's own `required` list), and the branch is refused at all three
26+
doors through the walk and the function that already refuse a blank one — by
27+
`FlowSchema.parse` with a `custom` issue anchored at the slot (for example
28+
`nodes.1.config.conditions.0.expression`), by `registerFlow` and `objectstack validate`
29+
through that same parse, and by `validateStackExpressions` for a stack handed to it
30+
directly — with one message, led by the published `PREDICATE_SLOT_STRING_REFUSAL`
31+
sentence. `expression: null` is refused the same way, and so is a branch that wrote
32+
its predicate under `condition` (the edge's spelling), which has no `expression`
33+
either. The Studio flow designer writes the refused shape when a branch row's
34+
expression cell is left empty. Where such a branch already sits, the whole flow is
35+
refused: registered from the metadata
36+
registry or `sys_metadata` at boot, it is skipped with a `failed to register flow`
37+
warn naming it while the flows beside it register; a `defineStack({ flows })` source
38+
throws `StackSchemaInvalidError` for the whole stack; an artifact file is refused
39+
whole at load.
40+
41+
## FROM → TO
42+
43+
| you wrote | write instead |
44+
|:--|:--|
45+
| `conditions: [{ label: 'high' }]` on a `decision` node | the predicate you meant — `{ label: 'high', expression: 'record.amount > 10000' }` |
46+
| `conditions: [{ label: 'high', condition: 'record.amount > 10000' }]` | the same predicate under `expression` |
47+
| `conditions: [{ label: 'high', expression: null }]` | the predicate you meant, or `expression: 'false'` to keep the branch and never take it |
48+
49+
**One-line fix:** write the predicate under `expression`. `expression: 'false'` keeps
50+
the branch and its label and never takes it — a change of behaviour, not a preserved
51+
one: a run that reached the branch used to FAIL there, and now routes on to the next
52+
branch or the declared fallback. ⚠️ Do not drop a decision's only branch: the node
53+
then routes by its out-edges alone, and the out-edge that branch labelled is no
54+
longer held back.
55+
56+
**Unchanged.** A branch carrying a non-blank predicate parses, registers and
57+
validates as before; a blank one keeps its refusal and its own prescription
58+
(`flow-predicate-slot-blank-string-refused`); a `decision` with no `conditions`, or
59+
an empty list, still routes by its out-edges; an absent screen field `visibleWhen`
60+
is still legal (that slot is not required); and `PREDICATE_SLOT_STRING_REFUSAL`
61+
keeps its name and its text.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`liveness/app.json`: every `AppSchema` row that cited objectui's `AppSidebar.tsx` as read evidence now cites the reader that is actually mounted, and `branding.logo` is `planned` instead of riding on its container's `live`. Ledger evidence and comment prose only. ⛔ No schema, parse or accept-set change.
6+
7+
The ledgers ship inside this package (`files[]` includes `liveness`), and `@objectstack/lint` reads them to decide which authored keys draw an advisory warning, so the rows an upgrading reader or tool consults are these.
8+
9+
- **Twelve rows re-pointed, and the file `_note`.** `label`, `description`, `icon`, `branding`, `active`, `hidden`, `navigation[].id`, `navigation[].visible`, `areas[].id`, `areas[].label`, `areas[].icon` and `areas[].navigation` cited `AppSidebar`. That component was deprecated, never mounted (the console renders `UnifiedSidebar`), and objectui has since removed it, so it was never evidence of a live reader. Each row now names its mounted reader, file and symbol: `AppSwitcher`, `UnifiedSidebar`, `AppHeader`, `ConsoleLayout` into `AppShell`'s `useAppShellBranding`, `HomeAppsStrip`, `filterActiveApps`, `NavigationRenderer`, `NavMenuRenderer` and `AppManagementPage`. All are read at the `.objectui-sha` pin f8a9d0fb, and each reads its key unchanged at objectui main fb91ac9b0.
10+
- **`branding.logo` is `planned`.** Plugin-designer's app wizard and branding editor write it, and nothing mounted renders it: the console's branding handoff passes `primaryColor`, `accentColor` and `favicon` and never `logo`. The row names the objectui card that will render it (objectui#10827). It is not `authorWarn`'d: a logo URL is display metadata, and an author who sets it now loses nothing, because the value takes effect when that renderer lands. The `branding` row is drilled so `primaryColor`, `accentColor` and `favicon` keep their own `live` verdicts. Its entry in `undrilled-containers.baseline.json` goes away with the drill, and `state-counts.md` is regenerated (`app`: 56 → 59 classified, one `planned`).
11+
- **`description` is still `live`, but narrower.** Its one mounted reader is the admin app list at `/system/apps`, which shows each app's description and searches it. The sidebar presentation this row used to describe has no successor.
12+
- **Two corrections found while re-reading.** The `active` row's note said the sidebar's active-app lookup spans all apps. It skips `active: false` apps, and what keeps an inactive app routable by direct URL is the console's route resolver, which matches against every app. The two `AppSidebar` mentions in `ui/app.zod.ts` (the `recordId` template-variable note and the `AREA_ORDER_RETIRED` note) now name `NavigationRenderer` / `UnifiedSidebar`.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): every protocol-18 retirement family now carries its D3 entry, including the 25 whose data repair is a lossless D2 conversion (#20201)
6+
7+
Clause-②: no
8+
9+
ADR-0087 D3 requires one semantic (D3) entry per retirement family, even when a
10+
lossless D2 conversion already repairs the data: D2 carries the mechanical repair
11+
only, and the D3 entry says what the consumer still has to decide. Twenty-five
12+
protocol-18 families shipped a D2 conversion and no D3 entry, some of them
13+
justified by "lossless, so no semantic residue". `MIGRATIONS_BY_MAJOR[18].semantic`
14+
gains one entry per family, so `os migrate meta` lists each as a TODO on the
15+
17 → 18 hop, with its reason and acceptance criteria. Among them:
16+
17+
- the seven duration renames (`hook.timeout`, `job.timeout`, `apis[].cacheTtl`,
18+
`dashboard.refreshInterval`, the connector health / trigger durations, the memory
19+
driver's `autoSaveInterval` and the turso `timeout`). The rename keeps the value,
20+
so only the author can say whether it was ever written in the unit the new key
21+
names.
22+
- `object.tenancy.organizationField`, `view.owner` / `view.hidden`,
23+
`permission.objects.*.allowRestore` / `allowPurge` and the list-view `page` mount.
24+
Each delete is lossless, and each leaves a belief the author held that the
25+
platform never honoured.
26+
27+
No accept set moves and no conversion changes. The registry's own test now fails
28+
when a protocol-18-or-later step graduates a conversion that no D3 entry of that
29+
step names. The prose that justified the missing entries is corrected, and the
30+
protocol-17 docblock no longer calls that step's `semantic` list empty.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `os migrate meta` guidance for the `ui-*` and `plugin-*` migration entries states each lesson in words instead of citing tracker numbers
6+
7+
Clause-②: no
8+
9+
The ADR-0087 semantic entries of the `ui-*` family (component props rows, form-field
10+
and list-view refusals, the react-tier `ListView` aliases, and the retired
11+
interaction, notification, embed, widget and i18n vocabularies) and of the `plugin-*`
12+
family (the plugin manifest, runtime, health-monitor and security-scanner retirements)
13+
are printed by `os migrate meta` as the header, `why:` and `verify:` lines of a manual
14+
change. Their text sent the reader to issue-tracker numbers — some of which no longer
15+
resolve — for what a ruling, measurement or fix had decided; it now says what was
16+
decided, in the sentence being read. The same holds for the two `surface` headers that
17+
carried a number. ADR ids are kept.
18+
19+
Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the
20+
chain rewrites exactly what it rewrote before. The generated migration registry,
21+
`spec-changes.json` and the protocol upgrade guide carry the same text.

0 commit comments

Comments
 (0)