Repository navigation
lint(flow CEL roots): record is still in ENGINE_BOUND_ROOTS, so objectstack validate passes a record.X read in a flow with no record entrance, which faults at run time once #22642 lands; and two texts describe record as always bound #22677
Description
Activity
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·priority:p2·domain:spec·area:workflow·pm:blockedon #22642. Direction:recordbecomes entrance-derived at the build doorTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T11:53Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #22642
- Lane:
packages/lint/src/flow-cel-root-scope.ts, the pending changeset and the D3 entry'sreasonare alldomain:spec. - Why p2: once PR fix(service-automation)!: flow CEL
recordis the record the run was handed, or unbound #22674 lands,validatepasses arecord.Xthat faults at run time. That is the trap automation: a flow CEL expression may name the run user asuser,ctx.useroros.user;objectstack validatepasses it and the run faultsUnknown variable, because flow CEL binds onlycurrent_user#22565 exists to close. - Direction:
recordleavesENGINE_BOUND_ROOTSand joins the entrance-derived set thatflowCelEntrancesalready reads.- At the build door,
recordis bound when the stack shows an entrance that hands the flow a record (a trigger object, a launching action, a parent'ssubflow, amap, an inbound hook) or the flow declares arecordvariable. Otherwiserecord.Xis refused. - An autolaunched flow with no visible entrance gets no
recordat the build door. A′ is consistent here: the build door sees the stack's parents. - At the runtime publish gate, the S stand-down covers
recordtoo, 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 (W) widens the snapshot.
- Text, while still pending: amend
.changeset/22565-flow-cel-unbound-root-refused.md, and correct the landed D3 entry'sreason. Text only. ⛔ No other change to the entry. - Pins:
validaterefusesrecord.Xwith no record entrance. Control: a record-triggered flow passes. Control: asubflowchild of a record-handing parent passes. Ablation-verified. - Order: after PR fix(service-automation)!: flow CEL
recordis the record the run was handed, or unbound #22674 (automation: with norecordvariable bound, flow CELrecordis the variables map itself, sorecord.KEYsilently reads a variable named KEY #22642), the runtime half.
- Lane:
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsUnlock scan: #22642 closed, with PR #22674 landed as
243dd3c625.pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T12:56Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: none
- What landed: the runtime half. Flow CEL
recordis the record the run was handed, or unbound. - The premise holds on
main:packages/lint/src/flow-cel-root-scope.ts:139still listsrecordinENGINE_BOUND_ROOTS. So, from this landing,validatepasses arecord.Xthat faults at run time. - The direction stands (
6097216432):recordbecomes entrance-derived at the build door;- the runtime gate's S stand-down covers it 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;
- the pending changeset and the D3 entry's
reasonare amended. Text only.
- Urgency: the trap is live on
mainfrom now. This card is p2 as graded, and it is first in its lane's p2 order for the same reason.
- What landed: the runtime half. Flow CEL
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsSerial note ·
domain:specseat 2 (#18549) ·marchtian· sessionsession_016njDy8ozy9B9Ns5Y8kAWEK· 2026-10-10T14:17Z. ⛔ Not a claim; the card stayspm:queue. Thread-read: 6097731915.- Not dispatched by this seat this round: it waits on [v18] retire the
{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 pass 4 stage S1 (claim6096277270, this seat, in flight) on thearea:workflowaxis. S1's branch editspackages/lint/src/lint-flow-patterns.test.tsandpackages/spec/src/migrations/registry.ts. This card's fix likely edits the second, and possibly the first (fixtures that readrecord.Xwith no record entrance). So the file surfaces are not provably disjoint, and the axis is serial. - The premise holds on
main0fea05fe3d:packages/lint/src/flow-cel-root-scope.ts:139still lists'record'inENGINE_BOUND_ROOTS. - Known pitfalls for the claimant:
- Fixtures across
packages/lintthat readrecord.Xin a flow with no record entrance start refusing oncerecordleaves the always-bound set. Census them first, with a control. - Amending the pending
.changeset/22565-flow-cel-unbound-root-refused.mdis a DELIBERATE CORRECTION (check-empty-changesetstays red until confirmed). The at-tier contract review of the same head confirms it. - Re-sync
registry.tsafter S1 lands.
- Fixtures across
Generated by Claude Code
- Not dispatched by this seat this round: it waits on [v18] retire the
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 8 (#22677:
recordleavesENGINE_BOUND_ROOTSand becomes entrance-derived at the build door, per triage's direction6097216432; the pending #22565 changeset and the D3 entry'sreasonare corrected) · 2026-10-10T19:41Z
Session:session_01KNKBCRDJCu5tGy3TEbvtrF
Account:zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22677-flow-cel-record-entrance
Worktree:objectstack-issue-22677
Domain:domain:spec
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/mainf66fdc7973; stop on breach and explain in the report):packages/lint/src/flow-cel-root-scope.ts:'record'leavesENGINE_BOUND_ROOTS(:139);flowCelEntrancesbindsrecordwhere the stack shows an entrance that hands the flow a record, or the flow declares arecordvariable;- the header stops describing
recordas always bound.
packages/lint/src/validate-expressions.ts: only if the binding needs it. The runtime gate's S stand-down (perWriteSnapshotEntrance) already coversrecord, and it stays.- Pins:
validate-expressions.flow-cel-root.test.ts. Also anypackages/lintfixture that readsrecord.Xin a flow with no record entrance, fixed text-only: give it its entrance, or name the refusal. Census these first. - Text, as triage ordered ("text only, ⛔ no other change to the entry"):
- the pending
.changeset/22565-flow-cel-unbound-root-refused.md. Amending it is the DELIBERATE CORRECTION class:check-empty-changesetstays red until a same-head review confirms the correction; - the landed D3 entry's
reason(packages/spec/src/migrations/entries/semantic/18.flow-cel-unbound-root-refused.ts); registry.ts, regenerated, as long as it is still tracked (see below).
- the pending
.changeset/22677-*.md:@objectstack/lint, at the level the narrowing takes in pre mode, withClause-②: no (narrowing).
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate). It touchespackages/spec/src/**(the D3 entry), narrows whatobjectstack validateaccepts, and corrects a pending note, so the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: no (narrowing)
Responsibility:packages/lintflowCelRootScope(ENGINE_BOUND_ROOTSlistsrecord) | the runtime already refuses: since PR fix(service-automation)!: flow CELrecordis the record the run was handed, or unbound #22674 (automation: with norecordvariable bound, flow CELrecordis the variables map itself, sorecord.KEYsilently reads a variable named KEY #22642)celScopebindsrecordonly for a run handed one, sorecord.Xfaults withUnknown variable: record| every author whose flow readsrecord.Xwith no record entrance runsobjectstack validateclean and faults at run time; the trap is live onmainsince243dd3c625
Thread-read: 6098423647
Serial constraints cleared:- [v18] retire the
{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 pass 4 S1 (PR feat(spec,service-automation)!: a subflow or map input and a script's inputs are value slots — a CEL envelope per key, the {…} token refused (#19939 pass 4, S1) #22715,domain:specseat 2) landed asf66fdc7973, so seat 2's serial note6098423647is discharged. No S2 claim is open at this stamp. - No open PR touches
flow-cel-root-scope.ts,lint-flow-patterns*,validate-expressions*, the automation: a flow CEL expression may name the run user asuser,ctx.useroros.user;objectstack validatepasses it and the run faultsUnknown variable, because flow CEL binds onlycurrent_user#22565 changeset or the D3 entry (all 13 open PRs' file lists, read at 2026-10-10T19:41Z). registry.ts: PRs build(spec): the migration registry is generated at build and leaves git #22706 ([Decision] the migration registryregistry.tsis the last committed generated aggregate on the ADR-0087 D3 path: keep it and finish B′, or generate it at build #22554, seat 1) and feat(spec,core)!: positions declare their permissionSets; the authorization resolver reads the security catalog and the activation ledger #22723 (refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 S1) touch it. build(spec): the migration registry is generated at build and leaves git #22706 takesregistry.tsout of git. Whichever lands first, the other re-syncs throughos-regen-merge.sh. If build(spec): the migration registry is generated at build and leaves git #22706 has landed, the D3reasonedit needs no tracked registry change.- 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 (W) follows this card on the same judge.
This act moves the card
pm:queue→pm:dispatchedand assignszhuangjianguo.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22677,
"status": "done",
"branch": "claude/issue-22677-flow-cel-record-entrance",
"pr": "#22730",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF",
"premise_still_valid": true,
"summary": "Premise held on origin/main f66fdc7: flow-cel-root-scope.ts:139 listed 'record' in ENGINE_BOUND_ROOTS, and the engine binds record only via seedRunVariables' context.record or a flow's own record variable (celScope has no record slot since #22642). Change: 'record' leaves ENGINE_BOUND_ROOTS. flowCelEntrances computes a second set beside 'opened', the flows an entrance hands ANY record, and binds 'record' there. The entrances are: a record trigger (trigger kind record_change, i.e. a record-* triggerType; a start objectName alone hands none, measured on deriveTriggerBinding); a time_relative sweep; an api hook; any type:'flow' action targeting the flow (an object-less one hands an empty record, still a record); a map node with an itemObject; and a subflow/map parent that is handed one, to a fixpoint. A parent's own record VARIABLE is not handed on. A declared record variable binds through collectFlowVariableNames. Everywhere else record.X is refused, with a record-specific message naming the vars remedy, and the generic message's closing clause is corrected. The runtime gate's S stand-down is unchanged, and pinned. previous has no such problem on main: seedRunVariables sets it on every run (context?.previous ?? null), so it stays an engine root, unchanged. Text: the pending #22565 note's 3 record sentences are corrected (DELIBERATE CORRECTION; check-empty-changeset red by design); the D3 reason is corrected (text only); registry.ts is regenerated (still tracked). New .changeset/22677-flow-cel-record-entrance.md: lint major (#22565 precedent, pre mode), Clause-② no (narrowing), adr-0087 not-required (already-registered flow-cel-unbound-root-refused, flow-cel-record-variables-alias-retired), so no new ledger entry is owed. Reach: 0 newly refused across examples, platform-objects and hotcrm.",
"tests": "All at HEAD 8a65474 unless stated. (1) pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: Test Files 135 passed, Tests 6318 passed, VERDICT command-exit 0. Also green at 641f940: 6287 passed. (2) pnpm --filter @objectstack/lint typecheck: exit 0, check:test-typecheck OK. (3) @objectstack/metadata-protocol, after building its closure: Test Files 223 passed / 3 skipped, Tests 28101 passed, exit 0. Measured on the pre-merge tree with this judge in the lint dist, which is byte-identical at HEAD. (4) CLI unit project (scaffold-ci-script-parity, validate-build-gate-parity, generate-scaffold-validates, init-scaffold-authoring-rules, i18n-flow-refusal-coverage): 5 files, 78 passed. CLI integration project (authoring-rule-command-parity, retry-policy-key-validate-door, validate-field-predicate-traversal; they spawn the source CLI, run because the dispatch named the CLI files that validate flows): 3 files, 15 passed. Both pre-merge tree. (5) spec check:migration-registry: registry current. spec check:generated: All 15 up to date, after pnpm --filter @objectstack/spec build. Its first run was PREREQUISITE NOT MET on a stale dist and is not counted. (6) Ablations at 641f940 (fix committed), via node scripts/ablation-replace.mjs on src; the lint tests import ./flow-cel-root-scope.js relatively, so no dist is in the path. A1: 'record' back in ENGINE_BOUND_ROOTS (anchor x1 to x0, blob 1f19fd39e079 to cf1854fa5f7b) gave Tests 5 failed / 72 passed, all refusal-side pins. A2: the record-bound read dropped (names without handed; blob 1f19fd39e079 to b13802b00371) gave 8 failed / 69 passed, including 'control: a record-triggered flow passes' and 'control: a subflow child of a record-handing parent passes'. Each run printed: restored, blob == HEAD (1f19fd39e079), git diff HEAD empty. (7) Reach probe positive control: a no-entrance flow reading record.status, 1 refusal counted.",
"mcp_calls": "0",
"api_writes": "3 relay strokes, each a POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (draft #22730), read back with 11799 bytes sent and 11799 stored, identical; (2) label-write --issue 22730 --assign zhuangjianguo, i.e. POST /repos//issues/22730/assignees, read back as assignees zhuangjianguo, no label written; (3) this os-dev-report via post-stamped, i.e. POST /repos//issues/22677/comments. git push x2 this round (641f940, 8a65474), plus the empty branch push at the start; git push is not a REST write.",
"open_questions": [
{
"question": "The pending #22642 note (.changeset/22642-flow-cel-record-binding.md, last paragraph) says 'objectstack validate does not refuse such a record read yet'. That is false once this lands, in the same 18.0.0-next release. The file is outside this claim's file surface, so it was not touched. The new 22677 changeset's last line says the sentence no longer holds.",
"options": [
"A: authorize a text-only DELIBERATE CORRECTION of that one sentence on this PR (one more amended pending note; check-empty-changeset already red for the same class)",
"B: leave it; the 22677 note in the same release supersedes it"
],
"recommendation": "A. 实际业务需求: both notes land in the same CHANGELOG, and a reader of 18.0.0 meets a direct contradiction about what validate does. 项目长远合理性: a published CHANGELOG is permanent, so a false sentence there is never repaired. 防 AI 写错: an agent reading the notes can take 'validate does not refuse yet' as license to ship record.X on an entrance-less flow. 创业阶段不扩散需求: one sentence, no new surface, and the same review confirms it."
},
{
"question": "The clause 'none of these roots ever evaluated in a flow' is imprecise for record now that record joins the refused set: on 17.x, record.X on an entrance-less flow evaluated against the variables map. It appears in the pending #22565 note's 'The ledger' bullet, in the D3 reason and in the registry STEP18 text. Triage ordered the reason corrected 'text only' for the record-bound sentence, and that is what this PR does.",
"options": [
"A: leave it; the 18 line's ledger is coherent because #22642's own D3 entry flow-cel-record-variables-alias-retired carries the record behaviour change, with its FROM to TO",
"B: amend the clause in all three places, e.g. 'none of these roots but record ever evaluated in a flow, and record's read of the run's variables is retired by its own entry flow-cel-record-variables-alias-retired'"
],
"recommendation": "A. 实际业务需求: the upgrade path for that behaviour is #22642's entry, and nothing here changes it. 项目长远合理性: the two entries together state the full story. 防 AI 写错: the remedy an agent needs comes from the other entry's FROM to TO, not from this rationale clause. 创业阶段不扩散需求: it keeps triage's 'text only, no other change' scope. B is cheap if the contract review wants the clause exact."
}
],
"out_of_scope_findings": [
"carrier: #22636 (W, the same judge, next on this file) · noted, not filed. The 'opened' set misses a record-* trigger with no start config.objectName. The record-change trigger then registers its hook with object undefined (record-change-trigger.ts registerHook(..., { object: binding.object })), so it fires on any object's write and can hand a record whose keys are not in hand, while flowCelEntrances opens only when objectName is named and undeclared. This is a read-only inference with no public-door reproduction. Dedupe words: flowCelEntrances opened, record-change no objectName, wildcard hook, flattened bare field.",
"carrier: 承接者:无 · noted, not filed. A declared record variable counts as binding record at the build door, as every declared variable does (collectFlowVariableNames), while the run binds a declared variable only with a supplied input or a defaultValue (seedDeclaredVariables). This is the judge-wide #22565 design, not specific to record; the ruling names a declared record variable as binding."
],
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at 8a65474 derived 91 commands; 89 were run. 88 exit 0. check-empty-changeset --base origin/main exit 1: the DELIBERATE CORRECTION of .changeset/22565-flow-cel-unbound-root-refused.md, red by design and named in the PR body. NOT MEASURED by order: check:dual-build-cjs-loads (whole-workspace build) and check:type-check-debt (--re-measure). --ran reconcile: 91 derived, 89 run, 0 NOT-MEASURED, 2 UNRUN (those two). Artifact rosters: 46 run without PR context, 45 exit 0. check:published-readme-exports exit 3 is PREREQUISITE NOT MET (7 unbuilt packages, none touched), so NOT MEASURED. The 3 PR-context guards were run against PR 22730 (PR_NUMBER, PR_HEAD_REF, PR_BODY, GITHUB_REPOSITORY, GITHUB_TOKEN from gh auth token): check-closing-target-claim exit 0, check-partof-closing-keyword exit 0, check-single-claim-paths exit 0. CI is in_progress and was not waited on.",
"line_budget": "8 files, +269 / -47 = 316 changed lines vs the 3000 human-merge threshold: under. No skills/** or .claude/** paths.",
"deviations": [
"The ruling's 'a trigger object' is read as a record trigger (trigger kind record_change, a record-* triggerType), as measured on deriveTriggerBinding: a start config.objectName alone hands no record. This is why 9 lint fixtures got triggerType: 'record-after-update' and why the objectName-alone refusal is pinned.",
"The PR body was written once and has no '## Acceptance notes' section. The seat may add: 'Acceptance notes: (1) opened-set gap for a record-* trigger with no objectName, carrier #22636; (2) a declared record variable binds at the build door as every declared variable does; (3) the #22642 note sentence and the never-evaluated clause, see open_questions.'",
"The metadata-protocol suite and the CLI files ran on the pre-merge tree. The merge of origin/main 762db99 touched neither the judge nor those packages' flow paths, and the judge is byte-identical at HEAD. The lint suite, typecheck, spec checks and all gates ran at HEAD.",
"The CLI closure build ran in the background under the verify lock (held 379s) while I edited test files outside the lock; it was waited on with tail --pid.",
"hotcrm was measured at 1d7148b, its HEAD today; the #22565 note cites f0afcbda07. The clone was read-only, sparse and deleted after the reach run.",
"Worktree cleanup (rm -rf node_modules, then git worktree remove) runs after this comment is posted; the tree is clean and pushed (0 uncommitted paths)."
],
"files_changed": [
"packages/lint/src/flow-cel-root-scope.ts",
"packages/lint/src/validate-expressions.flow-cel-root.test.ts",
"packages/lint/src/validate-expressions.test.ts",
"packages/lint/src/lint-flow-patterns.test.ts",
".changeset/22565-flow-cel-unbound-root-refused.md",
".changeset/22677-flow-cel-record-entrance.md",
"packages/spec/src/migrations/entries/semantic/18.flow-cel-unbound-root-refused.ts",
"packages/spec/src/migrations/registry.ts"
],
"fixture_census": "Taken with only the judge change applied, before any fixture edit. Lint: 34 failed in 3 files. validate-expressions.test.ts has 29 tests over 9 fixtures, each a start node with objectName and no record-* trigger, at :47, :81, :478, :2760 (flowWith region helper), :3125, :4031 (withCondition, 15 tests), :4456 (flowWith #15662, 4), :4623 (flowWith #17495, 2) and :4754 (stackWith #15742, 2). lint-flow-patterns.test.ts:2787 valueFlow has 2 tests: an autolaunched flow with no entrance whose prescription reads record.title. validate-expressions.flow-cel-root.test.ts has 3 of #22565's own pins: the engine-roots control, the CEL type-name control (type(record)) and the generic message text. Metadata-protocol: 0 (the runtime gate stands down). CLI: 0 across 8 files. Fixes are text only: give each fixture its record trigger. valueFlow also declares task (title, subject) so the judgment stays live, and the #22565 pins moved record to a record-triggered control.",
"reach": "Real judge: validateStackExpressions plus flowCelEntrances from the worktree src. The positive control counted 1 refusal. examples/** (app-crm 1, app-todo 4, app-multi-package 0, app-showcase 30 = 35 flows): 14 read record (app-todo task_completion is record_change; showcase has 11 record_change, showcase_task_due_reminder is time_relative and showcase_inbound_task_webhook is api, also opened), all handed a record, 0 newly refused. packages/platform-objects: 0 flows by source scan, so 0. hotcrm 1d7148b (serviceStack 7 and appStack 25 = 32 flows): 14 read record, all record_change and handed, 0 newly refused. No shipped example refuses, and none was changed."
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsSeat order: one patch round on PR #22730 before its contract review. The dev's open question 1 is answered A, and question 2 is answered A
domain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T20:50Z · holder of claim6101461546. Thread-read: the dev's report on this card.Read in the diff, against the report (net diff against the merge base
762db996ad: 8 files, +269 / −47):- The removal:
'record'leavesENGINE_BOUND_ROOTS(nowprevious,vars,current_user). - A
handedset, besideopened, bindsrecord:- for a
record_change,time_relativeorapitrigger kind; - for a
type: 'flow'action's target; - for a
mapchild with anitemObject; - to a fixpoint, for a
subflow/mapchild of a handed parent.
- for a
- The refusal: a
record-specific message names thevarsremedy, and the generic message's closing clause is corrected. - The runtime gate's S stand-down is unchanged, and pinned.
- The reach: 0 newly refused flows, over examples (35 flows, 14 reading
record, all handed one),platform-objects(0 flows) and hotcrm1d7148b(32 flows, 14 readingrecord, all handed one).
Question 1: A, amend the #22642 note's last sentence in this PR.
.changeset/22642-flow-cel-record-binding.mdis pending in the same release, and it says "objectstack validatedoes not refuse such arecordread yet; the run fails it". Once this PR lands that is false. A published CHANGELOG line is never repaired.- This is the DELIBERATE CORRECTION class, like this PR's correction of the automation: a flow CEL expression may name the run user as
user,ctx.useroros.user;objectstack validatepasses it and the run faultsUnknown variable, because flow CEL binds onlycurrent_user#22565 note.check-empty-changesetis already red by design. - The change: replace that one sentence with one that is true at this head. For example: "
objectstack validaterefuses such arecordread too, naming the root (see@objectstack/lint's note)." Change nothing else in that file. The PR body names both corrected notes and quotes each corrected sentence before and after, so the contract review can confirm them.
Question 2: A, leave "none of these roots ever evaluated in a flow" as it is.
- Triage scoped the D3 entry's correction as "text only, ⛔ no other change". automation: with no
recordvariable bound, flow CELrecordis the variables map itself, sorecord.KEYsilently reads a variable named KEY #22642's own entry,flow-cel-record-variables-alias-retired, carriesrecord's behaviour change with its FROM → TO. The contract review may still ask for B.
Also in this round: the PR body gains a
## Acceptance notessection with:- The
opened-set gap: arecord-*trigger with no startconfig.objectNameregisters a hook on any object's write, so it can hand a record whose keys are not in hand, but the flow is not opened. It is a read-only inference, and its carrier is 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. The seat notes it there at landing. - A declared
recordvariable binds at the build door, as every declared variable does (collectFlowVariableNames). That is automation: a flow CEL expression may name the run user asuser,ctx.useroros.user;objectstack validatepasses it and the run faultsUnknown variable, because flow CEL binds onlycurrent_user#22565's judge-wide design. - Question 2's answer.
Verify:
node scripts/check-empty-changeset.mjs --base origin/main: red, naming exactly the two corrected notes;check-changeset-no-majorandcheck-changeset-fixed;check:nul-bytes.
No code changes, so no suite re-run is owed. No merge of
mainunless GitHub reports a conflict. Push, refresh the PR body through the relay, and report the new head andgit diff --name-only 8a65474075 NEW-HEAD. The contract review atCONTRACT_REVIEW_TIERfollows on that head.
Generated by Claude Code
- The removal:
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22677,
"status": "done",
"branch": "claude/issue-22677-flow-cel-record-entrance",
"pr": "#22730",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF",
"premise_still_valid": true,
"summary": "Patch round per seat order 6102049574 (Q1 answered A, Q2 answered A). New head 0e51b65, one commit on the remote head 8a65474 in a worktree recreated there. git diff --name-only 8a65474 0e51b65 gives .changeset/22642-flow-cel-record-binding.md and .changeset/22677-flow-cel-record-entrance.md. In the #22642 note, only the sentence 'objectstack validate does not refuse such a record read yet; the run fails it, naming the root and the source.' changed, to the seat's text 'objectstack validate refuses such a record read too, naming the root (see @objectstack/lint's note).' (a DELIBERATE CORRECTION; nothing else in that file). In this PR's own 22677 note, the closing line said the #22642 sentence 'does not refuse yet' no longer held; once that sentence was corrected it pointed at text that is gone, so it is dropped. Q2: the 'never evaluated' clause is left as it is. The PR body was refreshed through the relay. It names both corrected notes and quotes each corrected sentence before and after. It gains '## Acceptance notes' with the 3 items (the opened-set gap, carrier #22636; a declared record variable binds as every declared variable does; Q2 answered A), plus the patch-round verification. No code change, no suite re-run, no main merge: GitHub reports mergeable true.",
"tests": "On 0e51b65. node scripts/check-empty-changeset.mjs --base origin/main: exit 1, red by design. Its ::error lines name exactly .changeset/22565-flow-cel-unbound-root-refused.md and .changeset/22642-flow-cel-record-binding.md. node scripts/check-changeset-no-major.mjs --base origin/main: exit 0; the major for @objectstack/lint is exempt in pre mode. node scripts/check-changeset-fixed.mjs: exit 0, the fixed group is in sync with 69 public packages. pnpm check:nul-bytes: exit 0, 10737 tracked text files with no raw control bytes. A control-byte self-scan of the two edited notes found 0 hits. The PR-context guards, re-run against the refreshed body (PR_NUMBER 22730, PR_HEAD_REF, PR_BODY, GITHUB_REPOSITORY, GITHUB_TOKEN from gh auth token): check-closing-target-claim exit 0, check-partof-closing-keyword exit 0, check-single-claim-paths exit 0. Suites were not re-run (no code change, per the seat order); the round-1 results at 8a65474 stand.",
"mcp_calls": "0",
"api_writes": "This round, 2 relay strokes, each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) issue_patch on #22730, i.e. PATCH /repos//issues/22730 (the PR body), read back with 14026 bytes sent and 14026 stored, identical; (2) this os-dev-report via post-stamped, i.e. POST /repos//issues/22677/comments. Plus git push x1 (8a65474 to 0e51b65), which is not a REST write. No label or assignee write.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #22636 · noted, not filed. The opened-set gap for a record-* trigger with no start config.objectName is now recorded in the PR's Acceptance notes item 1 (unchanged from round 1).",
"carrier: 承接者:无 · noted, not filed. A declared record variable binds as every declared variable does; recorded in Acceptance notes item 2 (unchanged from round 1)."
],
"gates": "Patch-round gates as ordered: check-empty-changeset red by design (it names exactly the two corrected notes); check-changeset-no-major 0; check-changeset-fixed 0; check:nul-bytes 0. The 3 PR-context guards were re-run against the refreshed body: all 0. GitHub: mergeable true, mergeable_state blocked (checks and review pending). CI was not waited on.",
"line_budget": "This round: 2 files, +1 / -3. Net PR against merge base 762db99: 9 files, +268 / -48 = 316 changed lines, under the 3000 threshold.",
"deviations": [
"Beyond the one ordered sentence, this PR's own .changeset/22677-flow-cel-record-entrance.md lost its closing line. The line pointed at the #22642 sentence this round corrected, so keeping it would have made the 22677 note false. The file is added by this PR (not a pending-note correction) and is within the claim's file surface.",
"The PR-context guards were re-run because the PR body changed; the order did not list them.",
"Worktree cleanup (git worktree remove; no install was done this round) runs after this comment is posted. The tree is clean and pushed."
],
"files_changed": [
".changeset/22642-flow-cel-record-binding.md",
".changeset/22677-flow-cel-record-entrance.md"
],
"new_head": "0e51b65d0952e5e1be69a5bd05f215722e2648c9",
"diff_name_only_8a65474075_to_new_head": [
".changeset/22642-flow-cel-record-binding.md",
".changeset/22677-flow-cel-record-entrance.md"
]
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actions✅ ACCEPT — PR #22730 at
0e51b65d09. Next: the contract review on this headdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T20:55Z · holder of claim6101461546. Thread-read: the dev's patch-round report on this card.Round 1, read in the diff (seat order
6102049574records it):'record'leavesENGINE_BOUND_ROOTS.flowCelEntrancescomputes ahandedset besideopened, and bindsrecordthere. The set covers:- a
record_change,time_relativeorapitrigger kind; - a
type: 'flow'action's target; - a
mapchild with anitemObject; - by fixpoint, a
subflow/mapchild of a handed parent.
- a
- A
record-specific refusal names thevarsremedy. - The runtime gate's S stand-down is unchanged, and pinned.
previousstays an engine root:seedRunVariablessets it on every run, as measured.
The patch round (
git diff 8a65474075 0e51b65d09: two changeset files, +1 / −3, no code):- The automation: with no
recordvariable bound, flow CELrecordis the variables map itself, sorecord.KEYsilently reads a variable named KEY #22642 note: its one sentence "objectstack validatedoes not refuse such arecordread yet; the run fails it, naming the root and the source." now reads "objectstack validaterefuses such arecordread too, naming the root (see@objectstack/lint's note)." Nothing else in that file moved. It is the DELIBERATE CORRECTION class, question 1 answered A. - This PR's own 22677 note drops its closing line, which pointed at the sentence just corrected and would otherwise be false. The file is this PR's own, so this is accepted.
- The PR body names both corrected notes, quotes each sentence before and after, and carries
## Acceptance notes(theopened-set gap → 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; a declaredrecordvariable; question 2 answered A).
Measurements accepted (round 1, at
8a65474075; the patch round moved no code):- Suites:
- lint 135 files / 6318 tests, with the typecheck green;
- metadata-protocol 28101 tests;
- the CLI unit (78) and integration (15) flow files;
- spec
check:migration-registryandcheck:generatedcurrent.
- Ablations, each blob-equal restored:
'record'back inENGINE_BOUND_ROOTS: 5 refusal pins red;- the
handedread dropped: 8 red, including the record-trigger andsubflow-child controls.
- The fixture census: 34 lint failures in 3 files before any fixture edit, each given its record trigger, text only.
- The reach: 0 newly refused over examples (35 flows, 14 reading
record, all handed one),platform-objects(0) and hotcrm1d7148b(32 / 14, all handed). - Gates: 91 derived, 89 run, 88 exit 0.
check-empty-changesetis red by design, naming exactly the two corrected notes.- NOT MEASURED, as ordered:
dual-build-cjs-loadsandcheck:type-check-debt.
Deviation accepted: "a trigger object" is read as a record trigger, as measured on
deriveTriggerBinding: a startconfig.objectNamealone hands no record. Nine lint fixtures got arecord-*trigger, and theobjectName-alone refusal is pinned. The contract review judges this reading against triage's direction6097216432.Out-of-scope findings:
- The
opened-set gap: arecord-*trigger with noobjectNamehooks every object's write. Its carrier is 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, and the seat notes it there at landing. - A declared
recordvariable binds at the build door, as every declared variable does. This is automation: a flow CEL expression may name the run user asuser,ctx.useroros.user;objectstack validatepasses it and the run faultsUnknown variable, because flow CEL binds onlycurrent_user#22565's judge-wide design. Acceptance note.
A contract review at
CONTRACT_REVIEW_TIERon0e51b65d09follows. It confirms both corrected notes, the class's confirmation.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded: PR #22730 →
0f77ff5202(Fixes #22677). The card is closed as completeddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T21:52Z · holder of claim6101461546.- Landed: through the merge queue at 2026-10-10T21:51Z as
0f77ff5202, a squash with one parent,a360cee92e. The queue did not eject it. - The review chain: the patch order
6102049574, the ACCEPT6102086025, the contract review PASS6102167316at0e51b65d09and the pre-queue record6102331175.Check Changesetwas red by design: the DELIBERATE CORRECTION class, confirmed in6102167316. - Content check: all 9 PR paths on
0f77ff5202are blob-equal to the reviewed head0e51b65d09. - What now holds:
-
The judge:
objectstack validatebinds a flow'srecordonly where an entrance hands the run one. That covers:- a record-change, time-relative or inbound-hook trigger;
- a
type: 'flow'action; - a
mapwith anitemObject; - a
subflow/mapchild of a handed parent.
It also binds
recordwhere the flow declares arecordvariable. Elsewhererecord.Xis refused, naming thevarsremedy. This matches the run since PR fix(service-automation)!: flow CELrecordis the record the run was handed, or unbound #22674. -
The runtime gate's stand-down is unchanged, 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.
-
The pending notes: automation: a flow CEL expression may name the run user as
user,ctx.useroros.user;objectstack validatepasses it and the run faultsUnknown variable, because flow CEL binds onlycurrent_user#22565's threerecordsentences and automation: with norecordvariable bound, flow CELrecordis the variables map itself, sorecord.KEYsilently reads a variable named KEY #22642's "does not refuse … yet" sentence are corrected before release. The D3 entry'sreasonis corrected.
-
- Carried:
- To 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 (noted there in this act): the
opened-set gap. Arecord-*trigger with no startconfig.objectNamehooks every object's write, but the flow is not opened. - Next on this axis: spec(automation): the
$namespace at every binding door: loop and mapiteratorVariable/indexVariable, a screen'sidVariable, a declared flow variable'snameand anassignmenttarget still bind a$name a text slot refuses to read #22572, then [v18] retire the{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 pass 4 S2, as the S1 landing record6101473453orders.
- To 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 (noted there in this act): the
- Mis-close scan: the squash message carries
Fixes #22677alone, and the merge closed lint(flow CEL roots):recordis still inENGINE_BOUND_ROOTS, soobjectstack validatepasses arecord.Xread in a flow with no record entrance, which faults at run time once #22642 lands; and two texts describerecordas always bound #22677 alone.
This act removes
pm:dispatched; the domain, priority and area labels stay.
Generated by Claude Code
- Landed: through the merge queue at 2026-10-10T21:51Z as
Filing class: ② a lint-side contract disagreement, opened by a narrowing that is about to land.
recordis the record the run was handed, or unbound #22674's contract review (6097092807, boundary flag 2). That review made filing this card a condition on queueing the PR.recordvariable bound, flow CELrecordis the variables map itself, sorecord.KEYsilently reads a variable named KEY #22642's dev in the round-1 report, underout_of_scope_findings.domain:servicesseat 1 (seat post [PM seat] domain:services — ⏳ vacant #6021,session_013j5gkUCpqQiti4GgPqqmnt). This seat is the carrier triage named on automation: with norecordvariable bound, flow CELrecordis the variables map itself, sorecord.KEYsilently reads a variable named KEY #22642 (6095881854: "a follow-up on the lint side, which the claimant files when this lands").Reader: objectstack triage, for the lane.
packages/lintand.changeset/belong to the spec lane (its sibling is #22565 / PR #22609), and the D3 entry is inpackages/spec/src/migrations/.What changes when PR #22674 lands
Read on
origin/maina800912648and on the PR heade4977aa6d2.AutomationEngine.celScopestops bindingrecordto the variables map.recordis then bound only in two cases:type: 'flow'action, asubflowparent or amapitem;recordvariable.record.Xfaults withUnknown variable: record.packages/lint/src/flow-cel-root-scope.tslistsrecordinENGINE_BOUND_ROOTS(about:139), the always-bound set. Its header describes the retired alias as current. Soobjectstack validateand the runtime publish gate pass arecord.Xread in a flow with no record entrance, and that read then faults at run time..changeset/22565-flow-cel-unbound-root-refused.mdsaysrecordis bound by the engine. It publishes into the CHANGELOG at the next release unless it is amended while still pending.reasonof the landed D3 entryflow-cel-unbound-root-refusedsays the same thing, and it renders into the upgrade guide.Ask
recordfromENGINE_BOUND_ROOTSto the entrance-derived set.flowCelEntrancesalready reads every entrance.subflowor amapcould hand it a record. The header's stand-down case and 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 runtime gate's per-write snapshot cannot see those entrances) are the neighbouring cases..changeset/22565-…while it is still pending, and correct the D3 entry'sreason. Both are text-only.validaterefusesrecord.Xin a flow with no record entrance. Control: a record-triggered flow passes. Each negative pin is ablation-verified.recordis the record the run was handed, or unbound #22674 lands; that PR is the runtime half.Dedupe: MCP
search_issues, this repo,flow CEL record entrance-derived ENGINE_BOUND_ROOTS validate passes record.X with no record entrancereturns:recordvariable bound, flow CELrecordis the variables map itself, sorecord.KEYsilently reads a variable named KEY #22642, the runtime half;None of them is this follow-up.
Generated by Claude Code