Skip to content

Commit 0c4dad2

Browse files
committed
Merge origin/main into claude/issue-15429-decision-first-match
Landing lap: origin/main dcd3bce, 6 commits past 15bf186. #20398 (`dcd3bcea`) appended `action-aria-removed` to the same three step-18 tails this branch appends `flow-decision-mode-inclusive-explicit` to. Resolved per the seat's answer B on #15429 (5865957805), and nowhere else: - `CONVERSIONS_BY_MAJOR[18]` and step18 `conversionIds`: both kept, main's `actionAriaRemoved` / `action-aria-removed` first (landing order), this branch's entry after it; - step18 `rationale`: main's sentence kept, this branch's sentence appended verbatim (the one string join: main's closing literal now ends in a space and the concatenation continues). No other hand edit; generated regions are regenerated in the next commit. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
2 parents 18cbf4e + dcd3bce commit 0c4dad2

80 files changed

Lines changed: 2593 additions & 378 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/19920-exported-types-not-unknown.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ Four published type aliases were derived from a schema whose own static type era
1818

1919
The types are the members' declared shapes, not the schemas' verdicts. Each schema still accepts some bodies its type refuses (the preprocess folds and strips) and still refuses some bodies its type admits (refinements are not types), so the schema remains the only judge.
2020

21-
`JoinedReportBlock` is not changed by this change, and still resolves to `unknown`.
21+
`JoinedReportBlock` is not changed by this change. It stops resolving to `unknown` in its own entry (#19920).
2222

2323
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes moves — no spec key, no export and no stored row changes and every runtime accept set is unchanged, so `objectstack migrate meta` has nothing to reach — and only TypeScript annotations narrow, whose channel is the consumer's compiler. -->
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec): `JoinedReportBlock`, a ViewItem's `config`, a flattened overlay's `viewKind` and a flattened list overlay's `type` / `columns` carry the shapes their doors accept (#19920)
6+
7+
Clause-②: yes (narrowing)
8+
9+
**BREAKING for TypeScript code that annotates with `JoinedReportBlock`, `Report`, `ReportParsed`, `ViewItem`, `ViewItemWire`, `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` or `AssembledViewArtifactParsed`, or that passes an unchecked value to `defineReport` / `defineViewItem`**: a narrowing of published TYPES, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). The runtime accept set does not move at all: no schema's parse, no value and no existing export changes. Three parsed-state type names are added (below); nothing is removed or renamed.
10+
11+
Four places in the published types were wider than the doors that judge the same bodies, so values those doors refuse type-checked:
12+
13+
- `JoinedReportBlock`: FROM `unknown` TO the input shape of `JoinedReportBlockSchema`. The schema was annotated `z.ZodTypeAny`, which erased its shape; it now carries its inferred type. The same erasure made every `blocks[]` element of `Report` / `ReportParsed` (and so of `defineReport`'s parameter) `unknown`; each is now a block.
14+
- A ViewItem's `config`: FROM `unknown` TO the arm's own config type, a `ListView` config on the `list` arm and a `FormView` config on the `form` arm. This holds on `ViewItem`, `ViewItemWire`, `defineViewItem`'s parameter and return, and the `viewItem` member of `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed`. The arm builder took `config` as `z.ZodTypeAny`; it is now a generic parameter.
15+
- A flattened overlay member's `viewKind`: FROM `'list' | 'form'` on both members TO `'list'` on the list overlay and `'form'` on the form overlay, the one value each member accepts. A list-shaped body naming `viewKind: 'form'` used to type-check, through the list overlay member, as `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed`.
16+
- A flattened list overlay's `type` and `columns`: FROM `unknown` TO the list view's own types, both optional: `type` one of the list view types, `columns` a field list. This holds on the list overlay member of `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed`. The member read both keys off the list view shape through a cast that erased them, so `{ object, viewKind: 'list', columns: 42 }` type-checked as all four while that member refuses it.
17+
18+
**If your code stops compiling.** A value you annotated with one of these names, or passed to `defineReport` / `defineViewItem`, is not the shape the door accepts: correct it, or type a value that is still unvalidated as `unknown` and let the schema's `safeParse` decide. A ViewItem's `config` must match its `viewKind`: a `ListView` config under `viewKind: 'list'`, a `FormView` config under `viewKind: 'form'`. A flattened list overlay's `columns` is a field list and its `type` one of the list view types.
19+
20+
The declared types of `JoinedReportBlockSchema`, `ViewItemSchema` and `ViewItemWireSchema` narrow with them, so `z.input` / `z.infer` of each is typed where it was `unknown` (or carried an `unknown` `config`). Typed, each schema's input and output now differ by its defaults, so three ADR-0122 parsed-state aliases are added beside the bare names: `JoinedReportBlockParsed`, `ViewItemParsed` and `ViewItemWireParsed`. Nothing is removed or renamed.
21+
22+
One default is applied by the parse and is absent from `ViewMetadataParsed` / `AssembledViewArtifactParsed`, and their TSDoc now says so: the flattened list overlay member re-applies `type: 'grid'` in an `.overwrite()`, so every body it parses carries `type`, while its output type leaves `type` optional.
23+
24+
The types are the members' declared shapes, not the schemas' verdicts: refinements are not types, so each schema remains the only judge.
25+
26+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes moves — no spec key, no existing export and no stored row changes (three parsed-state type names are added, none removed or renamed) and every runtime accept set is unchanged, so `objectstack migrate meta` has nothing to reach — and only TypeScript annotations narrow, whose channel is the consumer's compiler. -->
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 `driver-*`, `kernel-*` and `system-*` 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 `driver-*` family (the driver query-argument
10+
narrowings, the inert capability bits, the SQL driver's unresolvable-column and
11+
cross-row upsert refusals, and the retired Turso config keys), the `kernel-*` family
12+
(preview mode, and the kernel duration keys that now carry their unit in the key name)
13+
and the `system-*` family (the system duration keys renamed under the same rule) are
14+
printed by `os migrate meta` as the header, `why:` and `verify:` lines of a manual
15+
change. Their text sent the reader to issue-tracker and decision-batch numbers — some
16+
of which no longer resolve — for what a ruling, measurement or fix had decided; it now
17+
says what was decided, in the sentence being read. ADR ids are kept.
18+
19+
Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic changes,
20+
and the 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/20263-having-temporal-comparand-door.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ What is judged:
2828

2929
- 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.
3030
- 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.
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` resolves placeholders from the same release (#20334), after this door, so the refusal's remedy on a `date` or `datetime` column is the `where` refusal's and names them, e.g. `{30_days_ago}` / `{current_month_start}`.
3232
- 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`.
3333
- 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.
3434

3535
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.
3636

3737
**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.
3838

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.
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, either one with a relative-date placeholder the resolver knows (`{30_days_ago}`, `{current_month_start}`; `having` resolves them from the same release, #20334), and a `time` column with an `HH:MM` or `HH:MM:SS` wall clock.
4040

4141
**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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING** — `aria` on an action (`ActionSchema`, top-level `actions[]` and `objects[].actions[]`) is now refused at parse: no action surface ever applied it. Write the accessible name in the action's rendered `label`, and name the region that places the actions with `ariaLabel` / `ariaDescribedBy` / `role` in the placing node's `aria` block (`page.components[].aria` or the list view `aria`).
6+
7+
Clause-②: yes
8+
9+
`ActionSchema` declared a per-action ARIA block, and the liveness ledger graded it `live` on an uncited note — 「PARTIAL — honored by a few objectui renderers, not the core action buttons/menus」 — with no reader behind it. Re-measured at this checkout's own `.objectui-sha` pin `f8a9d0fb05`: none of the surfaces that render an action reads an action's `aria` — not `action:button`, `action:icon`, `action:menu`, `action:group` or `action:bar`, not the grid's row and bulk action menus, not `record:quick_actions`, not the declared-actions bar. The only `schema.aria` readers there are the placing nodes' own blocks (the `record:*` page components, the list view, `element:button`'s props), none of which looks inside an action. So an author — or an AI — who filled in `aria` got no accessible name on the rendered button, and nothing said so.
10+
11+
It is the fourth member of the `aria` family retired for exactly this, after `dashboard.aria`, `dashboard.widgets[].aria` and the chart config's `aria`.
12+
13+
**Removed rather than enforced** (ADR-0049 enforce-or-remove; the triage direction on the card, following the chart config retirement `2bf6ef18d`). The capability is already delivered under another key. Every one of those surfaces derives the accessible name from the action's **required** `label` — the visible button or menu-item text, and the `aria-label` of the icon-only `action:icon` and of the overflow-menu trigger — and the node that places the actions carries the node-level `ariaLabel` / `ariaDescribedBy` / `role`. The reversal condition the triage named (an icon-only action rendered with no accessible name at all) was measured and does not hold on any of them. A per-action block would be a second spelling of both, behind a precedence rule nobody has written.
14+
15+
## FROM → TO
16+
17+
| you wrote (17.4 and earlier) | write instead |
18+
| --- | --- |
19+
| `aria: { ariaLabel: 'Escalate this case' }` on an action, top-level or under `objects[].actions[]` | the name in the action's `label` — it is what every action renderer announces |
20+
| `aria: { ariaDescribedBy: … }` / `aria: { role: … }` on an action | delete it; to describe or role the toolbar or list the actions sit in, put it in the `aria` block of the node that places them — `page.components[].aria` or the list view `aria` |
21+
| `ariaLabel` / `ariaDescribedBy` / `role` on a page, page component or list view | unchanged — the shared `AriaProps` block stays live there |
22+
23+
**The one-line fix:** delete `aria` from the action; put the accessible name in its `label`.
24+
25+
`os migrate meta --from 17` lists the mechanical edits for existing sources; apply them by hand.
26+
27+
## The retirement kit
28+
29+
- **A `retiredKey()` tombstone, not a bare deletion** — even though `ActionSchema` is a `strictObject`. A bare delete would still be loud, but only as a generic unrecognized-key report that cannot carry the prescription; the tombstone types the key `never` for `tsc` and raises the upgrade text at parse. The key therefore stays in the walked shape: its liveness row stays (regraded `live` → `dead` with a `REMOVED` note that records the uncited 「PARTIAL」 claim it replaces) and the authorable-surface baseline marks `ui/Action:aria` `[RETIRED]`.
30+
- **The D2 conversion `action-aria-removed`** (protocol 18, retired from the load path) strips the key from stack `actions[]` and from `objects[].actions[]` as a pure lossless delete — it never had an effect to lose. Its D3 record is the semantic entry `action-aria-retired`: its own family, not a member of the chart config's.
31+
- **`AriaPropsSchema` is untouched** — a key retirement, not a def retirement; it stays live on pages, page components, the list view and the element props.
32+
- **No form input and no locale bundle move.** The key never reached `action.form.ts`. The Studio action inspector's "More fields" section is derived from the served schema, where a tombstone node is dropped from the payload, so the served `aria` column goes with this release.
33+
34+
## Reach, measured
35+
36+
- This repository: **0** authors of `aria` on an action in `examples/**`, `packages/**` fixtures or the published skills (control: 15 `variant:` lines in `examples/**`). Two hand-written docs pages taught the key and are corrected here.
37+
- HotCRM at `origin/main` `2f7b2326`: **0** on an action; its 6 `aria:` blocks are all page-level `page.aria`, which stays live (control: HotCRM authors actions — 7 files under `src/**/actions/` declare `locations:`, 17 times).
38+
- Other out-of-repo authors: NOT MEASURED.
39+
40+
## What an operator with a STORED action sees
41+
42+
A `sys_metadata` `action` or `object` row written before this release can carry the key. Nothing breaks at read: the conversion replays on rehydration and strips it, so the row is served canonical and parses. `os migrate meta --stored --apply` rewrites the rows.
43+
44+
<!-- adr-0087: registered action-aria-removed, action-aria-retired -->

0 commit comments

Comments
 (0)