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: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Declarative map views of structured-context spaces: the definition format, the r
## Layout

- `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); `tests/module-boundaries.test.ts` enforces this.
- `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/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.
Expand Down
2 changes: 1 addition & 1 deletion apps/navigator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"react": "^19.3.0",
"react-dom": "^19.3.0",
"structured-context": "^0.21.0",
"structured-context": "^0.22.1",
"structured-context-map-views": "workspace:*"
},
"devDependencies": {
Expand Down
7 changes: 1 addition & 6 deletions apps/navigator/src/UnattachedAnnotationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,7 @@ export function UnattachedAnnotationPanel({
{[...groups].map(([nodeId, items]) => (
<section key={nodeId}>
<h3>{nodeId || 'Space'}</h3>
{items[0]?.location && (
<p className="source muted">
{items[0].location.file}
{items[0].location.line !== undefined && `:${items[0].location.line}`}
</p>
)}
{items[0]?.line !== undefined && <p className="source muted">line {items[0].line}</p>}
<AnnotationList annotations={items} />
</section>
))}
Expand Down
8 changes: 5 additions & 3 deletions apps/navigator/src/server/validate-space.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { type Config, createSpaceContext, type ValidationResult, validateSpace } from 'structured-context/api';
import { type Config, createSpaceContext, nodeIssues, validateSpace } from 'structured-context/api';
import type { ValidationInput } from 'structured-context-map-views';

/**
* Validate a space for the overlay. No judge is passed, so only expression rules run (AI-judged rules
* would cost money and are not requested here).
*/
export function validateForOverlay(spaceName: string, config: Config): Promise<ValidationResult> {
return validateSpace(createSpaceContext(spaceName, config));
export async function validateForOverlay(spaceName: string, config: Config): Promise<ValidationInput> {
const result = await validateSpace(createSpaceContext(spaceName, config));
return { issues: nodeIssues(result), ruleViolations: result.ruleViolations };
}
10 changes: 5 additions & 5 deletions bun.lock

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

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

## Validation results

Hosts that validate a space can show the results on the nodes. Pass `annotationsFrom(input, graph)` structured-context's `ValidationResult`, or just the parts a host has (`ruleViolations`, `hierarchyViolations` and `nodeErrors` are each optional, so a host that runs only some checks passes only those). It returns `{ annotations, unattached }`: an `AnnotationIndex` (results by node id) to pass to `MapView`, and the results with no node on the graph for the host to surface itself.
Hosts that validate a space can show the results on the nodes. Pass `annotationsFrom(input, graph)` each node's issues as `issues`, and optionally the `ruleViolations`. From a `ValidationResult`, that is `{ issues: nodeIssues(result), ruleViolations: result.ruleViolations }`. The output of `sctx validate --json` has both fields, so it can be passed as it is. It returns `{ annotations, unattached }`: an `AnnotationIndex` (results by node id) to pass to `MapView`, and the results with no node on the graph for the host to surface itself.

structured-context names a result's node by `nodeId`. A result is attached when that node is in the graph. It is `unattached` when it has no `nodeId` (a global-scope rule) or its node isn't in the graph, such as a node of an unknown type. Nodes with field errors stay in the graph, so their schema errors attach. An unattached result keeps its `nodeId` and, for schema errors, its source `location`.
structured-context names a result's node by `nodeId`. A result is attached when that node is in the graph. It is `unattached` when it has no `nodeId` (a global-scope rule) or its node isn't in the graph, such as a node of an unknown type. Nodes with field errors stay in the graph, so their schema errors attach. An unattached result keeps its `nodeId` and, where known, its `line` and `column`.

Each annotation has a `severity` (`error`, `warning` or `info`), a `source` (`schema`, `hierarchy` or `rule`) and a `message`. Schema and hierarchy results are errors; rule results carry the rule's severity. Which show, and the lowest severity drawn, are the definition's `nodes.validation` and `nodes.validationSources` settings.
Each annotation has a `severity` (`error`, `warning` or `info`), a `source` and a `message`. The source is structured-context's issue kind: `schema`, `duplicate`, `broken-link`, `hierarchy`, `rule`, `gated` or `content-link`. Severity comes from structured-context with each result. Which show, and the lowest severity drawn, are the definition's `nodes.validation` and `nodes.validationSources` settings.

### Showing the messages

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ flowchart LR

Hosts pass the library structured-context's `SpaceGraph`: nodes keyed by id, the main hierarchy, and every hierarchy and relationship edge with its metadata, readable in both directions (`parentEdges`, `childEdges`). The Obsidian plugin assembles it in-process; the navigator's server sends the wire format (`exportSpaceGraph`) and its browser rebuilds the graph (`spaceGraphFrom`). The graph keeps edges to nodes outside it; the helpers in `core/graph.ts` drop them, so a filtered space stays consistent.

Hosts can also pass validation results as an `AnnotationIndex` (`core/annotations.ts`): results by node id, derived from structured-context's `ValidationResult` types. `MapView` narrows it by the region's `nodes.validation` and `nodes.validationSources` and provides the result to node cards through React context, so layouts don't thread it through. Every layout draws nodes with `NodeCard`, which draws the badge.
Hosts can also pass validation results as an `AnnotationIndex` (`core/annotations.ts`): results by node id, built by `annotationsFrom` from structured-context's per-node issues (`nodeIssues`, or the `issues` of `sctx validate --json`). `MapView` narrows it by the region's `nodes.validation` and `nodes.validationSources` and provides the result to node cards through React context, so layouts don't thread it through. Every layout draws nodes with `NodeCard`, which draws the badge.

## Projections

Expand Down
2 changes: 1 addition & 1 deletion docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ A setting the reader can change. A definition declares each parameter once, as a

## Annotation

A validation result attached to a node: a severity, a source (`schema`, `hierarchy` or `rule`) and a message. Hosts supply annotations; a region's `nodes.validation` and `nodes.validationSources` settings choose which are drawn.
A validation result attached to a node: a severity, a source (the kind of check, such as `schema`, `hierarchy` or `rule`) and a message. Hosts supply annotations; a region's `nodes.validation` and `nodes.validationSources` settings choose which are drawn.

## Host

Expand Down
2 changes: 1 addition & 1 deletion docs/definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ A map view definition is a JSON5 file. Its format is a set of JSON Schemas publi
| `nodes.style` | | `card` (title and fields, the default) or `shape` (a compact marker with a tooltip) |
| `nodes.fields` | | Schema fields shown on cards |
| `nodes.validation` | | The lowest severity of validation result drawn on nodes: `off` (the default), `error`, `warning` or `info`. Results come from the host; with none supplied nothing is drawn |
| `nodes.validationSources` | | Which results are drawn: any of `schema`, `hierarchy`, `rule` (default all three) |
| `nodes.validationSources` | | Which results are drawn: any of `schema`, `duplicate`, `broken-link`, `hierarchy`, `rule`, `gated`, `content-link` (default all) |
| `edges.show` | | Whether connectors are drawn (default `true`) |

## Layouts
Expand Down
4 changes: 2 additions & 2 deletions packages/map-views/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"peerDependencies": {
"react": "^19.3.0",
"react-dom": "^19.3.0",
"structured-context": "^0.21.0"
"structured-context": "^0.22.1"
},
"dependencies": {
"ajv": "^8.20.0",
Expand All @@ -68,7 +68,7 @@
"@vitejs/plugin-react": "^6.1.1",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"structured-context": "^0.21.0",
"structured-context": "^0.22.1",
"tailwindcss": "^4.3.3",
"vite": "^8.3.2"
}
Expand Down
64 changes: 37 additions & 27 deletions packages/map-views/src/core/annotations.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { RuleSeverity, SourceLocation, SpaceGraph, SpaceNode, ValidationResult } from 'structured-context/api';
import type { nodeIssues, RuleSeverity, SpaceGraph, SpaceNode, ValidationResult } from 'structured-context/api';
import type { ValidationIssueKind } from 'structured-context/types';

// core doesn't export NodeId from `structured-context/api` yet; this derives it.
type NodeId = SpaceNode['id'];

/** How serious a result is: structured-context's rule severity. Schema and hierarchy failures are errors. */
/** How serious a result is: structured-context's rule severity, which every result carries. */
export type Severity = RuleSeverity;

/** The kind of check a result came from. */
export type AnnotationSource = 'schema' | 'hierarchy' | 'rule';
/** The kind of check a result came from: structured-context's issue `kind`. */
export type AnnotationSource = ValidationIssueKind;

/** One validation result attached to a node. */
export type NodeAnnotation = {
Expand All @@ -22,22 +23,25 @@ export type NodeAnnotation = {
export type UnattachedAnnotation = NodeAnnotation & {
/** The node it concerns. Absent for a global-scope rule. A node that failed its schema isn't in the graph. */
nodeId?: NodeId;
/** Where that node is defined, if known. */
location?: SourceLocation;
/** Where in that node's file the result points, if known (1-indexed). */
line?: number;
column?: number;
};

/** Annotations by the id of the node they apply to. */
export type AnnotationIndex = ReadonlyMap<NodeId, readonly NodeAnnotation[]>;

/**
* The parts of structured-context's `ValidationResult` that attach to a node. Every part is optional:
* a host that only runs some checks (the Obsidian plugin runs no AI-judged rules, say) passes what it has.
* Validation results to attach: each node's issues, from structured-context's `nodeIssues(result)` or the
* `issues` of `sctx validate --json`, and the rule violations, of which only those without a node are read
* (the others are in `issues`). `sctx validate --json` output can be passed as it is.
*/
export type ValidationInput = Partial<
Pick<ValidationResult, 'ruleViolations' | 'hierarchyViolations'> & {
nodeErrors: ReadonlyArray<Pick<ValidationResult['nodeErrors'][number], 'nodeId' | 'source' | 'issues'>>;
}
>;
export type ValidationInput = {
issues: Readonly<ReturnType<typeof nodeIssues>>;
ruleViolations?: ReadonlyArray<
Pick<ValidationResult['ruleViolations'][number], 'nodeId' | 'ruleId' | 'severity' | 'description'>
>;
};

/**
* Attach validation results to the nodes of `graph`, by node id. Results that can't be attached are
Expand All @@ -50,24 +54,30 @@ export function annotationsFrom(
): { annotations: AnnotationIndex; unattached: UnattachedAnnotation[] } {
const annotations = new Map<NodeId, NodeAnnotation[]>();
const unattached: UnattachedAnnotation[] = [];
const add = (nodeId: NodeId | undefined, annotation: NodeAnnotation, location?: SourceLocation) => {
if (nodeId !== undefined && graph.nodes.has(nodeId)) {
annotations.set(nodeId, [...(annotations.get(nodeId) ?? []), annotation]);
} else {
unattached.push({ ...annotation, nodeId, ...(location && { location }) });
}
};

for (const { nodeId, source, issues } of input.nodeErrors ?? []) {
for (const issue of issues) {
add(nodeId, { severity: 'error', source: 'schema', message: issue.message }, source);
for (const [id, issues] of Object.entries(input.issues)) {
const nodeId = id as NodeId;
for (const [key, { kind, message, severity, line, column }] of Object.entries(issues)) {
const annotation: NodeAnnotation = {
severity,
source: kind,
message,
...(kind === 'rule' && key.startsWith('rule:') && { ruleId: key.slice('rule:'.length) }),
};
if (graph.nodes.has(nodeId)) {
annotations.set(nodeId, [...(annotations.get(nodeId) ?? []), annotation]);
} else {
unattached.push({
...annotation,
nodeId,
...(line !== undefined && { line }),
...(column !== undefined && { column }),
});
}
}
}
for (const { nodeId, description } of input.hierarchyViolations ?? []) {
add(nodeId, { severity: 'error', source: 'hierarchy', message: description });
}
for (const { nodeId, description, severity, ruleId } of input.ruleViolations ?? []) {
add(nodeId, { severity, source: 'rule', message: description, ruleId });
if (nodeId === undefined) unattached.push({ severity, source: 'rule', message: description, ruleId });
}
return { annotations, unattached };
}
Expand Down
3 changes: 2 additions & 1 deletion packages/map-views/src/core/format/definition-schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { VALIDATION_ISSUE_KINDS } from 'structured-context/types';
import { PARAM_REF, parameterisable } from './param-ref';
import { SCHEMA_URL } from './schema-url';

Expand Down Expand Up @@ -121,7 +122,7 @@ export const definitionSchema = {
),
validationSources: parameterisable(
{ $ref: `${SCHEMA_URL}/settings.json#/$defs/validationSources` },
['schema', 'hierarchy', 'rule'],
VALIDATION_ISSUE_KINDS,
'Which kinds of validation result are shown.',
),
},
Expand Down
5 changes: 3 additions & 2 deletions packages/map-views/src/core/format/settings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { VALIDATION_ISSUE_KINDS } from 'structured-context/types';
import { SCHEMA_URL } from './schema-url';
/** Reusable setting definitions. Parameters can `$ref` these and narrow them, e.g. with `allOf` and `enum`. */
export const settingsSchema = {
Expand All @@ -24,9 +25,9 @@ export const settingsSchema = {
validationSources: {
title: 'Validation sources',
description:
'Which kinds of validation result are shown. schema: a node failing its schema. hierarchy: a node under a parent type it is not allowed under. rule: a schema rule violation.',
'Which kinds of validation result are shown. schema: a node failing its schema. duplicate: an id more than one node uses. broken-link: a relationship to a node that does not exist. hierarchy: a node under a parent type it is not allowed under. rule: a schema rule violation. gated: a note that rules were not checked because an ancestor failed. content-link: a link in the body that resolves to no node.',
type: 'array',
items: { enum: ['schema', 'hierarchy', 'rule'] },
items: { enum: VALIDATION_ISSUE_KINDS },
uniqueItems: true,
},
fieldList: {
Expand Down
8 changes: 6 additions & 2 deletions packages/map-views/src/node/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
type Config,
createSpaceContext,
exportSpaceGraph,
nodeIssues,
validateSpace,
} from 'structured-context/api';
import type { ValidationInput } from '../core/annotations';
import { type MapViewDefinition, mapViewsForSchema } from '../core/definition';
import { loadMapViewDefinitions } from './index';

Expand All @@ -16,7 +18,8 @@ export type SpaceSnapshot = {
space: string;
schemaId: string | undefined;
graph: ReturnType<typeof exportSpaceGraph>;
validation: Awaited<ReturnType<typeof validateSpace>>;
/** What `annotationsFrom` attaches: each node's issues and the rule violations */
validation: ValidationInput;
definitions: MapViewDefinition[];
};

Expand All @@ -39,11 +42,12 @@ export async function exportSnapshot(
const context = createSpaceContext(spaceName, config);
const graph = await assembleSpaceGraph(context, {});
const schemaId = context.schema.$id;
const validation = await validateSpace(context, validateOptions);
return {
space: spaceName,
schemaId,
graph: exportSpaceGraph(graph.nodes.values(), context.schema.metadata),
validation: await validateSpace(context, validateOptions),
validation: { issues: nodeIssues(validation, validateOptions), ruleViolations: validation.ruleViolations },
definitions: mapViewsForSchema(loadMapViewDefinitions(config, { configDir }), schemaId),
};
}
Loading
Loading