Skip to content

feat(render): resourceDirs callback - #236

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

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

Conversation

@mindsocket

Copy link
Copy Markdown
Owner

Requested by Rog · project thread

What

This is the third render callback, agreed alongside validate and exportGraph (#232). A render plugin uses it to find resource folders such as map-views without importing core.

context.callbacks.resourceDirs(kind) returns, in order:

  1. each config root's resource folder of that kind (resourceDir), and
  2. the folder of each plugin the space names or that a plugins folder holds, together with the plugin's short name.

A plugin's resource folder follows the convention schemas already use: "sctx": { "<kind>": "…" } in its package.json, otherwise <pluginDir>/<kind>.

  • pluginResourceDir(pluginDir, kind) (in plugins/util.ts) now holds that rule, and resolvePluginSchemasDir uses it too.
  • spacePluginNames(space, config) is extracted from resolveSpacePluginSchemas, so schemas and resourceDirs work from the same list of plugin names (schema prefix, space plugins, global plugins).
  • discoverPlugins and resourceDirs now share small configRoots and pluginsFolderEntries helpers in the loader.

Review notes

  • Public surface: this adds one field, PluginContext.callbacks.resourceDirs. pluginResourceDir and spacePluginNames are internal and not exported from api.ts.
  • Effect on map-views: its loadMapViewDefinitions reads "sctx": { "mapViews": … } today. Under the shared convention that key becomes "sctx": { "map-views": … }. The sctx-map-views plugin slice will switch to it.

Tests

  • tests/render/render-plugins.test.ts now covers resourceDirs('map-views'), which returns the config root's .sctx/map-views and a plugin's map-views folder.
  • bun run test, typecheck and lint pass, and doc-review has been run on the two doc lines (docs/api.md, docs/config.md).

Generated by Claude Code

Render hooks get context.callbacks.resourceDirs(kind): each config
root's resource folder of that kind, then each plugin's, tagged with
the plugin's short name. A plugin's resource folder follows the
convention schemas already use (package.json sctx.<kind>, else
<pluginDir>/<kind>), now in pluginResourceDir. spacePluginNames is
extracted from resolveSpacePluginSchemas so both share one list.

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

claude Bot commented Nov 14, 2026

Copy link
Copy Markdown

Code review

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

Comment thread src/plugins/util.ts Outdated
) => Promise<ValidationIssue[]>;
/** Render only: the graph being rendered, in the space graph format. */
exportGraph?: () => SpaceGraphData;
/** Render only: every folder of one kind of resource (such as `map-views`), with the plugin providing it if any. */

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.

Why render only? Run doc-review skill on this PR

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.

And check against definition of done

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.

You're right that resourceDirs didn't need to be render-only. In 76fe6b8, parse and template sync get it too (read-space.ts, commands/template-sync.ts). The api.md sentence now sits under Resources, beside resourceDir, rather than under render hooks.

validate and exportGraph stay render-only:

  • validate reads the space through the parse hook, so offering it during parse would recurse.
  • exportGraph exports the graph being rendered, which only exists in render.

doc-review: I tightened the new api.md line, and the comments and the config.md sentence passed. Definition of done: no CLI, config key or term changes, so there was nothing else to update. The prose gate is marked.


Generated by Claude Code

Parse and template sync get callbacks.resourceDirs too; validate and
exportGraph stay render-only.

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

claude Bot commented Nov 14, 2026

Copy link
Copy Markdown

Code review

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

Revert resourceDirs for parse and template sync. Remove the "Render
only" notes from the callback types, since where callbacks are passed
is decided by their callers. doc-review gains a check for comments that
state a fact another module decides.

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

Copy link
Copy Markdown
Owner Author

This is about the "Why render only?" comment. A pending review of yours on this PR blocks thread replies, so I'm replying here.

I misread the comment at first. f6f8d17 corrects that:


Generated by Claude Code

@mindsocket mindsocket left a comment

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'm pretty frustrated that this is not opus-level code quality. What on earth is going on?

Comment thread docs/api.md Outdated
there is none. It follows the [resources folder](config.md#resources-folder) layout, so a tool that adds its
own kind of resource finds it the same way.

A plugin hook finds these folders for its space, including other plugins' folders, with `context.callbacks.resourceDirs(kind)`.

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 doesn't make sense here.

Comment thread .claude/skills/doc-review/SKILL.md Outdated

11. Flag comments that restate code or explain history, and library errors that mention CLI flags.
12. Flag comments that state a fact another module decides, such as where a type is used ("render only") or a
rule enforced elsewhere. Name the owner, or leave it out.

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.

No, just "leave it out", no leniency. The amount of comment noise being added from Claude models is a shit-show and is creating massive review overheads and maintenance burden

Comment thread docs/api.md Outdated
```

A render hook can validate the space and export the rendered graph through `context.callbacks.validate()` and `exportGraph()`. Pass `semantic` to `validate` only when the render run asks for it, never from plugin config.
A render hook can validate the space, export the rendered graph and find resource folders (including other plugins') through `context.callbacks.validate()`, `exportGraph()` and `resourceDirs()`. Pass `semantic` to `validate` only when the render run asks for it, never from plugin config.

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.

Drop this whole sentence. It's not needed in api docs at all. Trim anything from this doc that is already mentioned in code comments, that is intuitively inferred from the API shape, or that is a minor implementation detail or logic.

Comment thread docs/config.md Outdated
| Installed package (`sctx-*`) | Spaces that name it in `plugins` | Spaces that name it in `plugins` |

Plugins can also contribute schemas and partials via a `<pluginRoot>/schemas/` directory (or `"sctx": { "schemas": "..." }` in `package.json`). If a space specifies a plugin-prefixed schema (e.g. `"sctx-wardley-mapping/wardley_map.json"`), schemas are resolved statically without executing plugin code, and the plugin's code hooks are loaded when parsing or rendering.
Plugins can also contribute schemas and partials via a `<pluginRoot>/schemas/` directory (or `"sctx": { "schemas": "..." }` in `package.json`). Other resources follow the same pattern. For example, map view definitions go in `<pluginRoot>/map-views/`, or the folder named by `"sctx": { "map-views": "..." }`. If a space specifies a plugin-prefixed schema (e.g. `"sctx-wardley-mapping/wardley_map.json"`), schemas are resolved statically without executing plugin code, and the plugin's code hooks are loaded when parsing or rendering.

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.

What does this have to do with config?

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.

Treat the codebase as if you own it and don't just avoid making things worse, also clean up docs related to your change that don't meet the style guides and definition of done

Comment thread src/plugins/loader.ts Outdated
return [...named, ...builtinPlugins];
}

/** The roots of the loaded config files (the main config and the configs it includes), after `configDir` if given. */

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.

Is this appropriate design? Does it belong here? What is "loaded"?

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.

Whole file needs deeper consideration of function names, separation of concerns, module deaign, sequencing

Comment thread src/plugins/util.ts Outdated
try {
const declared = JSON.parse(readFileSync(pkgJsonPath, 'utf8')).sctx?.[kind];
if (typeof declared === 'string' && isDir(resolve(pluginDir, declared))) return resolve(pluginDir, declared);
} catch {}

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.

Blind catch?! At least explain why

Comment thread src/plugins/util.ts

return pluginSources;
/** Synchronously collect all plugin schema sources available to a space. */
export function resolveSpacePluginSchemas(

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.

Are all of these new functions getting used?

Loader reads top-down: loadPlugins, discoverPlugins, validatePluginConfig,
then private helpers. resourceDirs moves to plugins/util beside the other
plugin-location helpers and uses the space's config root only.
pluginResourceDir and spacePluginNames are private. No blind catches:
a malformed plugin package.json throws.

Docs: api.md drops what code comments and types already say;
config.md keeps only config; concepts.md Plugin says what a plugin is
and links to config. doc-review check 12 has no exception.

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

Copy link
Copy Markdown
Owner Author

Your review is addressed in e51c30f. Here's what changed, by comment.

loader.ts ("what is loaded", "whole file needs deeper consideration")

  • The file now reads top-down: the public loadPlugins, discoverPlugins and validatePluginConfig come first, then the private namedPlugins, importPlugin and resolveConfigPaths. spacePlugins, configRoots and pluginsFolderEntries are gone.
  • resourceDirs didn't belong in the loader. It now sits in plugins/util.ts, beside resolvePluginDir and resolveSpacePluginSchemas. It uses only the space's config root (context.configDir), with no global "loaded config files" state.

util.ts ("blind catch", "are all of these new functions used")

  • The package.json read has no catch, so a malformed file throws. The remaining try/catch and existsSync/statSync pairs became an isDirectory helper. One catch is left, for require.resolve failing on a package that isn't installed, and it says so.
  • pluginResourceDir and spacePluginNames are private. Each has two callers: the schema lookup and resourceDirs.

Docs ("what does this have to do with config", "doesn't make sense here", "drop this whole sentence", "clean up related docs")

  • config.md › Plugins keeps only config: the map, name rules, where a named plugin is found, and the table. The plugin-resource and static-resolution text is gone. The example no longer names a real plugin.

  • api.md drops the render callback sentence. It also drops whatever code comments or the API shape already cover:

    • the options detail in the table
    • the SpaceGraph and VALIDATION_ISSUE_KINDS lines
    • the "Validation vs. graph assembly" section
    • most of "Hand-assembled config"
    • the render hook example

    The convention for plugin resource folders is one sentence under Resources.

  • concepts.md › Plugin now says what a plugin is and links to config.md. The dispatch contract is scoped to parse and template sync.

  • doc-review check 12 ends with "Leave it out."

Tests, typecheck and lint pass, and doc-review has been run.


Generated by Claude Code

@claude

claude Bot commented Nov 14, 2026

Copy link
Copy Markdown

Code review

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

@mindsocket
mindsocket merged commit fb440ec into main Nov 14, 2026
2 checks passed
@mindsocket
mindsocket deleted the claude/project-thread-tmn7yk branch November 14, 2026 04:34
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