Skip to content

Commit 32d6e47

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16233-agents-return-propagating-seams
2 parents f2c3ebf + be7382d commit 32d6e47

223 files changed

Lines changed: 14491 additions & 239348 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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/runtime': minor
4+
---
5+
6+
**BREAKING for action handlers** — `ActionEngineFacade.find` takes the engine's query ENVELOPE; the bare-filter parameter shape is withdrawn (#15124)
7+
8+
Clause-②: yes (narrowing)
9+
10+
`ctx.engine.find(object, query)` now takes `EngineQueryOptions` — the same
11+
options bag `IDataEngine.find` and ObjectQL's own `engine.find` take, named by
12+
identity rather than restated. **One platform, one query shape.**
13+
14+
### Migration — FROM → TO
15+
16+
| You wrote | Write instead |
17+
| --- | --- |
18+
| `ctx.engine.find('task', { status: 'open' })` | `ctx.engine.find('task', { where: { status: 'open' } })` |
19+
| `ctx.engine.find('task', { amount: { $gt: 100 } })` | `ctx.engine.find('task', { where: { amount: { $gt: 100 } } })` |
20+
| `ctx.engine.find('task', {})` | unchanged — an empty envelope is still the unfiltered read |
21+
22+
The rewrite is lossless and mechanical: the filter moves under `where`, verbatim.
23+
`tsc --noEmit` over your handlers finds every unmigrated call — see below.
24+
25+
### Why the shape was withdrawn rather than the bar closed
26+
27+
Until now this parameter was the `where` HALF of a query while every other
28+
`find` on the platform took the whole envelope, and the runtime wrapped what it
29+
was given. That made the most natural spelling the wrong one, silently: an
30+
author who passed the engine's own envelope reached the engine as
31+
`{ where: { where: … } }` — a filter on a field named `where` — which matches no
32+
row and resolves to `[]` with **no error at all**. A handler that made the
33+
mistake ran to completion over zero rows for as long as it shipped, and its own
34+
hand-written test double, written to the same belief, passed every assertion.
35+
Because an empty `{}` skipped the wrap, one unfiltered read kept working under
36+
either belief, so a dead handler looked partially alive.
37+
38+
Refusing `where` at the top level instead — intersecting the old parameter with
39+
`{ where?: never }` — was rejected: it asserts a vocabulary fact the spec
40+
declares nowhere, reserving the field name `where` across every customer's data
41+
model to buy one parameter's compile-time check. Aligning the parameter removes
42+
the ambiguity at its root and reserves nothing.
43+
44+
### What the new declaration refuses, measured
45+
46+
If your handler is typed with the published `ActionHandlerContext`, a bare filter
47+
no longer type-checks on **either** path you can reach it by:
48+
49+
- an object literal (`{ status: 'completed' }`) fails the excess-property check —
50+
a field name is not an envelope key;
51+
- a filter held in a `FilterCondition` variable fails **TS2559** — every envelope
52+
key is optional, so a bag of field names has no property in common with it.
53+
54+
The envelope's own keys are typed too: `where: 'a = b'`, `fields: 'id,subject'`
55+
and `limit: '50'` are each refused.
56+
57+
**If your handler is NOT typed with it** — a handler in an `objectstack.config.js`
58+
/ `.mjs`, one annotated with your own copy of the context type, or a `(ctx: any)`
59+
handler — nothing above reaches you, so the facade refuses the withdrawn shape at
60+
**runtime** instead, before the engine, with the same prescription:
61+
62+
```
63+
find('task') was given a key 'status' the query envelope does not carry.
64+
ctx.engine.find(object, query) takes the engine QUERY ENVELOPE, not a bare
65+
filter — move the filter under `where`: find(object, { where: { … } }).
66+
Envelope keys: context, cursor, distinct, expand, fields, limit, offset,
67+
orderBy, search, searchFields, top, where.
68+
```
69+
70+
⚠️ **That refusal matters most for a filter whose value is `null`.** The engine's
71+
own unknown-option check exempts a `null` value, because on an option bag a
72+
`null` is a withdrawal. On a filter it is the "rows with no X" idiom, so
73+
`{ deleted_at: null }` would have been dropped unexecuted and the read would have
74+
widened to **every row** — including the ones you were excluding — with no error
75+
at all. It is refused instead.
76+
77+
### What this opens
78+
79+
`fields`, `orderBy`, `limit`, `offset` and `expand` are reachable from an action
80+
handler for the first time — under the old parameter there was nowhere to carry
81+
them. A caller-supplied `context` is **ignored**: this facade is trusted and
82+
context-less by design, and the runtime stamps its own elevated
83+
`ExecutionContext` last. Do not write one — it reads as authorization and is
84+
none.
85+
86+
### Checking a migrated handler
87+
88+
Do not settle for "it still resolves". A handler that had been passing the
89+
envelope was returning `[]` on **every** call, so a suite written against the
90+
mistake passes and the row count is the only witness. Re-run each migrated
91+
handler against seeded data and assert it returns the rows its filter selects.
92+
93+
<!-- adr-0087: registered action-engine-facade-find-query-envelope -->

‎.changeset/16045-spec-declaration-text-snapshots.md‎

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
Gallery, kanban and timeline view configs declare an author-settable row ceiling.
6+
7+
`GalleryConfigSchema`, `KanbanConfigSchema` and `TimelineConfigSchema` each gain a
8+
`limit` member — a positive integer, default **100** — saying how many records the
9+
view fetches. The default is APPLIED by the schema rather than only described, and
10+
the key's own text states the other half of the contract: when the ceiling applies,
11+
the renderer must show a visible truncation signal, because a bounded view that
12+
looks complete is worse than an unbounded one. `DEFAULT_VIEW_ROW_LIMIT` is exported
13+
so a consumer reads that number instead of re-declaring it.
14+
15+
The knob belongs in the protocol because two renderers already cap by author choice
16+
off keys the protocol never declared: objectui's kanban board fetches
17+
`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT` with `limit` declared in
18+
`@object-ui/types` alone, its timeline does the same off a component props
19+
interface, and its gallery caps not at all. `limit` is the name those consumers
20+
already read, so this declaration absorbs the consumer-local keys instead of
21+
introducing a second spelling of one concept.
22+
23+
Nothing is removed, renamed or narrowed, and no document that parsed before is
24+
refused now. Two things to know when upgrading:
25+
26+
- a parsed gallery / kanban / timeline config carries `limit: 100` where the author
27+
wrote no ceiling, so code that compares a parsed config against a literal object
28+
sees the new member;
29+
- `KanbanConfigParsed` is now declared (ADR-0122) because that schema has two shapes
30+
for the first time; `KanbanConfig` is unchanged and remains the author state.
31+
32+
The non-grid four — gantt, calendar, map and tree — are deliberately untouched:
33+
their rows stay bounded by a platform ceiling the renderer owns, because a gantt's
34+
range, a map's camera fit and a tree's parent chain are computed over the whole set.
35+
36+
Clause-②: yes (widening)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the three shipped confirmation-gate prescriptions state the gate in the present tense — they were denying a door that exists (#17487)
6+
7+
Clause-②: no
8+
9+
No accept-set change and no export moves. `ToolSchema` still refuses
10+
`requiresConfirmation` with a located parse error, `ActionSchema` still accepts
11+
`ai.requiresConfirmation` in both directions, and `check:authorable-surface` /
12+
`check:api-surface` are byte-identical across this diff. What moves is text.
13+
14+
Three customer-facing prescriptions were written while the runtime confirmation
15+
door was a separate, unlanded change, and each said so in the present tense. The
16+
door has since landed on `main` — `actionConfirmationRefusal`, called pre-dispatch
17+
by `invokeBusinessAction` in `@objectstack/runtime`, with the `confirm` member
18+
grown on the MCP `run_action` tool in the same change. From that moment the
19+
published prose DENIED a door that exists, and it denied it in the dangerous direction: an author who
20+
reads it concludes the safety flag stops nothing and either arranges a human in
21+
the loop some other way or stops setting the flag — losing the gate exactly when
22+
it starts working. That is the ADR-0049 false-compliance defect with the sign
23+
flipped.
24+
25+
**The three carriers**, all of them shipped text rather than comments:
26+
27+
1. the `requiresConfirmation` entry of `TOOL_RETIRED_KEY_GUIDANCE`
28+
(`ai/tool.zod.ts`), which reaches consumers as the parse error on the
29+
`.strict()` `ToolSchema` — the one channel every consumer bumping
30+
`@objectstack/spec` is guaranteed to hit;
31+
2. the ADR-0087 D3 entry's `replacement`, and
32+
3. its `acceptanceCriteria` — what `spec-changes.json`,
33+
`docs/protocol-upgrade-guide.md` and `os migrate meta` project to consumers.
34+
35+
FROM → TO, on the sharpest of the three (the acceptance criterion):
36+
37+
```
38+
was: Do NOT try to "prove the gate" by invoking the operation without the
39+
confirmation member: ... before that ships the call is not refused, it
40+
RUNS the destructive operation.
41+
now: ... that gate is PERFORMED: invoking the operation over an AI-exposed
42+
door without the confirmation member is REFUSED with
43+
ACTION_CONFIRMATION_REQUIRED (428) and nothing runs, so that call is a
44+
real check you can make rather than a destructive experiment.
45+
```
46+
47+
**The corrections carry the door's BOUNDS, because over-promising here is the
48+
same defect in the other direction.** Each prescription now states, as the door
49+
itself declares them: the refusal is `ACTION_CONFIRMATION_REQUIRED` / 428 naming
50+
the action and the member `confirm: true`; it is a GATE, not a queue — nothing
51+
is parked and a refused call did not run, no record read and none written; the
52+
enforced set is the doors that enforce the author's `ai.exposed` opt-in, today
53+
the action door reached from the MCP `run_action` tool, while REST `/actions` is
54+
not `ai.exposed`-gated and sits outside the gate; only the author's declared
55+
`ai.requiresConfirmation: true` refuses, while the wider listing heuristic
56+
advises and never refuses; and `confirm: true` is an unverifiable caller claim,
57+
so the gate makes FORGETTING loud without proving a human.
58+
59+
`ai/tool-confirmation-prescription-tense.pin.test.ts` is the tie that was
60+
missing the first time: it reads the three shipped strings AND the runtime door,
61+
so a prescription that re-acquires a not-yet-shipped denial fails, and a door
62+
that is removed, narrowed off the DECLARED flag, unhooked from
63+
`invokeBusinessAction`, or widened onto REST `/actions` fails naming both files.
64+
The denial predicate is fed the three retired sentences verbatim, so it cannot
65+
pass by the prose merely falling silent.
66+
67+
**On release ordering.** The door ships in the same release this correction
68+
does: the runtime changeset that carries it (`action-confirmation-gate-enforced`)
69+
is still pending alongside this one, and one `changeset version` run consumes
70+
both. A release cut before this lands is the failure this card exists to end —
71+
the runtime refusing calls while the published spec text tells authors the flag
72+
stops nothing.
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+
`SelectOptionSchema`'s six row properties carry a JSON Schema `title`, so Studio's property panel stops printing raw machine keys as the column headers of a field's `options` table (#17506).
6+
7+
Clause-②: no
8+
9+
Studio renders a `type: 'repeater'` form field as a table whose column headers read `items.properties[k].title ?? k` off the JSON Schema derived from the metadata type schema. `SelectOptionSchema` carried no `title` on any row property, so the fallback arm ran and the maker saw `label` / `value` / `description` / `color` / `default` / `visibleWhen` inside an otherwise translated panel — **in every locale, English included**. Titles are hard-coded English by design: `system/translation.zod.ts` states that a row property renders from `items.properties[k].title`, and `resolveMetadataFormSchemaTitles` only ever REPLACES a title that is already there, so an untitled property has no layer for a translation to overlay.
10+
11+
- **One edit clears two carriers.** `field:options` and `object:fields.options` resolve to the *same* `SelectOptionSchema` object — `FieldSchema.options` is `z.array(SelectOptionSchema)` and `object.fields` is a `z.record(..., FieldSchema)` of that same `FieldSchema` — verified by object identity (`===`) against the schemas `getMetadataTypeSchema('field')` and `getMetadataTypeSchema('object')` actually return, with `FormSelectOptionSchema` as the firing control that the probe can tell two schemas apart. Both entries are deleted from the shrink-only `repeater-item-titles` ledger in the same change; `object.zod.ts` needed no edit.
12+
- **Nothing the schema accepts or refuses moved.** `.meta({ title })` is presentation metadata: the generated `authorable-surface/` artifacts are byte-identical, and the pinned accept/refuse suites for this shape (`editability-boundary`, `visible-when-alias-guidance`, `form-select-option`, `evaluated-slot-population`) pass unchanged.
13+
- **The form-view face inherits the titles for free.** `FormSelectOptionSchema` is a shape-level Omit that reuses the same property schema instances, so the five keys it keeps arrive titled too, and its `default`-refusal is untouched.

0 commit comments

Comments
 (0)