Skip to content

Commit 5aa7f55

Browse files
fix(app-shell): a record-triggered Start node is judged against the scope the engine binds, with one verdict (objectui#11789) (#11849)
Fixes #11789 Clause-②: no ## What was wrong, measured on `main` (`87f7b6c`) A record-triggered flow's Start node read **"Valid CEL"** and, right under it, **"`record` is not a reference in scope at this step."** for the same entry condition. Two defects were behind that one screen. 1. **The Start node's scope had no `record`.** `resolveFlowScope` in `flow-scope.ts` pushed the whole-record `record` ref only `if (!onStart)`. The engine binds it there. `seedRunVariables` in `@objectstack/service-automation` sets `record`, `$record`, the record's flattened fields and `previous`, and the start-condition gate then evaluates against that same variables map. Every shipped spelling resolves at run time: `record.status` and bare `status`. 2. **Two verdicts for one expression.** The raw CEL editor (`CelPredicateField`, mounted by `ConditionBuilder`) says "Valid CEL" from its lint. Its lint knows the CEL scope roots, not the flow's scope at the node. The scope note comes from somewhere else, so a root the lint accepts and the flow scope rejects got both lines. This PR's new pins were first run with the source unchanged: **3 failed, 3 passed (6)**. The card's expression failed on the scope note. The `trigger.status` pin and the editor pin failed on "Valid CEL" shown beside the note. The 3 that passed are the controls. ## The change ### `record` is in scope on a record-triggered Start node In `flow-scope.ts`, the `record` ref is pushed at every node of a record-triggered flow, the Start node included. What still differs on the Start node is the per-field prefix only: bare there, `record.` downstream. A schedule, manual or API Start node gains nothing. The existing record-trigger gate (`RECORD_TRIGGER_TYPES` plus an `objectName`) is untouched. `flow-ref-check.ts` is **not** touched. Adding `record` / `previous` to its `RUNTIME_GLOBALS` would accept them on schedule and manual flows, where the engine binds no record. ### `previous` follows the engine's pre-image The engine binds `previous` on **every** run: to the pre-image the record-change trigger hands it, or to `null` when there is none. So Studio scopes it where a pre-image exists. | Trigger | `record` | `previous` | What the engine binds | |---|---|---|---| | `record-after-update`, `record-before-update` | in scope | in scope | the prior row | | `record-after-write`, `record-before-write` | in scope | in scope | `null` on the create leg, the prior row on the update leg (`previous == null` picks the create leg) | | `record-after-delete` | in scope | **in scope (new)** | the deleted row: the data engine binds the pre-image before the delete runs, and the trigger reads `record` from it too | | `record-after-create` | in scope | **out (unchanged)** | always `null`: there is no prior row | | `schedule`, `manual`, `api` | out | out | no record is handed to the run | **Why create stays out** (the seat's answer A to this PR's open question): a member read on `null`, such as `previous.status`, is a CEL evaluation error. The engine's `evaluateCondition` throws on it, so the run fails. And `previous == null` is constantly true there. Showing the scope note is the useful verdict. The table is pinned row by row in `flow-scope.test.ts`, at the Start node and downstream. ### One verdict: the scope note replaces "Valid CEL" **Zone 2 #3's location is falsified.** The scope line under the entry condition is not `FlowExprIssue`'s. It is `FlowNodeConfigField`'s own `describeUnknownRefs` note, rendered under the control it mounts. `FlowExprIssue` never renders beside a `CelPredicateField`: only the Start node's `condition` descriptor opts into `conditionBuilder`. So the rule lands where both verdicts render: - `FlowNodeConfigField` computes its scope verdict before it builds the control, and hands `scopeIssue` to the `ConditionBuilder` it mounts; - `ConditionBuilder` forwards the new optional `scopeIssue` to its raw editor; - `CelPredicateField` withholds "Valid CEL" when `scopeIssue` is set. The lint, its findings and `onLintChange` are unchanged. With no `scopeIssue`, nothing changes. That covers the permission set's row-level security clauses, pinned as a control. The note's wording is unchanged and no catalogue row was added. `FlowNodeConfigField`'s `FLOW_TRIGGER_CONTEXT_SUBJECTS` doc comment said `flow-scope.ts` withholds `record` on the Start node, which this change makes false, so it was reworded. The builder still offers only the bare spelling, because `record.FIELD` is the same value. Two sibling test files carried the same false reason in a test name and a comment, and were reworded the same way. No assertion changed. ### Side effect: an edge leaving the Start node An edge's guard is judged against the scope at its source (`resolveEdgeScope` / `useEdgeScope`). So an edge leaving the Start node now accepts `record` too. That matches the engine: `traverseNext` evaluates those guards against the same run variables. The Problems panel's expression scan skips the Start node and its out-edges (`flowExpressionProblems`, by design: there the trigger fields are not expanded), so its output does not move. ## Verification All at `b097f9a` (this branch merged with `main` `455c646`) unless stated. - `pnpm --filter @object-ui/app-shell type-check` (echoed `tsc --noEmit && tsc -p tsconfig.test.json`): **exit 0**. The dependency closure was rebuilt first with `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build`, exit 0. `--listFiles` on `tsconfig.test.json` lists the new pin file. - The targeted set (the new pin file, `CelPredicateField.test.tsx`, `flow-scope.test.ts`, both `entryCondition` suites, the `ConditionBuilder.*` suites): `Test Files 14 passed (14)`, `Tests 227 passed (227)`, exit 0. - At `1bbfc4b` (before the merge of `main`): `pnpm exec vitest run packages/app-shell/` gave `Test Files 1078 passed | 1 skipped (1079)`, `Tests 10616 passed | 9 skipped (10625)`, exit 0. `main`'s app-shell changes since then (objectui#11783, objectui#11811) touch none of these files. The package-wide run on the merged head is CI's. - `pnpm exec eslint` on the 8 touched source and test files: 0 errors, 8 warnings, all on lines outside this diff. - `pnpm check:control-bytes`, `check:test-path-roots`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:new-line-citations`, plus `scripts/check-changeset-presence.mjs` and `scripts/check-changeset-no-major.mjs`: all exit 0 on `b097f9a`. The i18n gates are not applicable: no locale pack or catalogue row changed. **Ablation**, run at `1bbfc4b` through `ablation-replace` (the anchor must hit, and the restore is proven against HEAD). The tests import the subjects by relative source path, so there is no `dist` leg. - **scope**: put `if (!onStart)` back on the `record` push in `flow-scope.ts`. Anchor 1 → 0, blob `3180f147cab2` → `65c6870b91c0`. Result **8 failed / 51 passed**: the card's pin, the Start-node pin and the 6 record-trigger rows of the table. Restored: blob == HEAD `3180f147cab2`, `git diff HEAD` empty. - **one verdict**: changed `issues.length === 0 && !scopeIssue;` to `issues.length === 0;` in `CelPredicateField.tsx`. Blob `51ef56396d7d` → `e2187dea09eb`. Result **2 failed / 57 passed**: the `trigger.status` pin and the editor-contract pin. Restored: blob == HEAD `51ef56396d7d`, diff empty. Both went red, as predicted. **Clause-② (no)**, measured on the built package. A transitive walk of `dist/index.d.ts`'s relative imports reaches 172 declaration files. None of `CelPredicateField`, `ConditionBuilder`, `FlowNodeConfigField`, `flow-scope`, `flow-ref-check`, `FlowExprIssue` or `useFlowScope` is among them. The positive control `DirectoryPage.d.ts` is reached. `scopeIssue` is in the emitted `CelPredicateField.d.ts` and `ConditionBuilder.d.ts`, and in none of the reachable files. `exports` declares only `.` and `./styles.css`. ## Overlap Per the seat's claim amendment, objectui#11788 may edit `FlowNodeConfigField.tsx` in another region (the notify Recipients and field-mapping rows). This PR's edit there is the entry condition's scope verdict, the `scopeIssue` handed to its `ConditionBuilder`, and the `FLOW_TRIGGER_CONTEXT_SUBJECTS` comment. Whoever lands second merges `main`. ## Acceptance notes Noted, not filed. Neither one gives a wrong verdict at a public door today. - **`time_relative` Start nodes get no trigger scope.** The engine's time-relative sweep hands each matched row to the run as `record`. `flow-scope.ts` gives `time_relative` no trigger scope: the type is not in `RECORD_TRIGGER_TYPES`, and its object is at `config.timeRelative.object`, not `config.objectName`. The effect is silent today. With no declared variable, the ref check has no roots and says nothing. The shipped producer (app-showcase's `showcase_task_due_reminder`, with `{record.title}` templates) declares none. A flow that also declared a variable would see `record` flagged. Carrier: none. - **Four record-trigger tokens are not in Studio's set.** The record-change trigger accepts `record-(before|after)-(create|insert|update|delete|write)`, and `RECORD_TRIGGER_TYPES` lists 6 of those 10 tokens. A grep over objectstack `main` (`15ec50e5`) examples and package sources finds no producer of the other four (before-create, before-insert, after-insert, before-delete). The control, the same grep for `record-after-update`, hits 15 times in 3 example files. Studio's trigger select does not offer them either. Carrier: none. Changeset: `.changeset/11789-cel-scope-verdict.md`, `patch` on `@object-ui/app-shell`. Implemented by the os-dev run under session `https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8`, dispatched by the `domain:ui` seat 3 claim on the card. --- _Generated by [Claude Code](https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4a9fe31 commit 5aa7f55

9 files changed

Lines changed: 354 additions & 56 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
Studio's flow designer judges a record-triggered flow's Entry condition against the scope the engine binds, and shows one verdict per expression (objectui#11789).
6+
7+
- **`record` is in scope on the Start node.** The engine binds the whole `record` beside the record's flattened fields before it evaluates the start condition, so `record.status == 'done' && previous.status != 'done'` is as valid as `status == 'done' && previous.status != 'done'`. The Start node used to leave `record` out, and the editor showed "Valid CEL" together with "`record` is not a reference in scope at this step." An edge leaving the Start node reads the same scope, so its guard accepts `record` too.
8+
- **`previous` follows the pre-image.** It is in scope on update, create-or-update and, new here, delete triggers, where the engine binds the deleted row as `previous`. It stays out on a create trigger, where the engine binds it only as `null`, so a member read of it fails when the flow runs. A schedule, manual or API flow gains neither `record` nor `previous`.
9+
- **One verdict.** When the Entry condition names a reference that is not in scope at the node, the scope note replaces "Valid CEL" in the raw CEL editor instead of appearing below it. Other surfaces of that editor, the permission set's row-level security clauses among them, are unchanged.
10+
11+
**Clause-②: no.** No export, exported type or language-pack key changes. The editor's new optional input is internal to the package and is not reachable from its entry.
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* **One expression, one verdict — judged against the scope the engine binds**
5+
* (objectui#11789).
6+
*
7+
* A record-triggered flow's Start node read "Valid CEL" and, directly below it,
8+
* "`record` is not a reference in scope at this step." for the same entry
9+
* condition. Two defects shared that one screen:
10+
*
11+
* 1. the scope Studio judged the Start node against had no `record`, while the
12+
* engine binds it there: `seedRunVariables` seeds `record`, `$record`, the
13+
* record's own fields and `previous` before the start-condition gate runs;
14+
* 2. when a root really is out of scope, the raw CEL editor still said "Valid
15+
* CEL" above the scope note, because its lint only knows the CEL scope roots.
16+
* The scope verdict replaces the syntax verdict, so the panel says one thing.
17+
*
18+
* Measured through the WHOLE inspector, never a hand-built scope: a draft goes
19+
* in, `useFlowScope` resolves it, and `FlowNodeConfigField` → `ConditionBuilder`
20+
* → `CelPredicateField` render what an author sees. The per-trigger scope table
21+
* itself is pinned in `inspectors/flow-scope.test.ts`.
22+
*/
23+
24+
import type { ComponentProps } from 'react';
25+
import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest';
26+
import { render, screen, cleanup, act, waitFor } from '@testing-library/react';
27+
import userEvent from '@testing-library/user-event';
28+
29+
// Same network stubs as the sibling FlowNodeInspector suites: the engine
30+
// config-schema hook (the inspector then uses its hardcoded field groups), the
31+
// trigger object's field catalog, and the shared metadata client.
32+
vi.mock('./previews/useFlowNodePalette', () => ({
33+
useActionConfigSchemas: () => ({}),
34+
useFlowNodePalette: () => [],
35+
}));
36+
const FIELDS = vi.hoisted(() => [
37+
{ name: 'status', label: 'Status', type: 'text', hidden: false },
38+
{ name: 'amount', label: 'Amount', type: 'number', hidden: false },
39+
]);
40+
vi.mock('./previews/useObjectFields', () => ({
41+
useObjectFields: () => ({ fields: FIELDS, loading: false, error: null }),
42+
}));
43+
const state = vi.hoisted(() => ({
44+
metadataClient: { get: vi.fn(async () => undefined), list: vi.fn(async () => [] as unknown[]) },
45+
}));
46+
vi.mock('./useMetadata', () => ({
47+
useMetadataClient: () => state.metadataClient,
48+
}));
49+
50+
/**
51+
* The real CEL lint, observed. Every result the raw editor receives is kept, so
52+
* a negative assertion below reads the editor AFTER its lint answered (and can
53+
* show the lint answered "clean"), never a screen caught before the debounce.
54+
*/
55+
const lint = vi.hoisted(() => ({ results: [] as Array<Promise<unknown[]>> }));
56+
vi.mock('./celAuthoring', async (importOriginal) => {
57+
const real = await importOriginal<typeof import('./celAuthoring')>();
58+
return {
59+
...real,
60+
lintCelPredicate: (...args: Parameters<typeof real.lintCelPredicate>) => {
61+
const p = real.lintCelPredicate(...args);
62+
lint.results.push(p);
63+
return p;
64+
},
65+
};
66+
});
67+
68+
import { FlowNodeInspector } from './inspectors/FlowNodeInspector';
69+
import { CelPredicateField } from './CelPredicateField';
70+
import { PermissionAdvancedFacets } from './PermissionAdvancedFacets';
71+
72+
afterEach(() => {
73+
cleanup();
74+
lint.results.length = 0;
75+
});
76+
77+
beforeAll(() => {
78+
for (const m of ['hasPointerCapture', 'setPointerCapture', 'releasePointerCapture'] as const) {
79+
if (!Element.prototype[m]) {
80+
// @ts-expect-error test shim
81+
Element.prototype[m] = m === 'hasPointerCapture' ? () => false : () => {};
82+
}
83+
}
84+
});
85+
86+
const VALID = 'Valid CEL';
87+
const SCOPE_NOTE = /is not a reference in scope at this step|Not in scope:/;
88+
89+
function renderStartNode(config: Record<string, unknown>, variables?: unknown[]) {
90+
const draft = {
91+
...(variables ? { variables } : {}),
92+
nodes: [{ id: 'start', type: 'start', label: 'When a lead changes', config }],
93+
edges: [],
94+
};
95+
return render(
96+
<FlowNodeInspector
97+
type="flow"
98+
name="lead_followup"
99+
draft={draft}
100+
selection={{ kind: 'node', id: 'start' }}
101+
onPatch={vi.fn()}
102+
onClearSelection={vi.fn()}
103+
readOnly={false}
104+
locale="en-US"
105+
/>,
106+
);
107+
}
108+
109+
/** Switch the entry condition's row builder to its raw CEL editor — the
110+
* editor the card was read off. */
111+
async function openRawEditor(container: HTMLElement) {
112+
const toggle = Array.from(container.querySelectorAll('button')).find((b) => b.textContent?.includes('Expression'));
113+
expect(toggle, 'the entry condition opens in row mode with an Expression toggle').toBeTruthy();
114+
await userEvent.click(toggle!);
115+
}
116+
117+
/** Wait until the raw editor's lint has answered, and hand back its last answer. */
118+
async function settledLint(): Promise<unknown[]> {
119+
await waitFor(() => expect(lint.results.length).toBeGreaterThan(0), { timeout: 3000 });
120+
const all = await Promise.all(lint.results);
121+
await act(async () => {});
122+
return all[all.length - 1];
123+
}
124+
125+
const UPDATE_TRIGGER = { triggerType: 'record-after-update', objectName: 'crm_lead' };
126+
127+
describe('objectui#11789 a record-triggered Start node is judged against the scope the engine binds', () => {
128+
it("`record.status == 'done' && previous.status != 'done'` on an update trigger reads only \"Valid CEL\"", async () => {
129+
const { container } = renderStartNode({
130+
...UPDATE_TRIGGER,
131+
condition: "record.status == 'done' && previous.status != 'done'",
132+
});
133+
// Row mode first: no scope note under the rows either.
134+
expect(screen.queryByText(SCOPE_NOTE)).toBeNull();
135+
await openRawEditor(container);
136+
expect(await screen.findByText(VALID, {}, { timeout: 3000 })).toBeInTheDocument();
137+
expect(screen.queryByText(SCOPE_NOTE)).toBeNull();
138+
});
139+
140+
it("control: the bare spelling `status == 'done' && previous.status != 'done'` is unchanged", async () => {
141+
const { container } = renderStartNode({
142+
...UPDATE_TRIGGER,
143+
condition: "status == 'done' && previous.status != 'done'",
144+
});
145+
expect(screen.queryByText(SCOPE_NOTE)).toBeNull();
146+
await openRawEditor(container);
147+
expect(await screen.findByText(VALID, {}, { timeout: 3000 })).toBeInTheDocument();
148+
expect(screen.queryByText(SCOPE_NOTE)).toBeNull();
149+
});
150+
151+
it('a schedule-triggered Start node does not gain `record`: the scope note names it, and nothing says "Valid CEL"', () => {
152+
// A declared variable makes the scope KNOWN at this node; with none, the
153+
// ref check has no roots and stays silent by design ("scope unknown").
154+
renderStartNode(
155+
{ triggerType: 'schedule', schedule: { expression: '0 7 * * *' }, condition: "record.status == 'done'" },
156+
[{ name: 'threshold', type: 'number' }],
157+
);
158+
expect(screen.getByText('`record` is not a reference in scope at this step.')).toBeInTheDocument();
159+
expect(screen.queryByText(VALID)).toBeNull();
160+
});
161+
});
162+
163+
describe('objectui#11789 one verdict: an out-of-scope root replaces "Valid CEL"', () => {
164+
it('a root the CEL lint accepts but the engine never binds here reads the scope note alone', async () => {
165+
// `trigger` is a CEL scope root (the approval approver's submit-time
166+
// snapshot), so the lint is clean; a flow's start condition binds no
167+
// `trigger`, so the flow scope check is the verdict that holds.
168+
const { container } = renderStartNode({
169+
...UPDATE_TRIGGER,
170+
condition: "status == 'done' && trigger.status != 'done'",
171+
});
172+
await openRawEditor(container);
173+
const issues = await settledLint();
174+
// The premise: the syntax verdict on its own WOULD be "Valid CEL".
175+
expect(issues).toEqual([]);
176+
expect(screen.getByText('`trigger` is not a reference in scope at this step.')).toBeInTheDocument();
177+
expect(screen.queryByText(VALID)).toBeNull();
178+
});
179+
180+
it('the editor itself: a host scope issue withholds "Valid CEL"; without one the clean verdict shows', async () => {
181+
const t = (k: string) => k;
182+
const { rerender } = render(
183+
<CelPredicateField value="status == 'done'" onChange={() => {}} label="Entry" fieldNames={['status']} t={t} scopeIssue />,
184+
);
185+
expect(await settledLint()).toEqual([]);
186+
expect(screen.queryByText('perm.cel.valid')).toBeNull();
187+
rerender(<CelPredicateField value="status == 'done'" onChange={() => {}} label="Entry" fieldNames={['status']} t={t} />);
188+
expect(await screen.findByText('perm.cel.valid')).toBeInTheDocument();
189+
});
190+
191+
it("control: the permission matrix's RLS editor still reads \"Valid CEL\" for a clean clause", async () => {
192+
const t = (k: string) => k;
193+
const user = userEvent.setup();
194+
render(
195+
<PermissionAdvancedFacets
196+
{...({
197+
draft: {
198+
rowLevelSecurity: [
199+
{ name: 'p1', object: 'account', operation: 'all', using: 'organization_id == current_user.organization_id', check: '', enabled: true },
200+
],
201+
},
202+
setDraft: () => {},
203+
writable: true,
204+
allSetNames: [] as string[],
205+
loadObjectFields: async () => ['organization_id', 'owner_id'],
206+
t,
207+
} as unknown as ComponentProps<typeof PermissionAdvancedFacets>)}
208+
/>,
209+
);
210+
await user.click(screen.getByText('perm.rls.title'));
211+
expect(await screen.findByText('perm.cel.valid', {}, { timeout: 3000 })).toBeInTheDocument();
212+
});
213+
});

‎packages/app-shell/src/views/metadata-admin/CelPredicateField.tsx‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,16 @@ export interface CelPredicateFieldProps {
107107
* on the inferred-result-type affordance.
108108
*/
109109
role?: 'predicate' | 'value';
110+
/**
111+
* Set by a host that judges this expression against a scope the lint cannot
112+
* see, when that verdict found a problem — the flow inspector's in-scope
113+
* references at the node, which name an out-of-scope root (objectui#11789).
114+
* The host renders that verdict; this editor then withholds its "Valid CEL"
115+
* affordance, so one expression reads one verdict. The lint itself, its
116+
* findings and `onLintChange` are unaffected. Omitted on every surface that
117+
* has no such second verdict (the permission matrix included).
118+
*/
119+
scopeIssue?: boolean;
110120
/** Reports the current lint issues up so the editor can gate Save on errors. */
111121
onLintChange?: (issues: CelLintIssue[]) => void;
112122
/**
@@ -146,6 +156,7 @@ export function CelPredicateField({
146156
roots,
147157
slot,
148158
role,
159+
scopeIssue,
149160
onLintChange,
150161
onInferredTypeChange,
151162
t,
@@ -342,7 +353,9 @@ export function CelPredicateField({
342353

343354
const errors = issues.filter((i) => i.severity === 'error');
344355
const warnings = issues.filter((i) => i.severity === 'warning');
345-
const clean = linted && !!value.trim() && issues.length === 0;
356+
// "Valid CEL" only when nothing — the lint, or the host's scope verdict —
357+
// says otherwise (objectui#11789).
358+
const clean = linted && !!value.trim() && issues.length === 0 && !scopeIssue;
346359
// Result-type affordance (role="value"): shown once the expression parses,
347360
// even alongside warnings — the type is what dataset measure eligibility
348361
// keys off, so the author should see it whenever it is known.

‎packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ function initFrom(value: string): { rows: Row[]; join: '&&' | '||'; raw: boolean
521521
return { rows: [], join: '&&', raw: !!value };
522522
}
523523

524-
export function ConditionBuilder({ label, value, onCommit, objectName, fields: fieldsProp, disabled, onBlockingIssuesChange, subjects, scope, roots }: {
524+
export function ConditionBuilder({ label, value, onCommit, objectName, fields: fieldsProp, disabled, onBlockingIssuesChange, subjects, scope, roots, scopeIssue }: {
525525
label?: string;
526526
value: string;
527527
onCommit: (cel: string) => void;
@@ -598,6 +598,13 @@ export function ConditionBuilder({ label, value, onCommit, objectName, fields: f
598598
* than a spelling.
599599
*/
600600
roots?: string[];
601+
/**
602+
* The host's scope verdict on `value` found a problem it renders itself
603+
* (objectui#11789) — forwarded to `CelPredicateField`, which then withholds
604+
* "Valid CEL" so the raw editor and the host's note cannot disagree. The row
605+
* builder renders no verdict of its own, so it does not read this.
606+
*/
607+
scopeIssue?: boolean;
601608
}) {
602609
const { fields: hookFields } = useObjectFields(objectName);
603610
const fields = fieldsProp ?? hookFields;
@@ -740,6 +747,7 @@ export function ConditionBuilder({ label, value, onCommit, objectName, fields: f
740747
with no `scope` still forwards `undefined` here, so its offered
741748
roots are the engine's own, unchanged. */
742749
roots={offeredRoots}
750+
scopeIssue={scopeIssue}
743751
t={tLocal}
744752
/>
745753
{value && !parse(value) && (

‎packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeConfigField.entryCondition.test.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,12 @@ describe('#6226 the flow entry field offers the FLATTENED trigger vocabulary', (
316316
expect(opts).toContain('previous.amount');
317317
});
318318

319-
it('does NOT offer `record.id` — the root this site does not bind', async () => {
319+
it('does NOT offer `record.id` — one subject per value, in the bare spelling', async () => {
320320
const opts = await subjectOptions(mountOneRow().container);
321321
// The builder's record-scoped DEFAULT context would have put it here. The
322-
// flow entry field declares its own (empty) context precisely so the editor
323-
// cannot emit the one spelling its own sibling ref-check flags.
322+
// flow entry field declares its own (empty) context: the engine binds
323+
// `record` at this gate too (objectui#11789), but `record.FIELD` is the
324+
// same value as the bare `FIELD` already offered, not a second subject.
324325
expect(opts).not.toContain('record.id');
325326
expect(opts.filter((o) => o.startsWith('record.'))).toEqual([]);
326327
});

0 commit comments

Comments
 (0)