Skip to content

docs(skills): teach the null-guard gate on a record-triggered flow's conditions in the formula skill and the flows page - #29864

Merged
os-zhuang merged 5 commits into
mainfrom
claude/project-thread-hv0rp3
Nov 3, 2026
Merged

os-zhuang merged 5 commits into
mainfrom
claude/project-thread-hv0rp3

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread
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 feat(lint): the publish-time null-guard gate judges a record-triggered flow's start, edge and decision-branch conditions #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 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 what packages/lint/src/validate-null-guards.ts (the ledger's flow row) and validate-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:
    • 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 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 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 docs(skills): the formula skill names action visible / disabled as the sparse, client-side exception to the total-record rule #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.

Package readings
  • 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.
Derived gates — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, 68 commands, run serially, `CMD :: exit N` recorded, reconciled with `--ran`
  • 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.

Human Minimum

Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (no schema touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is CI's run (repo-wide scan, not run locally).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer digest above is Chinese by AGENTS.md's communication rule).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

os-support-ai and others added 2 commits November 3, 2026 06:26
…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
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling labels Nov 3, 2026
…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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · os-support-ai · session_01WGufzhd2Ad9ANGTyAMW7pH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d97d2e3324a00b129b0a222897fbbac4d5845a82
Local-runs: none
Scope: skills/objectstack-formula/SKILL.md, skills/objectstack-formula/evals/cel-predicates-formulas.json + packages/lint/src/validate-null-guards.ts, packages/lint/src/validate-expressions.ts, packages/lint/src/flow-cel-root-scope.ts, packages/lint/src/authoring-rules.ts, packages/lint/src/runtime-gate.ts, packages/lint/src/validate-expressions.flow-null-guards.test.ts, packages/spec/src/automation/flow-trigger-kind.ts, packages/services/service-automation/src/dag-traversal.ts, packages/cli/src/commands/validate.ts, scripts/check-skills-token-ratchet.mjs, examples/app-showcase/src/automation/flows/index.ts, skills/README.md

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, git diff origin/main...pr/29864 at this head (4 files, +52/−24), and the 49 check-runs on d97d2e33. The remainder of the diff (.changeset/29482-formula-skill-flow-null-guards.md, content/docs/automation/flows.mdx) is the lane seat's ACCEPT at TIER_DEFAULT; read to judge, not judged in ④.

① Derived judgments

  • Skills token ratchet (scripts/check-skills-token-ratchet.mjs, ceiling row skills/objectstack-formula/SKILL.md 5997) — the earlier head 487ad8da went red after main landed PR docs(skills): the formula skill names action visible / disabled as the sparse, client-side exception to the total-record rule #29439 (+234 tokens in the same file); this head merges origin/main @ 4b71a82b and pays the overrun back with deletions in the same file (no re-wrap, no ceiling row moved). The gate's verdict on this head is Lint · repo & tooling gates = success. Right. The payback is judged in ④ for lost teaching: none lost.
  • Eval fixture under the ratchet — evals/** is population 1b: measured, NOT ratcheted (ruling record 6513306068, letter B, quoted in the gate's header); check:skill-eval-tokens covers ui/skills only. Adding eval 9 (+10 lines, +265 tokens) therefore moves no ceiling and needs none. Right.
  • Eval schema — eval 9 carries the fixture's full shape (id, prompt, expected_output, files, assertions.must_contain / must_not_contain), ids stay contiguous 1–9, skill_name unchanged; every must_contain string appears in expected_output, no must_not_contain string does. No mechanical gate reads skills/*/evals on this repo side (skills/README.md layout entry: maintainers' fixture, inert in consumer installs). Right.
  • .claude/** line ratchet (check:pm-skill-ratchet) — no .claude/** path in the diff; not applicable. Right.
  • Public surface — skills/** is synced into @objectstack/skills (packages/skills 18.2.0, scripts/sync-catalog.mjs), so the skill text and eval publish; no code, schema, rule id, message or accept set moves anywhere in packages/**. Metadata-protocol: none is right: nothing an author can write changes. Right.
  • Governed surface — skills/** is a Tier H register row; the Governed Surface Queue Guard check is green on the draft; landing remains the maintainer's (this record is the contract-tier review, not the Tier H approval). Right.
  • Check-runs on d97d2e33: 49 total — 35 success, 14 skipped (UI, opt-in and matrix placeholders), 0 failed, 0 pending. Every gate family the diff derives (changeset, skills token ratchet, skill docs/examples, doc authoring/anchors/links, PR-body guards, PM dispatch-gates, dogfood, Build Docs) is green.

② Semver level

③ Boundary flags

From os-dev-report 6599133588 (head 487ad8da, carried forward to this head by the PR body's Verification section):

  • NOT MEASURED locally: check:dual-build-cjs-loads, check:lean-entry-closure (need a full repo build; no package src/ moves). Answered: both run inside Lint & Repo Gates / Lint · repo & tooling gates on this head, success.
  • Deviation 1 — eval 9 added with no numeric budget: answered by the ratchet's own 1b population rule (evals measured, not ratcheted; ruling 6513306068). Not a gap.
  • Deviation 2 — the two built-output gates not run locally: same answer as above; CI is their reading.
  • Deviation 3 — session restarts and cancelled compound Bash calls: process only, re-done on the final head; no tree consequence. Closed.
  • Deviation 4 — harness attribution block as the PR body's first two lines, model-free commit trailers: PR-body guards check is green on both runs. Closed.
  • open_questions: [] — nothing to escalate.
  • Concurrency note (PR docs(skills): the formula skill names action visible / disabled as the sparse, client-side exception to the total-record rule #29439 on the same file): docs(skills): the formula skill names action visible / disabled as the sparse, client-side exception to the total-record rule #29439 landed on main first (e82bf64253); this head merges it with no conflict; its hunks (the narrowed "server-evaluated predicate" sentence, the action visible / disabled exception paragraph with its three example lines, the Surfaces table Action row) are byte-identical on this head. Closed.
  • Card's "Done when": both pages teach what validate-null-guards.ts (flow row of the ledger) and checkFlowNullGuards do as merged; the skill's evals exercise one refused flow edge (eval 9). Met — see ④.

④ Design judgment

skills/objectstack-formula/SKILL.md — each new sentence against the code on main:

  1. Which flows / which slots (lines 115–125): "a record-triggered flow's (triggerType: 'record-*') start config.condition, edge condition (nested regions too) or decision branch expression". Code: nullGuardObject = resolveFlowTriggerKind(flow) === 'record_change' ? objectName : undefined and resolveFlowTriggerKind returns record_change for any triggerType.startsWith('record-') (flow-trigger-kind.ts); checkFlowNullGuards is called at the top-level start node's config.condition (graph.path.length === 0), at every decision conditions[].expression, and at every edge of every graph collectFlowGraphs yields, container regions included (pinned: "judges an edge inside a container region", "judges a decision branch expression", "does not judge an inert config.condition on a non-start node"). Holds.
  2. Nullable definition with the roll-up nuance: "no required: true, no defaultValue, no default option, not a count / sum roll-up (empty reads 0; min / max / avg stay nullable)". Code: isNullableField → isAlwaysValuedSummary (summaryOperations.function === 'count' || 'sum'); pinned "treats a count / sum roll-up as always valued, and a max one as nullable". Holds. (The autonumber exclusion was already absent from this paragraph on main; not this card's.)
  3. Guard forms: "!= null / == null / !isBlank() test dominates it in the same boolean branch; has() deliberately does not satisfy that gate" — unchanged text, and truthGuards / falseGuards implement exactly these (a false isBlank proves non-null; has() collects only into the sharpened message). Holds.
  4. Flow callout, bare-name rule (lines 399–403): "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 every node's bindings are declared) and a guard on one guards both". Code: seedRunVariables sets record then for (const [k, v] of Object.entries(context.record)) if (!variables.has(k)) variables.set(k, v); the lint's nullGuardBare = nullable fields minus declaredVariables (variables + node ids) minus FLOW_ENGINE_BOUND_NAMES, only while rootScope.provable; fieldOf keys a bare name and record.name alike as record.name, so a guard on either spelling covers both. Holds. The parenthetical compresses provability to node bindings; flowCelRootScope is also opened by an entrance that hands the flow a record not in hand or a contract-less screen. The compression under-states the stand-down set only (bare names then go unjudged, never falsely refused), so an author cannot write a wrong flow from it — noted as should-fix below, not blocking.
  5. Start-condition presumption and the two-door spelling (lines 404–407): "What the start condition proves non-null is presumed at every edge and branch; spell that guard record.budget != null — the runtime publish gate judges the written flow alone and resolves no bare name." Code: nullGuardPresumed = provenNonNullKeys(startCondition, { bareFields: nullGuardBare }) is passed with afterStart: true to every edge and decision slot; at the runtime gate runtimeGateWrite hands perWriteSnapshotEntrance() to flowCelRootScope, so provable: false, nullGuardBare is empty, a bare budget != null proves nothing there and the edge comparing record.budget against 200000 is refused, while record.budget != null is accepted at both doors — pinned verbatim in the #29491 case of validate-expressions.flow-null-guards.test.ts. The second commit (487ad8da) was exactly this correction: the first draft said the runtime gate judges a bare name; the tree now says it does not. Holds, and it is the one teaching that stops an author from writing a flow that passes os validate and fails at the runtime publish gate.
  6. Outcome ("fails the run at that condition; the write that fired it still lands"): checkNullGuards(…, 'run-fails', …) → OUTCOME_CLAUSE['run-fails'] "the flow run fails at this condition: the write that fired it still lands"; evaluateCondition throws (ADR-0032 §1c) and the record-change trigger isolates the write. Holds.
  7. Both doors enforce: the rule validateStackExpressions is tier: 'gating', commands: ALL (so os build / os validate / os lint; validate.ts fails on ruleErrors), with runtimeTypes: ['flow', …] so runtime-gate.ts runs it with runtimeWriteType: 'flow' and its severity: 'error' findings are the gate's errors. Holds.

Would an assistant following the new text write a flow that passes both doors? Yes: it writes record.budget != null conjoined with the greater-than comparison of record.budget against 200000 on the edge (or record.budget != null in the start condition), which the pinned test accepts at validateStackExpressions and at runStackExpressionPasses(…, { runtimeWriteType: 'flow' }); it is the exact spelling the repaired showcase flow carries on main (examples/app-showcase/src/automation/flows/index.ts edge e2). Nothing taught is a form the validator would still flag: has()-only is named as not a guard; !isBlank() is a recognised false-guard; == null is taught in the same breath as "dominates in the same boolean branch", which is where the code honours it (|| right arm, ternary else). Nothing is taught that the code does not do.

skills/objectstack-formula/evals/cel-predicates-formulas.json, eval 9 — prompt is the card's scenario (record-after-update on project, nullable currency budget, edge from the decision node); expected_output is the pinned accepted spelling; its prose claims (both doors refuse the unguarded edge, run fails while the write lands, a bare budget compared against 200000 is refused by os validate, has(record.budget) is no guard) each map to a pinned case. must_not_contain catches the has()-only trap and the {record. brace error. The edge-out-of-a-decision shape is runtime-evaluated (FlowEdgeSchema.condition is an evaluated slot, #15807; the showcase defect was this very edge). Holds.

Payback deletions — teaching lost? Checked each against the rest of the file and all 9 evals:

  • "Null + string throws" section: pattern 1 (lines 233–247) still says "always coalesce nullable operands … CEL throws on null + string", and the translation table's CONCAT row restates the remedy; eval 1 ("because null + 'foo' throws") stays taught. No anchor to the heading exists anywhere on the head. No loss.
  • ABORTS paragraph's compile sentence: pattern 3 states "fails CEL compile: os build errors with a located message, and the flow engine throws". No loss.
  • ABORTS paragraph's insert-leg remedy and the compressed OLD.x / ISCHANGED callout: §5's table insert row (previous != null && … skips one; a column read off it faults) and the callout's own "null on an insert event and fails the write there unless guarded by previous != null &&" keep the rule; eval 3 stays taught. No loss.
  • Flow callout's "prefer record.x" sentence: row 1 of the table still names record.bare_field as the default and the callout now prescribes record.budget != null outright, which is the stronger form of the same preference. No loss.
  • "Verify your work" history clause: ADR-0032 citation and the validator's behaviour stay. No loss.
  • PR docs(skills): the formula skill names action visible / disabled as the sparse, client-side exception to the total-record rule #29439's hunks are untouched (checked by diff). No loss.

content/docs/automation/flows.mdx (lane ACCEPT, read to judge): item 5 of the failure-modes callout and the Edges table pointer say the same thing as the skill, sentence for sentence against the same code; triggerType: 'record-*' matches the resolver; the order_amount greater-than-10000 Basic Structure example is left alone correctly (no record trigger, a declared variable). Consistent.

Findings

  • should-fix (minor, non-blocking): SKILL.md line 402–403, "bare names only while every node's bindings are declared" — flowCelRootScope also stands down when an entrance hands the flow a record not in hand or a screen declares no input contract; "only while the flow's bound names are provable" would be exact at the same length. Consequence of the current wording is nil for what an author writes.
  • No must-fix.

Implemented-by: claude/project-thread-hv0rp3
Reviewed-by: session_01WGufzhd2Ad9ANGTyAMW7pH

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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · os-support-ai · session_01WGufzhd2Ad9ANGTyAMW7pH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 554bea27f6f3ed665654536aeca5d40fa8fe76fb
Local-runs: none
Scope: skills/objectstack-formula/SKILL.md, skills/objectstack-formula/evals/cel-predicates-formulas.json + packages/lint/src/validate-null-guards.ts, packages/lint/src/validate-expressions.ts, packages/lint/src/flow-cel-root-scope.ts, packages/lint/src/authoring-rules.ts, packages/lint/src/runtime-gate.ts, packages/lint/src/validate-expressions.flow-null-guards.test.ts, packages/spec/src/automation/flow-trigger-kind.ts, packages/services/service-automation/src/dag-traversal.ts, packages/cli/src/commands/validate.ts, scripts/check-skills-token-ratchet.mjs, examples/app-showcase/src/automation/flows/index.ts, skills/README.md

Round 1 of the review on this PR. The previous record (head d97d2e33, PASS with one should-fix) stands as read; this head adds one commit, 554bea27 "docs(skills): say when os validate judges a bare flow-condition name exactly", whose delta against d97d2e33 is one blockquote line of SKILL.md (402–403): "bare names only while every node's bindings are declared" becomes "bare names only while the flow's bound names are provable". Inputs read for this round: that delta, the refreshed PR body (head 554bea27, the callout description and acceptance note updated to the new wording, the Verification section naming the wording commit), the net diff against origin/main at this head (4 files, +52/−24, the same files as before), and the 49 check-runs on 554bea27. The remainder of the diff (.changeset/29482-formula-skill-flow-null-guards.md, content/docs/automation/flows.mdx) is unchanged from the reviewed head and stays the lane seat's ACCEPT at TIER_DEFAULT.

① Derived judgments

  • Skills token ratchet (scripts/check-skills-token-ratchet.mjs, ceiling row skills/objectstack-formula/SKILL.md 5997) — the delta swaps seven words for seven on one line, no line added, and the gate's verdict on this head is Lint · repo & tooling gates = success, so the ceiling still holds. The payback deletions from the previous head are unchanged and were judged lossless there. Right.
  • Eval fixture — untouched by the delta; eval 9 stands as judged (population 1b, measured not ratcheted; shape complete; assertions self-consistent). Right.
  • .claude/** line ratchet — not applicable, no such path. Right.
  • Public surface — still text-only into @objectstack/skills (packages/skills 18.2.0 via scripts/sync-catalog.mjs); no code, schema, rule id, message or accept set moves. Right.
  • Governed surface — skills/** Tier H; Governed Surface Queue Guard green on the draft; landing stays the maintainer's. Right.
  • Check-runs on 554bea27: 49 total — 35 success, 14 skipped (UI, opt-in and matrix placeholders), 0 failed, 0 pending. Identical family set and identical outcomes to the previous head.

② Semver level

  • Changeset '@objectstack/skills': patch, Clause-②: no, Metadata-protocol: none, mirrored line-initial in the PR body; unchanged by the delta. Text-only edits to a published skill and its eval fixture: none takes patch; no arm, no BREAKING banner, no ADR-0087 marker owed. Check Changeset green. Matches what the diff publishes.

③ Boundary flags

  • The os-dev-report's four deviations and two NOT-MEASURED gates (check:dual-build-cjs-loads, check:lean-entry-closure) were answered on the previous head by CI; this head's Lint & Repo Gates / Lint · repo & tooling gates are green again, so they stay answered. open_questions: [].
  • The wording commit was made in response to review record 6599648862 (named in the PR body's Verification section); the PR body's two-minute verification line still reads 5956 / 5997 (headroom 41), which the body says the wording commit did not change — the ratchet gate's green on this head is the reading of record, not the body's number.
  • Nothing new to escalate.

④ Design judgment

The delta, against the code on main. SKILL.md 402–403 now reads "os validate judges either spelling (bare names only while the flow's bound names are provable)". validate-expressions.ts builds nullGuardBare only if (nullGuardObject && rootScope.provable), and flowCelRootScope (flow-cel-root-scope.ts) returns provable: false for any of: a node outside CLOSED_BINDER_NODE_TYPES (script, connector_action, wait, subflow, map, a plugin's), a screen with no input contract, or an entrance that hands the flow a record not in hand (flowCelEntrances); at the runtime publish gate perWriteSnapshotEntrance() opens it unconditionally. "The flow's bound names are provable" is that criterion's own name (FlowCelRootScope.provable), so the sentence now states exactly when a bare name is judged and under-states nothing. Holds; the should-fix from the previous record is closed.

Everything else in the two hits files is byte-identical to the reviewed head and was traced there sentence by sentence: the judged slots (start config.condition at the top-level start node, decision conditions[].expression, every edge of every graph including container regions), the nullable definition with the count / sum vs min / max / avg nuance (isAlwaysValuedSummary), the guard forms (truthGuards / falseGuards; has() never a guard), the bare-name flattening (seedRunVariables sets a field at top level wherever no variable holds the name; fieldOf keys both spellings as record.name), the start-condition presumption (provenNonNullKeys passed with afterStart: true to every edge and decision slot), the two-door spelling (record.budget != null accepted at both doors; a bare start guard proves nothing at the runtime gate, pinned in the #29491 case of validate-expressions.flow-null-guards.test.ts), the run-fails outcome, and both doors' enforcement (validateStackExpressions gating on commands: ALL and runtimeTypes including flow; validate.ts fails on rule errors). An assistant following the text writes record.budget != null conjoined with the greater-than comparison of record.budget against 200000 — the exact spelling the repaired showcase flow carries on main (edge e2) — and it passes both doors. Eval 9 stands as judged. The payback deletions stand as judged: every pattern evals 1 and 3 rely on is still taught (pattern 1, pattern 3, the §5 table, the OLD.x callout), and PR #29439's hunks are untouched.

Findings

  • None. The previous round's should-fix is closed by the delta; no must-fix, no new should-fix.

Implemented-by: claude/project-thread-hv0rp3
Reviewed-by: session_01WGufzhd2Ad9ANGTyAMW7pH

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · os-support-ai · session_01WGufzhd2Ad9ANGTyAMW7pH

维护者速读

  • 改了什么:formula 技能与 flows 文档讲清楚 record-triggered 流程里「字段可能为空」的守卫:哪些写法 os validate 会拒、哪些在运行时发布门被拒、怎样写 record.budget != null 两道门都过;新增 eval [WIP] Create a new release version #9 覆盖拒绝的流程边。
  • 为什么改:守卫规则已在 lint 落地,但技能没教,助手照旧写出发布时被拒的流程。
  • 风险与回滚:只改技能文本、一条 eval 和一页文档,随 @objectstack/skills patch 发布;不动代码与规范。合 main 后 token 5956/5997,为此删掉了几处重复说明。回滚即 revert 本 PR。
  • 先看:skills/objectstack-formula/SKILL.md 第 115-125 行与第 399-407 行,再看 eval [WIP] Create a new release version #9。
  • 两分钟验证:CI 全绿(含 skills token 上限);开发在两道门各实测一遍,结果表在 PR 正文。
  • 需要你做的:请 GOVERNED_APPROVERS(os-zhuang 或 hotlong)之一在本 PR 点 Approve。契约复审 PASS 6599955304,Head-sha 554bea27。批准后由席位落地。

席位意见:教学内容与 lint 代码逐句对过,两轮复审 PASS,同意落地。

席位核对 — ACCEPT · domain:skills#3 · head 554bea27f6f3ed665654536aeca5d40fa8fe76fb

  • Shape: Fixes #29482; draft, base main, relay-opened, labels documentation, size/s, tooling, no assignees. 4 files, +52 −24: skills/objectstack-formula/SKILL.md, its eval fixture ([WIP] Create a new release version #9), content/docs/automation/flows.mdx, one changeset; mergeable_state clean.
  • Premise measured: the null-guard check for record-triggered flows was live in packages/lint on main while the formula skill and the flows page taught neither door; the PR merges origin/main 4b71a82, so PR docs(skills): the formula skill names action visible / disabled as the sparse, client-side exception to the total-record rule #29439's paragraph in the same SKILL.md is intact.
  • Gates (check-runs at this head): 49 completed, 35 success and 14 expected skips, 0 failed. The earlier head 487ad8d went red on the published skills token ratchet after docs(skills): the formula skill names action visible / disabled as the sparse, client-side exception to the total-record rule #29439 grew the file; this head pays it back by deletion in the same file (5956 / 5997).
  • Contract review: record 6599648862 (head d97d2e3, PASS, one should-fix) and delta r1 record 6599955304 (head 554bea2, PASS, should-fix closed), both Served-tier: CONTRACT_REVIEW_TIER, Local-runs: none. Payback deletions judged to remove only restatements still taught elsewhere.
  • Declarations: Clause-②: no, Metadata-protocol: none; changeset '@objectstack/skills': patch; Check Changeset success.
  • Accepted deviations: SKILL.md +6 net lines vs base and flows.mdx +12, both within the order; two NOT-MEASURED local gates (dual-build-cjs-loads, lean-entry-closure) answered by CI green. The seat refreshed the PR body to the final head.
  • Out of scope: none.
  • Landing: check-governed-merges --pr 29864: Tier H (skills/** register rows), 76 changed lines. Waits on an APPROVED review from a GOVERNED_APPROVERS account (os-zhuang, hotlong); the seat then readies it and arms auto-merge.

@os-zhuang
os-zhuang marked this pull request as ready for review November 3, 2026 09:44
@os-zhuang
os-zhuang enabled auto-merge November 3, 2026 09:45
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 00f72f9 Nov 3, 2026
55 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-hv0rp3 branch November 3, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-user-decision size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Teach the record-triggered flow null-guard gate in the formula skill and the flows page

3 participants