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]) => (

{nodeId || 'Space'}

- {items[0]?.location && ( -

- {items[0].location.file} - {items[0].location.line !== undefined && `:${items[0].location.line}`} -

- )} + {items[0]?.line !== undefined &&

line {items[0].line}

}
))} 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