Skip to content

Commit fc5eb85

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-5853-category-title-abbreviations
2 parents 0cdbc0e + cca11e9 commit fc5eb85

18 files changed

Lines changed: 1862 additions & 88 deletions

.changeset/analytics-read-scope-compile-failed-500.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,12 @@ the disclosure question to be re-decided message by message).
6767
`@objectstack/service-analytics` (ADR-0112 D3) and typed as
6868
`RegisteredErrorCode` at the constructor, so an unregistered code is a compile
6969
error. It is legible on the wire through the sibling `/analytics/query` exit,
70-
which puts a thrown `err.code` in `error.details.code` (#3842).
70+
which puts a thrown `err.code` at **`error.code`** (#3842) — read it there.
71+
`errorResponseBase` only stages the code inside a `details` object;
72+
`buildApiError` then runs `splitSemanticCode`, which promotes it into the
73+
declared `error.code` field and drops the now-empty `details`, so the key is
74+
omitted from the body and `error.details.code` is never present:
75+
`{"success":false,"error":{"code":"READ_SCOPE_COMPILE_FAILED","message":"Internal server error","httpStatus":500}}`.
7176

7277
**Which inputs are refused did not change.** No refusal condition moved: nothing
7378
that used to lower now throws, and nothing that used to throw now lowers. That is
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
"@objectstack/lint": minor
3+
"@objectstack/formula": minor
4+
---
5+
6+
feat(lint): view/page 可见性谓词的裸标识符构建期闸门 —— 坏谓词发不出去(#6128)
7+
8+
新增 **error 级** 规则 `visibility-bare-identifier`:view/page 的可见性谓词
9+
(`visibleWhen` 及其两个已弃用别名 `visibleOn` / `visibility`)里引用了任何绑定根都解析不到的
10+
顶层标识符时,`os validate` / `os build` / `os lint` 一律拒收。写成 `status == 'active'`
11+
而不是 `record.status == 'active'` 的谓词,从此发不出去。
12+
13+
#5149 维护者 2026-08-06 裁决的构建期半边落地(运行时 warn-once 半边已由 objectui#3541 合入)。
14+
本仓传统的准确表述是:fail-open 或 fail-closed 都可以裁,**静默不可以**。谓词失败仍然 fail-open
15+
(已发货 app 行为不变),但坏谓词不再能进入产物。
16+
17+
**为什么现有两道闸都放行**(#5149 Repro 1 实测,已写进规则注释,防后人误并):
18+
ADR-0032 的标识符闸(`validate-expressions.ts`)解析 record 作用域的裸引用,但它的遍历只覆盖
19+
objects / flows / actions / sharingRules / hooks,**从不走 views 与 pages**;ADR-0089 D3b
20+
只判**有根**的谓词根错层(runtime 面的 `data.`、metadata 面的 `record.`),**无根**的谓词两边都不匹配。
21+
两闸之间正好漏掉「作者按文档示例写了裸字段名 → 谓词永远解析失败 → 控制台 fail-open 静默显示」。
22+
23+
**判定由两个既有 oracle 合成,本包不自建 CEL 环境**(#4812 的教训):声明性判定取
24+
`@objectstack/formula``firstUndeclaredReference`(即 `validateExpression` 给 record 作用域
25+
裸引用定罪的同一个严格环境),AST 取规范入口 `parseCelToAst`。AST 先收集所有处于**接收者位置**
26+
的标识符(`a.b` / `a?.b` / `a['b']` / `a.exists(…)`)并在检查前声明它们,于是只剩「当作裸值引用」
27+
的标识符会被判 —— 未知****(`my_record.x`)交还给 ADR-0089 D3b,不在本规则射程内。
28+
29+
**#4953(全量 vs 稀疏绑定)的边界**:#4953 实测同一求值器在两种绑定下语义相反
30+
(`has(record.a)` 全量 true / 稀疏 false;`record.a != null` 全量 false / 稀疏 FAULT)。本规则
31+
**按构造与该分叉无关** —— 它从不追问某个 KEY 在已绑定的根上是否存在,只追问标识符有没有根,
32+
而无根标识符在两种绑定下都解析不到。`has(record.x)` / `record.x != null` 等守卫写法在本闸门下
33+
一律绿,无论 #4953 最终怎么裁;已加测试钉住这条边界。
34+
35+
**遍历按实测修正,否则规则生来即死**:`os build``examples/app-showcase` 得到的唯一一条
36+
view 表单谓词落在 `views[0].formViews.edit.sections[0].fields[6].visibleWhen` —— 运行时 app 形状下
37+
`views[]` 条目是**视图容器**(`ViewSchema` 声明的自有键就是 `list` / `form` / `listViews` /
38+
`formViews`),`sections` 在下一层。原遍历只读 `views[].sections`,在这份 stack 上报告「干净」。
39+
现在覆盖容器的 `form` 与每个 `formViews.<key>`,以及仍然直接携带 `sections``defineForm` 形状;
40+
pages 改走共享的 `walkPageComponents`(regions、slotted 页的 `slots`、以及 `properties` 里的
41+
`page:tabs` / `page:accordion` / `page:card` 子树都随之覆盖,source-authored 页按其既有语义跳过)。
42+
`objects[].views` 明确不读 —— 该键已被 schema 立碑拒绝,读它只会造出一条永不触发的幽灵检查。
43+
两条既有 ADR-0089 D3b advisory 随遍历一并变得真正可达。
44+
45+
注册表 tier `advisory``gating`(#5762 的先例):tier 声明并非自述,
46+
`authoring-rule-wiring.test.ts` 会读规则源码核对。
47+
48+
已知盲点(已钉测试、方向安全):字段名与 CEL **类型名**相同时(`type` / `int` / `string` / `list`
49+
/ `map` / `timestamp` …)不判 —— CEL 自身声明这些标识符,`type == 'grid'` 到检查器那里是类型
50+
overload 错误而非未知变量;改读 overload 消息会误杀合法的 `type(record.x) == string`。语法不通过
51+
的谓词同样不判,交还给拥有该判定的闸门。两者都是漏判,永远不会变成误红。
52+
53+
仓内 `app-todo` / `app-crm` / `app-showcase` 三个示例 `os validate` 全部通过、零 visibility finding,
54+
无需修改任何示例内容。
55+
56+
`@objectstack/formula` 侧:公开导出 `firstUndeclaredReference`(理由与既有的
57+
`collectCelRootIdentifiers` 一致 —— 绑定根集合不同的消费方需要的是同一个答案,替代方案是在消费方
58+
自建严格 `Environment`,而那正是 #4812 从本包消费方手里拿掉的私有前端)。
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/runtime': minor
3+
---
4+
5+
**`createStandaloneStack` now dispatches `libsql://` / Turso URLs** instead of refusing them as an unsupported scheme (#5820).
6+
7+
`detectDriverFromUrl()` recognised `memory://`, `postgres://`, `mongodb://` and `file:`, and threw on everything else — while `resolveDatabaseUrl()` listed `TURSO_DATABASE_URL` as one of its URL sources. A host that set it got the URL read in and then rejected on the way out. Since the CLI wired `libsql://` for `os serve` / `os start` (#5602), the same `OS_DATABASE_URL=libsql://…` booted under `os start` and failed under `os migrate`, which comes through this stack.
8+
9+
What changed:
10+
11+
- `libsql://…` and `http(s)://*.turso.…` resolve to the `turso` driver kind — the same two spellings the CLI classifies, kept identical on purpose.
12+
- `databaseDriver: 'turso'` (and `OS_DATABASE_DRIVER=turso`) is accepted by the config schema.
13+
- The driver comes from `@objectstack/driver-turso`, an **optional** install: it drags `@libsql/client` and its native bindings, so it is not a dependency of `@objectstack/runtime`. It is loaded lazily, only for a selection that asks for libSQL, and injected through the driver-factory seam `DefaultDatasourcePlugin` already exposes — so the connect path, the `bootCritical` fail-fast verdict, `OS_ALLOW_DRIVER_CONNECT_FAILURE` and the retained Setup → Datasources status are identical to every other kind.
14+
- Package missing? The boot fails **loudly**, carrying the exact install command (`npm install @objectstack/driver-turso`) as data as well as prose. There is no SQLite fallback: a silent step-down would open an empty local database while your libSQL data stays untouched, and every write — including an `os migrate` DDL — would land in the wrong place (#3276).
15+
- `databaseAuthToken` is no longer declared-and-ignored: the `turso` kind reads it, falling back to `OS_DATABASE_AUTH_TOKEN` and then the vendor's own `TURSO_AUTH_TOKEN` — the same precedence `os serve` uses.
16+
17+
Unknown schemes still throw, and the message now lists `libsql://` among the supported ones.

.changeset/wild-pugs-clap.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
写入载荷里的算子对象在标量字段上被响亮拒收(#5922)
6+
7+
**行为变化**:此前静默入库的算子对象现在被拒绝。`update('task', { title: { $in: ['a','b'] } }, …)` 会抛
8+
`VALIDATION_FAILED`(字段码 `invalid_type`),而不再把 `{"$in":["a","b"]}` 原样交给驱动写进 `title` 列。
9+
10+
原本这条错误的命运取决于字段类型,而不取决于错误本身:`number` 会立刻响亮拒绝(`n must be a number`),
11+
`text` 则零告警落库,之后以「这行的 title 变成了乱码」的形态在读路径上出现,离原因很远。实测(15 种字段类型,
12+
记录型 driver 驱动真实引擎)显示放行的远不止 `text`:`textarea`、未声明 `options``select`、以及
13+
`lookup` 等引用类(ADR-0104 warn-first)同样放行;而 `select`(有 options)/ `url` / `email` / `phone`
14+
之所以拒绝,只是因为 `String({ $in: […] })``"[object Object]"`,恰好过不了它们的正则或选项表 —— 一条
15+
在 4 种类型上偶然成立、在另外 11 种上不成立的规则,作者无法从元数据预测。
16+
17+
现在的规则只有一条:**声明值是标量的字段,一律不接受算子对象**。判定复用 spec 已导出的算子词表
18+
(`ALL_OPERATORS` + `RETIRED_FILTER_OPERATORS`),不是第六份手抄的 `startsWith('$')`,所以协议新增算子当天即
19+
自动收口。消息与 ADR-0104 的形状拒绝同族(同一 `invalid_value_shape` 文案,四语言均已本地化),点名字段、
20+
点名算子、点名声明类型。
21+
22+
刻意不动的两处:`json` 等结构化 JSON 类继续放行(`{ "$in": [...] }` 存在 `json` 列里是用户数据,不是写错的
23+
filter);多值字段保留既有的 `invalid_type_array` 拒绝。`insert``update`(单行与 multi)三个校验入口均已覆盖。

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

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,18 +1306,35 @@ same environment — its own container and fresh clone, decoupled from the PM
13061306
session's lifetime. Use it when devs need resources/lifetime beyond one
13071307
container, or the maintainer asks for it. Requires the `Claude_Code_Remote`
13081308
MCP tools (available in remote/web sessions; if absent, say so and fall back
1309-
to `mode:subagent`). Per issue:
1310-
1311-
1. `create_trigger` with `create_new_session_on_fire: true` and no schedule
1312-
(poke-only), name `pm-dispatch-issue-<n>`, prompt = the dispatch template
1313-
below **made fully standalone**: the fired session starts with zero
1314-
conversation context (it does get the repo clone, so it can be told to
1315-
follow `.claude/agents/os-dev.md`), and — since an independent session
1316-
cannot return a message to the PM — it must be told to **post the JSON
1317-
report as a comment on the issue** (prefixed `<!-- os-dev-report -->`)
1318-
instead of returning it, in addition to opening the draft PR.
1319-
2. `fire_trigger` to launch it, then `delete_trigger` once the report has
1320-
been collected (step 6) so poke-only triggers don't accumulate.
1309+
to `mode:subagent`).
1310+
1311+
**一次性云卡用 `create_session`,⛔ 不用 create_trigger+fire**(维护者
1312+
2026-08-07 拍板;trigger 流只保留给**定时/重复**型 —— 座位 Routine 一节)。
1313+
实测三课,#6083 首派一天踩齐,每一条都写进派发动作:
1314+
1315+
1. **授权面随 source,不随环境。** trigger 拉起的会话**没有仓库授权** ——
1316+
clone(匿名只读)可用,push / 开 PR / 发评论全 403(`not in this
1317+
session's authorized repository set`),`permission_mode: auto` 下也没有
1318+
可弹的授权窗,dev 只能做只读勘察。`create_session``source_url`
1319+
会话**出生即持推送授权**。同时带 `outcome_branch`(= 认领分支,平台托管
1320+
推送)与显式 `model`(trigger 流不可指模型 —— sonnet 默认惊吓即此出处)、
1321+
`title`(客户端卡片名 —— **以车道名开头,⛔ 不叫 os-dev**,维护者
1322+
2026-08-07 拍板:多车道并行时卡片按车道可扫;形如
1323+
`⚡ spec #5599 view 身份前置(裁 B)`,即 `⚡ <车道> #<单号> <短语>`)。
1324+
2. **派发词必须带自驱条款(回合终点约束)。** 云会话是对话形态 —— 回合结束
1325+
就停下等输入,不像 subagent 一口气跑完;不写这条,dev 会在中期汇报或提问
1326+
处停摆,而 PM 只能靠 poke 唤醒。条款原文形:⛔ 不为提问/中期汇报结束回合;
1327+
开放选择按裁决与三轴自裁记入终报 open_questions;合法回合终点只有
1328+
(a) 推送完成 + 终报 JSON 作为最后一条消息,或 (b) 硬阻塞详报。
1329+
3. **交付通道写明降级路径。** 云会话通常没有 GitHub API 工具(连接器不随
1330+
create_session/trigger 传递),派发词写明:开 PR / 发评论失败 ⛔ 不视为
1331+
阻塞 —— 推送 outcome branch + 终报作最后一条会话消息,**PM 代开 draft
1332+
PR、代转录报告**到 issue(权限面不因此放大)。
1333+
1334+
**监控与转向**:`get_session` 读实时状态(status / model / token 用量;
1335+
IDLE + 分支未推送 = 停摆待 poke);投递消息用**绑定会话的 poke 触发器**
1336+
(`create_trigger``persistent_session_id` + `fire_trigger` + 用后即
1337+
`delete_trigger`)。收报:巡检主动读会话终报与分支推送,⛔ 不等推送通知。
13211338

13221339
#### 座位 Routine 化(PM 侧的运行形态,#5472 第 5 点)
13231340

content/docs/data-modeling/drivers.mdx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ Drivers can be selected in two ways:
5454

5555
<Callout type="info">
5656
**Turso / libSQL needs one extra install.** `libsql://` and `*.turso.io` URLs *are*
57-
inferred, but `@objectstack/driver-turso` is an **optional peer dependency** of the
58-
CLI — it pulls in `@libsql/client`, so it is not part of a default install:
57+
inferred — by the CLI (`os serve` / `os start` / `os dev`) and by the standalone
58+
runtime stack the one-shot commands and embedders boot through (`os migrate`,
59+
`createStandaloneStack`) alike. But `@objectstack/driver-turso` is an **optional**
60+
install — it pulls in `@libsql/client` plus native bindings, so it is not part of a
61+
default install:
5962

6063
```bash
6164
npm install @objectstack/driver-turso
@@ -76,7 +79,7 @@ libSQL data stayed untouched. Pass the token with `--database-auth-token`
7679
| **SQLite** | `@objectstack/driver-sql` (peer: `better-sqlite3`) | `SqlDriver` | `sqlite` \| `sql` |
7780
| **SQLite (WASM)** | `@objectstack/driver-sqlite-wasm` | `SqliteWasmDriver` | `sqlite-wasm` \| `wasm-sqlite` \| `wasm` |
7881
| **MongoDB** | `@objectstack/driver-mongodb` | `MongoDBDriver` | `mongodb` \| `mongo` (single-tenant only — see [below](#multi-tenancy-not-supported)) |
79-
| **Turso / libSQL** | `@objectstack/driver-turso` (optional peer of the CLI) | `TursoDriver` | `turso` \| `libsql` |
82+
| **Turso / libSQL** | `@objectstack/driver-turso` (optional install — see the callout above) | `TursoDriver` | `turso` \| `libsql` |
8083
| **Memory** | `@objectstack/driver-memory` | `InMemoryDriver` | `memory` |
8184

8285
> All SQL flavours (PostgreSQL / MySQL / SQLite) are served by a single

packages/formula/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ export { celEngine, DEFAULT_LIMITS } from './cel-engine';
1515
// (approval `expression` approvers): lint and the runtime pre-check share this
1616
// one helper so what they accept can never drift.
1717
export { collectCelRootIdentifiers } from './cel-engine';
18+
// #6128 — the strict-environment "does this identifier resolve?" oracle, the
19+
// same one `validateExpression` gives its `record`-scoped bare-ref verdict from.
20+
// Published for the same reason as `collectCelRootIdentifiers` above: a lint
21+
// rule whose surface declares a DIFFERENT root set (`@objectstack/lint`'s
22+
// view/page visibility gate binds `current_user` / `page` on top of
23+
// SCOPE_ROOTS) needs this exact answer, and the alternative — rebuilding a
24+
// strict `Environment` in the consumer — is the private-front-end mistake
25+
// #4812 removed from that very package. One oracle, one answer to "what
26+
// resolves", whichever surface is asking.
27+
export { firstUndeclaredReference } from './cel-engine';
1828
// #4812 — the canonical parse-to-AST entry. Any consumer that needs the AST of
1929
// an authored CEL source takes it from here, so "what parses" has exactly ONE
2030
// answer across build, lint and runtime. Building a private `new Environment()`

packages/lint/src/authoring-rules.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -693,12 +693,20 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
693693
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
694694
run: (stack) => validateSeedStateMachine(stack),
695695
},
696-
// ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root.
697-
// Pre-parse: the schema folds `visibleOn`/`visibility` into `visibleWhen`
698-
// during parse, so the alias the author wrote is gone from `result.data`.
696+
// ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root,
697+
// plus (#6128) the bare-identifier gate. Pre-parse: the schema folds
698+
// `visibleOn`/`visibility` into `visibleWhen` during parse, so the alias the
699+
// author wrote is gone from `result.data`.
700+
//
701+
// `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
702+
// ADR-0089 rules stay advisory findings within it — the tier is a property of
703+
// the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
704+
// what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
705+
// has worked since #3760. The promotion follows the #5762 precedent: a family
706+
// that gains an `error` finding moves its registry tier in the same edit.
699707
{
700708
name: 'validateVisibilityPredicates',
701-
tier: 'advisory',
709+
tier: 'gating',
702710
input: 'normalized',
703711
commands: ALL,
704712
source: 'packages/lint/src/validate-visibility-predicates.ts',

packages/lint/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export {
139139
validateVisibilityPredicates,
140140
VISIBILITY_ALIAS_DEPRECATED,
141141
VISIBILITY_ROOT_MISLAYERED,
142+
VISIBILITY_BARE_IDENTIFIER,
142143
} from './validate-visibility-predicates.js';
143144
export type {
144145
VisibilityFinding,

0 commit comments

Comments
 (0)