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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Declarative map views of structured-context spaces: the definition format, the r
- `packages/map-views`: the published library, `structured-context-map-views`. Its `src/index.ts` is the public surface; add an export only when a host needs it.
- `packages/map-views/src/core`: framework-agnostic. Definitions, parameters, graph helpers and the projections. No React, DOM, Node or runtime structured-context imports (type-only is fine, and `structured-context/types` is browser-safe); `tests/module-boundaries.test.ts` enforces this.
- `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`) 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.
- `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.
Expand Down
5 changes: 3 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ flowchart LR
|---|---|---|
| `packages/map-views/src/core` | Definition parsing and validation, parameter resolution, graph helpers, and the projections (`buildTree`, `buildLandscape`, `buildXYPlot`) | Types from structured-context only. No React, DOM or Node |
| `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`: a page-filling viewer for space snapshots (space and map view switcher, parameter controls, the map view, a side panel with a node's fields and validation results) and its page styles. Hosts pass the snapshots and options such as card labels | 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 | core, Node, structured-context |
| `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 and layouts rules, including that neither imports the Node module. 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`.
`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`.

## Data in

Expand Down Expand Up @@ -55,5 +56,5 @@ Hosts theme map views through nine variables: `--mv-color-surface`, `--mv-color-
## Hosts

- **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, offers the spaces and their map views, renders `MapView` with the parameter controls, and lists a card's validation results in a side panel. It declares only the `db` capability and shows a message when the database is unavailable, empty or slow (10 s). The 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. Cards are given an accessible name `[<index>] <title> · <view>` (from `data-node-id`), where the index is the node's position in the stored graph's `nodes` array, because the 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.
- **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.
- **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.
2 changes: 1 addition & 1 deletion packages/map-views/scripts/build-artifact-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ for (const url of Object.values(pinnedUrls)) {
}
}

const css = [join(root, 'dist/styles.css'), join(root, 'src/artifact/page.css')]
const css = [join(root, 'dist/styles.css'), join(root, 'src/viewer/viewer.css')]
.map((f) => readFileSync(f, 'utf8'))
.join('\n');
const entry = `${CDN}/${pkg.name}@${pkg.version}/dist/artifact.js`;
Expand Down
26 changes: 9 additions & 17 deletions packages/map-views/src/artifact/load.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
import type { spaceGraphFrom } from 'structured-context/browser';
import type { ValidationInput } from '../core/annotations';
import type { MapViewDefinition } from '../core/definition';

/** A space as the artifact's db holds it: the `export` command's document with its parts as JSON strings. */
export type LoadedSpace = {
name: string;
schemaId: string | undefined;
graph: Parameters<typeof spaceGraphFrom>[0];
validation: ValidationInput;
definitions: MapViewDefinition[];
};
import type { SpaceSnapshot } from '../core/snapshot';

const SPACES = 'spaces';
const TIMEOUT_MS = 10_000;
Expand All @@ -18,8 +7,11 @@ type Db = {
collection(path: string): { get(): Promise<{ docs: { id: string; data(): Record<string, unknown> | undefined }[] }> };
};

/** Every space written to the artifact's `spaces` collection. Rejects with a message fit to show the viewer. */
export async function loadSpaces(): Promise<LoadedSpace[]> {
/**
* Every space written to the artifact's `spaces` collection: the `export` command's document, keyed by space
* name, with its parts as JSON strings. Rejects with a message fit to show the viewer.
*/
export async function loadSpaces(): Promise<SpaceSnapshot[]> {
const claude = (window as unknown as { claude?: { use(name: string): Promise<unknown> } }).claude;
const db = (claude ? await claude.use('db') : null) as Db | null;
if (!db) throw new Error('This page needs the artifact database, which is not available to this viewer.');
Expand All @@ -38,14 +30,14 @@ export async function loadSpaces(): Promise<LoadedSpace[]> {
return spaces;
}

function toSpace(name: string, data: Record<string, unknown>): LoadedSpace {
function toSpace(space: string, data: Record<string, unknown>): SpaceSnapshot {
const json = (field: string) => {
const value = data[field];
if (typeof value !== 'string') throw new Error(`Space "${name}": missing "${field}" in the artifact database.`);
if (typeof value !== 'string') throw new Error(`Space "${space}": missing "${field}" in the artifact database.`);
return JSON.parse(value);
};
return {
name,
space,
schemaId: typeof data.schemaId === 'string' ? data.schemaId : undefined,
graph: json('graph'),
validation: json('validation'),
Expand Down
Loading
Loading