Repository navigation
docs(skills): teach the null-guard gate on a record-triggered flow's conditions in the formula skill and the flows page - #29864
Conversation
…rd gate on a record-triggered flow's conditions The publish-time null-guard gate judges a record-triggered flow's start `config.condition`, every edge `condition` (nested regions too) and every `decision` branch `expression`. The formula skill now teaches which slots are judged, the `count` / `sum` roll-up exception, the bare-name rule, the start-condition guard spelled `record.<field> != null` so both doors accept it, and the run-fails outcome; one eval exercises a refused flow edge. The flows page carries the same rule where edge and decision conditions are taught. Paid in the same file: the `previous` callout restating §5 and the pre-ADR-0032 history clause. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfSZ55f7xwAfpfkPgY
…tion A bare `budget` in a record-triggered flow's condition is judged by `os validate` (where the flow's bound names are provable); the runtime publish gate judges the written flow alone and resolves no bare name. The skill callout, the flows page item and eval #9 now say `os validate` where they said "the gate" or "refused the same way", measured against the lint source at both doors. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfSZ55f7xwAfpfkPgY
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfSZ55f7xwAfpfkPgY
…arse-action paragraph landed The merged file read 6086 tokens against the 5997 ceiling. Paid by deletion in the same file: the flow-condition callout drops its restated "prefer record.x" sentence and the two-door clause now says only that the runtime publish gate resolves no bare name; the "Null + string throws" section restated pattern 1 (always coalesce; CEL throws on null + string); the ABORTS paragraph restated pattern 3's compile sentence and the previous-table's insert-leg remedy. Reads 5956 of 5997 after. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfSZ55f7xwAfpfkPgY
|
domain:skills#3 · os-support-ai · session_01WGufzhd2Ad9ANGTyAMW7pH Contract reviewServed-tier: Inputs read: card #29482 (body and all 6 comments, triage 6581992627 / 6584871430, claim 6594625338, in-flight 6597533772, os-dev-report 6599133588), PR #29864 body and 4-file list, ① Derived judgments
② Semver level
③ Boundary flagsFrom os-dev-report 6599133588 (head
④ Design judgment
Would an assistant following the new text write a flow that passes both doors? Yes: it writes
Payback deletions — teaching lost? Checked each against the rest of the file and all 9 evals:
Findings
Implemented-by: VERDICT: PASS |
…exactly The flow-condition callout said bare names are judged "only while every node's bindings are declared"; the scope also stands down for an entrance that hands a record not in hand and for a contract-less screen, so it now says "only while the flow's bound names are provable". Same line count, same token reading. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfSZ55f7xwAfpfkPgY
|
domain:skills#3 · os-support-ai · session_01WGufzhd2Ad9ANGTyAMW7pH Contract reviewServed-tier: Round 1 of the review on this PR. The previous record (head ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe delta, against the code on Everything else in the two Findings
Implemented-by: VERDICT: PASS |
|
domain:skills#3 · os-support-ai · session_01WGufzhd2Ad9ANGTyAMW7pH 维护者速读
席位意见:教学内容与 lint 代码逐句对过,两轮复审 PASS,同意落地。 席位核对 — ACCEPT · domain:skills#3 · head
|
Requested by Yibo Wang · project thread
Fixes #29482
Clause-②: no
Metadata-protocol: none
维护者速读
config.condition、每条边condition含嵌套区域、每个decision分支expression)、count/sum汇总字段恒有值而min/max/avg仍可空、裸字段名即record.FIELD、start 条件里的守卫在下游每条边与分支都算数且要拼成record.FIELD != null两道门才都收、未守卫时「写入落地、流运行失败」。技能 evals 新增一条被拒的流边。os validate/os build与运行时发布门拒收这类流条件,技能与文档仍只说 validation rule 与 hook,作者会在无教学的情况下撞上拒收。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)。skills/**);之后席位落地。What changes
skills/objectstack-formula/SKILL.md(491 → 487 lines againstmainafter PR docs(skills): the formula skill names action visible / disabled as the sparse, client-side exception to the total-record rule #29439, net −4; 5948 → 5956 tokens of 5997, headroom 41). Two paragraphs teach whatpackages/lint/src/validate-null-guards.ts(the ledger's flow row) andvalidate-expressions.ts(checkFlowNullGuards) do as merged in PR feat(lint): the publish-time null-guard gate judges a record-triggered flow's start, edge and decision-branch conditions #29474:triggerType: 'record-*'): startconfig.condition, edgecondition(nested regions too),decisionbranchexpression— beside the validation-rule and hook predicates it already named — and adds the roll-up nuance to the nullable definition: acount/sumsummary is always valued (empty reads0),min/max/avgstay nullable;budgetISrecord.budget;os validatejudges 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).OLD.x/ISCHANGED(x)callout (6 → 3 lines) restated §5'sprevioustable and its ABORTS paragraph; the "Verify your work" opener dropped its pre-ADR-0032 history clause; and after PR docs(skills): the formula skill names action visible / disabled as the sparse, client-side exception to the total-record rule #29439 (skills(objectstack-formula): "every predicate reads a record total over declared fields" contradicts the canonical sparse action-binding rule (declared-fields.ts, #4953), so agents drop the has() guard a list-row action needs #29094) landed onmainand 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 (theprevioustable's insert row and theOLD.xcallout state it), and the flow callout's own "preferrecord.x" sentence (row 1 of the table above it says the same).origin/main@4b71a82bauto-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 nullablebudget; expectedrecord.budget != null && record.budget > 200000, must not contain thehas()-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,nullwhen 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'sconditionrow 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 writesbudget > 200000on 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;554bea27adds one wording edit to the callout, same line and token count):packages/lintnull-guard suites underscripts/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
tsxagainstpackages/lint/src/validate-expressions.ts(validateStackExpressions= theos validatedoor;runStackExpressionPasses(…, { runtimeWriteType: 'flow' })= the runtime publish gate) on arecord-after-updateflow overproject(budgetcurrency, nullable;line_countacountsummary;largestamaxsummary):os validatebudget > 200000(bare)e2record.budget > 200000has(record.budget) && record.budget > 200000record.budget != null && record.budget > 200000(eval 9's answer)budget != null, edgerecord.budget > 200000record.budget != null, same edgeline_count > 1(countroll-up)largest > 1(maxroll-up)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 sayos validate, since the runtime gate judges no bare name.No ablation: no tested behaviour changed (docs, a skill and an eval fixture).
Acceptance notes
origin/main@c76c0e04):skills/objectstack-formula/SKILL.mdline 113 opened "This is a publish-time rejection, not advice.", the file had norecord_change/record-triggeredmention (0 hits; "record-change flow trigger" appears only in §5's bulk-write table, which teachesprevious, not the gate), andcontent/docs/automation/flows.mdxhad nonullable(0 hits). The order's premise holds.mainafter 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.main) → 5956 (ceiling 5997, headroom 41). Every token paid is a deletion in the same file, listed above; no ceiling row moves.order_amount > 10000) is left as it is: that flow has no record trigger andorder_amountis 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).flowCelRootScope's own criterion (the bound set is unprovable with ascript,subflow,map,wait,connector_actionor plugin node, an entrance that hands a record not in hand, or a contract-less screen); the full list stays in the code.previous != null && previous.x != nullon a flow's create leg) is not restated: §5 and theOLD.xcallout already teach it for every surface that bindsprevious.Verification
Head
554bea27: the two commits onc76c0e04, a merge oforigin/main@4b71a82b, one payback commit, and one wording commit from the at-tier review (record 6599648862). Readings taken ond97d2e33unless marked; the wording commit changed one blockquote line and no reading.Package readings
pnpm --filter '@objectstack/client-react...' --filter '@objectstack/lint...' build --concurrency=2under the lock after the merge: VERDICT command-exit 0, held 484s.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.Derived gates — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, 68 commands, run serially, `CMD :: exit N` recorded, reconciled with `--ran`
487ad8da(before the merge):--ranreconciliation68 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 themcheck-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-testrows of the ci / release scripts.d97d2e33(and again on554bea27forcheck-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.pnpm check:dual-build-cjs-loadsandpnpm check:lean-entry-closure— both exit 3, PREREQUISITE NOT MET: they load every package's builtdist/, which needs a full repo build this docs-and-skill diff does not warrant on the shared box (no packagesrc/moves); CI'sLint & Repo Gatesrun is the reading for those two.Human Minimum
Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.
packages/specholds no runtime logic. (no schema touched)pnpm lintis CI's run (repo-wide scan, not run locally).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (not touched)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code