Skip to content

Commit a096821

Browse files
committed
Merge origin/main (c6954d6) into claude/issue-21114-start-signal-forwarding
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
2 parents 22416c7 + c6954d6 commit a096821

127 files changed

Lines changed: 5137 additions & 1195 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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
Authoring a key whose liveness-ledger verdict is `dead` now draws a `liveness-dead-property` warning, and a `live-elsewhere` key a `liveness-live-elsewhere-property` warning, with no per-row `authorWarn` opt-in: the verdict itself is the warning. Before this, both rule ids were exported and never produced, because no shipped `dead` or `live-elsewhere` row opted in.
6+
7+
Clause-②: no
8+
9+
**Which keys warn.** A ledger row warns when its status is `dead`, `live-elsewhere` or `experimental`, or when it sets `authorWarn: true` (still the only way a `planned` row warns). Among authorable keys in the metadata types the rule walks, four are `dead` today: a view container's own `name` and `label` (the `defineView` container, not `list.label`), and a permission set's `rowLevelSecurity[].label` and `rowLevelSecurity[].description`. No walk visits `manifest`, `connectors` or realtime subscriptions, so their rows still warn nobody. That includes `manifest.runtime`, the one `live-elsewhere` row.
10+
11+
**The hint.** A row that warns only because of its `dead` or `live-elsewhere` verdict shows its `authorHint`, else the verdict's default hint: "Remove it — it is declared in the spec but not consumed at runtime." for `dead`. It never shows the ledger's internal `note`. Rows that opt in with `authorWarn`, and `experimental` rows, show exactly the hint they showed before.
12+
13+
**Retired keys.** A `retiredKey` tombstone keeps its `dead` row. Every command that parses (`os validate`, `os build`, the runtime publish gate) still refuses the key first, so it gets no second report. `os lint` does not parse: a config it accepts without `defineStack` that carries a retired key now gets a `liveness-dead-property` warning where it got nothing.
14+
15+
**What changes for a project.** Nothing is refused, and nothing changes without `--strict`. `os lint --strict` and `os validate --strict` now exit 1 instead of 0 on a stack that was otherwise warning-clean and authors a view container `label` or `name`, or a row-level-security policy `label` or `description`. A view container that carries its own `name` and `label` beside its `object` binding is one such shape: it draws two warnings per container, and under `--strict` that flips the exit. Across this repository's example apps, only `app-showcase` gains warnings: two, on one permission set's policy `label` and `description`, and no example's exit code changes. To clear the warning, delete the key: nothing reads it.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
---
4+
5+
A stored page whose `requires` names a plugin the deployment's console does not load is reported when the page loads, and a draft → active promotion re-stamps an html page's `requires` with the save door's own computation (ADR-0080 §5: `requires` is validated at save and load, and derived from the source).
6+
7+
Clause-②: no
8+
9+
**At load.** The boot hydration of stored metadata (`loadMetaFromDb`) prints one `warn` line for each stored page whose `requires` lists a namespace no component in the deployment's SDUI component manifest carries, naming the page and every such namespace under the marker `[page_requires_plugin_absent]`. It is a report, never a refusal: the page has already loaded when the line is printed, and it is served. The manifest is read through the same `SDUI_MANIFEST_SERVICE` key the save door reads; `os serve` registers it before any plugin initialises, so it is there when stored pages load. A host that registers no manifest judges no page at load, exactly as it judges none at save.
10+
11+
**At promotion.** `publishMetaItem` and `publishPackageDrafts` now store the promoted body with the `requires` that an active save of the same body would store, computed against the manifest registered at the moment of the publish. Before, the draft's `requires` was carried into the active row as it was. A draft saved before the host had a manifest reached `active` with no `requires`, and an agreeing list kept the draft's own spelling (its order and any repeats). Nothing is newly refused: wherever the runtime authoring gate runs, it already refused a draft whose source does not compile, or whose `requires` disagrees with its source, and it still does. A host with no manifest promotes the draft as written.
12+
13+
`SysMetadataRepository.promoteDraft` takes an optional `deriveActiveBody(draftBody)` that derives the active row's body from the draft row it promotes. When it is omitted, the draft body is promoted unchanged, as before.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `DashboardWidgetOptionsSchema` doc comment states which widget `options` keys a renderer reads, instead of naming presentation extras (`icon`, `trend`, `columns`, `striped`, `density`) it called renderer-understood
6+
7+
Clause-②: no — no key is declared and no value is typed, so the accept set is unchanged.
8+
9+
`options` still parses any key. A widget always binds a `dataset`, so it renders through
10+
objectui's dataset-bound path, and that path reads only the five declared keys
11+
(`dateGranularity`, `sortBy`, `sortOrder`, `limit`, `stageOrder`) and the `description`
12+
sub-caption. Any other key parses and renders nothing. To format a number, set `format` and
13+
`currency` on the dataset measure. To accent a tile, set the widget's `colorVariant`. To style
14+
a chart, set the widget's `chartConfig`.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/service-analytics": minor
3+
---
4+
5+
fix(service-analytics)!: the cube door asks the aggregate × field-type table for every measure, so a configured or suffix-inferred cube measure whose aggregate the table refuses for its column's declared type answers `INVALID_FIELD` / 400 on every driver and both strategies, and a `min` / `max` over a temporal column is described `time` in `fields[]`
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (already-registered dataset-measure-selecting-aggregate-field-type-refused, dataset-measure-aggregate-field-type-refused) the pairs this change refuses are exactly the pairs AGGREGATE_FIELD_TYPE_COMPATIBILITY already refuses, and the table is not edited: every refused min / max pair is registered under protocol major 18 by the first id and every refused sum / avg pair by the second, each with its routes (count, a sort for a first or last record, or a numeric / temporal field for a quantity stored as text). This change adds a query-time reader of the same table at the analytics cube door; it refuses a query shape, not a stored one, and no authorable key, export or stored row moves: CubeSchema and the analytics query body keep parsing every member. -->
10+
11+
**BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept, on every driver and on both strategies. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12+
13+
FROM → TO, for a `measures` entry that resolves to a cube measure over a column of the cube's own object (an authored cube measure, or a suffix-inferred one such as `note_max`):
14+
15+
- `min` / `max` over a type outside the numeric, temporal and boolean classes (the string family such as `text`, `email` and `url`; `select`, `radio`, `lookup`, `user`; `autonumber`; the JSON-stored, file and `formula` types): FROM, on the native-SQL strategy, `200` with the column's own value (a string such as `"y"`) under `fields[] { type: 'number' }`, on SQLite and PostgreSQL alike; on the ObjectQL strategy the engine's door already answered `400 INVALID_FIELD` after the strategy began. TO `400 INVALID_FIELD` before either strategy reads anything.
16+
- `sum` / `avg` over a type outside the numeric and boolean classes (`sum` also refuses `percent`): FROM `200` with a plausible `0` on SQLite and `500 DATABASE_ERROR` on PostgreSQL (the ObjectQL strategy refused `avg` at the engine and passed `sum` to the driver, which answered the same `0` / `500`). TO `400 INVALID_FIELD`.
17+
- `min` / `max` over a `date`, `datetime` or `time` column: FROM `fields[] { type: 'number' }` beside the instant. TO `fields[] { type: 'time' }`, the `DimensionType` word a temporal dimension column already carries, by the same rule the dataset door applies (`measureResultType`).
18+
19+
**What an author sees now.** `400 INVALID_FIELD`, naming the measure as the request wrote it, the cube, the column, the object and its declared type, saying the query was not run, and naming the types the aggregate accepts, read off `AGGREGATE_FIELD_TYPE_COMPATIBILITY`. The thrown error carries `member`, `param` (`measures`), `cube`, `field` and `object`.
20+
21+
**Why a refusal.** The dataset door (`POST /api/v1/analytics/dataset/query`) refuses every one of these pairs at compile by the same table (`DATASET_INVALID`), and the engine's aggregate door refuses most of them on the ObjectQL strategy; the native-SQL strategy compiled its own statement and asked nothing. Measured through the real dispatcher route on SQLite and PostgreSQL 16: a configured cube's `max` over a `text` column answered `"y"` under a column described `number` on the native strategy and `400` on the ObjectQL strategy, and `sum` over the same column answered `0` on SQLite and `500` on PostgreSQL. One cube, one query, an answer chosen by the driver.
22+
23+
**What to write instead.** Aggregate a field of a type the aggregate accepts. A question that was counting in disguise is `count` (or `count_distinct` over a scalar-stored field). A first or last record by a text value is a sort on a list, not an aggregate. A quantity stored as text belongs in a numeric field of its own, aggregated there.
24+
25+
**Who is affected.** A dashboard, report or caller that asked `min` / `max` / `sum` / `avg` of such a column through `/analytics/query` on the native-SQL strategy and read the answer as a real one. No example app and no shipped cube authors such a pair. A reader that branched on `fields[].type === 'number'` for a temporal `min` / `max` column now sees `time`.
26+
27+
**Unchanged.** Every pair the table accepts, a `max` over a `boolean` column included (its column keeps `number`: the rule declines the boolean class); `count` over any column; `count_distinct`, which keeps its own door and words; a measure over a relationship path (`account.name`), which this door does not judge; a column the host's field metadata cannot resolve, or a type outside `FieldType`; a measure whose `sql` is `*`; an expression metric type (`number` / `string` / `boolean`); and a host that wires no `sourceFieldMeta`, where the declaration cannot be read. The dataset door keeps its own `DATASET_INVALID` answer at compile.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
---
4+
5+
fix(service-datasource): `POST` / `PATCH /api/v1/datasources` and `POST /api/v1/datasources/test` judge the datasource record they will persist against `DatasourceSchema`, the contract `os build` and `PUT /api/v1/meta/datasource/:name` already enforce (#21058)
6+
7+
Clause-②: no
8+
9+
- Before, these doors checked the driver `config` alone. A record that `DatasourceSchema` refuses was accepted `201`, and `GET /api/v1/meta/datasource/:name` then reported it `valid: false`. The record is now judged as it will be stored, with the `external.credentialsRef` that a supplied `secret` (or the existing binding) carries, before any secret or record is written. A refusal answers `400 DATASOURCE_ADMIN_ERROR` with `DatasourceSchema`'s own message, and nothing is persisted.
10+
- Newly refused, for example: a mongo `config.url` whose userinfo names no user, or a composed mongo `config` with no `username`, beside a `secret`. The bound secret was never used and the datasource connected anonymously. Fix: put the user in the url (`mongodb://user@host/db`) or in `config.username`, or send no `secret`. Also refused: `schemaMode: 'external'` or `'validate-only'` with no `external` block. Fix: send `external: {}` or the federation settings. Also refused: a create with no `config`, or a `pool` key the schema does not declare.
11+
- `POST /api/v1/datasources/test` answers `ok: false` with the same message instead of probing, so a green test no longer comes before a refused save.
12+
- A `PATCH` that changes only `label` and/or `active` is not judged. A row stored before this change can still be renamed or taken out of service.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/core': minor
3+
---
4+
5+
fix(core)!: a date macro whose offset lands past every instant a JavaScript `Date` can hold is refused `INVALID_FILTER` / 400, naming the placeholder, instead of resolving to the text `Invalid Date` or throwing an uncoded `RangeError`
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of a VALUE in @objectstack/core's filter-placeholder resolver: a date macro (such as {300000_years_ago} or {99999999999999999999_minutes_ago}) whose offset lands past every instant a JavaScript Date can hold, refused INVALID_FILTER / 400 by resolveFilterToken and resolveFilterTokens. No authorable key, spelling, export or stored metadata shape moves: every filter, view, dataset and query shape parses as before, the date-macro vocabulary is unchanged, and @objectstack/core exports nothing new and nothing less (resolveFilterToken and resolveFilterTokens keep their signatures; the error class is module-private). The categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a value range or a resolved value and this diff adds none (not registered / already-registered); and the change is runtime behaviour, not a declaration (not runtime-interface-only / type-surface-only). -->
10+
11+
**BREAKING**: this narrows what `resolveFilterToken` and `resolveFilterTokens` answer for one class of inputs, and so what every door that resolves filter placeholders answers. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12+
13+
**What is refused now.** A relative-date placeholder whose offset lands past the instants a JavaScript `Date` can hold names no day and no time. The resolver used to answer a day-or-coarser one (`{300000_years_ago}`) with the text `Invalid Date`, which compares as text, and to throw an uncoded `RangeError: Invalid time value` for a sub-day one (`{99999999999999999999_minutes_ago}`). Measured before this through `engine.find` on InMemoryDriver and `POST /api/v1/data/:object/query` on SqlDriver over SQLite, over a `datetime` field with a row in 2026 and a row in 1500:
14+
15+
- `$lt {300000_years_ago}` answered both rows, and `judgeFilter` answered `{ ok: true }`;
16+
- `$lt {99999999999999999999_minutes_ago}` threw the uncoded `RangeError` from `engine.find` and `judgeFilter`, and the REST door answered `500 INTERNAL_ERROR`.
17+
18+
Both are refused now with `INVALID_FILTER` / 400 at every position the engine resolves (`where` on `find`, `findOne`, `count`, `aggregate`, a multi-row `update` and `delete`, a per-aggregation `filter`, `having`) and through `judgeFilter`, before any driver read. Every other caller of `resolveFilterToken` / `resolveFilterTokens` receives the same coded error in place of the text or the `RangeError`. The refusal is the same on every column, because the resolver does not know the column: such a placeholder names no value at all.
19+
20+
**What an author sees.** `Relative-date placeholder "{300000_years_ago}" names no instant: its offset lands past every instant a JavaScript Date can hold, so it resolves to no day and no time, and it is refused rather than compared. A date value names a year from 0001 to 9999, and a datetime value a year from 1000 to 9999: use a relative-date placeholder whose offset lands inside those years.` The thrown error carries `code: 'INVALID_FILTER'`, `status: 400` and `token` (the placeholder's name), the code the engine already answers for a placeholder that resolves outside its column's years.
21+
22+
**Unchanged.** A placeholder that names an instant resolves as before, including one past the years 0001..9999 that a `Date` still holds (`{273847_years_ago}` resolves to `-271821-09-30`); the engine's per-column year range judges that one, as before. Context placeholders and an unknown placeholder answer as before.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
fix(plugin-auth): the OIDC discovery documents answer on every boot, including one whose first request arrives before the auth instance is built
6+
7+
Clause-②: no
8+
9+
- `GET /.well-known/openid-configuration` and `GET /.well-known/oauth-authorization-server` used to be mounted only after the better-auth instance finished building, in the background. When the server answered any request before that (a readiness probe, for example), the router was already sealed. The late mount failed, the failure was logged, and both documents answered 404 until the process restarted. The RFC 8414 path-inserted alias and the two RFC 9728 protected-resource documents had the same problem.
10+
- All five routes are now mounted while the auth routes are registered, before the server opens its socket. Each request waits for the auth instance and then serves the document. A route that cannot be mounted now fails the boot instead of being logged and skipped.
11+
- When the OIDC provider plugin is degraded, these paths answer as they did before, as if they were not mounted. A boot-time error line still reports this.
12+
- If the auth instance cannot be built, a discovery request answers a server error, and the next request tries the build again. Before, these paths kept answering 404.

0 commit comments

Comments
 (0)