Skip to content

Commit f0f3de9

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-e8y326
2 parents f779162 + ea33919 commit f0f3de9

170 files changed

Lines changed: 7377 additions & 1346 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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/lint': minor
3+
'@objectstack/spec': minor
4+
---
5+
6+
feat(lint)!: `objectstack validate`, `compile` and the publish gate refuse a bulk action's `confirmText` hole over an unbound root, a single-brace token, a hole holding logic and a blank sentence (#25205)
7+
8+
Clause-②: no (narrowing: a bulk action's `confirmText` that names an unbound root, carries a single-brace token, holds logic in a hole or is blank is refused at `objectstack validate`, `compile` and the publish gate; it ships now under the amended 「甲」, #6015 `6303865025`)
9+
Metadata-protocol: narrowed
10+
11+
A list-view bulk action's `confirmText` is an ADR-0032 `template` slot over a closed scope, `count` and the def's params by name (#24798). `validateStackExpressions`, the rule all three doors run, judges each one through `closedScopeTemplateRefusal`, then compiles it as a template. Its findings were warnings. They are now errors, as the flow text slots' findings are. This supersedes the warning-only behaviour described for the same pass earlier in this release line; the finding no longer ends with "A warning, not a refusal".
12+
13+
**BREAKING — what moves for consumers.**
14+
15+
- FROM: a warning at `objectstack validate` and `compile`, and an entry on the publish gate's `advisories` channel while the view or object saved. TO: an error. Both commands fail, and an active `view` or `object` write answers `422 INVALID_METADATA` with an `expression-invalid` issue located at `bulkActionDefs[N] confirmText`. The item is not stored. This covers `saveMetaItem` in publish mode (Studio, REST `/meta`, MCP) and the promotion of a draft (`publishMetaItem`).
16+
- The four refused texts: a hole over a root the def does not bind (`{{ owner }}` when the def binds `count` and `tier`), a single-brace `{x}` token, a hole holding logic or naming an unknown formatter (`{{ count * 2 }}`, `{{ count | shout }}`), and a blank sentence (`''`).
17+
18+
**Fix (one line).** Write the hole over `count` or one of the def's params (`{{ count }}`, `{{ tier }}`), double a single-brace token as the finding prescribes, move logic out of the hole, or remove a blank `confirmText` so the default sentence shows.
19+
20+
**Unchanged.**
21+
22+
- The parse. `bulkActionDefs[].confirmText` accepts what it accepted, a blank string included (its second union arm stays). The boot load and the read path judge a stored view with this parse, so a stored view carrying such a sentence keeps loading and serving until it is next saved, and that save is judged. Saving as a draft is still allowed; publishing the draft is judged.
23+
- A sentence with no finding draws nothing at any door, in either form (the bare string or the `{ dialect: 'template', source }` envelope).
24+
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
25+
- No public export, signature or schema shape moves. The `confirmText` describe now says the texts are refused.
26+
- Measured before crossing, through the built `@objectstack/lint` at `925fd0772c`: 0 in-repo producers carry such a sentence. The 6 bulk defs with a `confirmText` in `examples/app-showcase` (`project.view.ts`) draw 0 findings, against 1 for a planted unbound hole. HotCRM at `1e71d5e` declares 3 bulk `confirmText` sentences on `account.view.ts` (`{{count}}`, and `{{tier}}` on the def whose param is `tier`), all over bound roots: 0 findings. Its other 9 of the 12 are translations of those 3 in its locale bundles, which this pass does not read.
27+
28+
<!-- adr-0087: not-required (no-migration-prescription) a refusal at `objectstack validate`, `compile` and the publish gate of a bulk action `confirmText` naming an unbound root, carrying a single-brace token, holding logic in a hole, or blank: no authorable key, spelling, export or stored shape moves, the parse is unchanged, and no stored row is read, rewritten or converted. A stored view whose sentence is refused keeps loading until it is next saved, and the repair is the author's rewrite of the sentence (which root a hole should name is the author's intent), which no ledger entry can derive. The other categories are closed on facts: the packages publish (not unpublished); no ADR-0087 id covers this verdict (not already-registered); and the change is a build and door verdict, not a declaration (not runtime-interface-only or type-surface-only). -->
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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
fix(plugin-approvals): an out-of-office delegation applies only in its own organization, and an organization-less delegation reroutes no approval
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 removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. What changes is how the approval service reads a stored `sys_approval_delegation` row: the reading ADR-0131 D1 retired for every tenant column, and that `grantAppliesInTenant` already applies to grant rows. -->
11+
12+
**BREAKING** for a stored `sys_approval_delegation` row that carries no `organization_id` (`NULL`, absent or blank). No write produces such a row any more under a walled posture, so only a database upgraded from before 18.0 can hold one.
13+
14+
- **Which delegation reroutes an approver.** FROM: an organization-less delegation applied in every organization ("null = applies across tenants for this delegator"), and a request with no organization took every active delegation of its approver, whatever organization it belonged to. TO: a delegation reroutes an approver only when its organization is the request's organization, the rule a stored position or permission-set grant follows. An organization-less delegation reroutes nothing anywhere, and a request with no organization takes no delegation.
15+
- **Why.** Under `group` or `isolated`, an organization-less delegation rerouted every organization's approvals for its delegator, onto a delegate who may hold no membership in that organization. ADR-0131 D1: nothing reads a NULL tenant value as an owner.
16+
- **Under `single`** a delegation written on 18.x applies as before: the engine stamps the Default Organization on a delegation its author writes and on the record a request is about, and the request takes its record's organization. Only an organization-less row left from before the ownership ceremony stops applying.
17+
- **Where such a row comes from.** Under a wall a member's insert is stamped with the active organization, an elevated insert with none is refused, and an update that empties the column is refused. A row stored before that, on a database upgraded from 17.x or an 18.0 pre-release, can still be organization-less: `os migrate organization-ownership` attributes it to the Default Organization under `single`, and under a wall, where the row names nothing to attribute it by, reports it and leaves it NULL.
18+
- **It is not silent.** Each time a lookup skips an active organization-less delegation, the approval service logs a `warn` naming the delegation, its delegator and delegate, the request's organization, and the remedy.
19+
- **Setup.** The delegation form's Organization help says what the code does, in every shipped locale.
20+
21+
**What an operator does.** On a `single` deployment, run `os migrate organization-ownership --apply` if it has not run yet; it attributes these rows to the Default Organization. On a walled deployment, ask the delegator to create the delegation again while signed in to the organization whose approvals it should cover, then delete the organization-less row the warning names.
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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/service-settings': patch
3+
'@objectstack/plugin-email': patch
4+
---
5+
6+
feat: Setup → Email states which mail transport is live, and says so when no mail leaves this machine
7+
8+
Clause-②: yes (widening: service-settings exports SettingsService.registerLiveStatus / liveStatusOf, SettingsLiveStatus, SettingsLiveStatusProvider and SETTINGS_LIVE_STATUS_BANNER_ID; nothing is refused)
9+
Metadata-protocol: none
10+
11+
The Email page showed what is stored, not what is in use. Out of the box the `mail` namespace stores provider `smtp` with an empty host, so the form read as SMTP while every message went to the development log and was recorded in `sys_email` as sent.
12+
13+
- **`@objectstack/service-settings`:** `SettingsService.registerLiveStatus(namespace, provider)` lets the package that owns a live object state it on that namespace's page. `GET /api/settings/:namespace` asks the provider on every read and puts its answer first in the manifest's specifiers, as an `info_banner` with the reserved id `live_status` (`SETTINGS_LIVE_STATUS_BANNER_ID`). The registered manifest and in-process `getNamespace()` are unchanged. If the provider throws, the page says the live status could not be read; the statement is never silently dropped. New exports: `SettingsLiveStatus`, `SettingsLiveStatusProvider`, `SETTINGS_LIVE_STATUS_BANNER_ID`.
14+
- **`@objectstack/plugin-email`:** registers the `mail` statement from `EmailService.isConfigured()`. When it answers `false` (the log transport), the banner reads "No mail leaves this machine". When it answers `true`, the banner names the live transport, for example "Mail is delivered through SMTP (smtp.example.com:587)". The statement waits for any settings re-apply still running, so a read made just after a save names the transport that save built.
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.

0 commit comments

Comments
 (0)