Skip to content

Commit 3854d14

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-23488-retire-adminscope-binding-keys
2 parents 0221b16 + 16abea8 commit 3854d14

68 files changed

Lines changed: 4991 additions & 5582 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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): the 11 dashboard widget-binding rule ids print a `fix:` line of one remedy sentence, and `os explain RULE_ID` carries what those lines used to explain
6+
7+
Clause-②: no
8+
9+
- **Shorter fix lines.** Each finding of these 11 rule ids (all in `validate-widget-bindings.ts`) now carries a `hint` (the CLI's `fix:` line, the runtime issue's `hint`) of one remedy sentence of at most 200 characters on every variant the lint suite fires (the lengths here are the `hint` alone; the CLI prints it after `fix: `). The longest of each, before → after:
10+
- dashboard filters: `dashboard-filter-field-unprovisioned` (561 → 177), `dashboard-filter-field-unknown` (403 → 148) and `dashboard-filter-field-not-included` (218 → 144);
11+
- widget filters: `widget-filter-field-unknown` (322 → 133);
12+
- refused `chartConfig` keys: `chart-field-unknown` (415 → 152);
13+
- empty selections: `chart-measures-missing` (369 → 144), `widget-measures-missing` (330 → 154) and `chart-dimensions-missing` (291 → 161);
14+
- legacy analytics keys: `widget-legacy-analytics-shape` (338 → 113) and `widget-legacy-analytics-unrenderable` (320 → 105);
15+
- tables: `table-count-only` (311 → 136).
16+
- **Lists are no longer echoed.** `dashboard-filter-field-unknown` and `widget-filter-field-unknown` no longer append the object's whole field list, and `dashboard-filter-field-not-included` no longer lists the dataset's declared `include` paths: the verdict's (or, on a bare dashboard-filter field, the fix line's own) "Did you mean" names the nearest field. The three empty-selection ids still name what the dataset declares, because no "Did you mean" can: at most three names, then `…`, or "it declares no measures yet".
17+
- **One line, one remedy.** The `suppressWarnings` clause leaves every warning's fix line, and with it: how DatasetWidget reads a dataset-bound widget; that a dotted filter target must traverse relationship fields and have its prefix in `include`, and that `a.b` includes `a`; how to vouch for or opt out of an unprovisioned anchor (`dashboard-filter-field-unprovisioned` now prints the shared one-line anchor fix the other anchor ids print); what replaces each refused `chartConfig` key, and the `os migrate meta --from 17` route; which widget families need no dimensions; and why a flat record listing is not an analytics dataset (ADR-0017).
18+
- **`os explain RULE_ID` carries the rest.** What the fix lines stopped saying is each id's `RULE_EXPLANATIONS` entry in `@objectstack/lint`, which `os explain RULE_ID` prints. Two ids gain an entry, so their `rule:` line now carries the `os explain` pointer: `table-count-only` and `widget-legacy-analytics-shape`. Seven existing entries gain text.
19+
- **At the runtime publish gate** (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), all 11 fire on a `dashboard` write; the `hint` FROM → TO, measured at the door:
20+
- 422 `issues[].hint`: `widget-legacy-analytics-unrenderable` 320 → 105; `dashboard-filter-field-unknown` 196 / 195 / 290 → 148 / 147 / 137 (an inherited bare field, a re-targeted bare field, a dotted path); `dashboard-filter-field-not-included` 213 → 139; `widget-filter-field-unknown` 220 → 126;
21+
- 2xx `advisories[].hint`: `widget-legacy-analytics-shape` 338 → 112; `dashboard-filter-field-unprovisioned` 550 → 173; `chart-field-unknown` 413 / 294 / 324 → 150 / 124 / 127 (an x-axis, a y-axis, a series); `chart-measures-missing` 240 → 96; `widget-measures-missing` 310 → 134; `chart-dimensions-missing` 297 → 156; `table-count-only` 311 → 136.
22+
23+
Studio's save-advisory toast prints an advisory's `hint` after its message, so the seven advisories shorten there; Studio renders no 422 issue's `hint`. No Studio, REST or MCP reader in this repository or in objectui matches on `hint` text; a tool that did should match on `rule` and `path` instead.
24+
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/service-i18n': minor
3+
---
4+
5+
fix(service-i18n)!: `I18nServicePlugin` no longer mounts its own copy of `/api/v1/i18n/*`; the runtime dispatcher is the one owner, and the `registerRoutes` / `basePath` options are removed
6+
7+
Clause-②: no
8+
9+
<!-- adr-0087: not-required (runtime-interface-only packages/services/service-i18n/src/i18n-service-plugin.ts#I18nServicePluginOptions) One TypeScript-only declaration in one runtime file narrows: the plugin's constructor options interface loses its `registerRoutes` and `basePath` members, which drove the plugin's own HTTP mounts and nothing else. No spec key, authorable metadata spelling, Zod schema or stored shape moves, and no stored row is read or rewritten, so `objectstack migrate meta` has nothing to rewrite. The channel that reaches an affected consumer is the compiler, at the consumer's own constructor call. The other half of the change is runtime routing in compositions that carry no dispatcher, which no ledger entry can describe. -->
10+
11+
**BREAKING** for code that constructs `I18nServicePlugin` with route options, and for a composition that serves HTTP without the runtime dispatcher. It ships as `minor` under the launch-window convention for breaking changes.
12+
13+
**What changed.** The three i18n routes (`GET /api/v1/i18n/locales`, `/translations/:locale`, `/labels/:object/:locale`) had two registrations: the runtime dispatcher's, in its `start()`, and this plugin's, in a `kernel:ready` hook. Measured on `objectstack serve` over the showcase app, the dispatcher's registration answered every request on all three paths, because it registers first and the HTTP adapter answers a duplicate path with its first registration. The plugin's copy was never reached there. Where it could be reached — a composition with an HTTP server and no dispatcher — it did not apply the authenticated-caller floor the dispatcher's i18n domain applies (ADR-0056 D2). It is removed: one route, one owner. The plugin still registers the `i18n` service and still syncs runtime-authored translations; the dispatcher's `/i18n` domain serves the routes from that service, exactly as before on `objectstack serve` and `objectstack dev`.
14+
15+
**If you passed `registerRoutes` or `basePath`.** Delete them from the options object. `registerRoutes: false` is now what always happens. There is no replacement for `basePath`: the routes live where the dispatcher mounts them.
16+
17+
**If you serve HTTP without the dispatcher.** The plugin no longer answers `/api/v1/i18n/*` there. Compose the runtime dispatcher (`createDispatcherPlugin` from `@objectstack/runtime`) to serve the i18n routes, with its authenticated-caller floor.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/plugin-sharing': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
fix(plugin-sharing, runtime): a shared conversation's messages open for exactly the callers its share link opens for, on both mounts
7+
8+
Clause-②: no — `/messages` now reaches the answer `/resolve` already gives the same caller for the same link, on the same mount; who can open a share link is unchanged.
9+
10+
A share link to an AI conversation has two public routes: `/resolve` returns the conversation, and `/messages` returns its messages. Both check the link through the same service call. `/resolve` passed it every input an audience is judged by. `/messages` passed fewer, so it refused callers `/resolve` served:
11+
12+
- **A `signed_in` link, a signed-in caller:** plugin-sharing's share-link routes answered `/messages` with `404 NOT_FOUND`, so the console's shared-conversation page showed the conversation with no messages. It now returns the messages, as the runtime dispatcher's share-links domain already did.
13+
- **An `email` link, an allowlisted `?email=`:** both mounts answered `/messages` with `404 NOT_FOUND`. Both now read `?email=` on `/messages` exactly as on `/resolve`, and return the messages.
14+
- **Unchanged:** every caller `/resolve` refuses is still refused on `/messages` with `404 NOT_FOUND`: an anonymous caller of a `signed_in` link, and a caller of an `email` link with no email or an off-list email, signed in or not. A password-protected link still needs its password on both routes. `/resolve` is unchanged.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
feat(spec): the page form offers the list page's own empty state, `interfaceConfig.emptyState`
7+
8+
Clause-②: no
9+
10+
The console's list page draws the page's own `interfaceConfig.emptyState` in place of the source view's, so the page metadata form now offers it.
11+
12+
- **`@objectstack/spec`**: `pageForm`'s Interface section gains an `emptyState` control under `interfaceConfig`, a popover of the list view's `EmptyStateSchema` keys (`title`, `message`, `icon`, `link`), placed after the add-record entry point. No schema changes: `InterfacePageConfigSchema.emptyState` already parsed, so every page that was valid before is valid now, and nothing new is accepted.
13+
- **`@objectstack/platform-objects`**: the metadata-forms translation bundles carry the new control's label and help text, translated in `zh-CN`, `ja-JP` and `es-ES`.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): a first draft of a sealed, package-shipped item is told to edit the source artifact, not to choose another name; a refused first-write pin is told what it asserted
6+
7+
Clause-②: no
8+
9+
**What was wrong.** `PUT /api/v1/meta/:type/:name?mode=draft` with `If-None-Match: *` on an item a code package ships, on a type with no environment overlay (an `object`, for one), answered `403 NOT_OVERRIDABLE` with the remedy for a create: "… so its name is taken. To author your own object, choose a name no package or built-in holds …". The same draft sent without the header was told "… Edit the source artifact and redeploy." The header asserts only that no draft is pending, so the request is a first draft of the item already under the name, an edit. A client or AI author that followed the create remedy authored a second object instead of editing the source.
10+
11+
**What changed.**
12+
13+
- A first draft over a held name names the item's own remedy, the sentence the same draft reads without the header (for a type with a regime row, its sanctioned path). A first write of the live row (`If-None-Match: *` with no `?mode=draft`) still names a free name.
14+
- A save refused `409 METADATA_CONFLICT` because it sent `If-None-Match: *` over an existing row now says so: "TYPE/NAME: this save asserted that no draft row exists (the first-write pin, If-None-Match: * on the REST door), but one does (current is VERSION). …" (`active row` for a save without `?mode=draft`). It used to read "TYPE/NAME has been modified since you loaded it. Expected parent null but current is VERSION.", though the caller had loaded nothing. Every other conflict keeps its sentence.
15+
16+
**Unchanged.** Which requests are accepted or refused, every code and status, and the conflict body's `currentVersion` and the refusal's keyed fields.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Component-member read points, part 1 of 3: the doc comments in the first half of `src/ui/component.zod.ts` no longer cite objectui files, lines or commits as read points.
6+
7+
Clause-②: no
8+
9+
- The doc comments of the `ComponentPropsMap` rows from the `page:*` and `record:*` blocks through the `element:*` and `action:*` rows, object-grid, object-metric and the first part of object-kanban now say "read points recorded in `liveness/page.json`, row `regions`". The per-hop re-read chains are removed. What each record claims about the renderer is kept in the shorter comment.
10+
- `liveness/page.json`, row `regions`, gains one evidence list in its `note`: 120 entries spelled `COMPONENT.member: objectui @SHA FILE#SYMBOL`. The list asserts no verdict, so component-level liveness stays unclassified. All three parts of this change carry the same list.
11+
- No schema, type, export, default, accept or refuse change. No `.describe()` text changed.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Component-member read points, part 2 of 3: the doc comments in the second half of `src/ui/component.zod.ts` no longer cite objectui files, lines or commits as read points.
6+
7+
Clause-②: no
8+
9+
- The doc comments of the `ComponentPropsMap` rows from the rest of object-kanban through object-calendar, object-form, object-master-detail-form, object-map, object-gantt, object-tree and object-timeline, and the row registrations at the end of the file now say "read points recorded in `liveness/page.json`, row `regions`". The per-hop re-read chains are removed. What each record claims about the renderer is kept in the shorter comment.
10+
- `liveness/page.json`, row `regions`, gains one evidence list in its `note`: 120 entries spelled `COMPONENT.member: objectui @SHA FILE#SYMBOL`. The list asserts no verdict, so component-level liveness stays unclassified. All three parts of this change carry the same list.
11+
- No schema, type, export, default, accept or refuse change. No `.describe()` text changed.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/rest': minor
3+
'@objectstack/runtime': minor
4+
---
5+
6+
fix(rest,runtime)!: both `/data` write doors validate the request as the caller sent it: a `PATCH` or `POST` create whose body is not a record, and a `PATCH` whose `expectedVersion` is a number, are refused `400 VALIDATION_FAILED` on both doors before the engine is asked
7+
8+
`RestServer` validates `PATCH /data/:object/:id` against `UpdateDataRequestSchema` and `POST /data/:object` against `CreateDataRequestSchema`. The runtime dispatcher's `/data` domain, the only answer on a host that mounts just the `@objectstack/hono` catch-all, validated neither, so a body that is not a record reached the engine. On a `PATCH` it answered `500` with the engine's own error. On a `POST`, an array created one row per element and a number created an empty row, both answered `201`. `RestServer`'s own `PATCH` gate parsed a coerced copy of the request: it read a JSON `null` body as `{}`, and it read a numeric `expectedVersion` as its string. Both doors now validate the assembled request as the caller sent it, and they answer each case alike.
9+
10+
Clause-②: no
11+
12+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, and no exported declaration is added, removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is the accept set of two HTTP routes on two doors: a write whose body is not the record the request schema declares, or whose body version token is a number where the schema declares a string, is now refused where it was applied or judged, and the remedy is in the request a caller sends, never a rewrite of a package source or a stored row. The other categories are closed on facts: both packages publish (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
13+
14+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
15+
16+
What changes for you, wherever `@objectstack/rest` or the runtime dispatcher serves these routes:
17+
18+
- `RestServer`, `PATCH /data/:object/:id` with a JSON `null` body: FROM `200`, an update that changed no field and still moved the record's `updated_at` (so every other holder of the old version saw a conflict on its next guarded save), TO `400 VALIDATION_FAILED` naming `data`, the record unchanged. The runtime dispatcher answers the same.
19+
- The runtime dispatcher, `PATCH /data/:object/:id` with an array, a string, a number or `null` as the body: FROM `500` carrying the engine's own error text, nothing written, TO `400 VALIDATION_FAILED` naming `data`, before the engine is asked. `RestServer` already answered `400` for the first three.
20+
- The runtime dispatcher, `POST /data/:object` with an array, a string or a number as the body: FROM `201` with one row created per array element, `201` with an empty row for a number, and the engine's own `400 INVALID_FIELD` for a string, TO `400 VALIDATION_FAILED` naming `data`, with no row created. `RestServer` already answered this.
21+
- Both doors, `PATCH /data/:object/:id` with a body `expectedVersion` that is a number (`5`, `0`): FROM judged as its string (`'5'`) and answered `409 CONCURRENT_UPDATE`, TO `400 VALIDATION_FAILED` before the protocol is asked, whatever `If-Match` is sent beside it. The request schema declares the token a string, and a number is now one of the token shapes that can never carry a version.
22+
23+
How it renders: `RestServer` answers `{ error, code: 'VALIDATION_FAILED', fields, object }`, the envelope its request gate already used. The runtime dispatcher returns its own `400 VALIDATION_FAILED` envelope, `fields` under `error.details`, instead of throwing, so a `@objectstack/hono` `createHonoApp` host renders it as `400`.
24+
25+
Unchanged: a record body, `{}` included; a string `expectedVersion` (judged; `409` on a mismatch); `If-Match` and the `DELETE`'s `?expectedVersion`, which are strings by transport; and a JSON `null` body on a `POST` create, which both doors still read as an empty record and answer `201`.
26+
27+
To send a patch or a row, send the fields as a JSON object. To guard a write, send the version you read (the record's `updated_at`) as a string. The `@objectstack/client` SDK's `data.update` sends the version as an `If-Match` header string, so an SDK caller is affected only when the `data` it passes is not an object, or carries a number `expectedVersion`.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): a flow name two packages hold is refused at `/api/v1/automation` with a remedy that works from there: the metadata door's `?package=` route, not the `?package=` this door does not read
6+
7+
Clause-②: no
8+
9+
A flow name that two or more packages hold is refused `409 RESOURCE_CONFLICT` by the `/automation` definition doors, because they save and delete without naming a package. The refusal is unchanged. Its remedy is not: the doors used to relay the metadata store's sentence, "on the REST door add ?package= with the package id", and `/automation` reads no `?package=`. A caller who followed it got the same `409` again.
10+
11+
The answer is still `409 RESOURCE_CONFLICT`. Its text now names a route that takes the package, and still states what was refused and which packages hold the name:
12+
13+
- **`PUT /api/v1/automation/NAME` and `POST /api/v1/automation`** name `PUT /api/v1/meta/flow/NAME?package=` with one of the listed packages and the flow definition as the body.
14+
- **`DELETE /api/v1/automation/NAME`** names `DELETE /api/v1/meta/flow/NAME?package=`, which removes that package's row and keeps the others. That route is the REST server's `/meta` door, the one `os serve` mounts. A host that sends every request to the runtime dispatcher (the `@objectstack/hono` catch-all) serves no `DELETE` on `/meta` and answers it `405`.
15+
- **`POST /api/v1/automation/SOURCE/clone`** onto such a name answers the clone door's own taken-name refusal: a clone takes a new machine name, and the refusal suggests one.
16+
17+
The door recognises this refusal by its envelope (`RESOURCE_CONFLICT` / 409), then reads the packages from the served flow list. When that list does not show the name held by two or more packages, the store's refusal is relayed as before.

0 commit comments

Comments
 (0)