Repository navigation
Commit 00f72f9
docs(skills): teach the null-guard gate on a record-triggered flow's conditions in the formula skill and the flows page (#29864)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBUyArWn6eHL33NNgvdpkwiW)_
Fixes #29482
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:formula 技能与 flows 文档页补上「空值守卫门禁也判记录触发流的条件」的教学:判哪些槽位(start
`config.condition`、每条边 `condition` 含嵌套区域、每个 `decision` 分支
`expression`)、`count`/`sum` 汇总字段恒有值而 `min`/`max`/`avg` 仍可空、裸字段名即
`record.FIELD`、start 条件里的守卫在下游每条边与分支都算数且要拼成 `record.FIELD != null`
两道门才都收、未守卫时「写入落地、流运行失败」。技能 evals 新增一条被拒的流边。
- 为什么改:PR #29474 已让 `os validate` / `os build` 与运行时发布门拒收这类流条件,技能与文档仍只说
validation rule 与 hook,作者会在无教学的情况下撞上拒收。
- 风险与回滚:仅教学文本与一条 eval,不动任何代码或 schema;回滚即 revert 本 PR。`skills/**` 为 Tier
H,等维护者批准后由席位落地。
- 先看:`skills/objectstack-formula/SKILL.md` 第 115–125 行(发布时拒收段落)与第
399–407 行(流条件例外的 callout);`content/docs/automation/flows.mdx` 的
Expressions in flows 警告框第 5 条。
- 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期
`skills/objectstack-formula/SKILL.md is 5956 tokens (ceiling 5997;
headroom 41)`。
- 需要你做的:批准(Tier H:`skills/**`);之后席位落地。
## What changes
- **`skills/objectstack-formula/SKILL.md`** (491 → 487 lines against
`main` after PR #29439, net −4; 5948 → 5956 tokens of 5997, headroom
41). Two paragraphs teach what
`packages/lint/src/validate-null-guards.ts` (the ledger's flow row) and
`validate-expressions.ts` (`checkFlowNullGuards`) do as merged in PR
#29474:
- the publish-time rejection paragraph (lines 115–125) names the flow
slots the gate judges on a record-triggered flow (start `triggerType:
'record-*'`): start `config.condition`, edge `condition` (nested regions
too), `decision` branch `expression` — beside the validation-rule and
hook predicates it already named — and adds the roll-up nuance to the
nullable definition: a `count` / `sum` summary is always valued (empty
reads `0`), `min` / `max` / `avg` stay nullable;
- the "Flow conditions are the exception to row 1" callout (lines
399–407) carries the bare-name rule (the run spreads the trigger
record's fields, nulls included, to top level wherever no flow variable
or node id holds the name, so a bare `budget` IS `record.budget`; `os
validate` judges either spelling, bare names only while the flow's bound
names are provable; a guard on one guards both), the start-condition
presumption (what the start condition proves non-null holds at every
edge and branch), the two-door spelling (`record.budget != null`: the
runtime publish gate judges the written flow alone and resolves no bare
name) and the outcome (the **run** fails at that condition; the write
that fired it still lands).
- Paid by deleting restatement in the same file, never by re-wrapping:
the `OLD.x` / `ISCHANGED(x)` callout (6 → 3 lines) restated §5's
`previous` table and its ABORTS paragraph; the "Verify your work" opener
dropped its pre-ADR-0032 history clause; and after PR #29439 (#29094)
landed on `main` and the merged file read 6086 tokens (over by 89),
three more: the "Null + string throws" section (it restated pattern 1,
*always coalesce nullable operands* — "CEL throws on null + string"),
the ABORTS paragraph's compile sentence (pattern 3 states it) and its
insert-leg remedy sentence (the `previous` table's insert row and the
`OLD.x` callout state it), and the flow callout's own "prefer
`record.x`" sentence (row 1 of the table above it says the same).
- PR #29439's sparse-action paragraph and its edit to the total-record
paragraph are untouched; the merge of `origin/main` @ `4b71a82b`
auto-merged this file with no conflict.
- **`skills/objectstack-formula/evals/cel-predicates-formulas.json`**
(85 → 95 lines; eval fixture, measured but not ratcheted — 1504 → 1769
tokens): eval 9 asks for the edge condition of a record-change flow on a
nullable `budget`; expected `record.budget != null && record.budget >
200000`, must not contain the `has()`-only trap.
- **`content/docs/automation/flows.mdx`** (3061 → 3073 lines, net +12):
item 5 of the "failure modes to memorize" callout in *Expressions in
flows* — the slots, the nullable definition with the roll-up nuance, the
`!= null` / `== null` / `!isBlank()` guard, why (the run binds every
declared field, `null` when empty; CEL has no `>` for null; the run
fails there while the write lands), the bare-name rule and the
start-guard spelling; the Edges table's `condition` row points at it
(same row, no new line).
- **`.changeset/29482-formula-skill-flow-null-guards.md`**:
`'@objectstack/skills': patch`, both declaration lines.
## Why
PR #29474 (#29026) widened the null-guard gate to record-triggered flow
conditions and left `Skills-docs: follow-up owed`. Until this lands, an
author who reads the skill or the flows page writes `budget > 200000` on
an edge and meets the refusal untaught; the skill said the gate covers
"any validation-rule or hook predicate" and nothing about flows, and the
flows page had no nullable-field rule at all.
## Tests
Every sentence was read off the merged code, then measured against it
(readings repeated on the merged head `d97d2e33`, unchanged; `554bea27`
adds one wording edit to the callout, same line and token count):
- `packages/lint` null-guard suites under
`scripts/pm/os-verify-lock.sh`:
`src/validate-expressions.flow-null-guards.test.ts` +
`src/validate-null-guards.test.ts` — Test Files 2 passed (2), Tests 46
passed (46); lock verdict command-exit 0.
- A probe through `tsx` against
`packages/lint/src/validate-expressions.ts` (`validateStackExpressions`
= the `os validate` door; `runStackExpressionPasses(…, {
runtimeWriteType: 'flow' })` = the runtime publish gate) on a
`record-after-update` flow over `project` (`budget` currency, nullable;
`line_count` a `count` summary; `largest` a `max` summary):
| condition | `os validate` | runtime publish gate |
|:--|:--|:--|
| edge `budget > 200000` (bare) | refused at edge `e2` | not judged
(bare name unresolvable there) |
| edge `record.budget > 200000` | refused | refused |
| edge `has(record.budget) && record.budget > 200000` | refused |
refused |
| edge `record.budget != null && record.budget > 200000` (eval 9's
answer) | accepted | accepted |
| start `budget != null`, edge `record.budget > 200000` | accepted |
refused |
| start `record.budget != null`, same edge | accepted | accepted |
| edge `line_count > 1` (`count` roll-up) | accepted | accepted |
| edge `largest > 1` (`max` roll-up) | refused | not judged (bare) |
The second commit (`487ad8da`) is what that table changed: the three
places that said "the gate judges a bare name" / "refused the same way"
now say `os validate`, since the runtime gate judges no bare name.
- No ablation: no tested behaviour changed (docs, a skill and an eval
fixture).
## Acceptance notes
- Premise measured on the branch base (`origin/main` @ `c76c0e04`):
`skills/objectstack-formula/SKILL.md` line 113 opened "**This is a
publish-time rejection, not advice.**", the file had no `record_change`
/ `record-triggered` mention (0 hits; "record-change flow trigger"
appears only in §5's bulk-write table, which teaches `previous`, not the
gate), and `content/docs/automation/flows.mdx` had no `nullable` (0
hits). The order's premise holds.
- The order's line budgets hold: SKILL.md net +6 against the base (473 →
479) and net −4 against `main` after the token payback (491 → 487);
flows.mdx net +12 (3061 → 3073). The evals file got the one eval the
card asks for (+10 lines); the order set no numeric budget for it and
the token ratchet measures but does not ratchet eval fixtures.
- Token reading for the Tier H review: 5948 (`main`) → 5956 (ceiling
5997, headroom 41). Every token paid is a deletion in the same file,
listed above; no ceiling row moves.
- The Basic Structure example (`order_amount > 10000`) is left as it is:
that flow has no record trigger and `order_amount` is a declared
variable, so the gate does not judge it — the rule is taught where edge
and decision conditions are taught (the *Expressions in flows* callout
and the Edges table).
- The skill's "bare names only while the flow's bound names are
provable" is `flowCelRootScope`'s own criterion (the bound set is
unprovable with a `script`, `subflow`, `map`, `wait`, `connector_action`
or plugin node, an entrance that hands a record not in hand, or a
contract-less screen); the full list stays in the code.
- The previous-operand clause (`previous != null && previous.x != null`
on a flow's create leg) is not restated: §5 and the `OLD.x` callout
already teach it for every surface that binds `previous`.
- Out of scope, noted, not filed: none.
## Verification
Head `554bea27`: the two commits on `c76c0e04`, a merge of `origin/main`
@ `4b71a82b`, one payback commit, and one wording commit from the
at-tier review (record 6599648862). Readings taken on `d97d2e33` unless
marked; the wording commit changed one blockquote line and no reading.
<details><summary>Package readings</summary>
- `pnpm --filter '@objectstack/client-react...' --filter
'@objectstack/lint...' build --concurrency=2` under the lock after the
merge: VERDICT command-exit 0, held 484s.
- Lint null-guard suites (above): 2 files, 46/46, lock verdict 0, on the
merged head.
- `pnpm check:pm-skill-ratchet` :: exit 0 (the `.claude/**` line
ratchet; this diff touches none of its files — run because the order
asks). `pnpm check:pm-skill-id-lint` :: exit 0.
</details>
<details><summary>Derived gates — `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack`, 68 commands, run
serially, `CMD :: exit N` recorded, reconciled with `--ran`</summary>
- On head `487ad8da` (before the merge): `--ran` reconciliation `68
derived famil(ies) accounted for — 66 run, 2 NOT-MEASURED (2 DERIVED
from a recorded exit 3)`; change set 4 paths, 70 changed lines (+54 /
−16), under the 3000 human-merge threshold. 66 run, every one `:: exit
0`, among them `check-changeset-no-major --base origin/main`,
`check-empty-changeset`, `check-adr-0087-registration`,
`check-skills-token-ratchet` (+ self-test), `check-doc-route-spelling`,
`check-docs-section-name`, `check-section-landing-index`,
`@objectstack/lint check:doc-formula-expressions`,
`check:doc-security-posture`, `@objectstack/spec check:docs`,
`check:empty-state`, `check:liveness`, `check:skill-docs`,
`check:skill-examples`, `check:strictness-ledger`, `check:variant-docs`,
`check:yaml-examples`, `check:doc-anchors`, `check:doc-authoring`,
`check:doc-frontmatter`, `check:docs-single-h1`,
`check:docs-spec-enumerations`, `check:docs-transcript-drift`,
`check:docs-redirects`, `check:nul-bytes`, `check:pm-governed-merges`,
`check:pm-skill-id-lint`, `check:published-files`,
`check:published-readme-links`, `check:role-word`,
`check:skill-compatibility`, `check:skill-frame-sync`,
`check:skill-identifier-liveness`, `check:closed-ceremony-text`,
`check:corpus-claim-drift`, `check:cross-package-test-inputs`,
`check:merge-driver`, `check:gitlink-declared`,
`check:vendor-version-stamps`, `check:watch-hint-literal`, and the
`--self-test` rows of the ci / release scripts.
- On the merged head `d97d2e33` (and again on `554bea27` for
`check-skills-token-ratchet` + self-test, `check:pm-skill-id-lint`,
`@objectstack/spec check:skill-docs`, `check:skill-frame-sync`,
`check:skill-identifier-liveness`, all `:: exit 0`), the families the
skill and docs edits read, rerun: `check-skills-token-ratchet` (+
self-test) 5956 / 5997, `check:pm-skill-id-lint`,
`check:pm-skill-ratchet`, `check:skill-compatibility`,
`check:skill-frame-sync`, `check:skill-identifier-liveness`,
`check:doc-authoring`, `check:nul-bytes`, `check:role-word`,
`check-changeset-no-major --base origin/main`, `check-empty-changeset
--base origin/main`, `@objectstack/spec check:skill-docs`,
`@objectstack/spec check:skill-examples` (283 prose examples type-check
across 3 surfaces, after the post-merge closure build),
`@objectstack/lint check:doc-formula-expressions` — all `:: exit 0`.
- NOT MEASURED (2): `pnpm check:dual-build-cjs-loads` and `pnpm
check:lean-entry-closure` — both exit 3, PREREQUISITE NOT MET: they load
every package's built `dist/`, which needs a full repo build this
docs-and-skill diff does not warrant on the shared box (no package
`src/` moves); CI's `Lint & Repo Gates` run is the reading for those
two.
</details>
## Human Minimum
Each item is one rule from
[CONTRIBUTING.md › Human
Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum),
which links the AGENTS.md text that states it.
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(no schema touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` is CI's run (repo-wide scan, not
run locally).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (not touched)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the maintainer digest
above is Chinese by AGENTS.md's communication rule).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzfSZ55f7xwAfpfkPgY)_
<!-- signed: domain:skills#3 · os-support-ai ·
session_01WGufzhd2Ad9ANGTyAMW7pH -->
---------
Co-authored-by: os-support-ai <support@objectstack.ai>
Co-authored-by: Claude <noreply@anthropic.com>1 parent b4a9d04 commit 00f72f9
4 files changed
Lines changed: 52 additions & 24 deletions
File tree
- .changeset
- content/docs/automation
- skills/objectstack-formula
- evals
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1810 | 1810 | | |
1811 | 1811 | | |
1812 | 1812 | | |
1813 | | - | |
| 1813 | + | |
1814 | 1814 | | |
1815 | 1815 | | |
1816 | 1816 | | |
| |||
2443 | 2443 | | |
2444 | 2444 | | |
2445 | 2445 | | |
| 2446 | + | |
| 2447 | + | |
| 2448 | + | |
| 2449 | + | |
| 2450 | + | |
| 2451 | + | |
| 2452 | + | |
| 2453 | + | |
| 2454 | + | |
| 2455 | + | |
| 2456 | + | |
| 2457 | + | |
2446 | 2458 | | |
2447 | 2459 | | |
2448 | 2460 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
117 | | - | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
118 | 120 | | |
119 | | - | |
| 121 | + | |
| 122 | + | |
120 | 123 | | |
121 | 124 | | |
122 | 125 | | |
| |||
137 | 140 | | |
138 | 141 | | |
139 | 142 | | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | 143 | | |
147 | 144 | | |
148 | 145 | | |
| |||
339 | 336 | | |
340 | 337 | | |
341 | 338 | | |
342 | | - | |
343 | | - | |
344 | | - | |
| 339 | + | |
345 | 340 | | |
346 | 341 | | |
347 | 342 | | |
| |||
397 | 392 | | |
398 | 393 | | |
399 | 394 | | |
400 | | - | |
401 | | - | |
402 | | - | |
403 | | - | |
404 | | - | |
405 | | - | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
406 | 398 | | |
407 | | - | |
408 | | - | |
409 | | - | |
410 | | - | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
411 | 408 | | |
412 | 409 | | |
413 | 410 | | |
| |||
471 | 468 | | |
472 | 469 | | |
473 | 470 | | |
474 | | - | |
475 | | - | |
| 471 | + | |
476 | 472 | | |
477 | 473 | | |
478 | 474 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
83 | 93 | | |
84 | 94 | | |
85 | 95 | | |
0 commit comments