Skip to content

Commit dd2685a

Browse files
os-justinclaude
andcommitted
Merge origin/main into claude/issue-14556-list-view-grouping-contract
Takes main's deletion of skills/objectstack-ui/contracts/react-blocks.contract.json (the react-blocks generator now emits one artifact, references/react-blocks.md); the regenerated artifacts follow in the next commit, after the merge is committed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
2 parents 880d45d + 0e24b0c commit dd2685a

79 files changed

Lines changed: 5682 additions & 1090 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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `BulkDataEvent` names the one organization a predicate write's affected records belong to
6+
7+
The realtime `BulkDataEvent` payload (`@objectstack/spec/api`, the body of every
8+
`data.records.updated` / `data.records.deleted` event) gains an optional
9+
`organizationId`: the organization every record the predicate write affected
10+
belongs to — one organization for the whole batch, never per-row and never a
11+
list. It takes the same spelling, the same position beside the match term
12+
`object`, and the same refusal of the empty string as `DataEvent`'s
13+
`organizationId`, so a tenant-scoped consumer discriminates both event families
14+
on one key with one comparison — never a partition of the batch.
15+
16+
Why one organization can be honest on a batch that names no rows: a predicate
17+
write reaches the driver with the security layer's tenant wall AND-composed
18+
onto the caller's filter first (under `isolated` an equality on the caller's
19+
active organization; under `group` membership in the caller's organization
20+
set), and nothing in business RLS or sharing can widen it. When that wall names
21+
exactly one organization, every affected row belongs to it, and the producer
22+
can state so from what it already holds.
23+
24+
What a consumer may assume — and where this deliberately diverges from
25+
`DataEvent`:
26+
27+
- **Present** — every record the write affected belongs to exactly that
28+
organization. Never fabricated, and never the caller's active organization
29+
standing in for the rows'.
30+
- **Absent** — the producer did not assert one organization for the batch: every
31+
event on a `single`-posture deployment; a system, environment-wide or
32+
cross-membership predicate write; any write whose affected rows are not known
33+
to belong to one organization. A bulk event names no rows, so absence is a
34+
statement about the producer's knowledge, not about the rows. It is NOT
35+
`DataEvent`'s reading "belongs to no organization, not behind any wall". A
36+
tenant-scoped consumer (a per-organization webhook subscription, a
37+
per-organization realtime subscriber) must treat an absent key as not
38+
attributable to its organization and must not deliver the event inside an
39+
organization wall; a deployment-wide consumer may use it.
40+
41+
Declared = enforced: the key is optional and nothing else. No default
42+
fabricates a tenant; `null` and the empty string are refused with a located
43+
issue, so "not asserted" has exactly one spelling — the key is absent.
44+
45+
Additive and shape-preserving: every bulk event that parsed before parses
46+
identically, and no producer emits the key yet — the ObjectQL engine's bulk
47+
publish site is a separate change that follows this contract. `DataEvent` and
48+
`MetadataEvent` are unchanged.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): a published `DataEvent` now names the organization the RECORD belongs to
6+
7+
`DataEventSchema.organizationId` has been declared and published since the spec
8+
half landed, and its TSDoc states the obligation on the producer's side: *"a
9+
producer that omits the key on an organization-stamped row publishes a
10+
cross-tenant event, which is fixed at the publish site — never by a
11+
consumer-side lookup."* The engine populated it on no event at all. Every
12+
`data.record.created` / `updated` / `deleted` went out with the key absent,
13+
which a consumer is required to read as *"this record is behind no organization
14+
wall"* — so an organization-stamped row was published as an unwalled one, and a
15+
tenant-scoped fan-out had nothing to discriminate on.
16+
17+
`publishDataEvent` now resolves the organization from the row itself and spreads
18+
the key in when there is one. The row is already in hand at all three call
19+
sites — the written record on `created`, the post-state on `updated`, and the
20+
pre-image on `deleted` (the by-id branch reads it unconditionally for its
21+
existence gate) — so this buys **no** per-event read: the key exists precisely
22+
to keep a per-event lookup off the fan-out path.
23+
24+
Three properties are deliberate:
25+
26+
- **The RECORD's organization, never the caller's.** The row's own tenant column
27+
is the only source consulted. `ExecutionContext.tenantId` is the caller's
28+
*active* organization; the two coincide on an ordinary tenant write and
29+
diverge on a system or unscoped one, where substituting it would mislabel an
30+
administrator's write into another organization as belonging to the
31+
administrator's.
32+
- **Absence has exactly one spelling: the key is omitted.** An object that is
33+
not tenant-scoped, a row whose column is empty, and a value no id can be read
34+
off all publish the key absent rather than `null`, `''` or an explicit
35+
`undefined`. The schema refuses the empty string outright, so producing one
36+
would have thrown at the publish site and dropped the event entirely.
37+
- **The column is resolved the way the write path resolves it** — the
38+
`tenancy.enabled: false` opt-out, then a declared `tenancy.tenantField`, then
39+
the injected `organization_id` — so the event cannot name an organization for
40+
a column the engine does not actually scope by. Note the two spellings differ:
41+
the column is `organization_id`, the published key is `organizationId`.
42+
43+
No schema, no accepted shape and no public export moves: the key was already
44+
declared, already validated and already part of what consumers parse. Only the
45+
implementation changed, from omitting a declared key to populating it.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: `ExecutionStepLog.iteration` is single-valued — the enclosing loop iteration — and the parallel branch index moves to a new optional `branch` key (#14414)
6+
7+
<!-- adr-0087: registered execution-step-iteration-single-valued -->
8+
9+
**BREAKING** for any consumer that read `iteration` as the parallel branch index
10+
on a `regionKind: 'parallel-branch'` step record: that number now means the
11+
enclosing `loop`'s iteration, and the branch index lives on `branch`. Shipped as
12+
`minor` under the repo's launch-window convention for breaking changes; the
13+
hand-migration prescription is registered under protocol major 18. Maintainer
14+
ruling 2026-09-03 on #14414 (director decision batch #15, verbatim 「同意」):
15+
option A.
16+
17+
`ExecutionStepLogSchema.iteration` was declared as "zero-based loop iteration
18+
OR parallel branch index of the enclosing region" — one field, two meanings,
19+
told apart only by reading `regionKind` first. For `loop { body: [ parallel {
20+
branches } ] }` the engine tagged each branch step with the branch index and no
21+
step of that branch with the loop iteration, so a per-row failure inside a
22+
branch was attributable to a branch, never to the row. The sibling `try_catch`
23+
rule (a try/catch region has no index of its own, so it carries the loop
24+
iteration) had deliberately left `parallel` open, because a parallel region
25+
DOES have an index of its own.
26+
27+
**What changes on the record shape** (`packages/spec/src/automation/execution.zod.ts`):
28+
29+
- `iteration` — single-valued: the zero-based iteration of the enclosing
30+
`loop`, carried through any nesting (`try` / `catch` already carried it;
31+
`parallel-branch` now does too). The try/catch sentence is unchanged.
32+
- `branch`**new**, optional, `integer >= 0`: the zero-based index of the
33+
enclosing `parallel` branch, present only on a step inside a parallel
34+
branch. A branch step of a parallel node inside a loop body carries both.
35+
- `regionKind` — unchanged vocabulary; its describe now points the
36+
`parallel-branch` index at `branch`.
37+
38+
**What does NOT change in this PR:** the engine tagger in
39+
`@objectstack/service-automation` still writes the innermost region only —
40+
today it writes the branch index into `iteration` on `parallel-branch` steps
41+
and never writes `branch`. The tagger change is a follow-on card in the same
42+
lane, blocked by this one (three-surface rule: spec first, no engine-only patch
43+
in between). A step record written by an engine that predates that follow-on
44+
therefore carries no `branch` key, and its `iteration` under `parallel-branch`
45+
still holds the legacy branch index.
46+
47+
## FROM → TO
48+
49+
```ts
50+
// before — one key, two meanings; the loop iteration of a branch step is lost
51+
const step = { regionKind: 'parallel-branch', iteration: 1 }; // 1 = branch index
52+
const branchIndex = step.regionKind === 'parallel-branch' ? step.iteration : undefined;
53+
54+
// after — one meaning per key; a branch step inside a loop body carries both
55+
const step = { regionKind: 'parallel-branch', iteration: 3, branch: 1 };
56+
const branchIndex = step.branch; // 1 — the branch
57+
const rowIteration = step.iteration; // 3 — the enclosing loop's row
58+
```
59+
60+
Fix: where a consumer groups or labels `parallel-branch` steps by `iteration`
61+
(the objectui `FlowRunsPanel` grouping key is the one measured reader), read
62+
`branch` for the branch index and keep `iteration` for the row. Reads of
63+
`iteration` on `loop-body`, `try` and `catch` steps need no change.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
chore(spec): govern the realtime `SubscriptionSchema` surface in the liveness ledger (#14446)
6+
7+
The `liveness/` ledgers ship inside this package's npm tarball (they are named in
8+
`files`), so this is a published-data change even though no runtime behaviour moves,
9+
no schema key changes spelling, and `packages/spec/src/api/realtime.zod.ts` is not
10+
edited at all.
11+
12+
A new ledger file — `realtime_subscription.json` — classifies all six authorable
13+
properties of `SubscriptionSchema`, what a client declares to open a realtime
14+
subscription: the item type of `RealtimeConfigSchema.subscriptions` and the
15+
`Subscription` the generated API reference publishes. It is enrolled through the
16+
gate's `SPEC_ONLY_SCHEMAS` override, the route `query` / `qa` / `manifest` and the
17+
four `RestServerConfig` sub-objects already take. A transport-protocol surface is
18+
neither a metadata item nor stored metadata nor a manifest, so no registry has ever
19+
held it and no ratchet rooted in one could ask who reads it — and
20+
`RealtimeConfigSchema` is `.passthrough()`, so nothing downstream even refuses an
21+
unknown key.
22+
23+
All six are `dead`, and the container is the finding: nothing outside
24+
`packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or
25+
`RealtimeConfigSchema` at all, so no key beneath them can be read. The two the
26+
census filed with this card measured are the sharp ones. `events[].type` accepts
27+
`RealtimeEventType`, whose four members (`record.created` / `record.updated` /
28+
`record.deleted` / `field.changed`) are disjoint from what the engine publishes —
29+
`DataEventType`'s `data.record.*`, with a live emitter in `service-knowledge` — so
30+
an author who writes the enum's own `record.created` gets a subscription that
31+
silently never fires, and the enum is what the API reference shows them.
32+
`events[].filters` is `z.unknown().optional()`: an authorable key with no shape and
33+
no consumer, failing in the permissive direction, since the only payload matching
34+
the platform performs compares object name and event type.
35+
36+
What this records, and what it deliberately does not. The enum's direction is
37+
settled and the row carries the 2026-09-02 triage ruling verbatim so the next reader
38+
does not re-open it: if the verdict is enforce it means repointing the enum, never
39+
changing what the runtime publishes, which would break a live event contract to
40+
satisfy a member nothing has ever used. `field.changed` is the same spelling the
41+
sibling `DataEventType` removed in 17.0.0 under ADR-0049 (#4673, PR #4685) for
42+
having no producer; it survives here only because this enum was never in a ratchet's
43+
denominator. No key is removed, enforced, deprecated or re-described here — the
44+
enforce-or-remove call per dead key is a follow-up on the human floor.
45+
46+
Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema`, which is
47+
measurement rather than taste: the ledger walk drills exactly one level, so with the
48+
config as the root `subscriptions` would be the drilled level and `events[].type` /
49+
`events[].filters` would have no row of their own, inheriting a container verdict —
50+
the same reasoning that rooted the four `RestServerConfig` sub-objects separately.
51+
`RealtimeConfigSchema`'s own three keys are not enrolled: whether enabling realtime
52+
does anything is a different question with no census behind it yet.
53+
54+
One correction the ledger records because the next reader will hit it too. The card
55+
and its triage both name the two keys on `SubscriptionSchema`; they are declared on
56+
`SubscriptionEventSchema` (`realtime.zod.ts:46-50`), reached from this root as
57+
`events[].type` and `events[].filters`. The cited lines and quoted shapes are exact;
58+
only the owning symbol was misattributed, and this package's own authorable-surface
59+
census (`authorable-surface/api.json`) already spells the two schemas apart.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): the packages domain reaches the `protocol` service through a typed handle (#13598)
6+
7+
`deps.resolveService(context, 'protocol')` answers `any``protocol` is
8+
deliberately left unmapped in `ServiceSlotContracts` — so every request literal
9+
downstream of that seam compiled against nothing. Twelve sites in
10+
`domains/packages.ts` held that `any` (two of them on the variable declaration
11+
rather than the call), and an undeclared or misspelt key in the ADR-0045
12+
publish-visibility flip's `getMetaItems` / `saveMetaItem` literals compiled
13+
silently. Measured on the base tree: injecting `bogusUndeclaredKey: true` into
14+
the `saveMetaItem` literal gave `tsc --noEmit` exit 0 and zero diagnostics.
15+
16+
The slot is now narrowed once, at one helper, to a handle `Pick`ed from the
17+
DECLARED contracts — `MetadataProtocol` / `PackageProtocol` from
18+
`@objectstack/spec`, plus the producer's own exported `DeletePackageRequest`
19+
so the same injection is now `error TS2353`. Every member is OPTIONAL and every
20+
`typeof protocol.<verb> === 'function'` capability probe is unchanged: a host
21+
may occupy the slot with a partial object, and the type answers "is this key
22+
declared?" while the probe still answers "did this host bring the verb?".
23+
24+
Compile-layer signal only — no request is newly accepted or refused, no
25+
response shape moves, and the eight verbs no contract declares keep an explicit
26+
`any` request rather than a private restatement nothing verifies.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
fix(service-automation): put the test layer in front of tsc, and repair the TS2341 x3 it was hiding (#15048)
6+
7+
`packages/services/service-automation` had **no `typecheck` script at all**
8+
its scripts were `build` and `test` — so no tsc program anywhere read this
9+
package (`turbo run typecheck` selects only packages that declare the task, so
10+
it skipped this one silently). `tsup` transpiles with esbuild and `vitest`
11+
runs through esbuild type-**stripping**; neither type-checks. The package's
12+
own `tsconfig.json` does include the tests and always did, so the program that
13+
would have read them already existed and was simply never invoked. This is
14+
the `packages/services/**` sibling of `@objectstack/service-cluster`'s same
15+
graduation (#14181 / PR #15032), reached by the same road in.
16+
17+
What that hid was three `TS2341`s, all in
18+
`src/nested-region-parity.test.ts` (lines 95/151/180):
19+
20+
```
21+
error TS2341: Property 'flows' is private and only accessible within class 'AutomationEngine'.
22+
```
23+
24+
Three tests dot-read the private `AutomationEngine#flows` map directly
25+
instead of going through the class's own public accessor,
26+
`await engine.getFlow(name)` — already the idiom every other test file in
27+
this package uses. The fix replaces the three private reads with that
28+
existing public call (making the two synchronous test bodies `async` where
29+
they were not already); no source signature was widened, no cast was added.
30+
31+
Wired by the route the `packages/plugins/**` family settled on in #14062 and
32+
`service-cluster` carried into `packages/services/**` in #14181: a sibling
33+
`tsconfig.test.json` that changes **module semantics only** (`esnext` /
34+
`bundler` / `lib: ES2022`, matching how vitest actually executes these files)
35+
with **strictness inherited and untouched**, named by a new `typecheck`
36+
script through the shared `check:test-typecheck` gate. Measured before the
37+
repair: 3 errors under build semantics (`tsc -p tsconfig.json`, which already
38+
included the tests), 3 under the new config — the two readings agree, so this
39+
package carried no config-tier pile, and all 3 were genuinely code-tier from
40+
the start. After: 0 and 0, across a 555-file program covering all 103 of its
41+
`src/**/*.test.ts`.
42+
43+
No `test-typecheck-debt.json` is added, and its **absence is the zero**: the
44+
gate reads a missing ledger as `{ entries: {} }`, under which any error in any
45+
file here is red immediately. The package's `DEBT` entry in
46+
`scripts/check-type-check-coverage.mjs` (`errors: 3`) is deleted in this PR
47+
rather than lowered — that is the graduation the ratchet's own invariant
48+
requires, and it is why the errors were fixed rather than ledgered.
49+
50+
`scripts/check-type-source-resolution.mjs` also gains a registry entry for
51+
this package: onboarding `tsconfig.test.json` moved the package's tsc program
52+
set (per that gate's documented onboarding-limb terms), exposing 9 workspace
53+
deps whose types resolve through `dist/` with no pre-existing program for them
54+
to have been laundered through. `paths` was measured and rejected as the
55+
alternative — it takes this package's test layer from 0 errors to 648, nearly
56+
all billed to other packages' source.
57+
58+
No runtime code changes: `src/**` (excluding the one edited test file, whose
59+
own assertions are unchanged — only how it reaches the flow moved) is
60+
otherwise byte-identical, so no shipped behaviour moves. The `patch` level
61+
reflects the published `package.json` gaining `typecheck` /
62+
`check:test-typecheck` scripts and a `tsx` devDependency.
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+
docs(spec): `PageTranslation.components` now says why `element:text`'s `content` is not one of its keys (#14412)
6+
7+
The per-component translation face names its deliberate exclusions with reasons — `help` because no component in the model declares it, `subtitle` because `page:header` is already addressed by page name. `content` was neither named nor excluded. An author looking for a bundle key for the one string `element:text` renders therefore found an absence, and an absence reads exactly like an oversight.
8+
9+
It is not one, and the schema comment now records that beside the other two. `element:text` declares `content: I18nLabelSchema` (`ui/component.zod.ts`), so the string is localizable at its own authoring site as an inline `{ en, 'zh-CN' }` locale map — the route `sys-user.page.ts` itself uses. Adding it to the bundle face would be the face widening the `submitLabel` retirement declined for the identical shape (#10926, ADR-0049).
10+
11+
No key was added and no behaviour changed: the bundle face is still `title` / `description` / `label` / `placeholder` / `emptyText`, and `translatePage` resolves exactly what it resolved before. Bundles, extractor output and existing page definitions are unaffected.
12+
13+
Known and tracked separately: inline locale maps are invisible to `os i18n extract` and `check:i18n-coverage`, so page prose written this way is not counted by coverage tooling. That is true of every inline `I18nLabel` field rather than this component alone, and is carried as #14749.

0 commit comments

Comments
 (0)