Skip to content

Commit b1340a6

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-5059-reference-page-descriptions
2 parents 33409bc + 502564d commit b1340a6

44 files changed

Lines changed: 2852 additions & 137 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/console-f995a452d2ca.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Derived from the changesets objectui declared over the range — 64 releasing of
1818
- **minor** — Field widgets no longer spread renderer-only props — or arbitrary keys from a field config — onto the DOM element they render (objectui#3291). (objectui `19b8c9be3`)
1919
- **minor**`RichTextField` honours `mobile_fullscreen`, so `mobile.fullscreenLongText` is finally true of rich text too (objectui#3301). (objectui `30ae33a77`)
2020
- **minor**`mobile.fullscreenLongText` finally reaches auto-generated long-text fields, and `mobile_fullscreen` gets one declared carrier (objectui#3245). (objectui `f44d8727f`)
21-
- **minor** — The flow designer writes node geometry as the spec's `FlowNode.position`, not its own `ui: { x, y }` (objectui#3172). (objectui `6e794a19e`)
21+
- **minor****BREAKING (v17)**The flow designer writes node geometry as the spec's `FlowNode.position`, not its own `ui: { x, y }` (objectui#3172). (objectui `6e794a19e`)
2222
- **minor****BREAKING (v17)** — field widgets receive their metadata on ONE key, `field`. `schema` is removed from the widget contract (objectui#3233). (objectui `042e09d77`)
2323
- **patch**`UserFilters` no longer carries its own operator table when it lowers a `ViewTab.filter` preset into an ObjectQL AST node. The private `specOperatorToAst` was the second hand-kept… (objectui `d7f350a89`)
2424
- **patch** — Name `CommentThread`'s three emoji-only buttons, and follow the session language past the 7-day mark (objectui#3441) (objectui `65516ba4a`)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
refactor(spec)!: `IDataDriver` 的 query 参数改为 `DriverQuery``Omit<QueryAST, 'object'>`),对象名只写一遍 (#5181)
6+
7+
`IDataDriver.find/findOne/count/updateMany/deleteMany/explain` 的第一个实参已经是对象名,而它们要求的 `QueryAST` 又把 `object` 列为必填 —— 同一个事实被要求写两遍,并因此有了两处互相矛盾的余地。上层为这份歧义已经付过账:objectql 引擎刻意把键序写成 `{ ...query, object }`,好让一个夹带的 `query.object` 覆盖不掉已解析的名字;wire 层则用一条具名 400(`QUERY_OBJECT_MISMATCH`)拒绝不一致。
8+
9+
驱动这一侧付的账是**成片的 cast**:一个手上只有 `where` 的直接调用方叫不出这个类型的名字,于是 `as any`,连带把 `where`/`orderBy`/`fields` 的类型检查一起关掉(cloud#1053 实测 20 处;cloud#1030 的 `$like` 就是从这个口子活到运行时的)。
10+
11+
**FROM → TO**
12+
13+
```ts
14+
// FROM —— 对象名写两遍
15+
await driver.find('account', { object: 'account', where: { status: 'open' } });
16+
// TO —— 第一个实参就是对象名
17+
await driver.find('account', { where: { status: 'open' } });
18+
```
19+
20+
一行修复:**删掉驱动调用字面量里的 `object:`**。编译器会把每一处指出来(TS2353 `'object' does not exist in type 'DriverQuery'`)。
21+
22+
**两个方向的兼容性,都不强迫任何一侧动**
23+
24+
- **调用方**:手上是一个 `QueryAST` ****的,原样传即可 —— 它具备 `DriverQuery` 要求的全部属性,多出来的那个在非新鲜字面量上 TypeScript 一律接受。新被拒绝的**恰好只是冗余本身**:写在调用点上、拼出 `object` 的内联字面量。本仓的迁移面因此实测只有 1 个文件 6 处(`@objectstack/metadata` 的 history-cleanup),已在同一 PR 里删除;引擎的 `driver.find(object, ast, …)` 一个字都不用改。
25+
- **驱动实现**:仍旧声明 `query: QueryAST` 的实现继续编译 —— 方法参数按双变比较。它们不再可以做的是**`query.object`**:调用方现在有权省略,声明会对一个运行时为 `undefined` 的值说谎。本仓五个驱动(memory / mongodb / sql / sqlite-wasm / turso)实测没有一个读它,因此本次不动驱动代码;把驱动签名一并迁到 `DriverQuery` 是后续的机械收尾。
26+
27+
`QueryAST` 的 zod 形状(`data/query.zod.ts``BaseQuerySchema`**没有动**`object` 在引擎与 hook 那一层是被读的,改的只是驱动契约的参数类型。`expand` 条目里的 `object` 同样保留 —— 那里它命名的是**关联对象**,没有任何实参携带这个事实,不是冗余。
28+
29+
标 major 是因为这是**源码级破坏性**变更(调用点字面量),运行时行为零变化。注意 `check:api-surface` 只看得见新增的 `DriverQuery` 导出、看不见参数类型的收窄(它记录导出存在与否,不记录签名),所以这条迁移说明是该变更唯一的下游载体。
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
fix(plugin-auth): break-glass 守卫扩到 `sys_permission_set`,并把「零管理员」从引导期豁免里分辨出来 (#6084)
6+
7+
break-glass 不变量(cloud ADR-0024 D5.2)此前守三张表:`sys_user`(ban/删行,#5892/#5941)与
8+
`sys_member`/`sys_user_permission_set`(撤销 standing,#5978)。**第四条写法绕开全部三条**:
9+
「谁是 platform admin」是**按名字**解析的——`resolveAdminUserIds`
10+
`where: { name: 'admin_full_access' }` 取那条 `sys_permission_set` 行,再去读指向它 id 的授权行。
11+
删掉那一行、或把它改个名字,授权行、`sys_user` 行、`sys_member`**一个都没动**,而所有
12+
platform admin 同时不再是管理员。
13+
14+
## 放大缺陷:这一条写法还会顺手关掉守卫本身
15+
16+
两个判据都以「这个环境有管理员吗?没有就放行」开场——引导期本就没有 break-glass 账号可保护,
17+
在那个窗口里拒绝一切身份写会是守卫拿一个空测量值自造政策。可是 `admin_full_access` 行没了的环境
18+
**读起来正是零管理员**,于是豁免生效,ban / 删用户 / 降级 / 撤授权**一并放行**。所以这一条写法
19+
不只是锁死环境,还在锁死的路上把 #5892 / #5941 / #5978 三条守卫一起解除。
20+
21+
## 两处改动
22+
23+
**① 同形状扩到第四张表。** `sys_permission_set``beforeUpdate` + `beforeDelete`,复用 #5978
24+
`enforceStanding` / `applyPending`,`PendingStandingWrite` 多认一张表;枚举的第一段 scan 现在也对
25+
pending 做模拟并**重测 `name`**——与 grant 半边重测 `permission_set_id` 同理,scan 自己的 `where`
26+
只证明了写**之前**那行叫什么。静态跳过键只有 `name` 一个:枚举只读这一列,所以每一次 projection
27+
回填、每一次 `os meta resync`、每一次 Setup 里编辑权限集(写的是 `label`/`description`/权限 JSON)
28+
一次读都不花。数据门自己已经拒绝改名(ADR-0094),这道守卫覆盖的是不经数据门的引擎级与
29+
system-context 写。
30+
31+
**② 收紧引导期豁免。** 「零管理员」拆成它本来混在一起的两种状态:
32+
33+
- **真引导期**——没有任何证据说这里曾经有过 platform admin。照旧放行。
34+
- **刚被清空**——仍存在无组织范围、有效期内的 `sys_user_permission_set` 授权行,而它指向的
35+
`sys_permission_set` 行已经不在了。fail-closed 拒写,并在报文里点名那些悬空授权行。
36+
37+
判据选的是**悬空授权行**,因为它在正常路径上根本写不出来:每一个生产者都先插权限集、再读回 id 写
38+
授权行(`bootstrapPlatformAdmin` 第 1 步 seed 权限集、第 2 步才提拔第一个用户,权限集缺席时返回
39+
`admin_permission_set_missing` 而不是发授权),所以**全新环境的可写性按构造不变**——测试里有一条
40+
「真引导期照常放行」的钉专门量这一点。改名不留下悬空授权行,这条判据看不见它;那条路径改由 ① 在
41+
写入处拦下,残留因此只剩一种状态:守卫尚未注册时落下的改名。曾考虑把判据放宽成「不存在
42+
`admin_full_access` 行 且 存在无组织范围授权行」,被否掉——它会改变「seed 顺序先写授权行」的全新
43+
环境的答案,而不改变全新环境的答案正是这条判据唯一不能碰的红线。
44+
45+
`sys_permission_set` 的拒绝报文结尾不走 SCIM 那句:IdP 不写这张表,写它的是元数据删除、
46+
`os meta` 与包卸载,报文点名的是这些门。
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): `check:liveness` 的 stale-evidence 从 `` 升级为 `` 判红,摘要行不再把「声明数」当作「解析成功数」(#5623)
6+
7+
`live` 判定的语义就是它的 evidence 指针 ——「这个属性有运行时消费者,在这里」。指针指向本仓已不存在的文件时,这条声明就不再可证伪:declared 有,enforced 无。而把它变成这样,只需要一次目录重组或一次改名。
8+
9+
实测(`origin/main`,把 `packages/spec/liveness/query.json` 的 5 条 evidence 路径指回迁移前的 `packages/plugins/driver-sql/...`):
10+
11+
```
12+
evidence paths: 330 resolved against this checkout, 101 attributed to another repo (…)
13+
14+
⚠ 5 'live' entr(ies) cite a missing file:
15+
query/fields → packages/plugins/driver-sql/src/sql-driver.ts
16+
17+
```
18+
19+
退出码 **0**。五条断链逐条点名了,CI 一路绿灯 —— 而且摘要行里的 `330` 一动没动,因为它数的是 local 路径**总数**,不是解析成功数,"resolved" 这个词是失真的。
20+
21+
**为什么以前是 ``,以及为什么现在不该是。** 这不是对本仓路径的有意宽容,是解析器时代的遗留:#3857 之前的检查取 `evidence.split(':')[0]` 当文件名,227 条里报 48 条、**全是误报**,那种情况下它当然不能判红。`evidence.mts` 修好了解析(realm marker 归属 + 仓根路径提取),此后这份清单一直是空的 —— 恰恰是这一点让「有命中」重新变成信号。当时被那 48 条噪声埋掉的唯一一条真腐烂(`object.enable.clone`,消费者从 `@objectstack/objectql` 搬到了 `@objectstack/metadata-protocol`),就是宽容的实际代价。
22+
23+
同族的旁证:同一目录下的 `check-empty-state.mts``rotted-evidence` 一直是 `` + exit 1,README 也写着它的执行点路径「resolves like `evidence` above, so a pointer that rots is reported rather than trusted」。真正有意的宽容在这个 gate 里都写明了理由(`verifiedAt` 的年龄、undrilled 容器计数、`PENDING_GOVERNANCE` —— 每一条都在代码里明说 "never fails CI"),stale-evidence 那一段一个字都没有。
24+
25+
**改动**
26+
27+
1. **`live` 条目引用本仓缺失文件 → `` 判红**,并给出三条修法(仓内搬家就改指针并补 `verifiedAt`;搬去别的仓就加 realm marker;消费者真没了就按 ADR-0049 重新判定,而不是随手指向一个看着像的幸存者)。
28+
**边界不变**:cross-repo attribution(`objectui:` / `cloud:` / `packages/services/service-ai/…`,当前 101 条)只计数、不解析,永远不判红 —— `checkEvidence` 只对 local 桶做存在性检查,这个分界是结构性的,不是靠约定。
29+
2. **摘要行改成两个数**,而不是二选一:
30+
```
31+
evidence paths: 330 repo-local path(s) declared by 'live' entries, 330 resolved against this checkout; 101 attributed to another repo (…)
32+
```
33+
两个数都留着是有原因的:"declared" 是 #3857 留下的解析健康度信号(有单测防止解析器退化成「什么都提不出来」而假绿),"resolved" 是判定。绿的时候两者相等 —— 这正是当初只印前一个会被读成通过的原因。有断链时行尾追加 `, N MISSING`
34+
3. 新增 `--ledger-root=<dir>`:让 gate 读 `packages/spec/liveness` 的一份副本。自测据此把真 gate 跑在「只坏了一条指针」的 ledger 副本上,不改动仓库里任何文件。
35+
36+
`check-liveness.test.ts`(新增 8 例)直接 spawn 真脚本断言退出码 —— 这个 bug 从来不是「检查看不见」:它把五条全点名了还是 exit 0,所以只测 `checkEvidence` 的单测全程是绿的。
37+
38+
脚本本身(`scripts/`)不进 npm 包,但 `packages/spec/liveness/**` **是发布内容**(`package.json``files` 里有 `liveness`,`npm pack --dry-run` 实测 29 个文件入包,含该目录的 `README.md`)。一个新开始判红的 gate 必须同步它面向作者的文档,否则下一位作者只能靠撞红的 CI 才知道规则变了 —— 所以 README 里那段新的失败语义也随包发出,按 patch 记账,而不是空 frontmatter(空 changeset 是 `changesets/action` 的真实输入,全空集合会静默绿着卡住发布 —— #4898 卡住 17.0.0-rc.2 的就是这个)。当前 `main` 上 330 条本仓路径全部解析成功,该 gate 落地即绿。
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): `seedAutonumber` 把非「表未建」的读故障上抛,不再从 0 重发自增号 (#5979)
6+
7+
引擎在 driver 不自带自增序列时,走 fallback 路径:先从库里读 `MAX(existing)`
8+
给内存计数器播种,再自增发号。这次播种读此前挂在一个裸 `} catch { return 0; }`
9+
后面 —— **所有**失败(连接断开、超时、权限拒绝、查询错误)都被答成跟「表里真的
10+
一行都没有」完全相同的 `0`
11+
12+
这两件事是相反的事实(ADR-0110 D3),在这里混淆是 #4728 / #4825 / #5108 同族里
13+
代价最高的一支:对一张已经有 N 行的表,一次抖动的读就让序列从 1 重新开始,发出
14+
与既有行**相撞**的自增号。插入是**成功**的,一行日志都没有,而撞号落在业务标识符
15+
上 —— 一个写错了的值,重试修不好,重启也修不好。危险本身早就写在那个读上方的
16+
注释里(#4371:"the catch below would have swallowed the guard's rejection into
17+
'seed from 0', i.e. duplicate autonumbers");当时读被修了,catch 没有。
18+
19+
现在按错误**类型**判别,问的是共享谓词 `isMissingTableError`
20+
(`@objectstack/metadata/errors`,#4825),而不是手抄一份 `code === '42P01'`:
21+
22+
- **表尚未 provision** → 仍旧从 0 起号。库里确实没有行,1 号不会撞到任何东西,
23+
这是唯一良性的失败原因,行为与此前完全一致。
24+
- **其余任何读故障** → 原样上抛,**不发号、不写入**。行没被看见不等于行不存在,
25+
所以引擎拒绝用一份自己从没读到的数据去推算号段。
26+
27+
**行为变化(升级须知)**:自增字段所在对象的写入,在存储读故障期间会**失败**,
28+
而不再像以前那样"成功"并写入一个可能撞号的编号。这是有意的 —— 一次响亮的失败
29+
可以重试,一个静默写错的业务编号不能。故障恢复后下一次插入会重新播种并从真实的
30+
`MAX(existing)` 继续(失败的播种不会污染内存计数器)。
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"@objectstack/service-datasource": patch
3+
---
4+
5+
fix(service-datasource): give this package's vitest run a 60s `testTimeout` — close the #4856 coverage hole that let the merge queue evict unrelated PRs (#6044)
6+
7+
`packages/services/service-datasource` had no `vitest.config.ts` at all, so
8+
every case ran under vitest's **5000ms** default. #4856 fixed this class of
9+
flake by setting per-package timeouts in each package's own `vitest.config.ts`
10+
— a structure that cannot reach a package with no config file to carry it.
11+
12+
The cases that build a REAL driver pay a one-time `@objectstack/driver-sql`
13+
(knex) import inside the first case that reaches it. In
14+
`datasource-pool-support.test.ts` the pool rejections throw before that import,
15+
so "sqlite WITHOUT a pool still builds exactly as before" is the first case
16+
through it: measured idle it runs ~1.1s while its neighbours run 0-2ms (the
17+
postgres/mysql cases ride the module cache at 31/82ms). ~4.6x headroom against
18+
5000ms holds on a PR branch and not on a merge-queue runner building several
19+
PRs' batches at once — the observed signature: intermittent reds only in queue
20+
full builds, evicting PRs that never touched this package (#5999 twice, #5973
21+
once, 2026-08-06).
22+
23+
`testTimeout: 60_000` reuses #4856's value rather than inventing a new number,
24+
set at the config layer so future cases are covered on arrival. Isolation was
25+
reviewed rather than assumed (the #6044 triage forbade a timeout-only closure):
26+
the flaky case builds `:memory:`, unprobed on the production path, never opens
27+
a connection or loads the native addon, and every factory-door case destroys
28+
its knex handle; the boot and wizard doors run on per-case fakes. No temp
29+
files, no ports, no shared mutable state across cases — the red was load
30+
variance on a real one-time import, not a leak.
31+
32+
No runtime, schema or public API change — test configuration only.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@objectstack/mcp": minor
3+
"@objectstack/runtime": minor
4+
"@objectstack/spec": patch
5+
---
6+
7+
feat(mcp): 开源发行版终于消费 skill —— `instructions` 半边投影为 MCP `prompts` 原语 (#3905)
8+
9+
`stack.zod.ts` 与 ADR-0063 §2 把 **skill 定为唯一第三方扩展原语**,而开源发行版
10+
(BYO-AI,cloud ADR-0025)里零消费方:`SkillSchema` 可作者化、被两条 lint 规则认真
11+
校验(`validateAiToolReferences` / `validateAiSurfaceAffinity`),却没有任何代码路径
12+
读它 —— 作者写 skill → 校验通过 → lint 通过 → **永不运行且无人告知**。这正是本仓
13+
ratchet 存在的意义所要消灭的 declared ≠ enforced 形状。
14+
15+
**skill 的两个半边,现在各自说清楚跑在哪。**
16+
17+
- **`instructions`(判断力)→ MCP `prompts` 原语,处处可用。** MCP 服务器补齐了
18+
`prompts/list` / `prompts/get`:每个带 `instructions` 的已注册 skill 成为一个
19+
MCP 客户端可以列出并取回的 prompt(prompt 名 = skill 机器名,`label``title`,
20+
`description` 原样带上)。HTTP 与 stdio 两条传输都服务它。
21+
- **`tools` / `surface` / `triggerConditions`(接线)→ 明确标注 cloud-runtime-only。**
22+
绑工具与激活判定是 in-product agent 循环的属性;MCP 里模型在客户端、服务端只有
23+
一张扁平工具表,AI 暴露的 Action 早已通过 `list_actions` / `run_action` 可达。
24+
文档与 schema JSDoc 如实写明,不再装样子 —— 但两半边在两个发行版里都照旧接受
25+
**校验**,所以开源里写的 skill 到 cloud 上语义完整,不必写两遍。
26+
27+
**协议合规。** `prompts` 能力按规范声明:只有当宿主能读到本环境的 skill 元数据时
28+
才声明并注册处理器(能力协商如实,与 action 工具同一套优雅降级);无 skill 时
29+
`prompts/list` 返回**空列表而非报错**;`prompts/get` 取不存在的名字返回
30+
`-32602 InvalidParams`;没有 `instructions` 的 skill 与 `active: false` 的 skill
31+
不投影。HTTP 面的投影从**本请求自己的 bridge** 读(与 `describeObject` 同一条
32+
per-environment 通道),多租户宿主不会把一个环境的 skill 服务给另一个环境。
33+
34+
**同时修掉同仓重名。** `packages/mcp/src/skill.ts` 从来不是 skill 元数据类型,
35+
而是 ADR-0036 Amendment C 的 `SKILL.md` 分发物 —— 在 `packages/mcp` 里 grep
36+
`skill` 先找到的一直是它。现在按各自承载的产物命名:`skill-md.ts`(SKILL.md
37+
分发物)与 `skill-prompts.ts`(skill 元数据 → prompts 投影),两侧模块头互指。
38+
包的公开导出名(`renderSkillMarkdown` / `OBJECTSTACK_SKILL_NAME` /
39+
`OBJECTSTACK_SKILL_DESCRIPTION` / `RenderSkillOptions`)一个未变。

0 commit comments

Comments
 (0)