Skip to content

Commit b48f908

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-23880-rerun-safety-nightly
2 parents fdc14b4 + 68ffa5d commit b48f908

71 files changed

Lines changed: 4805 additions & 291 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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): the six `data-model-rules.ts` 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 six rule ids now carries a `fix` of one remedy sentence of at most 200 characters on every variant measured, including long object and column names. The three ADR-0120 uniqueness ids carry it as the `hint` that `os validate` and `os build` print after `fix: `; all six print it as `os lint --fix`'s `→ fix:` line. The longest of each, FROM → TO (characters of the fix text alone):
10+
- `unique/legacy-organization-composite` 592 → 161;
11+
- `unique/unscoped-declared-index` 380 → 150;
12+
- `object/missing-name-field` 327 → 188;
13+
- `unique/double-declaration` 303 → 159 (the contradiction arm; the two redundancy arms were already short and now drop one word);
14+
- `rollup/non-numeric-aggregand` 300 → 165 (the line no longer names the parent object, so its length no longer grows with that name);
15+
- `field/select-missing-options` 207 → 180.
16+
- **What moved.** The text the fix lines stopped saying is in each id's `RULE_EXPLANATIONS` entry, which `os explain RULE_ID` prints:
17+
- `unique/unscoped-declared-index`: what `unique: 'organization'` makes the driver build, and the `os migrate meta --from 17` route for existing sources (one new paragraph);
18+
- `unique/legacy-organization-composite`: that the respelling makes the listed organization column NULL-safe in place, what the constraint then means, and that duplicate NULL-organization rows block the `recreate_index` with a report rather than failing a boot (one paragraph extended, one new);
19+
- `unique/double-declaration`: where each scope word may be written (one new paragraph).
20+
- `object/missing-name-field` now points at `os explain title-format-retired`, whose entry already says why a `titleFormat` template does not count. `field/select-missing-options` and `rollup/non-numeric-aggregand` lost only wording; the rollup entry already carries the reasoning.
21+
- **Runtime publish gate:** none of the six runs there. The three uniqueness entries are `cli`-only registry rules, and the other three run only in `os lint`'s `lintDataModel` sweep. So no 422 `issues[].hint` and no 2xx `advisories[].hint` changes.
22+
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): seven more 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 7 rule ids now carries a `hint` (the CLI's `fix:` line, the runtime advisory's `hint`) of one remedy sentence of at most 200 characters on every variant the lint suite fires (lengths are the `hint` alone; the CLI prints it after `fix: `). The longest of each, before → after:
10+
- flow reference syntax (`lint-flow-patterns.ts`): `flow-double-brace-interpolation` (1413 → 197) and `flow-bare-dollar-reference` (1234 → 198);
11+
- `liveness-ledger-unreadable` (270 → 82), `capability-reference-unknown` (241 → 131), `page-section-count-not-multi-value` (228 → 167), `list-view-filters-in-views-mode` (219 → 135) and `page-field-unknown` (206 → 94).
12+
- **The flow ids keep the judge's spelling, and only where it evaluates.** In a value slot the line names the spec value-slot judge's own CEL envelope for the reference, read out of the judge's remedy and never re-spelled: FROM `A value slot reads a CEL value envelope, … so a bare $ reference is stored as written. $source.id names $source, which is not one of the flow engine's own $ variables, … Write {source.id} as { dialect: 'cel', source: 'source.id' }. CEL refuses an absent variable or key … (the guarded form writes null).` (681) TO ``A value slot stores a bare `$` reference as written: write `{source.id}` as { dialect: 'cel', source: 'source.id' }.`` (116). Where an envelope at the string's place would not evaluate (a string inside an object or list literal, a legacy `assignments: [{ variable, value }]` shape, a `$User` path that never resolved), the line names no envelope and points at `os explain`. It never names the build's own refusal for a `{{ }}` string, whose spelling keeps the outer braces as literal text. In a text slot the line names the `{{ }}` hole for each reference the engine binds, and for any other `$` root says to compute the value into a variable with an `assignment` node and write `{{ v }}`. At a notify or http literal it says neither brace dialect resolves there: write the value itself.
13+
- **Lists are no longer echoed.** `page-field-unknown` no longer appends the object's whole field list; it names the nearest declared field ("Did you mean") instead.
14+
- **`os explain RULE_ID` carries the rest.** What the fix lines stopped saying is each id's `RULE_EXPLANATIONS` entry in `@objectstack/lint`: the `has()` guard for a value that may be absent, the run user's `current_user` null guard and the `sys_user` read for any other `$User` path, the whole-value literal and the canonical `assignments` map, what a filter operand's `null` does, which slots read `{{ }}`, and where a brace token in a literal is refused; that a capability another installed package declares needs no change; why page filter controls do nothing on an object list view (ADR-0047); that a missing liveness ledger is a platform packaging fault; which field types take `multiple: true`. Six ids gain an entry, so their `rule:` line now carries the `os explain` pointer: `flow-double-brace-interpolation`, `flow-bare-dollar-reference`, `liveness-ledger-unreadable`, `page-field-unknown`, `page-section-count-not-multi-value` and `list-view-filters-in-views-mode`. `capability-reference-unknown`'s entry gains one sentence.
15+
- **At the runtime publish gate** (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), the two flow ids fire on a `flow` write as 2xx `advisories[].hint`, FROM → TO measured at the door: a value-slot `$source.id` 681 → 116, `$User.Id` 681 → 121, `$User.Email` 742 → 198, a nested literal 1231 → 198, a legacy assignment 1030 → 198; a text-slot `$User.Id` 767 → 144 and `$error.message` 114 → 83; a value-slot `{{ }}` 746 → 116, a notify literal 297 → 137, a value no slot reads 362 → 158. Studio's save-advisory toast prints an advisory's `hint` after its message. `capability-reference-unknown` and `list-view-filters-in-views-mode` run at the CLI only; the two page ids do not fire on a `page` write; `liveness-ledger-unreadable` fires only on an install whose `@objectstack/spec` lost a ledger file.
16+
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): an `os serve` boot that binds no HTTP server sends no `objectstack:listening` message and writes no runtime state file, and the banner's `MCP:` row prints only when the MCP route is mounted
6+
7+
Clause-②: no
8+
9+
- **Before.** `os serve --no-server`, with no transport composed, sent `{ type: 'objectstack:listening', port, url: 'http://localhost:PORT' }` over an `ipc` channel and wrote `runtime.<environment>.<project>.json` with the same `port` and `url`. Nothing listened on that port: a `GET` to it was refused. The docs define `objectstack:listening` as "The HTTP server is bound".
10+
- **After, for a supervisor of a boot that binds no HTTP server** (`--no-server` with no config-mounted transport, or a transport plugin that did not load). It receives no `objectstack:listening` message. It still receives `objectstack:seed-settled`, which is then the boot's only message, so a parent waiting for the seed to settle does not hang. No `runtime.<environment>.<project>.json` is written, so "is my server running, and where?" gets no answer naming an address. The file's payload, where it is written, is unchanged.
11+
- **After, with an HTTP server composed.** Unchanged: the state file is written first, then the banner prints, then `objectstack:listening` is sent with the port actually bound, and `objectstack:seed-settled` follows it. That includes a `--no-server` boot whose config mounts its own `HonoServerPlugin`. The banner, the IPC message and the state file read the same check, a registered `http.server` service on the booted kernel, so they agree on whether there is an address.
12+
- **The banner's `MCP:` row.** It and its "connect an AI client" hint now also require the plugin that mounts `/api/v1/mcp`, the runtime dispatcher, to be registered on the kernel. `os serve` composes the dispatcher only under `--server`. So a `--no-server` boot whose config mounts its own `HonoServerPlugin` prints `API:` but no longer prints an `MCP:` address, which answered 404 there.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/cli': minor
3+
'@objectstack/driver-turso': minor
4+
---
5+
6+
fix(cli,driver-turso): `os migrate plan` and `os migrate apply` run on a Turso REMOTE database, and the apply names every drift entry it could not apply
7+
8+
Clause-②: yes
9+
10+
On a primary reached over the Turso REMOTE transport (`libsql://…`, every hosted tenant), `os migrate plan` and `os migrate apply` used to stop at their boot with `Deferred schema DDL is not supported by the Turso REMOTE transport`, so the remedy the drift report and the `OS_ARTIFACT_URL` boot gate print for destructive drift, `os migrate apply --allow-destructive`, could not run there at all. Both commands now boot the way `os migrate organization-ownership` does on that transport: with schema sync skipped instead of deferred. What an operator on a remote primary now sees:
11+
12+
- **The drift half runs.** `os migrate plan` lists the drift; `os migrate apply` applies what the transport can express, after the same confirmation: a column drop (with the indexes keyed on it, in one transaction) and every index entry. Nothing is written before the prompt, as before.
13+
- **The additive half is reported as NOT examined, never as "nothing to create".** Both commands print a warning under the database line saying that tables and columns the metadata declares and the database lacks are neither listed nor created by this run, and that the serving boot (`os serve` / `os start`) creates them on this transport. The `--json` documents carry `schemaSync: "skipped"`, so their `pending: []` reads as not examined; an apply with no drift answers `message: "in_sync_partial"`, not `"in_sync"`.
14+
- **Every entry the driver did not apply is named,** by table, column and kind, in both faces (`--json`: a new `notApplied` array on the post-apply document). The REMOTE transport runs no SQLite table rebuild, so a tightening, a relaxed NOT NULL, a width change or a dropped DEFAULT is not applied there.
15+
- **A destructive entry confirmed with `--allow-destructive` that was not applied makes the run PARTIAL:** the human face ends `PARTIAL apply: …` and the command exits `1`; `--json` carries `message: "partial"` and exits `1`. A non-destructive entry that was not applied is named and the exit stays `0`.
16+
17+
On every other database the apply behaves as before, with one difference in the same direction: a destructive change confirmed with `--allow-destructive` that the driver could not apply (its DDL failed, or the dialect cannot perform it) now exits `1` and is named, where the run used to exit `0` with a count. Its human line for the entries held back without `--allow-destructive` now reads `Skipped N destructive change(s) (re-run with --allow-destructive to include them)`.
18+
19+
`@objectstack/driver-turso`: the REMOTE face's `driftReportCaveat` no longer says a destructive entry has no remedy on this transport; it says `os migrate apply --allow-destructive` applies a column drop or an index change there and names the rest.
20+
21+
The other `os migrate` commands whose dry run arms deferred DDL (`duplicates`, `account-issuer`, `unmapped-columns`, `multi-value-columns`, and the dry runs of the data commands) still refuse on the REMOTE transport, unchanged.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
'@objectstack/service-messaging': patch
4+
---
5+
6+
Setup's Invitations, API Keys, OAuth Applications and Notification Preferences, Subscriptions and Templates lists say what belongs on them when they are empty, and API Keys gains a Create Key button (#23852)
7+
8+
Clause-②: no
9+
10+
**What an admin saw.** An empty Invitations, API Keys or OAuth Applications list read "No identity records — these records are created by the authentication provider … not added by hand here", which is false for all three. The three notification lists read the generic "Nothing here yet". API Keys had no way to create a key at all.
11+
12+
**What changes.**
13+
14+
- Every list view of `sys_invitation`, `sys_api_key` and `sys_oauth_application` declares its own `emptyState` (a title and one sentence), and a filtered view names itself: Invitations opens on "No pending invitations", the Revoked keys view reads "No revoked keys".
15+
- `sys_notification_preference`, `sys_notification_subscription` and `sys_notification_template` each ship one list view, `all`, declared only for its `emptyState`: it sets no columns (`columns: []`, so the grid keeps the columns it derived before), no filter, no sort and no page size.
16+
- `sys_api_key` gains `create_api_key` ("Create Key"), a primary list-toolbar action on every API Keys list, Setup's and the Account app's. It calls the existing `POST /api/v1/keys` route with the name you give it; the key is created for you, in the organization you are working in, and the raw key is shown once in a dialog you close after copying it. Nothing about the route changes, and the data API still does not create keys.
17+
- The new copy ships in `en`, `zh-CN`, `ja-JP` and `es-ES`.
18+
19+
Unchanged: every field, index, permission and the `revoke_api_key` / `restore_api_key` row actions.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/client': patch
3+
---
4+
5+
Clause-②: no
6+
7+
fix(client): `client.automation.getRuntimeStatus()` types its rows as the declared contract `FlowRuntimeState` (#23859)
8+
9+
**What was wrong.** The method declared its answer as `{ flows: Array<{ name, enabled, bound }>; total }`. `GET /automation/_status` serves the automation engine's `getFlowRuntimeStates()` rows unchanged, and each row is the contract `FlowRuntimeState` from `@objectstack/spec/contracts`. The SDK's type therefore hid five optional fields the wire already carries: `status`, `triggerType`, `object`, `reason` (one sentence saying why an enabled, triggered flow is not armed) and `unboundCause` (which of the closed `FLOW_UNBOUND_CAUSES` that sentence describes). A caller that read `row.reason` or `row.unboundCause` got a type error and either cast past it or concluded the field did not exist.
10+
11+
**What changes.** The return type is now `Promise<{ flows: FlowRuntimeState[]; total: number }>`. `name`, `enabled` and `bound` stay required, so existing callers compile unchanged. Read `row.unboundCause` to classify an unarmed flow, and render `row.reason` without parsing it. The request, the route and the values on the wire are unchanged.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os migrate organization-ownership` plans a v17 database that still holds the retired `sys_scim_provider` table, listing it as fate 4 instead of refusing it (ADR-0131 D10)
6+
7+
Clause-②: no
8+
9+
`sys_scim_provider` was retired from the platform before the ceremony's inventory was taken, so the inventory had no row for it. Its ADR-0087 entry (`scim-provider-object-retired`) leaves the table in place, untouched, by ruling, so every database that booted a release from before the retirement still holds it, `organization_id` column included. The plan refused such a database with `uninventoried-platform-table` (`sys_scim_provider carries organization_id but the ADR-0131 D10 inventory gives it no fate`), and so did `--apply`, so the ceremony could not run there at all and `os dev` / `os serve` stayed refused.
10+
11+
- **Now** the inventory lists `sys_scim_provider` with fate 4 (`report`), under a new census population, `retired-before-census`. The plan reports its rows and its NULL-organization rows by id. It never attributes them, not even to the Default Organization under `single`, and never deletes them.
12+
- **NOT NULL:** the table is exempt, so the plan reports it as not receiving NOT NULL. The object is retired, no declaration anchors tenancy on the column, and a SCIM connection scoped to no organization was a legitimate row.
13+
- The plan's `inventoryDigest` changes with the new row. A plan file written by an earlier CLI no longer matches, so `--apply --plan-file` refuses it with `plan-digest-mismatch`. Re-run `os migrate organization-ownership --out PLAN`, review it, and apply that.

‎.claude/settings.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"mcp__Claude_Code_Remote__get_session",
2626
"mcp__Claude_Code_Remote__list_sessions",
2727
"mcp__Claude_Code_Remote__create_trigger",
28+
"Edit(//home/user/objectstack-issue-*/.changeset/pre/**)",
2829
"Bash(agent-browser snapshot *)",
2930
"Bash(agent-browser skills *)",
3031
"Bash(curl -sS -X POST https://api.github.com/repos/objectstack-ai/objectstack/issues/*/labels *)",

‎.github/workflows/lint.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4057,6 +4057,12 @@ jobs:
40574057
continue-on-error: true
40584058
run: pnpm check:agent-test-spelling
40594059

4060+
# Rationale: `scripts/check-pnpm-flag-spellings.mjs` header (#23777).
4061+
- name: No committed pnpm call spells a flag the pnpm 11 frontend rejects
4062+
id: no-committed-pnpm-call-spells-a-rejected-flag
4063+
continue-on-error: true
4064+
run: pnpm check:pnpm-flag-spellings
4065+
40604066
# Every `<pkg>#<task>` override in turbo.json must name a package the
40614067
# workspace HAS and a script that package RUNS (#12046).
40624068
#

‎content/docs/deployment/cli.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,8 @@ output at all. Spawn with an `ipc` channel and wait for a message:
286286

287287
| Message | Sent by | Means |
288288
|---|---|---|
289-
| `objectstack:listening` | `os serve` | The HTTP server is bound. Carries `{ port, url }` — the port actually bound, which in dev may differ from the one requested. |
290-
| `objectstack:seed-settled` | `os serve` | Nothing is still seeding. Carries `{ ok, suppressed, sources }`. Sent once per boot, always **after** `objectstack:listening`. |
289+
| `objectstack:listening` | `os serve` | The HTTP server is bound. Carries `{ port, url }` — the port actually bound, which in dev may differ from the one requested. Not sent by a boot that binds no HTTP server (`--no-server` with no transport composed). |
290+
| `objectstack:seed-settled` | `os serve` | Nothing is still seeding. Carries `{ ok, suppressed, sources }`. Sent once per boot, always **after** `objectstack:listening`; on a boot that binds no HTTP server it is still sent, and is the only message. |
291291

292292
```js
293293
import { spawn } from 'node:child_process';

0 commit comments

Comments
 (0)