Skip to content

refactor(block-tools): pass RunContext to managed bodies + document the two layers#1706

Merged
dbolotin merged 1 commit into
feat/structurer-ci-scaffoldfrom
feat/structurer-managed-body-ctx
Jun 18, 2026
Merged

refactor(block-tools): pass RunContext to managed bodies + document the two layers#1706
dbolotin merged 1 commit into
feat/structurer-ci-scaffoldfrom
feat/structurer-managed-body-ctx

Conversation

@dbolotin

@dbolotin dbolotin commented Jun 18, 2026

Copy link
Copy Markdown
Member

Stacked on #1705 (base = feat/structurer-ci-scaffold). Completes the "every execution-level lambda receives ctx by argument" theme that #1705 started for generate/tpl.

What

  • ManagedBody becomes (ctx: RunContext) => void, so managed(path, initial, body) bodies receive ctx the same way generate/tpl producers and when triggers do. The three withManaged* wrappers pass it through (derived from the trigger context they already carry — no runner change).
  • The two rule bodies that actually read run state now use the body's ctx arg instead of the module-global getActiveRunContext():
    • root-catalog-bump.ts — the software-module check.
    • block-package-json.tsblock.components derivation.
  • getActiveRunContext() / blockVars() are now absent from rules/ 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:

  • Composition layer — calls inside defineStructure build the tree at load time; no block, no ctx.
  • Execution layer — trigger predicates, generate/tpl producers, and managed bodies run per block and each receive ctx by argument.

Plus an invariant: no execution lambda reaches for the run context ambiently.

Scope note

The ~15 ctx-free managed bodies are untouched (() => void is 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 check clean. No change to generated block output.

Greptile Summary

This PR completes the "execution lambdas receive ctx by argument" theme by extending it to managed bodies. ManagedBody is widened from () => void to (ctx: RunContext) => void, the three withManaged* 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-global getActiveRunContext() to the body's ctx parameter.

  • ManagedBody type 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.
  • Engine wrappers updated (content-rules.ts): withManagedBody, withManagedYaml, and withManagedLines now call body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext), deriving ctx from whichever option the caller provides.
  • Rule files cleaned up (block-package-json.ts, root-catalog-bump.ts): getActiveRunContext() removed; ctx.modules is now accessed via the body argument, making the ambient-context footprint in rules/ zero.

Key Terms

Term Definition Change
ManagedBody The content-rules lambda stored on a ManagedItem and executed by the runner to reconcile a managed file. Widened from () => void(ctx: RunContext) => void.
RunContext Per-block execution context carrying modules, isSdkInternal, blockVars, etc. Now passed explicitly as the argument to every ManagedBody.
withManagedBody / withManagedYaml / withManagedLines Engine-internal wrappers that establish active state so content-rule builders can mutate the file. Now forward ctx to the body; sourced from opts.ctx ?? opts.triggerContext?.ctx.
blockPackageJsonRules Drift-correcting body for the block's package.json; re-asserts block.components, scripts, deps. Signature changed from (): void to (ctx: RunContext): void; no longer uses getActiveRunContext().
getActiveRunContext Module-global accessor for the run context set by withRunContext. Removed from all rules/ files; retained as engine-internal plumbing used by ctxOrThrow.
TriggerContext Richer per-leaf execution context wrapping RunContext and adding FS-snapshot predicates. Unchanged; its inner .ctx is now the source when opts.ctx is 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.ctx or opts.triggerContext, so the as RunContext cast in the three engine wrappers is safe today. The cast does, however, hide the undefined case from TypeScript, meaning a future test that wires a ctx-using body without supplying either option will get an opaque TypeError rather 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 RunContext casts on lines 172, 205, and 227 are the only spots worth a second look.

Important Files Changed

Filename Overview
tools/block-tools/src/structure/engine/ir.ts ManagedBody type widened from () => void to (ctx: RunContext) => void; existing zero-arg lambdas remain assignable due to TS parameter-count compatibility.
tools/block-tools/src/structure/engine/content-rules.ts All three withManaged* wrappers now pass (opts.ctx ?? opts.triggerContext?.ctx) as RunContext to the body; the as RunContext cast hides the undefined case and may produce opaque TypeErrors in future tests lacking ctx.
tools/block-tools/src/structure/rules/block-package-json.ts blockPackageJsonRules signature changed to accept explicit ctx; getActiveRunContext() import removed; block.components derivation now uses the argument directly.
tools/block-tools/src/structure/rules/block.ts Managed body for package.json updated to thread ctx through to blockPackageJsonRules(ctx); no logic change.
tools/block-tools/src/structure/rules/root-catalog-bump.ts Software-module check migrated from getActiveRunContext() to the body's ctx argument; getActiveRunContext() import removed.
tools/block-tools/src/structure/CLAUDE.md Documents the composition vs. execution two-layer model and the new invariant that every execution lambda receives ctx by argument.
.changeset/structurer-managed-body-ctx.md Patch-level changeset correctly describing the ManagedBody signature change and its scope.

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
Loading
%%{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 obj
Loading

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
tools/block-tools/src/structure/engine/content-rules.ts:172
**`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).

Reviews (1): Last reviewed commit: "refactor(block-tools): pass RunContext t..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

  • Context used - Terms is a types in codebase. Provide the list of ... (source)

…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-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cf7b5c4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@platforma-sdk/block-tools Patch
@milaboratories/pl-middle-layer Patch
@platforma-sdk/pl-cli Patch
@platforma-sdk/test Patch

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Fix in Claude Code

@dbolotin dbolotin merged commit 1b71075 into feat/structurer-ci-scaffold Jun 18, 2026
2 checks passed
@dbolotin dbolotin deleted the feat/structurer-managed-body-ctx branch June 18, 2026 09:52
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