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.
Files changed (107) hide show
  1. package/README.md +85 -25
  2. package/dist/api.d.ts +25 -5
  3. package/dist/api.js +22 -2
  4. package/dist/commands/diagram.d.ts +1 -0
  5. package/dist/commands/diagram.js +7 -1
  6. package/dist/commands/dump.d.ts +15 -2
  7. package/dist/commands/dump.js +18 -4
  8. package/dist/commands/miro-sync.d.ts +3 -0
  9. package/dist/commands/miro-sync.js +8 -0
  10. package/dist/commands/render.d.ts +4 -0
  11. package/dist/commands/render.js +64 -9
  12. package/dist/commands/schemas.d.ts +2 -2
  13. package/dist/commands/schemas.js +117 -24
  14. package/dist/commands/show.d.ts +1 -0
  15. package/dist/commands/show.js +7 -1
  16. package/dist/commands/spaces.d.ts +3 -1
  17. package/dist/commands/spaces.js +11 -4
  18. package/dist/commands/validate-file.js +20 -5
  19. package/dist/commands/validate.js +54 -21
  20. package/dist/config.d.ts +26 -13
  21. package/dist/config.js +104 -31
  22. package/dist/filter/augment-nodes.d.ts +3 -1
  23. package/dist/filter/augment-nodes.js +12 -7
  24. package/dist/filter/filter-nodes.js +1 -1
  25. package/dist/index.js +51 -35
  26. package/dist/load-space-graph.d.ts +42 -0
  27. package/dist/load-space-graph.js +53 -0
  28. package/dist/plugins/index.js +3 -1
  29. package/dist/plugins/loader.js +4 -4
  30. package/dist/plugins/markdown/extract-content-links.d.ts +23 -0
  31. package/dist/plugins/markdown/extract-content-links.js +123 -0
  32. package/dist/plugins/markdown/index.js +5 -5
  33. package/dist/plugins/markdown/parse-embedded.d.ts +6 -0
  34. package/dist/plugins/markdown/parse-embedded.js +50 -7
  35. package/dist/plugins/markdown/read-space.js +41 -5
  36. package/dist/plugins/markdown/render-directory.d.ts +11 -0
  37. package/dist/plugins/markdown/render-directory.js +130 -0
  38. package/dist/plugins/mermaid/index.d.ts +2 -0
  39. package/dist/plugins/mermaid/index.js +14 -0
  40. package/dist/plugins/{markdown → mermaid}/render-mermaid.js +17 -15
  41. package/dist/{integrations → plugins}/miro/cache.d.ts +1 -1
  42. package/dist/plugins/miro/index.d.ts +2 -0
  43. package/dist/plugins/miro/index.js +27 -0
  44. package/dist/{integrations → plugins}/miro/layout.d.ts +2 -2
  45. package/dist/{integrations → plugins}/miro/layout.js +3 -16
  46. package/dist/{integrations → plugins}/miro/styles.d.ts +2 -2
  47. package/dist/plugins/miro/sync.d.ts +7 -0
  48. package/dist/{integrations → plugins}/miro/sync.js +22 -39
  49. package/dist/plugins/util.d.ts +48 -1
  50. package/dist/plugins/util.js +143 -0
  51. package/dist/read/resolve-graph-edges.d.ts +1 -1
  52. package/dist/read/resolve-graph-edges.js +78 -27
  53. package/dist/read/wikilink-utils.d.ts +17 -0
  54. package/dist/read/wikilink-utils.js +40 -0
  55. package/dist/render/render.d.ts +4 -1
  56. package/dist/render/render.js +20 -16
  57. package/dist/schema/evaluate-rule.js +1 -0
  58. package/dist/schema/meta-schemas.d.ts +13 -0
  59. package/dist/schema/meta-schemas.js +50 -0
  60. package/dist/schema/metadata-contract.d.ts +343 -187
  61. package/dist/schema/metadata-contract.js +68 -30
  62. package/dist/schema/schema.d.ts +17 -9
  63. package/dist/schema/schema.js +283 -200
  64. package/dist/schema/validate-graph.d.ts +2 -1
  65. package/dist/schema/validate-graph.js +40 -23
  66. package/dist/schema/validate-rules.d.ts +5 -1
  67. package/dist/schema/validate-rules.js +20 -1
  68. package/dist/schemas/_knowledge_wiki.json +21 -21
  69. package/dist/schemas/_ost_strict.json +2 -2
  70. package/dist/schemas/_sctx_base.json +2 -2
  71. package/dist/schemas/_strategy_general.json +38 -38
  72. package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  73. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
  74. package/dist/schemas/knowledge_wiki.json +10 -9
  75. package/dist/schemas/strategy_general.json +15 -14
  76. package/dist/schemas/strict_ost.json +17 -16
  77. package/dist/space-context.d.ts +27 -0
  78. package/dist/space-context.js +46 -0
  79. package/dist/space-graph.d.ts +9 -7
  80. package/dist/space-graph.js +7 -5
  81. package/dist/testing.d.ts +33 -0
  82. package/dist/testing.js +72 -0
  83. package/dist/types.d.ts +101 -8
  84. package/dist/validate.d.ts +15 -3
  85. package/dist/validate.js +55 -20
  86. package/docs/concepts.md +37 -4
  87. package/docs/config.md +10 -0
  88. package/docs/rules.md +49 -22
  89. package/docs/schemas.md +110 -35
  90. package/package.json +13 -8
  91. package/schemas/_knowledge_wiki.json +21 -21
  92. package/schemas/_ost_strict.json +2 -2
  93. package/schemas/_sctx_base.json +2 -2
  94. package/schemas/_strategy_general.json +38 -38
  95. package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  96. package/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
  97. package/schemas/knowledge_wiki.json +10 -9
  98. package/schemas/strategy_general.json +15 -14
  99. package/schemas/strict_ost.json +17 -16
  100. package/dist/integrations/miro/sync.d.ts +0 -8
  101. package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
  102. package/schemas/generated/_structured_context_schema_meta.json +0 -191
  103. /package/dist/plugins/{markdown → mermaid}/render-mermaid.d.ts +0 -0
  104. /package/dist/{integrations → plugins}/miro/cache.js +0 -0
  105. /package/dist/{integrations → plugins}/miro/client.d.ts +0 -0
  106. /package/dist/{integrations → plugins}/miro/client.js +0 -0
  107. /package/dist/{integrations → plugins}/miro/styles.js +0 -0
@@ -1,8 +1,10 @@
1
- import type { HierarchyLevel, SpaceNode } from './types';
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 levels via buildSpaceGraph().
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
- /** Hierarchy levels used to build this graph. */
25
- readonly levels: readonly HierarchyLevel[];
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 level definitions. */
28
- export declare function buildSpaceGraph(nodes: SpaceNode[], levels: readonly HierarchyLevel[]): SpaceGraph;
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;
@@ -1,7 +1,9 @@
1
- /** Build a SpaceGraph from a flat list of SpaceNodes and hierarchy level definitions. */
2
- export function buildSpaceGraph(nodes, levels) {
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.source === 'hierarchy') {
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((r) => r.source === 'hierarchy');
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
- levels,
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;
@@ -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
- levels: HierarchyLevel[];
121
- allowSkipLevels?: boolean;
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 {
@@ -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): Promise<ValidationResult>;
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): Promise<ValidateFileOutput>;
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, loadSchema } from './schema/schema';
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 orphan count (informational, not a validation error)
170
- if (metadata.hierarchy) {
171
- result.orphans = [...buildSpaceGraph(nodes, metadata.hierarchy.levels).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 = buildContextForSpace(space, config);
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}`] = { kind: 'schema', message };
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
- errors[`rule:${v.ruleId}`] = { kind: 'rule', message: `[${v.ruleId}] ${v.description}` };
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
  }