Skip to content
Draft
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
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run --cwd packages/map-views build:library
- run: bun run lint
- run: bun run typecheck
- run: bun run test
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Declarative map views of structured-context spaces: the definition format, the r
- `packages/map-views/src/layouts`: React components and the SVG edge overlay. Don't import host code from here.
- `packages/map-views/src/viewer`: the page-filling viewer that hosts share. It knows nothing about any one host: anything host-specific (such as card labels) is an option the host passes. Hosts (`src/artifact`, `src/page`) are imported by nothing else.
- `packages/map-views/src/node`: Node-only definition discovery (`loadMapViewDefinitions`), shipped as its own entry point (`structured-context-map-views/node`). Core and layouts must not import it.
- `packages/sctx-map-views`: the structured-context render plugin (format `map-views.html`), bundling the library's page template and `/node` entry. It reads everything through the render callbacks and imports structured-context for types only.
- `apps/navigator`: the standalone host (private package: a Vite app plus dev-server middleware calling the structured-context API). It consumes the library like any other host.
- `packages/map-views/map-views/*.json5`: definitions bundled with the library, only for structured-context's bundled schemas (`$id` `sctx-map-view://core/<name>`). `bun run generate:definitions` embeds them as `src/node/bundled-definitions.ts`; a test fails if that is stale. Plugins ship their own; personal ones go in a `map-views/` folder next to the structured-context config.
- `packages/map-views/src/core/format/*.ts`: the definition format's JSON Schemas (`as const`), the source of truth. Types derive from them (`FromSchema`); `bun run generate:schemas` writes `schemas/generated/latest/` at the repo root (so the published URLs stay stable), and a test fails if those are stale; `--release` (run when cutting a release) also freezes `schemas/generated/<version>/` if the format changed. `$id`s are the published https URLs (`schema-url.ts`). Use absolute `${SCHEMA_URL}/…` refs: `FromSchema` doesn't resolve relative refs inside referenced schemas.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This repo holds:
- **The library** (`packages/map-views`, package `structured-context-map-views`): React components that render a map view inside any host app, using the browser's own layout engine plus an SVG overlay for connectors.
- **An `export` command** (`structured-context-map-views export <space>`): a JSON snapshot of a space (graph, validation results, map view definitions) for rendering a map view in a host that can't run structured-context.
- **A Claude artifact host** (`dist/artifact.html`, `dist/artifact.js`): publishes a space's map views as a private artifact whose data lives in the artifact database.
- **A render plugin** (`packages/sctx-map-views`, package `sctx-map-views`): `sctx render <space> map-views.html` writes a space and its map views as one self-contained interactive HTML file.
- **The navigator** (`apps/navigator`, not published): a standalone app for browsing your spaces through their map views.

Pre-1.0. Docs:
Expand Down
21 changes: 19 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ Map views take structured-context's `SpaceGraph`. A Node host (such as the Obsid

`dist/artifact.html` is a page that needs no edits: publish it as a Claude artifact declaring the `db` capability, then write each space as `spaces/<space>` from the `export` document: `{ schemaId, graph, validation, definitions }`, the last three as JSON strings. The page lists the `spaces` collection, so one artifact can show several spaces, and refreshing is writing the documents again. The page pins this package's version (loaded from jsDelivr) and carries the styles inline. Comments on a card report its accessible name, `[<index>] <title> · <view>`; the index is the node's position in the stored graph's `nodes` array.

## Rendering to an HTML file

`sctx-map-views` is a structured-context plugin. Name it in a space's `plugins`, or put it in the config root's plugins folder, and `sctx render <space> map-views.html -o map.html` writes one HTML file with the page, its scripts and the space's data inlined. Definitions come from the same places as everywhere else. Validation runs with the `-d` options `semantic`, `depth`, `ruleFilter` (comma-separated) and `includeGated`.

## Validation results

`annotationsFrom(input, graph)` attaches structured-context's validation issues to the graph's nodes. Pass it a `ValidationResult` or the output of `sctx validate --json` as it is. It returns the `annotations` to pass to `MapView`, and the `unattached` issues, which have no node in the graph. The definition's `nodes.validation` and `nodes.validationSources` choose which annotations show.
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ flowchart LR
| `packages/map-views/src/layouts` | `MapView`, the layout components, `NodeCard`, `EdgeOverlay`, the parameter controls (`useParameterControls`, the typed inputs and the default `ParameterControls` layout), styles | core, React |
| `packages/map-views/src/viewer` | `Viewer`, the page-filling UI that hosts share. What is specific to a host comes in as an option | core, layouts, React, `structured-context/browser` |
| `packages/map-views/src/node` | `loadMapViewDefinitions`: finding definitions in the library (embedded at build time from `packages/map-views/map-views/`), plugins and config folders; `exportSnapshot` and the `export` bin, which snapshot a space for browser-only hosts; `fillPage`, which writes snapshots into the self-contained page | core, Node, structured-context |
| `packages/sctx-map-views` | The structured-context render plugin: format `map-views.html`. Takes the graph, validation issues and definition folders from the render callbacks, and writes them into the page template | `structured-context-map-views` and its `/node` entry, structured-context API types |
| `apps/navigator` | The standalone host: a Vite app plus dev-server middleware calling the structured-context API | Anything |

`packages/map-views/src/index.ts` is the published entry point and re-exports the few things hosts need (modules import each other directly, with no barrel files); `src/node` is the second entry point, and `src/node/cli.ts` is the bin. The library build puts the projections and annotations in a chunk of their own, so bundling just those from the package root leaves out React and ajv. `packages/map-views/tests/module-boundaries.test.ts` enforces the core, layouts and viewer rules, including that none imports the Node module or a host. Core runs anywhere (browser, Node, worker) and is tested without a DOM, against fixtures captured from spaces (`packages/map-views/tests/fixtures/`, written by `packages/map-views/scripts/capture-fixtures.ts`). The public sample space in `samples/launch-playbook/` is the source of `launch-playbook.json`.
Expand Down Expand Up @@ -57,5 +58,5 @@ Hosts theme map views through nine variables: `--mv-color-surface`, `--mv-color-

- **Navigator** (`apps/navigator`): dev-server middleware serves spaces (from the structured-context config), the definitions `loadMapViewDefinitions` finds, and, when a map view asks for them, validation results (`validateSpace`, expression rules only), as JSON. The chosen space, map view and parameter values live in the query string, so a reload restores them. Selecting a node opens a side panel with its fields, content and validation results.
- **Claude artifact** (`packages/map-views/src/artifact`, shipped as `dist/artifact.js` with its page `dist/artifact.html`): a host with no server. The publishing agent runs the `export` command and writes each space to the artifact's database (`spaces/<space>`: `schemaId`, and `graph`, `validation` and `definitions` as JSON strings). The page lists that collection and shows it in the `Viewer`. It declares only the `db` capability and shows a message when the database is unavailable, empty or slow (10 s). Claude's artifact viewer allows scripts only from a few CDNs and blocks external stylesheets, so `scripts/build-artifact-page.ts` writes a page that carries the CSS inline and loads `dist/artifact.js` (pinned to this package's version) and its dependencies from jsDelivr through an import map; `react` and `react-dom` are pinned to one version so the page runs one React. The host names cards `[<index>] <title> · <view>` through the `Viewer`'s `cardLabel` option, where the index is the node's position in the stored graph's `nodes` array, because the artifact viewer's comment mode reads a comment's location from it (cut at about 45 characters, too short for a node id), so a comment tells an agent which node it concerns. Refreshing the data is writing the documents again; the page doesn't change.
- **Self-contained page** (`packages/map-views/src/page`): a host that needs no server or network. The build inlines it, React and the CSS into the template `dist/page.html`, and `fillPage` writes snapshots into it.
- **Self-contained page** (`packages/map-views/src/page`): a host that needs no server or network. The build inlines it, React and the CSS into the template `dist/page.html`, and `fillPage` writes snapshots into it. The `sctx-map-views` plugin bundles that template.
- **Obsidian plugin** ([obsidian-structured-context](https://github.com/mindsocket/obsidian-structured-context)): renders map views in a pane, builds the data in-process, opens the source note on selection and keeps parameter values in the view state.
6 changes: 4 additions & 2 deletions packages/map-views/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@
"types": "./dist/types/node/index.d.ts",
"default": "./dist/node.js"
},
"./styles.css": "./dist/styles.css"
"./styles.css": "./dist/styles.css",
"./page.html": "./dist/page.html"
},
"scripts": {
"clean": "rm -rf dist",
"generate": "bun run generate:schemas && bun run generate:definitions",
"generate:schemas": "bun scripts/generate-schemas.ts",
"generate:definitions": "bun scripts/generate-definitions.ts",
"build": "bun run clean && bun run generate && bun --bun vite build && tsc -p tsconfig.build.json && tailwindcss -i src/layouts/styles.css -o dist/styles.css && bun scripts/build-page.ts && bun scripts/build-artifact-page.ts",
"build:library": "bun run clean && bun run generate && bun --bun vite build && tsc -p tsconfig.build.json && tailwindcss -i src/layouts/styles.css -o dist/styles.css && bun scripts/build-page.ts",
"build": "bun run build:library && bun scripts/build-artifact-page.ts",
"typecheck": "tsc --noEmit",
"capture-fixtures": "bun scripts/capture-fixtures.ts",
"test": "bun test",
Expand Down
18 changes: 14 additions & 4 deletions packages/map-views/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const DEFINITIONS_DIR = 'map-views';
const CONFIG_PLUGINS_DIR = 'plugins';
const PACKAGE_NAME = 'structured-context-map-views';

type Source = { kind: 'core' } | { kind: 'plugin'; plugin: string } | { kind: 'config' };
export type DefinitionSource = { kind: 'core' } | { kind: 'plugin'; plugin: string } | { kind: 'config' };

export type LoadMapViewDefinitionsOptions = {
/** Directory of the main config file. Defaults to where structured-context finds it. */
Expand All @@ -39,17 +39,25 @@ export function loadMapViewDefinitions(
const configDir = options.configDir ?? dirname(resolve(resolveConfigPath().path));
const configDirs = [...new Set([configDir, ...config.spaces.flatMap((s) => (s.sourceDir ? [s.sourceDir] : []))])];

const folders: { dir: string; source: Source }[] = [
const folders: { dir: string; source: DefinitionSource }[] = [
...pluginDirs(config, configDir, configDirs).flatMap(([plugin, dir]) => {
const definitions = pluginDefinitionsDir(dir);
return definitions ? [{ dir: definitions, source: { kind: 'plugin', plugin } as const }] : [];
}),
...configDirs.map((dir) => ({ dir: join(dir, DEFINITIONS_DIR), source: { kind: 'config' } as const })),
];

return readDefinitionFolders(folders);
}

/**
* Definitions in `folders` and this package's embedded ones, each checked against the namespace of its source.
* Throws on the first invalid definition or a repeated id.
*/
export function readDefinitionFolders(folders: { dir: string; source: DefinitionSource }[]): MapViewDefinition[] {
const foundAt = new Map<string, string>();
const definitions: MapViewDefinition[] = [];
const add = (label: string, text: string, source: Source) => {
const add = (label: string, text: string, source: DefinitionSource) => {
const definition = parseMapViewDefinition(label, text);
assertIdNamespace(definition.id, source, label);
const existing = foundAt.get(definition.id);
Expand Down Expand Up @@ -111,7 +119,7 @@ function pluginDefinitionsDir(pluginDir: string): string | undefined {
return join(pluginDir, DEFINITIONS_DIR);
}

function assertIdNamespace(id: string, source: Source, path: string): void {
function assertIdNamespace(id: string, source: DefinitionSource, path: string): void {
const segments = id.slice(SCHEME.length).split('/');
const expected =
source.kind === 'core'
Expand All @@ -131,3 +139,5 @@ function assertIdNamespace(id: string, source: Source, path: string): void {
function isDirectory(path: string): boolean {
return existsSync(path) && statSync(path).isDirectory();
}

export { fillPage } from './page';
40 changes: 40 additions & 0 deletions packages/sctx-map-views/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "sctx-map-views",
"version": "0.0.0",
"description": "structured-context render plugin: a space's map views as one self-contained interactive HTML file.",
"license": "MIT",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/mindsocket/structured-context-map-views.git",
"directory": "packages/sctx-map-views"
},
"keywords": [
"structured-context",
"sctx",
"plugin",
"map-view"
],
"files": [
"dist/"
],
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"clean": "rm -rf dist",
"build": "bun run clean && bun run --cwd ../map-views build:library && bun scripts/build.ts && tsc -p tsconfig.build.json",
"typecheck": "tsc --noEmit",
"test": "bun test"
},
"peerDependencies": {
"structured-context": "^0.24.0"
},
"devDependencies": {
"structured-context": "^0.24.0",
"structured-context-map-views": "workspace:*"
}
}
13 changes: 13 additions & 0 deletions packages/sctx-map-views/scripts/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** Bundles the plugin into dist/index.js: the map views library and the page template are inlined, structured-context is not. */

import { join } from 'node:path';

const root = join(import.meta.dir, '..');
const result = await Bun.build({
entrypoints: [join(root, 'src/index.ts')],
outdir: join(root, 'dist'),
target: 'node',
format: 'esm',
external: ['structured-context', 'structured-context/*'],
});
if (!result.success) throw new AggregateError(result.logs, 'Bundling the plugin failed');
4 changes: 4 additions & 0 deletions packages/sctx-map-views/src/html.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module 'structured-context-map-views/page.html' {
const text: string;
export default text;
}
13 changes: 13 additions & 0 deletions packages/sctx-map-views/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { StructuredContextPlugin } from 'structured-context/api';
import { renderHtml } from './render';

const plugin: StructuredContextPlugin = {
name: 'sctx-map-views',
configSchema: { type: 'object', additionalProperties: false },
render: {
formats: [{ name: 'html', description: 'Self-contained interactive HTML page of the space and its map views' }],
render: (context, _graph, { data }) => renderHtml(context, data),
},
};

export default plugin;
Loading
Loading