Skip to content

feat(render): formats from discovered plugins; validate and exportGraph callbacks - #232

Merged
mindsocket merged 6 commits into
mainfrom
claude/project-thread-tmn7yk
Nov 14, 2026
Merged

mindsocket merged 6 commits into
mainfrom
claude/project-thread-tmn7yk

Conversation

@mindsocket

Copy link
Copy Markdown
Owner

Requested by Rog · project thread

What

This PR gives a render plugin what it needs from core, so it doesn't have to import structured-context at runtime. It is the core prep for the map-views HTML render plugin.

  • Render formats now come from every available plugin. That covers the plugins a space declares plus any other plugin in a config root's .sctx/plugins folder. render and render list both use this list.
  • A plugin's config is validated only when one of its formats is rendered. loader.ts splits into availablePlugins (unvalidated { plugin, rawConfig }) and configurePlugin, which runs the old validation. loadPlugins still validates every declared plugin up front, so parse and templateSync behave as before.
  • Render hooks get two new callbacks in context.callbacks:
    • validate(options) runs validateSpace on the whole space and returns its issues. It reuses the read the render already did. The options are includeGated, ruleFilter, semantic and depth.
    • exportGraph() returns the rendered graph in the space graph format. The graph is filtered, and mainHierarchy is set to the hierarchy the render selected.
  • discoverPlugins takes optional config roots. Render adds the space's own config root to the loaded ones.

Review notes

  • Public surface: PluginContext.callbacks gains validate and exportGraph. These are the two callbacks agreed in the thread. resourceDirs from the same proposal is left for a follow-up PR.
  • Layering: the type of validate's options comes from ValidateSpaceOptions (Pick), which adds a type-only import from plugins/util up to validate/. The layer test checks only runtime imports, so it passes. The other option was restating the four fields in plugins/ or moving CommonValidateOptions down a layer. Tell me if you'd rather have one of those.

Tests

  • New tests/render/render-plugins.test.ts with fixture plugins the space doesn't declare. It covers:
    • rendering from a discovered plugin
    • the issues validate returns
    • the filtered exportGraph
    • a lazy config failure
    • render list
  • bun run test, typecheck and lint all pass.

Docs: docs/api.md (render hooks) and docs/concepts.md (Plugin).


Generated by Claude Code

…ph callbacks

Render formats now come from every available plugin, not only those a
space declares. A plugin's config is validated when its format is
rendered, so an undeclared plugin with required config fails only then.

Render hooks get context.callbacks.validate(options) and exportGraph(),
so a render plugin needs no runtime import of structured-context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XphpeFwEYm8K11SoT9ewz
@claude

claude Bot commented Nov 13, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Comment thread docs/api.md Outdated
};
```

A render hook gets two callbacks in `context.callbacks`, so a plugin needs no runtime import of structured-context:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Run doc-review skill on these changes. A new stop hook will enforce that pre-commit in future

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I ran doc-review on this. It made one change: the validate bullet no longer lists the option fields, which the code already defines. It also now gives the reason for passing semantic only per run (cost). The comments in the changed code passed.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is still too verbose. It doesn't need to talk about "configured" plugins etc. Think about what an API consumer needs to know that isn't already in the code comments

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Trimmed in 2bf4742. The section now says only what the code comments don't:

  • A plugin in a plugins folder renders for every space.
  • The callbacks are there so a plugin doesn't import core at runtime.
  • semantic must come from the run, never from plugin config.

The option list, the return shape and the lazy-config note are gone.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The callbacks are for a positive purpose, to provide functionality that might be needed, not to avoid an import.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, fixed in 0d79dee. The line now says what the callbacks do: "A render hook can validate the space and export the rendered graph through context.callbacks.validate() and exportGraph()."


Generated by Claude Code

Comment thread docs/concepts.md Resolved
Comment thread src/commands/render.ts Resolved
Comment thread src/plugins/loader.ts Resolved
Comment thread src/plugins/loader.ts Resolved
Comment thread src/plugins/loader.ts Outdated
* Validate a plugin's config block against its configSchema. Fields annotated with
* format:'path' are resolved relative to configDir first.
*/
export function configurePlugin({ plugin, rawConfig }: DeclaredPlugin, configDir: string): LoadedPlugin {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Terminology here is confusing. Available, declared, configure, validate, loaded. Conceptually makes sense but feels convoluted. Better names may help, a diagram wouldn't hurt in docs, but consider first if the concepts are clean enough and simple for the purpose

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I've cut the terms down to two: discover (find modules) and load (modules plus validated config). available, declared (now only an internal helper), and configure are gone. See my reply on DeclaredPlugin for the details. With two verbs I don't think a diagram earns its place, but say if you'd still like one.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is better but declared is still odd internally. It includes external plugins (ie not "declared" by my assumed definition) and discoverPlugins is just a thin wrapper?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, "declared" was the wrong word. In bc4bc67 the internal helper is now spacePlugins, the plugins a space uses: the ones its plugin map names (resolved from the plugins folder, then npm), followed by the built-ins. The words "declared" and "external" no longer appear in the loader's comments, except in resolveExternalPlugin, which predates this PR.

On thin wrappers, here is how the layers fit together:

  • spacePlugins: the plugins a space uses. This is the core list.
  • loadPlugins = spacePlugins plus each plugin's validated config. It's thin, but read and template-sync both call it, and it keeps their order and validation in one place.
  • discoverPlugins = spacePlugins (or just the built-ins when no space is given) plus a scan of the plugins folders. The scan is real work, so I don't think this one is thin.

If you meant that loadPlugins should go, I can inline it into its two callers. That would duplicate the map-and-validate step, though, so I'd keep it.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

One remaining confusion. I recall that the plug-in loader looks for any available sctx-prefixed module. Not sure where that happens. Related, a function mentions "external", which might be ambiguous. It's called from spacePlugins. Unclear relationship between space declared plugins, plugins in a .sctx/plugins dir and bun installed sctx-* plugins

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I traced it. Nothing scans for installed sctx-* packages, on main or in this PR. A package is imported only when a space names it in plugins. The only scan is readdirSync over a config root's .sctx/plugins folder, in discoverPlugins here and in list-schemas.ts for schemas. resolvePluginDir in plugins/util.ts falls back to node_modules, but only for a name it's given, to find schemas.

In 3bf621c:

  • resolveExternalPlugin → importPlugin. It imports one plugin by name, from the plugins folder if it's there, else as a package. "External" no longer appears in the loader.
  • docs/config.md › Plugins has a table of the three sources. It owns this, so I dropped the api.md sentence that repeated part of it.
Plugin Parse and template sync Render formats
Built-in Every space Every space
In a plugins folder Spaces that name it in plugins Every space
Installed package Spaces that name it in plugins Spaces that name it in plugins

If you'd like installed sctx-* packages to show up in render too, that would mean scanning node_modules or package.json dependencies, which is a new mechanism. I'd leave it for its own PR.


Generated by Claude Code

discoverPlugins takes an optional space (plugin map and config root) and
then includes its declared plugins. validatePluginConfig validates one
plugin's block from the map, and loadPlugins uses it. render/ owns
listing formats (renderFormats), so the command no longer builds the
registry. The render-format note moves from concepts.md to api.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XphpeFwEYm8K11SoT9ewz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XphpeFwEYm8K11SoT9ewz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XphpeFwEYm8K11SoT9ewz
Rename resolveExternalPlugin to importPlugin, and add a table to
docs/config.md showing when built-in, plugins-folder and installed
plugins are used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XphpeFwEYm8K11SoT9ewz
@mindsocket
mindsocket merged commit 68dfbcf into main Nov 14, 2026
4 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.

2 participants