Skip to content

Commit 65fdb2c

Browse files
committed
Merge origin/main into claude/issue-15429-decision-first-match
Both registries gained an entry on each side on the same lines (`view-overlay-owner-hidden-removed` from #20286, this branch's `flow-decision-mode-inclusive-explicit`); both kept, landing order. Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ Co-authored-by: Claude <noreply@anthropic.com>
2 parents d613ec2 + a78f731 commit 65fdb2c

99 files changed

Lines changed: 2544 additions & 533 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.

‎.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).
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.

‎.changeset/20240-date-year-four-digits.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ What changes:
3535

3636
**Fix.** Compare against a `YYYY-MM-DD` day, or a number or `Date` whose UTC calendar day falls in a four-digit year.
3737

38-
**Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL through the engine and REST: every `datetime` and `time` cell, the same numbers included; every string comparand on a `date` field; every number and `Date` in the years 1000 to 9999; `NaN`, ±Infinity and an Invalid Date, which name no year and are not judged; and every read-path presentation on those three. On MySQL, measured at the driver door, a stored year from 100 to 999 now reads back padded (`0999-06-15`, where it read `999-06-15`); a stored year below 100 still reads back a century late (`0009-03-04` as `1909-03-04`, mysql2's `Date.UTC` reading of a `DATE`), which this change does not touch. `having` does not reach the temporal-comparand door for any comparand, so a number or `Date` outside 0..9999 there is still compared as written. `service-analytics`' raw-SQL decline reads a time dimension by the `datetime` rule, so its answer does not move. `driver-mongodb` keeps its own copy of the `date` rule and is not changed here.
38+
**Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL through the engine and REST: every `datetime` and `time` cell, the same numbers included; every string comparand on a `date` field; every number and `Date` in the years 1000 to 9999; `NaN`, ±Infinity and an Invalid Date, which name no year and are not judged; and every read-path presentation on those three. On MySQL, measured at the driver door, a stored year from 100 to 999 now reads back padded (`0999-06-15`, where it read `999-06-15`); a stored year below 100 still reads back a century late (`0009-03-04` as `1909-03-04`, mysql2's `Date.UTC` reading of a `DATE`), which this change does not touch. `having` reaches the same door in the same release (#20263), so a number or `Date` outside 0..9999 is refused there too. `service-analytics`' raw-SQL decline reads a time dimension by the `datetime` rule, so its answer does not move. `driver-mongodb` keeps its own copy of the `date` rule and is not changed here.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"@objectstack/objectql": minor
3+
---
4+
5+
fix(objectql)!: `having` on `engine.aggregate` takes the temporal-comparand door `where` and the per-aggregation `filter` take, so a comparand its aggregated column cannot read is refused `INVALID_FILTER` / 400 instead of keeping no group or every group (#20263)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) this change adds no transition to migrate. Each refused comparand is one the column's own storage rule cannot read: `having` compared it as written and kept no group or every group, while the same bound in a `where` was already refused by the same door. There is no accepted spelling a refused comparand can be mechanically rewritten to: which day, instant or wall clock the author meant is an authoring decision, and the refusal names what the column takes. `having` is a request-only key that no metadata type stores, so there is no stored document for `objectstack migrate meta` to rewrite. The table below records the answer each comparand had and has; it prescribes no rewrite. -->
10+
11+
**BREAKING**: this narrows what `having` accepts on `engine.aggregate`, and on the REST aggregate query (`POST /data/:object/query`) that forwards it there. A comparand the aggregated column's storage rule cannot read used to answer 200; it is now refused with `INVALID_FILTER` / 400, once per query, before any driver is asked for a row, on both the native `driver.aggregate()` path and the in-memory fallback, on an empty and on a populated object. It ships as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
Measured through `engine.aggregate` and `POST /data/:object/query`, on `driver-memory`, `driver-sql` on SQLite and `driver-sql` on PostgreSQL 16, on both paths, with `groupBy` customer over four groups. The three drivers gave the same answer in every cell:
14+
15+
| `having` | before | now | its `where` twin |
16+
|:--|:--|:--|:--|
17+
| `{ last_placed: { $lt: 'not-a-date' } }`, `last_placed` = `max(placed_on)` of a `date` field | 200, every group | 400 | 400 |
18+
| the same under `$gt` | 200, no group | 400 | 400 |
19+
| `'+010000-01-01T00:00:00.000Z'` on the same column, `$gt` | 200, every group | 400 | 400 |
20+
| the number for 10000-01-01 on the same column, `$gt` (and its `Date`, in-process) | 200, every group | 400 | 400 |
21+
| `'not-a-date'` on `min` of a `datetime` field or `max` of a `time` field, `$lt` | 200, every group | 400 | 400 |
22+
| `'not-a-date'` on a groupBy key that is a `date` or `datetime` field or on a `day` bucket, `'noon'` on one that is a `time` field, `$gt` | 200, no group | 400 | 400 |
23+
| the number for 10000-01-01 on a `day` bucket, `$gt` | 200, every group | 400 | 400 |
24+
25+
Each one read the object once before; each is now refused with no read.
26+
27+
What is judged:
28+
29+
- The same walk and the same predicate, `isUninterpretableTemporalComparand` in `@objectstack/core`, that the door runs on `where` and on each per-aggregation `filter`. A change to that rule reaches `having` with it.
30+
- The kind is the aggregated column's class, the one the `addDays` rule already reads: `min` / `max` of a `date`, `datetime` or `time` field keeps that kind, a groupBy projection of such a field takes its kind, and a `day` bucket is a `date`. `count`, `count_distinct`, `sum` and `avg`, a `week` / `month` / `quarter` / `year` bucket, and every other column are not temporal, so they are not judged.
31+
- Every comparison and set operator's comparand, each `$in` / `$nin` member and `$between` endpoint, and the implicit-equality slot, under `$and`, `$or` and `$not`. As on `where`, a `{placeholder}` string, the empty string, `null` and a `{ $field }` reference are not judged. `having` does not resolve placeholders, and did not before, so the refusal's remedy names none.
32+
- The text operators (`$contains`, `$notContains`, `$startsWith`, `$endsWith`, `$icontains`) are not judged. On `where` the text-operator declared-type door answers them first, and that door does not front `having`.
33+
- The door runs after every other `having` door, so a clause one of them refuses (an unknown operator, a key naming no column, a comparand of no comparable type, an `addDays` pair, an array in the equality slot) keeps that refusal and its words.
34+
35+
The refusal follows the `where` door's words. It names the `having` path, the column, what the column aggregates and its kind, and the comparand, for example: `` `having` on 'last_placed' (max(placed_on), a date column) compares against "not-a-date" at having.last_placed.$lt ``. Like the `where` refusal, it names the column's kind, and otherwise only what the query carries.
36+
37+
**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 compares a numeric aggregation alias, which is not judged. Callers of `engine.aggregate` and of the REST aggregate query in a deployment were NOT measured.
38+
39+
**Fix.** Compare a `date` column with a `YYYY-MM-DD` day, a `datetime` column with an ISO-8601 instant, a bare day or epoch milliseconds, and a `time` column with an `HH:MM` or `HH:MM:SS` wall clock.
40+
41+
**Unchanged**, measured identical before and after on the three drivers, both paths and both doors: every `where` and per-aggregation `filter` answer; every `having` on a temporal column whose comparand the rule reads (a `YYYY-MM-DD` day, an ISO instant, an epoch-millisecond number or string, an in-range `Date`, a zone-naive instant, a wall clock, an extended-year instant on a `datetime` column, which that rule reads); `{today}`-style placeholders, known or not; the empty and the whitespace-only string; `null`, `$exists`, `$in` / `$nin`, `$between`, `$not` / `$or` / `$and` and `{ $field }` references; `$contains` and `$startsWith`; every `count` / `sum` / `avg` column, a string comparand included; a `month` bucket; and every existing `having` refusal, in its words.
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+
Provenance comments in `kernel/` and `contracts/` were re-anchored
6+
7+
Comment and docblock lines under `src/kernel` and `src/contracts` cited tracker
8+
numbers that no longer resolve on GitHub. Each one now cites the commit in this
9+
repository's history that decided the matter, or the ADR that records it, and
10+
says in its own words what was decided. Where nothing could be anchored, the
11+
sentence keeps its reason and the number is gone. Comments only: no type, schema,
12+
export or runtime behaviour changes.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: retire the flattened view overlay's `owner` and `hidden` keys — accepted at the save door, stored, and read by nothing (#20230)
6+
7+
**BREAKING** — `owner` and `hidden` are removed from the flattened view overlay:
8+
the lean `view` body with no `config` that `PUT /api/v1/meta/view/:name` (the
9+
Studio and MCP save) accepts, members 3 and 4 of the `view` metadata door, and
10+
the same members in the assembled-manifest `viewItems:` channel. ADR-0049
11+
enforce-or-remove; triage direction, verbatim: 「follow #20085's disposition for
12+
the same key pair」. This completes the family: the view item record's `owner` /
13+
`hidden` are retired in this same release by its own entry, with the same texts.
14+
15+
⚠️ **This supersedes one sentence of the view item retirement's note in this same
16+
release.** That note says the flattened overlay's own `owner` / `hidden` are
17+
untouched and that a `{ object, viewKind, hidden: true }` overlay still parses.
18+
True of that change alone; after this one, such an overlay is refused too. Read the
19+
two notes together: after this release, neither door accepts either key.
20+
21+
Clause-②: no (narrowing)
22+
23+
The overlay door declared both keys separately from the view item's pair. A bound
24+
overlay such as `{ object, viewKind, hidden: true }` saved clean and one row was
25+
stored with the key, and nothing ever read it. Both view-switcher read paths
26+
(`GET /meta/view?object=` and `getViewsByObject`) filter on `viewKind` + `object`
27+
and sort on `order`, so `hidden: true` hid nothing, and a view with `owner` set
28+
was listed for every user who can read the object.
29+
30+
Writer census, taken before removal: no writer of either overlay key in this
31+
framework or its examples, in objectui at its pinned commit and at `main` (the
32+
toolbar writes only `rowHeight`, `sort`, `hiddenFields`, `columnState` and
33+
`inlineEdit`; the switcher only `label`, `isPinned`, `isDefault` and `sortOrder`),
34+
or in the HotCRM app. The cloud repository was not reachable from the census.
35+
36+
### FROM → TO
37+
38+
| removed | what to write instead |
39+
| --- | --- |
40+
| flattened overlay `owner` | delete the key. Nothing restricts a view to one user today; a view is visible to everyone who can read its object. |
41+
| flattened overlay `hidden` | delete the key. To take a view out of the switcher, delete the view item (or stop shipping it from source). |
42+
43+
**The one-line fix: delete `owner:` and `hidden:` from every view body you save.**
44+
`os migrate meta --from 17` lists the mechanical edits for existing sources.
45+
46+
⚠️ Runtime behaviour is deliberately **unchanged**. Neither key ever changed what
47+
a view showed or to whom. What changes is the answer an author gets: a save that
48+
carries either key is refused `422 INVALID_METADATA`, with the prescription
49+
located at the key, instead of being stored with no effect. The prescriptions are
50+
the view item's own texts, so the family answers with one voice on both doors.
51+
52+
### Stored rows
53+
54+
Every read of a stored `view` row replays the conversion chain before the row is
55+
served or badged, and the D2 conversion strips both keys there. What that leaves
56+
depends on what else the row holds:
57+
58+
- **A row with any other view key** (a column state, a sort, a default flag, an
59+
order): served and badged valid without the keys. A GET then a PUT of the whole
60+
row saves (if it was otherwise valid), so the console's next read-merge-write of
61+
it saves, and `os migrate meta --stored --apply` rewrites it.
62+
- **A hide-only row**, holding nothing but its identity (`name`, `object`,
63+
`viewKind`, `label`) and `owner` / `hidden`, such as
64+
`{ object, viewKind, hidden: true }`: the strip leaves identity only, which the
65+
`view` door refuses ("only identity fields"). The row is served badged invalid
66+
(it was badged valid before this release). A whole-row re-save, or one that adds
67+
only identity (a rename sets `label`), answers `422 INVALID_METADATA`.
68+
`--apply` reports it `failed` and leaves it as stored; every read strips it
69+
again. A write that adds a real view key, such as a toolbar toggle, saves.
70+
**Fix: delete the row** (it never changed what anyone saw), or add the
71+
personalization setting its author meant and save that.
72+
73+
### The retirement kit
74+
75+
- **Tombstones on both overlay members.** `retiredKey()` in
76+
`flattenedViewOverlayFields()`, with the view item's prescription texts. Both
77+
members `.strip()`, so a bare deletion would have dropped the key in silence
78+
(ADR-0104).
79+
- **D2 conversion `view-overlay-owner-hidden-removed`** (step 18, retired from the
80+
load path). A lossless delete from the flattened spelling (no `config`, no
81+
container slot) in `views` (stack sources and stored rows) and `viewItems`
82+
(assembled artifacts). It is disjoint from `view-item-owner-hidden-removed` by
83+
`config`, so no row is judged by both.
84+
- **D3 semantic entry `view-overlay-owner-hidden-retired`**: the family's one D3
85+
record, naming its D2 conversion. The view item record's pair is a separate
86+
family with its own conversion and its own D3 entry; the two share the
87+
prescription texts.
88+
- **`RETIRED_KEYS_BY_MAJOR[18]`**: `ui/ViewMetadata:owner`, `ui/ViewMetadata:hidden`.
89+
`ui/ViewMetadata` is unemitted (its `z.undefined()` guards have no JSON Schema
90+
form), so no build gate judges these rows and the four surface ratchets are
91+
byte-identical on this retirement. The rows are pinned by the retirement test.
92+
- **No liveness row**: the `view` ledger walks the container keys only.
93+
- **No deprecation window**, per the project's startup-stage posture.
94+
95+
⚠️ **The out-of-repo population is NOT MEASURED.** `@objectstack/spec` is published,
96+
and production `sys_metadata` rows are not reachable from the repository. Stored
97+
rows are stripped on read by the conversion above, and a hide-only row among them
98+
needs the fix above. A client that still sends either key is refused at its next
99+
save.
100+
101+
<!-- adr-0087: registered view-overlay-owner-hidden-removed, view-overlay-owner-hidden-retired -->

0 commit comments

Comments
 (0)