diff --git a/AGENTS.md b/AGENTS.md
index b0b9a0d..713ef9a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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.
diff --git a/apps/navigator/package.json b/apps/navigator/package.json
index 15efa96..30bf3bf 100644
--- a/apps/navigator/package.json
+++ b/apps/navigator/package.json
@@ -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": {
diff --git a/apps/navigator/src/UnattachedAnnotationPanel.tsx b/apps/navigator/src/UnattachedAnnotationPanel.tsx
index 13b0bdd..847817e 100644
--- a/apps/navigator/src/UnattachedAnnotationPanel.tsx
+++ b/apps/navigator/src/UnattachedAnnotationPanel.tsx
@@ -24,12 +24,7 @@ export function UnattachedAnnotationPanel({
{[...groups].map(([nodeId, items]) => (
}
))}
diff --git a/apps/navigator/src/server/validate-space.ts b/apps/navigator/src/server/validate-space.ts
index 18c0323..dfb0220 100644
--- a/apps/navigator/src/server/validate-space.ts
+++ b/apps/navigator/src/server/validate-space.ts
@@ -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 {
- return validateSpace(createSpaceContext(spaceName, config));
+export async function validateForOverlay(spaceName: string, config: Config): Promise {
+ const result = await validateSpace(createSpaceContext(spaceName, config));
+ return { issues: nodeIssues(result), ruleViolations: result.ruleViolations };
}
diff --git a/bun.lock b/bun.lock
index 221e1ee..5d8bcf5 100644
--- a/bun.lock
+++ b/bun.lock
@@ -17,7 +17,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": {
@@ -32,7 +32,7 @@
},
"packages/map-views": {
"name": "structured-context-map-views",
- "version": "0.2.0",
+ "version": "0.4.0",
"bin": {
"structured-context-map-views": "dist/cli.js",
},
@@ -50,14 +50,14 @@
"@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",
},
"peerDependencies": {
"react": "^19.3.0",
"react-dom": "^19.3.0",
- "structured-context": "^0.21.0",
+ "structured-context": "^0.22.1",
},
},
},
@@ -518,7 +518,7 @@
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
- "structured-context": ["structured-context@0.21.0", "", { "dependencies": { "ajv": "^8.20.0", "chokidar": "^5.0.0", "commander": "^15.0.0", "fast-glob": "^3.3.3", "gray-matter": "^4.0.3", "js-yaml": "^5.4.3", "json-schema-to-ts": "^3.1.1", "json5": "^2.2.3", "jsonata": "^2.2.1", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "typescript": "^5" }, "bin": { "sctx": "dist/index.js" } }, "sha512-VTzmp1FneT3Y6Vym5oHDKzbXbwtyoPrEEKnLPTXYa2nNoK5patXkG8gWNrtS8rWJKXqheYXjjCz7YEIzCzrcfg=="],
+ "structured-context": ["structured-context@0.22.1", "", { "dependencies": { "ajv": "^8.20.0", "chokidar": "^5.0.0", "commander": "^15.0.0", "fast-glob": "^3.3.3", "gray-matter": "^4.0.3", "js-yaml": "^5.4.3", "json-schema-to-ts": "^3.1.1", "json5": "^2.2.3", "jsonata": "^2.2.1", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "typescript": "^5" }, "bin": { "sctx": "dist/index.js" } }, "sha512-YBCurh4UAL+0uC0OLYTfAVONu3RQkuozqXkHn3/iM/z7xIsjQQPdwPrS8Lf16TaIVs/NufOiQwRwPsbj7pY8dA=="],
"structured-context-map-views": ["structured-context-map-views@workspace:packages/map-views"],
diff --git a/docs/api.md b/docs/api.md
index da64d44..13b979a 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -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
diff --git a/docs/architecture.md b/docs/architecture.md
index 4bb3982..104ac27 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -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
diff --git a/docs/concepts.md b/docs/concepts.md
index f821b1c..8d88ae8 100644
--- a/docs/concepts.md
+++ b/docs/concepts.md
@@ -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
diff --git a/docs/definitions.md b/docs/definitions.md
index 640d2b8..6124abd 100644
--- a/docs/definitions.md
+++ b/docs/definitions.md
@@ -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
diff --git a/packages/map-views/package.json b/packages/map-views/package.json
index b14d93e..8fa3be1 100644
--- a/packages/map-views/package.json
+++ b/packages/map-views/package.json
@@ -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",
@@ -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"
}
diff --git a/packages/map-views/src/core/annotations.ts b/packages/map-views/src/core/annotations.ts
index ecc3ce0..05c94e0 100644
--- a/packages/map-views/src/core/annotations.ts
+++ b/packages/map-views/src/core/annotations.ts
@@ -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 = {
@@ -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;
/**
- * 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 & {
- nodeErrors: ReadonlyArray>;
- }
->;
+export type ValidationInput = {
+ issues: Readonly>;
+ ruleViolations?: ReadonlyArray<
+ Pick
+ >;
+};
/**
* Attach validation results to the nodes of `graph`, by node id. Results that can't be attached are
@@ -50,24 +54,30 @@ export function annotationsFrom(
): { annotations: AnnotationIndex; unattached: UnattachedAnnotation[] } {
const annotations = new Map();
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 };
}
diff --git a/packages/map-views/src/core/format/definition-schema.ts b/packages/map-views/src/core/format/definition-schema.ts
index 6f3a213..a07a0c9 100644
--- a/packages/map-views/src/core/format/definition-schema.ts
+++ b/packages/map-views/src/core/format/definition-schema.ts
@@ -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';
@@ -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.',
),
},
diff --git a/packages/map-views/src/core/format/settings.ts b/packages/map-views/src/core/format/settings.ts
index 3ffc9d1..ed83c57 100644
--- a/packages/map-views/src/core/format/settings.ts
+++ b/packages/map-views/src/core/format/settings.ts
@@ -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 = {
@@ -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: {
diff --git a/packages/map-views/src/node/snapshot.ts b/packages/map-views/src/node/snapshot.ts
index 5cf2613..b5fbce4 100644
--- a/packages/map-views/src/node/snapshot.ts
+++ b/packages/map-views/src/node/snapshot.ts
@@ -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';
@@ -16,7 +18,8 @@ export type SpaceSnapshot = {
space: string;
schemaId: string | undefined;
graph: ReturnType;
- validation: Awaited>;
+ /** What `annotationsFrom` attaches: each node's issues and the rule violations */
+ validation: ValidationInput;
definitions: MapViewDefinition[];
};
@@ -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),
};
}
diff --git a/packages/map-views/tests/annotations.test.ts b/packages/map-views/tests/annotations.test.ts
index 9ffba3f..7950334 100644
--- a/packages/map-views/tests/annotations.test.ts
+++ b/packages/map-views/tests/annotations.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, test } from 'bun:test';
import { buildSpaceGraph } from 'structured-context/api';
import { makeSpaceNode } from 'structured-context/testing';
+import { VALIDATION_ISSUE_KINDS } from 'structured-context/types';
import { annotationsFrom, filterAnnotations, type NodeAnnotation, worstSeverity } from '../src/core/annotations';
const graph = buildSpaceGraph([makeSpaceNode('A', 'Thing'), makeSpaceNode('B', 'Thing')], undefined);
@@ -11,57 +12,54 @@ const rule = (
nodeId: ReturnType | undefined,
severity: 'error' | 'warning' | 'info',
description: string,
-) => ({ nodeId, ruleId: 'r', category: 'expression', severity, description }) as never;
-const schemaIssue = (message: string) => ({ message, dedupeKey: message });
+) => ({ nodeId, ruleId: 'r', severity, description });
describe('annotationsFrom', () => {
- test('attaches each kind of result to its node, with its source and severity', () => {
+ test('attaches each node issue to its node, with its source and severity', () => {
const { annotations, unattached } = annotationsFrom(
{
- ruleViolations: [rule(idOf('A'), 'warning', 'needs an owner')],
- hierarchyViolations: [
- {
- nodeId: idOf('A'),
- nodeType: 'Thing',
- nodeTitle: 'A',
- parentType: 'X',
- parentTitle: 'P',
- description: 'bad parent',
+ issues: {
+ [idOf('A')]: {
+ 'hierarchy:bad parent': { kind: 'hierarchy', message: 'bad parent', severity: 'error' },
+ 'rule:owner': { kind: 'rule', message: 'needs an owner', severity: 'warning' },
+ 'content-link:Gone': { kind: 'content-link', message: 'Unresolved content link: Gone', severity: 'info' },
+ },
+ [idOf('B')]: {
+ 'schema:one': { kind: 'schema', message: 'one', severity: 'error' },
+ 'schema:two': { kind: 'schema', message: 'two', severity: 'error' },
},
- ],
- nodeErrors: [{ nodeId: idOf('B'), issues: [schemaIssue('one'), schemaIssue('two')] }],
+ },
+ // node rule violations are already in `issues`
+ ruleViolations: [rule(idOf('A'), 'warning', 'needs an owner')],
},
graph,
);
expect(unattached).toEqual([]);
- expect(annotations.get(idOf('A'))?.map((a) => [a.source, a.severity])).toEqual([
- ['hierarchy', 'error'],
- ['rule', 'warning'],
+ expect(annotations.get(idOf('A'))?.map((a) => [a.source, a.severity, a.ruleId])).toEqual([
+ ['hierarchy', 'error', undefined],
+ ['rule', 'warning', 'owner'],
+ ['content-link', 'info', undefined],
]);
- // one annotation per schema error
expect(annotations.get(idOf('B'))).toHaveLength(2);
});
- test('returns results with no node on the graph as unattached, keeping where they came from', () => {
- const source = { file: 'broken.md', line: 3 };
+ test('returns results with no node on the graph as unattached, keeping where they point', () => {
const { annotations, unattached } = annotationsFrom(
{
- ruleViolations: [rule(undefined, 'error', 'global rule'), rule(missing, 'info', 'unknown node')],
- nodeErrors: [{ nodeId: missing, source, issues: [schemaIssue('bad')] }],
+ issues: { [missing]: { 'schema:bad': { kind: 'schema', message: 'bad', severity: 'error', line: 3 } } },
+ ruleViolations: [rule(undefined, 'error', 'global rule')],
},
graph,
);
expect(annotations.size).toBe(0);
- expect(unattached.map((u) => [u.nodeId, u.source])).toEqual([
- [missing, 'schema'],
- [undefined, 'rule'],
- [missing, 'rule'],
+ expect(unattached.map((u) => [u.nodeId, u.source, u.line])).toEqual([
+ [missing, 'schema', 3],
+ [undefined, 'rule', undefined],
]);
- expect(unattached.find((u) => u.source === 'schema')?.location).toEqual(source);
});
- test('accepts partial input', () => {
- expect(annotationsFrom({}, graph)).toEqual({ annotations: new Map(), unattached: [] });
+ test('accepts no issues', () => {
+ expect(annotationsFrom({ issues: {} }, graph)).toEqual({ annotations: new Map(), unattached: [] });
});
});
@@ -72,7 +70,7 @@ describe('filterAnnotations', () => {
{ severity: 'error', source: 'schema', message: 'e' },
];
const index = new Map([[idOf('A'), a]]);
- const all = ['schema', 'hierarchy', 'rule'] as const;
+ const all = VALIDATION_ISSUE_KINDS;
test('the level is a minimum severity', () => {
expect(
diff --git a/packages/map-views/tests/definition.test.ts b/packages/map-views/tests/definition.test.ts
index 1502a32..0585175 100644
--- a/packages/map-views/tests/definition.test.ts
+++ b/packages/map-views/tests/definition.test.ts
@@ -36,7 +36,12 @@ describe('parseMapViewDefinition', () => {
{
id: 'main',
layout: { type: 'tree', orientation: 'vertical', dag: 'repeat' },
- nodes: { style: 'card', fields: [], validation: 'off', validationSources: ['schema', 'hierarchy', 'rule'] },
+ nodes: {
+ style: 'card',
+ fields: [],
+ validation: 'off',
+ validationSources: ['schema', 'duplicate', 'broken-link', 'hierarchy', 'rule', 'gated', 'content-link'],
+ },
edges: { show: true },
},
]);
diff --git a/packages/map-views/tests/module-boundaries.test.ts b/packages/map-views/tests/module-boundaries.test.ts
index 87e733c..5e229ee 100644
--- a/packages/map-views/tests/module-boundaries.test.ts
+++ b/packages/map-views/tests/module-boundaries.test.ts
@@ -17,11 +17,14 @@ function importsIn(dir: string): { file: string; specifier: string }[] {
}
describe('module boundaries', () => {
+ // `structured-context/types` is the one runtime import allowed: it has no imports of its own, so it is browser-safe
test('core has no React, DOM-host, Node or runtime structured-context dependencies and no upward imports', () => {
const forbidden = /^(react|react-dom)(\/|$)|^node:|^structured-context(\/|$)|^(\.\.\/)+(layouts|node)(\/|$)/;
const violations = importsIn('core').filter(
({ file, specifier }) =>
- forbidden.test(specifier) && !(specifier.startsWith('structured-context') && isTypeOnly(file, specifier)),
+ forbidden.test(specifier) &&
+ specifier !== 'structured-context/types' &&
+ !(specifier.startsWith('structured-context') && isTypeOnly(file, specifier)),
);
expect(violations).toEqual([]);
});
diff --git a/packages/map-views/tests/snapshot.test.ts b/packages/map-views/tests/snapshot.test.ts
index d8d7b54..2d138fc 100644
--- a/packages/map-views/tests/snapshot.test.ts
+++ b/packages/map-views/tests/snapshot.test.ts
@@ -11,7 +11,7 @@ describe('exportSnapshot', () => {
const result = await exportSnapshot('launch-playbook', config, sample);
expect(result.space).toBe('launch-playbook');
expect(result.graph.formatVersion).toBe(1);
- expect(result.validation).toHaveProperty('nodeErrors');
+ expect(result.validation).toHaveProperty('issues');
expect(result.definitions.every((d) => d.schema === result.schemaId)).toBe(true);
expect(JSON.parse(JSON.stringify(result))).toEqual(result);
});
@@ -21,6 +21,6 @@ describe('exportSnapshot', () => {
const all = await exportSnapshot('launch-playbook', config, sample);
const none = await exportSnapshot('launch-playbook', config, sample, { ruleFilter: ['id=no-such-rule'] });
expect(none.validation.ruleViolations).toEqual([]);
- expect(all.validation.ruleViolations.length).toBeGreaterThanOrEqual(none.validation.ruleViolations.length);
+ expect(all.validation.ruleViolations!.length).toBeGreaterThanOrEqual(none.validation.ruleViolations!.length);
});
});
diff --git a/schemas/generated/latest/definition.json b/schemas/generated/latest/definition.json
index 9487dfc..42287ad 100644
--- a/schemas/generated/latest/definition.json
+++ b/schemas/generated/latest/definition.json
@@ -170,8 +170,12 @@
"description": "Which kinds of validation result are shown.",
"default": [
"schema",
+ "duplicate",
+ "broken-link",
"hierarchy",
- "rule"
+ "rule",
+ "gated",
+ "content-link"
],
"anyOf": [
{
diff --git a/schemas/generated/latest/settings.json b/schemas/generated/latest/settings.json
index 9b0f854..1313380 100644
--- a/schemas/generated/latest/settings.json
+++ b/schemas/generated/latest/settings.json
@@ -31,13 +31,17 @@
},
"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.",
+ "description": "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",
+ "duplicate",
+ "broken-link",
"hierarchy",
- "rule"
+ "rule",
+ "gated",
+ "content-link"
]
},
"uniqueItems": true