structured-context 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -25
- package/dist/api.d.ts +25 -5
- package/dist/api.js +22 -2
- package/dist/commands/diagram.d.ts +1 -0
- package/dist/commands/diagram.js +7 -1
- package/dist/commands/dump.d.ts +15 -2
- package/dist/commands/dump.js +18 -4
- package/dist/commands/miro-sync.d.ts +3 -0
- package/dist/commands/miro-sync.js +8 -0
- package/dist/commands/render.d.ts +4 -0
- package/dist/commands/render.js +64 -9
- package/dist/commands/schemas.d.ts +2 -2
- package/dist/commands/schemas.js +117 -24
- package/dist/commands/show.d.ts +1 -0
- package/dist/commands/show.js +7 -1
- package/dist/commands/spaces.d.ts +3 -1
- package/dist/commands/spaces.js +11 -4
- package/dist/commands/validate-file.js +20 -5
- package/dist/commands/validate.js +54 -21
- package/dist/config.d.ts +26 -13
- package/dist/config.js +104 -31
- package/dist/filter/augment-nodes.d.ts +3 -1
- package/dist/filter/augment-nodes.js +12 -7
- package/dist/filter/filter-nodes.js +1 -1
- package/dist/index.js +51 -35
- package/dist/load-space-graph.d.ts +42 -0
- package/dist/load-space-graph.js +53 -0
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/loader.js +4 -4
- package/dist/plugins/markdown/extract-content-links.d.ts +23 -0
- package/dist/plugins/markdown/extract-content-links.js +123 -0
- package/dist/plugins/markdown/index.js +5 -5
- package/dist/plugins/markdown/parse-embedded.d.ts +6 -0
- package/dist/plugins/markdown/parse-embedded.js +50 -7
- package/dist/plugins/markdown/read-space.js +41 -5
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- package/dist/plugins/mermaid/index.d.ts +2 -0
- package/dist/plugins/mermaid/index.js +14 -0
- package/dist/plugins/{markdown → mermaid}/render-mermaid.js +17 -15
- package/dist/{integrations → plugins}/miro/cache.d.ts +1 -1
- package/dist/plugins/miro/index.d.ts +2 -0
- package/dist/plugins/miro/index.js +27 -0
- package/dist/{integrations → plugins}/miro/layout.d.ts +2 -2
- package/dist/{integrations → plugins}/miro/layout.js +3 -16
- package/dist/{integrations → plugins}/miro/styles.d.ts +2 -2
- package/dist/plugins/miro/sync.d.ts +7 -0
- package/dist/{integrations → plugins}/miro/sync.js +22 -39
- package/dist/plugins/util.d.ts +48 -1
- package/dist/plugins/util.js +143 -0
- package/dist/read/resolve-graph-edges.d.ts +1 -1
- package/dist/read/resolve-graph-edges.js +78 -27
- package/dist/read/wikilink-utils.d.ts +17 -0
- package/dist/read/wikilink-utils.js +40 -0
- package/dist/render/render.d.ts +4 -1
- package/dist/render/render.js +20 -16
- package/dist/schema/evaluate-rule.js +1 -0
- package/dist/schema/meta-schemas.d.ts +13 -0
- package/dist/schema/meta-schemas.js +50 -0
- package/dist/schema/metadata-contract.d.ts +343 -187
- package/dist/schema/metadata-contract.js +68 -30
- package/dist/schema/schema.d.ts +17 -9
- package/dist/schema/schema.js +283 -200
- package/dist/schema/validate-graph.d.ts +2 -1
- package/dist/schema/validate-graph.js +40 -23
- package/dist/schema/validate-rules.d.ts +5 -1
- package/dist/schema/validate-rules.js +20 -1
- package/dist/schemas/_knowledge_wiki.json +21 -21
- package/dist/schemas/_ost_strict.json +2 -2
- package/dist/schemas/_sctx_base.json +2 -2
- package/dist/schemas/_strategy_general.json +38 -38
- package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/knowledge_wiki.json +10 -9
- package/dist/schemas/strategy_general.json +15 -14
- package/dist/schemas/strict_ost.json +17 -16
- package/dist/space-context.d.ts +27 -0
- package/dist/space-context.js +46 -0
- package/dist/space-graph.d.ts +9 -7
- package/dist/space-graph.js +7 -5
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +72 -0
- package/dist/types.d.ts +101 -8
- package/dist/validate.d.ts +15 -3
- package/dist/validate.js +55 -20
- package/docs/concepts.md +37 -4
- package/docs/config.md +10 -0
- package/docs/rules.md +49 -22
- package/docs/schemas.md +110 -35
- package/package.json +13 -8
- package/schemas/_knowledge_wiki.json +21 -21
- package/schemas/_ost_strict.json +2 -2
- package/schemas/_sctx_base.json +2 -2
- package/schemas/_strategy_general.json +38 -38
- package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
- package/schemas/knowledge_wiki.json +10 -9
- package/schemas/strategy_general.json +15 -14
- package/schemas/strict_ost.json +17 -16
- package/dist/integrations/miro/sync.d.ts +0 -8
- package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
- package/schemas/generated/_structured_context_schema_meta.json +0 -191
- /package/dist/plugins/{markdown → mermaid}/render-mermaid.d.ts +0 -0
- /package/dist/{integrations → plugins}/miro/cache.js +0 -0
- /package/dist/{integrations → plugins}/miro/client.d.ts +0 -0
- /package/dist/{integrations → plugins}/miro/client.js +0 -0
- /package/dist/{integrations → plugins}/miro/styles.js +0 -0
package/dist/space-graph.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Hierarchy, SpaceNode } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* A navigable graph over a set of SpaceNodes.
|
|
4
4
|
*
|
|
5
|
-
* Built once from SpaceNode[] + hierarchy
|
|
5
|
+
* Built once from SpaceNode[] + one selected hierarchy via buildSpaceGraph().
|
|
6
|
+
* The hierarchy-specific views (roots, orphans, hierarchyChildren, ...) cover only that hierarchy;
|
|
7
|
+
* nodes of other hierarchies' types are non-hierarchy nodes in this graph.
|
|
6
8
|
* Provides typed access to nodes, edges, traversal, and classification
|
|
7
9
|
* so consumers don't need to build their own indexes.
|
|
8
10
|
*/
|
|
@@ -15,14 +17,14 @@ export type SpaceGraph = {
|
|
|
15
17
|
readonly orphans: readonly SpaceNode[];
|
|
16
18
|
/** Non-hierarchy nodes: type not in the hierarchy levels definition. */
|
|
17
19
|
readonly nonHierarchy: readonly SpaceNode[];
|
|
18
|
-
/** Hierarchy children map: parent title → direct children connected via hierarchy edges only. */
|
|
20
|
+
/** Hierarchy children map: parent title → direct children connected via this hierarchy's edges only. */
|
|
19
21
|
readonly hierarchyChildren: ReadonlyMap<string, readonly SpaceNode[]>;
|
|
20
22
|
/** All-edges children map: parent title → direct children connected via any edge (hierarchy + relationship). */
|
|
21
23
|
readonly children: ReadonlyMap<string, readonly SpaceNode[]>;
|
|
22
24
|
/** Set of all node titles that are part of the hierarchy (roots + their descendants + orphans). */
|
|
23
25
|
readonly hierarchyTitles: ReadonlySet<string>;
|
|
24
|
-
/**
|
|
25
|
-
readonly
|
|
26
|
+
/** The hierarchy used to build this graph, if the schema has one. */
|
|
27
|
+
readonly hierarchy: Hierarchy | undefined;
|
|
26
28
|
};
|
|
27
|
-
/** Build a SpaceGraph from a flat list of SpaceNodes and hierarchy
|
|
28
|
-
export declare function buildSpaceGraph(nodes: SpaceNode[],
|
|
29
|
+
/** Build a SpaceGraph from a flat list of SpaceNodes and the hierarchy to structure it by. */
|
|
30
|
+
export declare function buildSpaceGraph(nodes: SpaceNode[], hierarchy: Hierarchy | undefined): SpaceGraph;
|
package/dist/space-graph.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
/** Build a SpaceGraph from a flat list of SpaceNodes and hierarchy
|
|
2
|
-
export function buildSpaceGraph(nodes,
|
|
1
|
+
/** Build a SpaceGraph from a flat list of SpaceNodes and the hierarchy to structure it by. */
|
|
2
|
+
export function buildSpaceGraph(nodes, hierarchy) {
|
|
3
|
+
const levels = hierarchy?.levels ?? [];
|
|
3
4
|
const hierarchyTypes = new Set(levels.map((l) => l.type));
|
|
4
5
|
const rootType = levels[0]?.type;
|
|
6
|
+
const isHierarchyEdge = (r) => r.source === 'hierarchy' && r.hierarchy === hierarchy?.name;
|
|
5
7
|
const nodesMap = new Map();
|
|
6
8
|
const hierarchyChildrenMap = new Map();
|
|
7
9
|
const childrenMap = new Map();
|
|
@@ -22,7 +24,7 @@ export function buildSpaceGraph(nodes, levels) {
|
|
|
22
24
|
childrenMap.set(parentRef.title, []);
|
|
23
25
|
childrenMap.get(parentRef.title).push(node);
|
|
24
26
|
// Hierarchy-only map
|
|
25
|
-
if (parentRef
|
|
27
|
+
if (isHierarchyEdge(parentRef)) {
|
|
26
28
|
if (!hierarchyChildrenMap.has(parentRef.title))
|
|
27
29
|
hierarchyChildrenMap.set(parentRef.title, []);
|
|
28
30
|
hierarchyChildrenMap.get(parentRef.title).push(node);
|
|
@@ -37,7 +39,7 @@ export function buildSpaceGraph(nodes, levels) {
|
|
|
37
39
|
continue;
|
|
38
40
|
}
|
|
39
41
|
// Only hierarchy-sourced parents determine structural position in the DAG
|
|
40
|
-
const hierarchyParents = node.resolvedParents.filter(
|
|
42
|
+
const hierarchyParents = node.resolvedParents.filter(isHierarchyEdge);
|
|
41
43
|
if (hierarchyParents.length === 0) {
|
|
42
44
|
if (nodeType === rootType) {
|
|
43
45
|
hierarchyRoots.push(node);
|
|
@@ -77,6 +79,6 @@ export function buildSpaceGraph(nodes, levels) {
|
|
|
77
79
|
hierarchyChildren: hierarchyChildrenMap,
|
|
78
80
|
children: childrenMap,
|
|
79
81
|
hierarchyTitles,
|
|
80
|
-
|
|
82
|
+
hierarchy,
|
|
81
83
|
};
|
|
82
84
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BaseNode, Hierarchy, HierarchyLevel, Relationship, ResolvedParentRef, SpaceNode } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a ResolvedParentRef with sensible defaults for use in tests.
|
|
4
|
+
* Override any field to represent specific edge contexts.
|
|
5
|
+
*/
|
|
6
|
+
export declare const makeParentRef: (title: string, overrides?: Partial<ResolvedParentRef>) => ResolvedParentRef;
|
|
7
|
+
/**
|
|
8
|
+
* Creates a HierarchyLevel with defaults matching schema metadata normalization.
|
|
9
|
+
*/
|
|
10
|
+
export declare const makeLevel: (type: string, overrides?: Partial<HierarchyLevel>) => HierarchyLevel;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a named Hierarchy (default name 'main') from normalized levels.
|
|
13
|
+
*/
|
|
14
|
+
export declare const makeHierarchy: (levels: HierarchyLevel[], overrides?: Partial<Hierarchy>) => Hierarchy;
|
|
15
|
+
/**
|
|
16
|
+
* SchemaMetadata fields for a schema whose only hierarchy is its main hierarchy.
|
|
17
|
+
*/
|
|
18
|
+
export declare const withHierarchy: (levels: HierarchyLevel[], overrides?: Partial<Hierarchy>) => {
|
|
19
|
+
hierarchy: Hierarchy;
|
|
20
|
+
hierarchies: Hierarchy[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Creates a Relationship with defaults matching schema metadata normalization.
|
|
24
|
+
*/
|
|
25
|
+
export declare const makeRelationship: (parent: string, type: string, overrides?: Partial<Relationship>) => Relationship;
|
|
26
|
+
/**
|
|
27
|
+
* Creates a BaseNode with sensible defaults for use in tests.
|
|
28
|
+
*/
|
|
29
|
+
export declare const makeNode: (title: string, type: string, extra?: Record<string, unknown>, linkTargets?: string[]) => BaseNode;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a SpaceNode with sensible defaults for use in tests.
|
|
32
|
+
*/
|
|
33
|
+
export declare const makeSpaceNode: (title: string, type: string, extra?: Record<string, unknown>, linkTargets?: string[], resolvedParents?: ResolvedParentRef[], resolvedLinks?: SpaceNode["resolvedLinks"]) => SpaceNode;
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a ResolvedParentRef with sensible defaults for use in tests.
|
|
3
|
+
* Override any field to represent specific edge contexts.
|
|
4
|
+
*/
|
|
5
|
+
export const makeParentRef = (title, overrides = {}) => {
|
|
6
|
+
const source = overrides.source ?? 'hierarchy';
|
|
7
|
+
return {
|
|
8
|
+
title,
|
|
9
|
+
field: 'parent',
|
|
10
|
+
source,
|
|
11
|
+
...(source === 'hierarchy' ? { hierarchy: 'main' } : {}),
|
|
12
|
+
selfRef: false,
|
|
13
|
+
fieldOn: 'child',
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Creates a HierarchyLevel with defaults matching schema metadata normalization.
|
|
19
|
+
*/
|
|
20
|
+
export const makeLevel = (type, overrides = {}) => ({
|
|
21
|
+
type,
|
|
22
|
+
field: 'parent',
|
|
23
|
+
fieldOn: 'child',
|
|
24
|
+
multiple: false,
|
|
25
|
+
selfRef: false,
|
|
26
|
+
...overrides,
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* Creates a named Hierarchy (default name 'main') from normalized levels.
|
|
30
|
+
*/
|
|
31
|
+
export const makeHierarchy = (levels, overrides = {}) => ({
|
|
32
|
+
name: 'main',
|
|
33
|
+
levels,
|
|
34
|
+
...overrides,
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* SchemaMetadata fields for a schema whose only hierarchy is its main hierarchy.
|
|
38
|
+
*/
|
|
39
|
+
export const withHierarchy = (levels, overrides = {}) => {
|
|
40
|
+
const hierarchy = makeHierarchy(levels, overrides);
|
|
41
|
+
return { hierarchy, hierarchies: [hierarchy] };
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Creates a Relationship with defaults matching schema metadata normalization.
|
|
45
|
+
*/
|
|
46
|
+
export const makeRelationship = (parent, type, overrides = {}) => ({
|
|
47
|
+
parent,
|
|
48
|
+
type,
|
|
49
|
+
field: 'parent',
|
|
50
|
+
fieldOn: 'child',
|
|
51
|
+
multiple: false,
|
|
52
|
+
...overrides,
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Creates a BaseNode with sensible defaults for use in tests.
|
|
56
|
+
*/
|
|
57
|
+
export const makeNode = (title, type, extra = {}, linkTargets) => ({
|
|
58
|
+
label: `${title}.md`,
|
|
59
|
+
title,
|
|
60
|
+
schemaData: { title, type, ...extra },
|
|
61
|
+
linkTargets: linkTargets ?? [title],
|
|
62
|
+
type,
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* Creates a SpaceNode with sensible defaults for use in tests.
|
|
66
|
+
*/
|
|
67
|
+
export const makeSpaceNode = (title, type, extra = {}, linkTargets, resolvedParents = [], resolvedLinks = []) => ({
|
|
68
|
+
...makeNode(title, type, extra, linkTargets),
|
|
69
|
+
resolvedType: type,
|
|
70
|
+
resolvedParents,
|
|
71
|
+
resolvedLinks,
|
|
72
|
+
});
|
package/dist/types.d.ts
CHANGED
|
@@ -40,11 +40,21 @@ export type ResolvedParentRef = {
|
|
|
40
40
|
field: string;
|
|
41
41
|
/** Whether this edge originates from a hierarchy level or a relationship definition. */
|
|
42
42
|
source: 'hierarchy' | 'relationship';
|
|
43
|
+
/** Name of the hierarchy the edge belongs to. Set only when source is 'hierarchy'. */
|
|
44
|
+
hierarchy?: string;
|
|
43
45
|
/** Whether the parent and child are the same node type (self-referential edge). */
|
|
44
46
|
selfRef: boolean;
|
|
45
47
|
/** Whether the edge field is on the child node (default) or on the parent node (fieldOn:'parent' edges). */
|
|
46
48
|
fieldOn: 'child' | 'parent';
|
|
47
49
|
};
|
|
50
|
+
export type SourceLocation = {
|
|
51
|
+
/** File path relative to the space root. */
|
|
52
|
+
file: string;
|
|
53
|
+
/** 1-indexed starting line number in the source file, if available. */
|
|
54
|
+
line?: number;
|
|
55
|
+
/** 1-indexed starting column number in the source file, if available. */
|
|
56
|
+
column?: number;
|
|
57
|
+
};
|
|
48
58
|
export type UnresolvedRef = {
|
|
49
59
|
/** Source identifier of the node containing the broken link. */
|
|
50
60
|
label: string;
|
|
@@ -55,6 +65,44 @@ export type UnresolvedRef = {
|
|
|
55
65
|
reason: 'not_found' | 'ambiguous' | 'invalid_shape';
|
|
56
66
|
/** Human-readable message matching validate-graph output format. */
|
|
57
67
|
message: string;
|
|
68
|
+
/** Source location of the node containing the broken link, if available. */
|
|
69
|
+
source?: SourceLocation;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Shared fields for all content link variants.
|
|
73
|
+
*/
|
|
74
|
+
type ContentLinkBase = {
|
|
75
|
+
/** Display text of the link (text content for markdown links, inner text or target for wikilinks). */
|
|
76
|
+
text: string;
|
|
77
|
+
/** Raw link target: URL for markdown links, wikilink path (without [[ ]]) for wikilinks. */
|
|
78
|
+
target: string;
|
|
79
|
+
/** Whether the link navigates to a resource or transcludes/embeds it. */
|
|
80
|
+
action: 'link' | 'embed';
|
|
81
|
+
/** Anchor fragment (heading or block ref, e.g. 'heading-title' or '^blockid') if present. */
|
|
82
|
+
anchor?: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* A link extracted from node content at parse time, before location classification.
|
|
86
|
+
* Populated by plugins that support content link extraction (e.g. the markdown plugin).
|
|
87
|
+
*/
|
|
88
|
+
export type ContentLink = ContentLinkBase & {
|
|
89
|
+
/** Link syntax — used during resolution to determine how to classify the target. */
|
|
90
|
+
linkSyntax: 'wikilink' | 'markdown';
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* A fully classified link from node content, after location resolution.
|
|
94
|
+
* Present on SpaceNode after graph resolution.
|
|
95
|
+
*/
|
|
96
|
+
export type ResolvedContentLink = ContentLinkBase & {
|
|
97
|
+
/**
|
|
98
|
+
* Resolved location classification:
|
|
99
|
+
* - 'node' — wikilink resolved to a known space node
|
|
100
|
+
* - 'internal' — wikilink or relative path, in the source system but not a space node
|
|
101
|
+
* - 'external' — http/https URL
|
|
102
|
+
* - 'system' — file:// URL
|
|
103
|
+
* - 'protocol' — other scheme (e.g. obsidian://)
|
|
104
|
+
*/
|
|
105
|
+
location: 'node' | 'internal' | 'external' | 'system' | 'protocol';
|
|
58
106
|
};
|
|
59
107
|
/**
|
|
60
108
|
* A node as produced by a parse plugin — raw type from content, no graph resolution applied.
|
|
@@ -71,6 +119,15 @@ export type BaseNode = {
|
|
|
71
119
|
linkTargets: string[];
|
|
72
120
|
/** Raw type string from content, as written by the user. */
|
|
73
121
|
type: string;
|
|
122
|
+
/** Body content of the node, if any. First-class accessor mirroring schemaData.content. */
|
|
123
|
+
content?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Links extracted from content body and non-edge string fields at parse time.
|
|
126
|
+
* Populated by plugins that support content link extraction; absent if not populated.
|
|
127
|
+
*/
|
|
128
|
+
contentLinks?: ContentLink[];
|
|
129
|
+
/** Source location of the node definition in the filesystem. */
|
|
130
|
+
source?: SourceLocation;
|
|
74
131
|
};
|
|
75
132
|
/**
|
|
76
133
|
* A fully resolved node — enriched by core after plugin parsing.
|
|
@@ -85,6 +142,11 @@ export type SpaceNode = BaseNode & {
|
|
|
85
142
|
* Always present, empty if no parents resolved.
|
|
86
143
|
*/
|
|
87
144
|
resolvedParents: ResolvedParentRef[];
|
|
145
|
+
/**
|
|
146
|
+
* Links from content body and non-edge fields, with location classified after graph resolution.
|
|
147
|
+
* Always present, empty if the plugin did not populate rawContentLinks.
|
|
148
|
+
*/
|
|
149
|
+
resolvedLinks: ResolvedContentLink[];
|
|
88
150
|
};
|
|
89
151
|
export type ParseIssue = {
|
|
90
152
|
/** Source identifier of the file or item (filename or heading title). */
|
|
@@ -97,14 +159,25 @@ export type ParseIssue = {
|
|
|
97
159
|
type: 'no-type' | 'parse' | string;
|
|
98
160
|
/** Human-readable detail. */
|
|
99
161
|
message?: string;
|
|
162
|
+
/** 1-indexed line number in source file, if available. */
|
|
163
|
+
line?: number;
|
|
164
|
+
/** 1-indexed column number in source file, if available. */
|
|
165
|
+
column?: number;
|
|
100
166
|
};
|
|
101
167
|
/** Rule categories for organizing executable validation rules */
|
|
102
168
|
export type RuleCategory = 'validation' | 'coherence' | 'workflow' | 'best-practice';
|
|
169
|
+
/**
|
|
170
|
+
* Rule violation severity. Only `error` fails validation; `warning` and `info` are reported only.
|
|
171
|
+
* A rule's severity defaults from its category and may be overridden per rule.
|
|
172
|
+
*/
|
|
173
|
+
export type RuleSeverity = 'error' | 'warning' | 'info';
|
|
103
174
|
export type { Rule } from './schema/metadata-contract';
|
|
104
175
|
export type RuleViolation = {
|
|
176
|
+
/** Node label, or empty string for global-scope rules. */
|
|
105
177
|
file: string;
|
|
106
178
|
ruleId: string;
|
|
107
179
|
category: RuleCategory;
|
|
180
|
+
severity: RuleSeverity;
|
|
108
181
|
description: string;
|
|
109
182
|
};
|
|
110
183
|
export type GraphViolation = {
|
|
@@ -115,11 +188,18 @@ export type GraphViolation = {
|
|
|
115
188
|
parentTitle: string;
|
|
116
189
|
description: string;
|
|
117
190
|
};
|
|
191
|
+
/** A named hierarchy, normalized from a schema's `$metadata.hierarchy`. */
|
|
192
|
+
export type Hierarchy = {
|
|
193
|
+
/** Declared short name, defaulting to the declaring schema's file stem. */
|
|
194
|
+
name: string;
|
|
195
|
+
levels: HierarchyLevel[];
|
|
196
|
+
allowSkipLevels?: boolean;
|
|
197
|
+
};
|
|
118
198
|
export type SchemaMetadata = {
|
|
119
|
-
hierarchy
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
199
|
+
/** Main hierarchy: used for space_on_a_page parsing, embedding, template-sync and default rendering. */
|
|
200
|
+
hierarchy?: Hierarchy;
|
|
201
|
+
/** Every hierarchy in the composed schema (main included), in composition order. Each type belongs to at most one. */
|
|
202
|
+
hierarchies?: Hierarchy[];
|
|
123
203
|
typeAliases?: Record<string, string>;
|
|
124
204
|
rules?: Rule[];
|
|
125
205
|
relationships?: Relationship[];
|
|
@@ -155,16 +235,29 @@ export type SpaceContext = {
|
|
|
155
235
|
/** Directory of the config file that defines this space. Used for resolving relative paths in plugin configs. */
|
|
156
236
|
configDir: string;
|
|
157
237
|
};
|
|
238
|
+
export interface FileValidationIssue {
|
|
239
|
+
kind: string;
|
|
240
|
+
message: string;
|
|
241
|
+
/** Present for rule violations (`kind: 'rule'`). */
|
|
242
|
+
severity?: RuleSeverity;
|
|
243
|
+
/** 1-indexed line number in source file, if available. */
|
|
244
|
+
line?: number;
|
|
245
|
+
/** 1-indexed column number in source file, if available. */
|
|
246
|
+
column?: number;
|
|
247
|
+
}
|
|
158
248
|
export interface FileValidationResult {
|
|
159
249
|
file: string;
|
|
160
250
|
label: string;
|
|
161
251
|
space: string;
|
|
162
252
|
/** Errors keyed by composite id (e.g. `schema:/status:enum:active`, `rule:my-rule-id`). */
|
|
163
|
-
errors: Record<string,
|
|
164
|
-
kind: string;
|
|
165
|
-
message: string;
|
|
166
|
-
}>;
|
|
253
|
+
errors: Record<string, FileValidationIssue>;
|
|
167
254
|
errorCount: number;
|
|
255
|
+
/**
|
|
256
|
+
* Warnings keyed by composite id, including `warning` and `info` severity rule violations.
|
|
257
|
+
* Do not affect exit code or errorCount.
|
|
258
|
+
*/
|
|
259
|
+
warnings: Record<string, FileValidationIssue>;
|
|
260
|
+
warningCount: number;
|
|
168
261
|
inSpace: true;
|
|
169
262
|
}
|
|
170
263
|
export interface FileNotInSpaceResult {
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ErrorObject } from 'ajv';
|
|
2
2
|
import type { Config } from './config';
|
|
3
3
|
import { extractEntityInfo } from './schema/schema';
|
|
4
|
-
import type { GraphViolation, ParseIssue, RuleViolation, SchemaWithMetadata, SpaceContext, SpaceNode, ValidateFileOutput } from './types';
|
|
4
|
+
import type { GraphViolation, ParseIssue, ReadSpaceResult, RuleViolation, SchemaWithMetadata, SpaceContext, SpaceNode, ValidateFileOutput } from './types';
|
|
5
5
|
export interface FormattedError {
|
|
6
6
|
message: string;
|
|
7
7
|
dedupeKey: string;
|
|
@@ -11,11 +11,13 @@ export interface ValidationResult {
|
|
|
11
11
|
nodeErrorCount: number;
|
|
12
12
|
nodeErrors: Array<{
|
|
13
13
|
file: string;
|
|
14
|
+
line?: number;
|
|
14
15
|
errors: ErrorObject[];
|
|
15
16
|
nodeData: Record<string, unknown>;
|
|
16
17
|
}>;
|
|
17
18
|
refErrors: Array<{
|
|
18
19
|
file: string;
|
|
20
|
+
line?: number;
|
|
19
21
|
parent: string;
|
|
20
22
|
error: string;
|
|
21
23
|
}>;
|
|
@@ -23,10 +25,16 @@ export interface ValidationResult {
|
|
|
23
25
|
title: string;
|
|
24
26
|
files: string[];
|
|
25
27
|
}>;
|
|
28
|
+
/** All rule violations with their effective severity. Only `error` severity fails validation. */
|
|
26
29
|
ruleViolations: RuleViolation[];
|
|
27
30
|
hierarchyViolations: GraphViolation[];
|
|
28
31
|
orphans: SpaceNode[];
|
|
29
32
|
parseIssues: ParseIssue[];
|
|
33
|
+
/** Content links that could not be resolved to a space node. Warning only — not counted in errors. */
|
|
34
|
+
unresolvedContentLinks: Array<{
|
|
35
|
+
file: string;
|
|
36
|
+
target: string;
|
|
37
|
+
}>;
|
|
30
38
|
}
|
|
31
39
|
/**
|
|
32
40
|
* Format AJV errors for better readability.
|
|
@@ -37,7 +45,9 @@ export declare function formatErrors(errors: ErrorObject[], schema: SchemaWithMe
|
|
|
37
45
|
* Run full validation on a space and return a structured result.
|
|
38
46
|
* Does not output to console or call process.exit.
|
|
39
47
|
*/
|
|
40
|
-
export declare function validateSpace(context: SpaceContext
|
|
48
|
+
export declare function validateSpace(context: SpaceContext, options?: {
|
|
49
|
+
readResult?: ReadSpaceResult;
|
|
50
|
+
}): Promise<ValidationResult>;
|
|
41
51
|
/**
|
|
42
52
|
* Validate a single file within its space.
|
|
43
53
|
*
|
|
@@ -47,4 +57,6 @@ export declare function validateSpace(context: SpaceContext): Promise<Validation
|
|
|
47
57
|
* Returns a result object — does not output to console or call process.exit.
|
|
48
58
|
* If the file is not in any configured space, returns a result with `inSpace: false`.
|
|
49
59
|
*/
|
|
50
|
-
export declare function validateFile(filePath: string, config: Config
|
|
60
|
+
export declare function validateFile(filePath: string, config: Config, options?: {
|
|
61
|
+
configDir?: string;
|
|
62
|
+
}): Promise<ValidateFileOutput>;
|
package/dist/validate.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve } from 'node:path';
|
|
2
|
-
import { getSpaceConfigDir, resolveSchema } from './config';
|
|
3
2
|
import { readSpace } from './read/read-space';
|
|
4
|
-
import { extractEntityInfo
|
|
3
|
+
import { extractEntityInfo } from './schema/schema';
|
|
5
4
|
import { validateGraph } from './schema/validate-graph';
|
|
6
5
|
import { validateRules } from './schema/validate-rules';
|
|
6
|
+
import { createSpaceContext } from './space-context';
|
|
7
7
|
import { buildSpaceGraph } from './space-graph';
|
|
8
8
|
/**
|
|
9
9
|
* Format AJV errors for better readability.
|
|
@@ -96,10 +96,10 @@ export function formatErrors(errors, schema, schemaRefRegistry, nodeData) {
|
|
|
96
96
|
* Run full validation on a space and return a structured result.
|
|
97
97
|
* Does not output to console or call process.exit.
|
|
98
98
|
*/
|
|
99
|
-
export async function validateSpace(context) {
|
|
99
|
+
export async function validateSpace(context, options = {}) {
|
|
100
100
|
const { schema, schemaRefRegistry, schemaValidator } = context;
|
|
101
101
|
const metadata = schema.metadata;
|
|
102
|
-
const readResult = await readSpace(context);
|
|
102
|
+
const readResult = options.readResult ?? (await readSpace(context));
|
|
103
103
|
const { nodes, parseIssues } = readResult;
|
|
104
104
|
// Pre-extract valid types for early type validation
|
|
105
105
|
const validTypes = Array.isArray(schema.oneOf) ? extractEntityInfo(schema, schemaRefRegistry).map((e) => e.type) : [];
|
|
@@ -113,6 +113,7 @@ export async function validateSpace(context) {
|
|
|
113
113
|
hierarchyViolations: [],
|
|
114
114
|
orphans: [],
|
|
115
115
|
parseIssues,
|
|
116
|
+
unresolvedContentLinks: [],
|
|
116
117
|
};
|
|
117
118
|
for (const node of nodes) {
|
|
118
119
|
// Early type validation - check before full AJV validation to prevent cascading errors
|
|
@@ -120,7 +121,8 @@ export async function validateSpace(context) {
|
|
|
120
121
|
if (nodeType !== undefined && validTypes.length > 0 && !validTypes.includes(nodeType)) {
|
|
121
122
|
result.nodeErrorCount++;
|
|
122
123
|
result.nodeErrors.push({
|
|
123
|
-
file: node.label,
|
|
124
|
+
file: node.source?.file ?? node.label,
|
|
125
|
+
...(node.source?.line !== undefined ? { line: node.source.line } : {}),
|
|
124
126
|
errors: [
|
|
125
127
|
{
|
|
126
128
|
instancePath: '/type',
|
|
@@ -142,7 +144,8 @@ export async function validateSpace(context) {
|
|
|
142
144
|
else {
|
|
143
145
|
result.nodeErrorCount++;
|
|
144
146
|
result.nodeErrors.push({
|
|
145
|
-
file: node.label,
|
|
147
|
+
file: node.source?.file ?? node.label,
|
|
148
|
+
...(node.source?.line !== undefined ? { line: node.source.line } : {}),
|
|
146
149
|
errors: schemaValidator.errors || [],
|
|
147
150
|
nodeData: node.schemaData,
|
|
148
151
|
});
|
|
@@ -166,14 +169,27 @@ export async function validateSpace(context) {
|
|
|
166
169
|
const hierarchyValidation = validateGraph(nodes, metadata, readResult.unresolvedRefs);
|
|
167
170
|
result.refErrors.push(...hierarchyValidation.refErrors);
|
|
168
171
|
result.hierarchyViolations = [...hierarchyValidation.violations];
|
|
169
|
-
// Calculate
|
|
170
|
-
|
|
171
|
-
result.orphans
|
|
172
|
+
// Calculate orphans across every hierarchy (informational, not a validation error)
|
|
173
|
+
for (const hierarchy of metadata.hierarchies ?? []) {
|
|
174
|
+
result.orphans.push(...buildSpaceGraph(nodes, hierarchy).orphans);
|
|
172
175
|
}
|
|
173
176
|
// Load and execute rules validation if schema defines rules
|
|
174
177
|
if (metadata.rules) {
|
|
175
178
|
result.ruleViolations = await validateRules(nodes, metadata.rules);
|
|
176
179
|
}
|
|
180
|
+
// Collect unresolved content links (warnings — not errors)
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
for (const node of nodes) {
|
|
183
|
+
for (const link of node.resolvedLinks) {
|
|
184
|
+
if (link.location === 'internal') {
|
|
185
|
+
const key = `${node.label}::${link.target}`;
|
|
186
|
+
if (!seen.has(key)) {
|
|
187
|
+
seen.add(key);
|
|
188
|
+
result.unresolvedContentLinks.push({ file: node.label, target: link.target });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
177
193
|
return result;
|
|
178
194
|
}
|
|
179
195
|
/** Find which space a file belongs to by checking directory containment. */
|
|
@@ -189,12 +205,6 @@ function resolveFileSpace(filePath, config) {
|
|
|
189
205
|
}
|
|
190
206
|
return null;
|
|
191
207
|
}
|
|
192
|
-
function buildContextForSpace(space, config) {
|
|
193
|
-
const resolvedSchemaPath = resolveSchema(config, space);
|
|
194
|
-
const { schema, schemaRefRegistry, schemaValidator } = loadSchema(resolvedSchemaPath);
|
|
195
|
-
const configDir = getSpaceConfigDir(space.name);
|
|
196
|
-
return { space, config, resolvedSchemaPath, schema, schemaRefRegistry, schemaValidator, configDir };
|
|
197
|
-
}
|
|
198
208
|
/**
|
|
199
209
|
* Validate a single file within its space.
|
|
200
210
|
*
|
|
@@ -204,7 +214,7 @@ function buildContextForSpace(space, config) {
|
|
|
204
214
|
* Returns a result object — does not output to console or call process.exit.
|
|
205
215
|
* If the file is not in any configured space, returns a result with `inSpace: false`.
|
|
206
216
|
*/
|
|
207
|
-
export async function validateFile(filePath, config) {
|
|
217
|
+
export async function validateFile(filePath, config, options = {}) {
|
|
208
218
|
const resolved = resolveFileSpace(filePath, config);
|
|
209
219
|
if (!resolved) {
|
|
210
220
|
return {
|
|
@@ -214,7 +224,7 @@ export async function validateFile(filePath, config) {
|
|
|
214
224
|
};
|
|
215
225
|
}
|
|
216
226
|
const { space, label } = resolved;
|
|
217
|
-
const context =
|
|
227
|
+
const context = createSpaceContext(space.name, config, { configDir: options.configDir });
|
|
218
228
|
const { schema, schemaRefRegistry, schemaValidator } = context;
|
|
219
229
|
const metadata = schema.metadata;
|
|
220
230
|
const readResult = await readSpace(context);
|
|
@@ -222,6 +232,7 @@ export async function validateFile(filePath, config) {
|
|
|
222
232
|
// Pre-extract valid types for early type validation
|
|
223
233
|
const validTypes = Array.isArray(schema.oneOf) ? extractEntityInfo(schema, schemaRefRegistry).map((e) => e.type) : [];
|
|
224
234
|
const errors = {};
|
|
235
|
+
const warnings = {};
|
|
225
236
|
// Schema validation errors for this node
|
|
226
237
|
for (const node of nodes) {
|
|
227
238
|
if (node.label !== label)
|
|
@@ -232,6 +243,8 @@ export async function validateFile(filePath, config) {
|
|
|
232
243
|
errors[`schema:type:${validTypes.join(',')}`] = {
|
|
233
244
|
kind: 'schema',
|
|
234
245
|
message: `Invalid type "${nodeType}". Valid types are: ${validTypes.sort().join(', ')}`,
|
|
246
|
+
...(node.source?.line !== undefined ? { line: node.source.line } : {}),
|
|
247
|
+
...(node.source?.column !== undefined ? { column: node.source.column } : {}),
|
|
235
248
|
};
|
|
236
249
|
continue;
|
|
237
250
|
}
|
|
@@ -239,7 +252,12 @@ export async function validateFile(filePath, config) {
|
|
|
239
252
|
if (!valid) {
|
|
240
253
|
const formatted = formatErrors(schemaValidator.errors ?? [], schema, schemaRefRegistry, node.schemaData);
|
|
241
254
|
for (const { message, dedupeKey } of formatted) {
|
|
242
|
-
errors[`schema:${dedupeKey}`] = {
|
|
255
|
+
errors[`schema:${dedupeKey}`] = {
|
|
256
|
+
kind: 'schema',
|
|
257
|
+
message,
|
|
258
|
+
...(node.source?.line !== undefined ? { line: node.source.line } : {}),
|
|
259
|
+
...(node.source?.column !== undefined ? { column: node.source.column } : {}),
|
|
260
|
+
};
|
|
243
261
|
}
|
|
244
262
|
}
|
|
245
263
|
}
|
|
@@ -271,12 +289,27 @@ export async function validateFile(filePath, config) {
|
|
|
271
289
|
errors[`hierarchy:${v.description}`] = { kind: 'hierarchy', message: v.description };
|
|
272
290
|
}
|
|
273
291
|
}
|
|
274
|
-
// Rule violations for this node
|
|
292
|
+
// Rule violations for this node — only error severity counts as an error
|
|
275
293
|
if (metadata.rules) {
|
|
276
294
|
const ruleViolations = await validateRules(nodes, metadata.rules);
|
|
277
295
|
for (const v of ruleViolations) {
|
|
278
296
|
if (v.file === label) {
|
|
279
|
-
|
|
297
|
+
const target = v.severity === 'error' ? errors : warnings;
|
|
298
|
+
target[`rule:${v.ruleId}`] = { kind: 'rule', message: `[${v.ruleId}] ${v.description}`, severity: v.severity };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Unresolved content links for this node (warnings — do not affect errorCount)
|
|
303
|
+
const targetNode = nodes.find((n) => n.label === label);
|
|
304
|
+
if (targetNode) {
|
|
305
|
+
const seen = new Set();
|
|
306
|
+
for (const link of targetNode.resolvedLinks) {
|
|
307
|
+
if (link.location === 'internal' && !seen.has(link.target)) {
|
|
308
|
+
seen.add(link.target);
|
|
309
|
+
warnings[`content-link:${link.target}`] = {
|
|
310
|
+
kind: 'content-link',
|
|
311
|
+
message: `Unresolved content link: ${link.target}`,
|
|
312
|
+
};
|
|
280
313
|
}
|
|
281
314
|
}
|
|
282
315
|
}
|
|
@@ -286,6 +319,8 @@ export async function validateFile(filePath, config) {
|
|
|
286
319
|
space: space.name,
|
|
287
320
|
errors,
|
|
288
321
|
errorCount: Object.keys(errors).length,
|
|
322
|
+
warnings,
|
|
323
|
+
warningCount: Object.keys(warnings).length,
|
|
289
324
|
inSpace: true,
|
|
290
325
|
};
|
|
291
326
|
}
|