Skip to content

Commit de7dd45

Browse files
committed
Merge commit '00ff228fe' into claude/issue-14348-promote-authenticable-first-user
2 parents 9a9a5c0 + 00ff228 commit de7dd45

251 files changed

Lines changed: 18981 additions & 3471 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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
Startup `[action-governance]` resolves declarations through the same rungs the router does
6+
7+
The boot inventory built its declaration set from object-embedded `actions[]` plus the
8+
metadata service's `action` rows. `resolveRouteActionDeclaration` resolves through a third
9+
source between those two — the engine registry's standalone `action` items,
10+
`registry.getItem('action', name)`, accepted when the item owns the route. On the in-process
11+
boot (`new AppPlugin(...)` then `kernel.bootstrap()`), where the metadata plane holds no
12+
`action` rows at all, every object-less `defineAction` was therefore reported as a
13+
"registered handler with NO declaration — REFUSED at dispatch (ADR-0110 D3) and there is no
14+
opt-out" in the same boot in which the router resolved it at that rung and dispatched it.
15+
Both remedies the message offered were wrong for that shape: the action was already declared
16+
with `defineAction`, and dropping the registration would have broken a working endpoint under
17+
a green `pnpm validate`.
18+
19+
The registry rung is now injected into the audit by `ObjectQLPlugin` — the one caller holding
20+
the engine, because objectql cannot import the router — and judged by the same ownership test
21+
the router applies. The warning also stops asserting a dispatch outcome it never checked: it
22+
names the three sources it read, says it did not dispatch, and points an author whose action
23+
IS declared at the real bug instead of at deleting the registration. The other finding in the
24+
block, `declared script actions with NO handler`, is unchanged in wording and in population.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
lint: `dashboard-filter-field-unknown` resolves dotted dashboard-filter fields on the object graph, and answers system columns per object
6+
7+
A dashboard-level filter (`dateRange`, or a `globalFilters[]` entry) is ANDed into
8+
**every** widget's analytics query, so its effective field — after any
9+
`filterBindings` re-target — has to resolve on each bound widget's dataset object.
10+
The rule that enforces that shipped with two holes, and this closes both by
11+
migrating the check onto the shared `resolveFieldPath` / `joinablePrefixes` seam
12+
the widget's own `filter` keys already use one position over.
13+
14+
- **Dotted paths are no longer skipped.** The branch carried
15+
`if (field.includes('.')) continue;`, accurate when nothing in the package could
16+
walk relationship hops and false since the object-graph seam landed. A filter
17+
re-targeted to `account.signed_at` was unjudged whether or not `account` existed,
18+
whether or not `signed_at` existed on it, and whether or not `account` was
19+
declared in the dataset's `include`. It is now walked hop by hop, and a miss
20+
names **which** hop failed.
21+
- **System columns are resolved per object, not through the flat union.** The old
22+
test was `objectFields.has(field) || SYSTEM_FIELDS.has(field)`, which answers
23+
"could this be a system column *anywhere*". On an `ownership: 'none'` object the
24+
platform injects no `owner_id`, and on `systemFields: { audit: false }` no
25+
`created_at` — both were answered as resolvable and are now reported.
26+
27+
New error id **`dashboard-filter-field-not-included`**: the effective field
28+
resolves, but its relationship prefix is not declared in the bound dataset's
29+
`include`, so ADR-0021 compiles no join and the column is out of the broadcast
30+
query's reach. It mirrors `widget-filter-field-not-included` one level down, and is
31+
its own id because the fix is a different edit (declare the join, versus point the
32+
filter at something real).
33+
34+
This **narrows the accept set of a shipped gating rule**. Both new answers are
35+
error-tier, so like the rule's other errors they fail `os validate` / `os build`
36+
and the runtime publish gate for `dashboard` writes. Measured over the shipped
37+
dashboard corpus — the three example apps plus the platform's own
38+
`system_overview` — the change is 0 findings before and 0 after; the
39+
`dashboard-filter-field-unprovisioned` warning is unchanged and now travels with
40+
the verdict, so it answers a dotted path landing on an ADR-0015 `external` object
41+
too.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@objectstack/plugin-auth': minor
3+
'@objectstack/cli': patch
4+
---
5+
6+
Fix: `objectstack dev` now seeds its dev admin on a database that has PEOPLE but no LOGIN
7+
8+
An app that declares `sys_user` rows in `defineStack({ data })` used to lose the
9+
`objectstack dev` login permanently. The declarative seed is awaited inside
10+
`AppPlugin.start()`, so it always landed before the dev-admin seed's own
11+
`kernel:ready` hook; the seed's gate asked "does any human `sys_user` row
12+
exist?" and skipped — on that boot and on every later one, because the rows
13+
survive. The deployment ended up with no loginable account at all: people rows
14+
present, `sys_account` empty, `POST /auth/sign-in/email` returning 401.
15+
16+
A seeded person is a directory row with no credential. It is not a login, and
17+
the gate now says so: the seed acts while no account holds the configured seed
18+
address and no local password login (`sys_account.provider_id = 'credential'`)
19+
exists anywhere. "Never overwrites an existing account" is unchanged and now
20+
covers federated accounts on that address too. A credential store that cannot be
21+
read is its own verdict — the seed declines and says so, rather than minting a
22+
known-credential admin into an environment it could not see.
23+
24+
The seed's own provisioning call is now admitted as what it is — the
25+
deployment's own boot command provisioning its admin — instead of riding on the
26+
audience gate's zero-human bootstrap bypass, which the app's people seed also
27+
answers "populated". The public self-registration door is unchanged: nothing
28+
outside the process can stage that declaration, it names one address, and it is
29+
cleared as soon as the call returns.
30+
31+
`GET /api/v1/auth/bootstrap-status` now answers with the same bootstrap-window
32+
question the admission gate asks, instead of counting `sys_user` rows with no
33+
filter. On a database still carrying the legacy `usr_system` service row it used
34+
to report `hasOwner: true` while the admission gate and plugin-security's
35+
first-user detection both stood ready to admit and promote the first human — so
36+
the console withheld a first-run setup flow the platform would have accepted.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/driver-memory': minor
3+
---
4+
5+
feat(driver-memory): `update()` and `upsert()` publish their honest types (#13878)
6+
7+
**BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, the shape ADR-0087's 2026-08-30 addendum names (a published SDK method whose declared return moves off `any` onto the contract it always answered) — shipped as `minor` under the launch-window convention. The emitted `.d.ts` read `Promise<any>` for both doors: the return types were inferred through the backing store's `any[]` rows, so the union collapsed and no caller was asked to narrow. They are now declared as the contract declares them — `update()` returns `Promise<Record<string, unknown> | null>` (the `null` arm is the non-`strictMode` miss the driver has always answered with), `upsert()` returns `Promise<Record<string, unknown>>`. A caller that read fields off `update()`'s result through the `any` now narrows the `null` arm first; a caller that leaned on `any` to read undeclared members of either result now types them.
8+
9+
`upsert()` now asserts — throws on — the `null` arm of `update()` on a path it cannot reach (the row it updates was found in the same table a moment earlier, with nothing yielding in between), instead of widening its own declared return to carry an arm it can never produce. No reachable runtime behaviour changes.
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) A published return type moves off `any` onto the contract's own shape: no metadata key is removed, renamed or re-shaped and nothing exists for `objectstack migrate meta` to rewrite; the obligation is a TypeScript narrowing at the consumer's call site, delivered by the compiler. `type-surface-only` is not claimable here because the same diff touches `packages/spec/**` (the contract declaration itself). -->

.changeset/eighty-pumas-shave.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
Walk object-nested `list` / `listViews.*` through the view completeness rules.
6+
7+
`validateFunctionalCompleteness` walked only the top-level `views[]` containers, so
8+
a `timeline` / `gantt` / `map` / `tree` view authored on the object itself — the
9+
ADR-0017 "Object has-many View" spelling that `objects[].list` and
10+
`objects[].listViews.*` carry — never reached `checkViewCompleteness`. Both doors
11+
register the same expanded view items and reach the same renderer, so `os validate`
12+
and `os build` were silent on exactly the half of the stack the sibling rules
13+
(`lint-view-refs`, `validate-list-view-field-refs`) already walk.
14+
15+
Both authorable object spellings (array-form and name-keyed map) are covered, and a
16+
list view's own `data.object` retarget (ADR-0047) resolves the bound object the same
17+
way it does on the top-level door — so `view/layout-without-binding` and
18+
`view/tree-without-parent-field` now reach the nested door by construction rather
19+
than by a second wiring step. Findings report as
20+
`object "<name>" › listViews.<key>` / `objects[<i>].listViews.<key>.<block>`.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `ComponentPropsMap['element:number'].filter` converges onto the `ViewFilterRule` array form (#12039, objectui#6206 Option B)
6+
7+
<!-- adr-0087: registered element-number-filter-rule-array -->
8+
9+
**BREAKING** accept-set change on one props-map entry, shipped as `minor` under
10+
the repo's launch-window convention for breaking changes; the migration
11+
prescription is registered under protocol major 18.
12+
13+
One filter orthography platform-wide (maintainer batch adjudication
14+
2026-08-25, verbatim 「同意」, Option B): `element:number`'s `filter` was the
15+
one `filter` input in `ComponentPropsMap` declared as the MongoDB-style record
16+
(`FilterConditionSchema`) while every sibling (`record:related_list` and its
17+
Add-affordance picker) declared `z.array(ViewFilterRuleSchema)` — so the
18+
filter a list view stores and renders was refused by the KPI element beside
19+
it. The entry now declares the same array form its siblings do.
20+
21+
Sequenced consumer-first (the 2026-08-25 Option-A ordering ruling):
22+
objectui#6828 made `ObjectStackAdapter.aggregate()` lower a rule array through
23+
the same `translateFilterArray` its `find()` path runs before the analytics
24+
wire; the objectui pin carrying it (`d8ec8d6d`) was re-measured before this
25+
declaration moved, and the end-to-end chain is authored array → adapter
26+
lowering → filter AST → accepted by `lowerAnalyticsWhere` (which still refuses
27+
a RAW rule-object array — by design, and unchanged here).
28+
29+
Migration — FROM → TO, one rule per key (several keys AND):
30+
31+
```ts
32+
// before (refused now — `invalid_type` at `filter`, expected array)
33+
filter: { status: 'won' }
34+
filter: { amount: { $gt: 100 } }
35+
// after
36+
filter: [{ field: 'status', operator: 'equals', value: 'won' }]
37+
filter: [{ field: 'amount', operator: 'greater_than', value: 100 }]
38+
```
39+
40+
The ruled migration check ran with the change: the sweep of first-party
41+
corpora (examples/, skills/, create-objectstack, content/docs/, packages/apps/,
42+
spec fixtures) found one `element:number` author writing a record-form
43+
`filter` — a spec test fixture, rewritten to the array form here — and zero
44+
outside the spec package. `ElementNumberPropsParsed` is declared
45+
(ADR-0122: the parsed state now differs on `filter`, whose `operator` is
46+
normalized on parse). `element:record_picker.filter` is not changed here.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): a contained per-iteration failure is visible in the flow run contract — run-level `failed`, loop iteration through `try` / `catch`, row identity on `$error` (#13681, spec half)
6+
7+
The ruled containment spelling for a per-iteration failure that must not end
8+
the sweep is `loop { body: [ try_catch { try, catch } ] }` — ⛔ no
9+
`loop.config.onIterationError` key (maintainer 2026-08-31, verbatim
10+
「其他同意」; branch B selected by measurement: 5/5 iterations processed, the
11+
run completes). The ruling's rider is that such a caught failure is invisible
12+
at run level, and this change declares the three contract surfaces that close
13+
that gap. Additive only: nothing narrows, no existing key moves, no
14+
`BREAKING`.
15+
16+
- **`FlowRunSummary.failed`** — total node executions that failed, a fold of
17+
the existing per-node counter (`failed = Σ nodes[].failures`; that counter
18+
already exists and already counts a caught try-region attempt, so there is
19+
no second per-node key). On a run that completed, every one of them was
20+
contained — caught by a `try_catch` or routed down a `fault` edge — and the
21+
run went on. **Absent is not zero**, exactly as `unmeasured`: a run
22+
recorded before the field existed did not track it, and a parse leaves the
23+
key absent rather than defaulting it to `0`. It reads BESIDE the
24+
broken-sweep filter (`selected > 0 AND acted = 0 AND unmeasured = 0`), not
25+
inside it: `acted = 0` with `failed > 0` is a sweep whose writes were
26+
attempted and failed, not one that silently stopped.
27+
- **`ExecutionStepLog.iteration` through `try` / `catch`** — a step executed
28+
inside a try/catch region that is itself inside a loop body carries the
29+
ENCLOSING LOOP's `iteration` (a try/catch region has no index of its own)
30+
while `regionKind` stays `try` / `catch`. The schema already declared both
31+
keys; the contract now states how they combine, and pins it.
32+
- **`TryCatchErrorValueSchema`** (`@objectstack/spec/automation`, beside
33+
`TryCatchConfigSchema`, with `TryCatchErrorValue` /
34+
`TryCatchErrorValueParsed`) — the value a `try_catch` binds to
35+
`errorVariable` (default `$error`) was assembled by the engine and typed
36+
nowhere. It is now declared: `nodeId`, `message`, and — only when the
37+
failure happened inside a loop body — `iteration` (the enclosing loop's)
38+
and `item` (the loop item being processed, `unknown`). A plain object
39+
closed by convention: a value the engine assembles, not an authoring
40+
surface.
41+
42+
The engine half — populating `failed` in `summarizeRun`, propagating the loop
43+
iteration through `try_catch``runRegion`, binding `$error.iteration` /
44+
`$error.item`, and printing `failed=` on the run summary line when present —
45+
is the `domain:services` card that follows this contract.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/formula": patch
3+
---
4+
5+
fix(formula): a stdlib function written as a method gets the bare call shape, not the dialect (#14203)
6+
7+
`validateExpression` refused `record.name.upper()` correctly and then handed the
8+
author the generic dialect trailer — "`predicate`s are bare CEL (e.g.
9+
`record.rating >= 4`)" — advice that cannot succeed on a source that already IS
10+
bare CEL and parses fine. The third instance of the same defect family as the
11+
`bounds` class (#7073) and the unknown-name class (#13821), and the one neither
12+
of them could cover: #13821's arm fires only when the name is ABSENT from
13+
`CEL_STDLIB_FUNCTIONS`, and `upper` is present, so this class had no
14+
prescription at all. The name is right; the call SHAPE is wrong.
15+
16+
It is a high-frequency AI-author mistake, not an exotic one: method-call syntax
17+
is what almost every other language uses for string operations, so a generator
18+
that knows `upper` exists reaches for `record.name.upper()` before
19+
`upper(record.name)`. The remedy is one sentence and it is mechanical — the
20+
correct spelling is derivable from the fault itself:
21+
22+
```
23+
invalid CEL predicate: found no matching overload for 'dyn.upper()'
24+
25+
> 1 | record.name.upper()
26+
^ — `upper` is callable bare, not as a method — a CALL-SHAPE fault, not
27+
a dialect mistake, so re-spelling the expression will not fix it. Write
28+
`upper(record.name)` instead. The callable names this platform advertises for
29+
authoring (the `functions` list `introspectScope` returns,
30+
`CEL_STDLIB_FUNCTIONS`) take their subject as an argument; only cel-js's own
31+
receiver methods (`record.name.split(',')`) are written after a dot.
32+
```
33+
34+
The spelling is assembled from the SOURCE, because cel-js's message names the
35+
receiver's TYPE (`dyn.upper()`) and never the author's expression. When the
36+
receiver is not a plain dotted chain (`record.tags[0].upper()`,
37+
`(a + b).upper()`, `'lit'.upper()`) the message names the call shape —
38+
`upper(…)` with the receiver as its first argument — rather than inventing a
39+
spelling it cannot derive.
40+
41+
The arm is keyed on membership of the bare-callable catalog plus the
42+
environment's own record of the receiver form, never on the call shape alone.
43+
Two classes therefore keep exactly the behaviour they had:
44+
45+
- the 33 receiver-only names cel-js registers (`split`, `map`, `getFullYear`)
46+
are correct ONLY after a dot — `record.name.split(',')` type-checks and never
47+
reaches this arm;
48+
- the seven advertised names registered BOTH ways (`contains`, `endsWith`,
49+
`matches`, `size`, `startsWith`, `string`, `trim`) keep the existing trailer
50+
when a receiver call of them faults, because the fault there is the arguments
51+
(`record.name.contains()`), and a bare rewrite would fault just as hard.
52+
53+
No change to `CEL_STDLIB_FUNCTIONS`, to the registered environment, or to what
54+
`validateExpression` accepts: the receiver call was refused before this change
55+
and is refused after it. Only the sentence the author is told to act on changes.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os generate flow` scaffolds a flow `os validate` accepts (#14087)
6+
7+
The `flow` scaffold could not survive its own toolchain. Measured on 17.2.0,
8+
`os g flow my_flow` followed by `os validate` produced four refusals in one
9+
parse:
10+
11+
```
12+
flows[0].nodes[0].label expected string, received undefined
13+
flows[0].nodes[0] unrecognized key(s): `name`, `next`
14+
flows[0].edges expected array, received undefined
15+
flows[0] unrecognized key(s): `trigger`
16+
```
17+
18+
`FlowSchema` is `.strict()` and has never declared a top-level `trigger` on
19+
protocol 17. A record-change flow binds on the START node's `config`
20+
`{ objectName, triggerType, condition }` — which is where
21+
`AutomationEngine.resolveTriggerBinding` reads it from. The scaffold also wrote
22+
an `events: ['after_insert', 'after_update']` vocabulary that exists nowhere on
23+
the current surface, and named a single node it then pointed at a node it never
24+
emitted.
25+
26+
So the first flow anybody scaffolded was a file their own `os validate`
27+
rejected — against a `.strict()` error enumerating what is allowed rather than
28+
saying where the trigger had moved to.
29+
30+
The template now emits the shape the schema accepts and the engine binds:
31+
`type: 'record_change'`, a labelled START node carrying
32+
`config: { objectName, triggerType: 'record-after-write' }`, a labelled END
33+
node, and the `edges` array joining them. `status` stays `'draft'` — the arming
34+
decision is the author's, and `os validate` says so.
35+
36+
`generate-scaffold-validates.test.ts` puts every generator's output through the
37+
two steps `os validate` runs on an authored stack (schema parse, then the
38+
author-time rule registry), loaded through the same `bundle-require` path
39+
`loadConfig` uses, so the generator and the schema cannot drift apart again
40+
silently. Both layers are needed: a flow node's `config` is an open slot
41+
(ADR-0018), so the schema cannot judge the trigger vocabulary at all — the
42+
`record-*` grammar is held by `validate-flow-trigger-readiness` one layer
43+
later.
44+
45+
No other generator's output changed. Four of them (`object`, `view`, `action`,
46+
`app`) are refused for unrelated reasons of their own; the new test records
47+
them in a shrink-only ledger that fails when one is repaired and its entry is
48+
left behind.

0 commit comments

Comments
 (0)