Skip to content

Commit 8f3eb49

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-4834-plugin-runtime-family-retire
# Conflicts: # packages/spec/src/migrations/registry.ts
2 parents eb4cd29 + 0e96e46 commit 8f3eb49

50 files changed

Lines changed: 1851 additions & 273 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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(service-analytics): `compareTo` applies measure-scoped filters, so `<measure>__compare` is the same measure as the column beside it (#4820)
6+
7+
A dataset measure declared with its own `filter` is scoped by running a
8+
supplementary grouped sub-query — `combineFilters(baseFilter, measureFilters[m])`
9+
— and merging it back by dimension key. The `compareTo` pass did not: it issued
10+
**one** shifted query over every base measure with only the base filter as its
11+
`where`, and never consulted `compiled.measureFilters` at all.
12+
13+
For a dataset like
14+
15+
```ts
16+
measures: [
17+
{ name: 'revenue', aggregate: 'sum', field: 'amount' },
18+
{ name: 'won_count', aggregate: 'count', filter: { stage: 'closed_won' } },
19+
]
20+
```
21+
22+
the current-period column was scoped and the comparison column was not — two
23+
different measures rendered side by side under one label:
24+
25+
| # | measures | where | |
26+
|:---|:---|:---|:---|
27+
| 1 | `revenue` || current |
28+
| 2 | `won_count` | `{"stage":"closed_won"}` | current |
29+
| 3 | `revenue`, `won_count` | **absent** | shifted |
30+
31+
`won_count__compare` was therefore a count of **every** opportunity in the
32+
previous window, inflated by exactly the rows the measure exists to exclude.
33+
The error runs one way: the comparison period always looks better, so a "won
34+
deals vs. last month" tile reads as a collapse when nothing went wrong. Only
35+
filter-scoped measures were affected — the unfiltered ones next to them compared
36+
correctly, which is what made it survive.
37+
38+
The comparison window now runs the **same pass** as the current period —
39+
unfiltered measures in one shifted query plus one shifted sub-query per
40+
filter-scoped measure, merged by dimension key — through a single shared
41+
implementation, so the two paths cannot re-diverge at the next change. The
42+
dataset filter, the presentation's `runtimeFilter` and the measure's own filter
43+
compose identically in both windows; the only difference between them is the
44+
shifted `dateRange`.
45+
46+
Numbers reported by existing dashboards change where a filtered measure was
47+
compared: with 3 won deals this month against 1 won of 5 opportunities last
48+
month, `won_count__compare` was `5` and is now `1`.
49+
50+
Cost: one extra query per filter-scoped measure when `compareTo` is set.
51+
Selections whose measures carry no filter are untouched and still compare in a
52+
single shifted query.
53+
54+
The empty-group fill (#4708) covers the new seam: a group the measure's filter
55+
empties in the *previous* window now reports `0` for a `count`/`sum` compare
56+
column rather than blanking it, exactly as it already did for the current period.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): register `ROLLED_BACK` / `NOT_ATTEMPTED` batch-row error codes; record the batch-row shape migration (#4793)
6+
7+
Support for the `@objectstack/metadata-protocol` v17 batch-row migration
8+
(#4793 — see its major changeset for the wire change itself):
9+
10+
- `ERROR_CODE_LEDGER` registers two codes under `@objectstack/metadata-protocol`:
11+
`ROLLED_BACK` (atomic data-batch row was written, then undone by the batch
12+
rollback) and `NOT_ATTEMPTED` (row never ran — an earlier row's failure
13+
aborted the batch). They are the structured, `ApiError.code`-level form of
14+
the message-string prefixes #4620 introduced; `ApiErrorSchema.code` now
15+
accepts them and clients branch on the code instead of regexing messages.
16+
- The ADR-0087 migration registry gains the protocol-17 semantic entry
17+
`batch-row-result-schema-shape` (a RESPONSE surface — nothing stored to
18+
rewrite, so it is a documented TODO for readers of the legacy `row.error` /
19+
`row.record` keys), and `docs/protocol-upgrade-guide.md` is regenerated
20+
with it.
21+
- `BatchOptionsSchema.atomic` / `BatchOperationResultSchema.errors` describe
22+
strings now document the code-based rollback marking (reference docs
23+
regenerated).
24+
25+
No schema *shape* changes: `BatchOperationResultSchema` already declared
26+
`errors` / `data` / `index` — the runtime caught up to it.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/metadata-protocol": major
3+
---
4+
5+
fix(metadata-protocol)!: batch per-row results now deliver the declared `BatchOperationResultSchema` shape (#4793)
6+
7+
**Breaking wire change** on the per-row `results` entries of the three
8+
bulk-write endpoints — `POST /data/:object/batch`, `/updateMany`,
9+
`/deleteMany`. The rows had drifted from the schema that declares them:
10+
`BatchOperationResultSchema`, the client SDK's exported `BatchOperationResult`
11+
type and the reference docs all said `errors: ApiError[]` / `data` / `index`,
12+
while the wire carried `error: string` / `record` and never sent `index`. A
13+
TypeScript consumer written against the published type compiled, validated,
14+
and read `undefined` at runtime. The wire now delivers exactly what is
15+
declared (a conformance pin parses every emitted row against the schema, so
16+
the two cannot silently fork again).
17+
18+
**FROM → TO, per row:**
19+
20+
| Before (legacy wire) | After (declared schema) | Your fix |
21+
| --- | --- | --- |
22+
| `row.error` (string) | `row.errors` (`ApiError[]`) | read `row.errors?.[0]?.message`; branch on `row.errors?.[0]?.code` |
23+
| `row.record` | `row.data` | rename the read |
24+
| — (never sent) | `row.index` (number) | new — the row's position in the request array; use it to correlate failure rows that carry no `id` |
25+
| `row.droppedFields` | `row.droppedFields` | unchanged |
26+
27+
**Rollback marking is structured now.** The `ROLLED_BACK:` /
28+
`NOT_ATTEMPTED:` message-string prefixes that #4620 introduced (see the
29+
`many-data-atomic-real-or-refused` changeset — its description of those
30+
markers is superseded by this entry) are promoted to first-class
31+
`ApiError.code` values, registered in the spec's ERROR_CODE_LEDGER:
32+
33+
- `errors[0].code === 'ROLLED_BACK'` — the row was written, then undone by the
34+
atomic batch rollback; `message` carries the causal row's index and error.
35+
- `errors[0].code === 'NOT_ATTEMPTED'` — the row never ran; an earlier row's
36+
failure aborted the batch.
37+
- the causal row keeps its own error code (e.g. `RECORD_NOT_FOUND`,
38+
`VALIDATION_FAILED`; an unclassified engine throw maps to `INTERNAL_ERROR`,
39+
with `httpStatus` mirrored when the error carried one).
40+
41+
Branch on the code — do **not** regex message prefixes; the prefixes are gone.
42+
43+
**Who is affected:** only readers of the *legacy* keys — which were never in
44+
the schema or the SDK types, so they were reachable only via `as any` or bare
45+
JS. Code written against `BatchOperationResult` (the published contract) needed
46+
this change to start working and needs no migration. There is no
47+
dual-emission or compatibility fallback: this is a hard cut inside the v17
48+
major window, and the old keys simply no longer exist on the wire.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
CI-only (#4859): Test Core / Dogfood 各 2→3 分片 + merge-queue 失败自动分诊评论 workflow。仅 `.github/workflows/**`,不发布任何包。
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/cli": major
4+
"@objectstack/runtime": major
5+
---
6+
7+
refactor(spec,cli,runtime)!: 退役 `crypto.hash` 能力 —— 声明了四层、构建期还自动推断,沙箱从没实现(#4391,ADR-0049 enforce-or-remove)
8+
9+
`crypto.hash` 是四层声明、零层实现:`HookBodyCapability` 枚举收它、枚举旁的文档表列它、CLI 提取器**自动推断**它、`ScriptContext.crypto.hash` 还写了签名 —— 而 `installCtx` 只往 VM 的 `ctx.crypto` 上装了 `randomUUID`。于是这个 token 唯一授权的那次调用,**每一次都在 VM 里抛**
10+
11+
这比普通的 declared ≠ enforced 更毒一档,坏就坏在**构建期推断**:作者(尤其是 AI 作者)写下 `ctx.crypto.hash(...)`,提取器就替他把能力加进 `capabilities`,`os build` 因此全绿 —— 系统亲手把人送进一条必炸的死路,而唯一诚实的记录是文档表格里一句 `_(not yet wired)_`,没有作者会先读表格再写 body。
12+
13+
**裁决是 remove,不是实现**(维护者 2026-08-02):从未实现、调用即抛、**零投诉** —— 对一个每次使用都抛错的能力来说,这本身就是最强的活性证据,没人需要它。在沙箱里实现 crypto 会扩大沙箱的能力面与安全审查面,那是长期成本而非一次性工时,无业务拉动不做。真需要哈希时按能力准入流程重提:**实现先行,声明随实现走**(ADR-0049 的 enforce 腿留给有实现的那天)。
14+
15+
## FROM → TO
16+
17+
| 写了什么 | 现在怎么办 |
18+
| :--- | :--- |
19+
| `capabilities: ['crypto.hash']` | **删掉这个 token**。它从未授权成任何东西 |
20+
| `await ctx.crypto.hash(algo, data)` | **删掉这次调用**。它从未返回过值 —— 今天能跑的代码没有一行依赖它 |
21+
| 确实需要哈希 | 在 host 侧做(Connector recipe,或引擎侧 hook)。沙箱内哈希须走能力准入流程重开,实现先行 |
22+
23+
一句话修法:**两个都删**`os migrate meta --from 16` 会自动帮你剥掉 token;那行**死调用是你自己要删的** —— 转换层刻意不改 body 源码(见下)。
24+
25+
## 定级理由(逐条自证,未照抄前例)
26+
27+
三问按 #4535 §5 逐条走:
28+
29+
1. **会不会 TS2305 / TS2339?** 会,两处。`HookBodyCapability` 是 public 导出类型,把它当**字面量联合**用的代码(`const c: HookBodyCapability = 'crypto.hash'`、对 token 做穷举 switch)现在编译失败;`ScriptContext.crypto.hash` 的调用点以 TS2339 失败。实测三仓(objectstack / cloud / objectui)裸名扫描 `crypto.hash` / `ctx.crypto.hash` / `'crypto.hash'` —— **两个兄弟仓零命中**,本仓命中全在本 PR 内清理。
30+
2. **有没有元数据迁移?** 有。token 是写在作者源 hook/action body `capabilities: []` 数组里的****,也会躺在已存的 `sys_metadata` 行里 —— 故注册了 ADR-0087 D2 转换 `hook-body-crypto-hash-removed`(D3 挂 protocol-17)。这是与 #4767 / #4783 / #4616 的分界:那三单退役的是**导出名 / 运行时描述符**,没有作者源可改写;本单有,和 `object-enable-trash-mru-removed` / #4734 同侧。
31+
3. **形状变更?****枚举值收窄**(6 → 5),不是 key 移除。故**没有 `retiredKey()` 墓碑** —— `capabilities` 这个 key 本身依然活着、依然被强制。处方改由枚举自己的 error map 承载,并按 `object.managedBy: 'system'` 的先例**`issue.input` 为键**:只有「曾经合法」的那个拼写会被告知「was removed」,写错成 `crypto.hsah` 的作者拿到的仍是 zod 自己那条列出合法 token 的消息 —— 告诉他「你的值被退役了」属于误导。
32+
33+
`@objectstack/cli``@objectstack/runtime` 同定 **major**:前者 `ExtractedBody.capabilities` 的公开联合类型收窄(赋值给它的代码 TS2322),后者 `ScriptContext.crypto` 少一个成员(TS2339)。
34+
35+
## 门禁实报
36+
37+
枚举值收窄对四张 ratchet **全部不可见**,这一点值得单独记一笔:`authorable-surface.json` 记到 key 级(`data/ScriptBody:capabilities`),`json-schema.manifest.json` 记 def 名(`data/HookBodyCapability` 仍在),`packages/spec/json-schema/` 本身 gitignore。所以 `check:authorable-surface` / `check:api-surface` 实跑**零变化**,`check:liveness` / `check:empty-state` 同样 PASS(`capabilities` key 仍活,不产生台账行变更)。
38+
39+
也就是说:**本次移除没有任何一张基线能自动兜住它** —— 兜住它的只有本 PR 新增的 pin 测试(spec / cli / runtime 各一组,已 sabotage 实跑验证复活即红)。`check:generated` 8/8 绿,移动的是 `spec-changes.json``docs/protocol-upgrade-guide.md` 与两页生成参考文档(`data/hook-body.mdx``ui/action.mdx`,枚举选项随之少一项)。
40+
41+
## 转换刻意不做的事
42+
43+
`hook-body-crypto-hash-removed` 只从 `body.capabilities` 里剥掉死 token,**不碰** body 源码里那行 `ctx.crypto.hash(...)`。这是有意的:那行调用从未返回过值,剥掉授权不会让任何还能跑的东西变坏;但把它一并「修好」会让作者失去唯一一个还在提醒他「这里有段死代码」的信号。`retiredFromLoadPath: true` —— 枚举当场拒绝,活作者在 parse 时就被教育,转换存在的意义是让已存的 16.x / 17-rc 行重放干净(否则永远被打成 `metadata_spec_invalid`,把链上历史误标成当期违约)以及让 `os migrate meta --from 16` 改写作者源。

.github/workflows/ci.yml

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,13 @@ jobs:
7575
- '.github/workflows/ci.yml'
7676
7777
test:
78-
# Sharded 2-way BY PACKAGE: a core-touching PR ran the affected suite
79-
# ~11½ min on one 4-vCPU runner — the longest pole in the whole workflow.
80-
# scripts/partition-test-shards.mjs splits the package list into two
81-
# deterministic, test-file-count-balanced halves (573/572 at the time of
82-
# writing) and each shard runs its half through turbo. NOT the dogfood
78+
# Sharded 3-way BY PACKAGE: a core-touching PR ran the affected suite
79+
# ~11½ min on one 4-vCPU runner — the longest pole in the whole workflow —
80+
# and at 2 shards the slower shard still ran ~10 min, keeping merge-queue
81+
# builds at ~11 min end-to-end (#4859).
82+
# scripts/partition-test-shards.mjs splits the package list into three
83+
# deterministic, test-file-count-balanced thirds and each shard runs its
84+
# slice through turbo. NOT the dogfood
8385
# job's vitest --shard passthrough, deliberately: that works for dogfood
8486
# because it is ONE package with ~60 files, but applied workspace-wide,
8587
# vitest 4 hard-fails every package with fewer test files than the shard
@@ -90,13 +92,13 @@ jobs:
9092
# Branch protection requires the bare "Test Core" context, which a matrix
9193
# can never publish again — the test-gate job below carries that name
9294
# (the #3622 lesson; see dogfood-gate).
93-
name: Test Core (${{ matrix.shard }}/2)
95+
name: Test Core (${{ matrix.shard }}/3)
9496
needs: filter
9597
if: needs.filter.outputs.core == 'true'
9698
runs-on: ubuntu-latest
9799
# Backstop only — the stall guard on the test steps is the primary
98100
# detector for a #4250-style hang and fires well before this. 30 min is
99-
# ~4× a normal sharded run (~6-7 min), with margin for a cold Turbo cache;
101+
# ~5× a normal sharded run (~4-6 min), with margin for a cold Turbo cache;
100102
# the old 45 left a hung job "running" for half an hour past any plausible
101103
# healthy finish.
102104
timeout-minutes: 30
@@ -105,7 +107,7 @@ jobs:
105107
strategy:
106108
fail-fast: false
107109
matrix:
108-
shard: [1, 2]
110+
shard: [1, 2, 3]
109111

110112
steps:
111113
- name: Checkout repository
@@ -190,7 +192,7 @@ jobs:
190192
pnpm exec turbo ls --output=json > "$RUNNER_TEMP/turbo-ls.json"
191193
fi
192194
node scripts/partition-test-shards.mjs "$RUNNER_TEMP/turbo-ls.json" \
193-
--shard ${{ matrix.shard }}/2 --exclude @objectstack/dogfood \
195+
--shard ${{ matrix.shard }}/3 --exclude @objectstack/dogfood \
194196
> "$RUNNER_TEMP/shard-packages.txt"
195197
echo "Packages on this shard:"
196198
cat "$RUNNER_TEMP/shard-packages.txt"
@@ -499,18 +501,19 @@ jobs:
499501
test
500502
501503
dogfood:
502-
# Sharded 2-way: the suite is ~60 independent test files, each booting its
503-
# own in-process app, and a single 4-vCPU runner needed ~7½ minutes for the
504-
# lot — the longest pole in the whole workflow. vitest partitions the file
505-
# list deterministically across shards; both shards must pass. If branch
506-
# protection lists "Dogfood Regression Gate" as a required check, it must be
507-
# updated to the two sharded check names.
508-
name: Dogfood Regression Gate (${{ matrix.shard }}/2)
504+
# Sharded 3-way: the suite is ~60 independent test files, each booting its
505+
# own in-process app; a single 4-vCPU runner needed ~7½ minutes for the
506+
# lot, and at 2 shards each half still ran ~7 min — the longest pole left
507+
# once Test Core went 3-way (#4859). vitest partitions the file list
508+
# deterministically across shards; all shards must pass. Branch protection
509+
# requires only the bare "Dogfood Regression Gate" context, carried by the
510+
# dogfood-gate job below — the shard count can change without touching it.
511+
name: Dogfood Regression Gate (${{ matrix.shard }}/3)
509512
needs: filter
510513
if: needs.filter.outputs.core == 'true'
511514
runs-on: ubuntu-latest
512515
# Backstop only — the stall guard on the test step is the primary detector
513-
# for a #4250-style hang (see Test Core). 30 min is ~4× a shard (~7 min;
516+
# for a #4250-style hang (see Test Core). 30 min is ~6× a shard (~5 min;
514517
# the verify-CLI pass that used to ride shard 1 is its own parallel job
515518
# now — dogfood-verify below).
516519
timeout-minutes: 30
@@ -519,7 +522,7 @@ jobs:
519522
strategy:
520523
fail-fast: false
521524
matrix:
522-
shard: [1, 2]
525+
shard: [1, 2, 3]
523526

524527
steps:
525528
- name: Checkout repository
@@ -552,7 +555,10 @@ jobs:
552555
# Shard-scoped key: the turbo test hash differs per shard (pass-through
553556
# args are part of the task hash). Restore-only on PRs — see the Restore
554557
# Turbo cache comment in the test job; the save step at the end of this
555-
# job seeds from main only.
558+
# job seeds from main only. The job-level catch-all (same as Test Core's)
559+
# is what keeps a NEW shard number warm before main has ever saved it:
560+
# turbo's cache is content-addressed per task, so another shard's entries
561+
# replay the shared build closure even when the test slice differs.
556562
- name: Restore Turbo cache
557563
uses: actions/cache/restore@v6
558564
with:
@@ -561,6 +567,7 @@ jobs:
561567
restore-keys: |
562568
${{ runner.os }}-turbo-${{ github.job }}-${{ matrix.shard }}-${{ github.ref_name }}-
563569
${{ runner.os }}-turbo-${{ github.job }}-${{ matrix.shard }}-
570+
${{ runner.os }}-turbo-${{ github.job }}-
564571
565572
- name: Install dependencies
566573
run: pnpm install --frozen-lockfile
@@ -585,7 +592,7 @@ jobs:
585592
mkdir -p "$RUNNER_TEMP/stall-reports"
586593
node scripts/run-with-stall-guard.mjs --log "$RUNNER_TEMP/dogfood.log" --stall-minutes 10 \
587594
--report-dir "$RUNNER_TEMP/stall-reports" -- \
588-
pnpm turbo run test --filter=@objectstack/dogfood -- --shard=${{ matrix.shard }}/2
595+
pnpm turbo run test --filter=@objectstack/dogfood -- --shard=${{ matrix.shard }}/3
589596
590597
# Dogfood boots real apps in-process, so a native/OOM abort is likelier
591598
# here than in the unit suites — and a shard that dies silently looks like

0 commit comments

Comments
 (0)