Skip to content

Commit d0937e2

Browse files
committed
Merge origin/main into the list-view page-mount retirement branch
os-regen artifacts: main's side taken for the one path both sides moved (content/docs/references/ui/view.mdx); regeneration follows as its own commit. Three hand-written conflicts resolved as semantic merges, both intents stacked: packages/spec/src/ui/view.zod.ts #16577 scope docblock + the retirement's rewritten attachment note packages/spec/src/ui/view.test.ts #16577 module-scoped flattenUnionIssues + the retirement's door comment packages/spec/src/migrations/registry.ts #16320 and #17063 step-18 paragraphs Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
2 parents 59b2e5f + abc4b83 commit d0937e2

733 files changed

Lines changed: 52393 additions & 10866 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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`search-fields.ts`'s module docblock says `$search` expands to an `$or` of `$icontains`, the operator the engine actually emits
6+
7+
The docblock's ENGINE bullet claimed `@objectstack/objectql`'s
8+
`expandSearchToFilter` expands a `$search` term into an `$or` of **`$contains`**
9+
clauses. It has compiled to `$icontains` since objectstack#7641:
10+
`packages/objectql/src/search-filter.ts:23` carries the ruling verbatim — *"The
11+
case-insensitive operator is `$icontains`, NOT `$contains`. `$contains` is
12+
contractually case-SENSITIVE (#4706 Q2 = A)"* — and both return paths of
13+
`fieldClausesForTerm` (`:109`, `:111`) emit `$icontains`.
14+
15+
**Why the distinction is worth a clause rather than a word swap.** `$contains`
16+
is contractually case-SENSITIVE, so a reader who trusted the old sentence built
17+
an ingress gate, a test or a driver **stricter** than the platform is — a false
18+
refusal, not a leak. The corrected bullet now says that in one clause, so the
19+
next reader of this module does not have to reconstruct it from two other
20+
packages.
21+
22+
⛔ No behaviour changes. This is a module docblock; the engine has been right
23+
since #7641 and no accept set, authorable key or published behaviour moves.
24+
25+
**This is shipped, which is why it carries a changeset rather than
26+
`skip-changeset`.** `@objectstack/spec`'s published `files[]` ships `dist`, and
27+
this TSDoc is emitted into `dist/data/index.d.ts` and `dist/data/index.d.mts`
28+
measured on the built artifact, with the old spelling absent from all 216 built
29+
files afterwards and the docblock's own neighbouring sentence present at 2 as
30+
the lit control. `src/data/search-fields.ts` is not a `.zod.ts`, so it is not
31+
shipped as source; the emitted declarations are the whole of its published
32+
reach, and they change.
33+
34+
The sibling INGRESS sentence two lines below — `@objectstack/metadata-protocol`
35+
`findData` refusing a `$searchFields` override the resolved set does not admit
36+
(#4254) — was measured on the same tip and is unchanged: `findData` still calls
37+
`assertSearchFieldsAreSearchable`, which resolves through this module's own
38+
`resolveSearchFieldResolution` rather than re-implementing the rule.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`EventMetadata.cluster` and `ServiceMetadata.cluster` cite the live docs page by SITE URL, not a dead filename
6+
7+
Both `.describe()` strings pointed at `cluster-semantics.mdx`, a page that is no
8+
longer in the tree — `apps/docs/redirects.mjs` has redirected
9+
`/docs/concepts/cluster-semantics` to `/docs/kernel/cluster` since the page was
10+
folded in. The section numbers still resolved, so nothing was broken for a
11+
reader following a link; what was broken is retrieval by filename, which finds
12+
nothing.
13+
14+
These two strings are the published half. `gen:docs` copies them into
15+
`content/docs/references/kernel/events-core.mdx` and `service-registry.mdx`, and
16+
they also ship as JSON Schema `description` values under `packages/spec/json-schema/`
17+
and as string literals in `packages/spec/dist/`. So the citation had to become
18+
something a SITE reader can follow:
19+
20+
```
21+
- See cluster-semantics.mdx §4. (a file that does not exist)
22+
+ See /docs/kernel/cluster §4. (the address the redirect already resolves to)
23+
```
24+
25+
⛔ Deliberately NOT the in-repo house style. Source comments elsewhere in the
26+
tree cite `` `content/docs/kernel/cluster.mdx` §N `` — a repo path, correct for a
27+
reader who has the repo checked out. Copying that convention into a `.describe()`
28+
would tell a docs-site reader to open a `content/docs/...` file they do not
29+
have, which is the same class of unfollowable reference pointed the other way.
30+
There is no in-repo precedent to copy either way: these are the only two
31+
`.describe()` strings in `packages/spec/src` that cite a docs page at all.
32+
33+
The site URL is also redirect-independent — it is the redirect's own target, so
34+
the reference survives the redirect being retired.
35+
36+
No accept set moves and no authorable key is added or removed: the schemas,
37+
their parse behaviour and their exported types are byte-identical apart from
38+
these two description strings. The two regenerated reference pages carry the
39+
same one-line change on three rows.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
"@objectstack/service-analytics": minor
3+
---
4+
5+
fix(service-analytics): a `min`/`max` over a `formula` field is typed from the formula's declared `returnType`, not described as `number` (#16236)
6+
7+
**Behaviour change — read this if any dataset measure aggregates a `formula`
8+
field.** `AnalyticsResult.fields[].type` for such a measure column was always
9+
`number`, whatever the formula computes. It is now translated from the field's
10+
declared `FieldSchema.returnType`:
11+
12+
```
13+
FROM {"rows":[{"first_label":"alpha","latest_due":"2026-06-01"}],
14+
"fields":[{"name":"first_label","type":"number"},
15+
{"name":"latest_due","type":"number"}]}
16+
17+
TO {"rows":[{"first_label":"alpha","latest_due":"2026-06-01"}],
18+
"fields":[{"name":"first_label","type":"string"},
19+
{"name":"latest_due","type":"time"}]}
20+
```
21+
22+
Both values were strings; both descriptors said `number`, so a renderer that
23+
branches on the declared type never reached its textual or temporal branch.
24+
25+
**The mapping is a TRANSLATION, not a pass-through.** `returnType` speaks the
26+
authoring vocabulary (`number` / `text` / `boolean` / `date`);
27+
`fields[].type` speaks `DimensionType` (`string` / `number` / `boolean` /
28+
`time` / `geo`). Two of the four words do not exist on the wire at all:
29+
30+
| declared `returnType` | `fields[].type` |
31+
|:---|:---|
32+
| `text` | `string` |
33+
| `date` | `time` |
34+
| `number` | unchanged — the producer's `number` is already correct |
35+
| `boolean` | unchanged — three readings disagree on what `min`/`max` over a boolean returns |
36+
37+
**A formula with no `returnType` is unchanged.** The key is optional — "absent
38+
when the type can't be proven (an ambiguous/`dyn` expression)" — and an
39+
unproven formula's measure column keeps the `number` it had. The absence is not
40+
read as an answer. That tier is written down as a row in `measureResultType`'s
41+
own table rather than left as an implied code path, and so is the treatment of
42+
a word outside the declared four: left alone, never guessed at.
43+
44+
**For hosts wiring `AnalyticsService` directly.** `AnalyticsServiceConfig`'s
45+
`sourceFieldMeta` hook gains an optional fourth member on its return —
46+
`returnType?: string` beside `type` / `defaultCurrency` / `max`. Additive: a
47+
host that returns the three-member shape still satisfies the contract and gets
48+
exactly today's behaviour for every column. `AnalyticsServicePlugin` relays the
49+
key automatically, so a host on the plugin needs no change at all.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
A run whose nodes all succeeded is no longer answered `failed` — or, under `errorHandling.strategy: 'retry'`, RE-EXECUTED — because its terminal run-history write threw (#16274)
6+
7+
`AutomationEngine.execute()` and `executeWithoutRetry()` each called `recordLog({ status: 'completed' })` from inside the `try` whose `catch` exists for **node** failures, so a throw out of a history write on a run that had already finished successfully was handled as though a node had thrown. This is the initial-execution half of the pattern fixed on the resume path in 17.4.0; that fix deliberately scoped these two sites out.
8+
9+
**The consequence was measured, and it is a double run, not just a mislabelled one.** `execute()`'s node-failure arm ends at the retry strategy branch, which hands the false `failed` result to the retry loop; the loop reads `result.success` and therefore re-enters `executeWithoutRetry()` — the whole flow, every node, again. Driven with `maxRetries: 2`: a flow whose node always succeeded ran it **three** times and wrote three `failed` rows, unattended, inside one `execute()` call, with the node's side effects repeated each time. Controls on the same instrument: the identical flow on healthy sinks runs the node once, and a genuine node failure runs it three times (retry working correctly).
10+
11+
**What can throw there is a host surface, not in-repo code** — which is why it could not be reproduced from inside the package and why the package owed the fix:
12+
13+
- the run-summary line `logger.info(line, meta)`, on by default (`runSummaryLog: 'info'`) and calling a **host-injected** `Logger`. This one needs no store at all.
14+
- `store.recordTerminal(record)` throwing **synchronously**, before it returns a promise — the `void write.catch(...)` beneath that call only ever sees a returned promise's rejection. Both stores shipped in this package are `async` methods and cannot do it, but `SuspendedRunStore` is an exported interface whose `recordTerminal` is optional, so a host store is unconstrained. (A store returning a non-thenable escapes identically: `write.catch` is then itself a synchronous `TypeError`.)
15+
16+
On that second variant the old code did not even answer `failed`: the node-failure arm's own `recordLog({ status: 'failed' })` threw again out of the same store and escaped `execute()` entirely — a rejected promise where `AutomationResult` is declared.
17+
18+
What changes:
19+
20+
- **Each completion-path history write is guarded at its own call site**, restoring the invariant that call's own documentation states: a history write must never block or break the run that produced it. The caller is told the truth — `success: true`, no `status`, the flow's `successMessage`, and a `summary` recomputed by the same pure function `recordLog` runs first — the node runs exactly once, and one `completed` row is recorded rather than `1 + maxRetries` `failed` ones.
21+
- **The swallowed failure is reported once per run at `error`**, with the consequence and the fix in the first line: the run completed, its terminal history row never landed, nothing retries it, and the run must not be re-run. The thrown text rides the structured slot.
22+
23+
⛔ No `catch` arm's meaning is widened: a genuine node failure still reaches the node-failure arm, is still recorded `failed`, still carries the node's own text, and is still retried the full `1 + maxRetries` times.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): the boot banner's `🔑 Dev admin` says what that account will and will not see (#17081)
6+
7+
`--seed-admin` (on by default in `os dev`) prints one credential, and it is the
8+
**only** one a first-run operator is given. It is also, by construction, the
9+
account with every *platform* capability and no *app-declared* one: its standing
10+
is `admin_full_access`, whose `systemPermissions` are `setup.access`,
11+
`studio.access`, `manage_users`, `manage_metadata`, `manage_platform_settings`
12+
and `manage_sharing` — all platform built-ins — plus the `'*'`
13+
view-all/modify-all record bits.
14+
15+
So in any app that gates its apps, tabs or nav entries on
16+
`requiredPermissions` — the filter `/me/apps` and `/meta/app` apply, and a
17+
first-class platform feature the docs teach — the credential the terminal hands
18+
over is the account that resolves to an **empty navigation**. A downstream
19+
maintainer ran `pnpm dev`, signed in with it, and read the empty shell as a
20+
broken product. The app was correct. The banner had asserted a login and said
21+
nothing about its audience, and it outranks whatever the app's own README says,
22+
because it sits directly under the command that was just run.
23+
24+
FROM → TO, on a boot that seeds:
25+
26+
```
27+
🔑 Dev admin: admin@objectos.ai / admin123
28+
seeded on empty DB · dev only — do not use in production
29+
+ platform admin — Setup, Studio and every record, but NO app-declared capability, so
30+
+ an app that gates navigation on requiredPermissions may show it an empty menu; grant
31+
+ it a permission set under Setup → Users, or sign in as an account your app seeds
32+
```
33+
34+
**Nothing about the seed changes.** What the first run creates — the account,
35+
its address, its password, its promotion to platform admin — is a product-shape
36+
decision and is untouched; only the banner's words move. The three lines print
37+
only inside the branch that already prints the credential, so a boot that seeds
38+
nothing is byte-identical to before.
39+
40+
Dim continuation lines rather than a warning, deliberately: ADR-0115's
41+
`OS_ALLOW_DEV_PLUGIN` amendment excluded the dev-admin seed from that hazard set
42+
because "a warning about a non-event spends the attention the real ones need".
43+
That exclusion is kept — this qualifies an event that just happened, on the line
44+
that already announces it, and adds no new line where there was none.
45+
46+
The route the sentence names is asserted against the declarations that make it
47+
reachable, not re-spelled: `SETUP_APP.requiredPermissions` is a subset of what
48+
this account holds, the `Users` entry is ungated, and the `sys_user` detail page
49+
carries the "Grant permission set" related list. A rename on any of those reds
50+
the pin instead of leaving the banner pointing at nothing.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`ComponentPropsMap['object-grid'].exportOptions` names all five members the renderer reads, not two
6+
7+
The entry is `z.unknown()`, so nothing about this key is parsed, refused or
8+
stripped: a member that does not exist draws no error and has no effect, and a
9+
member that does exist cannot be discovered from the schema. That makes the
10+
`.describe()` string the entire account of the key's shape rather than a summary
11+
of an enforced one — and it projects straight into
12+
`content/docs/references/ui/component.mdx`, which is what an author (or a
13+
generating model, ADR-0033) reads.
14+
15+
It named two members, `formats` and `streaming`. The only renderer reads five.
16+
17+
Measured at the `.objectui-sha` pin `53ded82bf7a494f54e344e19099dbf00854b8694`
18+
— objectui `packages/plugin-grid/src/ObjectGrid.tsx`, through the
19+
`schema.exportOptions` expression and the `exportConfig` local bound to it, with
20+
objectui's own scanner (`ObjectGrid.exportOptionsKeys.test.ts`, whose
21+
comment/string stripping is what stops a prose mention of a key being counted as
22+
a read): `formats` 2 read sites, `streaming` 2, `maxRecords` 1,
23+
`includeHeaders` 1, `fileNamePrefix` 1, and an absent-name control
24+
(`zzzNotAMember`) 0 on the same instrument — which is what makes those five
25+
counts readings rather than a matcher that matches anything. The same instrument
26+
answers the same five, with the same per-member counts, at objectui
27+
`3fbdd4a2dae1`, so the set is not an artefact of the pin's age.
28+
29+
The three missing members are `maxRecords`, `includeHeaders` and
30+
`fileNamePrefix`. An author reading the old string learned that
31+
`exportOptions` takes `{ formats, streaming }` and had no way to reach the other
32+
three short of reading the renderer's source — the shape objectstack#8010
33+
closed for this same key one layer out, when `streaming` was read for releases
34+
while no schema declared it.
35+
36+
⛔ The key is unchanged: it stays `z.unknown()` and no accept set moves in either
37+
direction. Giving `exportOptions` a real shape is a separate and much larger
38+
change with its own review requirements; this is the docs half only.
39+
40+
The new list is not restated in prose that can drift on its own. A pin holds the
41+
describe string's member enumeration equal to the members
42+
`ListViewExportOptionsSchema` declares — the spec's own five-key declaration of
43+
this same authoring block, reached through `ListViewSchema.exportOptions`'s
44+
object branch and itself derived from that same read set. Both spellings reach
45+
one renderer, so narrowing or widening the declared block now reds the
46+
`z.unknown()` prose instead of leaving it quietly behind: the declared side has
47+
parse failures to catch drift, this side had nothing. The pin also records that
48+
the key is unvalidated today, so the day it grows an accept set is a deliberate
49+
decision rather than a silent one.
50+
51+
`content/docs/references/ui/component.mdx` is regenerated from the string
52+
(`gen:schema` then `gen:docs`) and carries the same one-line change.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/runtime": minor
3+
"@objectstack/mcp": minor
4+
---
5+
6+
fix(runtime,mcp): `action.ai.requiresConfirmation` is ENFORCED at the AI-facing action door — an unconfirmed call is refused, and `run_action` grows the `confirm` member that satisfies it (#15942)
7+
8+
**Behaviour change — read this if any of your actions declare `ai.requiresConfirmation: true`.** An AI-facing invocation of such an action (`invokeBusinessAction`, reached from the MCP `run_action` tool) is now REFUSED unless the request carries the confirmation member. A call that succeeded before starts answering `428 ACTION_CONFIRMATION_REQUIRED`, and nothing dispatches: the action body does not run, and the subject record is not even read.
9+
10+
FROM → TO, for a caller of a gated action:
11+
12+
```
13+
run_action({ actionName: 'archive_lead', recordId: 'lead_1' }) // was: ran
14+
run_action({ actionName: 'archive_lead', recordId: 'lead_1', confirm: true }) // now: required
15+
```
16+
17+
The refusal is machine-readable so the retry is mechanical rather than guessed — `error.details` carries `{ actionName, objectName?, confirmationMember }`, and `confirmationMember` echoes the member's exact spelling (`AI_ACTION_CONFIRMATION_MEMBER`, `@objectstack/spec/contracts`). The `run_action` tool schema advertises `confirm` as an optional boolean, so an agent discovers the retry from the tool definition rather than from prose.
18+
19+
**What is NOT gated**, because this narrows a published accept set and the narrowing is deliberately as small as the author's own declaration:
20+
21+
- Only the DECLARED flag gates. `ai.requiresConfirmation: true`, set by the action's author, and nothing else. The wider `list_actions` heuristic — `mode: 'delete'` / `variant: 'danger'` on an action whose author declared nothing — still reports `requiresConfirmation: true` to advise a client, and still does NOT refuse. An explicit `ai.requiresConfirmation: false` never refuses.
22+
- Only the boolean `true` confirms. `'true'`, `1` and `false` are not attestations.
23+
- Only the AI-facing doors. The enforced set is the doors that enforce `ai.exposed` — today `invokeBusinessAction` via MCP `run_action`. REST `/actions` is not `ai.exposed`-gated and sits outside this gate.
24+
- `list_actions` is unchanged.
25+
26+
**A gate, not a queue.** Nothing is parked, nothing is held for an operator, and there is no resume path: a refused call simply did not run, and the caller confirms with its human and retries. And `confirm: true` is an unverifiable caller claim — an agent that always sends it bypasses the gate. The gate makes FORGETTING loud; it does not prove a human.
27+
28+
Why it is worth the break: the flag was read once and consumed once, to fill a field of the `list_actions` summary. It stopped nothing. That is the failure ADR-0049 retired `tool.requiresConfirmation` for — "a SAFETY flag that is merely accepted is false compliance" — reappearing on the very key the retirement's own ledger entry told authors to move to. The contract this implements landed in `@objectstack/spec` first (#16293).

0 commit comments

Comments
 (0)