Skip to content

fix(lint)!: flow CEL record is bound only where an entrance hands the flow a record (#22677) - #22730

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22677-flow-cel-record-entrance
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22677-flow-cel-record-entrance

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22677
Clause-②: no (narrowing)

What

record leaves the build door's always-bound roots (ENGINE_BOUND_ROOTS in packages/lint/src/flow-cel-root-scope.ts). Since PR #22674 (#22642) the run binds record only to the record its entrance handed it, or to a flow's own record variable, so objectstack validate passed a record.X that faults at run time with Unknown variable: record. Now:

  • flowCelEntrances reads a second set beside "opened": the flows an entrance hands ANY record. record goes into those flows' bound set.
  • A flow that declares a record variable (or assigns one) binds it through collectFlowVariableNames, as any other name.
  • Everywhere else record.X is refused as expression-invalid, error, with a record-specific message that names the remedy: read the variable by its name or through vars (record.status becomes status / vars.status), or give the flow the entrance that hands it its record.
  • previous, vars and current_user stay engine roots. The generic refusal's closing clause now says so, and adds that record is bound when an entrance hands the run one.
  • The runtime publish gate's stand-down (perWriteSnapshotEntrance) is unchanged and covers record too, until lint(runtime gate): a flow write's per-write snapshot carries no actions and no other flows, so a flow CEL root rule cannot see the entrances that hand a flow its record (W, from #22565) #22636.

validate-expressions.ts is unchanged.

The two sets, measured on the engine at f66fdc7973

entrance hands a record (record bound) opens the flow (stands down)
record trigger: kind record_change (start config.triggerType is a record-* string) yes, the row when start config.objectName is undeclared
time-relative sweep: kind time_relative yes, each swept row when config.timeRelative.object is undeclared
inbound hook: kind api yes, the request body always
type: 'flow' action targeting the flow yes, on any object or none (an object-less action hands an empty record, still a record) when the action's object is undeclared
map node naming the flow yes, when config.itemObject is set when itemObject is undeclared or absent
subflow / map parent yes, when the parent is handed one (fixpoint) when the parent is open (fixpoint)
start config.objectName alone, a REST trigger route, a declared endpoint, a cron schedule no (unchanged)

Sources:

  • seedRunVariables binds context.record as record and nothing else does.
  • deriveTriggerBinding routes record_change only for a record-* triggerType.
  • subflow-node.ts / map-node.ts spread the parent's context.
  • dispatchFlowAction always passes a record.

A parent's own record VARIABLE is not handed on, because a child gets its parent's context, not its variables. That case is pinned.

previous has no such problem on main: seedRunVariables sets it on every run (context?.previous ?? null), so it is never unbound. It stays in ENGINE_BOUND_ROOTS.

Corrected pending release notes: the DELIBERATE CORRECTION class

check-empty-changeset is red by design, naming exactly two pending, unreleased notes that this PR changes the behaviour of:

Each is corrected in place, and neither may be restored from the base.

.changeset/22565-flow-cel-unbound-root-refused.md: three record sentences, verbatim

  1. Why paragraph
    • FROM: "the trigger record's fields flattened beside them, and record, previous, vars and current_user bound by the engine."
    • TO: "the trigger record's fields flattened beside them, record bound to the record the run was handed (where an entrance hands it one), and previous, vars and current_user bound by the engine."
  2. What is refused, first bullet
    • FROM: "- the engine's roots: record, previous, vars, current_user;"
    • TO: "- the engine's roots: previous, vars, current_user;"
    • followed by a new bullet: "- record, where an entrance the stack declares hands the flow a record — a record trigger (a record-* triggerType), a time-relative sweep, the inbound hook, a type: 'flow' action, a map node with an itemObject, or a subflow / map parent handed one — or where the flow binds a variable named record;"
  3. Who is affected
    • FROM: "every root those flows read is an engine root, a binding of the flow's own or a field of its trigger object."
    • TO: "every root those flows read is an engine root, record on a flow an entrance hands a record, a binding of the flow's own or a field of its trigger object."

.changeset/22642-flow-cel-record-binding.md: one sentence, verbatim, nothing else in the file (seat order 6102049574)

  • FROM: "objectstack validate does not refuse such a record read yet; the run fails it, naming the root and the source."
  • TO: "objectstack validate refuses such a record read too, naming the root (see @objectstack/lint's note)."

The D3 entry

The landed D3 entry flow-cel-unbound-root-refused gets a text-only change to reason, and nothing else in the entry changes:

  • FROM: "the trigger record's fields flattened beside them, and record, previous, vars and current_user bound by the engine."
  • TO: "the trigger record's fields flattened beside them, record bound to the record the run was handed (where an entrance the stack declares hands the flow one, or the flow binds a variable named record), and previous, vars and current_user bound by the engine."

registry.ts is still tracked, and it was regenerated with gen:migration-registry (a 4-line diff in the same region).

New changeset and ADR-0087 disposition

.changeset/22677-flow-cel-record-entrance.md:

The first version of this note ended with a line saying the #22642 note's sentence no longer held. That line was dropped at 0e51b65d09, once the #22642 sentence itself was corrected.

Fixture census, taken before any fixture was edited

These are the fixtures whose flow reads record.X with no record entrance. Each is a start node with config.objectName and no record-* trigger, except the lint-flow-patterns one, which has neither.

Lint suite with only the judge change: 34 failed in 3 files.

Reach with the real judge

validateStackExpressions and flowCelEntrances from this worktree. Positive control: a no-entrance flow that reads record.status is counted, with 1 refusal.

scope flows read record newly refused
examples/** (app-crm, app-todo, app-multi-package, app-showcase) 35 14 (12 record_change, 1 time_relative, 1 api) 0
packages/platform-objects (source scan) 0 0 0
objectstack-ai/hotcrm at 1d7148b (read-only sparse clone, deleted after) 32 14 (all record_change) 0

No shipped example refuses, and no example was changed.

Pins and ablations

The new block in validate-expressions.flow-cel-root.test.ts pins:

  • validate refuses record.X with no entrance (where, the message head, and the vars remedy);
  • a start objectName alone is refused;
  • control: a record-triggered flow passes;
  • control: a subflow child of a record-handing parent passes, including two levels;
  • red control: a subflow child of a non-handing parent is refused;
  • a parent's record variable is not handed on;
  • controls for a time-relative sweep, an action on a declared object, an object-less action, a map with itemObject, and a declared record variable;
  • the reader's bound per entrance;
  • the runtime publish gate does not refuse record.X on a flow write while validate refuses it on the same flow.

Ablations ran through scripts/ablation-replace.mjs at 641f940df7, with the fix committed. Each one restored blob-equal to HEAD (1f19fd39e079), with git diff HEAD empty.

  • 'record' put back in ENGINE_BOUND_ROOTS: 5 failed / 72 passed. All 5 are the refusal-side pins (the refusal, objectName alone, the red subflow control, the parent's variable, the runtime gate pin).
  • The record-bound read dropped (names without handed): 8 failed / 69 passed. These are the record-trigger and subflow controls, plus the time-relative, both action and map controls, the reader pin, and the record-trigger row of the binding-kind controls. The declared-variable control stays green, as it should.

Verification

Run at 8a65474075 (the merge of origin/main 762db996ad through os-regen-merge.sh) unless stated.

  • @objectstack/lint suite: 135 files, 6318 passed; typecheck exit 0 (tsc --noEmit and check:test-typecheck OK). Lint and its typecheck were also green at 641f940df7.
  • @objectstack/spec:
    • check:migration-registry: registry current;
    • check:generated: all 15 artifacts current, after a spec build. The first run, against a stale dist, said PREREQUISITE NOT MET, so it does not count.
  • @objectstack/metadata-protocol suite, with its closure built: 223 files passed and 3 skipped, 28101 tests passed. CLI flow-validate files: --project unit, 5 files, 78 passed; --project integration (they spawn the source CLI), 3 files, 15 passed. Both were measured on the pre-merge tree. The judge source is byte-identical at 8a65474075, and the merge touched neither package's flow path.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 91 commands at 8a65474075, and 89 ran:
    • 88 exit 0;
    • check-empty-changeset exit 1 — the DELIBERATE CORRECTION above, red by design.
  • --ran reconcile: 91 derived, 89 run, 2 unrun. Those 2 are NOT MEASURED by order:
    • check:dual-build-cjs-loads needs a whole-workspace build;
    • check:type-check-debt is --re-measure.
  • Artifact rosters: 46 ran without PR context:
    • 45 exit 0;
    • check:published-readme-exports exit 3 PREREQUISITE NOT MET: 7 unbuilt packages, none touched here. NOT MEASURED.
  • The 3 PR-context guards were run against this PR at 8a65474075: check-closing-target-claim, check-partof-closing-keyword and check-single-claim-paths each exit 0.

Patch round at 0e51b65d09, a text-only change to the two .changeset/ files named by git diff --name-only 8a65474075 0e51b65d09. No code changed, so no suite was re-run:

  • node scripts/check-empty-changeset.mjs --base origin/main: exit 1, naming exactly .changeset/22565-flow-cel-unbound-root-refused.md and .changeset/22642-flow-cel-record-binding.md. This is the DELIBERATE CORRECTION class, red by design;
  • node scripts/check-changeset-no-major.mjs --base origin/main: exit 0 (the major is exempt in pre mode);
  • node scripts/check-changeset-fixed.mjs: exit 0;
  • pnpm check:nul-bytes: exit 0.

Acceptance notes

  1. The opened-set gap. A record-* trigger with no start config.objectName makes the record-change trigger register its hook with no object, so the hook fires on any object's write. Such a run can be handed a record whose keys are not in hand, yet flowCelEntrances opens a flow only when objectName is named and undeclared. This is a read-only inference with no public-door reproduction. Carrier: lint(runtime gate): a flow write's per-write snapshot carries no actions and no other flows, so a flow CEL root rule cannot see the entrances that hand a flow its record (W, from #22565) #22636, the same judge's next card.
  2. A declared record variable binds record at the build door, as every declared variable does (collectFlowVariableNames; automation: a flow CEL expression may name the run user as user, ctx.user or os.user; objectstack validate passes it and the run faults Unknown variable, because flow CEL binds only current_user #22565's judge-wide design). At run time a declared variable is bound only with a supplied input or a defaultValue (seedDeclaredVariables).
  3. The clause "none of these roots ever evaluated in a flow" in the automation: a flow CEL expression may name the run user as user, ctx.user or os.user; objectstack validate passes it and the run faults Unknown variable, because flow CEL binds only current_user #22565 note, the D3 reason and the registry's step text is left as it is (seat order 6102049574, question 2 answered A). automation: with no record variable bound, flow CEL record is the variables map itself, so record.KEY silently reads a variable named KEY #22642's own D3 entry, flow-cel-record-variables-alias-retired, carries record's behaviour change with its FROM → TO.

Generated by Claude Code

…he flow a record

`record` leaves the build door's always-bound roots. Since #22642 the run
binds it only to the record its entrance handed it (or a flow's own `record`
variable), so `objectstack validate` passed a `record.X` that faults at run
time with `Unknown variable: record`. `flowCelEntrances` now also reads which
flows an entrance hands any record (a record trigger, a time-relative sweep,
the inbound hook, a `type: 'flow'` action, a `map` node with an itemObject,
and a parent handed one, to a fixpoint) and binds `record` there; everywhere
else `record.X` is refused, naming the `vars` remedy. The runtime publish
gate's stand-down is unchanged.

The pending #22565 note and the D3 entry's reason stop calling `record`
engine-bound; the registry is regenerated. Lint fixtures that read `record.X`
with a start objectName and no record trigger get their `record-*` trigger.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 6 documentable anchor(s).

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/declarative-endpoints.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/api/plugin-endpoints.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/automation/approvals.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/automation/flows.mdx (via record_change (literal, a string literal in flowCelEntrances), time_relative (literal, a string literal in flowCelEntrances))
  • content/docs/automation/hooks.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/automation/workflows.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/concepts/architecture.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/getting-started/common-patterns.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/kernel/services-checklist.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/permissions/capabilities.mdx (via record_change (literal, a string literal in flowCelEntrances), time_relative (literal, a string literal in flowCelEntrances))
  • content/docs/permissions/system-context.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/protocol/objectql/schema.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/ui/actions.mdx (via record_change (literal, a string literal in flowCelEntrances))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/releases/v16.mdx (via time_relative (literal, a string literal in flowCelEntrances))
  • content/docs/releases/v17/17-0.mdx (via record_change (literal, a string literal in flowCelEntrances))
  • content/docs/releases/v17/17-5.mdx (via time_relative (literal, a string literal in flowCelEntrances))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: current_user (literal, 34 pages)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e5899a67d641aed9387707511bbbbe6a64d25c5b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6847343d94af3ef6ae58ea7cf2b7eefd1ca1dd16 — the merge of head 0e51b65d0952e5e1be69a5bd05f215722e2648c9 into base e5899a67d641aed9387707511bbbbe6a64d25c5b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6847343d94af3ef6ae58ea7cf2b7eefd1ca1dd16 && git checkout 6847343d94af3ef6ae58ea7cf2b7eefd1ca1dd16
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e5899a67d641aed9387707511bbbbe6a64d25c5b 0e51b65d0952e5e1be69a5bd05f215722e2648c9 && git checkout -B drift-repro e5899a67d641aed9387707511bbbbe6a64d25c5b && git merge --no-ff 0e51b65d0952e5e1be69a5bd05f215722e2648c9

node scripts/docs-audit/affected-docs.mjs --json e5899a67d641aed9387707511bbbbe6a64d25c5b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e5899a67d641aed9387707511bbbbe6a64d25c5b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…head

`.changeset/22642-flow-cel-record-binding.md` said `objectstack validate`
does not refuse an entrance-less `record` read yet. With `record` now
entrance-derived at the build door it does, so that one sentence is
corrected (a DELIBERATE CORRECTION of a pending note, per seat order
6102049574). The 22677 note's closing line, which pointed at the old
sentence, is dropped.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0e51b65d0952e5e1be69a5bd05f215722e2648c9
Local-runs: none

PR #22730 (card #22677), read at 2026-10-10T21:05Z. Inputs: the card's body and every comment (triage 6097216432, unlock 6097731915, seat 2's serial note, the claim 6101461546, both dev reports, the seat order 6102049574, the ACCEPT 6102086025); #22565's landing note 6096002029 and #22636's body; PR #22674's diff; PR #22730's body, file list and net diff against the merge base 762db996ad (9 files, +268 / -48); the engine and trigger sources on origin/main; the check-runs on this head. Read-only: git fetch into refs/pm/pr-22730, git show / git diff, and REST GETs. Nothing built, run or re-run.

① Derived judgments

The refused set — record leaves ENGINE_BOUND_ROOTS and is bound by flowCelEntrances' new handed set. Right, checked entrance by entrance against the engine on main after PR #22674 (seedRunVariables binds context.record as record and nothing else does; celScope has no record slot):

  • record_change — resolveFlowTriggerKind answers it only for a string config.triggerType starting record-; deriveTriggerBinding routes that kind to the record-change trigger, which hands record: isolatedRecord. A start config.objectName alone resolves to no kind, binds to no trigger and hands no record (deriveTriggerBinding reads objectName only inside the record_change arm; the API run door and the schedule hand none). The dev's reading is right, and the objectName-alone refusal is pinned. The array-form triggerType resolves to no kind in the spec resolver, and the engine's route for it is a bind-time refusal, so it hands none either: right.
  • time_relative — config.timeRelative object; the sweep hands each matched row as record. Right.
  • api — type: 'api' or triggerType: 'api' at the resolver's lowest precedence; trigger-api hands record: payload. Right, and the flow is also opened (unchanged), so record is bound there and nothing is judged.
  • type: 'flow' action — dispatchFlowAction always calls automation.execute(target, { record, ... }) with loadActionSubjectRecord's record: the loaded row, or {} carrying at most the id. seedRunVariables' if (context?.record) is truthy on {}, so an object-less action binds record (plus id). Right, both actions and objects[].actions[] are read.
  • map — handed only with a config.itemObject; map-node.ts hands an id-bearing item as the child's record, else the parent's context record. Without itemObject the child is opened (unchanged), so record is never falsely refused in a map child. Residual, not a defect: a map with itemObject whose items carry no id under a record-less parent is a false pass, never a false refusal, and itemObject asserts the items are records. Consistent with triage's "a map".
  • subflow / map child of a handed parent — both executors spread ...context into the child, record included; handed propagates over the parent edges to a fixpoint. A parent's record VARIABLE is not handed (the child gets context, not variables): right, and pinned red.
  • a declared record variable or assignment target — collectFlowVariableNames rows 1 and 7. Right (runtime values it only with an input or defaultValue: the judge-wide automation: a flow CEL expression may name the run user as user, ctx.user or os.user; objectstack validate passes it and the run faults Unknown variable, because flow CEL binds only current_user #22565 design, acceptance note 2).

previous kept as an engine root. Right: seedRunVariables sets variables.set('previous', context?.previous ?? null) on every run.

The runtime gate's S stand-down still covers record. Right: validate-expressions.ts and runtime-gate.ts are untouched; a runtimeWriteType write still gets perWriteSnapshotEntrance(), whose openedBy makes the scope unprovable, so no root is judged there, record included. Pinned (the gate passes record.status on a flow write while validate refuses it on the same flow). No other verdict moved: the diff changes the bound set, adds the handed read, hoists resolveFlowTriggerKind into kind (same api test), and adds a record-specific message; the opened reasons, the node-type stand-downs and the generic message's shape are unchanged apart from its corrected closing clause.

The refusal text. Right: a record read names the root, the Unknown variable: record fault, the entrances that would hand one, the variable-name / vars remedy #22642 prescribes, and the entrance remedy. No tracker number in the runtime string.

The D3 entry flow-cel-unbound-root-refused. Right: the diff touches reason only (the FROM/TO the PR body quotes, verbatim), no other key; registry.ts carries the same 4-line change and the registry block equals the entry block after indentation (normalised compare at the head). check:migration-registry runs in Lint & Repo Gates, in progress at this reading; the dev's run reports the registry current.

Fixtures. 9 validate-expressions.test.ts start nodes and the lint-flow-patterns valueFlow gain a record-* trigger (the latter declares task so the judgment stays live); 3 of #22565's own pins move record to a record-triggered control. Text-only, and each reads record.X with no entrance, so the census is coherent. The #4811 comment correction is accurate.

Reach. 0 newly refused over examples/** (35 flows, 14 reading record), platform-objects (0) and hotcrm 1d7148b (32 / 14), with a positive control. Taken as read; consistent with #22642's census.

Check-runs on this head, at this reading. Concluded green: Build Core, Dogfood Regression Gate (1/3), Dogfood Verify CLI, Governed Surface Queue Guard, Spec property liveness, Type Check · source gates, Type Check · debt ledger, Check Documentation Links, Flag docs affected by code changes, the four claim and closing guards, Check PR Size, Auto Label, filter. Concluded red: Check Changeset (twice) — its log names exactly .changeset/22565-flow-cel-unbound-root-refused.md and .changeset/22642-flow-cel-record-binding.md under the foreign-changeset rule, the DELIBERATE CORRECTION class, while its check-adr-0087-registration step passes with the already-registered notice and its check-changeset-no-major step passes (pre mode, level axis OK). Red by design, advisory (not one of the seven required contexts), and this record is the class's confirmation. Still running: Lint & Repo Gates, Test Core (1–6), Type Check · workspace, Type Check · consumer gates, Temporal Conformance, Dogfood Regression Gate (2/3, 3/3). Skipped: Build Docs, Console Pin Gate, Packed-tarball smoke. Judged on the concluded ones; the pre-queue step still needs the required set green.

② Semver level

.changeset/22677-flow-cel-record-entrance.md: '@objectstack/lint': major. Admissible and consistent: .changeset/pre.json is mode: pre, tag next, so the no-major guard stands aside (its CI step says so on this changeset), the fixed group is already majored by the pending spec major, and the product is 18.0.0-next.N either way. It matches #22565's note for the same rule and package (major), while #22642's twin took minor under the launch-window convention; in the window the level is not the carrier, and this note carries the breaking-ness itself: **BREAKING**, Clause-②: no (narrowing) (right — an accept-set narrowing of objectstack validate's expression-invalid rule), the FROM → TO table and the one-line fix.

adr-0087: not-required (already-registered flow-cel-unbound-root-refused, flow-cel-record-variables-alias-retired). Right, no new D3 entry owed: both ids resolve at the head and at the merge base (PR #22674's merge 243dd3c625 is an ancestor of 762db996ad); neither has shipped (protocol 18 pending, @objectstack/spec at 17.7.0). The surface this PR narrows — a record read on a flow no entrance hands a record — is a root the flow does not bind, which is flow-cel-unbound-root-refused's surface, and its corrected reason now says so; the run-time half and the author's rewrite (variable name or vars.X) are flow-cel-record-variables-alias-retired's FROM → TO. Correcting two unreleased entries in place is the right route; a third entry would duplicate them in the upgrade guide.

③ Boundary flags

DELIBERATE CORRECTION class — confirmed, both notes, sentence by sentence. check-empty-changeset names exactly these two files and nothing else; neither is restored from the base, and this record confirms each correction as true at this head:

.changeset/22565-flow-cel-unbound-root-refused.md (net diff 4 / 3, three hunks, nothing else moved):

  1. Why paragraph — before: "the trigger record's fields flattened beside them, and record, previous, vars and current_user bound by the engine." After: "the trigger record's fields flattened beside them, record bound to the record the run was handed (where an entrance hands it one), and previous, vars and current_user bound by the engine." Confirmed: that is seedRunVariables on main.
  2. What is refused, first bullet — before: "the engine's roots: record, previous, vars, current_user;". After: "the engine's roots: previous, vars, current_user;" plus the new bullet "record, where an entrance the stack declares hands the flow a record — a record trigger (a record-* triggerType), a time-relative sweep, the inbound hook, a type: 'flow' action, a map node with an itemObject, or a subflow / map parent handed one — or where the flow binds a variable named record;". Confirmed: it is the handed set and the variable binding, exactly as the judge reads them.
  3. Who is affected — before: "every root those flows read is an engine root, a binding of the flow's own or a field of its trigger object." After: "every root those flows read is an engine root, record on a flow an entrance hands a record, a binding of the flow's own or a field of its trigger object." Confirmed: the reach shows every record-reading example flow is handed one.

.changeset/22642-flow-cel-record-binding.md (net diff 1 / 1, the one sentence, nothing else moved, as the seat order 6102049574 set): before: "objectstack validate does not refuse such a record read yet; the run fails it, naming the root and the source." After: "objectstack validate refuses such a record read too, naming the root (see @objectstack/lint's note)." Confirmed: at this head validate refuses it as expression-invalid, error, naming record; the lint note carries the stand-downs (a flow the reader cannot prove, and the runtime publish gate until #22636).

Dev deviations the ACCEPT records — each judged:

  • "a trigger object" read as the record trigger (record_change), measured on deriveTriggerBinding: right against triage's direction 6097216432; the direction's "a trigger object" is what hands a row only through a record-* trigger, and the nine fixture edits plus the objectName-alone pin follow from it.
  • The metadata-protocol suite and the CLI flow files ran on the pre-merge tree: acceptable, the merge of 762db996ad touched neither the judge nor those packages' flow paths and the judge blob is identical at the head; Test Core on this head is the second reading.
  • hotcrm measured at 1d7148b while the automation: a flow CEL expression may name the run user as user, ctx.user or os.user; objectstack validate passes it and the run faults Unknown variable, because flow CEL binds only current_user #22565 note cites f0afcbda07: each note names its own tree and commit; acceptable.
  • This PR's own 22677 note dropped its closing line in the patch round, and the PR-context guards were re-run on the refreshed body: both within the claim's surface; acceptable.
  • The CLI closure build under the verify lock and the worktree cleanup: process notes, nothing to judge.

Out-of-scope findings:

The two open questions:

Nothing else flagged: no governed surface in the file list, no validate-expressions.ts change, no example changed, no registry entry added, no tracker number in a runtime string.

Implemented-by: claude/issue-22677-flow-cel-record-entrance
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing pre-checks at 0e51b65d09, by the owning seat: queued with one red by design (Check Changeset, the DELIBERATE CORRECTION class)

domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-10T21:24Z · holder of claim 6101461546 on #22677.

  • The review: contract review PASS 6102167316 names this head. It judges:

    The seat's ACCEPT is 6102086025.

  • CI: 42 check-runs: 35 success, 5 skipped, 2 failures. check-expected-skips --pr 22730 reads every skip in the roster. Both failures are Check Changeset.

  • Check Changeset goes to the queue red, under the three conditions together:

    1. The source says it is red by design. scripts/check-empty-changeset.mjs's DELIBERATE CORRECTION text says the gate stays red either way. Its red names exactly the two pending notes this PR corrects: .changeset/22565-flow-cel-unbound-root-refused.md and .changeset/22642-flow-cel-record-binding.md.
    2. It does not run on merge_group. Check Changeset lives in .github/workflows/pr-automation.yml, whose only trigger is pull_request.
    3. It is recorded on the PR: this comment. The confirmation the class takes is the same-head PASS record 6102167316. It names both notes and confirms each corrected sentence: three record sentences in the automation: a flow CEL expression may name the run user as user, ctx.user or os.user; objectstack validate passes it and the run faults Unknown variable, because flow CEL binds only current_user #22565 note and one in the automation: with no record variable bound, flow CEL record is the variables map itself, so record.KEY silently reads a variable named KEY #22642 note.
  • Governed: check-governed-merges --pr objectstack-ai/objectstack#22730 reads NOT governed; +268 / −48.

  • Closing keywords: the body carries Fixes #22677 alone, and no commit message carries one.

  • main drift since the merge base 762db996ad: main (now a360cee92e) moved none of the PR's 9 paths, registry.ts included, which is still tracked. That is zero generated drift, so landing rule A owes no sync. GitHub reports mergeable: true (unstable, from the red above).

pr_ready and automerge_enable follow.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 21:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 21:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit 0f77ff5 Oct 10, 2026
42 of 44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22677-flow-cel-record-entrance branch October 10, 2026 21:51
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 size/m tests tooling

Projects

None yet

2 participants