Skip to content

Commit 7536bfc

Browse files
committed
Merge origin/main into claude/issue-14704-single-door-sandbox-wrapper
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
2 parents b219e43 + 0fb944b commit 7536bfc

44 files changed

Lines changed: 2049 additions & 262 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/service-analytics": minor
4+
---
5+
6+
feat(spec,analytics): `AnalyticsResult.fields[].builtinAggregate` — a closed discriminator for a measure column whose display name is the server's built-in default (#14492)
7+
8+
**What a consumer sees.** `queryDataset()` (and `POST /api/v1/analytics/dataset/query`,
9+
which relays the result verbatim) now carries an optional
10+
`fields[].builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'`
11+
on a measure column. It is present exactly when the dataset measure behind the
12+
column declares an `aggregate` and **no** `label` — the producer then has nothing
13+
but the aggregate to name the column by, so it says which aggregate that is. It is
14+
absent whenever the author declared a label (a plain string or an inline locale
15+
map, even one with no entry for the request locale: an author's text is never
16+
re-labelled by a consumer), and absent on dimension columns and derived measures.
17+
The vocabulary is `AggregationFunction` (`data/query.zod.ts`), the one closed
18+
aggregate enum — no second spelling. `AnalyticsResultResponseSchema`
19+
(`api/analytics.zod.ts`) mirrors the member, refusing a spelling outside the enum.
20+
21+
**Why.** An AI-built dashboard's "count of customers by status" chart showed the
22+
English axis title "Count" on a Chinese UI. The renderer (objectui
23+
`buildChartSeries()` / `labelOf()`) treats `fields[].label` as resolved author
24+
content and passes it through verbatim — correctly, since a real custom label
25+
("Tasks") must survive. What it could not tell apart was an author's text from
26+
the server's built-in default for a bare `count`. Guessing from the label text
27+
was refused (it would catch an author who really named a field `Count`, and break
28+
the moment the default is spelled in another language); translating on the
29+
server was not taken (it copies the front end's language decision into the
30+
producer and leaves nothing for a per-widget override). The ruling (2026-09-02,
31+
option B) is a structured discriminator on the contract: the consumer prefers a
32+
locale lookup keyed by `builtinAggregate` — mirroring its existing
33+
`report.aggregate.*` keys — and falls back to `label`, then `name`.
34+
35+
**Producer-side changes.**
36+
37+
- `@objectstack/service-analytics``queryDataset`'s measure enrichment sets
38+
`builtinAggregate` from the dataset measure's own `aggregate` when the measure
39+
has no authored `label`. Judged on the authored key, never on the resolved
40+
string.
41+
- `@objectstack/spec` — the `dataset` create seed (`metadata-create-seeds.ts`)
42+
drops its hardcoded `label: 'Count'` from the seeded `count` measure, so a
43+
dataset created from Studio is a built-in default (wire: `builtinAggregate:
44+
'count'`) instead of an authored English literal. `getMeta()` for such a
45+
dataset now titles the metric by its name (`count`) rather than `Count`;
46+
`CubeMeta.measures[].type` already carried the aggregate there.
47+
48+
Purely additive: no key is removed or renamed, no authorable schema changes shape,
49+
and a consumer that ignores the member sees exactly the response it saw before.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): `composeStacks` carries each bound standalone action once in the composed object — the second merge no longer re-appends the copy each input's build already made (#14847)
6+
7+
`defineStack` ends with `mergeActionsIntoObjects`: every standalone action
8+
carrying `objectName` is copied into that object's `actions` on the way out,
9+
and the standalone stays in `stack.actions`. `composeStacks` concatenates its
10+
inputs' `actions` and ended with the same merge — so each bound action was
11+
appended to its object a SECOND time, beside the copy the input's own build had
12+
put there: two copies for one declaration, three with three stacks under
13+
`objectConflict: 'override'` / `'merge'`, `manifest: 'preserve'` inheriting it.
14+
Downstream (the runtime note recorded on #14686): `collectActionDeclarations`
15+
pushes every embedded entry, so MCP `listActions` listed a composed app's bound
16+
actions twice and bare-name `resolveActionByName` refused the ambiguity; `os
17+
build` shipped the doubled entries to `dist/objectstack.json` unremarked,
18+
because `compile.ts` validates the lowered stack with `safeParse`, not
19+
`defineStack`.
20+
21+
`mergeActionsIntoObjects` is now idempotent over its own output: a bound action
22+
the object already carries BY IDENTITY is not appended again. Identity against
23+
the standalone list, deliberately not equality — the only way an entry of
24+
`stack.actions` is the very same object as an entry of `object.actions` is that
25+
a previous merge put it there. A hand-written twin (one action authored in both
26+
positions) is two objects after the strict parse and is still refused by
27+
#14686's same-key rule, which runs before the merge and is untouched; a marker
28+
cannot do this job (`ActionSchema` is strict — an unknown key is refused).
29+
`collectComposedActionKeyCollisions` (#14854) is untouched too: it runs before
30+
the merge and counts distinct stacks per key, so its refusals and their wording
31+
do not move.
32+
33+
Shape change, and only this: a composed object's `actions` is the previous
34+
output minus the duplicate entries — nothing is reordered, and `order` sorts the
35+
once-merged set as before. The accept set does not move: nothing that composed
36+
before is refused now, and nothing refused is accepted. Round trip: a composed
37+
artifact is still refused by `defineStack` when an input binds an action — a
38+
built artifact carries each bound action in both positions by #14686's landed
39+
design (author the source shape, not the artifact) — but with the same
40+
"declared twice" line a single built input gets, no longer "3 times"; with no
41+
bound action it parses cleanly, as before. One measured consequence outside
42+
composition: under `strict: false`, ONE action object placed in both positions
43+
is now carried once (before: twice) — a single declaration, in the mode that
44+
opts out of #14686's walk by choice.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
refactor(service-automation): take the trigger KIND from spec's `resolveFlowTriggerKind` instead of a second private copy of the chain (#14328)
6+
7+
No behaviour change and no API change — `patch` because nothing observable moves.
8+
`resolveTriggerBinding` is `private`, no export is added or removed, no payload
9+
key changes, and the kind reported for every flow is the kind reported before
10+
(1,223 `service-automation` cases and 81 `trigger-record-change` cases green
11+
unchanged, plus new pins across the whole precedence chain). What changes is that
12+
one rule now has one home.
13+
14+
**The defect.** `AutomationEngine.resolveTriggerBinding` hand-kept the chain that
15+
decides which trigger a flow asks for — string `record-*` token, array form,
16+
`timeRelative` descriptor, `schedule` cadence or `type: 'schedule'`, `type: 'api'`
17+
or `triggerType: 'api'` — in parallel with `@objectstack/spec`'s
18+
`resolveFlowTriggerKind`, the authoring-time mirror of that same rule. Both
19+
authoring surfaces already read the spec one: `defineStack`'s trigger-capability
20+
refusal and `@objectstack/lint`'s `validate-flow-trigger-readiness`. The engine
21+
did not, and nothing pinned the two together. A branch added to one side leaves
22+
`defineStack` accepting a stack the runtime leaves inert, or refusing one it would
23+
arm — the drift the shared resolver was hoisted to prevent, reopened one layer
24+
down. The two agreed on every string-form flow, so this was an observation rather
25+
than a live defect; the harm was future drift.
26+
27+
**The shape.** `resolveTriggerBinding` now takes its kind from
28+
`resolveFlowTriggerKind(flow)` and keeps only the per-kind BINDING construction —
29+
which start-node fields each trigger needs. `getTriggerBindingAudit` and the boot
30+
banner therefore name the kind authoring named, by construction.
31+
32+
**The one deliberate divergence is preserved, not unified.** The ARRAY form of
33+
`triggerType` (`['record-after-create', 'record-after-delete']`) resolves to *no*
34+
kind in spec — multi-event unions are unsupported (#3457), and reading the shape
35+
as "asks for a record-change trigger" would have `defineStack` demand a capability
36+
the flow can never use and would widen the lint rule's auto-triggered set. The
37+
engine routes it to the record-change trigger anyway, from an explicit pre-check
38+
that runs BEFORE the resolver, for one reason: so that trigger refuses it LOUDLY
39+
at bind time (#3481) instead of the flow folding into "manual" and vanishing from
40+
every surface. Pre-check *ordering* is load-bearing too — array form outranks
41+
`timeRelative`, which the resolver, blind to the array, would otherwise answer for
42+
a start node carrying both.
43+
44+
**What now catches the drift.** Two guards, one static and one runtime. The
45+
per-kind `switch` is exhaustive over `FlowTriggerKind` with a `never` default, so a
46+
kind added to spec fails this package's type-check until its binding shape is
47+
written; and a new case asserts every kind in `FLOW_TRIGGER_KINDS` is reachable
48+
through the real engine. The preserved divergence is pinned on both sides: engine
49+
routing and pre-check precedence in `service-automation`, and the refusal itself —
50+
asserted as a refusal, on a binding the real engine produced — end-to-end against
51+
the real trigger in `@objectstack/trigger-record-change`.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
`os validate` now refuses a react-page `<ListView>` bound only by the metadata-tier data source instead of accepting it. The react-blocks contract deprecates `objectName` in favour of `data={{ provider: 'object', object }}`, but no renderer reads that spelling yet — so a page written to the contract's own summary validated green and then rendered an empty list with no diagnostic, and the check tightens back to what actually renders. The refusal names `objectName` as the spelling to write, and field-name props (`columns`, `searchableFields`, filter positions, …) again resolve against it, which is also the correct object when a page carries both spellings.
6+
7+
Scoped deliberately: pages binding with `objectName` are unaffected, and so are the `value` and `api` data providers, a plain-array `data`, and a non-static `data` prop — all of those do render. The deprecation warnings on `objectName` and `viewType` remain, with their text corrected; both are still the spellings to write until the renderer folds the canonical data source in. Pages that were green on the canonical spelling alone now fail by design.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): route the flow param seeder through the single object-less predicate (#14864)
6+
7+
`isObjectLessActionKey` (`@objectstack/objectql`) is the canonical answer to
8+
"is this routed object the object-less placeholder": the canonical
9+
`GLOBAL_ACTION_OBJECT_KEY`, the legacy `'*'`, or nothing at all.
10+
`dispatchFlowAction` asks it directly when it decides whether to hand the
11+
automation service an `object` at all — and then, three lines later, handed the
12+
same `objectName` to `seedFlowActionParams`, which answered the same question
13+
with a second, narrower comparison of its own (`objectName !==
14+
GLOBAL_ACTION_OBJECT_KEY`).
15+
16+
The two parted on exactly one input, `'*'`. A request routed at the legacy
17+
wildcard — `POST /actions/*/<action>/<id>`, which resolves today because
18+
`actionHandlerObjectKeys` deliberately probes `'*'` last so a handler user code
19+
registered against it still resolves — was object-less to the automation
20+
envelope (no `object` sent) and object-BOUND to the params bag, which seeded a
21+
nonsense `'*Id'` key beside `recordId`. Same dispatch, two answers.
22+
23+
The empty-string half was never part of the divergence: the `objectName &&`
24+
truthiness leg of the old guard already covered it, and `undefined` with it.
25+
`'*'` was the whole of it.
26+
27+
**Direction.** The guard is widened onto the shared predicate rather than
28+
`isObjectLessActionKey` being narrowed. `'*'` is *unused today*, not *dead*:
29+
nothing first-party registers under it, but it is a deliberately-honoured
30+
legacy read path with its own docblock, reachable through the public
31+
`engine.registerAction(objectName, …)` surface that user code calls. Retiring
32+
it is a compatibility decision about someone else's package, not a tidy-up this
33+
fix is entitled to make.
34+
35+
**Coverage.** Both functions this touches were ablated repo-wide first rather
36+
than grepped, because a grep scoped to the file you expect a pin in cannot see
37+
a pin living elsewhere:
38+
39+
- `seedFlowActionParams` gutted → 5 tests red. It was pinned all along,
40+
indirectly, through the REST route — but every case there routes at a real
41+
object, so the object-LESS leg, where the two predicates actually disagreed,
42+
was the unpinned part. Now pinned, over the whole predicate domain.
43+
- `enforceActionParams` replaced with an unconditional `return null` → 3143
44+
passed, 0 failed. The ADR-0104 D2 gate could stop existing with nothing in
45+
the repo noticing. Its validator is well pinned in `@objectstack/spec`; the
46+
runtime gate around it was not, and that gate is what keeps an AI/MCP
47+
caller's plausible-but-wrong bag out of an action body. Now pinned.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
"@objectstack/service-cluster": patch
3+
---
4+
5+
fix(service-cluster): put the test layer in front of tsc, and repair the TS2322 it was hiding (#14181)
6+
7+
`packages/services/service-cluster` had **no `typecheck` script at all** — its
8+
scripts were `build` and `test` — so no tsc program anywhere read this package.
9+
Turbo/CI typecheck lanes skipped it silently, because a zero-matching filter run
10+
exits 0. `tsup` transpiles with esbuild and `vitest` runs through esbuild
11+
type-**stripping**; neither type-checks. The package's own `tsconfig.json` does
12+
include the tests and always did, so the program that would have read them
13+
already existed and was simply never invoked.
14+
15+
What that hid was in the worst possible file. `src/memory/memory.contract.test.ts`
16+
is the package's **contract witness** — type conformance to the `IPubSub` /
17+
`ILock` / `IKV` / `ICounter` contracts is the entire point of its existence — and
18+
it did not compile:
19+
20+
```
21+
src/memory/memory.contract.test.ts(26,46): error TS2322:
22+
Type 'number' is not assignable to type 'void | Promise<void>'.
23+
```
24+
25+
`cluster.pubsub.subscribe('e', (m) => received.push(m.payload))` passes a concise
26+
arrow body as a `PubSubHandler`, whose contract return type is
27+
`void | Promise<void>`. The body returns `Array.prototype.push`'s `number`, and
28+
TypeScript's void-return assignability relaxation does **not** forgive it,
29+
because the target is a UNION rather than a bare `void`. It is repaired with a
30+
block body — the handler is side-effect-only by contract, and the returned length
31+
was an accident of arrow syntax, never intent. The identical shape is what
32+
`@objectstack/metadata` graduated on (20 of them, `(evt) => arr.push(evt)` in a
33+
watcher slot).
34+
35+
⛔ The spec contract is untouched: `PubSubHandler` returning `void | Promise<void>`
36+
is correct and deliberate (the union is what lets a driver `await` an async
37+
handler). The defect was in the test, so the test is where it is fixed — no
38+
consumer-side widening, no source signature change.
39+
40+
Wired by the route the `packages/plugins/**` family settled on in #14062: a
41+
sibling `tsconfig.test.json` that changes **module semantics only** (`esnext` /
42+
`bundler` / `lib: ES2022`, matching how vitest actually executes these files)
43+
with **strictness inherited and untouched**, named by a new `typecheck` script
44+
through the shared `check:test-typecheck` gate. Measured before the repair: 1
45+
error under build semantics, 1 under the new config — the two readings agree, so
46+
this package carried no config-tier pile. After: 0 and 0, across a 410-file
47+
program covering all 7 of its `src/**/*.test.ts`.
48+
49+
No `test-typecheck-debt.json` is added, and its **absence is the zero**: the gate
50+
reads a missing ledger as `{ entries: {} }`, under which any error in any file
51+
here is red immediately. The package's `DEBT` entry in
52+
`scripts/check-type-check-coverage.mjs` (`errors: 1`) is deleted in this PR
53+
rather than lowered — that is the graduation the ratchet's own invariant
54+
requires, and it is why the error was fixed rather than ledgered.
55+
56+
No runtime code changes: `src/**` (excluding tests) is byte-identical, so no
57+
shipped behaviour moves. The `patch` level reflects the published `package.json`
58+
gaining `typecheck` / `check:test-typecheck` scripts and a `tsx` devDependency.

.claude/launch.json

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,6 @@
1818
"file:/tmp/showcase-dogfood-3777/data.db"
1919
],
2020
"port": 3777
21-
},
22-
{
23-
"name": "pr3505-attach-3477",
24-
"runtimeExecutable": "pnpm",
25-
"runtimeArgs": [
26-
"-C",
27-
"/home/user/objectstack-pr3505/examples/app-showcase",
28-
"exec",
29-
"objectstack",
30-
"dev",
31-
"--ui",
32-
"--seed-admin",
33-
"-p",
34-
"3477",
35-
"-d",
36-
"file:/tmp/pr3505-attach/data.db"
37-
],
38-
"port": 3477
3921
}
4022
]
4123
}

.claude/skills/dogfood-verification/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dev 工作树、dev-server 端口、preview 浏览器全是**共享的**:并行
3535
`lsof -nP -iTCP:<port> -sTCP:LISTEN`。在 `.claude/launch.json` 加一条指向****
3636
工作目录的具名配置,例如
3737
`pnpm -C <abs>/examples/app-showcase exec objectstack dev --ui --seed-admin -p <port> -d file:/tmp/<run>/data.db`
38+
工作树没了就**删掉自己那条** —— 死条目让 `preview_start` 挂在缺失的 `pnpm -C` 上。
3839
- [ ] **同时导出 `OS_PORT` —— 只给 `-p` 不够。** showcase 的自 ping 连接器读的是*
3940
*(`SHOWCASE_SELF_URL``OS_PORT``PORT`→3000),于是 `fetch failed` 冒充出口被封。
4041
- [ ] **自有数据**:`--seed-admin` 在空 DB 上给出 `admin@objectos.ai / admin123`。持

.claude/skills/pm-dispatch/references/contract-review.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
隔离的 fable 复核子代理**出第二意见,只喂卡片、既有裁决与 PR 本体,⛔ 不喂派发令与
3737
派发席自己的结论(污染即失独立性);简报写成**对抗性**(找拒绝理由)。未达档席 ⛔ 不凭
3838
自述复核,恒走本路径;子代理裁决按转录核验采信(下节),逐字采纳或整体作废。
39+
- **裁决载独立性对(机读,C4 行比对)**(2026-09-01 裁定 A、2026-09-02 裁定读法 a):产出 diff
40+
的身份写 `Implemented-by:` —— `mode:subagent` dev 记其分支(`claude/issue-…`,子代理无自有
41+
session),`mode:remote` dev 记 session id;`Reviewed-by:` = 出裁决席位的 session。两者同
42+
session ⇒ 报 SELF-REVIEW,⛔ 不作独立复核;两行皆无的历史裁决恒静默(⛔ 缺失永不转红)。
3943
- **放行 = 清标即落地**(2026-08-25 裁定):PASS ⇒ 同席同笔剥标(双载体),
4044
清标评论引 2026-08-31 裁定作 provenance,按
4145
`landing-operations.md` 走落地前检 → 转 ready → 挂 auto-merge/入队;轮报设**复审清单**

0 commit comments

Comments
 (0)