Skip to content

fix(docs,lint): 两处裸引用公式样例改回 canonical,并补上公式样例的 CEL 语义门 (#5116) - #5140

Merged
xuyushun441-sys merged 3 commits into
mainfrom
claude/issue-5116-formula-example-drift
Aug 4, 2026
Merged

fix(docs,lint): 两处裸引用公式样例改回 canonical,并补上公式样例的 CEL 语义门 (#5116)#5140
xuyushun441-sys merged 3 commits into
mainfrom
claude/issue-5116-formula-example-drift

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #5116

分两半:① 修正点名的两处裸引用样例(必做);② 评估并落地"公式样例过 CEL 语义校验"的门。

依赖说明:#5026 的 PR #5118 尚未 merge,mainvalidate-expressions.ts 仍读 f.formula(恒 undefined)。所以下面所有"实测判红/判绿"都是在激活后的代码上跑的 —— 我另开了一个 detached worktree checkout origin/claude/issue-5026-field-formula-activate 做验证。本 PR 本身不依赖 #5118 先落地:样例改成 canonical 在两种状态下都正确,新门用的是 @objectstack/formulavalidateExpression(它一直是激活的),与 #5118 无耦合。

一、样例修正(实测,不是目测)

把每条样例装进最小 spec 合法 stack(先 ObjectStackSchema.safeParse,再喂 validateStackExpressions,与 compile 路径完全一致),改前改后各跑一次:

位置 改前 改后 改前 改后
content/docs/data-modeling/fields.mdx:230 'quantity * price * (1 - discount / 100)' 'record.quantity * record.price * (1 - record.discount / 100)' RED GREEN
content/blog/context-window-is-the-constraint.mdx:108 cel`amount * probability` cel`record.amount * record.probability` RED GREEN
packages/spec/src/data/field.test.ts:363 'first_name + " " + last_name' 'record.first_name + " " + record.last_name' RED GREEN

改前的实测输出(六条断言全部执行):

[BEFORE] content/docs/data-modeling/fields.mdx:230
  issues: 1
    - [error] object 'order_line' · field 'total' expression
      bare reference `quantity` — a formula/validation expression binds the record as
      the `record` namespace, not at top level, so `quantity` resolves to nothing and
      the expression silently evaluates to null. Write `record.quantity`.

[AFTER ] content/docs/data-modeling/fields.mdx:230
  issues: []

没有加 null 保护,因为实测不需要:字段公式是 value 角色、天然可空,null-guard 那条判决在 validate-null-guards.ts 的 surface ledger 里对 Field.formula刻意排除的,改后三条的 issue 列表都是空数组。加保护会偏离议题的处方,也偏离同页邻居 —— formulas.mdx:447record.amount * (record.probability / 100) 正是同一形状且判绿。field.test.ts 的断言(FieldSchema.parse 不抛)未动。

blog 只改了表达式一行,没有重写文章。

二、语义门:评估结论是"便宜",所以落了

现状的洞

check:doc-authoring 看字面量的形状(有没有包在 defineX 工厂里),check:skill-examplesos:check 标记块跑 tsc --noEmit。两者之间,"能编译但 CEL 写错"没有任何门 —— expression 的类型就是 string,'quantity * price''record.quantity * record.price' 编译得一模一样好,而只有后者能用。这正是这两条能长期存活的原因。

补一句更要命的:两处缺陷都不在标记块里,而 content/blog/ 根本不在 check:skill-examples 的 SOURCE_ROOTS 里(它只扫 skills/content/docs/)。所以哪怕把门做在标记块基上做到完美,这两条一条也抓不到。这是我把门做成独立扫描、并把 content(而非 content/docs)作为 root 的直接理由。

为什么便宜:判决是免费的

关键测量:bare-reference 判决不需要任何对象上下文 —— 它是作用域的性质,不是字段表的性质。文档片段没有对象声明,但这不妨碍判定:

'quantity * price * (1 - discount / 100)'   →  1 error (bare reference `quantity`)
'record.quantity * record.price * …'        →  0
'record.revenue > 0 ? (…) * 100 : 0'        →  0
'daysBetween(today(), record.close_date)'   →  0
'joinNonEmpty([record.salutation, …], " ")' →  0

所以判决直接 import @objectstack/formulavalidateExpression('value', src, { scope: 'record' }) —— os build / os validate / agent 的 validate_expression 工具是同一个调用。这条是 PD #12:自己重写一遍"看起来像不像裸引用"会对同一个契约产生第二种意见,文档就变成被规则的方言把关,而不是被规则本身把关。

真正贵的是判据 —— 这是全部设计所在

语料里同一个 expression: 键承载至少三种互不相干的契约:

位置 契约 amount * 2
Field.formula({ expression }) 记录作用域 CEL 错的
decision 节点 config.conditions[].expression flow 扁平作用域 对的
schedule: { type: 'cron', expression: '0 9 * * *' } cron 串,压根不是 CEL 不适用

实测:40 处 expression:7 处是 flow 作用域、4 处是 cron。按键名匹配的门会把这 11 处全部自信地判红 —— 实测确认 'order_amount > 10000''0 9 * * *' 喂给记录作用域判决都出 error。一条判据虚的门比没有门更坏,因为它教人加 ignore。

所以门只认解析后的结构(TypeScript parser,不是正则),且只认两种不可能有歧义的形状:

  • A Field.*({ … expression … }) —— 字段工厂调用;cron 在 schedule 下,flow 谓词在节点 config 下,都进不来。
  • B type: 'formula'expression 并列的对象字面量 —— 同一个字段的非工厂拼法。

实测:在真实语料上收全了 22 条真公式样例,一条 flow/cron 都没误收

一个实现细节值得记:文档片段常常是裸对象字面量({ type: 'formula', expression: '…' }),而 TS 在语句位置把开头的 { 解析成块语句,树里根本没有对象字面量 —— 天然假阴性,而且是静默的。恢复办法是结构性的:正常解析后,把每个顶层块语句用括号包起来重解析。这也覆盖了"先 import 再字面量"的常见文档形状(skills/objectstack-data/rules/field-types.md:296 就是,门的 loudness 检查把它抓出来了,我才发现最初那版 starts-with-{ 的启发式不够)。

覆盖面是明说的,不含糊

声称的部分,全部写进脚本头注释:只看 TS/TSX 代码块(YAML 样例不解析)、只看能静态取出的源(插值模板报告为不可判定而非猜)、不做字段存在性校验(片段没有对象声明)、flow / action / validation 谓词刻意不在范围内(它们的作用域取决于片段不携带的外层结构,猜就是上面那种假红)。

"看起来带公式但提取不出来"的块是硬错误,不是跳过 —— absence must be loud。

双向证明

常驻 self-test,11 例,全部执行(坏例用的是 #5116 两处缺陷的逐字原文,所以门一旦不再抓得住它本来为之而建的缺陷,self-test 就红):

✓ RED — the fields.mdx defect (#5116), verbatim
✓ RED — the blog defect (#5116), verbatim (tagged template)
✓ RED — the raw `type: 'formula'` spelling, bare refs
✓ RED — a syntactically broken formula
✓ GREEN — the canonical fix (the gate discriminates, it is not just on)
✓ GREEN — helper functions and ternaries are ordinary CEL
✓ GREEN — a multi-line template formula
✓ NOT ADMITTED — a cron schedule spells `expression` too, and is not CEL
✓ NOT ADMITTED — a decision branch is FLATTENED scope, where a bare ref is correct
✓ NOT ADMITTED — a validation rule `condition` is a different slot entirely
✓ LOUD — a formula block whose source is interpolated is reported, not skipped

外加真实语料上的改前/改后对照(不是 fixture,是把两个文件 git stash 回改前状态跑的):

改前 —— 门自己把这两条抓出来,定位精确到行:

✗ check:doc-formula-expressions — 2 formula example(s) in the docs/skills
  corpus would be REJECTED by `os build` / `os validate`:

    content/blog/context-window-is-the-constraint.mdx:108  [Field.formula(…)]
      source: "amount * probability"
      bare reference `amount` — … Write `record.amount`.

    content/docs/data-modeling/fields.mdx:230  [Field.formula(…)]
      source: "quantity * price * (1 - discount / 100)"
      bare reference `quantity` — … Write `record.quantity`.

改后:

✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across
  375 files / 1408 TS blocks judged clean by @objectstack/formula.

三、接线(显式报告)

  • packages/lint/package.json — 新增 check:doc-formula-expressions script。放 packages/lint 而不是根 scripts/,理由有二:门需要 validateExpression,而根 script 目前没有任何一个 import workspace 包(ESLint job 只 pnpm install、不 build);packages/lint 本来就是拥有这条判决的包,依赖方向自然,且不动根 package.json / lockfile。files 白名单是 ["dist","README.md","CHANGELOG.md"],scripts/ 不会发布(check:published-files 已跑绿)。
  • .github/workflows/lint.yml — 在 TypeScript Type Check job 里、紧挨 check:skill-examples 加一步。必须在 build 之后(读构建产物的 formula 包),已确认顺序:Build workspace packages(step 20)→ 本步(step 28)。
  • 未触碰 packages/spec/package.json,所以 check:generated 的 ledger 对账不受影响(它只读 spec 的 package.json,已跑绿确认)。
  • 未碰 content/docs/releases/

验证

pnpm --filter @objectstack/lint run check:doc-formula-expressions
    → self-test 11/11 passed;语料 22 例 / 375 files / 1408 TS blocks 全绿
pnpm --filter @objectstack/lint test        → 57 files / 1156 passed (4 skipped)
pnpm --filter @objectstack/lint typecheck   → tsc --noEmit, clean
pnpm --filter @objectstack/spec test        → 302 files / 7683 passed
pnpm --filter @objectstack/spec typecheck   → tsc --noEmit, clean
eslint packages/lint/scripts/check-doc-formula-expressions.mjs → exit 0
pnpm check:doc-authoring                    → 362 files clean
pnpm check:docs-audit-scope                 → 178 docs in sync
pnpm check:published-files                  → 69 publishable packages clean
pnpm --filter @objectstack/spec check:generated --reconcile-only → 17 check: + 10 gen: 全部归类

changeset:@objectstack/lint patch。


Generated by Claude Code

…+ 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
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 6:42am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file protocol:data tests tooling size/l labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits August 4, 2026 06:34
…tes (#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:data size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

文档/博客里的公式样例写裸引用 —— #5026 激活字段公式校验后,照抄这两处的元数据会被 os build 判红

2 participants