Skip to content

Commit 499c479

Browse files
committed
Merge branch 'main' into claude/issue-19011-revert-declaration-text-snapshot
Resolves 13 modify/delete conflicts under packages/spec/api-surface-declarations/. Every conflict has the same shape: this branch deletes the file (no stage 2), main regenerated it (stage 3). Retiring that directory is the revert's whole purpose, so each conflict resolves to the delete. All 17 shards are gone from the merged tree -- the 4 main did not touch auto-resolved to delete already. The one other overlapping path, scripts/pm/dispatch-gates.mjs, auto-merged: main's hunk sits about 1600 lines from the reverted one. Verified on the merged tree rather than assumed: - no code, script, workflow, gitattributes or package.json entry references api-surface-declarations in any spelling; the only three mentions left are historical prose in .changeset release notes (17108, 18991, 19085), reported separately and deliberately not edited here. - of the 31 paths the reverted commit touched, none still carries a line that commit added; the four that differ from its parent are later, unrelated work main landed (lint.yml keeps #18889's step; check-published-files, dispatch-gates and regen-artifacts carry post-revert commits). - api-surface-signatures.json is back with its 27 hashes and, built from these merged sources, check:api-surface reports the public API surface and factory signatures unchanged -- so the restored pin is correct, not merely present. - check:generated reports all 15 artifacts up to date; main's count is 16, and 16 is what #18971 made it when it registered check:api-surface-declarations. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-Authored-By: Claude <noreply@anthropic.com>
2 parents b6dea32 + 15f9284 commit 499c479

495 files changed

Lines changed: 36381 additions & 3550 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 -->
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/formula": minor
4+
---
5+
6+
feat(spec)!: every engine-evaluated expression slot requires a non-blank `source` — the #15430 rule generalised from the flow-node ledger to the other 36 declaring positions (#15811, decision batch #122 item 2)
7+
8+
<!-- adr-0087: registered evaluated-expression-slots-source-required -->
9+
10+
**BREAKING** accept-set narrowing on 36 published metadata slots. Each of them
11+
composed `ExpressionInputSchema` and now composes `EvaluatedExpressionInputSchema`,
12+
so an envelope carrying only `ast` (`{ dialect: 'cel', ast: … }` with no `source`)
13+
and a `source` that is blank after trimming — through the envelope key or through
14+
the bare-string shorthand — are refused at the door instead of parsing and then
15+
faulting at run time. The prescription is registered under protocol major 18 as
16+
the semantic migration `evaluated-expression-slots-source-required`.
17+
18+
**⚠️ Graded `minor`, not `major`, and the ruling said `major`.** Decision batch
19+
#122 item 3 ordered a 「`major` changeset」. This repo's launch-window convention
20+
ships breaking changes as `minor` while the fixed group versions in lockstep, and
21+
`scripts/check-changeset-no-major.mjs` enforces it: a `major` marker here would
22+
promote all ~70 packages to a whole-stack major release, which is a release act.
23+
The convention's own written carriers for breaking-ness are used instead and both
24+
are present — this **BREAKING** banner and the ADR-0087 disposition above. The
25+
ruling's substance (a breaking narrowing, carried by an ADR-0087 semantic
26+
migration entry) is delivered; only the marker differs, and it differs because a
27+
repo gate forbids the marker.
28+
29+
**What is NOT narrowed.** `ExpressionSchema` / `ExpressionInputSchema` remain the
30+
persistence contract (`source` OR `ast`), by item 2 of the same ruling, and so
31+
does `PredicateInputSchema`, which is a plain alias of the latter. A slot that
32+
only PERSISTS an envelope is untouched; the narrowing is at the slots an engine
33+
EVALUATES. An `ast` carried BESIDE a string `source` stays admitted everywhere.
34+
35+
**The population was re-derived, not inherited.** By identity — a negative
36+
lookaround on identifier characters, so `CronExpressionInputSchema` and
37+
`TemplateExpressionInputSchema` cannot leak in as substrings — over
38+
`packages/spec/src`, non-test: 34 declaring source lines, two of which are
39+
file-local alias consts (`ui/action.zod.ts` `ActionConditionInputSchema`,
40+
`system/settings-manifest.zod.ts` `SettingsVisibilityInputSchema`) that mount two
41+
slots each, giving **36 declaring positions**. Three of them reach the schema as a
42+
union member rather than head-of-declaration (`RecordAlertProps.visible`,
43+
`ServiceLevelIndicator.successCriteria`, `TraceSamplingConfig.composite[].condition`).
44+
45+
On **two of those three the sibling arm is untouched**: `RecordAlertProps.visible`
46+
still takes a boolean literal, and `ServiceLevelIndicator.successCriteria` still
47+
takes its structured `{ threshold, operator, percentile? }` object — including one
48+
that happens to carry a `dialect` key.
49+
50+
⚠️ **On the third, `TraceSamplingConfig.composite[].condition`, the sibling arm
51+
narrows too, and deliberately.** Its structured-filter arm is a bare
52+
`z.record(z.string(), z.unknown())`, which accepted `{ dialect: 'cel', ast }` as an
53+
ordinary filter — so swapping the expression arm changed nothing at all there. That
54+
arm now declines any object carrying a `dialect` key, and six shapes the base
55+
accepted THROUGH THAT ARM ALONE (measured: the base's `ExpressionInputSchema`
56+
refused every one of them) are refused at this slot:
57+
58+
| authored `condition` | base | now |
59+
|---|---|---|
60+
| `{ dialect: 'cel' }` | accepted | refused |
61+
| `{ dialect: 'js', source: 'x' }` | accepted | refused |
62+
| `{ dialect: 'nope', source: 'x' }` | accepted | refused |
63+
| `{ dialect: 'cel', source: 5 }` | accepted | refused |
64+
| `{ dialect: 'cel', source: 'x', meta: { rationale: 5 } }` | accepted | refused |
65+
| `{ dialect: 'zzz', foo: 1 }` | accepted | refused |
66+
67+
FROM → TO at that slot: if the value really is a **structured filter**, drop the
68+
`dialect` key (`{ dialect: 'cel', service: 'api' }` → `{ service: 'api' }`); if it is
69+
an **expression**, give it a dialect this platform evaluates and a non-blank `source`
70+
(`{ dialect: 'js', source: 'x' }` → `{ dialect: 'cel', source: 'x' }`). A structured
71+
filter that carries no `dialect` key — `{}`, `{ service: 'api' }`,
72+
`{ attributes: { 'http.route': '/v1/orders' } }` — is accepted exactly as before.
73+
74+
**Why an authoring-time refusal and not a run-time one.** Measured at the
75+
chokepoint, `celEngine.evaluate` never silently succeeds on either shape — it
76+
returns a `parse` fault — so what happened next was decided entirely by the
77+
slot's fail policy, and the two halves of that population fail in opposite
78+
directions: fail-CLOSED slots (`ObjectFieldGroup.visibleWhen`,
79+
`RowCrudActionOverride.visibleWhen`, `BulkActionDef.visible`, the two
80+
settings-manifest `visible` slots) hid a group, a row button, or silently excluded
81+
every selected record from a bulk run and reported them as *skipped*; fail-SOFT
82+
slots left a gate that had stopped gating. Nothing in between said a word: the
83+
authoring lint `validateVisibilityPredicates` measured 0 findings on an `ast`-only
84+
envelope and 0 on a blank `source`, against two control legs that each measured 1.
85+
86+
**`@objectstack/formula` gains `printCelAst(ast)`** — the inverse of
87+
`parseCelToAst`, and the lossless half of the migration: an `ast`-only CEL
88+
envelope is printed back to surface syntax mechanically, with no judgment asked of
89+
the author. It is lossless about MEANING, not bytes (the printer re-renders from
90+
the parse tree, so `'x'` comes back as `"x"`), and it answers `null` — never a
91+
guess — for anything it cannot round-trip through the platform's own bounded
92+
parser. That `null`, and every blank `source`, are what the semantic migration
93+
entry's structured TODO covers.
94+
95+
**The published TypeScript interface `RowCrudPredicates` narrows with it**
96+
(`Expression | ExpressionInput` → `EvaluatedExpression | EvaluatedExpressionInput`),
97+
because it mirrors the two `RowCrudActionOverride` slots and a type that still
98+
promised an `ast`-only envelope would advertise what the schema now refuses.
99+
100+
**So do the four expression constructors — `expression()`, `cel`, `tmpl`, `cron`
101+
(and therefore the `F` / `P` aliases) — which now return `EvaluatedExpression`
102+
instead of `Expression`.** Each one assigns a `string` to `source`
103+
unconditionally, so the wider return type described none of them; it was slop
104+
that cost nothing until an evaluated slot began requiring `source`, at which
105+
point ``visibleWhen: P`…` `` — the spelling the spec's own docblock teaches —
106+
stopped type-checking, and `@objectstack/platform-objects` failed its DTS build
107+
on exactly that. `EvaluatedExpression` is assignable to `Expression`, so every
108+
persistence-contract slot keeps accepting these values unchanged; what the
109+
narrower return type adds is that an evaluated slot accepts them too. An author
110+
who genuinely has no `source` was never calling these constructors — an
111+
`ast`-only envelope is an object literal, and an evaluated slot refuses it on
112+
purpose.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: `CronSchedule.timezone` is judged by the `iana_time_zone` membership predicate (#16292)
6+
7+
**BREAKING** — an accept-set narrowing on a published authoring key.
8+
`CronScheduleSchema.timezone` was a bare `z.string().optional().default('UTC')`, so
9+
`defineJob` and `JobSchema.parse` took `timezone: 'UTC+8'` at authoring and build time
10+
and said nothing. It is now judged by `isValueDomainMember('iana_time_zone', …)` — the
11+
predicate `@objectstack/spec/shared` already exports, and the same judge the four
12+
`valueDomain: 'iana_time_zone'` columns (`sys_business_unit.timezone`,
13+
`sys_organization.timezone`, `sys_job.timezone`, `sys_report_schedule.timezone`) are
14+
written against. Shipped as `minor` under the repo's launch-window convention for
15+
accept-set narrowings.
16+
17+
No job that ran yesterday stops running. The value was already carried unchanged to
18+
`CronJobAdapter.schedule`, where croner — constructed with a callback — throws on a
19+
non-member and `AppPlugin` records a per-job `FAILED TO SCHEDULE` at `error` level plus
20+
a `jobScheduleFailuresTotal` increment: the job was declared and never ran. What moves
21+
is WHEN its author is told, from the first environment that boots to `defineJob` /
22+
`os build`. So a stack whose job carries a zone the platform cannot honour now stops
23+
building instead of booting-and-not-running.
24+
25+
Membership is the `Intl.DateTimeFormat` probe rather than a checked-in list, so the
26+
accepted set is the host's own tz database — deliberately, and identically to those four
27+
columns, the settings door and `resolveAuthzContext`. It is what every `Intl`-based
28+
consumer downstream accepts, so the parse-time answer and the schedule-time answer
29+
cannot disagree on one host. `UTC`, the key's own declared default, is a member on every
30+
conforming runtime, so an omitted key is untouched.
31+
32+
`interval` and `once` schedules carry no zone and are unaffected. The boundary type
33+
`JobSchedule.timezone` on `@objectstack/spec/contracts` is a third, separate door and is
34+
deliberately left out of this change.
35+
36+
Clause-②: yes (narrowing)
37+
38+
<!-- adr-0087: not-required (no-migration-prescription) No key, export, config field or stored shape is added, removed, renamed or re-spelled, and no metadata document has to be rewritten into a different one. The narrowing is value-level and undecidable in the upgrade direction: an offset spelling such as `UTC+8` names no zone at all, so nothing can derive whether its author meant `Asia/Shanghai`, `Asia/Singapore` or `Australia/Perth` — the answer is a fact about the deployment, never about the refused string. `objectstack migrate meta` therefore has nothing mechanical it could apply, and a ledger row would carry an empty mapping. The sibling column-tier narrowing of the same concept is already recorded as `platform-timezone-columns-iana-domain-refused`; this authoring-tier door is a different door and is deliberately not filed under that id. -->
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)

0 commit comments

Comments
 (0)