Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/skills/doc-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Skills and references (`plugin/skills/**`), also:
Code comments and messages:

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. Leave it out.

## Output

Expand Down
66 changes: 14 additions & 52 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,20 @@ see [architecture.md](architecture.md#information-flow) for that flow.
| `loadConfig()` | Load the resolved config (spaces, schemas) from the standard config locations. |
| `createSpaceContext(spaceName, config, options?)` | Build a `SpaceContext` for a named space (resolves bundled, local, or plugin schemas). |
| `readSpace(context)` | Read a space into a flat `ReadSpaceResult` (`nodes`, `parseIssues`, `diagnostics`, `source`). |
| `loadSpaceGraph(spaceName, config, options?)` | Read, assemble, and optionally filter a space into a navigable `SpaceGraph`. `options.hierarchy` names the hierarchy that structures the graph (default: the main hierarchy). |
| `assembleSpaceGraph(context, options?)` | Assemble (and optionally filter) a graph from a context you already hold — reuses the context instead of rebuilding it. |
| `loadSpaceGraph(spaceName, config, options?)` | Read, assemble, and optionally filter a space into a navigable `SpaceGraph`. |
| `assembleSpaceGraph(context, options?)` | Assemble (and optionally filter) a graph from a context you already hold. |
| `buildSpaceGraph(nodes, hierarchy?)` | Build a navigable `SpaceGraph` directly from an array of `SpaceNode`s and a `Hierarchy` definition. |
| `exportSpaceGraph(nodes, metadata)` | Turn nodes and the schema's hierarchies into plain JSON for another process. See [Space graph format](#space-graph-format). |
| `spaceGraphFrom(data, options?)` | Build a `SpaceGraph` from that JSON. `options.hierarchy` names the hierarchy that structures it (default: the main hierarchy). |
| `validateSpace(context, options?)` | Run full validation (schema, references, rules, duplicates) over a space. Semantic rules run only when `semantic` is passed; it takes the same values as `sctx validate --semantic`, with the provider coming from config and the environment. |
| `spaceGraphFrom(data, options?)` | Build a `SpaceGraph` from that JSON. |
| `validateSpace(context, options?)` | Run full validation (schema, references, rules, duplicates) over a space. |
| `validateFile(filePath, config)` | Validate a single file within its space. |
| `listSchemas(options?)` | List the schemas available to a config: bundled, plugin-contributed and configured. |
| `formatWikilink(target, alias?)` | Format a target into an Obsidian wikilink (`[[target]]` or `[[target|alias]]`). |
| `parseWikilink(link)` | Parse a wikilink or target string into `{ target, anchor?, displayText? }`. |
| `wikilinkToTarget(link)` | Extract the plain target lookup key from a wikilink string. |
| `buildTargetIndex(nodes)` | Build a fast lookup index mapping link target strings to nodes (marking ambiguous targets as `null`). |

`ValidationResult.issues` lists every [validation issue](concepts.md#validation-results-and-nodes) the run found, and `validateFile` returns one node's issues. Only `error` issues are failures. Read each issue's `severity` rather than deriving it from `kind`. `VALIDATION_ISSUE_KINDS` lists the kinds at runtime.

A `SpaceGraph` classifies nodes against one hierarchy, and indexes child nodes and typed edges in both
directions.
Read each [validation issue](concepts.md#validation-results-and-nodes)'s `severity` rather than deriving it from `kind`.

## Space graph format

Expand Down Expand Up @@ -75,30 +72,19 @@ import {

const config = loadConfig();

// Common path: read, assemble, and filter in one call.
// `filter` resolves a named view from the space config, falling back to a raw filter expression.
const graph = await loadSpaceGraph('my-space', config, { filter: 'my-view' });

// Read once, then assemble a graph and validate from the same read and context.
// Reusing the context avoids re-reading the space *and* recompiling the schema.
// Read once; assemble and validate from the same read.
const context = createSpaceContext('my-space', config);
const readResult = await readSpace(context);
const sharedGraph = await assembleSpaceGraph(context, { readResult });
const validation = await validateSpace(context, { readResult });
```

## Validation vs. graph assembly

`loadSpaceGraph` keeps every node whose type the schema defines, including nodes with field errors
such as an invalid enum value. Nodes of an unknown type are left out. The graph is *not* a validation
pass: use `validateSpace` (or `validateFile`) for field errors, broken references, rule violations and
duplicates.

## Hand-assembled config

`config` does not have to come from `loadConfig()` — you can construct a `Config` in memory and pass
it to `createSpaceContext`, `loadSpaceGraph`, or `validateFile`, the same way the CLI does. Provide
each space's `path` and a resolvable `schema` (absolute paths are simplest):
A `Config` can be built in memory. Pass `configDir` with it, since relative paths in plugin config resolve from the
[config root](config.md#config-file-location) and an in-memory config has none:

```ts
import { loadSpaceGraph, type Config } from 'structured-context/api';
Expand All @@ -109,38 +95,14 @@ const config: Config = {
const graph = await loadSpaceGraph('my-space', config, { configDir: process.cwd() });
```

Relative paths in a space's plugin config are resolved against the [config root](config.md#config-file-location)
of the file that defined the space. A hand-assembled `Config` has no such file, so you must pass an explicit
`configDir` to anchor them (`createSpaceContext`, `loadSpaceGraph`, and `validateFile` all accept it).
Omitting it throws rather than silently guessing the working directory — prefer absolute paths so the
anchor never matters.

## Resources

`resolveConfigPath()` returns the config's `root` along with its path. `resourceDir(root, name)` returns the
folder holding one kind of resource for that root (such as `plugins` or `map-views`), or `undefined` when
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.

## Plugin render hooks
`resourceDir(root, name)` finds one kind of resource (such as `plugins` or `map-views`) for a config root, by
the [resources folder](config.md#resources-folder) layout. A tool that adds its own kind of resource uses it too.

Plugins declare render formats via `render.formats` and implement `render(context, graph, options)`. The hook may return a single string or multi-file output:
A plugin ships its own resources in `<pluginRoot>/<kind>/`, or in the folder its `package.json` names under
`"sctx": { "<kind>": "…" }`. Plugin schemas use the `schemas` kind.

```ts
import type { RenderHook, RenderResult, RenderMultiFileOutput } from 'structured-context/api';

const myRenderHook: RenderHook = {
formats: [{ name: 'my-format', description: 'Outputs multiple files' }],
render(context, graph, options) {
return {
files: [
{ path: 'section-1.md', content: '...' },
{ path: 'subfolder/section-2.md', content: '...' },
],
};
},
};
```

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.
## Plugin render hooks

A plugin adds formats to `sctx render` with a `render` hook (`RenderHook`).
13 changes: 2 additions & 11 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,24 +472,15 @@ Status is required on all node types at _validation_ time. Note however that cur

## Plugin

A **plugin** is a module that extends the tool's capabilities.

Plugins that support parsing produce raw `SpaceNode[]` results given a suitable configuration. Graph edge resolution (`resolveGraphEdges`) is called by the core afterwards.

The `plugins` field in config is a **map** from plugin name to plugin config object. All plugin names must start with `sctx-`, but the prefix is optional in config and normalised on load. Built-in plugins take precedence and all are loaded by default. External plugin names specified in config are then resolved in order:

1. The config root's plugins folder: `.sctx/plugins/{sctx-name}`
2. npm: a package matching `sctx-*`

Each plugin declares a `configSchema` JSON Schema; the loader validates the config block against it before invoking the plugin. Config fields annotated with `format: 'path'` are resolved relative to the config root by the loader.
A **plugin** is a module that reads spaces from a source (parse), syncs templates, or renders a space in a format. The `plugins` config names the plugins a space uses and holds each one's config. See [Plugins](config.md#plugins).

### Config root

The **config root** is the folder a config file's relative paths resolve from: the folder holding `.sctx.json` or `.sctx/`, or the global config's folder. Its **resources folder** (`.sctx/`, or the global config's folder) holds plugins, schemas and map view definitions. In code, `configDir` and `SpaceConfig.sourceDir` hold a config root. See [Resources folder](config.md#resources-folder).

### Dispatch contract

Plugins are called in order (external plugins first, then built-ins). For each operation:
For parse and template sync, plugins are called in order: those named in config first, then the built-ins.
- A plugin that does not implement the hook is skipped.
- A plugin that implements the hook returns `T | null`: non-null means "I handled it"; null means "not me, try the next plugin."
- The first non-null result wins. If no plugin handles the operation, the command throws an error.
Expand Down
10 changes: 4 additions & 6 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ A config must define at least one of `spaces` or `includeSpacesFrom`. When using

## Plugins

Use `plugins` to load parse plugins that read spaces from non-markdown sources. The built-in markdown plugin is always available without any declaration. Plugins are tried in order; the first to return a result wins. The `plugins` field is a map of plugin name to plugin config, and can be declared at the top level (applies to all spaces) or per-space (overrides the top level):
`plugins` maps a plugin name to its config. Declare it at the top level for every space, or on a space to override the top level:

```json
{
Expand All @@ -83,23 +83,21 @@ Use `plugins` to load parse plugins that read spaces from non-markdown sources.
}
],
"plugins": {
"sctx-confluence": { "baseUrl": "https://example.atlassian.net" }
"sctx-example": { "baseUrl": "https://example.com" }
}
}
```

All plugin names must start with `sctx-` (the prefix is optional in config and normalised on load). The special name `markdown` refers to the built-in markdown plugin. A plugin named in `plugins` is resolved in order: the root's plugins folder (`.sctx/plugins/{name}`, see [Resources folder](#resources-folder)), then npm. Each plugin must export a `configSchema` JSON Schema; config is validated against it on load. Fields annotated `format: 'path'` in a plugin's `configSchema` are resolved relative to the config root.
Plugin names start with `sctx-`, which config may leave out. `markdown` is the built-in markdown plugin. A named plugin is found in the root's plugins folder (`.sctx/plugins/<name>`), else as an installed package. Its config is checked against the plugin's schema, and relative paths in it resolve from the config root.

A plugin comes from one of three places, and where it comes from decides when it is used:
Where a plugin comes from decides when it is used:

| Plugin | Parse and template sync | Render formats |
|---|---|---|
| Built-in | Every space | Every space |
| In a plugins folder (`.sctx/plugins/sctx-*`) | Spaces that name it in `plugins` | Every space |
| 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.

## Model providers and semantic rules

structured-context evaluates [semantic rules](semantic-rules.md) with a **model provider**: a named definition that says which wire API to speak, which model to use and where. A provider definition combines an `api`, explicit values or environment-variable names, and the key source.
Expand Down
Loading
Loading