refactor(block-tools): pass RunContext to managed bodies + document the two layers#1706
Conversation
…e two layers Complete the "execution-level lambdas receive ctx by argument" theme: `ManagedBody` is now `(ctx: RunContext) => void`, so `managed(...)` bodies get `ctx` the same way `generate`/`tpl` producers and `when` triggers do. The two rule bodies that read run state (`root-catalog-bump` software check, `block-package-json` block.components) now use the body's `ctx` arg instead of the module-global `getActiveRunContext()` — which is now absent from `rules/` entirely and stays as engine-internal plumbing. Document the composition-vs-execution mental model in the structurer CLAUDE.md: composition declares the tree at build time (no ctx); execution lambdas compute per-block values and each receives ctx by argument.
🦋 Changeset detectedLatest commit: cf7b5c4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Code Review
This pull request refactors the structurer engine so that managed rule bodies receive the active RunContext as an argument (ctx) instead of relying on the module-global getActiveRunContext(). This brings consistency across execution-level lambdas. The review feedback highlights a potential issue where ctx could be passed as undefined if both opts.ctx and opts.triggerContext are missing, and suggests falling back to tryGetActiveRunContext() to prevent runtime errors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| active = { kind: "json", obj, ctx: opts.ctx, triggerContext: opts.triggerContext }; | ||
| try { | ||
| body(); | ||
| body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext); |
There was a problem hiding this comment.
If both opts.ctx and opts.triggerContext are undefined, body will receive undefined as ctx. If the body tries to access properties on ctx (e.g., ctx.modules), it will throw a TypeError. Falling back to tryGetActiveRunContext() ensures that if an ambient run context is active (which is common in tests or standard runs), it is passed to the body.
| body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext); | |
| body((opts.ctx ?? opts.triggerContext?.ctx ?? tryGetActiveRunContext()) as RunContext); |
| }; | ||
| try { | ||
| body(); | ||
| body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext); |
There was a problem hiding this comment.
If both opts.ctx and opts.triggerContext are undefined, body will receive undefined as ctx. Falling back to tryGetActiveRunContext() ensures that the ambient run context is used if available.
| body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext); | |
| body((opts.ctx ?? opts.triggerContext?.ctx ?? tryGetActiveRunContext()) as RunContext); |
| }; | ||
| try { | ||
| body(); | ||
| body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext); |
There was a problem hiding this comment.
If both opts.ctx and opts.triggerContext are undefined, body will receive undefined as ctx. Falling back to tryGetActiveRunContext() ensures that the ambient run context is used if available.
| body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext); | |
| body((opts.ctx ?? opts.triggerContext?.ctx ?? tryGetActiveRunContext()) as RunContext); |
| active = { kind: "json", obj, ctx: opts.ctx, triggerContext: opts.triggerContext }; | ||
| try { | ||
| body(); | ||
| body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext); |
There was a problem hiding this comment.
as RunContext silences a potential undefined-ctx diagnostic
opts.ctx ?? opts.triggerContext?.ctx is typed RunContext | undefined; the cast passes undefined to the body unchecked when neither option is supplied. For the ~15 ctx-free bodies that ignore their argument this is harmless, but any future test that passes a ctx-using body to withManagedBody/withManagedYaml/withManagedLines without setting opts.ctx or opts.triggerContext will receive a TypeError: Cannot read properties of undefined rather than the descriptive message ctxOrThrow would have produced. The same pattern repeats on the two parallel callsites (lines 205, 227).
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/block-tools/src/structure/engine/content-rules.ts
Line: 172
Comment:
**`as RunContext` silences a potential undefined-ctx diagnostic**
`opts.ctx ?? opts.triggerContext?.ctx` is typed `RunContext | undefined`; the cast passes `undefined` to the body unchecked when neither option is supplied. For the ~15 ctx-free bodies that ignore their argument this is harmless, but any future test that passes a ctx-using body to `withManagedBody`/`withManagedYaml`/`withManagedLines` without setting `opts.ctx` or `opts.triggerContext` will receive a `TypeError: Cannot read properties of undefined` rather than the descriptive message `ctxOrThrow` would have produced. The same pattern repeats on the two parallel callsites (lines 205, 227).
How can I resolve this? If you propose a fix, please make it concise.
Stacked on #1705 (base =
feat/structurer-ci-scaffold). Completes the "every execution-level lambda receivesctxby argument" theme that #1705 started forgenerate/tpl.What
ManagedBodybecomes(ctx: RunContext) => void, somanaged(path, initial, body)bodies receivectxthe same waygenerate/tplproducers andwhentriggers do. The threewithManaged*wrappers pass it through (derived from the trigger context they already carry — no runner change).ctxarg instead of the module-globalgetActiveRunContext():root-catalog-bump.ts— the software-module check.block-package-json.ts—block.componentsderivation.getActiveRunContext()/blockVars()are now absent fromrules/entirely — they remain exported as engine-internal plumbing (content-rule builders resolve ctx through them under the hood).Mental model (structurer
CLAUDE.md)Documents the composition-vs-execution split:
defineStructurebuild the tree at load time; no block, noctx.generate/tplproducers, andmanagedbodies run per block and each receivectxby argument.Plus an invariant: no execution lambda reaches for the run context ambiently.
Scope note
The ~15 ctx-free
managedbodies are untouched (() => voidis assignable to(ctx) => void). This homogenizes the rule-author surface; it does not remove the module-global internally (a larger separate refactor).Test
243 structure tests pass;
pnpm checkclean. No change to generated block output.Greptile Summary
This PR completes the "execution lambdas receive
ctxby argument" theme by extending it tomanagedbodies.ManagedBodyis widened from() => voidto(ctx: RunContext) => void, the threewithManaged*engine wrappers thread the context through, and the two rule files that actually read block state (block-package-json.ts,root-catalog-bump.ts) are migrated from the module-globalgetActiveRunContext()to the body'sctxparameter.ManagedBodytype change (ir.ts): signature widened to(ctx: RunContext) => void; the ~15 ctx-free bodies remain assignable unchanged (TypeScript parameter-count compatibility), so no call-sites break.content-rules.ts):withManagedBody,withManagedYaml, andwithManagedLinesnow callbody((opts.ctx ?? opts.triggerContext?.ctx) as RunContext), deriving ctx from whichever option the caller provides.block-package-json.ts,root-catalog-bump.ts):getActiveRunContext()removed;ctx.modulesis now accessed via the body argument, making the ambient-context footprint inrules/zero.Key Terms
ManagedBodyManagedItemand executed by the runner to reconcile a managed file.() => void→(ctx: RunContext) => void.RunContextmodules,isSdkInternal,blockVars, etc.ManagedBody.withManagedBody/withManagedYaml/withManagedLinesctxto the body; sourced fromopts.ctx ?? opts.triggerContext?.ctx.blockPackageJsonRulespackage.json; re-assertsblock.components, scripts, deps.(): voidto(ctx: RunContext): void; no longer usesgetActiveRunContext().getActiveRunContextwithRunContext.rules/files; retained as engine-internal plumbing used byctxOrThrow.TriggerContextRunContextand adding FS-snapshot predicates..ctxis now the source whenopts.ctxis absent in the wrappers.Confidence Score: 4/5
Safe to merge; the change is an internal refactor with no impact on generated block output, confirmed by 243 passing structure tests and a clean pnpm check.
All production paths always supply either
opts.ctxoropts.triggerContext, so theas RunContextcast in the three engine wrappers is safe today. The cast does, however, hide theundefinedcase from TypeScript, meaning a future test that wires a ctx-using body without supplying either option will get an opaqueTypeErrorrather than a descriptive error. The two rule files that needed the context have been correctly updated, and the ~15 ctx-free bodies are structurally unaffected.tools/block-tools/src/structure/engine/content-rules.ts — the three
as RunContextcasts on lines 172, 205, and 227 are the only spots worth a second look.Important Files Changed
() => voidto(ctx: RunContext) => void; existing zero-arg lambdas remain assignable due to TS parameter-count compatibility.(opts.ctx ?? opts.triggerContext?.ctx) as RunContextto the body; theas RunContextcast hides the undefined case and may produce opaque TypeErrors in future tests lacking ctx.block.componentsderivation now uses the argument directly.Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Runner participant withManagedBody participant ManagedBody participant ContentRuleBuilders Runner->>withManagedBody: "withManagedBody(obj, body, { ctx, triggerContext })" withManagedBody->>withManagedBody: "active = { kind:json, obj, ctx, triggerContext }" withManagedBody->>ManagedBody: body(opts.ctx ?? triggerContext?.ctx) ManagedBody->>ContentRuleBuilders: ensureField(block.components, blockComponents(ctx)) ContentRuleBuilders->>withManagedBody: reads active state (via requireJson) ContentRuleBuilders-->>ManagedBody: mutation applied ManagedBody-->>withManagedBody: returns withManagedBody->>withManagedBody: "active = undefined" withManagedBody-->>Runner: mutated obj%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Runner participant withManagedBody participant ManagedBody participant ContentRuleBuilders Runner->>withManagedBody: "withManagedBody(obj, body, { ctx, triggerContext })" withManagedBody->>withManagedBody: "active = { kind:json, obj, ctx, triggerContext }" withManagedBody->>ManagedBody: body(opts.ctx ?? triggerContext?.ctx) ManagedBody->>ContentRuleBuilders: ensureField(block.components, blockComponents(ctx)) ContentRuleBuilders->>withManagedBody: reads active state (via requireJson) ContentRuleBuilders-->>ManagedBody: mutation applied ManagedBody-->>withManagedBody: returns withManagedBody->>withManagedBody: "active = undefined" withManagedBody-->>Runner: mutated objPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "refactor(block-tools): pass RunContext t..." | Re-trigger Greptile
Context used: