Skip to content

Commit 64729fd

Browse files
committed
Merge origin/main into claude/issue-5367-dataset-invalid-envelopes
2 parents 11ccde2 + eb26126 commit 64729fd

20 files changed

Lines changed: 1429 additions & 74 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): ADR-0117 scoped 接受 —— `owning_business_unit_id` 规范名进入登记处并列入公开表单拒收名单 (#4611)
6+
7+
ADR-0105 D13 的 promotion 工具要求「按子树的 scoping field 回填 `organization_id`」,
8+
但没有任何元数据声明「哪个字段承载 BU 归属」。维护者裁定加速 ADR-0117 补上这一层。
9+
本次落地**协议决定的名字面**,不含运行时注入。
10+
11+
- **ADR-0117 定稿为 `Accepted (D1/D3 scoped)`**:仅 D1(`business_unit` 档与
12+
`owning_business_unit_id` 记录戳的命名与语义)与 D3(`record.organization_id ==
13+
BU(owning_business_unit_id).organization_id` 不变量)进入协议;D5、D2 的默认盖章
14+
策略、D8 的启用门粒度、D4 的权限位仍为 Proposed,合并本 ADR 不构成对这四项的裁定。
15+
- **新增 `SystemFieldName.OWNING_BUSINESS_UNIT_ID`**(`'owning_business_unit_id'`),
16+
明确标注 **open-core 暂不注入** —— 该表是**名字登记处而非注入集合**,`tenant_id` /
17+
`user_id` / `deleted_at` 是既有的三条同类先例。早登记是为了阻断消费方各造一个
18+
`business_unit_id` / `bu_id` / `dept_id`,即 cloud#982`tenant_id`/`org_id`/`space`
19+
付过学费的漂移形态。
20+
- **`PUBLIC_FORM_SERVER_MANAGED_FIELDS` 新增该名**:它是与 `owner_id` /
21+
`organization_id` 同类的归属锚点,一旦盖章,匿名面上被伪造的值会把记录推到别的部门
22+
墙后。在列存在**之前**就拒收是零成本且 fail-closed 的;等列上线后再补名单,中间那个
23+
版本就是一个带着发布号的洞。
24+
25+
**本轮刻意不动 `ownership` 枚举。** `packages/objectql/src/registry.ts``wantOwner`
26+
**排除式**判定(只排除 `'org'` / `'none'`),此刻加入第四个值会让 `business_unit`
27+
照常注入 `owner_id` —— 与 ADR-0117 D1 表格恰好相反,属 ADR-0049「spec 不得声明运行时
28+
不执行的东西」所禁止的形态。因此 `ownership: 'business_unit'` 目前仍被 Zod 响亮拒绝
29+
(并列出 user / org / none 三个合法值),这是**正确**行为,已加 pin 钉住,防止后人
30+
「顺手补全」。枚举值与其注入实现同 PR 落地。
31+
32+
**行为变更提示**:若某应用自行声明了名为 `owning_business_unit_id` 的业务字段并将其
33+
放在匿名公开表单上,该字段自本版本起不再接受客户端提交的值。本仓内无任何此类声明;
34+
ADR-0117 已将该名收为协议保留的系统列名。
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): `flow-runas-unscoped` now sees data nodes nested in a `loop` body / `parallel` branch / `try_catch` region (#5633)
6+
7+
**This widens the coverage of a build-GATING rule.** `flow-runas-unscoped` is
8+
`severity: 'error'`, so a flow it newly catches goes from a green build to a
9+
failed one. That is the correct outcome — those flows cannot run at all — but it
10+
is a real blast radius and the reason this shipped as its own change rather than
11+
riding along with #5383.
12+
13+
**What was wrong.** #5383 gave the flow anti-pattern family a per-region walk and
14+
deliberately left this one rule reading the flow's **top-level** `nodes` only. Its
15+
data-node search is the rule's evidence that the flow *performs a data operation
16+
at all* — and a data node inside a `loop` body is exactly as unscoped as one at
17+
the top level. So a scheduled flow that queried a set, looped it, and wrote per
18+
item passed `os build` / `os validate` clean and was then **refused at run time**:
19+
since #3760 a user-less run really does refuse the data operation rather than
20+
running it unscoped. Passing the build and then being unable to run is precisely
21+
what promoting this rule to `error` was for, and the shape it was missing —
22+
query, loop, write per item — is *the* standard shape for a scheduled flow, so
23+
the write is almost always the nested node.
24+
25+
Measured, same flow with only the node's position changed:
26+
27+
```
28+
update_record at TOP level -> 1 finding [error]
29+
update_record INSIDE loop body -> 0 findings (now: 1 finding [error])
30+
```
31+
32+
**What changed.** The data-node search runs across `collectFlowGraphs(flow)`
33+
every ADR-0031 region, at any depth — while the finding itself stays **flow-level**
34+
exactly as before: one per flow, `where` = ``flow 'x' · runAs``, because `runAs`
35+
is a flow property and the region only supplies the evidence. The region is named
36+
in the **message** so you can find the node:
37+
38+
```
39+
flow 'nightly_sweep' · runAs: schedule-triggered flow runs as `runAs:'user'`, but a
40+
schedule run has no trigger user — so its data node 'touch' (update_record), in loop
41+
'loop_rows' body, has no identity to scope to and will be REFUSED at run time.
42+
```
43+
44+
**Nothing about the top-level case moved.** A flow whose evidence is a top-level
45+
data node produces the byte-identical message it always did (no region clause),
46+
and when a flow has data nodes at both altitudes the top-level one is still the
47+
node cited — `collectFlowGraphs` yields the flow's own graph before it descends.
48+
Both are pinned by tests.
49+
50+
**If this newly fails your build:** the flow was already broken at run time. Add
51+
`runAs: 'system'` to declare the elevation the sweep needs (a schedule /
52+
time-relative / API run has no user to scope to — there is none). See ADR-0049,
53+
ADR-0073 D5, #1888, #3760.
54+
55+
The repo's three example apps (`app-showcase`, `app-crm`, `app-todo`) are
56+
unaffected — `os validate` output is line-for-line identical before and after.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/driver-turso': patch
3+
---
4+
5+
fix(driver-turso): remote 分页读补齐确定性排序,与 local 面共用同一条规则
6+
7+
`TursoDriver` 在 remote 传输(`libsql://` / `https://` 等 URL)下的分页读不满足
8+
`IDataDriver.find` 的确定性分页 MUST:`RemoteTransport.buildSelectSQL` 把调用方的
9+
`orderBy` 原样拼进 SQL 后直接接 `LIMIT` / `OFFSET`,不追加任何唯一列,无序分页读
10+
更是完全不排序。SQLite 不承诺并列行在两条语句之间排布一致,所以表一大、计划一变,
11+
`ORDER BY status LIMIT 50 OFFSET 50` 翻页时就会有记录出现两次、另一条永远不出现 ——
12+
每一页都是满的、每一行都合法,从任何单个响应里都看不出来。
13+
14+
同一个驱动的 local 面早已按 #4363 办事,于是一个驱动的两条传输对同一个分页查询给出
15+
不同的排序保证,而传输模式只由 URL 决定。
16+
17+
修法是**复用**而不是复制:`TursoDriver.find` / `findOne` 现在通过继承来的
18+
`SqlDriver.orderKeysFor()` 解析出完整排序键再交给传输层,三态规则只有一份实现 ——
19+
20+
| `orderBy` | 分页 | 结果 |
21+
|---|---|---|
22+
| 非空 | 任意 | 调用方的键 + `id` |
23+
||`limit`/`offset` | 单独 `id` |
24+
|| 都没有 | 不加 ORDER BY(#4363 carve-out,原样保留) |
25+
26+
`findOne` 的语义一并保住:它的 `limit: 1` 由传输层自己注入,若在 `buildSelectSQL`
27+
里判定就会被误读成「页大小为 1 的第一页」,从而给系统里最热的读加上
28+
`ORDER BY id LIMIT 1` —— 正是让计划器放弃谓词自身索引的形状。
29+
30+
唯一列的判定沿用 local 面同样保守的前提:只有本驱动自己建的表才追加 `id`
31+
(`RemoteTransport` 建表时无条件写入 `"id" TEXT PRIMARY KEY`);不是自己建的表保持
32+
原样并告警一次,绝不凭空发明排序列。

.claude/skills/pm-dispatch/SKILL.md

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,12 @@ issues that collide on shared files — and「谁来分诊」原本是每个 PM
346346
—— Routine 座位查调度器(`last_fired` / `next_run`),会话座位查它最近一条
347347
产出评论的时间戳,**>24h 无产出即可回收**(编辑该行 + 一条审计评论)。子树/
348348
批次里在飞的认领仍按认领协议由原认领者跟完。
349+
- **每轮巡检核对自己的正文行。** 协议或表格结构升级会迁移状态:2026-08-05
350+
spec 座位的在任 PM(接管时按升级前惯例只发过登记评论)在正文表格化迁移后
351+
被记为「⏳ 待认领 / 前任已收官」—— 而上一条的惰性回收会把这个错位变成
352+
误回收,别的会话也可能照着「待认领」真来接管。自查一行的成本是零;发现
353+
不符,当场改行 + 审计评论,不等冲突发生。这与「从 labels 重建状态」同源:
354+
正文行也是状态,读它、修它,不靠记忆。
349355
- **epic 委托不在座位表登记** —— `pm:epic` 父单正文自带会话与领地,
350356
`label:pm:epic` 即全量索引(见「Epic 子树车道」)。座位表只记常设座位,一件
351357
事只记一处。
@@ -987,8 +993,11 @@ disobedience.
987993

988994
`/compact`, and any host-level interruption of the PM session, kills running
989995
subagents together with their pending tool calls — #4700 and #4775 both died on
990-
the same second. **The agent is not resumable; its worktree, branch and commits
991-
are intact.** Never re-run the original dispatch prompt over that state: a fresh
996+
the same second. **先试 SendMessage 复活,再谈接手**:对已死 agent 发一条消息,
997+
宿主会「从 transcript 恢复」—— 带着它全部上下文接着干,worktree、分支、提交
998+
都还在(2026-08-05 三个死 agent 全部由此复活并各自收尾,见 step 6 探活)。
999+
仅当 resume 不可用(跨会话接手、transcript 丢失)时才走下面的 worktree 接手
1000+
协议。Never re-run the original dispatch prompt over that state: a fresh
9921001
agent that follows it will try to create the worktree that already exists, or
9931002
redo work already committed. Dispatch a **new** agent with these four additions
9941003
instead:
@@ -1093,9 +1102,26 @@ connector grant 只能传递调用会话自身持有的,CCR 平台注入的 gith
10931102

10941103
### 6. Collect
10951104

1096-
**Subagent mode:** wait for the background task notifications — do not poll,
1097-
do not fabricate a pending agent's result. A dev that dies or returns
1098-
malformed output counts as `status: "blocked"` with its raw output attached.
1105+
**Subagent mode:** wait for the background task notifications — do not poll
1106+
for *results*, do not fabricate a pending agent's result. A dev that dies or
1107+
returns malformed output counts as `status: "blocked"` with its raw output
1108+
attached.
1109+
1110+
**探活是每轮巡检的固定动作 —— 完成通知不可靠,它的缺席什么都不证明。**
1111+
下面的停摆纠偏处理「带任务中状态的通知到了」;这一条处理更隐蔽的另一半:
1112+
**通知根本不来**。宿主进程重启会把运行中的 subagent 连同其完成通知一起
1113+
静默杀掉 —— 2026-08-05 实测,五个「在飞」dev 里三个(#5050/#5515/#5483)
1114+
已死数小时,批次视图仍显示 5/5,实际吞吐 2/5,零信号。规程三条:
1115+
1116+
- 每次巡检(定时器唤醒、轮间隙)对**每个已派发且尚无远程分支/PR**
1117+
dev 发一次状态询问(SendMessage,措辞「回一段简报后继续干活」,不改变
1118+
任务);派发后 ~45 分钟无任何远程产出即到探活门槛。
1119+
- 两种回包都有价值:活着 → 拿到进度与阻塞点;**「no active task; resumed
1120+
from transcript」→ agent 生前已死,这次询问本身就是复活** —— 从其
1121+
transcript 带全部上下文恢复,比 worktree 接手协议(step 5)便宜得多,
1122+
优先用它;resume 不可用时才走接手协议。
1123+
- 判据永远取正向证据(远程分支、PR、报告、探活回包),⛔ 绝不把「还没
1124+
收到失败通知」读作「还在跑」。
10991125

11001126
**A stalled subagent is this half's most common failure, and it never
11011127
self-heals.** When a dev stops mid-task reasoning that "a background watcher will
@@ -1142,6 +1168,14 @@ dispatch 由下一轮按同一判据处置(~2h 无报告即 `blocked`),`delete_t
11421168
Routine 的取舍是:凡验证管线可能超过一个 fire 的活,**一开始就走 `mode:cloud`**,
11431169
把恢复权交给下一轮的 GitHub 读数,而不是赌它能在本轮内被唤醒。
11441170

1171+
**报告丢失 ≠ 验收停摆(直接验收兜底)。** dev 的 JSON 报告是证据来源之一,
1172+
不是验收的先决条件 —— 状态模型第一句就是「所有状态在 GitHub」。同时满足
1173+
(a) draft PR 已存在且 CI 全绿、(b) 探活确认 agent 已死或 ≥2h 无任何推送、
1174+
(c) 报告未达 —— 则 PM 直接按 PR 验收:逐文件核对 diff 与认领申报的文件面,
1175+
对照 `origin/main` 复核 PR 正文的前提声明与验证叙述,step 7 其余判据不变
1176+
(2026-08-05 的 #5550/#5556 即此路径落地并合并)。顺序保护:agent 可能还
1177+
活着时**先探活、后翻 ready** —— 抢先翻会与它的收尾推送竞态。
1178+
11451179
### 7. Review each report
11461180

11471181
You are the reviewer of record. For each report, verify against GitHub — not
@@ -1187,6 +1221,22 @@ against the report's own claims:
11871221
correction belongs in the PR/issue comments so the next reader inherits the
11881222
corrected premise, and a wrong premise still sitting in an issue body gets
11891223
its own follow-up issue rather than being silently dropped.
1224+
- **验收判据本身也是前提的一部分,可被 dev 证伪。** #5452 的 issue 把验收写成
1225+
「某条字面 grep 归零」,dev 实测证明该 pattern 修前修后命中数不变(修好的
1226+
正确输出同样匹配它),于是改钉真不变量(行内代码跨度花括号配平)做门禁,
1227+
并因此多抓出 2 处 issue 的 grep 天然看不见的同根因缺陷。评审姿势:dev
1228+
测量推翻字面判据、换上等价或更强的不变量门禁 = 好运行,照 ACCEPT;但推翻
1229+
过程必须写在 PR 正文里,且新判据要附在 main 语料上的实测信噪比(误报为零
1230+
的证据),否则按 REWORK 要证据。
1231+
- **Tests/docs-only PR 走 `skip-changeset` 标签,不走空 changeset**(空
1232+
changeset 滞留发布,#4898)。标签由 PM 在验收时打。历史坑(#5497/#5502
1233+
实测):该闸曾从**事件载荷**读标签,rerun 重放旧载荷看不见新标签,得靠
1234+
「摘掉再打回」制造新 labeled 事件 —— **#5625(#5580)已根治**,闸门改为
1235+
实时读 PR 标签,rerun 即翻绿。留此一条是因为它是一类通病的标本:**任何
1236+
从事件载荷而非现状读判据的闸,rerun 都复现旧世界** —— 撞上同形状的红,
1237+
先查该闸读的是载荷还是现状,再决定是补事件还是改闸。
1238+
边界:改动若含读者可见的生成产物(如参考文档),dev 选 changeset 而非
1239+
标签是对的 —— 以 PR 正文说明的理由为准,两条路都有效,别来回改。
11901240
- **`+0/-0` in a PR diff is not proof of an empty file.** git renders a file
11911241
as binary — zero added, zero removed — as soon as it contains a NUL byte.
11921242
#4870's 347-line test file showed `+0/-0` and was briefly misread as an

docs/adr/0117-owning-business-unit-record-stamp.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
# ADR-0117: 记录级业务单元归属(owning business unit)
22

3-
- **状态**: Proposed(提案,待评审)
4-
- **日期**: 2026-07-31
3+
- **状态**: **Accepted (D1/D3 scoped)**(2026-08-05)—— 仅 **D1**`ownership` 新增
4+
`business_unit` 一档与 `owning_business_unit_id` 记录戳的命名与语义)与 **D3**
5+
`record.organization_id == sys_business_unit(owning_business_unit_id).organization_id`
6+
不变量)进入协议。**D5、D2 的默认盖章策略、D8 的启用门粒度、D4 的权限位选择仍为
7+
Proposed**,见文末「未决问题」——合并本 ADR ****构成对这四项的裁定,它们需各自单独评审。
8+
- **日期**: 2026-07-31(提案)/ 2026-08-05(scoped 接受)
9+
- **裁定依据**: #4611(ADR-0105 D13 的「scoping field」无元数据落点)维护者 2026-08-05
10+
选 1 —— 加速 ADR-0117,使 promotion 获得可校验的后置条件。该裁定回答了本 ADR 未决问题
11+
第 2 条(这一档值得新增),其余四条未被触及。
512
- **关联**: ADR-0057(BU 树与深度档位)、ADR-0090(岗位与任职锚点)、ADR-0091(授权时效)、
613
ADR-0103(`managedBy` 写策略)、ADR-0105(租户姿态与 org 作用域)
714
- **动因**: 集团管控场景需要"记录属于哪个部门/法人"成为结构事实,而不是从所有者推导
@@ -186,14 +193,34 @@ unit 档用户眼前抹掉。因此:
186193
- D7 豁免清单需长期维护,遗漏的故障形态(子公司看不到集团主数据)在测试里不显眼,
187194
需要专门的 conformance 用例守住。
188195
189-
## 未决问题(提交评审)
196+
## 落地状态(2026-08-05,scoped 接受时)
197+
198+
被接受的 D1/D3 是**协议决定**,其运行时执行分两步落地,本轮只完成第一步:
199+
200+
| 面 | 本轮(#4611) | 后续 |
201+
|---|---|---|
202+
| 规范名 `owning_business_unit_id` | ✅ 已登记为 `SystemFieldName.OWNING_BUSINESS_UNIT_ID`,标注 **open-core 暂不注入**,并进入公开表单 server-managed 拒收名单(防御纵深,匿名面永不可由客户端提供) | —— |
203+
| `ownership: 'business_unit'` 枚举档 | ❌ **本轮不加**。`packages/objectql/src/registry.ts``wantOwner` 是**排除式**判定(只排除 `org` / `none`),此时加入枚举会让该档照常注入 `owner_id`,与 D1 表格相反 —— 属 ADR-0049 所禁止的「声明而不执行」 | #5678(须与 #5677 同 PR 或严格后置) |
204+
| 列注入(`wantOwner` 翻为正面清单 + 列) | ❌ 未实现 | #5677(engine-core 车道) |
205+
| 盖章策略(D2)/ D3 校验 / D4 守卫 / D8 迁移 | ❌ 未实现,且 D2 默认值等四项**尚未裁定** | 各自单独评审后再开单 |
206+
207+
即:**协议已接受,执行待实现**;在注入落地前,`ownership: 'business_unit'` 会被 Zod
208+
以「合法值为 user / org / none」响亮拒绝,这是**正确**行为,不得「顺手补全」。
209+
210+
## 未决问题
211+
212+
**已裁定**
213+
214+
- ~~2. **`ownership: 'business_unit'` 是否值得新增**~~ —— **是**(#4611 维护者 2026-08-05
215+
裁定选 1)。ERP 场景(库存、台账、部门预算)有真实需求,且 promotion 的可校验后置条件
216+
依赖它。
217+
218+
**仍待评审(合并本 ADR 不视为通过)**
190219
191220
1. **D5 的偏离**:法人归属做成解析规则而非物化列,是否接受?(上游表述为"盖章时
192221
物化"。)若坚持物化,需先决定多态外键 vs 两个可空列。
193-
2. **`ownership: 'business_unit'` 是否值得新增**,还是让这类对象用 `ownership:'user'`
194-
并接受一个名义所有者?(Dataverse 没有对应档位;ERP 场景有真实需求。)
195-
3. **`pinned` 作为默认值**是否正确——平台既有对象以 CRM 形态居多,默认 `pinned`
222+
2. **`pinned` 作为默认值**是否正确——平台既有对象以 CRM 形态居多,默认 `pinned`
196223
会与它们的直觉相反;但对新建的 ERP 类对象,默认 `follow_owner` 更危险。
197-
4. **D8 启用门的粒度**:按对象启用,还是按部署一次性启用?前者迁移更平滑,后者
224+
3. **D8 启用门的粒度**:按对象启用,还是按部署一次性启用?前者迁移更平滑,后者
198225
语义更简单。
199-
5. **是否需要独立的权限位**(如 `allowChangeOwningUnit`)而不是复用 `allowTransfer`
226+
4. **是否需要独立的权限位**(如 `allowChangeOwningUnit`)而不是复用 `allowTransfer`

0 commit comments

Comments
 (0)