Skip to content

Commit c35bfbf

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20280-mysql-date-read-century
# Conflicts: # .changeset/20240-date-year-four-digits.md
2 parents 93af658 + a78f731 commit c35bfbf

236 files changed

Lines changed: 11101 additions & 2027 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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
fix(cli): **BREAKING** — `os generate schema` is retired, and it now says why and points at `os validate` and the per-type JSON Schemas `@objectstack/spec` publishes (#19098)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) A CLI COMMAND NAME is an invocation surface, not authorable metadata. There is no authorable key, no `sys_metadata` row and no schema to tombstone here, so there is nothing for `objectstack migrate meta` to rewrite, nothing for `spec-changes.json` to project and no FROM -> TO spelling for the upgrade guide to carry: the ruling generates no replacement file, so there is no call to rename, only one to delete. Nor is the ledger the notification channel: the command itself now refuses, exits 1 and names `os validate` and the published per-type schemas at the exact moment and place of use. Same reasoning shape as the `os g agent` retirement, one command over. -->
10+
11+
**⛔ If a script, a Makefile or a CI step in your project runs `os generate schema`
12+
(or `os g schema`), it will now exit 1 and write nothing.** That is the intended
13+
outcome: the command is gone by maintainer ruling, and the failure is how you find
14+
out. Nothing in this repository reads the file it wrote.
15+
16+
`minor`, not `major`: during the launch window this stack ships breaking changes as
17+
`minor` (pre-1.0 semantics under lockstep versioning — see
18+
`scripts/check-changeset-no-major.mjs`).
19+
20+
**What the command actually did.** `os generate schema` wrote
21+
`objectstack.schema.json`, a JSON Schema of the whole stack definition for an editor
22+
to check `objectstack.config.ts` against. It projected `ObjectStackDefinitionSchema`
23+
through a bare `z.toJSONSchema`, so the refinements the platform enforces beyond the
24+
shape — a non-blank string, a required one-of, a banned key — were missing from the
25+
file. Measured against the published projection on the tree the ruling was made on,
26+
the file lacked 874 keywords, every one of them an absence. An editor pointed at it
27+
reported a config as valid, and the platform then refused that config.
28+
29+
**Why it is retired rather than repaired.** A TypeScript configuration is typed by its
30+
own `define*` helper, and `objectstack.config.ts` is typed end to end by
31+
`defineStack`, so no config format this CLI loads is one an editor validates against
32+
a JSON Schema. JSON metadata already has the per-type schemas `@objectstack/spec`
33+
publishes, which carry the published projection. Repairing the command would have
34+
added a permanent public export to `@objectstack/spec` for a file with no reader. The
35+
ruling generates no replacement file. What you see now:
36+
37+
```
38+
✗ `os g schema` was retired — its JSON Schema passed configs the platform refuses (maintainer ruling).
39+
40+
The file it wrote described only the shape of a stack. Every rule the
41+
platform enforces beyond that shape — a non-blank string, a required
42+
one-of, a banned key — was missing from it, so an editor showed a config
43+
as valid and the platform then refused it. By maintainer ruling it is
44+
retired, not repaired, and no replacement file is generated.
45+
46+
Check a project against the rules that actually run:
47+
48+
os validate
49+
50+
For JSON metadata, point your editor at the per-type schemas that
51+
@objectstack/spec publishes. They state the rules a JSON Schema can
52+
express, and name the ones it cannot under `x-dropped-refinements`:
53+
54+
node_modules/@objectstack/spec/json-schema/<category>/<Type>.json
55+
56+
`objectstack.config.ts` needs neither: `defineStack` types it in your
57+
editor. Delete the `os generate schema` call, and any editor setting
58+
that maps `objectstack.schema.json` — nothing writes that file now.
59+
60+
Docs: https://objectstack.ai/docs/deployment/cli
61+
```
62+
63+
**What to do.** The refusal's pointer is the whole of it. Delete the
64+
`os generate schema` call, and any editor setting that maps `objectstack.schema.json`
65+
(a `json.schemas` or `yaml.schemas` entry, for example). Run `os validate` to check a
66+
project against the rules that actually run. For JSON metadata, point the editor at
67+
`node_modules/@objectstack/spec/json-schema/`, one file per metadata type. There is no
68+
call to rename: nothing replaces the command.
69+
70+
**Reach outside this repository is NOT MEASURED.** There is no telemetry, so whether
71+
any project runs the command or reads its file is unknown. Inside this repository
72+
nothing does: no reader and no editor mapping of `objectstack.schema.json` exists.
73+
If these release notes also record that `os generate schema` can now write its file,
74+
this retirement supersedes that repair.
75+
76+
**Two neighbouring answers change with it.** The retirement ledger is now read before
77+
the command routes its sub-commands and before it asks for a `<name>`, which is what
78+
lets `os generate schema` (no name) reach the refusal at all. So `os g agent` with no
79+
name now prints the agent retirement instead of `Missing required argument: <name>`.
80+
The ledger lookup also reads its own keys only: `os g constructor <name>` used to be
81+
taken for a retired type and crashed with a `TypeError`, and it now falls through to
82+
the ordinary type checks.
83+
84+
The docs row that advertised the command — "Autocomplete and validation for
85+
`objectstack.config.ts` (via `os generate schema`)" in
86+
`content/docs/api/data-flow.mdx` — is gone, with the diagram's JSON Schema node above
87+
it.
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.

‎.changeset/19974-having-comparand-shape-face.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ The gate is ONE call in `engine.aggregate`, ahead of both `having` evaluations,
2727

2828
Who is affected: `having` is a request-only key (`QuerySchema.having`, `EngineAggregateOptions.having`), and no metadata type stores it. Every `having` in this repository's docs and published skills is a scalar comparison (`{ order_count: { $gt: 5 } }` and the like), and none authors a refused shape. Callers of `engine.aggregate` and of the REST aggregate query in a deployment were NOT measured.
2929

30-
Not changed: scalars, `null` in the equality slot (the has-no-value predicate), `$in` / `$nin` lists including the empty list, a two-bound `$between`, and scalar ordering bounds all answer exactly as before, on both paths. `$ne` with a list is not judged by the face yet, so `having` still answers it. Neither the comparand-TYPE door nor the unknown-field and declared-type gates that `where` also passes are run on `having`; this change adds the comparand-shape face only.
30+
Not changed: scalars, `null` in the equality slot (the has-no-value predicate), `$in` / `$nin` lists including the empty list, a two-bound `$between`, and scalar ordering bounds all answer exactly as before, on both paths. `$ne` with a list is not judged by the face yet, so `having` still answers it. Neither the comparand-TYPE door nor the unknown-field and declared-type gates that `where` also passes are run on `having` by this change, which adds the comparand-shape face only (the comparand-TYPE door, #20099, and the temporal-comparand door, #20263, reach `having` in the same release).

0 commit comments

Comments
 (0)