Skip to content

Commit 7dd4eb6

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-s43tmc
2 parents b91d5d6 + 6c2699b commit 7dd4eb6

123 files changed

Lines changed: 3883 additions & 1223 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
'@objectstack/cli': minor
5+
---
6+
7+
feat(spec,lint): a bulk action's `confirmText` is a `template` slot over `count` and the action's params
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: semantic
11+
12+
- **Before.** `bulkActionDefs[].confirmText` was a plain `z.string()` that declared no hole. A sentence such as `'Update tier on {{count}} account(s) to {{tier}}?'` parsed, nothing judged its holes, and the console showed them as literal text.
13+
- **Now.** `confirmText` is an ADR-0032 `template` slot (Decisions 2–3). A bare string is shorthand for `{ dialect: 'template', source }`, and the envelope form is accepted too. Holes are `{{ path }}` and `{{ path | formatter[:arg] }}`, with formatters from the template engine's whitelist and no logic inside a hole. They read a closed scope, and `bulkConfirmTemplateRoots(def)` (`@objectstack/spec/ui`) declares its roots: `count` (`BULK_CONFIRM_COUNT_ROOT`, the number of records the run covers) and each of the def's params by `name`. The roots are written bare: `{{ tier }}`, never `{{ params.tier }}`. That matches how the flow text slots spell their roots and how shipped sentences already spell them, and `count` wins over a param of that name. A param hole renders the param's display value: the chosen option's `label` (labels joined with `', '` for a multi-value param), the picked record's display name for a lookup or user param, and the entered value otherwise, so number, date and currency values take a formatter.
14+
- **Parsed shape.** A parsed `confirmText` is now the `{ dialect: 'template', source }` envelope rather than a string, as `titleFormat` and a notify node's `title` already are. A reader of the parsed or served def takes the text from `source` (`templateSlotSource` in `@objectstack/spec/automation` reads both forms). A stored sentence with no hole parses as before, its text unchanged.
15+
- **Judged by the one template judge.** `objectstack validate`, `objectstack compile` and the runtime publish gate (now also on `view` writes, and on an object's own `listViews` at an object write) judge each bulk `confirmText` through `closedScopeTemplateRefusal`. That is the closed-scope entry of the judge the flow text slots use (`automation/flow-text-slot-template.ts`). A hole over an unbound root and a single-brace `{x}` token are named, the latter with its `{{ }}` spelling. A text carrying neither is then compiled as a template, which names a hole holding logic or an unknown formatter.
16+
- **A warning, not a refusal.** Each of those texts parsed while `confirmText` was a plain string, so refusing it is a narrowing that no ADR-0087 conversion carries. This release ships every finding above as a **warning**: an advisory at the publish gate, never a refusal, so no command's exit code changes. A blank `confirmText` (`''`) still parses, and the doors warn on it.
17+
- **`os i18n extract`.** A bulk `confirmText` written as the envelope is seeded exactly as the bare string: the extractor reads the slot's text through `templateSlotSource`, so the envelope form no longer records as an absent key with nothing to translate.
18+
- **Served translations.** The catalog overlay on a served view or an object's `listViews` compares a bulk `confirmText` with its packaged base by the slot's text, whichever form either side holds: an unchanged envelope still takes the catalog's sentence, and an edited one keeps the authored sentence.
19+
- **Unchanged.** The `confirm` / `confirmmessage` near-misses still name `confirmText`. Every other bulk-def key, the default confirm sentence and the flow text slots' verdicts are unchanged.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/service-datasource': minor
3+
---
4+
5+
fix(service-datasource)!: the datasource admin API refuses to make a datasource managed while objects bind it with `external`, as the metadata save door does
6+
7+
Clause-②: no (narrowing)
8+
9+
Metadata-protocol: narrowed
10+
11+
<!-- adr-0087: not-required (already-registered object-external-binding-managed-datasource-refused) no rule is added or changed: the admin door now asks the save door's existing ADR-0015 §4.2 predicate, and that ledger entry already describes the refused shape and its fix -->
12+
13+
**BREAKING** (what the datasource admin API stores narrows), shipped as `minor` under the 18.x convention for breaking changes.
14+
15+
`POST /api/v1/datasources` (`createDatasource`) and `PATCH /api/v1/datasources/:name` (`updateDatasource`) write the datasource without entering the metadata save door. So they stored a datasource that is managed (`schemaMode: 'managed'`, or no `schemaMode`, the spec default) while objects bind it with `external`: the pair `PUT /api/v1/meta/datasource/:name` refuses (ADR-0015 §4.2). The runtime then skips those objects' DDL as federated while the boot validation and the write gate judge them as managed.
16+
17+
- **FROM:** the admin API created a managed datasource under objects that already name it with `external`, and a `PATCH` that set `schemaMode: 'managed'` (or edited another bound field of a datasource that is already managed) was stored.
18+
- **TO:** both refuse with `400 DATASOURCE_ADMIN_ERROR`, one line per bound object, each the spec predicate's own message (`externalBindingDatasourceRefusal`, `@objectstack/spec/data`). Nothing is stored or registered, no secret is minted and the live pool is untouched.
19+
- **Unchanged:** a federated datasource (`schemaMode: 'external'` or `'validate-only'`) and a datasource no object binds with `external` keep their answers. A `PATCH` that changes only `label` or `active` is not judged, so a datasource can still be renamed or taken out of service.
20+
- **For a host that builds `DatasourceAdminService` itself:** `DatasourceAdminServiceConfig` gains a required member, `listObjectsBoundTo(datasource)`, returning the objects that name the datasource with their `external` binding. `DatasourceAdminServicePlugin` answers it from the engine's object registry (the `objectql` service's `registry`, the one the save door writes through to), so an object saved or imported at runtime counts at once.
21+
- **The fix for an author:** declare the datasource `schemaMode: 'external'` (or `'validate-only'`) with its `external` settings, or remove `external` from the bound objects first, then make it managed.
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: the degraded rate-limit warning names the remedy that boots: a shared `ICacheService` registered as `cache`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
With no `cache` service registered, the lazy counter store falls back to a per-process store and logs one warning. That warning told a multi-node deployment to use "Redis via @objectstack/service-cache". But `CacheServicePlugin` throws at `init` when `adapter: 'redis'` is set, so an operator who followed it got a boot failure.
11+
12+
The warning now names the working path: an `ICacheService` backed by a store every node reaches, registered as the `cache` service (`ctx.registerService('cache', impl)`). It no longer names a bundled adapter, so the text cannot go stale when one ships. The ADR-0069 D2 reference and the per-caller impact suffix are unchanged. The same applies to every counter built on `createLazyCounterStore`: better-auth's rate limit, the per-number OTP send budget, the SMS quotas and the runtime's inbound and endpoint limiters.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): a dataset dimension's `nullLabel` is live, so `os lint` stops warning that nothing draws it, and the dataset form offers it
6+
7+
Clause-②: no (a liveness-ledger re-grade plus a form column for a key the schema already accepts: no schema, accept set, served body or error code moves)
8+
Metadata-protocol: none
9+
10+
The console now draws a dataset chart's null bucket under the name its dimension declares: the server carries it on the answer's column descriptor as `fields[].nullLabel` (objectstack#25402), and the dashboard's dataset chart reads it (objectui#13354, at the `.objectui-sha` pin this release builds). So:
11+
12+
- **Liveness:** `liveness/dataset.json` re-grades `dimensions[].nullLabel` from `planned` to `live`, citing the console's reader at the pin and the server's carry as its producer. Its `authorWarn` goes with it, so the advisory lint no longer tells an author who sets `nullLabel` that no renderer draws the bucket under it.
13+
- **Dataset form:** the Dimensions repeater gains a Null Label column, the schema's own `.meta({ title })`. The form had left the key out only while nothing drew it. That reason went with the `planned` verdict, so its row in the form ↔ Zod reconciliation ledger is gone.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/mcp': patch
3+
'@objectstack/cli': patch
4+
---
5+
6+
fix(cli,mcp): `os dev`'s boot banner and the served SKILL.md name the MCP server the same way
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
`os dev`'s "connect a coding agent" block printed `claude mcp add --transport http FOLDER URL`, where FOLDER was the basename of the directory `os dev` ran in. The served `SKILL.md` (`GET /api/v1/mcp/skill`) and the console's Connect an Agent snippets name the server `objectstack`. So one endpoint ended up in an agent's config under two names, depending on which instructions were followed.
12+
13+
Both producers now read one exported constant, `OBJECTSTACK_MCP_SERVER_NAME` (`'objectstack'`), from `@objectstack/mcp`. The banner's `Connect` row prints `claude mcp add --transport http objectstack URL`. If you registered the server under the folder name from an earlier banner, that entry keeps working. Remove it with `claude mcp remove FOLDER` if you also added it as `objectstack`.
14+
15+
`printMcpConnectHint` no longer takes a `name` option.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
fix: a new out-of-office delegation opens with the Delegator already set to the signed-in user, and the field's help says why no one else can be chosen
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The Delegator of `sys_approval_delegation` (Setup → Delegations) only ever offers the signed-in user, because the delegation write guard admits nobody else. The field still opened empty, and a search for a colleague answered "No options found" with no reason.
11+
12+
- `delegator_id` now declares `defaultValue: 'current_user'`, the spec's runtime token for a `sys_user` lookup, so the one admissible value is filled in from the start. The write guard is unchanged and stays the enforcement.
13+
- The field's help now reads: "The user going out of office. You can delegate only your own approvals, so this is always you: it starts as you and offers no one else." The `es-ES`, `ja-JP` and `zh-CN` bundles carry the translated text.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/service-sms': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
fix(service-sms, runtime): the Send test SMS refusals and the flow clone notice name the admin's controls instead of a REST recipe (#25345)
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
Two pieces of copy the console shows an admin told them to call the REST API.
12+
13+
- **Send test SMS** (`sms/test`): when the test could not go to the signed-in admin's own number, the refusal said to pass "to" in the request body. The settings page has no recipient field, so that advice could not be followed from it. Each refusal now says that nothing was sent and what the admin can do on the page: press Send test SMS signed in as an admin whose account has a phone number, or retry once the account can be read. A `to` in the request still wins, as before. It is now documented only in the handler's TSDoc, for API callers.
14+
- **Flow clone notice** (`FLOW_CLONE_NOTICE`, returned by `POST /api/v1/automation/:name/clone`): the notice told the admin to `PUT` the clone's complete definition with `status: 'obsolete'`, and named the toggle endpoint. It now names the two switches the console has. A packaged original is switched off with its Switch "…" off action on the Packaged automation page, and that switch does not reach the clone. The clone is switched off in Studio, with its Enabled switch and a publish. The REST recipe stays in the client SDK's `automation.toggle` TSDoc.
15+
16+
No route, response shape, status or code changes: only the message text does. A caller that compared either string verbatim must update its copy.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
fix: the `get_record` designer form no longer offers a `limit` below 1
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The `get_record` node's `limit` is a positive integer, and every flow save door refuses `0`, a negative or a fraction. The executor's form descriptor still declared `limit` as a bare integer, so the designer form offered values the save always refused. The descriptor's `limit` now declares `minimum: 1`, the same floor as the contract. Nothing a flow can store changes; only the form stops offering what the save refuses.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/cloud-connection': patch
3+
---
4+
5+
fix(cloud-connection)!: a cloud-fetched marketplace install whose package ships a position, permission set or capability name an environment-authored item holds is refused before any write, as the file-import install already was
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- 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. No export, type, flag or error code is added, removed or renamed: the refusal reuses the door's own `PLUGIN_REGISTER_FAILED` code and the registry's existing security-catalog sentence. What narrows is one install door's answer for a package the registry already refused to load; the remedy for a deployment that already took such an install is an operator one, stated below. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this door (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
11+
12+
**BREAKING** (the install door's accept set narrows), shipped as `patch`: it refuses only installs that could never load.
13+
14+
`POST /api/v1/marketplace/install-local` fetching a package from the marketplace (`packageId` plus `versionId`) used to answer `200` with `hotLoaded: false` when the registry refused the package because an item authored in this system already holds a security catalog name the package ships, for example a customer's clone of a vendor permission set saved under a name the vendor's next version also uses. The door wrote the package to its install ledger anyway and said it would load at the next restart. It never could: the environment's item is loaded at every boot, so the restart met the same refusal. Measured on a real restart, the boot runs, logs `rehydrate failed for <package id>` with the security-catalog sentence, and the package is not loaded at all. On an upgrade, not even its earlier version loads, because the ledger entry that named that version was overwritten.
15+
16+
- **Now:** that install is refused `422 PLUGIN_REGISTER_FAILED` before anything is registered or written, with the registry's sentence naming each colliding name and its holder. The earlier version, on an upgrade, stays installed and loaded. This is the answer an install from an imported file (`manifest` in the body) already gave.
17+
- **Unchanged:** an install with no such collision, fresh or upgrade. A cloud-fetched install the registry refuses for any other reason keeps its lenient `200` answer. A name another package or a built-in holds is not part of this change. `POST /api/v1/packages` cannot ship a catalog name at all: its body parse refuses a `positions`, `capabilities` or permission-set list before any write.
18+
- **Who sees it:** under `single`, the administrator who installs packages. Under `group` and `isolated`, the platform operator, who alone reaches marketplace installs.
19+
20+
**If a deployment already took such an install.** Its boot log carries `rehydrate failed for <package id>` with `Security catalog name conflict`, and the package's apps, objects and permission sets are missing although `GET /api/v1/marketplace/install-local` lists it. Choose one of these:
21+
22+
- To stay on the earlier version, install that version again through the same door (`POST /api/v1/marketplace/install-local` with its `versionId`). It loads at once, and its ledger entry replaces the one that cannot load, so it also loads after a restart.
23+
- To take the new version, first move the environment's item off the name. Save it again under a new name, point every assignment that named the old name at the new one, then delete the item under the old name. Then restart, or install the new version again. Only after that does the old name belong to the package, so an assignment left on the old name grants the package's definition from then on.

‎.changeset/console-1d178e06ac5e.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/console": patch
3+
---
4+
5+
Console (objectui) refreshed to `1d178e06ac5e`. Frontend changes in this range:
6+
7+
Derived from the changesets objectui declared over the range — 6 releasing of 6 changesets added across 5 non-merge commits.
8+
9+
- **minor** — Setup → Positions now says who holds each position and which permission sets it grants, and a click anywhere on a row opens the item (objectstack#25333). (objectui `a14737b14`)
10+
- **patch** — types: a node derived from an `@objectstack/spec` schema keeps the spec's `credential` registry key (`'value' | 'map'`, objectstack#25036, which marks the webhook's `secret` and `… (objectui `1d178e06a`)
11+
- **patch** — dashboard: a dataset chart draws its null bucket under the name the dataset declares for it (`fields[].nullLabel`, the dimension's `nullLabel` resolved by the server), over the gl… (objectui `1d178e06a`)
12+
- **patch** — Setup's Permission Sets and Positions lists ask before the Active switch turns an item off, and a toast confirms the change either way (objectui#13338). (objectui `fcbf1b280`)
13+
- **patch** — A member who may read none of the objects an app names now lands on one page that says they need access and whom to ask, instead of the app's dashboard drawn as a grid of "You don… (objectui `2c4e5f5af`)
14+
- **patch** — A single-series bar chart draws every bar in its series colour (objectstack#25330). (objectui `56b65c0ce`)
15+
16+
Metadata-protocol: none (the console refresh changes no key, value or type a metadata author writes)
17+
18+
objectui range: `5fef032b1ada...1d178e06ac5e`

0 commit comments

Comments
 (0)