Skip to content

fix(workflows): scope the canonical sub-block index to the active surface - #6990

Merged
icecrasher321 merged 4 commits into
stagingfrom
fix/canonical-index-surface-scoping
Aug 22, 2026
Merged

fix(workflows): scope the canonical sub-block index to the active surface#6990
icecrasher321 merged 4 commits into
stagingfrom
fix/canonical-index-surface-scoping

Conversation

@icecrasher321

@icecrasher321 icecrasher321 commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • A block that is both an action and a trigger holds one subBlocks array — its own fields plus its trigger's, spread in after them. The two sets routinely share a canonicalParamId under different ids (siteSelector/manualSiteId vs triggerSiteId, all siteId), so indexing them together collapses the trigger field into an action pair whose basicId it can never be. Every group-relative question about that field then answers for the dormant surface.
  • Added getCanonicalSubBlocksForSurface / buildCanonicalIndexForSurface and moved the three sites that already had the filter inline onto them.
  • Fork/sync cleared live trigger fields. createCanonicalModeGates indexed the whole array, so isDormantMember answered true for a trigger field the moment the shared mode key resolved to advanced — and the documented policy for a dormant member is to clear its value outright. Reachable with no explicit toggle: configure a block as an action with a manual id, switch it to trigger mode, and the pair's value heuristic resolves to advanced on its own. Threaded triggerMode through RemapForkContext, SubBlockTransform, clearDependentsOnRemap, collectClearedDependents, the reference scanners, and the promote cleared-ref collectors.
  • The canvas card dropped trigger fields the editor panel showed. getCardSubBlocks now derives its own index instead of accepting one; it already took triggerMode, and accepting an index is what let all three callers pass one built for the other surface. 12 blocks were affected on the card and in the autolayout height estimate — Airtable's Base ID and Table ID (both required), Webflow's Site and Collection, and eight blocks' trigger Credentials.
  • Scoped the remaining consumers that resolve against the block's full value map: autolayout, both preview surfaces, the dependsOn gate, the canonical value hook, reactive conditions, and the copilot selector lint.
  • A canonical group with no advanced member no longer takes the legacy advancedMode path, which forced 'advanced', resolved nothing, and still deleted the basic member — so the block serialized without a field the user had filled and then reported it as missing and required (posthog.apiKey, hubspot.objectType, managed_agent.environmentType, and 6 more).
  • Legacy type-scoped tool canonical modes are now a baseline under index-scoped ones. Toggles are written one key at a time, so the first re-toggle on a legacy tool was reverting every canonical id the user had not touched.

The serializer keeps the unscoped index deliberately, and that is documented: shouldSerializeSubBlock drops the inactive surface before the canonical collapse reads it, so it resolves against a value map the dormant surface cannot appear in. That filter-then-resolve ordering is why execution has always been correct here. Trigger config never travels this path anyway — buildProviderConfig indexes triggerDef.subBlocks, projected by construction. Nested tool params and the fork dependent scan are also unchanged: a tool is always the action surface, and the dependent scan already narrows its configs.

No API, schema, persisted-field, or generated-artifact changes.

Type of Change

  • Bug fix

Testing

  • New registry-wide regression test asserts a trigger card is a function of the trigger surface alone: dropping the action fields must not change what the card shows. Verified it fails on the old behavior (14 cases) and passes after — comparing against the same function on a reduced config rather than a hand-written expected set, since a second model of "what is on a card" is what that module exists to prevent.
  • New fork tests cover the gates directly and end-to-end through remapForkSubBlocks, asserting a live trigger field survives a remap under an advanced shared mode, plus a no-regression guard that the action surface still gates normally. Both new assertions confirmed failing before the fix.
  • New serializer tests for the non-pair legacy advancedMode case, including that the surviving value is no longer reported as a missing required field. Both confirmed failing before the fix.
  • New unit tests for surface scoping and for the partially re-toggled legacy tool.
  • 6,451 tests across ee, lib/workflows, lib/copilot, hooks, serializer; 11,087 across ee, stores, triggers, tools, providers, executor, blocks; 5,680 across lib/workflows, app/workspace, hooks.
  • turbo run type-check 26/26, bun run lint clean, all 32 audits in check:audits pass, check-block-registry against staging passes.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…face

A block that is both an action and a trigger holds one `subBlocks` array — its
own fields plus its trigger's, spread in after them. The two sets routinely
share a `canonicalParamId` under different ids, so indexing them together
collapses a trigger field into an action pair whose `basicId` it can never be.
Every group-relative question about that field then answers for the dormant
surface.

The serializer was never affected: `shouldSerializeSubBlock` drops the inactive
surface before the canonical collapse reads it, so it resolves against a value
map the dormant surface cannot appear in. Every other caller resolves against
the block's full value map, so the scoping has to live in the index.

- add `getCanonicalSubBlocksForSurface` / `buildCanonicalIndexForSurface`, and
  move the three sites that already had the filter inline onto them
- `getCardSubBlocks` derives its own index instead of accepting one; it already
  took `triggerMode`, and accepting an index is what let all three callers pass
  one built for the other surface
- scope the remaining consumers that resolve against a full value map: the
  canvas card, autolayout, both preview surfaces, the dependsOn gate, the
  canonical value hook, reactive conditions, and the copilot selector lint
- keep a canonical group with no advanced member out of the legacy
  `advancedMode` path, which deleted its basic member and republished nothing
- merge legacy type-scoped tool modes as a baseline under index-scoped ones, so
  the first re-toggle stops reverting the ids the user has not touched
@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 22, 2026 11:28pm

Request Review

@cursor

cursor Bot commented Aug 22, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches canonical grouping used for visibility, credential resolution, serialization edge cases, and fork/promote remapping—wrong scoping can still drop or clear user-configured fields. Mitigated by targeted tests and a call-site audit, but the blast radius is workflow-wide.

Overview
Stops mixed action/trigger blocks from treating trigger fields as members of the action canonical pair (shared canonicalParamId, different ids). That mis-grouping hid fields on the canvas, resolved dependsOn/credentials to stale action values, and let fork/sync clear live trigger config.

Adds getCanonicalSubBlocksForSurface / buildCanonicalIndexForSurface and threads triggerMode through editor, preview, autolayout, reactive conditions, copilot selector lint, and fork remap (createCanonicalModeGates keeps a full-index fallback so dormant action values still clear and are not treated as mapping blockers). Nested tools stay unscoped (always the action surface). The serializer stays unscoped on purpose: it filters the inactive surface before collapse.

Also: getCardSubBlocks now builds its own surface-scoped index (Airtable/Webflow/credentials rows return to the card); legacy advancedMode no longer wipes non-pair groups; tool canonical-mode overrides merge index-scoped keys over the type-scoped baseline. New check:canonical-index audit plus registry/fork/serializer tests.

Reviewed by Cursor Bugbot for commit 4a083b9. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR scopes canonical sub-block indexing to the active action or trigger surface so UI, fork/sync, preview, and validation paths resolve overlapping canonical fields consistently.

  • Adds shared surface-scoped canonical-index helpers.
  • Propagates trigger mode through fork, remap, reference-scanning, preview, editor, and autolayout paths.
  • Preserves legacy canonical-mode behavior and adds registry-wide regression coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/workflows/subblocks/visibility.ts Introduces the central active-surface projection and canonical-index helper used throughout the change.
apps/sim/ee/workspace-forking/lib/remap/remap-references.ts Scopes canonical mode gates during fork remapping while retaining the full index for dormant-member classification.
apps/sim/lib/workflows/blocks/canvas-card-fields.ts Builds the canonical index from the requested card surface to prevent action fields from suppressing trigger fields.
apps/sim/serializer/index.ts Retains deliberate filter-before-collapse serializer behavior and fixes canonical groups without an advanced member.
scripts/check-canonical-index-surface.ts Adds a static audit guarding surface-sensitive canonical-index call sites.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Block[Block configuration] --> Surface{Active surface}
  Surface -->|Action| Action[Action sub-blocks]
  Surface -->|Trigger| Trigger[Trigger sub-blocks]
  Action --> Index[Canonical index]
  Trigger --> Index
  Index --> Consumers[Editor, canvas, preview, fork/sync, validation]
Loading

Reviews (3): Last reviewed commit: "fix(audits): stop the canonical-index gu..." | Re-trigger Greptile

…ve surface

`createCanonicalModeGates` indexed a block's whole `subBlocks` array, so on a
mixed action/trigger block a trigger field sharing a `canonicalParamId` with an
action pair was read as a member of THAT pair. Being neither its `basicId` nor
in its `advancedIds`, `isDormantMember` answered true the moment the shared mode
resolved to advanced — and a fork acts on that by clearing the value, so a
configured trigger field was silently wiped on fork/sync.

Reachable without any explicit toggle: a block configured as an action with a
manual id and then switched to trigger mode leaves the pair's value heuristic
resolving to advanced on its own.

- `createCanonicalModeGates` takes the surface and scopes its index
- thread `triggerMode` through `RemapForkContext`, `SubBlockTransform`,
  `clearDependentsOnRemap`, `collectClearedDependents`, the reference scanners,
  and the promote cleared-ref collectors
- nested tool params and the dependent scan are unchanged: a tool is always the
  action surface, and the dependent scan already narrows its configs
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread apps/sim/ee/workspace-forking/lib/remap/remap-references.ts Outdated
… before scoping

Surface scoping decides canonical membership for the ACTIVE surface. Applying it
to every key also re-classified the dormant surface's own values: they stopped
being dormant members, which meant the remap no longer cleared them AND started
detecting them as references — turning a stale action selector on a trigger-mode
block into a mapping requirement that can block promote/sync.

The gates now pick the index per key: the scoped one for anything the active
surface defines (the fix — a trigger field gets its own group instead of being
read as a stranded member of an action pair), the whole array for everything
else, which is byte-for-byte the pre-scoping behavior.

Also adds `check:canonical-index`, an audit that fails any call building a
canonical index off a config's whole `subBlocks`, or calling the fork gates
without a surface, unless annotated with why. This defect shipped three times in
three subsystems; the 14 sites that legitimately mean one fixed surface now say
so at the call.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

Good catch on the inactive-surface refs — that was a real regression in the fork commit, fixed in 2d53b4a.

Scoping the gates was meant to stop a live trigger field being misread as a stranded member of an action pair (which made isDormantMember answer true and the remap clear it). But applying the scoped index to every key also re-classified the dormant surface's own values: they stopped being dormant members, so the remap no longer cleared them and started detecting them — turning a stale action selector on a trigger-mode block into a mapping requirement that can block promote/sync.

The gates now pick the index per key: the scoped one for anything the active surface defines, the whole array for everything else. That leaves the dormant surface byte-for-byte as it was before scoping existed.

Worth noting the blast radius was narrower than "High" implies: isDormantMember on an inactive-surface member already returned false whenever the shared mode resolved to basic, which is the block-creation default — so those refs were already being detected. The regression only covered the advanced slice. Fixed regardless, since it's the wrong semantic either way.

Two tests pin both directions, and I verified each fails against the previous commit: one asserts the dormant action members classify identically to the unscoped gates, and one drives remapForkSubBlocks end-to-end to assert the dormant basic member is still cleared and still absent from unmapped, while the live trigger field survives.

The audit holds `buildCanonicalIndex(` and `createCanonicalModeGates(` as string
literals to search for, and its own regex matched them — the arg-count rule then
fired on the literal. It passed locally only because the file was still untracked
when it ran, so `git ls-files` did not list it; committing it made the audit scan
itself and fail CI on the first run.

Exempts the audit's own source alongside the module that defines the primitives.
Verified the guard still fails on both regression shapes after the exemption.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 4a083b9. Configure here.

@icecrasher321
icecrasher321 merged commit cbf3aad into staging Aug 22, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant