structured-context 0.11.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 +63 -22
- package/dist/api.d.ts +7 -5
- package/dist/api.js +4 -1
- 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/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 +114 -22
- 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 -2
- package/dist/commands/validate-file.js +7 -4
- package/dist/commands/validate.js +38 -21
- package/dist/config.d.ts +18 -1
- package/dist/config.js +81 -10
- 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 +39 -15
- package/dist/load-space-graph.d.ts +4 -2
- package/dist/load-space-graph.js +15 -4
- package/dist/plugins/loader.js +2 -2
- package/dist/plugins/markdown/extract-content-links.js +8 -29
- package/dist/plugins/markdown/index.js +8 -2
- package/dist/plugins/markdown/parse-embedded.d.ts +6 -0
- package/dist/plugins/markdown/parse-embedded.js +41 -7
- package/dist/plugins/markdown/read-space.js +38 -5
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- package/dist/plugins/mermaid/render-mermaid.js +2 -1
- package/dist/plugins/miro/sync.js +7 -6
- package/dist/plugins/util.d.ts +39 -1
- package/dist/plugins/util.js +139 -0
- package/dist/read/resolve-graph-edges.d.ts +1 -1
- package/dist/read/resolve-graph-edges.js +46 -27
- package/dist/read/wikilink-utils.d.ts +17 -0
- package/dist/read/wikilink-utils.js +40 -0
- package/dist/render/render.d.ts +3 -1
- package/dist/render/render.js +2 -2
- 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 +14 -8
- package/dist/schema/schema.js +277 -198
- 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 +20 -20
- 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 +3 -0
- package/dist/space-context.js +7 -2
- 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 +54 -13
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +19 -9
- package/docs/concepts.md +14 -4
- package/docs/config.md +8 -0
- package/docs/rules.md +49 -22
- package/docs/schemas.md +110 -35
- package/package.json +13 -8
- package/schemas/_knowledge_wiki.json +20 -20
- 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/schemas/generated/_structured_context_schema_meta.json +0 -191
- package/schemas/generated/_structured_context_schema_meta.json +0 -191
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,8 @@ 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;
|
|
58
70
|
};
|
|
59
71
|
/**
|
|
60
72
|
* Shared fields for all content link variants.
|
|
@@ -107,11 +119,15 @@ export type BaseNode = {
|
|
|
107
119
|
linkTargets: string[];
|
|
108
120
|
/** Raw type string from content, as written by the user. */
|
|
109
121
|
type: string;
|
|
122
|
+
/** Body content of the node, if any. First-class accessor mirroring schemaData.content. */
|
|
123
|
+
content?: string;
|
|
110
124
|
/**
|
|
111
125
|
* Links extracted from content body and non-edge string fields at parse time.
|
|
112
126
|
* Populated by plugins that support content link extraction; absent if not populated.
|
|
113
127
|
*/
|
|
114
128
|
contentLinks?: ContentLink[];
|
|
129
|
+
/** Source location of the node definition in the filesystem. */
|
|
130
|
+
source?: SourceLocation;
|
|
115
131
|
};
|
|
116
132
|
/**
|
|
117
133
|
* A fully resolved node — enriched by core after plugin parsing.
|
|
@@ -143,14 +159,25 @@ export type ParseIssue = {
|
|
|
143
159
|
type: 'no-type' | 'parse' | string;
|
|
144
160
|
/** Human-readable detail. */
|
|
145
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;
|
|
146
166
|
};
|
|
147
167
|
/** Rule categories for organizing executable validation rules */
|
|
148
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';
|
|
149
174
|
export type { Rule } from './schema/metadata-contract';
|
|
150
175
|
export type RuleViolation = {
|
|
176
|
+
/** Node label, or empty string for global-scope rules. */
|
|
151
177
|
file: string;
|
|
152
178
|
ruleId: string;
|
|
153
179
|
category: RuleCategory;
|
|
180
|
+
severity: RuleSeverity;
|
|
154
181
|
description: string;
|
|
155
182
|
};
|
|
156
183
|
export type GraphViolation = {
|
|
@@ -161,11 +188,18 @@ export type GraphViolation = {
|
|
|
161
188
|
parentTitle: string;
|
|
162
189
|
description: string;
|
|
163
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
|
+
};
|
|
164
198
|
export type SchemaMetadata = {
|
|
165
|
-
hierarchy
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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[];
|
|
169
203
|
typeAliases?: Record<string, string>;
|
|
170
204
|
rules?: Rule[];
|
|
171
205
|
relationships?: Relationship[];
|
|
@@ -201,21 +235,28 @@ export type SpaceContext = {
|
|
|
201
235
|
/** Directory of the config file that defines this space. Used for resolving relative paths in plugin configs. */
|
|
202
236
|
configDir: string;
|
|
203
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
|
+
}
|
|
204
248
|
export interface FileValidationResult {
|
|
205
249
|
file: string;
|
|
206
250
|
label: string;
|
|
207
251
|
space: string;
|
|
208
252
|
/** Errors keyed by composite id (e.g. `schema:/status:enum:active`, `rule:my-rule-id`). */
|
|
209
|
-
errors: Record<string,
|
|
210
|
-
kind: string;
|
|
211
|
-
message: string;
|
|
212
|
-
}>;
|
|
253
|
+
errors: Record<string, FileValidationIssue>;
|
|
213
254
|
errorCount: number;
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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>;
|
|
219
260
|
warningCount: number;
|
|
220
261
|
inSpace: true;
|
|
221
262
|
}
|
package/dist/validate.d.ts
CHANGED
|
@@ -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,6 +25,7 @@ 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[];
|
package/dist/validate.js
CHANGED
|
@@ -121,7 +121,8 @@ export async function validateSpace(context, options = {}) {
|
|
|
121
121
|
if (nodeType !== undefined && validTypes.length > 0 && !validTypes.includes(nodeType)) {
|
|
122
122
|
result.nodeErrorCount++;
|
|
123
123
|
result.nodeErrors.push({
|
|
124
|
-
file: node.label,
|
|
124
|
+
file: node.source?.file ?? node.label,
|
|
125
|
+
...(node.source?.line !== undefined ? { line: node.source.line } : {}),
|
|
125
126
|
errors: [
|
|
126
127
|
{
|
|
127
128
|
instancePath: '/type',
|
|
@@ -143,7 +144,8 @@ export async function validateSpace(context, options = {}) {
|
|
|
143
144
|
else {
|
|
144
145
|
result.nodeErrorCount++;
|
|
145
146
|
result.nodeErrors.push({
|
|
146
|
-
file: node.label,
|
|
147
|
+
file: node.source?.file ?? node.label,
|
|
148
|
+
...(node.source?.line !== undefined ? { line: node.source.line } : {}),
|
|
147
149
|
errors: schemaValidator.errors || [],
|
|
148
150
|
nodeData: node.schemaData,
|
|
149
151
|
});
|
|
@@ -167,9 +169,9 @@ export async function validateSpace(context, options = {}) {
|
|
|
167
169
|
const hierarchyValidation = validateGraph(nodes, metadata, readResult.unresolvedRefs);
|
|
168
170
|
result.refErrors.push(...hierarchyValidation.refErrors);
|
|
169
171
|
result.hierarchyViolations = [...hierarchyValidation.violations];
|
|
170
|
-
// Calculate
|
|
171
|
-
|
|
172
|
-
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);
|
|
173
175
|
}
|
|
174
176
|
// Load and execute rules validation if schema defines rules
|
|
175
177
|
if (metadata.rules) {
|
|
@@ -230,6 +232,7 @@ export async function validateFile(filePath, config, options = {}) {
|
|
|
230
232
|
// Pre-extract valid types for early type validation
|
|
231
233
|
const validTypes = Array.isArray(schema.oneOf) ? extractEntityInfo(schema, schemaRefRegistry).map((e) => e.type) : [];
|
|
232
234
|
const errors = {};
|
|
235
|
+
const warnings = {};
|
|
233
236
|
// Schema validation errors for this node
|
|
234
237
|
for (const node of nodes) {
|
|
235
238
|
if (node.label !== label)
|
|
@@ -240,6 +243,8 @@ export async function validateFile(filePath, config, options = {}) {
|
|
|
240
243
|
errors[`schema:type:${validTypes.join(',')}`] = {
|
|
241
244
|
kind: 'schema',
|
|
242
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 } : {}),
|
|
243
248
|
};
|
|
244
249
|
continue;
|
|
245
250
|
}
|
|
@@ -247,7 +252,12 @@ export async function validateFile(filePath, config, options = {}) {
|
|
|
247
252
|
if (!valid) {
|
|
248
253
|
const formatted = formatErrors(schemaValidator.errors ?? [], schema, schemaRefRegistry, node.schemaData);
|
|
249
254
|
for (const { message, dedupeKey } of formatted) {
|
|
250
|
-
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
|
+
};
|
|
251
261
|
}
|
|
252
262
|
}
|
|
253
263
|
}
|
|
@@ -279,17 +289,17 @@ export async function validateFile(filePath, config, options = {}) {
|
|
|
279
289
|
errors[`hierarchy:${v.description}`] = { kind: 'hierarchy', message: v.description };
|
|
280
290
|
}
|
|
281
291
|
}
|
|
282
|
-
// Rule violations for this node
|
|
292
|
+
// Rule violations for this node — only error severity counts as an error
|
|
283
293
|
if (metadata.rules) {
|
|
284
294
|
const ruleViolations = await validateRules(nodes, metadata.rules);
|
|
285
295
|
for (const v of ruleViolations) {
|
|
286
296
|
if (v.file === label) {
|
|
287
|
-
|
|
297
|
+
const target = v.severity === 'error' ? errors : warnings;
|
|
298
|
+
target[`rule:${v.ruleId}`] = { kind: 'rule', message: `[${v.ruleId}] ${v.description}`, severity: v.severity };
|
|
288
299
|
}
|
|
289
300
|
}
|
|
290
301
|
}
|
|
291
302
|
// Unresolved content links for this node (warnings — do not affect errorCount)
|
|
292
|
-
const warnings = {};
|
|
293
303
|
const targetNode = nodes.find((n) => n.label === label);
|
|
294
304
|
if (targetNode) {
|
|
295
305
|
const seen = new Set();
|
package/docs/concepts.md
CHANGED
|
@@ -93,6 +93,8 @@ A **space node** (or **node** for short) is a single entity in a `space` — a n
|
|
|
93
93
|
|
|
94
94
|
Node types are defined by the schema in use and may vary across schemas. Examples from the default schema: `vision`, `mission`, `goal`, `opportunity`, `solution`. The tooling is not prescriptive about which types exist — schemas are designed to be extended and replaced.
|
|
95
95
|
|
|
96
|
+
Each node retains its physical `source` location (`file`, and `line` / `column` where available) so that validation diagnostics, edge errors, and downstream tooling pinpoint the exact location in the vault.
|
|
97
|
+
|
|
96
98
|
> `space_on_a_page` and `dashboard` are not `space node` types — they are `tooling types`.
|
|
97
99
|
|
|
98
100
|
### Embedded node
|
|
@@ -111,7 +113,7 @@ A `space node`'s resolved type (`resolvedType`) is its canonical type after alia
|
|
|
111
113
|
|
|
112
114
|
## Typed page
|
|
113
115
|
|
|
114
|
-
A **typed page** is a markdown file whose frontmatter declares a `space node` type (e.g. `type: goal`). The file itself represents one node, and its body may additionally contain `embedded nodes`.
|
|
116
|
+
A **typed page** is a markdown file whose frontmatter declares a `space node` type (e.g. `type: goal`). The file itself represents one node, and its body may additionally contain `embedded nodes`. Body content appearing before any embedded headings (or the entire body when no embedded headings are present) is preserved as the page node's `content`.
|
|
115
117
|
|
|
116
118
|
Typed pages are distinct from `space on a page` files: a typed page *is* a `space node`; a `space_on_a_page` file is merely a container.
|
|
117
119
|
|
|
@@ -123,7 +125,7 @@ A **schema** defines the valid structure for nodes in a `space`: the fields, typ
|
|
|
123
125
|
|
|
124
126
|
The schema handles structural validation. Cross-node and workflow checks are handled by executable `rules` defined in `$metadata.rules`.
|
|
125
127
|
|
|
126
|
-
Schemas are composable
|
|
128
|
+
Schemas are composable. Structural definitions are reused with `$ref` (validation only). Metadata is reused only by listing schema `$id`s in `$metadata.imports`: imports merge transitively, in order, with the schema's own metadata last. Hierarchies, relationships, aliases and rules all follow one rule — a duplicate is an error unless the later entry sets `override: true` — and each declared hierarchy is kept as a separate named hierarchy.
|
|
127
129
|
|
|
128
130
|
### Rules
|
|
129
131
|
|
|
@@ -141,6 +143,8 @@ Rules may be:
|
|
|
141
143
|
|
|
142
144
|
Rules are distinct from schema validation: the schema checks structure; rules check meaning and quality.
|
|
143
145
|
|
|
146
|
+
Each executable rule has a **category** (`validation`, `coherence`, `workflow`, `best-practice`) that supplies its default **severity** (`error`, `warning`, `info`). A rule may override its category's severity, but categories are the preferred grouping. Only `error` severity violations fail validation.
|
|
147
|
+
|
|
144
148
|
See [docs/rules.md](rules.md) for the rules reference, including JSONata expression syntax and the full `$metadata` field reference.
|
|
145
149
|
|
|
146
150
|
---
|
|
@@ -160,6 +164,10 @@ The **hierarchy** is the ordered list of node types in a space, from root to lea
|
|
|
160
164
|
|
|
161
165
|
The hierarchy is modelled as a layered DAG: a non-root node may have zero parents (orphaned), one parent, or multiple parents. The `show` command renders this as an indented tree, marking repeated nodes with `(*)` where the subtree is already shown elsewhere.
|
|
162
166
|
|
|
167
|
+
A space may have **several hierarchies**, one for each imported schema that declares one. Each hierarchy has a **name** (defaulting to the declaring schema's file stem), and each type belongs to at most one hierarchy. Structural validation runs per hierarchy; relationships may connect types across hierarchies.
|
|
168
|
+
|
|
169
|
+
The **main hierarchy** is the one declared by the root schema; failing that, the only imported hierarchy; failing that, the one the root selects with `"hierarchy": { "$ref": ... }`. The main hierarchy drives `space on a page` parsing, hierarchy embedding, template sync, and default rendering. `show`, `diagram` and `render` take `--hierarchy <name>` to render another. See [docs/schemas.md](schemas.md#multiple-hierarchies).
|
|
170
|
+
|
|
163
171
|
Each non-root level uses the shared `field`, `fieldOn`, and `multiple` edge options (see [Graph edges](#graph-edges)). Hierarchy-specific options:
|
|
164
172
|
|
|
165
173
|
| Option | Default | Meaning |
|
|
@@ -250,9 +258,10 @@ Each entry is a `ResolvedParentRef` object:
|
|
|
250
258
|
| `title` | `string` | The parent node's title |
|
|
251
259
|
| `field` | `string` | The frontmatter field that held the wikilink |
|
|
252
260
|
| `source` | `'hierarchy' \| 'relationship'` | Whether the edge came from a hierarchy level or a relationship |
|
|
261
|
+
| `hierarchy` | `string` | Name of the hierarchy the edge belongs to; present only when `source` is `'hierarchy'` |
|
|
253
262
|
| `selfRef` | `boolean` | Whether the edge is a same-type (self-referential) parent link |
|
|
254
263
|
|
|
255
|
-
The `source`
|
|
264
|
+
The `source` and `hierarchy` labels let downstream consumers distinguish edge types and hierarchies without re-inspecting the schema. Validation routes `hierarchy` edges to structural checks within their own hierarchy (parent-type rules, skip-level detection) and `relationship` edges to field reference checks (type-match, missing-target). Tree rendering and rule evaluation use the full set.
|
|
256
265
|
|
|
257
266
|
### Content links
|
|
258
267
|
|
|
@@ -315,7 +324,8 @@ Each entry in `ancestors[]` or `descendants[]` includes all schema fields of the
|
|
|
315
324
|
| Metadata field | Type | Description |
|
|
316
325
|
|----------------|------|-------------|
|
|
317
326
|
| `_field` | `string` | The edge field name that connects to this ancestor/descendant |
|
|
318
|
-
| `_source` | `'hierarchy' \| 'relationship'` | Whether the edge came from
|
|
327
|
+
| `_source` | `'hierarchy' \| 'relationship'` | Whether the edge came from a hierarchy or a relationship |
|
|
328
|
+
| `_hierarchy` | `string` | Hierarchy name; present only for hierarchy edges |
|
|
319
329
|
| `_selfRef` | `boolean` | Whether the edge is a same-type (self-referential) link |
|
|
320
330
|
|
|
321
331
|
### SELECT spec
|
package/docs/config.md
CHANGED
|
@@ -26,6 +26,12 @@ A space is a named directory or single file registered in the config. Example:
|
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
**`schema`** — the schema used to validate nodes in the space. Can be:
|
|
30
|
+
- A bundled schema name (e.g. `strategy_general.json`, `strict_ost.json`)
|
|
31
|
+
- A local path relative to the config file (e.g. `./schemas/custom.json`) or absolute path
|
|
32
|
+
- A plugin schema prefixed with the plugin name (e.g. `sctx-wardley-mapping/wardley_map.json` or `wardley-mapping/wardley_map.json`). Specifying a plugin-prefixed schema automatically loads that plugin for the space.
|
|
33
|
+
- A simple filename (e.g. `wardley_map.json`) provided by a plugin explicitly declared under `plugins`.
|
|
34
|
+
|
|
29
35
|
**`includeSpacesFrom`** — import space definitions from other config files. Useful for aggregating spaces from multiple projects into a central config. Duplicate space names are not allowed.
|
|
30
36
|
|
|
31
37
|
A config must define at least one of `spaces` or `includeSpacesFrom`. When using `includeSpacesFrom` alone, `spaces` can be omitted entirely.
|
|
@@ -53,6 +59,8 @@ Use `plugins` to load parse plugins that read spaces from non-markdown sources.
|
|
|
53
59
|
|
|
54
60
|
All plugin names must start with `sctx-` (the prefix is optional in config and normalised on load). The special name `markdown` refers to the built-in markdown plugin. External plugins are resolved in order: config-adjacent (`{configDir}/plugins/{name}`), then npm. Each plugin must export a `configSchema` JSON Schema; config is validated against it on load. Fields annotated `format: 'path'` in a plugin's `configSchema` are resolved relative to the config file directory.
|
|
55
61
|
|
|
62
|
+
Plugins can also contribute schemas and partials via a `<pluginRoot>/schemas/` directory (or `"sctx": { "schemas": "..." }` in `package.json`). If a space specifies a plugin-prefixed schema (e.g. `"sctx-wardley-mapping/wardley_map.json"`), schemas are resolved statically without executing plugin code, and the plugin's code hooks are loaded when parsing or rendering.
|
|
63
|
+
|
|
56
64
|
## Markdown plugin config
|
|
57
65
|
|
|
58
66
|
Set under `plugins.markdown` per space.
|
package/docs/rules.md
CHANGED
|
@@ -24,22 +24,56 @@ Rules are a flat array.
|
|
|
24
24
|
|---|---|---|
|
|
25
25
|
| `id` | yes | Unique rule identifier |
|
|
26
26
|
| `category` | yes | `validation` \| `coherence` \| `workflow` \| `best-practice` |
|
|
27
|
+
| `severity` | no | `error` \| `warning` \| `info`; overrides the category default (see [Severity](#severity)) |
|
|
27
28
|
| `description` | yes | Human-readable rule intent |
|
|
28
29
|
| `check` | yes | JSONata expression; must evaluate to `true` |
|
|
29
30
|
| `type` | no | Restrict rule to nodes of this `resolvedType` |
|
|
30
31
|
| `scope` | no | Use `"global"` to evaluate once for the whole space |
|
|
31
32
|
| `override` | no | Only for merge conflicts: allows later duplicate `id` to replace earlier |
|
|
32
33
|
|
|
33
|
-
## Categories
|
|
34
|
+
## Categories and severity
|
|
34
35
|
|
|
35
|
-
Categories
|
|
36
|
+
Categories describe a rule's intent and supply its default severity. They do not change expression execution semantics.
|
|
36
37
|
|
|
37
|
-
| Category | Typical use |
|
|
38
|
-
|
|
39
|
-
| `validation` | Hard correctness constraints |
|
|
40
|
-
| `coherence` | Cross-node consistency checks |
|
|
41
|
-
| `workflow` | Process/operating-discipline checks |
|
|
42
|
-
| `best-practice` | Advisory quality checks |
|
|
38
|
+
| Category | Typical use | Default severity |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `validation` | Hard correctness constraints | `error` |
|
|
41
|
+
| `coherence` | Cross-node consistency checks | `warning` |
|
|
42
|
+
| `workflow` | Process/operating-discipline checks | `warning` |
|
|
43
|
+
| `best-practice` | Advisory quality checks | `info` |
|
|
44
|
+
|
|
45
|
+
### Severity
|
|
46
|
+
|
|
47
|
+
Every rule violation is reported with a severity:
|
|
48
|
+
|
|
49
|
+
| Severity | Meaning | Fails validation |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `error` | Content is wrong | yes |
|
|
52
|
+
| `warning` | Content is likely inconsistent or incomplete | no |
|
|
53
|
+
| `info` | Content could be better | no |
|
|
54
|
+
|
|
55
|
+
Only `error` violations fail validation: `sctx validate` and `sctx validate-file` exit non-zero, and JSON output reports `valid: false`. Warnings and info violations are reported but don't fail, so a work-in-progress space can still pass while advisory checks remain visible.
|
|
56
|
+
|
|
57
|
+
Prefer choosing the right category and relying on its default. Set `severity` on an individual rule only when that rule genuinely differs from the rest of its category:
|
|
58
|
+
|
|
59
|
+
```json5
|
|
60
|
+
{
|
|
61
|
+
"id": "goal-not-blocked",
|
|
62
|
+
"category": "workflow",
|
|
63
|
+
"severity": "error",
|
|
64
|
+
"description": "Goals must not be left blocked",
|
|
65
|
+
"type": "goal",
|
|
66
|
+
"check": "current.status != 'blocked'"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`sctx schemas show` lists each category with its default severity and marks rules that override it.
|
|
71
|
+
|
|
72
|
+
### Output
|
|
73
|
+
|
|
74
|
+
- **`sctx validate`** — the summary counts rule violations per severity, and details are grouped by severity with each line showing the rule's category and id.
|
|
75
|
+
- **`sctx validate --json`** — `errors` includes only `error` severity rule violations (keyed `rule:<id>`, with a `severity` field). A `ruleViolations` array lists every violation with `file`, `ruleId`, `category`, `severity` and `description` (`file` is empty for global rules).
|
|
76
|
+
- **`sctx validate-file`** — `error` severity violations appear under `errors`; `warning` and `info` violations appear under `warnings`. Rule entries carry a `severity` field.
|
|
43
77
|
|
|
44
78
|
## Evaluation model
|
|
45
79
|
|
|
@@ -73,28 +107,21 @@ Inside `nodes[...]`, bare names refer to each candidate node. Use `$$` to refere
|
|
|
73
107
|
$count(nodes[resolvedParentTitle=$$.current.title and resolvedType='solution'])
|
|
74
108
|
```
|
|
75
109
|
|
|
76
|
-
##
|
|
110
|
+
## Reusing rules
|
|
77
111
|
|
|
78
|
-
`$metadata.rules`
|
|
79
|
-
- one rule
|
|
80
|
-
- a rule-set object with `rules: []`
|
|
81
|
-
|
|
82
|
-
Example:
|
|
112
|
+
To share rules, put them in a schema's `$metadata.rules` and list that schema's `$id` in `$metadata.imports`. An import brings the whole schema's metadata, including its own imports:
|
|
83
113
|
|
|
84
114
|
```json5
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
]
|
|
115
|
+
"$metadata": {
|
|
116
|
+
"imports": ["sctx://_rule-pack"]
|
|
117
|
+
}
|
|
89
118
|
```
|
|
90
119
|
|
|
91
|
-
Imported entries are normalized into the same flat runtime list.
|
|
92
|
-
|
|
93
120
|
## Merge and conflict behavior
|
|
94
121
|
|
|
95
|
-
When metadata is
|
|
122
|
+
When metadata is merged from imports (imports in order, then the schema's own metadata):
|
|
96
123
|
- Rules are merged by `id`.
|
|
97
|
-
-
|
|
124
|
+
- A duplicate `id` is an error.
|
|
98
125
|
- A later rule may replace an earlier one only with `override: true`.
|
|
99
126
|
|
|
100
127
|
Example override:
|