Skip to content

Commit 5e616fc

Browse files
committed
Merge origin/main into claude/project-thread-a0vh3h (objectstack#25711, WIP)
Brings the branch up to main abebc4e for the cutover sitting. Clean merge; main's #25789 (cli console.ts), #25757 (spec vitest.repo-tests.json) and #25780 (dispatch-gates) overlap files this PR touches and are re-checked after it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0149BHSXfCdQywGaLdpRAtWe
2 parents 4ac569d + abebc4e commit 5e616fc

206 files changed

Lines changed: 10542 additions & 8122 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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING** — the package upgrade model (`kernel/package-upgrade.zod.ts`: upgrade plan, metadata diff, impact level, pre-upgrade snapshot, upgrade and rollback request/response, phase vocabulary), the four `IPackageService` upgrade members and the `plan` member of `PackageUpgradeResponse` leave the public surface (#25608).
6+
7+
A `major`-class change, recorded as `minor` under the launch-window convention. Maintainer ruling A (`6259332195`, item 3, 「其他同意」): "**Retired:** `packages/spec/src/kernel/package-upgrade.zod.ts` (plan, diff, conflict, snapshot, rollback; zero runtime consumers), with its ADR-0087 conversion." Done directly in 18.x, with no retirement stage; ADR-0049 enforce-or-remove.
8+
9+
Clause-②: no (narrowing)
10+
Metadata-protocol: narrowed
11+
Ruling-ref: 6259332195
12+
13+
**Why.** `@objectstack/spec` declared a complete package upgrade lifecycle — plan the upgrade against a metadata diff, grade its impact, take a pre-upgrade snapshot, three-way merge the customer's customizations, roll back to the snapshot — and nothing on the platform implemented any of it. No provider implements `IPackageService`, so `planUpgrade`, `upgrade`, `rollback` and `getSnapshot` had no body anywhere; no route answers `UpgradePackageRequest` or `RollbackPackageRequest`; and no package, example, app or skill in this repository, in objectui or in hotcrm read any of the names. `UpgradePackageRequest.createSnapshot: true` and `mergeStrategy: 'three-way-merge'` read as protections the platform gave an operator's customizations, and it never did.
14+
15+
### FROM → TO
16+
17+
| removed | from | what to write instead |
18+
| --- | --- | --- |
19+
| `UpgradePlanSchema`, `MetadataDiffItemSchema`, `MetadataChangeTypeSchema`, `UpgradeImpactLevelSchema` (with their types and `…Parsed` aliases) | `@objectstack/spec/kernel` | nothing — no code ever generated an upgrade plan or diffed two package versions. |
20+
| `UpgradeSnapshotSchema`, `RollbackPackageRequestSchema`, `RollbackPackageResponseSchema` (with their types and `…Parsed` aliases) | `@objectstack/spec/kernel` | nothing — no snapshot was ever taken. The package rollback the platform serves is the commit rollback, `POST /api/v1/packages/:id/rollback` with a `commitId`, answered by `RollbackToPackageCommitResponseSchema` (`@objectstack/spec/api`). |
21+
| `UpgradePackageRequestSchema`, `UpgradePackageResponseSchema`, `UpgradePhaseSchema` (with their types and `…Parsed` aliases) | `@objectstack/spec/kernel` | nothing — no upgrade ever ran. Installing a new version over an installed package is a re-install: `POST /api/v1/packages` with `overwrite: true` (the SDK's `packages.install(manifest, { overwrite: true })`). |
22+
| `IPackageService.planUpgrade`, `.upgrade`, `.rollback`, `.getSnapshot`, and their inputs `PlanUpgradeInput`, `ExecuteUpgradeInput`, `RollbackInput` | `@objectstack/spec/contracts` | delete the members from your implementation and the imports of the three inputs — the contract no longer carries them. |
23+
| `PackageUpgradeResponse.data.plan` | `@objectstack/spec/api` | nothing — it carried the retired `UpgradePlan`. `PackageUpgradeRequestSchema` and `PackageUpgradeResponseSchema` themselves stay published, bound to no route. |
24+
25+
**The one-line fix: delete every import of the names above and every read of `PackageUpgradeResponse.data.plan`.** The compiler finds them (`TS2305: Module '"@objectstack/spec/kernel"' has no exported member …`, and `TS2339` for `plan`). No behaviour is lost — nothing ever read or produced these shapes.
26+
27+
**What stays.** The package model beside it — `ManifestSchema`, `InstalledPackageSchema`, `PackageArtifactSchema`, the install, uninstall, enable/disable and dependency-resolution members of `IPackageService` — is unchanged. The `UpgradePlan.estimatedDuration` tombstone leaves with its def's source; its `RETIRED_KEYS_BY_MAJOR[18]` entry and the D3 entry `kernel-package-lifecycle-durations-unit-in-key` stay as history.
28+
29+
⚠️ Runtime behaviour is deliberately **unchanged**: nothing ever parsed a retired shape or called a retired member, so every request answers exactly as before. The removal retracts a false claim, not a capability. **No deprecation window** (startup-stage posture: retirements take effect immediately).
30+
31+
⚠️ **The out-of-repo consumer population beyond objectui and hotcrm is NOT MEASURED.** Inside this repository the names occurred only in their declarations, their own tests, generated artifacts and prose; objectui (main and the pinned `.objectui-sha`) and hotcrm name none of them; `@objectstack/spec` is published, so readers elsewhere were not measured.
32+
33+
The ADR-0087 D3 semantic entry `kernel-package-upgrade-family-retired` carries the judgement, and the ten defs are registered in `RETIRED_DEFS_BY_MAJOR[18]`: none of these shapes is a stack collection or a metadata type, so there is no source for a D2 conversion to rewrite and no carrier key for a tombstone.
34+
35+
<!-- adr-0087: registered kernel-package-upgrade-family-retired -->
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
---
5+
6+
feat(spec,lint): object-level `composite` / `repeater` are untyped JSON — a typed list is a child object
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: semantic
10+
Ruling-ref: 6386744473
11+
12+
On an object field, `composite` and `repeater` are untyped JSON containers of CMS lineage (a Strapi component, an ACF group or repeater). The `FieldType` comments used to promise "declared sub-fields", but `FieldSchema` has no member that declares them, so nothing gave the inner keys labels, types, options, required flags or translations, and nothing could validate or address one row. The comments and the field-types docs now say so, and point a list that needs typed columns to a child object: a `master_detail` field pointing back at the parent, with `inlineEdit: 'grid'` to edit the rows inline on the parent's form.
13+
14+
The strict field schema already refused `fields`, `subFields` and `columns` on a field, but named only the key. The refusal now names the route: a typed list is a child object (`columns` also names the live `lookupColumns`, `relatedListColumns` and `inlineColumns`). This changes the message, not what parses.
15+
16+
New `@objectstack/lint` rule `field/embedded-sub-fields` (error): a `composite` / `repeater` object field carrying `fields`, `subFields` or `columns`. It fires on the doors that never parse (`os lint` on a raw object-literal config, `defineStack(x, { strict: false })`, the metadata-generation rubric); every parsed door refuses the field first. `os explain field/embedded-sub-fields` prints the reasoning. A form view's `fields` sub-rows and the Studio metadata forms are not judged.
17+
18+
What to do: nothing for a field that declares no sub-field key — the type is unchanged. A field that carried one never parsed under `defineStack`; model the list as a child object.
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+
docs(spec): a `notify` node's `recipients` names an email address written out as an author constant, not an audience selector, and states the computed-address and email-field behaviour the runtime makes
6+
7+
Clause-②: no
8+
Metadata-protocol: semantic
9+
Ruling-ref: 6386629237
10+
11+
The `recipients` text of a `notify` node config now says what an email address in `recipients` means, and what the notify node does with one. That text is the field's `describe()` (which the reference docs and the JSON Schema read), the union's description, and the sentence a refused `recipients` value leads with.
12+
13+
- **Written out as a string, it is an author constant.** `recipients: 'ops@example.com'` is no audience selector. It is sent as written: to the user who holds that address, or else, on the `email` channel, to the address itself. Nothing about it changes; the text no longer calls it an audience selector. The audience selectors are the prefixed forms, such as `team:ID`.
14+
- **An address a CEL envelope evaluates to is not one.** In a node whose `channels` include `email`, a computed recipient that reads as an address, bare or as `user:ADDRESS`, must belong to a user. One a user holds resolves to that user. One that no user holds fails the node, and the message points to `{ emailField }`. A node that does not name `email` (`inbox` alone, the default) mails nobody, so its computed recipients go to the messaging service as before. The screen is scoped to nodes that include the `email` channel (confirmed by the maintainer).
15+
- **`{ emailField }` is delivered.** The text no longer says delivery is not live. The address is read from the stored trigger record when the node runs. It must be one plain ASCII mailbox, or the node fails and nothing is sent. One address receives at most 5 such emails per hour; the limit is best-effort.
16+
17+
No key, shape, default or accepted value changes. Everything that parsed still parses, and authored metadata needs no change.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/mcp': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
fix(mcp,runtime)!: the stateless `/api/v1/mcp` surface answers `GET` and `DELETE` with `405` and `Allow: POST`, so an MCP client stops re-opening a GET stream that ends at once
7+
8+
Clause-②: yes (narrowing)
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) A transport verb answer on the MCP HTTP surface: no key of any metadata schema is removed, renamed or re-shaped, no export, type or error code is removed or renamed, and the 405 body is the MCP SDK transport's own. There is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->
12+
13+
**BREAKING** accept-set narrowing at the MCP HTTP transport (`MCPServerRuntime.handleHttpRequest`, `@objectstack/mcp`), and on the standalone server's `/api/v1/mcp` route (`createDispatcherPlugin`, `@objectstack/runtime`).
14+
15+
- **Before.** The transport is stateless (no session id) and closes its per-request server when the request ends. A `GET` still reached the SDK's stream handler, which answered `200 text/event-stream` and then lost the stream when the server closed. An MCP client reads a GET stream that ends as a dropped connection and opens another one: claude-code re-opened it 2–3 times a second for as long as a session stayed connected. A `DELETE` answered `200` and ended nothing, because no session exists. Every other verb answered `405` with `Allow: GET, POST, DELETE`.
16+
- **Now.** Every verb but `POST` that reaches the transport (`OPTIONS` with CORS off; with CORS on the CORS middleware's `204` answers first on both doors) answers `405` with `Allow: POST` and the JSON-RPC body `{"jsonrpc":"2.0","error":{"code":-32000,"message":"Method not allowed."},"id":null}`, before a server is built. This is the Streamable HTTP transport's answer for a server that offers no GET stream and does not end sessions. The MCP SDK client treats a `405` on GET as "no stream offered" and does not retry it.
17+
- **The standalone server.** `createDispatcherPlugin` mounts `/api/v1/mcp` for `GET`, `POST` and `DELETE`, and now also declares it a path whose domain decides every verb (the declaration `/mcp/skill` and `/keys` already carry). Before, a `PUT`, a `PATCH` or an `OPTIONS` with CORS off matched no route there and the HTTP adapter answered it itself: `405` with `Allow: DELETE, GET, HEAD, POST` and a `METHOD_NOT_ALLOWED` envelope, to an anonymous caller too. Now those verbs reach the dispatcher like the mounted ones: an anonymous caller gets `401`, a signed-in one the transport's `405` with `Allow: POST`. A `createHonoApp` host already handed every verb to the dispatcher, so both doors now answer every verb alike.
18+
- **Unchanged.** `POST` (JSON-RPC: `initialize`, `tools/list`, `tools/call`, …) answers exactly as before. An anonymous caller is refused `401` before any verb answer, on every verb and on both doors.
19+
- **Who sees it.** A client that opens the GET stream on `/api/v1/mcp` now gets `405` instead of a `200` stream that ends at once. Nothing was ever sent on that stream. On the standalone server, an anonymous `PUT`, `PATCH` or `OPTIONS` (CORS off; with CORS on the CORS middleware's `204` answers `OPTIONS` first on both doors) on `/api/v1/mcp` now gets `401` instead of `405`.
20+
21+
**The remedy.** None needed for an MCP client: it stops opening the stream. A probe or health check that read `GET /api/v1/mcp` as `200` should send a `POST initialize` instead.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the shipped liveness ledger cites the automation seam modules that now hold the moved engine bodies
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Seven rows in `liveness/connector.json`, `liveness/flow.json` and `liveness/translation.json` cited `AutomationEngine` methods in `service-automation/src/engine.ts` whose bodies now live in the service's seam modules, leaving one-hop wrappers behind. They now cite the body that holds the quoted code: `plugin-extension-api.ts#registerConnector`, `dag-traversal.ts#executeWithoutRetry`, `run-resume.ts#resumeInternal`, `plugin-extension-api.ts#deriveTriggerBinding` with the shared `flow-trigger-kind.ts#resolveFlowTriggerKind` (which is where `flow.type` is read now), and `node-text-rendering.ts#translatedFlowTemplate` / `#renderRefusalMessage`. The `translation` `flows.screens` row cites `i18n-resolver.ts#flowScreenCopyKey`, the file that spells its address, because the generic renderer it hands each slot to never names the key. That row is what turned `check:liveness` red.
11+
12+
What to do: nothing. No status, key or verdict changes; every row keeps its classification. Only the evidence pointers and the prose around them change.
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+
build(spec): every entry point shares one module graph, so a schema two entries reach ships once and is the same object through either
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`@objectstack/spec`'s JS outputs used to be one self-contained bundle per entry point. A consumer that imported several subpaths bundled one copy of every schema those subpaths share (`FieldSchema` was defined in 14 published `.mjs` files), and the same symbol reached through two subpaths was two different objects (`defineConnector` from `@objectstack/spec` was not `defineConnector` from `@objectstack/spec/integration`).
11+
12+
The build now emits shared chunks (`dist/chunk-<hash>.mjs` and `dist/chunk-<hash>.js`) that every entry imports. Measured on the console's own import list (256 value imports across 11 subpaths, esbuild `bundle` + `minify` + `platform: browser`, gzip level 9): 1,628.4 KB gzipped from the per-entry layout, 602.9 KB from this one. `FieldSchema` is defined once. Across the 119 export names two entries both expose, all 119 are now the same object under the `browser` condition, and 114 under Node, against 0 before. The 5 that still differ under Node are the metadata-collection normalizers reached through both `@objectstack/spec` and `@objectstack/spec/shared`: they sit on the path to the Node-only postgres URL grammar, so the Node outputs of the `browser`-conditioned entries keep their own copy of them.
13+
14+
What to do: nothing. The `exports` map and the type declarations are unchanged. Import only through the `exports` map: the chunk files are not entry points, and their names change from build to build.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
feat(plugin-auth): in multi-org mode, Create User adds the new user to the administrator's current organization
7+
8+
Clause-②: yes (narrowing): under a walled posture `POST /api/v1/auth/admin/create-user` now refuses `400 NO_ACTIVE_ORGANIZATION` when the calling administrator has no current organization, where it used to create a member-less account; a request from an administrator in an organization now also adds the new user to it (widening)
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No key of any metadata schema is added, removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. What narrows is one admin route's answer for an administrator who has no current organization under a walled posture; the caller's migration is the one the refusal names: switch to an organization, then create the user. -->
12+
13+
**BREAKING** — under a walled tenancy posture (`group` / `isolated`), `/admin/create-user` no longer creates a user with no organization. The administrator's current (active) organization is the new user's organization, the way mainstream admin consoles add people to the workspace you are standing in. ADR-0093 D3 gains an amendment for this one route.
14+
15+
- **The join.** The new user joins the calling administrator's current organization as `member`, through better-auth's own `addMember`, the same act as Add Member, inside the account's transaction. A refused join (a membership limit) refuses the call with the vendor's status and code, and no user is created. A membership already there is kept and reported as `membershipCreated: false`. The response and the audit row carry `organizationId` and `membershipCreated` as before.
16+
- **No choice to make.** The dialog and the route take no organization. To add a user to another organization, switch to it first.
17+
- **No current organization.** FROM a member-less account TO `400 NO_ACTIVE_ORGANIZATION`, before any account exists. The fix is to switch to the organization the user belongs to, then create them.
18+
- **Policy.** Under `membershipPolicy: 'invite-only'` the user still joins: creating a user inside your organization is an explicit act, like Add Member (ADR-0093 D1).
19+
- **Capability.** A walled server without the organization plugin, or without a unit of work for the creation, answers `501` before any account exists.
20+
- **Error code.** `NO_ACTIVE_ORGANIZATION` is registered in `@objectstack/spec`'s error-code ledger under `@objectstack/plugin-auth`. It is better-auth's own code for the same condition on Add Member.
21+
- **Unchanged.** The `single` posture (a degraded wall included), self sign-up, bulk import, SSO sign-in and invitations.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/cli': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
feat: `kind:'react'` pages are off unless the deployment turns them on
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
**BREAKING**: the `kind:'react'` page tier, which runs page source in the Console, now defaults to **off** (ADR-0081, amended). A deployment that serves react pages turns the tier on with `OS_PAGE_REACT_ENABLED=true`, in the environment or the project's `.env` file. With it off, each react page shows a notice naming that switch instead of running, and the Console document's content-security-policy no longer allows `'unsafe-eval'`. `kind:'html'` pages are unaffected.
12+
13+
To keep react pages running after the upgrade, set `OS_PAGE_REACT_ENABLED=true` on the deployment. Stored pages need no change.
14+
15+
<!-- adr-0087: not-required (no-migration-prescription) an operator environment default moved; no spec key, export or stored row changed, so there is nothing for objectstack migrate meta to rewrite -->
16+
17+
The earlier name is still read while the new one is unset: `OS_PAGE_REACT=on` turns the tier on and `OS_PAGE_REACT=off` keeps it off, each with a one-time deprecation warning. `true`, `1`, `on`, `yes` and `enabled` turn it on; unset, `false`, `0`, `off`, `no` and `disabled` leave it off; any other value leaves it off and is reported once in the server log.
18+
19+
The server now writes the Console's capability global in both directions, so a Console build with either default follows the server's setting. `examples/app-showcase` opts in through its `.env`, so its three react pages keep running. The `PageSchema.kind` description in `@objectstack/spec` now states the new default.
20+
21+
Operators who collect content-security-policy reports may see one `script-src` eval refusal per Console load while the tier is off: the Console's schema library probes for `eval` once at boot and falls back without it. Nothing on the page is affected, and the probe is removed on the objectui side.

0 commit comments

Comments
 (0)