@objectstack/lint 17.0.0-rc.5 → 17.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1414 -0
- package/dist/index.cjs +1787 -624
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +324 -123
- package/dist/index.d.ts +324 -123
- package/dist/index.js +1765 -610
- package/dist/index.js.map +1 -1
- package/dist/{runtime-Cs64ShwN.d.cts → runtime-B50yywI_.d.cts} +49 -4
- package/dist/{runtime-Cs64ShwN.d.ts → runtime-B50yywI_.d.ts} +49 -4
- package/dist/runtime.cjs +1214 -428
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1204 -407
- package/dist/runtime.js.map +1 -1
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1419 @@
|
|
|
1
1
|
# @objectstack/lint
|
|
2
2
|
|
|
3
|
+
## 17.0.0-rc.6
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d0e5537: feat(lint): `validate-ai-agent-authoring` 新增 `app.defaultAgent` 取值检查(warning 档,#6041)
|
|
8
|
+
|
|
9
|
+
`app.defaultAgent` 的 Zod 类型是 `SnakeCaseIdentifierSchema`,任何 snake_case
|
|
10
|
+
字符串都能 parse、build、通过 `os:check` —— 但运行期只解析平台 agent 名单
|
|
11
|
+
(`ask`/`build` 及其历史别名 `data_chat`/`metadata_assistant`,ADR-0063 §2),
|
|
12
|
+
表外的名字会静默回落到平台默认值。#5985 实测:把坏例子
|
|
13
|
+
`defaultAgent: 'sales_copilot'` 放回语料后,`check:skill-examples` 仍 208
|
|
14
|
+
全绿、EXIT=0 —— 现有门禁对这类缺陷结构性失明,这正是坏语料当初得以发布的机制
|
|
15
|
+
(语料本身已由 PR #6030 修复)。
|
|
16
|
+
|
|
17
|
+
本 PR 是 `validate-ai-agent-authoring` 已有规则(此前只扫描 `stack.agents`
|
|
18
|
+
数组)的取值半边:遍历 `stack.apps[].defaultAgent`,取值不在
|
|
19
|
+
`PLATFORM_AGENT_NAMES`(复用同文件既有名单,未新建重复列表)内即产出一条
|
|
20
|
+
`warning` 级 finding(规则 id `default-agent-outside-roster`),消息中点名
|
|
21
|
+
实际取值与允许集合。维护者裁定(2026-08-07,2026-08-09 重申)为 **A 档**:
|
|
22
|
+
warning 而非 error —— 危害等级是静默回落而非崩溃,且不惩罚存量元数据;
|
|
23
|
+
schema 本身不收窄为 enum(ADR-0063 已经撤回过一次 breaking 的收紧)。
|
|
24
|
+
|
|
25
|
+
落地前已按裁定要求测量现存 in-repo `app.defaultAgent` 取值:仅
|
|
26
|
+
`packages/platform-objects/src/apps/studio.app.ts` 一处真实赋值
|
|
27
|
+
(`defaultAgent: 'metadata_assistant'`,合法平台别名),对该值实际跑规则
|
|
28
|
+
0 条 finding —— "不惩罚存量" 的前提已验证而非假设。
|
|
29
|
+
|
|
30
|
+
- 4bda5f8: feat(lint): `flow-trigger-unroutable` 收紧到"省略"形态 —— 无 `triggerType` 的 `record_change` flow 同样报错 (#7215)
|
|
31
|
+
|
|
32
|
+
`flow-trigger-unroutable`(#6637)此前只判"矛盾"形态:`config.triggerType` **存在**但引擎路由不到
|
|
33
|
+
任何 trigger(如 `triggerType: 'onCreate'`)。本次按 #7215(维护者裁定,方案一:现在就收紧)扩展到
|
|
34
|
+
"省略"形态:`type: 'record_change'` 且**完全没有** `triggerType` 这个键。两种写法在运行期是同一个
|
|
35
|
+
缺陷 —— `AutomationEngine.resolveTriggerBinding` 对两者走的是同一条回退链,最终都返回
|
|
36
|
+
`undefined`,flow 被静默降级为手动 flow,连 `getTriggerBindingAudit` 都因为"看起来像手动/screen
|
|
37
|
+
flow"而跳过它,不会在任何地方点名。因此复用同一个 rule id 与同一档 severity(`error`),而不是新开
|
|
38
|
+
一条 —— 这是同一个缺陷的两种写法,不是两个缺陷。
|
|
39
|
+
|
|
40
|
+
## 为什么现在收紧,而不是 #6637 立规则时就收紧
|
|
41
|
+
|
|
42
|
+
#6637 立规则时,语料测出一个真实的省略实例:`examples/app-todo` 的 `TaskCompletionFlow`。当时把它
|
|
43
|
+
判死,等于在一个已发布的示例 app 上,对该 app 的语义意图下一个未经确认的猜测,所以判据当时要求
|
|
44
|
+
`triggerType` 这个 key 必须**存在**,省略形态被单独立卡搁置(#7041 item 2)。#7039 已经把那个实例
|
|
45
|
+
修好 —— `TaskCompletionFlow` 现在显式声明 `triggerType: 'record-after-update'` 并正确路由 ——
|
|
46
|
+
语料窗口转绿,#7215 因此裁定:一个零命中规则,只要缺陷类别有过真实实例(学费已经交过)、oracle 是
|
|
47
|
+
封闭的(能不能路由是引擎自己的硬编码链,不是猜测)、当下语料对它是绿的(收紧不产生 churn)、
|
|
48
|
+
severity 与危害匹配,就应当趁窗口开着落地,而不是等下一个省略实例出现、把落地成本重新推高。
|
|
49
|
+
|
|
50
|
+
## 语料计数(先测,后收紧)
|
|
51
|
+
|
|
52
|
+
用生产入口(`validateFlowTriggerReadiness`)跑过本仓 `examples/`、`apps/`、`packages/` 下按内容
|
|
53
|
+
搜索到的**每一个** `type: 'record_change'` 真实 flow 定义 —— 全库只有两个:
|
|
54
|
+
`examples/app-todo/src/flows/task.flow.ts`(`TaskCompletionFlow`)与
|
|
55
|
+
`examples/app-showcase/src/automation/flows/index.ts`(`UrgentTaskAlertFlow`),两者都已显式声明
|
|
56
|
+
`triggerType`。**省略实例命中数为 0**。收紧后的判据在整棵树上是绿的:不产生任何新的 baseline 条目,
|
|
57
|
+
不需要修任何示例 app。
|
|
58
|
+
|
|
59
|
+
## 判据里没有变的部分
|
|
60
|
+
|
|
61
|
+
"这条规则不判的第二种形态"维持原样:一个 `record_change` flow 若同时声明了引擎**确实**会路由的东西
|
|
62
|
+
(`config.schedule`、`triggerType: 'api'`、`config.timeRelative` 对象),它会按错误的 trigger 绑定
|
|
63
|
+
并触发 —— 这是一个不同的缺陷("绑错"而不是"没绑上"),仍然不是这条规则要判的,`routesToSomeTrigger`
|
|
64
|
+
分支字符对字符保持不变。
|
|
65
|
+
|
|
66
|
+
`validate-flow-trigger-readiness.test.ts` 里原先钉住"省略形态故意不判"的边界测试(其自身注释写明
|
|
67
|
+
"收紧是必须删掉这个测试的有意行为,不是顺手带过的副作用")已按 #7215 删除,替换为覆盖"省略形态触发"、
|
|
68
|
+
"省略但有其它路由 sibling 时不触发"、"非 `record_change` 类型的 flow 即使省略 `triggerType` 也不
|
|
69
|
+
触发"的新用例。
|
|
70
|
+
|
|
71
|
+
- 8b82686: 新增 gating 规则 `flow-trigger-unroutable`(#6637):`type: 'record_change'` 的 flow 若在 start 节点声明了引擎无法路由的 `triggerType`(如 `onCreate`、`on_update`、`['onCreate']`),现在在 `os lint` / `os validate` / `os build` 与运行时发布闸门上报 `error`。
|
|
72
|
+
|
|
73
|
+
这是 never-fire 家族里最安静的一种失败。引擎的 `resolveTriggerBinding` 只按字面量 `startsWith('record-')` 认领 record-change flow,落空后整条分支链走到底返回 `undefined`,`activateFlowTrigger` 直接 `return`,该 flow 就被当成手动 flow —— 而专门为「悄悄没绑上」而建的 `getTriggerBindingAudit` 调用的是同一个 resolver,会以「manual / screen flow — nothing to bind」跳过它。因此启动告警和 CLI 启动摘要都不会点名,唯一痕迹是 banner 里 flow 总数比 bound 数多一。
|
|
74
|
+
|
|
75
|
+
规则范围刻意收窄到「声明了 `record_change`」的 flow:落空到无绑定本身也正是一个 flow 合法地成为手动 flow 的机制,而 `autolaunched` / `screen` 才是手动 flow 声明的类型,所以这条规则在结构上不可能误伤真正的手动 flow。`triggerType` 完全缺失的情形(同样必死)不在本次范围内,另行处理。
|
|
76
|
+
|
|
77
|
+
- d06b3dc: feat(lint): literal empty combinators are refused at authoring time, with a per-shape prescription (#5330)
|
|
78
|
+
|
|
79
|
+
#5322 settled what an empty combinator MEANS at run time — the boolean identity
|
|
80
|
+
reduction — and #5659/PR #6528 made that reduction one implementation
|
|
81
|
+
(`reduceFilterVerdict` in `@objectstack/spec/data`, proven against
|
|
82
|
+
`FILTER_LOGIC_CASES`, consumed by every backend). This change adds the other half
|
|
83
|
+
the ruling deliberately left open: the literal SPELLINGS are now refused where an
|
|
84
|
+
author writes them, which is Prime Directive #12's standard shape (reject at the
|
|
85
|
+
producer, do not tolerate at the consumer) and #5240's same-direction precedent
|
|
86
|
+
one shape over.
|
|
87
|
+
|
|
88
|
+
`validateEmptyCombinators` is a new gating rule in `AUTHORING_RULES`, so it runs
|
|
89
|
+
on `os validate` / `os build` / `os lint` at once, and on the runtime publish
|
|
90
|
+
gate for `flow` writes — the door a Studio tenant, a REST `/meta` client and an
|
|
91
|
+
MCP/AI author all use. Two rule ids:
|
|
92
|
+
|
|
93
|
+
- `filter-empty-combinator` — a literal `$and: []`, `$or: []` or `$not: {}`.
|
|
94
|
+
- `filter-empty-node` — a literal `{}` standing as the whole filter, or as a
|
|
95
|
+
branch of `$and` / `$or`.
|
|
96
|
+
|
|
97
|
+
**The prescription is per shape, because the identities disagree.** `{$and: []}`
|
|
98
|
+
and `{}` reduce to TRUE (match EVERY row); `{$or: []}` and `{$not: {}}` reduce to
|
|
99
|
+
FALSE (match NO row). A generic "empty combinator, fix it" message teaches the
|
|
100
|
+
wrong fix half the time, so each shape names its own: delete the key to mean "no
|
|
101
|
+
filter"; fill the array to mean a constraint; put the negated condition inside
|
|
102
|
+
`$not`; and, when zero rows really is the intent, `{ <field>: { $in: [] } }` is
|
|
103
|
+
the declared spelling that says so instead of implying it. The row-set wording in
|
|
104
|
+
every message is DERIVED from `reduceFilterVerdict` rather than retyped, and a
|
|
105
|
+
test drives the four #5322 identity cases straight out of `FILTER_LOGIC_CASES` and
|
|
106
|
+
asserts the message agrees with the rows the table says the filter selects.
|
|
107
|
+
|
|
108
|
+
**Nothing at run time changed.** No translate or evaluation path is touched, the
|
|
109
|
+
conformance matrix is untouched, and a stack that ignores the finding runs exactly
|
|
110
|
+
as before. The literal-vs-programmatic boundary the ruling requires is structural,
|
|
111
|
+
not heuristic: this rule sees only values that reached the metadata graph, so a
|
|
112
|
+
producer that assembles zero disjuncts while serving a request — an RLS lowering,
|
|
113
|
+
a CEL `!expr`, a client-built query — never reaches it and keeps the runtime
|
|
114
|
+
identity, which is what makes `{$or: []}` = zero rows fail-closed (#5134).
|
|
115
|
+
|
|
116
|
+
Also internal: the filter-subtree traversal `validate-filter-tokens.ts` grew for
|
|
117
|
+
#3574 moved to a shared `filter-walk.ts` now that it has a second consumer — the
|
|
118
|
+
same argument `page-walk.ts` (#3583) and `view-walk.ts` (#6381) make. Each rule
|
|
119
|
+
still declares its OWN surface list, so one rule's widening cannot land silently
|
|
120
|
+
in the other; `validate-filter-tokens`'s behaviour is unchanged.
|
|
121
|
+
|
|
122
|
+
- 424c510: feat(lint): metadata 表单谓词的路径解析闸门 —— 引用不存在的路径在发布期就被拒(#7010)
|
|
123
|
+
|
|
124
|
+
新增 **error 级** 规则族 `validate-predicate-path-refs`,在 `os validate` / `os build` /
|
|
125
|
+
`os lint` 三条命令上判定:一个 metadata 编辑表单(数据源为 `{ provider: 'schema', schemaId }`
|
|
126
|
+
的 `defineForm` 形状)里的可见性谓词,其 `data.` 路径必须能在该 `schemaId` 对应的 schema 上逐段
|
|
127
|
+
解析。两条规则,同一个问题:
|
|
128
|
+
|
|
129
|
+
- `predicate-path-unresolved` —— `data.` 有根,但某一段不是该层 schema 声明的键
|
|
130
|
+
(`data.tpye == 'formula'`)。消息点名**不可解析的那一段**,hint 给出编辑距离最近的候选。
|
|
131
|
+
- `predicate-path-unrooted` —— 裸标识符,而这个名字**恰好是目标 schema 的键**
|
|
132
|
+
(`type == 'formula'`)。这是 #6254 的形状:名字写对了,根丢了。
|
|
133
|
+
|
|
134
|
+
## 为什么现有三道闸都放行
|
|
135
|
+
|
|
136
|
+
`validate-visibility-predicates.ts`(ADR-0089 D3b)判的是谓词的**形状**:能不能解析
|
|
137
|
+
(`visibility-predicate-syntax`,#6253)、有没有根(`visibility-bare-identifier`,#6128)、
|
|
138
|
+
根对不对层(`visibility-root-mislayered`)。三条都**不打开目标 schema**,所以
|
|
139
|
+
`data.tpye == 'formula'` 三条全过,而后在控制台 fail-open —— 元素无条件渲染,和完全不写谓词
|
|
140
|
+
像素级一致(#5149 一族)。
|
|
141
|
+
|
|
142
|
+
#6254 已经实测过这个洞的另一半:`object.form.ts` 的 16 处裸谓词写成 `type == 'formula'`,而
|
|
143
|
+
#6248 的裸标识符闸**按构造抓不到** —— `type` 是 CEL 自己声明的类型名标识符,到严格检查器那里
|
|
144
|
+
是类型 overload 错误而非未知变量。本规则不问 CEL「什么能解析」,只问**目标 schema**「这个键声明了
|
|
145
|
+
没有」,所以 CEL 的类型名词汇表与它无关。
|
|
146
|
+
|
|
147
|
+
## 落点:`data.*` 一层,这是决定而非省略
|
|
148
|
+
|
|
149
|
+
`error` 级闸门要求 oracle 是**封闭**的 —— 一个能枚举、且「不在其中」确实等于「解析不到」的键集。
|
|
150
|
+
ADR-0089 D3 的两层里只有一层满足:metadata 编辑表单的行是某个 metadata type 的实例,形状由
|
|
151
|
+
`getMetadataTypeSchema` 这一份规范注册表逐键给出。运行期 record 面(`record.*`)**今天不封闭**
|
|
152
|
+
—— lookup 穿透、authored `fields` 从不列出的系统列、formula/rollup 输出都是合法路径,在开集上架
|
|
153
|
+
`error` 闸只会制造误红,而误红是闸门唯一不能犯的方向。已在规则注释与 #7010 上记为待裁的开放问题。
|
|
154
|
+
|
|
155
|
+
## repeater 行重绑 `data`,规则跟着重绑(#6254)
|
|
156
|
+
|
|
157
|
+
`type: 'record'` / `repeater` / `composite` 子字段列表里,`data` 绑定的是**这一行**
|
|
158
|
+
(objectui 的 metadata SchemaForm 以 `{ data: row }` 求值),但根**仍然拼作 `data`**。所以
|
|
159
|
+
`object.form.ts` 的 `data.type` 指的是 `FieldSchema.type` 而不是并不存在的 `ObjectSchema.type`。
|
|
160
|
+
规则按同样的重绑下降 —— 不这么做,已发货的语料会读出 16 条误红而不是 0 条。
|
|
161
|
+
|
|
162
|
+
## 语料计数(先量再收紧)
|
|
163
|
+
|
|
164
|
+
规则通过**生产入口**跑过本仓发货的全部 metadata 表单(`METADATA_FORM_REGISTRY`,17 张表 46 条
|
|
165
|
+
谓词):**两条规则的命中数都是 0**,因此才落 `error`。反向验证同时钉住:把 #6254 修前的裸写法还原
|
|
166
|
+
到 `object.form.ts` 的深拷贝上,规则报出 **恰好 16 条** `predicate-path-unrooted` —— 正是该单
|
|
167
|
+
当年人工读出来的那 16 处。
|
|
168
|
+
|
|
169
|
+
## 明确不判(都是漏判方向,永远不会变成误红)
|
|
170
|
+
|
|
171
|
+
规范前端解析不了的谓词(交还 #6253);不声明键集的作用域(`z.record(z.string(), z.unknown())`
|
|
172
|
+
/ `z.unknown()`);record map 的**键**段(`z.record(z.string(), X)` 按构造接受任何键);推导宏
|
|
173
|
+
绑定的循环变量(`data.tags.all(t, …)`);下标访问(`data.x['y']`);解析不到 schema 的
|
|
174
|
+
`schemaId`。裸标识符里**不是** schema 键的那些也不判 —— 那是 `visibility-bare-identifier`
|
|
175
|
+
的判决,一条坏谓词只应产生一条 finding。
|
|
176
|
+
|
|
177
|
+
规则注册在 `AUTHORING_RULES`(`tier: 'gating'`,三条命令全覆盖),`surfaces` 保持 `cli`:它其实
|
|
178
|
+
只需要被写入的那一条 item,但 `views[]` 可见性谓词这一族的另外三条规则今天都是 CLI-only,单独把
|
|
179
|
+
三分之一的判决搬到 Studio 写入门上,比一条都不搬更难预测 —— 该族应当一次整体迁移,这是关于
|
|
180
|
+
`views` 写入门的决定,不该搭在本单的车上。理由已写成 `RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY`。
|
|
181
|
+
|
|
182
|
+
- 6965160: feat(lint): view/page 可见性谓词的裸标识符构建期闸门 —— 坏谓词发不出去(#6128)
|
|
183
|
+
|
|
184
|
+
新增 **error 级** 规则 `visibility-bare-identifier`:view/page 的可见性谓词
|
|
185
|
+
(`visibleWhen` 及其两个已弃用别名 `visibleOn` / `visibility`)里引用了任何绑定根都解析不到的
|
|
186
|
+
顶层标识符时,`os validate` / `os build` / `os lint` 一律拒收。写成 `status == 'active'`
|
|
187
|
+
而不是 `record.status == 'active'` 的谓词,从此发不出去。
|
|
188
|
+
|
|
189
|
+
按 #5149 维护者 2026-08-06 裁决的构建期半边落地(运行时 warn-once 半边已由 objectui#3541 合入)。
|
|
190
|
+
本仓传统的准确表述是:fail-open 或 fail-closed 都可以裁,**静默不可以**。谓词失败仍然 fail-open
|
|
191
|
+
(已发货 app 行为不变),但坏谓词不再能进入产物。
|
|
192
|
+
|
|
193
|
+
**为什么现有两道闸都放行**(#5149 Repro 1 实测,已写进规则注释,防后人误并):
|
|
194
|
+
ADR-0032 的标识符闸(`validate-expressions.ts`)解析 record 作用域的裸引用,但它的遍历只覆盖
|
|
195
|
+
objects / flows / actions / sharingRules / hooks,**从不走 views 与 pages**;ADR-0089 D3b
|
|
196
|
+
只判**有根**的谓词根错层(runtime 面的 `data.`、metadata 面的 `record.`),**无根**的谓词两边都不匹配。
|
|
197
|
+
两闸之间正好漏掉「作者按文档示例写了裸字段名 → 谓词永远解析失败 → 控制台 fail-open 静默显示」。
|
|
198
|
+
|
|
199
|
+
**判定由两个既有 oracle 合成,本包不自建 CEL 环境**(#4812 的教训):声明性判定取
|
|
200
|
+
`@objectstack/formula` 的 `firstUndeclaredReference`(即 `validateExpression` 给 record 作用域
|
|
201
|
+
裸引用定罪的同一个严格环境),AST 取规范入口 `parseCelToAst`。AST 先收集所有处于**接收者位置**
|
|
202
|
+
的标识符(`a.b` / `a?.b` / `a['b']` / `a.exists(…)`)并在检查前声明它们,于是只剩「当作裸值引用」
|
|
203
|
+
的标识符会被判 —— 未知**根**(`my_record.x`)交还给 ADR-0089 D3b,不在本规则射程内。
|
|
204
|
+
|
|
205
|
+
**与 #4953(全量 vs 稀疏绑定)的边界**:#4953 实测同一求值器在两种绑定下语义相反
|
|
206
|
+
(`has(record.a)` 全量 true / 稀疏 false;`record.a != null` 全量 false / 稀疏 FAULT)。本规则
|
|
207
|
+
**按构造与该分叉无关** —— 它从不追问某个 KEY 在已绑定的根上是否存在,只追问标识符有没有根,
|
|
208
|
+
而无根标识符在两种绑定下都解析不到。`has(record.x)` / `record.x != null` 等守卫写法在本闸门下
|
|
209
|
+
一律绿,无论 #4953 最终怎么裁;已加测试钉住这条边界。
|
|
210
|
+
|
|
211
|
+
**遍历按实测修正,否则规则生来即死**:`os build` 跑 `examples/app-showcase` 得到的唯一一条
|
|
212
|
+
view 表单谓词落在 `views[0].formViews.edit.sections[0].fields[6].visibleWhen` —— 运行时 app 形状下
|
|
213
|
+
`views[]` 条目是**视图容器**(`ViewSchema` 声明的自有键就是 `list` / `form` / `listViews` /
|
|
214
|
+
`formViews`),`sections` 在下一层。原遍历只读 `views[].sections`,在这份 stack 上报告「干净」。
|
|
215
|
+
现在覆盖容器的 `form` 与每个 `formViews.<key>`,以及仍然直接携带 `sections` 的 `defineForm` 形状;
|
|
216
|
+
pages 改走共享的 `walkPageComponents`(regions、slotted 页的 `slots`、以及 `properties` 里的
|
|
217
|
+
`page:tabs` / `page:accordion` / `page:card` 子树都随之覆盖,source-authored 页按其既有语义跳过)。
|
|
218
|
+
`objects[].views` 明确不读 —— 该键已被 schema 立碑拒绝,读它只会造出一条永不触发的幽灵检查。
|
|
219
|
+
两条既有 ADR-0089 D3b advisory 随遍历一并变得真正可达。
|
|
220
|
+
|
|
221
|
+
注册表 tier `advisory` → `gating`(#5762 的先例):tier 声明并非自述,
|
|
222
|
+
`authoring-rule-wiring.test.ts` 会读规则源码核对。
|
|
223
|
+
|
|
224
|
+
已知盲点(已钉测试、方向安全):字段名与 CEL **类型名**相同时(`type` / `int` / `string` / `list`
|
|
225
|
+
/ `map` / `timestamp` …)不判 —— CEL 自身声明这些标识符,`type == 'grid'` 到检查器那里是类型
|
|
226
|
+
overload 错误而非未知变量;改读 overload 消息会误杀合法的 `type(record.x) == string`。语法不通过
|
|
227
|
+
的谓词同样不判,交还给拥有该判定的闸门。两者都是漏判,永远不会变成误红。
|
|
228
|
+
|
|
229
|
+
仓内 `app-todo` / `app-crm` / `app-showcase` 三个示例 `os validate` 全部通过、零 visibility finding,
|
|
230
|
+
无需修改任何示例内容。
|
|
231
|
+
|
|
232
|
+
`@objectstack/formula` 侧:公开导出 `firstUndeclaredReference`(理由与既有的
|
|
233
|
+
`collectCelRootIdentifiers` 一致 —— 绑定根集合不同的消费方需要的是同一个答案,替代方案是在消费方
|
|
234
|
+
自建严格 `Environment`,而那正是 #4812 从本包消费方手里拿掉的私有前端)。
|
|
235
|
+
|
|
236
|
+
- ecff951: feat(lint): view/page 可见性谓词的 CEL 语法构建期闸门 —— `country === "USA"` 不再零诊断(#6253)
|
|
237
|
+
|
|
238
|
+
新增 **error 级** 规则 `visibility-predicate-syntax`:view/page 的可见性谓词
|
|
239
|
+
(`visibleWhen` 及其两个已弃用别名 `visibleOn` / `visibility`)如果规范 CEL 前端根本
|
|
240
|
+
解析不了,`os validate` / `os build` / `os lint` 一律拒收。`===` 这类写法从此发不出去。
|
|
241
|
+
|
|
242
|
+
按维护者 2026-08-07 对 #6253 的裁定落地:**判 blocking error**,与其它谓词面
|
|
243
|
+
(validation rule / flow / action,ADR-0032)同级;不设 warning 档,也不为本面写豁免——
|
|
244
|
+
warning 在 CI 里通常不拦,那只是「多绕几步的静默」。
|
|
245
|
+
|
|
246
|
+
**为什么这一面此前无人判**:`validate-expressions.ts`(ADR-0032)对它遍历到的每条谓词都跑
|
|
247
|
+
`validateExpression`,语法错报 blocking error——但它的遍历面是 objects / flows / actions /
|
|
248
|
+
sharingRules / hooks,**从不走 `views` 与 `pages`**。走这一面的三条规则(ADR-0089 D3b 两条
|
|
249
|
+
advisory,加 #6128 的裸标识符闸)都明确不判语法,理由是「不发明第二个语法判定」。那条政策
|
|
250
|
+
在它自己的调用点上成立(`validateExpression` 就在同一批调用点上跑),**在 view/page 面上不成立:
|
|
251
|
+
那里没有第二个判定,沉默就是没人报**。后果与 #5149 同型:谓词求值失败 → `evalFieldPredicate`
|
|
252
|
+
返回 fallback → 可见性 fallback 是 `true` → 元素无条件渲染,与「没写谓词」在屏幕上一模一样。
|
|
253
|
+
`packages/spec/src/ui/view.test.ts` 的 fixture 就写着 `'country === "USA"'`,正说明这是作者
|
|
254
|
+
(尤其 AI)会写出来的形状。
|
|
255
|
+
|
|
256
|
+
**判定仍然不是本包给的**——旧政策要保护的正是这一点,它完整保留:判定取 `parseCelToAst`
|
|
257
|
+
(规范前端,带 #3306 改写与 `DEFAULT_LIMITS`,#4812),本规则不自建 `Environment`、不手写
|
|
258
|
+
tokenizer。#6253 加的是**对既有判定的上报**,外加原始报错缺的自纠措辞:cel-js 只说
|
|
259
|
+
`Unexpected character: =` 并画一个 caret,既没点名作者写的运算符,也没给出 CEL 的写法。
|
|
260
|
+
|
|
261
|
+
**明确不走 `validateExpression` / `celEngine.compile`**,尽管那才是 ADR-0032 的入口:
|
|
262
|
+
`compile()` 是 parse **+ 类型检查**,差别不是理论上的——实测它会以
|
|
263
|
+
`no such overload: type == string` 拒掉 `type == 'grid'`,而那正是本文件**已钉测试的既有盲点**
|
|
264
|
+
(字段名与 CEL 类型名相同时不判,因为改读 overload 消息会误杀合法的 `type(record.x) == string`)。
|
|
265
|
+
从语法分支绕过去会把那条决定悄悄推翻,并把一条 error 级闸门从「解析不了」扩张成「类型检查不过」——
|
|
266
|
+
而这一面的谓词绝大多数是 `dyn`。裁定说的是语法,parse 判定恰好就是语法。
|
|
267
|
+
|
|
268
|
+
**消息自纠**:实测过的非 CEL 拼法各自点名并给出 CEL 写法——`===`→`==`、`!==`→`!=`、
|
|
269
|
+
`<>`→`!=`、`and`→`&&`、`or`→`||`、`not`→`!`、单个 `=`→`==`。扫描前先把字符串字面量抹平,
|
|
270
|
+
所以 `record.msg == 'a === b' and record.n > 1` 归咎于 `and` 而不是字面量里的 `===`;
|
|
271
|
+
`record.msg == 'a === b'` 本身能解析,压根不报。`??` 与 SQL 的 `IN (…)` 故意不进表:两者
|
|
272
|
+
都会解析失败、都照报(带前端原话),但都没有「换一个 token」就能修好的等价写法,给半个修法
|
|
273
|
+
只会让作者多跑一趟。
|
|
274
|
+
|
|
275
|
+
**边界**(均已钉测试):空/纯空白谓词不是语法错(`parseCelToAst` 对空源也返回 `null`,
|
|
276
|
+
没有这道 guard 会把「没写谓词」报成坏 CEL);`DEFAULT_LIMITS` 超限属于**边界**错而非语法错,
|
|
277
|
+
照报但引用前端原话、不假装找到了 typo,与 ADR-0032 把两者一并归入「invalid CEL predicate」
|
|
278
|
+
的既有做法一致,且超长谓词在消息里省略,单条 runaway 表达式刷不满控制台;一条坏谓词**只出一个
|
|
279
|
+
finding**——源码解析不出 AST 就没有标识符可判,裸标识符闸自动让位,该互斥性由「断言整个上报集合」
|
|
280
|
+
钉住而不是靠调用方内部实现。
|
|
281
|
+
|
|
282
|
+
注册表无需改动:`validateVisibilityPredicates` 的 tier 在 #6128 已是 `gating`、commands 已是
|
|
283
|
+
build/lint/validate,本规则的 `error` 直接沿用(已加测试复核该前提仍然成立)。
|
|
284
|
+
|
|
285
|
+
仓内清扫:除 `packages/spec/src/ui/view.test.ts` 那几条**纯 schema 测试样本**(它们只跑
|
|
286
|
+
`FormFieldSchema.parse`,不经 lint,属于本单援引的证据而非待修点)外,全仓 examples / apps /
|
|
287
|
+
packages 的 view/page 可见性谓词均能通过规范前端解析,无需修改任何示例内容。
|
|
288
|
+
|
|
289
|
+
- 31cbe90: feat(spec,lint): declare the SDUI deep-link "navigate = run action" as a validatable `runAction` nav reference (#4848)
|
|
290
|
+
|
|
291
|
+
The `?runAction=<actionName>` deep link (cloud#844) lived as two private string
|
|
292
|
+
halves — objectui's `CloudOnboardingNext.tsx` concatenating the query and its
|
|
293
|
+
`EnvironmentListToolbar.tsx` consuming it by literal match — an implicit
|
|
294
|
+
cross-repo contract neither side's rename turned red. Per the maintainer's
|
|
295
|
+
2026-08-06 ruling on #4848, the contract is now spec-declared, contract-first.
|
|
296
|
+
|
|
297
|
+
**New slot — `ObjectNavItemSchema.runAction` (optional).** An `object` nav item
|
|
298
|
+
may declare the action to auto-run once on arrival at the object's list
|
|
299
|
+
surface. The name resolves against the stack's declared actions (global
|
|
300
|
+
`stack.actions` + any object's `actions`) — the same "defined ANYWHERE" scope
|
|
301
|
+
every other name-bound action surface uses. `runAction` + `recordId` is
|
|
302
|
+
parse-rejected (`objectNavTargetExclusivity`): a record detail has no list
|
|
303
|
+
toolbar, so the combination is a dead affordance made unrepresentable.
|
|
304
|
+
|
|
305
|
+
**Validation, both layers.** `defineStack`'s cross-reference walk rejects a
|
|
306
|
+
`runAction` naming no defined action (size-gated like the neighboring
|
|
307
|
+
dashboard/page/report checks), and `validate-action-name-refs` gains a nav
|
|
308
|
+
`runAction` arm — error severity, near-miss "did you mean", running on every
|
|
309
|
+
`os validate`/`lint`/`compile` via the reference-integrity suite.
|
|
310
|
+
|
|
311
|
+
The slot is ledgered `planned` + `authorWarn` (enforce-or-mark, like
|
|
312
|
+
`object.externalSharingModel`'s P1): no shipped shell reads the declared slot
|
|
313
|
+
yet, so authors are told the auto-run still fires only via the transitional URL
|
|
314
|
+
query param until the objectui consumer half lands. cloud#1048's pin remains
|
|
315
|
+
the transitional guard.
|
|
316
|
+
|
|
317
|
+
- f012f55: Add the startup open-vocabulary verdict rule — "not registered YET" and "no provider at all" are the same value, and a verdict recorded from it is never retracted (#4776).
|
|
318
|
+
|
|
319
|
+
A boot fills its registries incrementally, so asking one "is X there?" while it is still filling is fine — the answer is simply not final yet. Turning that not-yet into a **verdict and recording the verdict** is the defect: the provider registers a moment later and nothing goes back to undo the record. One showcase cold start produced three instances of the shape in three unrelated subsystems (#4769, #4771, #4772).
|
|
320
|
+
|
|
321
|
+
`findStartupRegistryVerdicts(source, { file })` is a pure decision procedure over plugin source (parsed, never executed, never type-checked). It reports two rule ids:
|
|
322
|
+
|
|
323
|
+
- `startup-open-vocabulary-verdict` — inside `constructor` / `init` / `start`, a read of a capability vocabulary ADR-0018 keeps runtime-extensible whose conclusion is **recorded** (announced in a `warn`/`error` log, cached in an instance field or module binding, or persisted). All three parts, or it is not a finding — a read-only probe is legal and is not flagged.
|
|
324
|
+
- `startup-verdict-assertive-wording` — emitted only at a site the first rule already flagged, when the diagnostic asserts a terminal outcome about a world that has not finished forming ("will fail at execution time", "you need Redis").
|
|
325
|
+
|
|
326
|
+
Every finding's hint prescribes the three shapes the fixes took: resolve where the value is used (a `kernel:ready` hook or a lazy accessor — `createLazyCacheRateLimitStorage()`, #4772), seal the vocabulary then judge (`AutomationEngine.sealNodeTypeVocabulary()`, #4771), or order the verdict after the mutation it describes (#4769). All three cures are recognised by shape and pass.
|
|
327
|
+
|
|
328
|
+
Severity is always `warning` — the rule reasons about a boot sequence it cannot execute, so it advises and never gates. The kernel SERVICE-registry half of the same family stays with `pnpm check:startup-registry-verdict`; the rule module states the measured division of labour between the two.
|
|
329
|
+
|
|
330
|
+
- 92e13a0: refactor(lint)!: retire `visibility-alias-deprecated` — the rule could not fire on any real CLI input (#6318, ADR-0049)
|
|
331
|
+
|
|
332
|
+
`@objectstack/lint` shipped a fourth conditional-visibility rule whose only job
|
|
333
|
+
was to report the deprecated predicate **key** (`visibleOn` on a view form
|
|
334
|
+
section/field, `visibility` on a page component) and steer the author to
|
|
335
|
+
`visibleWhen`. It never reported on anything a command actually loads.
|
|
336
|
+
|
|
337
|
+
**Why it could not fire.** The rule is registered `input: 'normalized'`, so what
|
|
338
|
+
`os validate` / `os build` / `os lint` hand it is the output of
|
|
339
|
+
`normalizeStackInput`. The two ADR-0087 D2 conversions that fold the alias —
|
|
340
|
+
`view-visibleOn-to-visibleWhen` and `page-component-visibility-to-visibleWhen` —
|
|
341
|
+
run **inside** `normalizeStackInput`, one layer above. The key is therefore
|
|
342
|
+
already renamed by the time the rule sees the stack. Re-measured per site:
|
|
343
|
+
|
|
344
|
+
| alias site | rule fed the raw authored object | rule fed the `normalized` tier |
|
|
345
|
+
| ----------------------------------- | -------------------------------- | ------------------------------ |
|
|
346
|
+
| `views[].form.sections[]` | 1 finding | **0** |
|
|
347
|
+
| `views[].formViews.edit.sections[]` | 1 finding | **0** |
|
|
348
|
+
| `pages[].regions[].components[]` | 1 finding | **0** |
|
|
349
|
+
|
|
350
|
+
The one shape it did still fire on is a view **container** carrying top-level
|
|
351
|
+
`sections` — the shape its own unit tests used, and the shape strict
|
|
352
|
+
`ViewSchema` refuses outright (`Unrecognized key(s) on this view container:
|
|
353
|
+
\`sections\``). A green unit test over a fixture production can never send.
|
|
354
|
+
|
|
355
|
+
**No working app loses a signal.** Authors were never hearing this rule, and
|
|
356
|
+
they do hear the conversion: the same D2 entry emits a `warnConversionNotice`
|
|
357
|
+
from `defineStack` that names the site, the conversion id and the retirement
|
|
358
|
+
window — wording the lint rule never had.
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
defineStack: views[0].form.sections[0].visibleWhen: 'visibleOn' -> 'visibleWhen'
|
|
362
|
+
(converted at load; conversion 'view-visibleOn-to-visibleWhen', retires in protocol 16).
|
|
363
|
+
Update the source to the canonical shape — the conversion stops running then.
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Authored metadata is unaffected.** `visibleOn` / `visibility` remain accepted
|
|
367
|
+
exactly as before, still fold to `visibleWhen`, and still retire with protocol 16. Nothing an app author writes has to change.
|
|
368
|
+
|
|
369
|
+
**Consumer migration — one removed export.** The rule id constant leaves the
|
|
370
|
+
published barrel:
|
|
371
|
+
|
|
372
|
+
- `VISIBILITY_ALIAS_DEPRECATED` (`'visibility-alias-deprecated'`) is removed from
|
|
373
|
+
`@objectstack/lint`. Delete the import; no finding carries that `rule` value
|
|
374
|
+
any more, so a `suppressWarnings: ['visibility-alias-deprecated']` entry or a
|
|
375
|
+
filter comparing against it is now dead code and can go with it.
|
|
376
|
+
|
|
377
|
+
The other three rules in the same module are **unchanged** — they judge the
|
|
378
|
+
predicate's _value_, which crosses the fold into `visibleWhen` intact, and each
|
|
379
|
+
still reports normally on the `normalized` tier:
|
|
380
|
+
`visibility-root-mislayered`, `visibility-bare-identifier`,
|
|
381
|
+
`visibility-predicate-syntax`. `checkElement` also keeps reading the predicate
|
|
382
|
+
through the deprecated keys (canonical-first, so an alias can never override
|
|
383
|
+
`visibleWhen`), which is what lets those three still judge an alias-spelled
|
|
384
|
+
predicate handed to the exported function directly.
|
|
385
|
+
|
|
386
|
+
Retired rather than re-anchored: making the rule read a genuine pre-normalize
|
|
387
|
+
value would have changed `runAuthoringRules`' external input contract, which is
|
|
388
|
+
a `packages/lint` public-API decision for the maintainer rather than a rule
|
|
389
|
+
file's to take.
|
|
390
|
+
|
|
391
|
+
<!-- adr-0087: not-required (already-registered view-visibleOn-to-visibleWhen, page-component-visibility-to-visibleWhen) The authored alias surface is already covered by those two D2 conversions, which are unchanged by this PR — they keep accepting `visibleOn` / `visibility`, keep folding them to `visibleWhen`, and keep their protocol-16 retirement window. This change removes only a lint rule id from a TS export surface; no authored or stored metadata shape changes, so there is nothing new for the ledger to carry. -->
|
|
392
|
+
|
|
393
|
+
### Patch Changes
|
|
394
|
+
|
|
395
|
+
- e0f300b: feat(spec): `ChartAggregateSchema` / `ChartGroupBySchema` reject unknown keys instead of dropping them (#5583, #4001 批 15's last two sites)
|
|
396
|
+
|
|
397
|
+
`<ObjectChart aggregate={{ … }}>` is the react tier's object-bound chart binding,
|
|
398
|
+
and until now a key it did not declare was **silently stripped by the parse**.
|
|
399
|
+
`groupby` for `groupBy` degraded the chart to a single ungrouped point, `fn` for
|
|
400
|
+
`function` fell back to the default, `dateGranularty` for `dateGranularity`
|
|
401
|
+
turned off date bucketing — each with `os build` / `os validate` fully green.
|
|
402
|
+
That is #4001's founding failure mode, on the surface an AI page author is most
|
|
403
|
+
likely to write.
|
|
404
|
+
|
|
405
|
+
Both object shapes are `strictObject` now, so an undeclared key is a named
|
|
406
|
+
rejection carrying the surface, the offending key and a rename:
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
Unrecognized key(s) on this chart aggregate: `groupby`.
|
|
410
|
+
Did you mean `groupby` → `groupBy`? Until #5583 an undeclared aggregate key was
|
|
411
|
+
dropped at parse — …
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Curated beyond edit distance where the near-miss is semantic rather than a typo:
|
|
415
|
+
`fn` / `agg` / `aggregation` → `function`, `measure` → `field`, and the ADR-0021
|
|
416
|
+
dataset vocabulary an author carries over from the other binding mode
|
|
417
|
+
(`dimension` / `category` → `groupBy`). Wrong-LAYER keys get a prescription
|
|
418
|
+
instead of a rename — `dateGranularity` written _beside_ `groupBy` did nothing at
|
|
419
|
+
all and now says where it belongs; `alias`, `filter`, `objectName` and a
|
|
420
|
+
`measures` array are pointed at the surface that owns them.
|
|
421
|
+
|
|
422
|
+
**Why this took two issues.** `.strict()` is a property of a PARSE, and until
|
|
423
|
+
#5020 nothing parsed these schemas: the react-page publish gate re-derived the
|
|
424
|
+
vocabulary by hand. Closing them first would have shipped a precisely-validated
|
|
425
|
+
door with nothing behind it (#4583). #5020 wired the parse; this is the posture.
|
|
426
|
+
|
|
427
|
+
**The zod-4 union collapse is load-bearing here.** `groupBy` is a union, so the
|
|
428
|
+
`unrecognized_keys` its strict arm raises never reaches `error.issues` — zod
|
|
429
|
+
reports one `invalid_union` whose own message is the bare string `"Invalid
|
|
430
|
+
input"`. What carries the named rejection to the author is `packages/lint`'s
|
|
431
|
+
`describeIssue` arm unpacking, pinned end to end on both sides.
|
|
432
|
+
|
|
433
|
+
**`groupBy` stays REQUIRED — the product question this pair raised is answered,
|
|
434
|
+
and the answer does not move the schema.** An ungrouped single-value chart is
|
|
435
|
+
not a supported `<ObjectChart>` shape: the single-value need is served by the
|
|
436
|
+
separate `object-metric` block, the example corpus authors zero ungrouped
|
|
437
|
+
`<ObjectChart>` aggregates, and objectui's `schema.aggregate?.groupBy ||
|
|
438
|
+
schema.xAxisKey` reads are optional-chained on `aggregate` itself — they serve
|
|
439
|
+
charts with **no aggregate at all**, not ungrouped ones. #5020's `warning`-level
|
|
440
|
+
tolerance for an absent `groupBy` therefore stays a tolerance rather than
|
|
441
|
+
becoming a blessing; its hint now states the ruling.
|
|
442
|
+
|
|
443
|
+
**Upgrading:** if a chart aggregate carried a key this schema does not declare,
|
|
444
|
+
it was already being ignored — the rejection names it and prescribes the fix. No
|
|
445
|
+
legal declaration changes meaning.
|
|
446
|
+
|
|
447
|
+
- e9b5265: fix(formula,lint): `current_user` becomes a declared root, and its field-level rejection becomes a real rule (#6290)
|
|
448
|
+
|
|
449
|
+
`@objectstack/formula` told two stories about one root. `introspectScope` handed
|
|
450
|
+
`current_user` to authors as a legal namespace and `checkRoleCatalog`'s four
|
|
451
|
+
position-membership regexes all lead with it — both correct, because ADR-0068 D1
|
|
452
|
+
makes `current_user` THE canonical spelling and `buildScope` really does mount
|
|
453
|
+
the same `EvalUser` under it. Only `cel-engine.ts`'s `SCOPE_ROOTS` disagreed, so
|
|
454
|
+
the strict environment read the blessed spelling as a BARE FIELD REFERENCE while
|
|
455
|
+
its two aliases (`user`, `ctx`) passed unremarked.
|
|
456
|
+
|
|
457
|
+
Three things change.
|
|
458
|
+
|
|
459
|
+
**1. `SCOPE_ROOTS` declares `current_user`.** That list is a "never faults"
|
|
460
|
+
baseline, not a per-surface contract, and it now advertises exactly what the
|
|
461
|
+
package advertises elsewhere. A new pin asserts the property directly: every
|
|
462
|
+
root `introspectScope` reports must resolve in the strict env.
|
|
463
|
+
|
|
464
|
+
**2. The wrong prescription is gone.** Because the rejection used to fall out of
|
|
465
|
+
the baseline's omission, the author got the GENERIC bare-field diagnostic —
|
|
466
|
+
"Write `record.current_user`". That shape binds on no layer of the platform, so
|
|
467
|
+
an author who followed the message ended up with something strictly worse than
|
|
468
|
+
what they started with, still silent. The field-level verdict now comes from a
|
|
469
|
+
rule of its own in `@objectstack/lint`, which names the real failure (unbound ⇒
|
|
470
|
+
fault ⇒ visibility falls back to `true` ⇒ the field a `current_user` test was
|
|
471
|
+
meant to hide stays visible for everyone, #6146) and prescribes surfaces that
|
|
472
|
+
exist: move the predicate to the option's own `visibleWhen`, declare field-level
|
|
473
|
+
security on a permission set (`fields: { '<object>.<field>': { readable: false } }`),
|
|
474
|
+
or rewrite it against `record`. It covers `visibleWhen`, `readonlyWhen` and
|
|
475
|
+
`requiredWhen`, which share the one evaluator.
|
|
476
|
+
|
|
477
|
+
**3. Per-option `visibleWhen` is validated at all.** `validate-expressions.ts`
|
|
478
|
+
walked field-level conditional rules and stopped there, so `SelectOption.visibleWhen`
|
|
479
|
+
— an authorable CEL slot the client filters on AND the server enforces — reached
|
|
480
|
+
compile, validate and run time checked by nobody. A bare field reference, a
|
|
481
|
+
reference to a field that does not exist, a syntax error or a template-dialect
|
|
482
|
+
predicate in an option all shipped in silence, and the option simply never
|
|
483
|
+
offered itself. Options are now walked, located by option value, on the same
|
|
484
|
+
`record` scope as their host field.
|
|
485
|
+
|
|
486
|
+
The two surfaces deliberately give opposite verdicts on `current_user`, because
|
|
487
|
+
their evaluators differ: field-level rules go through `evalFieldPredicate`
|
|
488
|
+
(`record` + `previous` + `parent`, never a user), options through
|
|
489
|
+
`resolveCascadingOptions` against the host's predicate scope, which does bind it
|
|
490
|
+
(ADR-0068 / objectui#2284). The showcase's role-gated option
|
|
491
|
+
(`'admin' in current_user.positions`) had never met this rule before and is now
|
|
492
|
+
pinned as the legal usage it is.
|
|
493
|
+
|
|
494
|
+
Sweep: `objectstack validate` is clean on all three example apps
|
|
495
|
+
(`app-showcase`, `app-crm`, `app-todo`) with the option walk active — zero new
|
|
496
|
+
findings, including the showcase object that carries both a record-scoped
|
|
497
|
+
cascade and the role-gated option.
|
|
498
|
+
|
|
499
|
+
- d5e9f6e: 字段级 `*When` 的未绑定根检查:黑名单翻成白名单,并把因果句按槽位分档
|
|
500
|
+
|
|
501
|
+
同一段诊断上的两条**正交**分档轴,一次设计通过 —— 分开做会把这段文案写两遍,
|
|
502
|
+
且第二遍推翻第一遍。
|
|
503
|
+
|
|
504
|
+
## 轴一:根集合从 3 项黑名单翻成 3 项白名单(#6713)
|
|
505
|
+
|
|
506
|
+
字段级 `visibleWhen` / `readonlyWhen` / `requiredWhen` 实测只绑 `record`、
|
|
507
|
+
`previous`、`parent` 三个根,三处独立证据一致:服务端
|
|
508
|
+
`rule-validator.ts` 的两处绑定(`readonlyWhen` 绑
|
|
509
|
+
`{ record, previous, extra: { parent } }`,`requiredWhen` 绑
|
|
510
|
+
`{ record, previous, ...parentScope }`);客户端 `evalFieldPredicate` 绑
|
|
511
|
+
`record` + `previous` + 调用方 `scope`,而 objectui 全部五个字段级调用点
|
|
512
|
+
(`form.tsx` ×3、`WizardForm.tsx`、`GridField.tsx`)传的 `scope` 只可能是
|
|
513
|
+
`undefined` 或 `{ parent }`;作者端 objectui 的
|
|
514
|
+
`FIELD_RULE_ROOTS = ['record', 'previous', 'parent']`,注释明写 "nothing else"。
|
|
515
|
+
|
|
516
|
+
而检查此前是一张**黑名单** —— #6584 一项、#6711 三项
|
|
517
|
+
(`current_user` / `user` / `ctx`)。黑名单在这个面上结构性地追不上
|
|
518
|
+
`SCOPE_ROOTS`:每新增一个根都要有人记得抄过来(`current_user` 自己就是 #6290
|
|
519
|
+
加进去、#6584 才被发现的)。实测有 **21 个根**落在这条缝里,它们同样未绑定、
|
|
520
|
+
同样 fault、而且同样**静默** —— 都在 `SCOPE_ROOTS` 里,所以裸引用检查也从不
|
|
521
|
+
报它们。其中两个是高可信度的作者笔误而非理论成员:
|
|
522
|
+
|
|
523
|
+
- `os.user.id` —— ADR-0068 D1 的**第四种**用户拼写(`buildScope` 把同一个
|
|
524
|
+
`EvalUser` 挂在 `current_user` / `user` / `ctx.user` / `os.user` 下),#6711
|
|
525
|
+
收了三种,`os` 这一支没收;
|
|
526
|
+
- `data.status == 'x'` —— `data` 是**元数据表单**里同一个 `visibleWhen` 键的
|
|
527
|
+
**合法**根(`view.zod.ts`:"Root: `record` … in runtime forms, or `data` in
|
|
528
|
+
metadata forms"),两种表单同一个键名、不同的根。
|
|
529
|
+
|
|
530
|
+
判定改为 `SCOPE_ROOTS` 成员减去白名单,列表直接从 `@objectstack/formula` 取,
|
|
531
|
+
不在消费端重述 —— 因此 `SCOPE_ROOTS` 将来新增的成员自动被覆盖。
|
|
532
|
+
|
|
533
|
+
处方随之**按根分档**:用户根(`current_user` / `user` / `ctx` / `os`)保留原有
|
|
534
|
+
的选项级 `visibleWhen` 与权限集 FLS 两条用户向处方;`data` 给出元数据表单 vs
|
|
535
|
+
运行期表单的解释;其余根给出通用的「改写成 `record` 谓词」。此前只有用户向处方,
|
|
536
|
+
对写了 `data.type == 'select'` 的作者是答非所问。
|
|
537
|
+
|
|
538
|
+
## 轴二:因果句按槽位分档(#6716)
|
|
539
|
+
|
|
540
|
+
三个槽位此前共用一句「falls back to VISIBLE … showing for everyone」,而这句话
|
|
541
|
+
只对其中一个精确。三格全部**实测**,每格量了两端:
|
|
542
|
+
|
|
543
|
+
- **`visibleWhen` —— 仅客户端、fail-OPEN,原文案正确。** 服务端根本不评估字段级
|
|
544
|
+
`visibleWhen`(`ConditionalFieldDef` 无此成员,`fieldsNeedPrior` 只看
|
|
545
|
+
`requiredWhen || readonlyWhen ||` 选项可见性),唯一裁决来自渲染端,
|
|
546
|
+
`resolveFieldRuleState` 对可见性传 `fallback: true`。
|
|
547
|
+
- **`readonlyWhen` —— 两端方向相反,服务端说了算,原文案是反的。** 服务端
|
|
548
|
+
`isReadonlyWhenLocked` 命中 `unknownVariableOf` 后返回 `true`(#4889 的
|
|
549
|
+
carve-out,其触发条件正是未绑定根这一类),`stripReadonlyWhenFields` 随即把该
|
|
550
|
+
字段从 payload 中删除;客户端 `resolveFieldRuleState` 传 `fallback: false`,
|
|
551
|
+
表单仍渲染为可编辑。按 ADR-0057 D10(server enforces, client is courtesy)以
|
|
552
|
+
服务端为准:作者改了字段、保存报成功、值静默不落库。原文案告诉作者「对所有人
|
|
553
|
+
可见」—— 失败方向与排障方向都相反。
|
|
554
|
+
- **`requiredWhen` —— 两端都 fail-OPEN,且与可见性无关。** 服务端记日志后
|
|
555
|
+
`continue`(#4977 明确没有采用 #4889 的 carve-out),客户端 `fallback: false`,
|
|
556
|
+
两端都不强制,记录带着空字段保存成功。原文案在这里不只是不精确,而是说错了
|
|
557
|
+
字段的哪个属性。
|
|
558
|
+
|
|
559
|
+
`conditionalRequired` 在 `FieldSchema` 里是 `retiredKey`(按名字拒绝),解析后的
|
|
560
|
+
编译路径上该分支是惰性的,因此给它一条与槽位无关的通用句,而不是编造第四格测量。
|
|
561
|
+
|
|
562
|
+
## `@objectstack/formula`
|
|
563
|
+
|
|
564
|
+
`SCOPE_ROOTS` 改为公开导出。一个绑定**封闭**根集合的面,必须能说出它**不**绑定
|
|
565
|
+
的那些根,而那个补集就是 `SCOPE_ROOTS` 减去该面自己的白名单;消费端手抄的列表
|
|
566
|
+
追不上这张表。注意它不能用 `firstUndeclaredReference` 替代:严格环境同时声明了
|
|
567
|
+
CEL 的**类型名**,`type(record.x) == string` 里的 `string` 会被判成「能解析的根」
|
|
568
|
+
—— 实测按可解析性判定会误杀这条合法谓词(1 例),按 `SCOPE_ROOTS` 成员判定不会。
|
|
569
|
+
|
|
570
|
+
- e48d861: fix(lint): the field-level user-root rejection covers all three ADR-0068 spellings, not just `current_user` (#6585)
|
|
571
|
+
|
|
572
|
+
#6290 gave field-level `visibleWhen` / `readonlyWhen` / `requiredWhen` a
|
|
573
|
+
surface-level rejection when the predicate reaches for the signed-in user, with
|
|
574
|
+
a prescription that names surfaces which actually bind one. That check matched a
|
|
575
|
+
single spelling — `current_user` — while ADR-0068 D1 makes `user` and `ctx.user`
|
|
576
|
+
**the same object under different names**: `buildScope` hangs one `EvalUser`
|
|
577
|
+
reference on `current_user` / `user` / `ctx.user` / `os.user`. So the identical
|
|
578
|
+
semantic error produced an error under one spelling and **total silence** under
|
|
579
|
+
the other two (both have always been in `SCOPE_ROOTS`, so the bare-reference
|
|
580
|
+
check never fired on them either). Which of three ADR-equivalent spellings the
|
|
581
|
+
author happened to pick decided whether they got a build-time diagnostic at all.
|
|
582
|
+
|
|
583
|
+
The failure direction is the one #6146 named: an unbound root faults, the fault
|
|
584
|
+
falls back, and visibility's fallback is `true` — so a predicate written to HIDE
|
|
585
|
+
a field by role left it visible to everyone, silently.
|
|
586
|
+
|
|
587
|
+
All three roots now share one verdict, one prescription and one message; only
|
|
588
|
+
the root named in the message varies. Nothing about the option level changes:
|
|
589
|
+
per-option `visibleWhen` resolves against the host's predicate scope, which
|
|
590
|
+
binds the user under every spelling, so the showcase's role-gated option
|
|
591
|
+
(`'admin' in current_user.positions`) stays legal — under the aliases too.
|
|
592
|
+
|
|
593
|
+
**`ctx` is judged as a whole root, not only in `ctx.user` form.** At this
|
|
594
|
+
surface that is simply what is true: `buildScope` creates the `ctx` root _only_
|
|
595
|
+
when the evaluation carries a user, and no field-level site passes one — the
|
|
596
|
+
server binds `record` + `previous` (+ `parent`) and the client's
|
|
597
|
+
`evalFieldPredicate` binds `record` + `previous` + a caller scope that is only
|
|
598
|
+
ever `{ parent }`. `ctx.locale` therefore faults exactly like `ctx.user.id`
|
|
599
|
+
here. The narrower reading was rejected because it needs a source-level spelling
|
|
600
|
+
match, which would re-open this very fork one level down (`ctx["user"].id`
|
|
601
|
+
silent, `ctx.user.id` rejected) while leaving a real fail-open fault
|
|
602
|
+
unreported. `ctx` remains ActionEngine's predicate root elsewhere and is
|
|
603
|
+
untouched there — the platform's own `ctx.user` predicates all sit on action
|
|
604
|
+
`visible` (`sys-user.object.ts`, `sys-invitation.object.ts`), a surface this
|
|
605
|
+
rule never reads, and that acceptance is pinned.
|
|
606
|
+
|
|
607
|
+
Sweep: field-level `*When` predicates reading any user root measure **zero**
|
|
608
|
+
across `examples/`, `packages/` and the downstream `objectui` repo, by both a
|
|
609
|
+
slot-keyed scan and an alias-keyed one — so no shipping metadata is refused by
|
|
610
|
+
the widening. `objectstack validate` stays clean on all three example apps.
|
|
611
|
+
|
|
612
|
+
- b127c8b: fix(spec,core): a filter placeholder is recognised by INTENT — `{TODAY()}` refuses loudly instead of comparing as a literal (#5586)
|
|
613
|
+
|
|
614
|
+
`UnknownFilterTokenError` had a hole exactly where authors fall in. Recognition
|
|
615
|
+
used the token-NAME grammar `/^\$?\{([a-zA-Z0-9_]+)\}$/`, so any placeholder
|
|
616
|
+
carrying a **non-word character** classified as "not a placeholder at all" and
|
|
617
|
+
was handed to the driver verbatim, to be compared as a literal string — the
|
|
618
|
+
silent-wrong-result failure the diagnostic exists to abolish.
|
|
619
|
+
|
|
620
|
+
The failure was inverted against the author. Measured on 17.0.0-rc.2 against a
|
|
621
|
+
four-row fixture:
|
|
622
|
+
|
|
623
|
+
| filter value | before | |
|
|
624
|
+
| ------------------------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
625
|
+
| `due_date < '{today}'` | 2 rows | correct — the two overdue rows |
|
|
626
|
+
| `due_date < '{TODAY}'` | throws `UnknownFilterTokenError` | diagnostic working |
|
|
627
|
+
| `due_date < '{TODAY()}'` | **4 rows** | diagnostic bypassed — literal string compare, and `'2026-…' < '{'` in lexicographic order swallowed a row due a week later |
|
|
628
|
+
|
|
629
|
+
So misspelling `{today}` as `{TODAY}` was reported by name, while misspelling it
|
|
630
|
+
as `{TODAY()}` returned the wrong rows in silence — and the parenthesised,
|
|
631
|
+
kebab-case, natural-language and dotted spellings (`{TODAY()}`,
|
|
632
|
+
`{current-user-id}`, `{30 days ago}`, `{user.id}`) are precisely what an author
|
|
633
|
+
migrating from another system's macro syntax writes first.
|
|
634
|
+
|
|
635
|
+
**Both directions of the behaviour change:**
|
|
636
|
+
|
|
637
|
+
- **Previously silent, now refuses loudly** — a filter value that is entirely
|
|
638
|
+
brace-wrapped and outside the vocabulary now throws `UnknownFilterTokenError`
|
|
639
|
+
(`code: FILTER_TOKEN_UNKNOWN`, `status: 400`) on the ObjectQL read and write
|
|
640
|
+
paths and the analytics dataset executor, and is reported as
|
|
641
|
+
`filter-token-unknown` by `objectstack build` / `validate` / `lint`. Before,
|
|
642
|
+
it reached the data engine and compared as text.
|
|
643
|
+
- **Unchanged** — `{today}` / `{current_user_id}` still resolve; `{TODAY}` still
|
|
644
|
+
refuses with the same identity; a value that merely _contains_ braces
|
|
645
|
+
(`'acme {x} deal'`), or is not ONE pair around the whole value (`{a}{b}`,
|
|
646
|
+
`{{x}}`, `{}`), is still an ordinary literal and still reaches the driver
|
|
647
|
+
untouched.
|
|
648
|
+
|
|
649
|
+
Recognition and vocabulary are now two named grammars rather than one:
|
|
650
|
+
`FILTER_TOKEN_WRAPPED_RE` (`/^\$?\{([^{}]+)\}$/`) answers "did the author mean a
|
|
651
|
+
placeholder", and `isContextToken` / `isDateMacroToken` answer "is it in the
|
|
652
|
+
vocabulary". Wide in, strict out. No escape hatch for a literal `{…}` comparand
|
|
653
|
+
ships with this: a repo-wide measurement across structured metadata, examples,
|
|
654
|
+
seed data and fixtures found zero legitimate consumers comparing a
|
|
655
|
+
brace-wrapped literal, and an escape syntax is a public micro-contract that can
|
|
656
|
+
be added the day one shows up.
|
|
657
|
+
|
|
658
|
+
Flow templates are unaffected. `interpolateFilter` in
|
|
659
|
+
`@objectstack/service-automation` already recognised the same wide shape and
|
|
660
|
+
resolves `{record.id}` / `{TODAY() + 30}` from flow variables **before** the
|
|
661
|
+
filter reaches ObjectQL; its hand-off to the engine is keyed on the token
|
|
662
|
+
vocabulary (`isKnownFilterToken`), which this change does not touch.
|
|
663
|
+
|
|
664
|
+
- 01fd9e1: fix(lint): `validateFormLayout` walks the view CONTAINER ladder, so both its rules stop reporting clean on every real app (#6251)
|
|
665
|
+
|
|
666
|
+
`form-field-unknown` and `absolute-colspan-discouraged` read a `sections` array
|
|
667
|
+
off the **`views[]` entry itself** and skipped everything else. But a `views[]`
|
|
668
|
+
entry is a view CONTAINER, not a view: `ViewSchema` declares exactly `name` /
|
|
669
|
+
`label` / `object` / `list` / `form` / `listViews` / `formViews`, and form
|
|
670
|
+
sections live one level down, under `form` and each `formViews.<key>`. So the
|
|
671
|
+
one shape the traversal read is the one shape strict `ViewSchema` **refuses** —
|
|
672
|
+
measured, `unrecognized_keys` naming `sections` — and the shapes every app
|
|
673
|
+
actually ships were never inspected at all.
|
|
674
|
+
|
|
675
|
+
Measured on the three shipped example apps, before and after: `app-showcase`,
|
|
676
|
+
`app-crm` and `app-todo` carry **0** form sites at the entry root and **14**
|
|
677
|
+
under `form` / `formViews.<key>`. The old traversal therefore had nothing to
|
|
678
|
+
read on any of them, and reported clean for that reason — the "ghost check"
|
|
679
|
+
shape (#4984 / #5009): a rule that is green because it never read anything is
|
|
680
|
+
worse than no rule, because it occupies the slot that would otherwise look
|
|
681
|
+
empty.
|
|
682
|
+
|
|
683
|
+
One broken form, three placements, before → after:
|
|
684
|
+
|
|
685
|
+
| placement | before | after |
|
|
686
|
+
| ---------------------------------------------------- | ------- | ------- |
|
|
687
|
+
| `views[0].sections` (entry IS a bare form view) | reports | reports |
|
|
688
|
+
| `views[0].form.sections` (container default form) | silent | reports |
|
|
689
|
+
| `views[0].formViews.edit.sections` (named form view) | silent | reports |
|
|
690
|
+
|
|
691
|
+
What changed, precisely:
|
|
692
|
+
|
|
693
|
+
- The traversal is the one `validate-visibility-predicates.ts` landed in #6248
|
|
694
|
+
for the identical hole on the sibling rule — copied, not re-derived, so two
|
|
695
|
+
rules on one surface cannot drift apart about which forms exist. `list` /
|
|
696
|
+
`listViews.<key>` are `ObjectListViewSchema` and carry no `sections`, so they
|
|
697
|
+
are deliberately not walked; `objects[].views` stays out because
|
|
698
|
+
`object.zod.ts` tombstones that key by name.
|
|
699
|
+
- The legacy `groups` bucket (`FormSectionSchema[]`, the documented alias of
|
|
700
|
+
`sections`) is read too. Measured: it is **not** folded into `sections` at
|
|
701
|
+
parse, so a `groups`-authored form was a second silent shape.
|
|
702
|
+
- A finding names its sub-container — `view "contact_views" · formViews.create`
|
|
703
|
+
— because an artifact-emitted container carries neither `name` nor `object`,
|
|
704
|
+
and without it two forms under one view were indistinguishable.
|
|
705
|
+
- A sub-container inherits the container's object binding when it declares no
|
|
706
|
+
`data.object` of its own, resolved through the same `objectName` → `object` →
|
|
707
|
+
`data.object` ladder the other view-walking rules in this package use.
|
|
708
|
+
- A map-shaped `views` reports at the key it sits at (`views.contact_views.…`)
|
|
709
|
+
rather than a synthetic index, so a finding stays usable as an edit target.
|
|
710
|
+
|
|
711
|
+
Both rules remain advisory `warning`s and their messages, hints and severities
|
|
712
|
+
are unchanged. No new finding appeared on any example app, so nothing that was
|
|
713
|
+
green goes red on existing metadata — what changes is that a form defect in the
|
|
714
|
+
places apps actually put forms is now reported instead of silently passed.
|
|
715
|
+
|
|
716
|
+
- a5ca08d: fix(lint): `object/missing-name-field` 认 `nameField`、不再把已退役的 `titleFormat` 当作 name 面(#6108)
|
|
717
|
+
|
|
718
|
+
`object/missing-name-field` 的谓词从来不读 `obj.nameField`,却仍然采信 `obj.titleFormat`:
|
|
719
|
+
|
|
720
|
+
```
|
|
721
|
+
hasNameField = !!obj.primaryField || !!obj.titleFormat || fields.some(name-like)
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
净效果是同一个包里两条规则互相矛盾。`validate-record-title.ts` 把每一处 `titleFormat`
|
|
725
|
+
声明都报成 `title-format-retired`,并按 **ADR-0079** 指示作者迁移到 `nameField`
|
|
726
|
+
(`titleFormat` 是 render-only 模板,服务端既不能返回也不能查询);而共享的
|
|
727
|
+
`objectTitleCompleteness`(`@objectstack/spec/data`)判定标题面时也从不读它。于是:
|
|
728
|
+
**照平台自己的迁移建议把 `titleFormat` 换成 `nameField` 的对象,反而多得一条
|
|
729
|
+
"records will display as raw IDs" suggestion;守着已退役的键不动的对象反而干净。**
|
|
730
|
+
|
|
731
|
+
下游实测(hotcrm main,`@objectstack/* 17.0.0-rc.3`):6 处命中里 4 处是误报,
|
|
732
|
+
四个对象——`crm_campaign_member` / `crm_event_attendee` / `crm_contract` /
|
|
733
|
+
`crm_forecast`——都显式声明了 `nameField`;只有两个 line-item 对象是真命中。
|
|
734
|
+
|
|
735
|
+
本次修正:
|
|
736
|
+
|
|
737
|
+
- 谓词补读 `nameField`(ADR-0079 的规范主标题指针),显式声明它的对象不再被告警;
|
|
738
|
+
- 摘掉 `titleFormat` 这一支。**只声明 `titleFormat`、没有 `nameField` 的对象因此会
|
|
739
|
+
新得一条本规则的 suggestion** —— 这是刻意的翻转,不是回归:这类对象正是 ADR-0079
|
|
740
|
+
要求迁移的那一批,`validate-record-title` 今天已经对它同时报
|
|
741
|
+
`title-format-retired` 与 `title-unresolvable`。两条规则从此对同一个对象给出一致判断;
|
|
742
|
+
- `primaryField` 与 name-like 字段两支行为不变;
|
|
743
|
+
- 提示文案改为只点名作者真正能声明的面(`nameField` 与 name-like 字段),并新增 `fix` 提示
|
|
744
|
+
说明 `titleFormat` 不算标题面 —— 读到旧文案的作者很容易顺手再写一个 `titleFormat`,
|
|
745
|
+
又掉回同一个矛盾里。旧文案里的 `primaryField` 同时不再出现:该键在 `packages/spec` 中
|
|
746
|
+
没有任何声明,`ObjectSchema.create()` 会以 `unrecognized_keys` 拒收它(实测,已立 #6326),
|
|
747
|
+
提示不该向作者广告一个会被 schema 硬拒的键。谓词里的这一支保持不动。
|
|
748
|
+
|
|
749
|
+
- 6ce10bd: fix(lint): 摘掉 `primaryField` 这个幽灵键——两条规则不再把它当作标题面(#6326)
|
|
750
|
+
|
|
751
|
+
`primaryField` 在 `packages/spec` 里**没有任何声明**。实测(`17.0.0-rc.5` dist):
|
|
752
|
+
|
|
753
|
+
```
|
|
754
|
+
ObjectSchema.safeParse({ name: 'probe_obj', label: 'Probe', primaryField: 'code',
|
|
755
|
+
fields: { code: { type: 'text', label: 'Code' } } })
|
|
756
|
+
// => success: false
|
|
757
|
+
// => issues: [{ code: 'unrecognized_keys', keys: ['primaryField'], path: [] }]
|
|
758
|
+
|
|
759
|
+
ObjectSchema.create(/* 同上 */)
|
|
760
|
+
// => throws: ObjectSchema.create('probe_obj'): unknown key(s) — primaryField.
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
同一形状换成 `nameField: 'code'` 则 `safeParse` 通过。也就是说,这个键**从来不是可声明面**,
|
|
764
|
+
而三处消费者没跟上——两面同源,却各自有一个可达面:
|
|
765
|
+
|
|
766
|
+
- **文档面(作者会照做,当下活着的那一半)**:`skills/objectstack-data/SKILL.md` 是 AI 编写
|
|
767
|
+
元数据时读的技能文档,它把 `primaryField` 明说成 `object/missing-name-field` 的合法逃逸口。
|
|
768
|
+
照它写出来的对象在 `ObjectSchema.create()` 上被 ADR-0032「不静默丢弃未知键」的闸硬拒——
|
|
769
|
+
**这是在教 AI 写出必然失败的元数据。**
|
|
770
|
+
- **规则面(判定永不成立)**:`data-model-rules.ts` 的 `!!obj.primaryField` 一支,以及
|
|
771
|
+
`validate-semantic-roles.ts` 标题解析链里的那一项,对任何 schema 收得下的对象恒为 false,
|
|
772
|
+
属于 #4984 家族的死支——看起来在保护什么,实际什么都判不到。
|
|
773
|
+
|
|
774
|
+
本次按维护者裁定 **remove,不 declare**(`nameField` 已是 ADR-0079 的规范主标题指针,
|
|
775
|
+
再立一个平行指针没有拉力,且与 Prime Directive #7「One Zod source per metadata type」相悖):
|
|
776
|
+
|
|
777
|
+
- `data-model-rules.ts`:`object/missing-name-field` 的谓词收敛为
|
|
778
|
+
`!!obj.nameField || fields.some(name-like)`;
|
|
779
|
+
- `validate-semantic-roles.ts`:规则 (d) 的标题解析链收敛为
|
|
780
|
+
`[nameField, displayNameField]`(`displayNameField` 实测可声明,保留);
|
|
781
|
+
- `skills/objectstack-data/SKILL.md`:该规则的表述改为只点名作者真正能声明的面——
|
|
782
|
+
`nameField` 与 name-like 字段(并列出这七个名字)。
|
|
783
|
+
|
|
784
|
+
**零 `packages/spec` 改动,不需要迁移:`primaryField` 从来不是可声明键,写了它的对象在
|
|
785
|
+
schema 上本来就发布不了,所以没有任何能工作的 app 会因此回归。** 行为上唯一的变化是:
|
|
786
|
+
一个只靠 `primaryField` 充当标题面的对象,现在会新得一条 `object/missing-name-field`
|
|
787
|
+
的 suggestion(severity 为 suggestion,不失败命令)——而这类对象本就通不过 `ObjectSchema`。
|
|
788
|
+
真正的修法是改声明 `nameField`。
|
|
789
|
+
|
|
790
|
+
- 5087ac6: fix(lint): script-node retired-key diagnostic no longer says "rewrite it" (#7030)
|
|
791
|
+
|
|
792
|
+
`validate-expressions`'s lint diagnostic for a `script` node carrying a retired
|
|
793
|
+
dispatch key (`config.actionType` / `template` / `recipients` / `variables` /
|
|
794
|
+
`script`, retired in `@objectstack/spec` 17, #4343) closed with `Run \`os
|
|
795
|
+
migrate meta --from 16\` to rewrite it automatically.`For the`template`/`recipients`/`variables`/`script`branches the value is **deleted**,
|
|
796
|
+
not rewritten into anything, so "rewrite **it**" named the wrong antecedent —
|
|
797
|
+
the same false-antecedent shape #6856 (route D, maintainer-ruled) already swept
|
|
798
|
+
out of every`packages/spec/src` tombstone. This was the one live site the
|
|
799
|
+
sweep's scan surface (`packages/spec/src` only) could not see.
|
|
800
|
+
|
|
801
|
+
The sentence now reads `Run \`os migrate meta --from 16\` to rewrite existing
|
|
802
|
+
sources automatically.`— naming a property of the TOOL (it rewrites your
|
|
803
|
+
source files), never the retired key's fate, which the message body already
|
|
804
|
+
states per branch. Message copy only: the diagnostic still fires on the same
|
|
805
|
+
inputs, at the same severity, with the same`#4343` / per-key / replacement
|
|
806
|
+
guidance untouched.
|
|
807
|
+
|
|
808
|
+
`packages/spec/src/shared/retired-key-migrate-sentence.test.ts` — the #6856
|
|
809
|
+
class pin — is widened to scan `packages/lint/src` alongside `packages/spec/src`
|
|
810
|
+
so this sentence cannot drift from the house form again, in either package.
|
|
811
|
+
That widening is test-only (no `@objectstack/spec` runtime code changed);
|
|
812
|
+
listed here only because the pin's own file lives inside `packages/spec`.
|
|
813
|
+
|
|
814
|
+
- 7618ee8: `translation-target-unknown` 按运行时视图身份判定容器默认 `list` 的 `_views` 键(#5164 第 2 棒 / lint 段)
|
|
815
|
+
|
|
816
|
+
`validate-translation-references` 的 `collectViewRecord()` 过去读 `view.list.name`
|
|
817
|
+
来决定容器默认列表贡献哪个 `_views` 名 —— 作者没写 `name` 时它什么也不注册,而组装器
|
|
818
|
+
(`expandViewContainer`,`packages/spec/src/ui/view.zod.ts`)给同一个视图的身份是
|
|
819
|
+
`<object>.default`。第 1 棒(#6124)已把 i18n 提取器改为向组装器查询同一个键,于是
|
|
820
|
+
**同一次 `os lint` 运行里**出现了一对自相矛盾的结论:
|
|
821
|
+
|
|
822
|
+
- 要求方 `i18n/missing-view`:`objects.<object>._views.default.label` 缺翻译;
|
|
823
|
+
- 否定方 `translation-target-unknown`:`_views.default` 是孤儿键,「no view of object
|
|
824
|
+
`<object>` declares it」。
|
|
825
|
+
|
|
826
|
+
作者补了译文被判孤儿,删了译文被判缺翻译,两条都躲不掉。本仓库自带示例上实测有 8 处
|
|
827
|
+
(`examples/app-showcase` 6 处、`examples/app-todo` 2 处)。
|
|
828
|
+
|
|
829
|
+
本规则现在同样**向组装器查询**这个键,而不是第三次自行推导,因此继承了组装器仅有的三条
|
|
830
|
+
规则:
|
|
831
|
+
|
|
832
|
+
- 无 `name` 的默认列表键为 `default`;带 `name` 的沿用作者的 `name`;
|
|
833
|
+
- 结构上与某个 `listViews` 条目完全相同的默认列表被组装器按签名**折叠**进该条目,只有
|
|
834
|
+
存活的那个键合法 —— 被折叠掉的 `list.name` 不再是合法键(`examples/app-crm` 形状);
|
|
835
|
+
- 因命名冲突被改名的键(`default` → `default_2`)按**改名后**判定,因为改名后的名字才是
|
|
836
|
+
注册表键。
|
|
837
|
+
|
|
838
|
+
## 判定变化(全是 warning,不改 `os lint` 退出码)
|
|
839
|
+
|
|
840
|
+
| 形状 | 变化前 | 变化后 |
|
|
841
|
+
| ----------------------------------------------------------------------- | ------------ | --------------------------------------- |
|
|
842
|
+
| 默认 `list` 无 `name`,包里写 `_views.default.*` | 报孤儿(误报) | 通过 |
|
|
843
|
+
| 默认 `list` 无 `name`,包里写 `_views.list.*` | 报孤儿 | 报孤儿(不变;提示语现在会列出 `default`) |
|
|
844
|
+
| 默认 `list` 与某个 `listViews.<k>` 同签名,包里写 `_views.<list.name>.*` | 通过(漏报) | 报孤儿 —— 该键运行时解析不到 |
|
|
845
|
+
| 默认 `list` 因冲突被改名 `default_2`,包里写 `_views.default_2.*` | 报孤儿(误报) | 通过 |
|
|
846
|
+
|
|
847
|
+
本仓库 12 个受棘轮覆盖的配置上实测:**新增 0 条**,消除 8 条误报;
|
|
848
|
+
`check:i18n-coverage` 基线不变(该棘轮只数 `i18n/` 前缀,本规则不在其内)。
|
|
849
|
+
|
|
850
|
+
裁决依据:维护者 2026-08-06(#5164)—— `_views` 翻译键的 canonical 拼写 = 运行时身份的
|
|
851
|
+
裸键。第 3 棒 objectui `viewSuffixes` 去第二候选(objectui#3502)不在本次变更内。
|
|
852
|
+
|
|
853
|
+
- 2d1ddf0: fix(spec): liveness-ledger follow-through — `dashboard.widgets[].colorVariant` is `live`, and `field.widget`'s note names the widget that is actually stamped (#6774, #6773)
|
|
854
|
+
|
|
855
|
+
Two ledger records that stopped being answerable to reality after objectui
|
|
856
|
+
implementations landed. Both are corrections to the **evidence base**, not new
|
|
857
|
+
judgments: the legal-metadata set is byte-identical before and after, and no
|
|
858
|
+
schema acceptance test changed.
|
|
859
|
+
|
|
860
|
+
## `dashboard.widgets[].colorVariant` — `dead` → `live` (#6774)
|
|
861
|
+
|
|
862
|
+
The 2026-08-03 `dead` verdict was right when it was written: every read of
|
|
863
|
+
`widget.colorVariant` was an authoring surface, `DashboardRenderer` built the
|
|
864
|
+
metric component schema explicitly, and only `options.colorVariant` ever reached
|
|
865
|
+
`MetricWidget`. #5010 ruling B then resolved the enforce-or-remove the other way
|
|
866
|
+
— keep the declaration, objectui implements it — and objectui#3359 /
|
|
867
|
+
PR objectui#3799 (merge `c4c0ac897`) did exactly that: `DatasetWidget` resolves
|
|
868
|
+
the declared token through the accent table `MetricWidget` already shared. This
|
|
869
|
+
repo absorbed it with the `.objectui-sha` pin `09987b68`, whose ancestry over
|
|
870
|
+
that merge is re-verified in the row.
|
|
871
|
+
|
|
872
|
+
So the 16 authored sites — 7 in `packages/platform-objects`' `system_overview`,
|
|
873
|
+
9 across `examples/app-showcase` — now paint the accent they declare. A widget
|
|
874
|
+
that never authored the key, and the enum's own `default`, still resolve to no
|
|
875
|
+
class, so their markup is unchanged.
|
|
876
|
+
|
|
877
|
+
**What changes for an author.** The row drops `authorWarn`/`authorHint`, so
|
|
878
|
+
`os validate` (and any other `@objectstack/lint` consumer) no longer emits
|
|
879
|
+
`liveness-dead-property` telling you to move `colorVariant` under `options`. That
|
|
880
|
+
advisory would now be wrong twice over: the key works where it is declared, and
|
|
881
|
+
`options.colorVariant` is the slot that measured dead. PR #5255's pinned
|
|
882
|
+
"`colorVariant` still warns beside the four retired keys" positive contrast is
|
|
883
|
+
released with it — its premise was that no renderer reads the key.
|
|
884
|
+
|
|
885
|
+
The dashboard ledger now warns on nothing, which is the resolved state
|
|
886
|
+
`webhook` and `email_template` already sit in. `dashboard` stays registered in
|
|
887
|
+
the lint's `TYPE_COLLECTIONS` so a future regression that re-deadens a widget
|
|
888
|
+
key warns on its own, and the block's silence pins gained the anti-vacuity guard
|
|
889
|
+
#4651's area gates use — a lint that had stopped loading ledgers returns "no
|
|
890
|
+
findings" too.
|
|
891
|
+
|
|
892
|
+
## `field.widget` — the note named a widget nobody ever stamped (#6773)
|
|
893
|
+
|
|
894
|
+
The note offered `sys_permission_set (capability-multiselect)` as a worked
|
|
895
|
+
example of the override in use. That half was never true. ADR-0056 P1 stamps
|
|
896
|
+
`permission-facet-link` on all six `sys_permission_set` facets through a single
|
|
897
|
+
choke point, and `field:capability-multiselect` was registered only by objectui's
|
|
898
|
+
docs-site-only `registerFields()` — never by the live `registerAllFields()` walk
|
|
899
|
+
over `fieldWidgetMap` — so authoring it always fell through to the `type`
|
|
900
|
+
renderer. objectui#3308 / PR objectui#3793 then retired the name outright under
|
|
901
|
+
ADR-0049; at pin `09987b68` it survives only as tombstone comments and a
|
|
902
|
+
retirement pin test.
|
|
903
|
+
|
|
904
|
+
The verdict is untouched and was never at risk: `widget` is `live` on the
|
|
905
|
+
`sys_sharing_rule` trio alone (`object-ref` / `filter-condition` /
|
|
906
|
+
`recipient-picker`, all three re-checked in `fieldWidgetMap` at the same pin).
|
|
907
|
+
What was wrong was one example — false evidence in the base the next
|
|
908
|
+
enforce-or-remove audit reads, which is the #5175 lesson.
|
|
909
|
+
|
|
910
|
+
Nothing to migrate in either half: the schemas, the parsed shapes and the
|
|
911
|
+
runtime are unchanged — only the classification of what they already do, and the
|
|
912
|
+
evidence cited for it.
|
|
913
|
+
|
|
914
|
+
- cd584d5: fix(lint): the `element:record_picker` field-binding entry drops #5775's retired `displayField` / `searchFields` (#6629)
|
|
915
|
+
|
|
916
|
+
`COMPONENT_FIELD_SPECS` — the one hand-written table naming which component
|
|
917
|
+
props carry FIELD NAMES — still listed `displayField` and `searchFields` for
|
|
918
|
+
`element:record_picker`, with a comment ("The schema says `displayField`; real
|
|
919
|
+
pages author `labelField`. Accept both.") describing a spec that no longer
|
|
920
|
+
exists. #5775 retired both: `displayField` was renamed to `labelField`
|
|
921
|
+
(ADR-0087 D2) and `searchFields` was deleted (ADR-0049), and both are
|
|
922
|
+
`retiredKey()` tombstones on `ElementRecordPickerPropsSchema`. The entry now
|
|
923
|
+
names `labelField` alone.
|
|
924
|
+
|
|
925
|
+
What changes for an author: a page that writes one of the retired keys no
|
|
926
|
+
longer collects a second `page-field-unknown` finding on top of the #5068 props
|
|
927
|
+
gate's rename/delete prescription. That finding was the misleading half — it
|
|
928
|
+
reported that the field named by a key which no longer exists does not exist
|
|
929
|
+
either, while the prescription is what actually moves the page forward. No
|
|
930
|
+
spec-conformant page is affected; nothing else in the table moves.
|
|
931
|
+
|
|
932
|
+
The harder half of the residue is that nothing reconciled this hand-written
|
|
933
|
+
table against the spec, which is how a retirement that disposed of the schema,
|
|
934
|
+
the tombstone, the ADR-0087 conversion and the generated artifacts still left
|
|
935
|
+
dead spelling standing in a live rule — the ADR-0078 reader face, where the
|
|
936
|
+
next author infers the spelling is current. `component-field-specs-liveness.test.ts`
|
|
937
|
+
closes that class table-wide: every prop the table names must exist on the
|
|
938
|
+
corresponding `ComponentPropsMap` schema and must not be a tombstone, so the
|
|
939
|
+
next retirement that forgets this table goes red naming the entry instead of
|
|
940
|
+
surviving as residue.
|
|
941
|
+
|
|
942
|
+
- 9bc846b: fix(objectql,lint): 服务端为 `requiredWhen` 绑定 parent 作用域,并把构建期硬闸扩到同一格
|
|
943
|
+
|
|
944
|
+
`readonlyWhen` 的 parent 作用域洞在 #4889 已经补上;同一个字段上、由同一个求值器处理的
|
|
945
|
+
`requiredWhen` 隔一个槽位还漏着。detail 对象上声明的
|
|
946
|
+
`` requiredWhen: P`parent.status == 'sent'` `` ——「表头一旦 Sent,每一行都必须填写说明」——
|
|
947
|
+
只在内联表格里被求值,服务端从来只绑 `record` / `previous`,谓词直接 fault 走 fail-open
|
|
948
|
+
分支,写入带着空字段落库,API 还回 200。
|
|
949
|
+
|
|
950
|
+
注意它与 #4889 是**镜像**而不是同一种故障:`readonlyWhen` fail-open 是**写进了本该冻结的字段**,
|
|
951
|
+
`requiredWhen` fail-open 是**收下了本该被拒的记录**。两者都是同一处声明点上的 `declared ≠ enforced`
|
|
952
|
+
(PD #10)。
|
|
953
|
+
|
|
954
|
+
本次按维护者 2026-08-06 的裁决落 A + C 两条,**刻意不对称于 #4889**:
|
|
955
|
+
|
|
956
|
+
- **A —— 绑作用域,求值语义不动。** 引擎用 #4889 已经建好的
|
|
957
|
+
`resolveMasterDetailParent(s)` 解析主表头行并传入求值器,insert / 单 id update /
|
|
958
|
+
bulk update 三个调用点都覆盖。**不可求值仍然 fail-open**(记日志、跳过、放行):
|
|
959
|
+
表头此刻读不到就 422 掉一次本来合法的写入,比 `readonlyWhen` 那边「拒掉一个字段」响得多。
|
|
960
|
+
这是 issue 的 B 案,明确不做,留给 ADR-0058 D5 下一次复审。
|
|
961
|
+
- **C —— 改在构建期拦。** `@objectstack/lint` 的 parent 作用域闸原本只盖 `readonlyWhen`,
|
|
962
|
+
现在同样判 `requiredWhen`:对象没有恰好一个 `master_detail` 关系时,`parent` 不是元数据
|
|
963
|
+
陈述过的事实,声明直接判 error。两格共用同一个闸,但**报错文案不同** —— 两边运行时的失败
|
|
964
|
+
方向相反(`readonlyWhen` fail-closed ⇒ 字段永远写不进;`requiredWhen` fail-open ⇒ 要求
|
|
965
|
+
永远不生效),文案指错了就等于给了相反的修法。运行时敢保持 fail-open,正是因为这道闸
|
|
966
|
+
拦住了那条会无声烂掉的声明。
|
|
967
|
+
|
|
968
|
+
同一次改动里补了 ADR-0113 非回归判定在 parent 作用域下的正确输入:「存量行本来就违规吗」问的是
|
|
969
|
+
**写入前**那一行的状态,而它挂的是**旧**表头。改挂(repoint)到另一个主表时,若把落地表头也
|
|
970
|
+
喂给这个前置判定,就会把「移到 Sent 表头之下」读成既有违规而放行 —— 正是本 issue 要堵的那个
|
|
971
|
+
收下动作,只是换了个入口。因此求值器新增 `previousParent`,仅在载荷确实改挂时由引擎解析,
|
|
972
|
+
其余情况沿用同一行、不多付一次读。
|
|
973
|
+
|
|
974
|
+
对象级 `script` / `cross_field` 规则共用这个求值调用点,自 #4649 起对不可求值谓词是
|
|
975
|
+
**fail-closed**,本次**没有**给它们绑新根 —— 绑了会把它们今天拒掉的写入翻成接受。这条由 pin
|
|
976
|
+
测试钉住(#4972 当初把本改动挡在范围外,就是为了这个爆炸半径)。
|
|
977
|
+
|
|
978
|
+
仓内暂无 app 声明 parent 作用域的 `requiredWhen`(showcase 的 invoice line 用的是行作用域的
|
|
979
|
+
`record.quantity >= 100`),所以这是补潜伏缺口,不改变任何现有 app 的写入行为。
|
|
980
|
+
|
|
981
|
+
- ca522e9: fix(lint): 超预算的 RLS 谓词有了自己的规则 id,不再被当成方言写错 (#6778)
|
|
982
|
+
|
|
983
|
+
`rowLevelSecurity[].using` / `.check` 里一条**语法完美、可下推**、只是太大的 CEL
|
|
984
|
+
谓词(例如 80 项合取,超过 `maxAstNodes` 256),此前报在
|
|
985
|
+
`rls-predicate-unparseable` 名下——那条规则的提示语讲的是 SQL 与 CEL 的方言混淆
|
|
986
|
+
("用 `&&` 别用 `AND`"、"`LIKE` 没有 CEL 拼法")。判决是**对的**,指路是错的:
|
|
987
|
+
作者要做的是把谓词改小或拆开,而不是检查自己的方言。
|
|
988
|
+
|
|
989
|
+
新增第三个 id **`rls-predicate-over-budget`**(与既有两个并列导出):
|
|
990
|
+
|
|
991
|
+
- 消息点名**具体越界的那个界**和平台取值——`maxAstNodes` (256) / `maxDepth` (32)
|
|
992
|
+
/ `maxListElements` (64) 各自报各自的,取自 formula 姊妹入口
|
|
993
|
+
`parseCelToAstWithReason` 的 `kind: 'bounds'` 载荷,而不是写死一个;被告知去缩短
|
|
994
|
+
错误的那根轴,作者就会改错地方。越界谓词按定义很长,引文因此截断到 200 字符。
|
|
995
|
+
- 提示语给的是真正的补救:把长 `||` 链折成 `field in [...]`;把集合预解析成
|
|
996
|
+
`current_user.<key>` 成员键(ADR-0105 D11);把重复子表达式反范式化成本对象上的
|
|
997
|
+
一个 formula/rollup 字段;以及——**只对顶层 `||`** 可以拆成多条策略(适用策略之
|
|
998
|
+
间是 OR),顶层 `&&` 这样拆会**放大**访问权限而不是保持它。
|
|
999
|
+
|
|
1000
|
+
**没有行为变更。** 判定边界仍然是 `isSupportedRlsExpression` 本身,一个字符没动;
|
|
1001
|
+
同样的输入照样被拒,只是其中一类被告知了真正的原因。区分只发生在**解释**里:
|
|
1002
|
+
运行时把越界折叠进 `reason: 'parse-error'` 是有意为之("每个消费者都已经把这个
|
|
1003
|
+
reason 路由到自己的拒绝路径"),对只需决定拒不拒的运行时是对的,对职责就是点明该
|
|
1004
|
+
改哪里的授时诊断则不然。
|
|
1005
|
+
|
|
1006
|
+
该规则不读 `cel-pushdown-limits.ts` 的 GA 日期开关,对它保持中立:17.0.0-rc.x 宽限
|
|
1007
|
+
窗口内越界谓词仍被放行,本规则一条都不报(实测 0 条);v17 GA 翻转后同一谓词被拒,
|
|
1008
|
+
落到新 id 上。两个开关位置都有测试钉住,越界与真正的语法错误两侧各自成对钉住,
|
|
1009
|
+
将来任何把二者重新合并的改动都会变红。
|
|
1010
|
+
|
|
1011
|
+
- 4ac12ef: fix(spec,lint): a virtual `formula` field in `searchableFields` is refused loudly, not admitted verbatim (#6674)
|
|
1012
|
+
|
|
1013
|
+
#4254 closed the fail-open on the unknown-name axis: a `$searchFields` entry the
|
|
1014
|
+
engine would not scan is `400 INVALID_FIELD`, never a silently widened search.
|
|
1015
|
+
The same shape survived one axis over, on names that are perfectly real.
|
|
1016
|
+
|
|
1017
|
+
The declared branch of `resolveSearchFieldResolution` filtered entries by
|
|
1018
|
+
EXISTENCE only, so a `formula` field declared in `searchableFields` entered the
|
|
1019
|
+
allowed set — and the ingress gate, which reads that same set, accepted it for
|
|
1020
|
+
exactly that reason. Measured on `origin/main`:
|
|
1021
|
+
|
|
1022
|
+
```
|
|
1023
|
+
AUTO: {"allowed":["name","project_name"],"source":"auto"} formula excluded
|
|
1024
|
+
DECL-FORMULA: {"allowed":["name","project_name_formula"],"source":"declared"} admitted verbatim
|
|
1025
|
+
?search=Apollo&searchFields=project_name_formula -> 200, 0 rows silent
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
Zero rows is the defect. A formula value is computed on read and no driver
|
|
1029
|
+
materializes a column for it (`driver-sql` `fieldHasColumn`, driver-turso's
|
|
1030
|
+
"Virtual — no column"), so the `$contains` the engine expands `$search` into has
|
|
1031
|
+
nothing to scan: 0 rows on driver-memory (the property is absent from the stored
|
|
1032
|
+
row) and 0 rows WITH NO ERROR on driver-sql/better-sqlite3. The declaration read
|
|
1033
|
+
as search coverage and delivered none.
|
|
1034
|
+
|
|
1035
|
+
- **`@objectstack/spec` — the deciding face.** The declared branch now filters on
|
|
1036
|
+
existence AND scannability: an entry naming a virtual field is not admitted.
|
|
1037
|
+
New exports `SEARCH_VIRTUAL_TYPES` (exactly `formula`, pinned) and
|
|
1038
|
+
`isVirtualSearchField` — one judgment, so the resolution, the gate and the
|
|
1039
|
+
linter cannot drift about which types have a column. The resolution itself
|
|
1040
|
+
stays non-throwing: it is consulted on every search by internal callers that
|
|
1041
|
+
never pass an ingress, which is why #4254 put the loudness at the ingress.
|
|
1042
|
+
- **`@objectstack/metadata-protocol` — `400 INVALID_FIELD` with its own reason.**
|
|
1043
|
+
Split out before the declared/auto branch, because both of those messages are
|
|
1044
|
+
wrong for it: "outside the declared set" is false when the entry IS in the
|
|
1045
|
+
list, and the auto-default's "declare `searchableFields` to choose the
|
|
1046
|
+
searchable set" would instruct the author to write the declaration being
|
|
1047
|
+
refused. The new message names the field, its type, that the value is computed
|
|
1048
|
+
on read and never stored, and the fix (mirror onto a stored text field).
|
|
1049
|
+
- **`@objectstack/lint` — a build error at authoring time**, on the object's own
|
|
1050
|
+
`searchableFields` as well as a view's narrowing, under the existing
|
|
1051
|
+
`searchable-field-unsearchable` rule (no new rule id). This narrows the
|
|
1052
|
+
canonical surface, which #4830 had deliberately left existence-only.
|
|
1053
|
+
|
|
1054
|
+
The carve-out that made canonical existence-only is deliberately KEPT and pinned
|
|
1055
|
+
by controls in all three packages: the dividing line is STORAGE, not search
|
|
1056
|
+
quality. A `json` or `lookup` column declared in `searchableFields` is still the
|
|
1057
|
+
author's choice and still executed — a `$contains` over the stored JSON text or
|
|
1058
|
+
the stored foreign key. Narrow and rarely useful, but a scan that CAN match, so
|
|
1059
|
+
it is neither a 400 nor a finding. Only "there is no column at all" is refused.
|
|
1060
|
+
|
|
1061
|
+
**Compatibility.** A corpus sweep of this repo plus `objectui` and `cloud` found
|
|
1062
|
+
ZERO authored `searchableFields` naming a formula-typed field, so nothing in the
|
|
1063
|
+
tree changes verdict. For an already-published object that does carry one:
|
|
1064
|
+
loading is unaffected (no schema-parse change — `searchableFields` is still
|
|
1065
|
+
`z.array(z.string())`, this is a resolution and enforcement rule); a plain
|
|
1066
|
+
`?search=` keeps returning the SAME rows, because the dropped entry matched none
|
|
1067
|
+
of them; only a request that NAMES the formula field flips from `200` with no
|
|
1068
|
+
rows to `400 INVALID_FIELD` — including objectui's list search, which echoes the
|
|
1069
|
+
declaration verbatim. An object whose `searchableFields` is ENTIRELY formula
|
|
1070
|
+
entries filters to empty and falls through to the auto-default, exactly as an
|
|
1071
|
+
all-stale declaration has since #4254; the linter reports the declaration rather
|
|
1072
|
+
than leaving that swap silent.
|
|
1073
|
+
|
|
1074
|
+
- 59e9b7c: fix(lint): `searchable-field-unknown` / `searchable-field-unsearchable` prescribe a **stored** mirror, not a formula (#6673)
|
|
1075
|
+
|
|
1076
|
+
Both authoring-time hints for a bad `searchableFields` entry told the author to
|
|
1077
|
+
mirror a related record's value onto a formula field — a fix that can never
|
|
1078
|
+
work:
|
|
1079
|
+
|
|
1080
|
+
- FROM (dotted-path entry, e.g. `project_id.name`): "…expand the relation and
|
|
1081
|
+
search the related object, or copy the value onto **a formula field** here."
|
|
1082
|
+
- TO: "…or copy the value onto **a stored text field** here."
|
|
1083
|
+
|
|
1084
|
+
- FROM (a `lookup`/`master_detail` column outside the allowed set): "…mirror
|
|
1085
|
+
it onto **a text/formula** field here and declare that instead."
|
|
1086
|
+
- TO: "…mirror it onto **a stored text** field here and declare that instead."
|
|
1087
|
+
|
|
1088
|
+
A `formula` field is virtual — no driver materializes a column for it
|
|
1089
|
+
(`packages/objectql/src/engine.ts`, `driver-sql/src/schema-drift.ts`,
|
|
1090
|
+
`driver-turso/src/remote-transport.ts`), so a `$contains` predicate against one
|
|
1091
|
+
has nothing to scan. A CEL formula also only reads the record's own fields
|
|
1092
|
+
(`record.<field>`), so it cannot fetch the related title in the first place.
|
|
1093
|
+
Only the **stored** half of the old prescription ever worked; an author who
|
|
1094
|
+
followed it verbatim got metadata that passed both lint and the `#4254`
|
|
1095
|
+
runtime gate and then just never matched.
|
|
1096
|
+
|
|
1097
|
+
The corpus already prescribes the stored-field mirror everywhere else
|
|
1098
|
+
(`content/docs/data-modeling/schema-design.mdx`, the `objectstack-data` and
|
|
1099
|
+
`objectstack-ui` skills, PR #6670 / #6898) — this brings the tool's own hint
|
|
1100
|
+
text into agreement with it.
|
|
1101
|
+
|
|
1102
|
+
Message text only — no schema, rule id, severity, or runtime behaviour change.
|
|
1103
|
+
|
|
1104
|
+
- 3510e4a: refactor(spec,drivers,lint): one implementation of the filter identity reduction (#5659)
|
|
1105
|
+
|
|
1106
|
+
`{ $and: [] }` matches every row, `{ $or: [] }` matches none, `{}` is a TRUE
|
|
1107
|
+
disjunct that absorbs its `$or`, `{ $not: {} }` is FALSE. That is a ruling
|
|
1108
|
+
(#5322/#5134) pinned for every backend by the four identity cases in
|
|
1109
|
+
`FILTER_LOGIC_CASES` — and it was implemented four times over: `reduceFilterNode`
|
|
1110
|
+
in `driver-sql`, the same function again in `driver-mongodb`, the
|
|
1111
|
+
`every`/`some`/truthiness algebra of `driver-memory`'s matcher, and nearly a
|
|
1112
|
+
fifth hand-written copy inside `@objectstack/lint`, which declined to write one
|
|
1113
|
+
and filed this issue instead.
|
|
1114
|
+
|
|
1115
|
+
**New in `@objectstack/spec` (`@objectstack/spec/data`): `reduceFilterVerdict`**,
|
|
1116
|
+
beside the case table that proves it. It answers `'true' | 'false' | 'clause'`
|
|
1117
|
+
for a filter node and never throws on its own; each backend's own refusals — the
|
|
1118
|
+
undeclared `$`-combinator and the `undefined` comparand in `driver-sql`, the
|
|
1119
|
+
query-level keys and the `$null` comparand in `driver-mongodb` — are passed in as
|
|
1120
|
+
`FilterVerdictHooks` and are invoked from exactly the positions they were invoked
|
|
1121
|
+
from before. `reduceFilterKeyVerdict` answers the same question for one key, which
|
|
1122
|
+
is what both SQL and MongoDB emitters consult while walking a node.
|
|
1123
|
+
|
|
1124
|
+
**No behaviour changes in the three drivers.** The move is mechanical: the shared
|
|
1125
|
+
algebra replaces each private copy, the refusals stay where they were, and the
|
|
1126
|
+
`FILTER_LOGIC_CASES` conformance suites are green on both sides of the change —
|
|
1127
|
+
including the SQL-inheriting `driver-sqlite-wasm` and `driver-turso`.
|
|
1128
|
+
|
|
1129
|
+
**`@objectstack/lint` gains two warnings it was structurally blind to.** The
|
|
1130
|
+
`multi: true` unbounded-bulk-write rule (#5482) asked "does this filter have zero
|
|
1131
|
+
keys", so a `delete_record` bounded by `filter: { $and: [] }` or
|
|
1132
|
+
`filter: { $or: [{}] }` — a whole-object write by the ruling every driver executes
|
|
1133
|
+
— passed silently. It now asks the reduction, and it warns about both while
|
|
1134
|
+
staying quiet on `{ $or: [] }` and `{ $not: {} }`, which match nothing. The
|
|
1135
|
+
message names the shape it saw (`a filter that REDUCES TO TRUE ({"$and":[]})`)
|
|
1136
|
+
rather than calling a non-empty filter "empty".
|
|
1137
|
+
|
|
1138
|
+
If you have a flow declaring a bulk write bounded by one of those two shapes, the
|
|
1139
|
+
lint will now tell you so — the write was already unbounded at run time; only the
|
|
1140
|
+
feedback is new.
|
|
1141
|
+
|
|
1142
|
+
- 8599c21: i18n section headings: read only the declared `label` spelling, never `title`
|
|
1143
|
+
|
|
1144
|
+
`record:details` sections and form-view sections declare exactly one heading key —
|
|
1145
|
+
`label` (`RecordDetailsProps.sections[]` and `FormSectionSchema`; #5611 settled this
|
|
1146
|
+
by declaring `label` and deliberately NOT declaring `title`). Two consumers still
|
|
1147
|
+
read `label ?? title`:
|
|
1148
|
+
|
|
1149
|
+
- `os i18n extract` / `os lint`'s coverage walk (`i18n-extract.ts`) scaffolded
|
|
1150
|
+
`objects.<object>._sections.<name>.label` from a `title`;
|
|
1151
|
+
- the `translation-section-name-missing` lint rule accepted a `title` as the
|
|
1152
|
+
heading it reports on.
|
|
1153
|
+
|
|
1154
|
+
Both now read `label` only. Per Prime Directive #12 the tolerance was the bug: a
|
|
1155
|
+
consumer that reads an undeclared spelling turns it into a second de-facto contract,
|
|
1156
|
+
and here it did so on the loudest possible surface — the extractor would seed a
|
|
1157
|
+
translation bundle key from a spelling the schema rejects, teaching the wrong key to
|
|
1158
|
+
every translator downstream.
|
|
1159
|
+
|
|
1160
|
+
FROM → TO: a section authored as `{ name: 'timeline', title: 'Timeline' }` becomes
|
|
1161
|
+
`{ name: 'timeline', label: 'Timeline' }`. No migration is expected in practice —
|
|
1162
|
+
every `record:details` section in this repo and in `packages/platform-objects`
|
|
1163
|
+
already authors `label` (~12 sections, zero `title`).
|
|
1164
|
+
|
|
1165
|
+
Behaviour change if you do author `title`: the heading is treated as absent. The
|
|
1166
|
+
extractor still emits the section's expected key (it is derived from `name`) but
|
|
1167
|
+
seeds it with the section name instead of your `title` text, and the lint rule no
|
|
1168
|
+
longer reports that section. Rename the key to `label` — which is also what the
|
|
1169
|
+
schema itself will tell you, since `title` is not a declared key there.
|
|
1170
|
+
|
|
1171
|
+
- 1bb679c: fix(lint): `_views` keys for named `listViews`/`formViews` entries are the runtime's, single spelling (#6422)
|
|
1172
|
+
|
|
1173
|
+
`validateTranslationReferences` accepted two spellings for a named view entry —
|
|
1174
|
+
the map key and the entry's inner `name` — while the composer
|
|
1175
|
+
(`expandViewContainerWithDiagnostics`) constructs the runtime identity from the
|
|
1176
|
+
map key alone and ignores `name` entirely. Per the #5164 ruling (canonical =
|
|
1177
|
+
the runtime identity's bare key), the named branches now read their keys from
|
|
1178
|
+
the composer, exactly as the default `list` already does: an inner `name`
|
|
1179
|
+
diverging from its map key stops being a legal bundle key (the runtime never
|
|
1180
|
+
resolves it), and a collision-renamed entry (`formViews.default` beside a
|
|
1181
|
+
default `list` → `default_2`) becomes legal under the renamed key — the one
|
|
1182
|
+
spelling that actually resolves — instead of being reported as an orphan.
|
|
1183
|
+
|
|
1184
|
+
Measured over all 12 ratchet-covered configs in this repo: `os lint` verdicts
|
|
1185
|
+
are byte-identical before/after (`added: 0 / removed: 0`).
|
|
1186
|
+
|
|
1187
|
+
- ea8e849: `visibility-predicate-syntax`: an over-budget predicate is a SIZE fault, not "not valid CEL" (#7217)
|
|
1188
|
+
|
|
1189
|
+
view / page `visibleWhen` 的门禁把两类拒绝合成了一类。`parseCelToAst` 对"不是 CEL"
|
|
1190
|
+
和"是 CEL、但超过平台解析预算"返回同一个 `null`(生产侧刻意如此),于是一条
|
|
1191
|
+
80 项合取的谓词——完全合法的 bare CEL,只是超过 `maxAstNodes` 256——被报成
|
|
1192
|
+
`visibility predicate is not valid CEL`,并附上方言处方("写 `==` 不是 `===`、
|
|
1193
|
+
`&&` 不是 `and` …")。标题是假的,处方在这条源码上根本不可能成功:作者(尤其是
|
|
1194
|
+
照着最后一句话执行的 LLM 作者)会去改一堆本来就没错的运算符,然后带着同一条超预算
|
|
1195
|
+
谓词回来。#7073 / PR #7209 在 ADR-0032 的共享生产者上修的是同一个缺陷,而本门禁
|
|
1196
|
+
按其自身 docblock 刻意不走 `validateExpression`,所以生产侧的修复到不了这里。
|
|
1197
|
+
|
|
1198
|
+
**判定不变**:拒绝的输入集合、严重级别、每条坏谓词一个 finding,全部与修复前一致。
|
|
1199
|
+
红绿边界仍然是规范前端接受什么——`celRefusal` 改问 `parseCelToAstWithReason`,而
|
|
1200
|
+
`parseCelToAst` 本身就是"它把 reason 丢掉"(同一个 env、同一套 limits),所以没有
|
|
1201
|
+
任何一条源码换了颜色。变的只有解释。
|
|
1202
|
+
|
|
1203
|
+
新增第三个 error 级 id **`visibility-predicate-over-budget`**(与
|
|
1204
|
+
`visibility-predicate-syntax` / `visibility-bare-identifier` 并列导出),理由与
|
|
1205
|
+
#6778 / PR #6831 在 RLS 侧把 `rls-predicate-over-budget` 从
|
|
1206
|
+
`rls-predicate-unparseable` 拆出来时相同:后果相同,**修法不同**,而 `--json`
|
|
1207
|
+
消费者与抑制清单都按 id 取值。超预算谓词现在报:
|
|
1208
|
+
|
|
1209
|
+
> visibility predicate is syntactically valid CEL but overruns the `maxAstNodes`
|
|
1210
|
+
> budget (platform limit 256) (Exceeded maxAstNodes (256)) (predicate: …) …
|
|
1211
|
+
>
|
|
1212
|
+
> hint: There is no syntax or dialect error to correct here — this is a SIZE
|
|
1213
|
+
> fault, not a dialect mistake, so re-spelling the predicate will not fix it.
|
|
1214
|
+
> Make it smaller, or move the work off the predicate: (1) collapse a long
|
|
1215
|
+
> `record.f == 'a' || record.f == 'b' || …` chain into a single
|
|
1216
|
+
> `record.f in ['a', 'b', …]` …; (2) precompute the heavy part into a
|
|
1217
|
+
> formula/rollup field on the object and test that one field instead. …
|
|
1218
|
+
|
|
1219
|
+
越界的那条界(`maxAstNodes` / `maxDepth` / `maxListElements` / …)与平台取值来自
|
|
1220
|
+
前端自己的结构化 `overrun`,不是硬编码,也不是二次解析它的散文(#6223);提示里的
|
|
1221
|
+
绑定根随 layer 走(runtime 用 `record`,`*.form.ts` 元数据表单用 `data`),否则处方
|
|
1222
|
+
本身又会是一句照做不了的话。真正的方言/语法错误保持 #6253 的 id、message 与 hint
|
|
1223
|
+
逐字不变——两个方向都有 pin。
|
|
1224
|
+
|
|
1225
|
+
**兼容性**:这是新增 id,不是改名。抑制 `visibility-predicate-syntax` 的配置从此不再
|
|
1226
|
+
抑制超预算这一类——与 #6778 接受的代价相同,而且本来就是抑制错了对象(抑制的是
|
|
1227
|
+
"语法",命中的是"太大")。仓库内除 `packages/lint` 自身与 changelog 外,没有任何
|
|
1228
|
+
配置、文档或示例引用这些 id。
|
|
1229
|
+
|
|
1230
|
+
- Updated dependencies [3d5c090]
|
|
1231
|
+
- Updated dependencies [e5bd768]
|
|
1232
|
+
- Updated dependencies [e027b3e]
|
|
1233
|
+
- Updated dependencies [c2429b0]
|
|
1234
|
+
- Updated dependencies [445a0c2]
|
|
1235
|
+
- Updated dependencies [f6609e6]
|
|
1236
|
+
- Updated dependencies [a70358a]
|
|
1237
|
+
- Updated dependencies [97e7e3c]
|
|
1238
|
+
- Updated dependencies [8828b9e]
|
|
1239
|
+
- Updated dependencies [53068c1]
|
|
1240
|
+
- Updated dependencies [ee58392]
|
|
1241
|
+
- Updated dependencies [f16e54e]
|
|
1242
|
+
- Updated dependencies [06be54e]
|
|
1243
|
+
- Updated dependencies [259459d]
|
|
1244
|
+
- Updated dependencies [3f7f14e]
|
|
1245
|
+
- Updated dependencies [6968885]
|
|
1246
|
+
- Updated dependencies [eaed61f]
|
|
1247
|
+
- Updated dependencies [debe2f6]
|
|
1248
|
+
- Updated dependencies [97b0798]
|
|
1249
|
+
- Updated dependencies [43a7a8d]
|
|
1250
|
+
- Updated dependencies [73f69dc]
|
|
1251
|
+
- Updated dependencies [04c56aa]
|
|
1252
|
+
- Updated dependencies [b3efeb7]
|
|
1253
|
+
- Updated dependencies [ddd075a]
|
|
1254
|
+
- Updated dependencies [88154be]
|
|
1255
|
+
- Updated dependencies [e8dc61e]
|
|
1256
|
+
- Updated dependencies [2f3e793]
|
|
1257
|
+
- Updated dependencies [d8e8d9c]
|
|
1258
|
+
- Updated dependencies [94e749b]
|
|
1259
|
+
- Updated dependencies [ea1d916]
|
|
1260
|
+
- Updated dependencies [ae31a19]
|
|
1261
|
+
- Updated dependencies [b230e5e]
|
|
1262
|
+
- Updated dependencies [5d24f4b]
|
|
1263
|
+
- Updated dependencies [29b94ed]
|
|
1264
|
+
- Updated dependencies [07c68b0]
|
|
1265
|
+
- Updated dependencies [f6cd635]
|
|
1266
|
+
- Updated dependencies [e0f300b]
|
|
1267
|
+
- Updated dependencies [62b6a2f]
|
|
1268
|
+
- Updated dependencies [5b4780b]
|
|
1269
|
+
- Updated dependencies [a933452]
|
|
1270
|
+
- Updated dependencies [8140915]
|
|
1271
|
+
- Updated dependencies [7b48cf9]
|
|
1272
|
+
- Updated dependencies [b5404f4]
|
|
1273
|
+
- Updated dependencies [f764691]
|
|
1274
|
+
- Updated dependencies [e120a5a]
|
|
1275
|
+
- Updated dependencies [e9b5265]
|
|
1276
|
+
- Updated dependencies [e650d67]
|
|
1277
|
+
- Updated dependencies [04476e7]
|
|
1278
|
+
- Updated dependencies [79228cd]
|
|
1279
|
+
- Updated dependencies [b3363e9]
|
|
1280
|
+
- Updated dependencies [2ef1807]
|
|
1281
|
+
- Updated dependencies [d03fe25]
|
|
1282
|
+
- Updated dependencies [2672f85]
|
|
1283
|
+
- Updated dependencies [11066f6]
|
|
1284
|
+
- Updated dependencies [916af17]
|
|
1285
|
+
- Updated dependencies [84c86fb]
|
|
1286
|
+
- Updated dependencies [2a2a9fb]
|
|
1287
|
+
- Updated dependencies [a2e157c]
|
|
1288
|
+
- Updated dependencies [95c4227]
|
|
1289
|
+
- Updated dependencies [2a61116]
|
|
1290
|
+
- Updated dependencies [d4df105]
|
|
1291
|
+
- Updated dependencies [e2798fa]
|
|
1292
|
+
- Updated dependencies [0fd8556]
|
|
1293
|
+
- Updated dependencies [74155c7]
|
|
1294
|
+
- Updated dependencies [6908830]
|
|
1295
|
+
- Updated dependencies [8b06bba]
|
|
1296
|
+
- Updated dependencies [4c54037]
|
|
1297
|
+
- Updated dependencies [0f7157b]
|
|
1298
|
+
- Updated dependencies [d9bef45]
|
|
1299
|
+
- Updated dependencies [f549a0d]
|
|
1300
|
+
- Updated dependencies [82da264]
|
|
1301
|
+
- Updated dependencies [9b9b70f]
|
|
1302
|
+
- Updated dependencies [f5a9bc2]
|
|
1303
|
+
- Updated dependencies [881a3cc]
|
|
1304
|
+
- Updated dependencies [ad6317b]
|
|
1305
|
+
- Updated dependencies [d5e9f6e]
|
|
1306
|
+
- Updated dependencies [8a88885]
|
|
1307
|
+
- Updated dependencies [5f7669e]
|
|
1308
|
+
- Updated dependencies [becbe53]
|
|
1309
|
+
- Updated dependencies [b127c8b]
|
|
1310
|
+
- Updated dependencies [a80302a]
|
|
1311
|
+
- Updated dependencies [474f131]
|
|
1312
|
+
- Updated dependencies [050cd82]
|
|
1313
|
+
- Updated dependencies [4d552af]
|
|
1314
|
+
- Updated dependencies [44d677c]
|
|
1315
|
+
- Updated dependencies [c32944d]
|
|
1316
|
+
- Updated dependencies [1dd780f]
|
|
1317
|
+
- Updated dependencies [cafec0a]
|
|
1318
|
+
- Updated dependencies [c8d6f6e]
|
|
1319
|
+
- Updated dependencies [92a67f2]
|
|
1320
|
+
- Updated dependencies [9136327]
|
|
1321
|
+
- Updated dependencies [bf0ae99]
|
|
1322
|
+
- Updated dependencies [cb3b6cd]
|
|
1323
|
+
- Updated dependencies [73b7234]
|
|
1324
|
+
- Updated dependencies [d2b97c3]
|
|
1325
|
+
- Updated dependencies [59b794f]
|
|
1326
|
+
- Updated dependencies [fc3a36a]
|
|
1327
|
+
- Updated dependencies [69787f0]
|
|
1328
|
+
- Updated dependencies [5d022a1]
|
|
1329
|
+
- Updated dependencies [042b9ee]
|
|
1330
|
+
- Updated dependencies [f549a0d]
|
|
1331
|
+
- Updated dependencies [a36db28]
|
|
1332
|
+
- Updated dependencies [3f8817a]
|
|
1333
|
+
- Updated dependencies [a2443e3]
|
|
1334
|
+
- Updated dependencies [e1554b1]
|
|
1335
|
+
- Updated dependencies [4856789]
|
|
1336
|
+
- Updated dependencies [c3f4916]
|
|
1337
|
+
- Updated dependencies [33e0385]
|
|
1338
|
+
- Updated dependencies [2205363]
|
|
1339
|
+
- Updated dependencies [09fe58d]
|
|
1340
|
+
- Updated dependencies [d0a5ceb]
|
|
1341
|
+
- Updated dependencies [e18a162]
|
|
1342
|
+
- Updated dependencies [d127ff0]
|
|
1343
|
+
- Updated dependencies [9b86cf6]
|
|
1344
|
+
- Updated dependencies [8825a06]
|
|
1345
|
+
- Updated dependencies [5087ac6]
|
|
1346
|
+
- Updated dependencies [6965160]
|
|
1347
|
+
- Updated dependencies [2d1ddf0]
|
|
1348
|
+
- Updated dependencies [354b00f]
|
|
1349
|
+
- Updated dependencies [3de535b]
|
|
1350
|
+
- Updated dependencies [fe2e15a]
|
|
1351
|
+
- Updated dependencies [c6b6bb4]
|
|
1352
|
+
- Updated dependencies [2f59da0]
|
|
1353
|
+
- Updated dependencies [8ad609c]
|
|
1354
|
+
- Updated dependencies [bbee302]
|
|
1355
|
+
- Updated dependencies [08863dd]
|
|
1356
|
+
- Updated dependencies [56664f5]
|
|
1357
|
+
- Updated dependencies [31cbe90]
|
|
1358
|
+
- Updated dependencies [90bbf25]
|
|
1359
|
+
- Updated dependencies [eb91eba]
|
|
1360
|
+
- Updated dependencies [42da73d]
|
|
1361
|
+
- Updated dependencies [643b7c7]
|
|
1362
|
+
- Updated dependencies [1a15893]
|
|
1363
|
+
- Updated dependencies [b70e534]
|
|
1364
|
+
- Updated dependencies [2233a85]
|
|
1365
|
+
- Updated dependencies [62dd69a]
|
|
1366
|
+
- Updated dependencies [e15e679]
|
|
1367
|
+
- Updated dependencies [2ab1257]
|
|
1368
|
+
- Updated dependencies [4cc4fb7]
|
|
1369
|
+
- Updated dependencies [2c26040]
|
|
1370
|
+
- Updated dependencies [f758cec]
|
|
1371
|
+
- Updated dependencies [78f0be8]
|
|
1372
|
+
- Updated dependencies [35f7fb4]
|
|
1373
|
+
- Updated dependencies [a5302c7]
|
|
1374
|
+
- Updated dependencies [7084313]
|
|
1375
|
+
- Updated dependencies [0e043d8]
|
|
1376
|
+
- Updated dependencies [dadd1ad]
|
|
1377
|
+
- Updated dependencies [2f2e63c]
|
|
1378
|
+
- Updated dependencies [486d526]
|
|
1379
|
+
- Updated dependencies [89d7b35]
|
|
1380
|
+
- Updated dependencies [85ec26d]
|
|
1381
|
+
- Updated dependencies [f6476fc]
|
|
1382
|
+
- Updated dependencies [4ac12ef]
|
|
1383
|
+
- Updated dependencies [b88f5e8]
|
|
1384
|
+
- Updated dependencies [42cc219]
|
|
1385
|
+
- Updated dependencies [d7e0b42]
|
|
1386
|
+
- Updated dependencies [3510e4a]
|
|
1387
|
+
- Updated dependencies [aa4b90d]
|
|
1388
|
+
- Updated dependencies [54299ca]
|
|
1389
|
+
- Updated dependencies [dc61def]
|
|
1390
|
+
- Updated dependencies [251e888]
|
|
1391
|
+
- Updated dependencies [183b4c4]
|
|
1392
|
+
- Updated dependencies [2fdb36e]
|
|
1393
|
+
- Updated dependencies [20526f5]
|
|
1394
|
+
- Updated dependencies [c5eef1d]
|
|
1395
|
+
- Updated dependencies [e0f300b]
|
|
1396
|
+
- Updated dependencies [761a0ba]
|
|
1397
|
+
- Updated dependencies [be87153]
|
|
1398
|
+
- Updated dependencies [60f0dd8]
|
|
1399
|
+
- Updated dependencies [a87c5cd]
|
|
1400
|
+
- Updated dependencies [a47f338]
|
|
1401
|
+
- Updated dependencies [2598216]
|
|
1402
|
+
- Updated dependencies [2c7e62d]
|
|
1403
|
+
- Updated dependencies [eb7613c]
|
|
1404
|
+
- Updated dependencies [ecc9110]
|
|
1405
|
+
- Updated dependencies [f7bd4e2]
|
|
1406
|
+
- Updated dependencies [361bd5b]
|
|
1407
|
+
- Updated dependencies [1818998]
|
|
1408
|
+
- Updated dependencies [09ee21c]
|
|
1409
|
+
- Updated dependencies [f549a0d]
|
|
1410
|
+
- Updated dependencies [3fc2e48]
|
|
1411
|
+
- Updated dependencies [e8f435c]
|
|
1412
|
+
- Updated dependencies [41610f6]
|
|
1413
|
+
- @objectstack/spec@17.0.0-rc.6
|
|
1414
|
+
- @objectstack/formula@17.0.0-rc.6
|
|
1415
|
+
- @objectstack/sdui-parser@17.0.0-rc.6
|
|
1416
|
+
|
|
3
1417
|
## 17.0.0-rc.5
|
|
4
1418
|
|
|
5
1419
|
### Patch Changes
|