Skip to content

Commit a23dabe

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-4756-sys-comment-dead-fields
2 parents ae464eb + c4ab50b commit a23dabe

27 files changed

Lines changed: 1719 additions & 669 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/metadata": patch
3+
---
4+
5+
fix(metadata): `sys_metadata` 的 DDL 失败不再被静默吞掉 —— 只有「表已存在」这一种原因可以静音 (#4728)
6+
7+
`DatabaseLoader.ensureSchema()` 过去用一个空 `catch` 吞掉 **全部** DDL 失败,并且照样把
8+
`schemaReady` 置为 `true`:
9+
10+
```ts
11+
} catch {
12+
// If syncSchema fails (e.g. table already exists), mark ready and continue
13+
this.schemaReady = true;
14+
}
15+
```
16+
17+
注释里的免责理由只覆盖了失败原因中最良性的一种,却用它为**所有**原因开脱。真实的失败
18+
(权限不足、数据源根本没连上、列类型冲突)之后,表或新列压根不存在,而进程的状态与成功
19+
路径**逐字节相同**,启动日志里一行痕迹都没有 —— 这正是 #4420 的形态:声称已持久化、实
20+
际没落盘、系统看起来完全健康。#4632 把它定成规则(AGENTS.md → "Degradation log levels"),
21+
机械检查 `pnpm check:durability-log-level` 已经能发现这一处。
22+
23+
现在按**错误类型**判别,而不是按注释里的乐观假设:
24+
25+
- **良性的「已存在」**(SQLite 的 `table … already exists` / `duplicate column name`
26+
Postgres 的 SQLSTATE `42P07`/`42701`/`42710`、MySQL 的 `ER_TABLE_EXISTS_ERROR` 等及其
27+
`errno`,并跟随 `cause` 链)—— 表确实已就绪,当作 no-op 静默通过,并照常执行后续的
28+
`project_id → environment_id` 迁移与 ADR-0005 索引。
29+
- **其余一切失败** —— 以 `console.error` 上报,文案同时说清**后果**(`sys_metadata` 的表/
30+
列未创建,后续每一次元数据写入都会报错、或在宽松驱动上悄悄丢列,而服务器仍报告健康)
31+
**修复动作**(修掉下面那条驱动/数据源错误后重启)。只说**一次**,不是每次写入都刷屏。
32+
- `schemaReady` **不再**在真实失败后置 `true`。启动依旧不被阻断(该方法不抛),但 loader
33+
不再声称一个它并不具备的就绪状态,下一次元数据操作会重试 —— 数据源只是还在连接这类瞬
34+
时故障因此可以自愈,恢复时补一条 `info`
35+
36+
`ensureHistorySchema()` 按同一规则对齐:良性「已存在」不再每次写入都打一条 `error`(过度
37+
使用 `error` 是镜像失败),真实失败则同样只响亮一次并保持重试。
38+
39+
无 API / schema 变更;新增内部工具 `isSchemaAlreadyExistsError()`(未从包入口导出)。
40+
`scripts/durability-degradation.baseline.json` 中指向本单的条目随之删除(该文件 shrink-only)。
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
docs(protocol): `protocol/kernel/http-protocol` 的 API Discovery 一节拆成两段式 —— `@objectstack/rest` 服务的 `/api/v1`(与 `/api/v1/discovery`)与 dispatcher 服务的 `/.well-known/objectstack` 各给一份真实响应形状,不再共用一份混合示例。Docs-only;releases nothing.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/objectql': minor
3+
'@objectstack/cli': patch
4+
---
5+
6+
修复:每个 `os migrate` 子命令关停后,#4551 悬空引用巡检都会把 `sys_metadata` / `sys_view_definition` 报成 `unreadableObjects`(#4747)
7+
8+
一条**成功**的命令过去会在返回 JSON 之后打出两行 `ERROR Find operation failed` 和一份
9+
`unreadableObjects` 非空的巡检报告 —— 对抓 ERROR 的 CI 流水线是直接误报源,更要命的是它把
10+
`unreadableObjects` 变成了恒为真的告警:那个桶存在的意义正是区分「我没能检查」和「我检查了,
11+
没问题」,一个每次健康运行都非空的桶不再携带任何信息。
12+
13+
两处静默空转叠出了这个结果:
14+
15+
- `ObjectQLPlugin` 的关停逻辑写在 `stop()` 里,而内核的插件契约是 `init`/`start`/`destroy` ——
16+
`stop()` 从来没有被任何人调用过,ADR-0057 巡检定时器因此在任何宿主上都不会被解除。改为
17+
`destroy()`(与 `DefaultDatasourcePlugin` 一致)。
18+
- `bootSchemaStack().shutdown()` 调的是 `(runtime as any).stop?.()`,而 `Runtime` 根本没有
19+
`stop` —— 可选调用把「没有关停」伪装成了「关停过了」。改为走内核自己的 `kernel.shutdown()`,
20+
`os serve` 收到 SIGTERM 时同一条路径。
21+
22+
同时 `LifecycleService.stop()` 不再只是清定时器:它还会把「引擎正在拆」这一位交给正在飞行中的
23+
sweep,巡检据此在读之前停手。因关停而失败的读**不再进入** `unreadableObjects` —— 那不是关于
24+
数据源的证据;报告改用新增的 `DanglingReferenceReport.aborted` 记录「这次没跑完」,所以不完整
25+
依然是响的,只是不再占用发现桶。
26+
27+
**真正读不出来的对象(数据源故障)照旧进 `unreadableObjects`**,巡检在 CLI 场景也照旧运行 ——
28+
这里没有「一次性命令不跑巡检」的开关,只有「引擎活着才读」的生命周期边界。
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: `@objectstack/spec/system` no longer exports the orphan notification-template vocabulary — `EmailTemplate(Schema)`, `SMSTemplate(Schema)`, `PushNotification(Schema)`, `InAppNotification(Schema)` (#4616)
6+
7+
These four schemas existed **only** as the member shapes of the
8+
`NotificationConfigSchema.template` union, and #4610 (#4535 C3) deleted that
9+
union. Since then they have been reachable from no parent schema and from no
10+
metadata-type root: nothing in framework, cloud or objectui parsed a document
11+
against them, so they declared delivery capability the runtime never read
12+
(ADR-0049 enforce-or-remove, resolved by REMOVE in the v17 breaking window).
13+
14+
Migration — one line each, and in every case the replacement already exists:
15+
16+
- FROM `import { EmailTemplateSchema, type EmailTemplate } from '@objectstack/spec/system'`
17+
TO `import { EmailTemplateDefinitionSchema, type EmailTemplateDefinition } from '@objectstack/spec/system'`.
18+
**Shape change** — this is a different, richer contract, not a rename:
19+
`EmailTemplateDefinitionSchema` is keyed `name` + `locale` (not `id`), splits
20+
the body into `bodyHtml` / `bodyText` (not `body` + `bodyType`), and adds
21+
`label` / `category` / `active` / `fromOverride` / `replyTo`. It is also a
22+
`strictObject`, so the old keys are rejected loudly rather than stripped.
23+
This is the schema the `email_template` metadata kind has resolved to since
24+
spec **7.1.0**, which demoted `EmailTemplateSchema` when it fixed that Prime
25+
Directive #8 double-declaration and kept it "only as an inline sub-shape
26+
inside `Notification`" — #4610 removed that holder, and #4616 finishes the
27+
job. If your code registers a client-side or publish-time validator for
28+
`email_template`, it must point at `EmailTemplateDefinitionSchema`;
29+
`BUILTIN_METADATA_TYPE_SCHEMAS` (`kernel/metadata-type-schemas.ts`) is the
30+
authority.
31+
- FROM `import { SMSTemplateSchema, type SMSTemplate } from '@objectstack/spec/system'`
32+
TO: no spec replacement, and none is needed. SMS templates are
33+
`sys_notification_template` rows resolved by `(topic, 'sms', locale)`
34+
(`service-messaging/src/sms-channel.ts`) and rendered by
35+
`template-renderer.ts`; the provider-side template is Aliyun's pre-registered
36+
`TemplateCode` in `service-sms` — a vendor API shape, never a spec constant.
37+
- FROM `import { PushNotificationSchema, type PushNotification } from '@objectstack/spec/system'`
38+
and FROM `import { InAppNotificationSchema, type InAppNotification } from '@objectstack/spec/system'`
39+
TO: no replacement. Neither channel has a delivery implementation (#3197):
40+
the dispatcher dead-letters any message addressed to them, so these payload
41+
shapes advertised a capability nothing delivers. The live delivery ingress is
42+
`NotificationService.emit` (`INotificationService`,
43+
`@objectstack/spec/contracts`); the in-app bell reads `./api`'s
44+
`Notification(Schema)` inbox row; the presentation vocabulary is
45+
`@objectstack/spec/ui` (`NotificationTypeSchema`, `NotificationSeveritySchema`,
46+
`NotificationPositionSchema`, `NotificationActionSchema` — all unchanged).
47+
48+
Unchanged and explicitly NOT part of this removal:
49+
`@objectstack/spec/system`'s `NotificationChannel(Schema)` (live — re-exported
50+
by `@objectstack/spec/contracts`, consumed by `service-messaging`),
51+
`EmailTemplateDefinition*`, and every `@objectstack/spec/ui` notification
52+
export.
53+
54+
No ADR-0087 D2 conversion accompanies this change, deliberately: a conversion
55+
rewrites authored or stored sources, and these defs were reachable from no
56+
metadata-type root, so `os migrate meta` would have nothing to match. The
57+
removal is a TypeScript export-surface break only — same disposition as #4610
58+
in this very module. `json-schema.manifest.json` loses 4 keys and
59+
`authorable-surface.json` loses their 22 lines; both deletions are adjudicated
60+
by `gen:schema`'s #4650 route-3 check ("def no longer emitted by this build").
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
---
3+
4+
docs(pm-dispatch): domain 车道协议 —— 按「修复落点的包」划域,支持同仓多 PM 并发 (#4819)
5+
6+
`.claude/skills/pm-dispatch/SKILL.md` 新增「Domain lanes(同仓多 PM 并发)」一节:
7+
锚定规则(每个包恰好属于一个 domain,`domain:*` 标签取**修复落点所在包**的域,分诊时
8+
读代码后打,不从标题词汇猜 —— #4775 的 hook condition 概念属 automation,落点却是
9+
`packages/objectql/src/hook-wrappers.ts`,故归 `domain:engine`)、六域分类表、标签纪律
10+
(打标 ≠ 认领;未打标不得认领)、认领范围(在 #4604 登记 domain 集合)、跨域单与借单
11+
规则、选批时的全局在飞检查,以及合并队列仍是全体共享串行资源的提醒(flaky 税,#4796)。
12+
认领注释模板加「域」「文件面」两行(跨域与借单必填);Multi-repo coordination 规则 4 的
13+
「同队列多 PM 一律禁止」改为「仅在 domain 车道协议生效时允许」,repo 分片阶梯保留,
14+
domain 车道作为第三级。
15+
16+
仅改内部 agent 协议文本,不发布任何包。

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

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,13 @@ known case: accepting a `repo:objectui` PR ⇒ file a `pm:queue` issue in
102102
`objectstack` — "run `pnpm objectui:refresh` and land the console bump",
103103
referencing the merged PR, blocked-by it until it actually merges.
104104

105-
**4. Multiple PM sessions shard by repo — never share one queue.** The
106-
claim protocol makes concurrent PMs *safe*, not *useful*: batch
107-
independence (file-disjointness) is only checked within one PM's view, so
108-
two PMs on the same queue can claim different issues that collide on
109-
shared files, and the merge queue is one lane regardless. Scaling order:
105+
**4. Multiple PM sessions shard by repo; one shared queue only under
106+
domain lanes.** The claim protocol makes concurrent PMs *safe*, not
107+
*useful* on its own: batch independence (file-disjointness) is only checked
108+
within one PM's view, so two PMs on the same queue can claim different
109+
issues that collide on shared files, and the merge queue is one lane
110+
regardless. Making that check **global** is exactly what the next section
111+
does. Scaling order:
110112

111113
1. One PM, bigger batch (`batch:5` is the maintainer's chosen operating
112114
point, riding on the resource discipline above), heavy tasks via
@@ -115,7 +117,13 @@ shared files, and the merge queue is one lane regardless. Scaling order:
115117
repo** as its shard (`/pm-dispatch repo:objectstack-ai/objectui`) —
116118
file universes are disjoint by construction. A sharded PM states its
117119
shard in every claim comment and **never claims outside it**.
118-
3. Multiple PMs on the SAME queue: prohibited — all cost, no throughput.
120+
3. Multiple PMs on the SAME queue: **prohibited unless the Domain-lanes
121+
protocol (next section) is active** — every PM in its own session and
122+
its own container, domain sets registered in the registry issue, label
123+
discipline observed, and the global in-flight check run at every batch
124+
selection. Without that protocol the ban stands as written: all cost,
125+
no throughput, and the collision stays invisible to both PMs until the
126+
merge.
119127

120128
**Shard ownership is registered, never assumed.** A registry issue in the
121129
main backlog (`[PM] 分片分工登记表`) records which session owns which
@@ -159,6 +167,81 @@ gives the maintainer a single view (filter by `repo:*` and `pm:*`). The PM
159167
maintains no tracking state outside GitHub — that invariant is what keeps
160168
the loop resumable and the board honest.
161169

170+
## Domain lanes(同仓多 PM 并发)
171+
172+
Rule 4's ladder ran out at one PM per repo because file-disjointness is only
173+
ever checked inside one PM's own view. Domain lanes are the **third rung**:
174+
one PM's triage verdict is cached as a `domain:*` label every other PM can
175+
read, so batch selection filters at the label layer instead of at the merge.
176+
Premise: each PM is its **own session in its own container** — adding a PM
177+
adds compute, not contention — and collisions are prevented by the
178+
domain→package mapping, not by hoping two PMs pick different work.
179+
180+
**Anchoring rule.** The whole scheme rests on this one sentence:
181+
182+
> Every package belongs to exactly **one** domain; an issue's `domain:*`
183+
> label is the domain of **the package the fix lands in**, decided at triage
184+
> by reading the code — **never guessed from the issue's title vocabulary**.
185+
186+
The counter-example that makes it a rule: #4775 is a hook `condition`, which
187+
reads as automation, but the fix lands in
188+
`packages/objectql/src/hook-wrappers.ts``domain:engine`. Labeling by topic
189+
would have routed it to a different PM than the one already inside that
190+
package — the exact collision lanes exist to prevent. If you cannot say which
191+
file the fix touches, you have not triaged it yet, and it is not labelable.
192+
193+
| 标签 | 包家族 |
194+
|:--|:--|
195+
| `domain:engine` | `packages/objectql``packages/metadata*``packages/platform-objects``packages/core``packages/plugins/driver-*` |
196+
| `domain:services` | `packages/services/*``packages/plugins/plugin-approvals``plugin-webhooks``packages/connectors/*` |
197+
| `domain:identity` | `packages/plugins/plugin-auth``plugin-security``plugin-sharing``plugin-audit` |
198+
| `domain:devx` | `packages/lint``skills/**``content/docs/**``scripts/`(门禁类) |
199+
| `domain:spec` | `packages/spec` 及其生成物(现 spec 车道不变) |
200+
| `domain:cli` | `packages/cli``runtime``verify``qa``types` |
201+
202+
`examples/**` belongs to the subsystem it exercises; anything that fits
203+
nowhere is judged at triage by its principal landing site. A package missing
204+
from the table is classified the first time it is triaged and the table
205+
updated **by PR** — the taxonomy evolves deliberately, never per-claim.
206+
207+
**Label discipline.** `domain:*` is applied during the backlog sweep (round
208+
loop step 0) by whichever PM triages the issue. **Labeling ≠ claiming**: any
209+
PM may label any issue, including ones it will never claim — the label is
210+
shared routing, not a reservation. An **unlabeled issue may not be claimed by
211+
anyone**: triage and label it first, or selection has silently gone back to
212+
happening inside one PM's private view.
213+
214+
**Claim scope.** Each PM session registers its **domain set** in the registry
215+
issue (`[PM] 分片分工登记表`, #4604 — the same registry that records repo
216+
shards) and claims only issues whose label falls inside that set. A set, not
217+
a single domain: lanes are a routing table, not a job title.
218+
219+
**Cross-domain issues.** Prefer the contract-first split of rule 2 — one
220+
sub-issue per domain, each carrying its own `domain:*` label, ordered with
221+
`Blocked-by:`. When a split costs more than it buys, a single PM claims the
222+
whole issue and **declares the full file surface** in its claim comment, so
223+
every other PM's in-flight check can see all of it.
224+
225+
**Borrowing.** An idle PM may claim outside its registered set when all three
226+
hold: (a) that domain's PM has not claimed the issue, (b) the claim comment
227+
declares the file surface, (c) the global in-flight check below passes.
228+
Borrowing is a one-issue exception, not a lane transfer — the registry entry
229+
does not change, so nobody has to guess who owns the domain afterwards.
230+
231+
**Global in-flight check — run it at batch selection (step 3).** List every
232+
`pm:dispatched` issue across the repo, read the file-surface declaration on
233+
each one's latest claim comment, and require your candidates to be disjoint
234+
from all of them. This is step 3's independence test raised from your batch
235+
to the whole repo; skip it and two individually-independent batches are
236+
jointly dependent, which is precisely the failure the same-queue ban was
237+
protecting against.
238+
239+
**The merge queue is still one shared serial resource.** Lanes buy parallel
240+
authorship, not parallel landing: the flaky-test tax (#4796) scales linearly
241+
with the number of PMs, and a red queue blocks every lane at once. Queue
242+
health is therefore a shared duty — a PM that notices a flake fixes or files
243+
it rather than re-queuing past it, whichever lane it came from.
244+
162245
## The round loop
163246

164247
### 0. Backlog sweep — classification is a standing duty, not a request
@@ -280,6 +363,12 @@ execute atomically, in order:
280363
> 会话:`session_<id>`
281364
> 分支:`claude/issue-<n>-<slug>`
282365
> Worktree:`<repo>-issue-<n>`
366+
> 域:`domain:<x>`
367+
> 文件面:`<预计触碰的目录列表>`(越界即停,报告说明)
368+
369+
「文件面」is **required** for cross-domain and borrowed claims and
370+
**recommended** for ordinary same-domain ones — it is the only input
371+
another PM's global in-flight check has to read.
283372
3. **Race check**: assignment is idempotent, so two agents can both
284373
"succeed". Re-read the comments; if an earlier claim comment with a
285374
*different* session ID or branch name exists, you lost — touch nothing of

0 commit comments

Comments
 (0)