Skip to content

fix(app-shell): a CEL comprehension macro's iteration variable is bound inside its macro, not flagged as out of scope (objectui#10538) - #10619

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-10538-cel-macro-bound-variable
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-10538-cel-macro-bound-variable

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10538

Clause-②: no

Implemented by a dispatched dev run of the domain:ui seat 5, session https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy.

What changes

findUnknownRefs in packages/app-shell/src/views/metadata-admin/inspectors/flow-ref-check.ts read every root identifier in a CEL source as a flow variable reference. So a comprehension macro's own iteration variable (r in rows.map(r, r.subject)) drew the amber "not in scope" warning. That included the assignment-value example in @objectstack/spec, joinNonEmpty(rows.map(r, r.subject), "\n"), in the AssignmentValue JSDoc.

rootIdentifiers now collects the bindings of map, filter, exists, all and exists_one, through a new private helper macroBindings in the same file. For a member call .MACRO(v, ...) whose first argument is a bare identifier, v is bound between that call's ( and its matching ), and nowhere else. Scope is lexical: the receiver, and the same name after the closing ), are still roots. A nested macro binds each variable in its own span. findUnknownRefs itself is unchanged. The warning stays advisory; nothing about what blocks Save changes.

Why the local scanner and not the parser-backed reader (H2, measured)

@objectstack/formula 17.4.0, already an app-shell dependency, exports collectCelRootIdentifiers. Measured on the five macros, it reports the bound variable too:

source collectCelRootIdentifiers(source).roots
rows.map(r, r.subject) ["rows","r"]
rows.filter(x, x.done).size() ["rows","x"]
rows.exists(e, e.a > 1) ["rows","e"]
rows.all(a, a.ok) ["rows","a"]
rows.exists_one(o, o.id == 1) ["rows","o"]
rows.map(r, r.items.filter(i, i.ok)) ["rows","r","i"]

Switching findUnknownRefs to that reader would keep this defect. So the fix stays in the local regex scanner, scoped to each macro's argument span, and no parser-backed path is added. The upstream walker has the same defect and its consumers inherit it. That is reported to the dispatching seat as a separate finding (see Acceptance notes). objectstack is not edited here.

Pins

A new describe in flow-ref-check.test.ts, "findUnknownRefs (CEL comprehension macros bind their iteration variable) (objectui#10538)":

  • the canonical example reports [];
  • each of the five macros (plus the three-argument map) with its own bound variable reports [];
  • a nested macro reports [];
  • negative legs: rows.map(r, r.subject) + r flags the trailing r; an inner variable used after its own macro closes is flagged; a bound variable used as a different macro's receiver is flagged; rowz.map(r, r.subject) flags rowz with suggestion rows; a member call that is not one of the five (rows.join(sep, other)) binds nothing.

Base vs fix, single file (pnpm exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/flow-ref-check.test.ts, through the shared verify lock):

  • base source, byte-identical to 21d34d5 (git diff --quiet exit 0), new pins added: exit 1, Tests 6 failed | 15 passed (21). The canonical pin fails with expected [ { token: 'r', …(1) } ] to deeply equal [].
  • with the fix (commit 5225195): exit 0, Tests 21 passed (21).

Two of the negative legs pass on the base as well; they guard against the fix binding too much.

Gates (at head 9fd5fcc, after merging origin/main adeecd6)

  • pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/views/metadata-admin/inspectors/ packages/app-shell/src/views/metadata-admin/previews/flow-expr-problems.test.ts: exit 0, Test Files 98 passed (98), Tests 1357 passed | 1 skipped (1358). The skip is an existing spec-version skipIf in flow-node-config.spec-reconciliation.test.ts. The previews test is added because flow-expr-problems.ts is the one caller of findUnknownRefs outside the inspectors folder.
  • pnpm turbo run type-check --filter @object-ui/app-shell --concurrency=2: exit 0, Tasks: 30 successful, 30 total. tsc -p tsconfig.test.json --listFilesOnly lists flow-ref-check.test.ts, so the pins are type-checked.
  • pnpm exec eslint on the two changed source files: exit 0, no output.
  • pnpm check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations (0 new citations), check:changeset-claims, check:pending-changeset-literals, changeset:check, node scripts/check-changeset-presence.mjs and node scripts/check-changeset-overwrite.mjs: all exit 0.

Changeset: .changeset/10538-cel-macro-bound-variable.md, '@object-ui/app-shell': patch.

Acceptance notes

  • Upstream, not filed by this PR (reported to the seat): collectCelRootIdentifiers in @objectstack/formula reports a macro-bound variable as a root, so its closed-root consumers refuse valid CEL. Probe on @objectstack/lint 17.4.0: validateApprovalApprovers on an approval node whose expression approver is vars.list.map(x, x.id) gives error approval-expression-invalid, "expression approver references x". The control vars.list gives no such error.
  • cel.bind(x, rows, x.size()) evaluates upstream (ExpressionEngine.evaluate gives { ok: true, value: 3 }), and this scanner flags cel and x on it, before and after this change. cel.bind is outside the five macros this card rules on, and its shape differs: the variable is bound in the third argument only, and the receiver cel is a namespace constant. So it is reported to the seat, not changed here.
  • Docs: no page under content/docs describes this designer warning, so there is no docs change.

Generated by Claude Code

…nd inside its macro, not flagged as out of scope (objectui#10538)

findUnknownRefs read every root identifier in a CEL source as a flow
variable reference, so `r` in `rows.map(r, r.subject)` drew the "not in
scope" warning, including on the spec's own assignment-value example.

rootIdentifiers now collects the bindings of map / filter / exists / all /
exists_one: the first argument, bound between the macro's `(` and its
matching `)`. The same name outside that span (after the close, or as a
different macro's receiver) is still a root. Pins beside the existing
tests cover the canonical example, each macro, a nested macro, and the
negative legs.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3048.2 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-DK0cQ6_W.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 546.37KB 130.65KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 259.05KB 65.69KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.40KB 14.61KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.96KB 61.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 147.92KB 37.83KB
plugin-gantt (index.js) 169.62KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.26KB 15.04KB
plugin-list (index.js) 114.60KB 28.31KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.67KB 8.95KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.84KB 21.96KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9fd5fcc3d525a96c5a966b87f4bc26f65f71f31c

Reviewed from git only: refs/review/pr-10619 (head 9fd5fcc) diffed against merge-base adeecd666868d11aa0fc132f42298306f1e88742; refs/review/main at c2d86599b334dc97fa225bca2e0fa57df9044739. Three files in the diff: packages/app-shell/src/views/metadata-admin/inspectors/flow-ref-check.ts (+55/-2), flow-ref-check.test.ts (+43/-0), .changeset/10538-cel-macro-bound-variable.md (+7). Evidence: the card and its three comments, the triage ruling 5830908145, the claim 5831034609, the PR body, the check-runs at the head, and my own run of the head and merge-base sources (extracted from git into the review scratch dir, i18n import stubbed, run with node strip-types) over the PR's 13 pin expectations plus 34 adversarial sources. /home/user/objectui/node_modules is absent after the container restart, so the installed @objectstack/spec@17.4.0 and @objectstack/formula@17.4.0 were read, read-only, from the pnpm virtual store under /home/user/objectui-issue-10545/node_modules/.pnpm/. The os-dev-report comment was not used as evidence.

① Derived judgments

Binding text. Triage 5830908145: 「treat the iteration variable of map / filter / exists / all / exists_one as bound inside its macro; pin the canonical example ⇒ [] and a genuinely unknown root ⇒ flagged.」 Claim 5831034609 scope: 「the iteration variable is bound inside its macro's body only; the same name outside the macro is still a root」; pins: 「the canonical example reports nothing; rows.map(r, r.subject) + r still flags the trailing r; an unknown root is still flagged with its suggestion」; file surface: 「flow-ref-check.ts (rootIdentifiers / findUnknownRefs only), pins beside its existing tests, and one .changeset/10538-…md」.

Behaviour changes, each at the head:

  1. flow-ref-check.ts:82 — COMPREHENSION_MACROS is the set map, filter, exists, all, exists_one. Exactly the five the triage rules on, nothing more. RIGHT.
  2. flow-ref-check.ts:100-120 — new module-private macroBindings(src). Regex at line 103 matches a member call whose name is followed by a bare identifier and a comma; line 106 keeps only the five macros; line 107 takes the call's open paren; lines 108-116 walk to the depth-matched close paren, or the source length while unclosed; line 117 records { name, open, close }. Bound span is that macro's own argument list. RIGHT against 「bound inside its macro's body only」.
  3. flow-ref-check.ts:128 — bindings are computed on noStrings, the same string-stripped text the root scan reads, so indices agree and a paren inside a string literal cannot close a span. Verified: rows.map(r, r.subject == "a)") + rows ⇒ [] at head. RIGHT.
  4. flow-ref-check.ts:136-142 — the root scan records each identifier's index and skips it only when a binding of the same name has the identifier strictly after its open paren and strictly before its close paren. The declaration position is inside; the receiver (before the paren) and anything after the close are outside. Verified at head: r.map(r, r.x) flags r; rows.map(r, r.x) + r + rows.map(r, r.y) flags r; rows.exists(r, r.ok) && r.items.all(i, i.ok) flags r; rows.map(r, rr.x) flags rr not r. RIGHT against 「the same name outside the macro is still a root」.
  5. flow-ref-check.ts:138 — m.index renamed to at; same test, same slice. No behaviour change. RIGHT.
  6. flow-ref-check.ts:16-18 — header bullet documenting the rule. True to lines 82-142. RIGHT.
  7. flow-ref-check.ts:152-183 — findUnknownRefs body is byte-identical to the merge-base: the template-hole scan, de-dupe, RUNTIME_GLOBALS, $-prefix skip and suggestion search are untouched. Verified: a template hole Hi {rows.map(r, r.name)} {r} still flags the trailing r; empty scope still returns []. RIGHT.
  8. The canonical example is real: @objectstack/spec@17.4.0 dist/automation/index.d.ts, the AssignmentValue JSDoc, reads 「joinNonEmpty(rows.map(r, r.subject), "\n") builds a digest body from a list」. Merge-base source reports r on it; head reports []. RIGHT.

Adversarial sources at head, all as CEL semantics require: whitespace before the paren, multi-line arguments, nested parens in the body, chained macros (rows.filter(x, x.done).map(y, y.id)), an outer variable read inside an inner macro, same-name shadowing, sibling macros, a parenthesised or deep receiver, a $-prefixed receiver, a bound name equal to a known root, an underscore variable — all []. A non-macro name (mapx), a capitalised Map, a global-style map(r, r.x), a non-identifier first argument (r.x or a string) bind nothing and still flag. An unclosed macro binds to the end of the source, the conservative direction the file header declares. cel.bind(x, rows, x.size()) still flags cel and x before and after; it is outside the ruled five and the PR says so.

Asked and missing: nothing. Five macros bound (test flow-ref-check.test.ts:82-89, one expectation per macro plus the three-argument map); canonical example pinned to [] (:77-80); a genuinely unknown root still flagged, with its suggestion (:107-109, rowz ⇒ rows) and without (:111-113); the claim's own rows.map(r, r.subject) + r leg (:95-97).

Done and not asked: the header bullet at :16-18 and the private helper, interface and constant at :78-91. All in the one file the claim names and serving rootIdentifiers only; no other file is touched. The second commit 9fd5fcc is a merge of origin/main adeecd6 with no content of its own. Not a breach.

Existing pins edited or deleted: none. The test file is additions only (+43/-0); the source's two removed lines are the replaced one-line doc comment and the m.index rename.

New pins can fail. Lit control, my run of the merge-base source against the eight new it blocks: canonical, five-macros, nested, inner-outside, receiver and rowz fail (six), matching the PR body's 「Tests 6 failed | 15 passed (21)」; after-close (:95-97) and join (:111-113) pass on base, which the PR body states and which is their job, guarding against over-binding. Negative legs present: :95-113, five of them, each expecting a non-empty result.

② Semver level

Changeset .changeset/10538-cel-macro-bound-variable.md:1-3: '@object-ui/app-shell': patch. Two src/ files of a fixed-group package changed (.changeset/config.json lists @object-ui/app-shell in fixed), so AGENTS.md line 168 and scripts/check-changeset-presence.mjs require a declaration, and one is present. AGENTS.md 版本号策略: no major in a changeset; a behaviour fix to an advisory warning with no new or changed export is a patch. Level is RIGHT. Not a content/docs-only change, so the empty-frontmatter rule does not apply. Check-runs Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check and Changeset Overwrite Report all success at the head.

Changeset prose, sentence by sentence:

  • Title 「the flow designer's "not in scope" warning no longer flags a CEL comprehension macro's own iteration variable」 — TRUE (item 4 above; canonical [] at head).
  • 「findUnknownRefs read every root identifier in a CEL source as a flow variable, so the r in rows.map(r, r.subject) drew an amber "not in scope" warning.」 — TRUE. Merge-base reports r; the callers render it amber with role="note" (FlowEdgeInspector.tsx:258-261, FlowExprIssue.tsx:39-43) or as level: 'warning' (flow-expr-problems.ts:50-51).
  • 「That included the assignment-value example in @objectstack/spec, joinNonEmpty(rows.map(r, r.subject), "\n").」 — TRUE (item 8).
  • 「The scanner now treats the first argument of map, filter, exists, all and exists_one as a variable bound inside that macro's own argument list.」 — TRUE (:82, :103-117, :142).
  • 「The same name used outside that list (after the closing parenthesis, or as another macro's receiver) is still checked as a root, and an unknown receiver is still flagged with its suggestion.」 — TRUE (test.ts:95-109; my probes).
  • 「The warning stays advisory; nothing about what blocks Save changes.」 — TRUE. No caller changed; validateExpressionClient (the role="alert" error path) is untouched; the diff touches no save path.

Docs and README prose added by the diff: none besides the in-file JSDoc. :78-80 「Each is receiver-style … and its first argument DECLARES the iteration variable instead of reading one」 — TRUE for CEL. :89 「or the source length while it is still unclosed」 — TRUE (:108). :97-98 「src must already have its string literals stripped」 — TRUE, the one call site is :128 on noStrings. :140-141 「(the declaration included)」 — TRUE, the skip uses strictly-after-open.

③ Boundary flags

  • PR body's own flags. (a) 「Why the local scanner and not the parser-backed reader (H2, measured)」: reproduced on the installed @objectstack/formula@17.4.0; collectCelRootIdentifiers returns ["rows","r"], ["rows","x"], ["rows","e"], ["rows","a"], ["rows","o"] and ["rows","r","i"] for the six table rows, exactly as the body states, so switching to it would keep the defect; the choice to fix the local scanner is sound and objectstack is not edited, consistent with the claim's file surface. (b) cel.bind: the scanner half (flags cel and x before and after) is verified; the upstream evaluate value of 3 I did not reproduce, my probe did not bind variables the way that API expects. It is outside the ruled five and not load-bearing. (c) 「no page under content/docs describes this designer warning」: TRUE; the one not in scope hit, content/docs/guide/react-pages.md:290, is about a React page ReferenceError, a different surface. packages/app-shell/README.md does not mention the warning. No open questions are listed in the body.
  • CI at head 9fd5fcc, polled 12:19Z, 12:23Z and 12:25Z: final count 43 check-runs, 40 success, 3 skipped (dependabot, Test (coverage), Test (coverage shard ${{ matrix.shard }}/4)), 0 failure, 0 in progress. All eight Test (shard n/8), Type Check, Lint, Line Citation Gate, README Export Check, Spec Main Shape Gate, Build & E2E are success. The PR body's local vitest and gate counts were not re-run here (no node_modules in the review checkout); the shards are the evidence.
  • Draft: YES. The PR is a draft at review time; it needs un-drafting before merge. Not a contract defect.
  • Clause-②: no HOLDS. findUnknownRefs, scopeRoots, describeUnknownRefs and UnknownRef keep their signatures; the additions at :82-120 are module-private; flow-ref-check.ts is not re-exported from packages/app-shell/src/index.ts and the package exports map is unchanged. The warning is advisory in all four callers, so no accept set moves.
  • Main under the PR's files: did NOT move. Three commits landed on main after merge-base adeecd6; none touches the three PR files. mergeable: true, state behind.
  • AGENTS.md [WIP] Update documentation for project #11 (cite by content): the diff carries no path-line citations; the test name and comments cite objectui#10538 and the AssignmentValue symbol. Line Citation Gate success.

Implemented-by: claude/issue-10538-cel-macro-bound-variable
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 12:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit d583db1 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10538-cel-macro-bound-variable branch September 25, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants