Repository navigation
feat(render): formats from discovered plugins; validate and exportGraph callbacks - #232
Conversation
…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
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
| }; | ||
| ``` | ||
|
|
||
| A render hook gets two callbacks in `context.callbacks`, so a plugin needs no runtime import of structured-context: |
There was a problem hiding this comment.
Run doc-review skill on these changes. A new stop hook will enforce that pre-commit in future
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
semanticmust 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
There was a problem hiding this comment.
The callbacks are for a positive purpose, to provide functionality that might be needed, not to avoid an import.
There was a problem hiding this comment.
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
| * 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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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=spacePluginsplus 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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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
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.
.sctx/pluginsfolder.renderandrender listboth use this list.loader.tssplits intoavailablePlugins(unvalidated{ plugin, rawConfig }) andconfigurePlugin, which runs the old validation.loadPluginsstill validates every declared plugin up front, so parse and templateSync behave as before.context.callbacks:validate(options)runsvalidateSpaceon the whole space and returns itsissues. It reuses the read the render already did. The options areincludeGated,ruleFilter,semanticanddepth.exportGraph()returns the rendered graph in the space graph format. The graph is filtered, andmainHierarchyis set to the hierarchy the render selected.discoverPluginstakes optional config roots. Render adds the space's own config root to the loaded ones.Review notes
PluginContext.callbacksgainsvalidateandexportGraph. These are the two callbacks agreed in the thread.resourceDirsfrom the same proposal is left for a follow-up PR.validate's options comes fromValidateSpaceOptions(Pick), which adds a type-only import fromplugins/utilup tovalidate/. The layer test checks only runtime imports, so it passes. The other option was restating the four fields inplugins/or movingCommonValidateOptionsdown a layer. Tell me if you'd rather have one of those.Tests
tests/render/render-plugins.test.tswith fixture plugins the space doesn't declare. It covers:validatereturnsexportGraphrender listbun run test,typecheckandlintall pass.Docs:
docs/api.md(render hooks) anddocs/concepts.md(Plugin).Generated by Claude Code