Skip to content

Commit 7ea3bb0

Browse files
committed
Merge origin/main into claude/issue-23434-pin-citations-pr2
One conflict, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts: main (1692cd1, #23568) deleted the page / interfaceConfig / emptyState omit row whose why string this branch had edited. Resolved by taking main's side: the row's own text said it leaves once the renderer half lands. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm
2 parents 03d0be9 + 744b73f commit 7ea3bb0

54 files changed

Lines changed: 2733 additions & 954 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: 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: 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.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): under a wall, `os migrate organization-ownership --apply` names the act that clears a position binding the rewrite gate refuses, instead of pointing at a step that reports done (#23542)
6+
7+
Clause-②: no
8+
9+
**What changed.** Under the `group` or `isolated` posture, no ceremony step carries `sys_position_permission_set` rows into a position definition. `authored-conversion` reports and finishes before it reads a binding (ADR-0131 D3). The rewrite gate still refused those rows, rightly, but its remedy sent the operator to `authored-conversion`, which had already reported `done` with no blocker. The gate still refuses, and nothing is deleted or guessed. Only the remedy changes, and it now depends on where the position's definition lives.
10+
11+
The blocker's first sentence is unchanged: "Binding rows give position NAME sets its catalog definition does not name (SETS); deleting them would lose a grant." The sentence after it changes, under a wall only:
12+
13+
- **A position no catalog declares.**
14+
- FROM: "The authored-conversion step carries them; resolve its blocker, then re-run."
15+
- TO: "Under the POSTURE posture no ceremony step carries binding rows (ADR-0131 D3). Declare the position NAME with permissionSets naming SETS, through one door only: the deployment's code (compiled into the artifact this ceremony boots), or the metadata door as the platform administrator (PUT /api/v1/meta/position/NAME). A cold boot refuses a position name a package declares and the environment catalog also holds. Then re-run."
16+
- **A declared position the metadata door reports editable, whose definition lacks some of the bound sets.**
17+
- FROM: the same pointer.
18+
- TO: "Under the POSTURE posture no ceremony step carries binding rows (ADR-0131 D3). Add MISSING to the permissionSets of the position NAME through the metadata door as the platform administrator (PUT /api/v1/meta/position/NAME), then re-run."
19+
- **A declared position the metadata door reports not editable.** A definition a package ships is sealed (ADR-0131 D6), so no door on the database adds the sets. The text states the fact and names no act.
20+
- FROM: the same pointer.
21+
- TO, when the definition's provenance names the package: "Under the POSTURE posture no ceremony step carries binding rows (ADR-0131 D3). Package PACKAGE ships the definition of NAME, which is sealed (ADR-0131 D6: the metadata door reports it not editable, lock LOCK), so no door on this database adds those sets."
22+
- TO, otherwise: "Under the POSTURE posture no ceremony step carries binding rows (ADR-0131 D3). The metadata door reports the definition of NAME not editable (lock LOCK), so no door on this database adds those sets."
23+
- When the door's verdict cannot be read, the text says so and names no act.
24+
25+
Whether a definition is editable is the metadata door's own answer, the `editable` flag of its item read, the same verdict the ceremony already asks before its one in-place write.
26+
27+
**Unchanged.**
28+
29+
- Under `single`, the pointer at `authored-conversion` stays, because that step carries the bindings there.
30+
- A built-in position that does not resolve because the boot did not compose the security plugin keeps that composition's remedy, under every posture.
31+
- The `conflicting` blocker, for a name that organizations bind differently, keeps its text. It states the fact and names no act, because no door makes the rows agree.
32+
- `authored-conversion`'s walled note is unchanged. The rewrite gate's blocker is where the operator reads about binding rows.

0 commit comments

Comments
 (0)