Skip to content

Commit 641363a

Browse files
fix(docs,lint): 两处裸引用公式样例改回 canonical,并补上公式样例的 CEL 语义门 (#5116) (#5140)
* fix(docs,lint): canonical `record.` prefixes in two formula examples + a CEL semantic gate (#5116) #5026 activated the field-formula check in validate-expressions.ts and its real-metadata sweep found two doc examples teaching the bare-reference form: content/docs/data-modeling/fields.mdx:230 'quantity * price * (1 - discount / 100)' content/blog/context-window-is-the-constraint.mdx:108 cel`amount * probability` A bare reference in a record-scoped CEL expression does not throw — it resolves to nothing and the expression silently evaluates to null. Both are corrected to the canonical `record.` form, verified by loading each into a minimal spec-valid stack and running the activated validateStackExpressions (RED before, GREEN after). packages/spec/src/data/field.test.ts:363 demonstrated the same wrong spelling and is corrected too; its assertion is unchanged. Adds `@objectstack/lint`'s check:doc-formula-expressions, the semantic gate that was missing: check:doc-authoring judges literal SHAPE and check:skill-examples runs tsc, so a formula that compiles and is semantically wrong passed both (`expression` is typed `string`). The verdict is validateExpression imported from @objectstack/formula — the same call `os build` makes, not a lookalike. The discriminator is the design: `expression:` carries three unrelated contracts in this corpus (record-scoped CEL, flow-flattened predicate where a bare ref is CORRECT, and a cron string), so sites are opted in only by parsed structure — `Field.*({ expression })` or `type: 'formula'` beside `expression`. A block that looks like it carries one but cannot be extracted is a hard error, not a skip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ * fix(lint): write the dedup key's control chars as escapes, not raw bytes (#5116) `check:nul-bytes` (#4890) caught a raw NUL at packages/lint/scripts/check-doc-formula-expressions.mjs:258 — offset 11513, well outside git's 8000-byte binary sniff, which is exactly the blind spot that gate exists for. A scan for every control byte found a second one beside it (0x01, which the NUL gate does not even look for), both in the extraction dedup key. Replaced by script with their `\u….` escape sequences — byte-equivalent at runtime, so the verdict is unchanged: the gate's self-test still passes 11/11 and the corpus scan still reports the same 22 clean examples across 375 files / 1408 TS blocks. Added a comment saying both control chars are deliberate and must stay escaped, since a raw one is invisible in review and a literal NUL makes grep/ripgrep treat the whole file as binary and silently return zero matches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 81e2744 commit 641363a

7 files changed

Lines changed: 557 additions & 4 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(docs,lint): 修正两处裸引用公式样例,并给公式样例补一道 CEL 语义门 (#5116)
6+
7+
#5026 把字段公式校验从 `f.formula`(spec 按名拒绝的别名)收敛到声明的
8+
`f.expression`,**激活了一条从未跑过的检查**。它的真实元数据扫描顺带发现文档和博客
9+
里有两处公式样例写的是**裸引用**:
10+
11+
- `content/docs/data-modeling/fields.mdx``'quantity * price * (1 - discount / 100)'`
12+
- `content/blog/context-window-is-the-constraint.mdx```cel`amount * probability` ``
13+
14+
裸引用在 CEL 里不报错,而是**静默求值为 null**:公式表达式把记录绑定在 `record`
15+
命名空间下,顶层的 `quantity` 什么也解析不到。照这两行写出来的元数据,在 #5026
16+
之后会被 `os build` / `os validate` 判红 —— 文档教的写法和平台的门直接矛盾。两处
17+
都已改成 canonical 的 `record.` 前缀形式。
18+
19+
**新增 `@objectstack/lint``check:doc-formula-expressions`**,堵住让这两条长期
20+
存活的那个洞。`check:doc-authoring` 看的是字面量的**形状**,`check:skill-examples`
21+
对标记块跑 `tsc --noEmit` —— 两者之间,"能编译但 CEL 写错"的样例没有任何门:
22+
`expression` 的类型就是 `string`,`'quantity * price'`
23+
`'record.quantity * record.price'` 编译得一样好,而只有后者能用。
24+
25+
判决直接 import `@objectstack/formula``validateExpression` —— 和 `os build`
26+
走的是同一个调用,不是仿制品。于是文档是被**规则本身**把关,而不是被规则的一种方言
27+
把关(Prime Directive #12)。
28+
29+
门的难点不在判决而在**判据**:同一个 `expression:` 键在语料里承载至少三种互不相干的
30+
契约 —— 记录作用域的 CEL 公式、flow 的扁平作用域谓词(那里裸引用是**对的**)、以及
31+
`schedule` 下压根不是 CEL 的 cron 串。按键名匹配会把后两类全部误判为红。所以它只认
32+
**解析后的结构**,且只认两种不可能有歧义的形状:`Field.*({ expression })`,以及
33+
`type: 'formula'``expression` 并列的对象字面量。无法提取的块**报错而不是跳过**
34+
("absence must be loud")。
35+
36+
覆盖面是明说的,不含糊:只看 TS/TSX 代码块、只看能静态取出的表达式源、不做字段存在性
37+
校验(文档片段没有对象声明)、flow / action / validation 谓词**刻意不在范围内**

.github/workflows/lint.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,30 @@ jobs:
588588
- name: Check skills TypeScript examples compile
589589
run: pnpm --filter @objectstack/spec run check:skill-examples
590590

591+
# The SEMANTIC half of the same surface (#5116). The gate above proves a
592+
# formula example COMPILES; it cannot prove the CEL is right, because
593+
# `expression` is typed `string` — `'quantity * price'` type-checks exactly
594+
# as well as `'record.quantity * record.price'`, and only the second one
595+
# works. A bare reference does not throw at runtime either: it resolves to
596+
# nothing and the expression silently evaluates to null. That is how two doc
597+
# examples stayed wrong until #5026's activation sweep found them — one of
598+
# them in content/blog/, which check:skill-examples does not scan at all.
599+
#
600+
# The verdict is `validateExpression` imported from @objectstack/formula —
601+
# the same call `os build` / `os validate` makes — never a lookalike, so the
602+
# docs are gated by the rule rather than by a dialect of it. Hence its home
603+
# in packages/lint (which owns that verdict) and its place in this
604+
# post-build job: it reads the built formula package like its neighbours.
605+
#
606+
# Deliberately narrow: a site is opted in only by parsed structure that can
607+
# mean nothing else (`Field.*({ expression })`, or `type: 'formula'` beside
608+
# `expression`), because the corpus spells three unrelated contracts with
609+
# the one key `expression:` — a cron string, and a flow-scoped predicate
610+
# where a bare reference is CORRECT, are the other two. Self-test first,
611+
# both directions.
612+
- name: Check docs formula examples are valid CEL
613+
run: pnpm --filter @objectstack/lint run check:doc-formula-expressions
614+
591615
# Same anti-drift class as the gates above, for the generated translation
592616
# bundles in packages/platform-objects/src/apps/translations/. Nothing
593617
# regenerated them either, so they rotted three ways at once (#3670):

content/blog/context-window-is-the-constraint.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export const Opportunity = ObjectSchema.create({
105105
probability: Field.percent({ label: 'Probability', defaultValue: 0.5 }),
106106
expected_revenue: Field.formula({
107107
label: 'Expected Revenue',
108-
expression: cel`amount * probability`,
108+
expression: cel`record.amount * record.probability`,
109109
}),
110110
},
111111
});

content/docs/data-modeling/fields.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ profile_image: Field.avatar({ label: 'Profile Image' }),
227227
// Formula
228228
total: Field.formula({
229229
label: 'Total',
230-
expression: 'quantity * price * (1 - discount / 100)',
230+
expression: 'record.quantity * record.price * (1 - record.discount / 100)',
231231
}),
232232

233233
// Summary (roll-up)

packages/lint/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"build": "tsup",
2323
"dev": "tsc -w",
2424
"test": "vitest run",
25-
"typecheck": "tsc --noEmit"
25+
"typecheck": "tsc --noEmit",
26+
"check:doc-formula-expressions": "node scripts/check-doc-formula-expressions.mjs --self-test && node scripts/check-doc-formula-expressions.mjs"
2627
},
2728
"dependencies": {
2829
"@marcbachmann/cel-js": "^8.0.0",

0 commit comments

Comments
 (0)