Skip to content

Commit daf0aa8

Browse files
committed
Merge origin/main into claude/issue-18386-template-option-default
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
2 parents cc26158 + 33b6e8b commit daf0aa8

44 files changed

Lines changed: 2164 additions & 121 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `picklist` field key's description, two doc comments and the refusal of `picklist` with `options` no longer say that the reference is resolved and its options served (#19518)
6+
7+
Clause-②: no
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Notes in the `field` liveness ledger and sentences in the ledger README that cited a tracker number which no longer resolves now either cite a commit in this repository or say in words what the number stood for
6+
7+
Clause-②: no
8+
9+
Eleven citations in the `field` ledger's notes and twenty-nine in `liveness/README.md` pointed at
10+
a GitHub issue or pull request that no longer exists, so they led nowhere. Each one now either
11+
names the commit that did or recorded what the number pointed at or, where the number alone
12+
carried the meaning, says that meaning in words. The `liveness/` ledgers and their README ship in
13+
this package's tarball, which is why this is a release note at all. Prose only: no row's status,
14+
evidence, proof, producer or date changes, no README table row or heading is added, removed or
15+
renamed, and no schema, export or runtime behaviour changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
The `object-grid` page block now declares `description`, `emptyState` and `keyboardNavigation`, so a page that authors them validates clean instead of having each reported as a prop the block does not declare (#20694).
6+
7+
Clause-②: yes (widening)
8+
9+
- **`description`** — an `I18nLabel` (a string, or an inline locale map): one line of help text the grid draws above its rows, resolved against the display locale.
10+
- **`emptyState`** — `{ title?, message?, icon? }`, what the grid draws instead of an empty table. It is the list view's own empty-state shape, now exported as `EmptyStateSchema` (author type `EmptyState`) and taken by reference on both `list-view` and `object-grid`, so the two cannot drift apart.
11+
- **`keyboardNavigation`** — a boolean, marked `[EXPERIMENTAL — not enforced]`: arrow-key cell navigation on the WAI-ARIA grid pattern, on by default when `editable` is set. No renderer reads it yet, so authoring it changes nothing today.
12+
13+
Nothing that parsed before is refused now. The `object-grid` row still refuses every key it does not declare, and the list view's `emptyState` accepts exactly the values it accepted before the shape was extracted. One refusal message is reworded: an `action` or `button` key inside an empty state is still refused and still points at the list view's `addRecord` block, now phrased so that it also reads true on `object-grid`, which has no add-record block.
14+
15+
On `object-grid`, write `emptyState.title` and `emptyState.message` as plain strings for now: the grid renderer draws both as they are and does not yet resolve an inline locale map there the way it resolves `description`, so a map in either member fails to render.
16+
17+
Where it surfaces: the component-props gate (`os validate`, `os build`, `os lint`) no longer reports these three keys on an `object-grid` node, and the published JSON Schema for `ObjectGridProps` describes them.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/core': patch
3+
---
4+
5+
`isUninterpretableTemporalComparand` reads a bare wall clock on a `time` column by the spec's `ClockTimeValueSchema` (`@objectstack/spec/data`), not by a private copy of it (#20771)
6+
7+
Clause-②: no
8+
9+
The wall-clock half of core's `time` rule (`HH:MM[:SS[.fraction]]`, hours 00 to 23, minutes and seconds 00 to 59, no time zone) was spelled twice: once as the spec's `ClockTimeValueSchema`, the stored form of a `time` value, and once as a private regex in `@objectstack/core`. The two admitted the same strings, but nothing tied them together, so an edit to either one changed one side only. Core now asks the spec schema. Every caller of `isUninterpretableTemporalComparand('time', …)` therefore answers from the rule the spec's `time` default gate uses: the engine's temporal-comparand door, the analytics comparand check, the record validator's `time` arm and the import's `time` coercion.
10+
11+
Unchanged:
12+
13+
- Every string gets the verdict it got before. Measured over 8,655,360 generated strings: 8,640 read by both the old regex and the schema, the rest refused by both, 0 answered differently.
14+
- An instant, a number or a `Date` on a `time` column is judged as before.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/service-analytics": minor
3+
---
4+
5+
fix(service-analytics)!: a cube or dataset dimension on a structured-JSON field is refused with `INVALID_FIELD` / 400 at the analytics door, before any SQL is built
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of a grouping TARGET at the analytics door: a `dimensions` entry, or a bucketed `timeDimensions` entry, whose column is a declared json, composite, repeater, record, location, address or vector field. No authorable key, spelling, export or stored shape moves (the door module is internal; `@objectstack/service-analytics` exports nothing new and nothing less, and `CubeSchema`, `DatasetSchema` and the analytics query body keep parsing every member), and no stored row is read or rewritten. The grouping had no shared meaning to preserve (one group per serialized document on SQLite, a 500 on PostgreSQL), and which scalar part of the document a caller meant to group on is not something a ledger entry can rewrite. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers a grouping target (not `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
10+
11+
**BREAKING**: this narrows what the analytics query doors accept as a dimension. A cube dimension, or a dataset dimension, whose column is a declared field of the structured-JSON class (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) is refused before either strategy builds a statement, when it groups the result: a `dimensions` entry, or a `timeDimensions` entry with a `granularity`. The column is judged where it is declared: on the cube's object, or, for a dotted path such as a dataset dimension over `account.hq`, on the object the cube's declared join for that path names. It holds on `POST /api/v1/analytics/query`, on its dry run `POST /api/v1/analytics/sql`, and on `POST /api/v1/analytics/dataset/query`, on every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12+
13+
**What an author sees now.** `400 INVALID_FIELD`, naming the member as the request wrote it (the cube dimension, or the dataset dimension), the column it groups by, the object and the column's declared type, saying the query was not run, and naming the route: group by a field that stores one scalar value, storing the part of the document you group on in a field of its own. The thrown error carries `member`, `param` (`dimensions` or `timeDimensions`), `cube`, `field` and `object`.
14+
15+
**Why a refusal.** A JSON document is no group key the SQL dialects share. Measured through `POST /api/v1/analytics/query` over three rows with a different document each, on the service `AnalyticsServicePlugin` composes over a real engine: SQLite answered 200 with one group per serialized document, and PostgreSQL 16 answered 500 `DATABASE_ERROR`. A dataset dimension over a joined object's `json` field answered the same two ways through `POST /api/v1/analytics/dataset/query`. The native-SQL strategy compiled the `GROUP BY` itself, so the engine's own refusal of a structured-JSON `groupBy` never saw the query; the engine-aggregate strategy did reach that refusal, but named the engine's `groupBy[0]` position rather than the member the caller wrote. The class is `@objectstack/spec/data`'s `STRUCTURED_JSON_TYPES`, the one the engine's refusal reads. No producer groups by such a field: no cube or dataset dimension in the example apps names one.
16+
17+
**Who is affected.** A dashboard, report or caller that grouped an analytics query by a structured-JSON field on SQLite and read one group per serialized document as real groups. On PostgreSQL the same query was already a 500.
18+
19+
**Unchanged.** A dimension on any other type; a `timeDimensions` entry with no `granularity`, which bounds a range and groups nothing; measures (this door judges only the members that group); a dotted dimension path the cube declares no join for, whose object is not a declaration; a member naming a column the object does not have, which keeps its existing `INVALID_FIELD` answer first; and a host that wires no `sourceFieldMeta`, where the column's type cannot be read.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the `ui-object-grid-page-size-positive-integer-refused` migration entry states what the platform does with a stored `object-grid` page size of `0` — the page saves and loads, and the finding is advisory on the CLI
6+
7+
Clause-②: no
8+
9+
**`ui-object-grid-page-size-positive-integer-refused` (protocol 18).** The entry's acceptance
10+
criterion said that a stored page whose `object-grid` node carries `pageSize: 0` "is refused on
11+
its next authoring-path save with a per-key issue at `pagination.pageSize`". Nothing on the
12+
metadata save path judges a page component's `properties`. `PageComponentSchema.properties` is an
13+
open record, and the one judge of the `ComponentPropsMap` row is the component-props gate, an
14+
advisory rule that runs on the CLI only. Measured through `saveMetaItem`, the call behind
15+
`PUT /api/v1/meta/page`: such a page saves (`success: true`, and the result carries no `advisories` member), is stored as an
16+
active row and reads back with `pageSize: 0` intact. On the same page, `os validate`, `os build`
17+
and `os lint` each report one advisory `component-props-invalid` finding at
18+
`properties.pagination.pageSize` and no error. The criterion now says exactly that. It also names
19+
the `pageSizeOptions` entry and the flat `pageSize` shorthand, which are reported the same way at
20+
their own paths.
21+
22+
Text only: no entry id, conversion or matching logic changes, and `os migrate meta` rewrites
23+
exactly what it rewrote before. The generated migration registry carries the corrected text. The
24+
sibling entry `object-grid-default-filters-rule-array` already stated the advisory-only outcome
25+
and is unchanged.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@objectstack/plugin-auth': minor
3+
---
4+
5+
feat(plugin-auth): a host declares its own sign-in handoff route with `hostSignInHandoff`, and the `no_sign_in_account_at_boot` boot report stops calling that deployment a dead end (#20861)
6+
7+
Clause-②: yes (widening)
8+
9+
`AuthPluginOptions` gains one option, `hostSignInHandoff?: boolean` (default
10+
`false`). The HOST that constructs the plugin sets it when it signs people in
11+
through a handoff route of its own: a route that is not a login-page provider
12+
and that creates the session without writing a `sys_account` row. A hosted
13+
kernel whose owner signs in through the control plane is the case it is for.
14+
That owner can still sign in when the login page shows no platform sign-in
15+
button:
16+
17+
```ts
18+
new AuthPlugin({ /* … */ hostSignInHandoff: true });
19+
```
20+
21+
With it declared, human `sys_user` rows and zero `sys_account` rows are that
22+
deployment's normal state. The boot report then logs the shape at `debug` and
23+
names `hostSignInHandoff` as the reason. It no longer logs an `error` saying
24+
nobody can sign in. The option's only reader is that boot report.
25+
26+
- It is a declaration, not a detection. The option is the only way to set it:
27+
there is no environment variable or setting. Nothing infers it from an
28+
environment's name, from a control plane's platform-SSO flag, or from missing
29+
rows.
30+
- The login page is not changed. `getPublicConfig()` returns the same value with
31+
or without the option, and no provider is registered.
32+
- Set it only where the host really serves such a route. On a deployment with
33+
no such route, the option turns the error for a deployment nobody can sign in
34+
to into a quiet `debug` line.
35+
- A deployment that does not declare it gets the same report as before. That
36+
includes every self-hosted deployment with no delegated sign-in path.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/service-automation': minor
3+
---
4+
5+
fix(automation): boot-time flow precedence takes which same-named flow is the packaged one from the package loader's set, not from the flow definitions' own provenance (#20864)
6+
7+
Clause-②: yes (widening)
8+
9+
When several flow definitions share one name at startup, the automation plugin arms one of them and shadows the rest: a flow authored in the deployment wins over the packaged flow of that name (ADR-0005). Which contender counts as the packaged one is now the answer of the set of flows a managed package's loader registered. That is the same answer the ADR-0126 §7.3 subflow guards, the arming gate and the activation switch read since #20761. The package provenance a flow definition carries is kept for display only.
10+
11+
- `resolveFlowPrecedence(items, logger?, packagedFlowOwner?)` and `describeFlowContender(item, packagedFlowOwner?)` take the reader as a new optional last argument, typed `PackagedFlowSource` (the reader `AutomationEngine.setPackagedFlowSource` takes). `AutomationServicePlugin` passes the engine's own `packagedFlowOwner` for you.
12+
- A definition that claims a package's provenance for a name no package loaded ranks as a flow of the deployment. The shadowing record (`getShadowedFlows()`, and the startup warnings) no longer names that package as its source.
13+
- With no reader, no contender is packaged. That is the engine's own answer when no reader is attached.
14+
- Two contenders that both rank as the deployment's keep the order they were listed in. The package id orders packaged contenders only, as before.
15+
- A startup whose registry the package loader and the stored-flow hydration filled arms the same flows as before: those entries already agree with the loader's set.
16+
17+
**If you call `resolveFlowPrecedence` or `describeFlowContender` yourself:** pass the loader's-set reader as the last argument, for example `(name) => engine.packagedFlowOwner(name)`. Without it no contender ranks as packaged.

‎content/docs/deployment/self-hosting.mdx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,17 @@ first thing to look for — and note that it asks only whether *an* account row
540540
exists, so a row you wrote yourself silences it whether or not anyone can
541541
actually authenticate with it.
542542

543+
The same shape is logged at **debug** instead, under the same token and naming
544+
the reason, when the deployment has a sign-in path that needs no `sys_account`
545+
row of its own: SSO-only mode (`OS_AUTH_SSO_ONLY` or `ssoOnlyMode`), a
546+
configured social or OIDC provider, enterprise SSO with at least one registered
547+
identity provider, or a host that constructs `AuthPlugin` with
548+
`hostSignInHandoff: true` because it signs people in through a handoff route of
549+
its own. A deployment with none of these still reports at **error**.
550+
`hostSignInHandoff` is a statement by the code that serves such a route, not a
551+
way to quiet the report: set on a deployment without one, it hides exactly the
552+
dead end this section describes.
553+
543554
**The recovery is out of band, and it is one row.** Write a pending invitation
544555
directly against the store the deployment reads, then have that person register
545556
through the ordinary sign-up endpoint — the invitation carve-out admits that

‎content/docs/references/data/field.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ const result = CurrencyConfigSchema.parse(data);
7575
| **accept** | `string[]` | optional | Permitted upload types for media fields, as MIME types or extensions (e.g. ["image/*", ".pdf"]). Offered to the file picker AND enforced on write. |
7676
| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
7777
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for select/multiselect |
78-
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). The server resolves the reference: the field clients read carries the resolved `options`. |
78+
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). |
7979
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target. |
8080
| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
8181
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |

0 commit comments

Comments
 (0)