structured-context 0.10.0 → 0.11.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 +22 -3
- package/dist/api.d.ts +21 -3
- package/dist/api.js +19 -2
- package/dist/commands/diagram.js +1 -1
- package/dist/commands/miro-sync.d.ts +3 -0
- package/dist/commands/miro-sync.js +8 -0
- package/dist/commands/schemas.js +3 -2
- package/dist/commands/spaces.js +0 -2
- package/dist/commands/validate-file.js +17 -5
- package/dist/commands/validate.js +16 -0
- package/dist/config.d.ts +8 -12
- package/dist/config.js +24 -22
- package/dist/index.js +13 -21
- package/dist/load-space-graph.d.ts +40 -0
- package/dist/load-space-graph.js +42 -0
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/loader.js +2 -2
- package/dist/plugins/markdown/extract-content-links.d.ts +23 -0
- package/dist/plugins/markdown/extract-content-links.js +144 -0
- package/dist/plugins/markdown/index.js +1 -7
- package/dist/plugins/markdown/parse-embedded.js +9 -0
- package/dist/plugins/markdown/read-space.js +3 -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 -16
- 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 +23 -41
- package/dist/plugins/util.d.ts +9 -0
- package/dist/plugins/util.js +4 -0
- package/dist/read/resolve-graph-edges.js +32 -0
- package/dist/render/render.d.ts +1 -0
- package/dist/render/render.js +20 -16
- package/dist/schema/evaluate-rule.js +1 -0
- package/dist/schema/schema.d.ts +3 -1
- package/dist/schema/schema.js +6 -2
- package/dist/schemas/_knowledge_wiki.json +1 -1
- package/dist/space-context.d.ts +24 -0
- package/dist/space-context.js +41 -0
- package/dist/types.d.ts +52 -0
- package/dist/validate.d.ts +12 -3
- package/dist/validate.js +37 -12
- package/docs/concepts.md +23 -0
- package/docs/config.md +2 -0
- package/package.json +1 -1
- package/schemas/_knowledge_wiki.json +1 -1
- package/dist/integrations/miro/sync.d.ts +0 -8
- /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/render/render.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { shortenPluginName } from '@/plugins/util';
|
|
2
|
+
import { updateSpaceField } from '../config';
|
|
3
|
+
import { assembleSpaceGraph } from '../load-space-graph';
|
|
2
4
|
import { loadPlugins } from '../plugins/loader';
|
|
3
|
-
import { readSpace } from '../read/read-space';
|
|
4
|
-
import { buildSpaceGraph } from '../space-graph';
|
|
5
5
|
import { buildFormatRegistry } from './registry';
|
|
6
6
|
export async function executeRender(formatName, context, options) {
|
|
7
7
|
const pluginMap = context.space?.plugins ?? {};
|
|
@@ -12,17 +12,21 @@ export async function executeRender(formatName, context, options) {
|
|
|
12
12
|
const available = registry.map((r) => r.qualifiedName).join(', ');
|
|
13
13
|
throw new Error(`Unknown render format: "${formatName}".${available ? ` Available: ${available}` : ' No formats registered.'}`);
|
|
14
14
|
}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
const graph = await assembleSpaceGraph(context, { filter: options.filter });
|
|
16
|
+
const shortName = shortenPluginName(entry.plugin.plugin.name);
|
|
17
|
+
const pluginContext = {
|
|
18
|
+
...context,
|
|
19
|
+
pluginConfig: entry.plugin.pluginConfig,
|
|
20
|
+
callbacks: {
|
|
21
|
+
persistConfig: (updates) => {
|
|
22
|
+
for (const [field, value] of Object.entries(updates)) {
|
|
23
|
+
updateSpaceField(context.space.name, field, value, shortName);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
return entry.plugin.plugin.render.render(pluginContext, graph, {
|
|
29
|
+
format: entry.format.name,
|
|
30
|
+
data: options.data,
|
|
31
|
+
});
|
|
28
32
|
}
|
|
@@ -52,6 +52,7 @@ function flattenNode(node) {
|
|
|
52
52
|
resolvedType: node.resolvedType,
|
|
53
53
|
resolvedParentTitle: node.resolvedParents[0]?.title, // first parent or undefined, provided for convenience
|
|
54
54
|
resolvedParentTitles: node.resolvedParents.map((r) => r.title), // full array of parent titles
|
|
55
|
+
resolvedLinks: node.resolvedLinks,
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
/**
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type AnySchemaObject, type ValidateFunction } from 'ajv';
|
|
2
2
|
import type { SchemaMetadata, SchemaWithMetadata } from '../types';
|
|
3
|
-
export declare
|
|
3
|
+
export declare let bundledSchemasDir: string;
|
|
4
|
+
/** Override the bundled schemas directory (e.g. when running in a bundled context). */
|
|
5
|
+
export declare function setBundledSchemasDir(dir: string): void;
|
|
4
6
|
export declare function readRawSchema(schemaPath: string): AnySchemaObject;
|
|
5
7
|
/**
|
|
6
8
|
* Build the full two-layer registry for a schema path:
|
package/dist/schema/schema.js
CHANGED
|
@@ -6,8 +6,12 @@ import Ajv, {} from 'ajv';
|
|
|
6
6
|
import JSON5 from 'json5';
|
|
7
7
|
import { DIALECT_META_SCHEMA, METADATA_SCHEMA, SCHEMA_META_ID, } from './metadata-contract';
|
|
8
8
|
import { isObject, mergeVariantProperties, resolveJsonPointer } from './schema-refs';
|
|
9
|
-
const packageDir = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
export
|
|
9
|
+
const packageDir = import.meta.url ? dirname(fileURLToPath(import.meta.url)) : '';
|
|
10
|
+
export let bundledSchemasDir = packageDir ? join(packageDir, '..', '..', 'schemas') : '';
|
|
11
|
+
/** Override the bundled schemas directory (e.g. when running in a bundled context). */
|
|
12
|
+
export function setBundledSchemasDir(dir) {
|
|
13
|
+
bundledSchemasDir = dir;
|
|
14
|
+
}
|
|
11
15
|
const validateMetadataContract = new Ajv().compile(METADATA_SCHEMA);
|
|
12
16
|
export function readRawSchema(schemaPath) {
|
|
13
17
|
return JSON5.parse(readFileSync(resolve(schemaPath), 'utf-8'));
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
},
|
|
208
208
|
"note": {
|
|
209
209
|
"type": "object",
|
|
210
|
-
"description": "Personal notes, journal entries,
|
|
210
|
+
"description": "Personal notes, journal entries, talk notes, or rough thoughts not drawn from an external URL.",
|
|
211
211
|
"allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
|
|
212
212
|
"properties": {
|
|
213
213
|
"type": { "const": "note" },
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Config } from './config';
|
|
2
|
+
import type { SpaceContext } from './types';
|
|
3
|
+
/** Thrown by createSpaceContext when no space matches the given name. */
|
|
4
|
+
export declare class SpaceNotFoundError extends Error {
|
|
5
|
+
readonly spaceName: string;
|
|
6
|
+
constructor(spaceName: string);
|
|
7
|
+
}
|
|
8
|
+
export interface CreateSpaceContextOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Directory to anchor relative plugin-config paths against. Defaults to the source
|
|
11
|
+
* directory recorded by loadConfig (space.sourceDir). Required for a hand-assembled
|
|
12
|
+
* Config, which has no source file — pass e.g. process.cwd(), or the directory the
|
|
13
|
+
* paths are relative to.
|
|
14
|
+
*/
|
|
15
|
+
configDir?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build a SpaceContext for a named space from an already-loaded config.
|
|
19
|
+
*
|
|
20
|
+
* Library-safe: throws {@link SpaceNotFoundError} on an unknown space rather than
|
|
21
|
+
* writing to the console or exiting the process. CLI callers that want the
|
|
22
|
+
* exit-on-error behaviour should wrap this (see buildSpaceContext in index.ts).
|
|
23
|
+
*/
|
|
24
|
+
export declare function createSpaceContext(spaceName: string, config: Config, options?: CreateSpaceContextOptions): SpaceContext;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolveSchema } from './config';
|
|
2
|
+
import { loadSchema } from './schema/schema';
|
|
3
|
+
/** Thrown by createSpaceContext when no space matches the given name. */
|
|
4
|
+
export class SpaceNotFoundError extends Error {
|
|
5
|
+
spaceName;
|
|
6
|
+
constructor(spaceName) {
|
|
7
|
+
super(`Unknown space "${spaceName}"`);
|
|
8
|
+
this.spaceName = spaceName;
|
|
9
|
+
this.name = 'SpaceNotFoundError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build a SpaceContext for a named space from an already-loaded config.
|
|
14
|
+
*
|
|
15
|
+
* Library-safe: throws {@link SpaceNotFoundError} on an unknown space rather than
|
|
16
|
+
* writing to the console or exiting the process. CLI callers that want the
|
|
17
|
+
* exit-on-error behaviour should wrap this (see buildSpaceContext in index.ts).
|
|
18
|
+
*/
|
|
19
|
+
export function createSpaceContext(spaceName, config, options = {}) {
|
|
20
|
+
const space = config.spaces.find((s) => s.name === spaceName);
|
|
21
|
+
if (!space) {
|
|
22
|
+
throw new SpaceNotFoundError(spaceName);
|
|
23
|
+
}
|
|
24
|
+
const configDir = options.configDir ?? space.sourceDir;
|
|
25
|
+
if (configDir === undefined) {
|
|
26
|
+
throw new Error(`Cannot resolve a config directory for space "${spaceName}": its Config was not loaded from a ` +
|
|
27
|
+
`file. Pass options.configDir (e.g. process.cwd()) to anchor relative plugin paths, or load ` +
|
|
28
|
+
`the config with loadConfig().`);
|
|
29
|
+
}
|
|
30
|
+
const resolvedSchemaPath = resolveSchema(config, space);
|
|
31
|
+
const { schema, schemaRefRegistry, schemaValidator } = loadSchema(resolvedSchemaPath);
|
|
32
|
+
return {
|
|
33
|
+
space,
|
|
34
|
+
config,
|
|
35
|
+
resolvedSchemaPath,
|
|
36
|
+
schema,
|
|
37
|
+
schemaRefRegistry,
|
|
38
|
+
schemaValidator,
|
|
39
|
+
configDir,
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -56,6 +56,42 @@ export type UnresolvedRef = {
|
|
|
56
56
|
/** Human-readable message matching validate-graph output format. */
|
|
57
57
|
message: string;
|
|
58
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Shared fields for all content link variants.
|
|
61
|
+
*/
|
|
62
|
+
type ContentLinkBase = {
|
|
63
|
+
/** Display text of the link (text content for markdown links, inner text or target for wikilinks). */
|
|
64
|
+
text: string;
|
|
65
|
+
/** Raw link target: URL for markdown links, wikilink path (without [[ ]]) for wikilinks. */
|
|
66
|
+
target: string;
|
|
67
|
+
/** Whether the link navigates to a resource or transcludes/embeds it. */
|
|
68
|
+
action: 'link' | 'embed';
|
|
69
|
+
/** Anchor fragment (heading or block ref, e.g. 'heading-title' or '^blockid') if present. */
|
|
70
|
+
anchor?: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* A link extracted from node content at parse time, before location classification.
|
|
74
|
+
* Populated by plugins that support content link extraction (e.g. the markdown plugin).
|
|
75
|
+
*/
|
|
76
|
+
export type ContentLink = ContentLinkBase & {
|
|
77
|
+
/** Link syntax — used during resolution to determine how to classify the target. */
|
|
78
|
+
linkSyntax: 'wikilink' | 'markdown';
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* A fully classified link from node content, after location resolution.
|
|
82
|
+
* Present on SpaceNode after graph resolution.
|
|
83
|
+
*/
|
|
84
|
+
export type ResolvedContentLink = ContentLinkBase & {
|
|
85
|
+
/**
|
|
86
|
+
* Resolved location classification:
|
|
87
|
+
* - 'node' — wikilink resolved to a known space node
|
|
88
|
+
* - 'internal' — wikilink or relative path, in the source system but not a space node
|
|
89
|
+
* - 'external' — http/https URL
|
|
90
|
+
* - 'system' — file:// URL
|
|
91
|
+
* - 'protocol' — other scheme (e.g. obsidian://)
|
|
92
|
+
*/
|
|
93
|
+
location: 'node' | 'internal' | 'external' | 'system' | 'protocol';
|
|
94
|
+
};
|
|
59
95
|
/**
|
|
60
96
|
* A node as produced by a parse plugin — raw type from content, no graph resolution applied.
|
|
61
97
|
* Core enriches this into a SpaceNode after parsing.
|
|
@@ -71,6 +107,11 @@ export type BaseNode = {
|
|
|
71
107
|
linkTargets: string[];
|
|
72
108
|
/** Raw type string from content, as written by the user. */
|
|
73
109
|
type: string;
|
|
110
|
+
/**
|
|
111
|
+
* Links extracted from content body and non-edge string fields at parse time.
|
|
112
|
+
* Populated by plugins that support content link extraction; absent if not populated.
|
|
113
|
+
*/
|
|
114
|
+
contentLinks?: ContentLink[];
|
|
74
115
|
};
|
|
75
116
|
/**
|
|
76
117
|
* A fully resolved node — enriched by core after plugin parsing.
|
|
@@ -85,6 +126,11 @@ export type SpaceNode = BaseNode & {
|
|
|
85
126
|
* Always present, empty if no parents resolved.
|
|
86
127
|
*/
|
|
87
128
|
resolvedParents: ResolvedParentRef[];
|
|
129
|
+
/**
|
|
130
|
+
* Links from content body and non-edge fields, with location classified after graph resolution.
|
|
131
|
+
* Always present, empty if the plugin did not populate rawContentLinks.
|
|
132
|
+
*/
|
|
133
|
+
resolvedLinks: ResolvedContentLink[];
|
|
88
134
|
};
|
|
89
135
|
export type ParseIssue = {
|
|
90
136
|
/** Source identifier of the file or item (filename or heading title). */
|
|
@@ -165,6 +211,12 @@ export interface FileValidationResult {
|
|
|
165
211
|
message: string;
|
|
166
212
|
}>;
|
|
167
213
|
errorCount: number;
|
|
214
|
+
/** Warnings keyed by composite id. Do not affect exit code or errorCount. */
|
|
215
|
+
warnings: Record<string, {
|
|
216
|
+
kind: string;
|
|
217
|
+
message: string;
|
|
218
|
+
}>;
|
|
219
|
+
warningCount: number;
|
|
168
220
|
inSpace: true;
|
|
169
221
|
}
|
|
170
222
|
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;
|
|
@@ -27,6 +27,11 @@ export interface ValidationResult {
|
|
|
27
27
|
hierarchyViolations: GraphViolation[];
|
|
28
28
|
orphans: SpaceNode[];
|
|
29
29
|
parseIssues: ParseIssue[];
|
|
30
|
+
/** Content links that could not be resolved to a space node. Warning only — not counted in errors. */
|
|
31
|
+
unresolvedContentLinks: Array<{
|
|
32
|
+
file: string;
|
|
33
|
+
target: string;
|
|
34
|
+
}>;
|
|
30
35
|
}
|
|
31
36
|
/**
|
|
32
37
|
* Format AJV errors for better readability.
|
|
@@ -37,7 +42,9 @@ export declare function formatErrors(errors: ErrorObject[], schema: SchemaWithMe
|
|
|
37
42
|
* Run full validation on a space and return a structured result.
|
|
38
43
|
* Does not output to console or call process.exit.
|
|
39
44
|
*/
|
|
40
|
-
export declare function validateSpace(context: SpaceContext
|
|
45
|
+
export declare function validateSpace(context: SpaceContext, options?: {
|
|
46
|
+
readResult?: ReadSpaceResult;
|
|
47
|
+
}): Promise<ValidationResult>;
|
|
41
48
|
/**
|
|
42
49
|
* Validate a single file within its space.
|
|
43
50
|
*
|
|
@@ -47,4 +54,6 @@ export declare function validateSpace(context: SpaceContext): Promise<Validation
|
|
|
47
54
|
* Returns a result object — does not output to console or call process.exit.
|
|
48
55
|
* If the file is not in any configured space, returns a result with `inSpace: false`.
|
|
49
56
|
*/
|
|
50
|
-
export declare function validateFile(filePath: string, config: Config
|
|
57
|
+
export declare function validateFile(filePath: string, config: Config, options?: {
|
|
58
|
+
configDir?: string;
|
|
59
|
+
}): 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
|
|
@@ -174,6 +175,19 @@ export async function validateSpace(context) {
|
|
|
174
175
|
if (metadata.rules) {
|
|
175
176
|
result.ruleViolations = await validateRules(nodes, metadata.rules);
|
|
176
177
|
}
|
|
178
|
+
// Collect unresolved content links (warnings — not errors)
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
for (const node of nodes) {
|
|
181
|
+
for (const link of node.resolvedLinks) {
|
|
182
|
+
if (link.location === 'internal') {
|
|
183
|
+
const key = `${node.label}::${link.target}`;
|
|
184
|
+
if (!seen.has(key)) {
|
|
185
|
+
seen.add(key);
|
|
186
|
+
result.unresolvedContentLinks.push({ file: node.label, target: link.target });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
177
191
|
return result;
|
|
178
192
|
}
|
|
179
193
|
/** Find which space a file belongs to by checking directory containment. */
|
|
@@ -189,12 +203,6 @@ function resolveFileSpace(filePath, config) {
|
|
|
189
203
|
}
|
|
190
204
|
return null;
|
|
191
205
|
}
|
|
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
206
|
/**
|
|
199
207
|
* Validate a single file within its space.
|
|
200
208
|
*
|
|
@@ -204,7 +212,7 @@ function buildContextForSpace(space, config) {
|
|
|
204
212
|
* Returns a result object — does not output to console or call process.exit.
|
|
205
213
|
* If the file is not in any configured space, returns a result with `inSpace: false`.
|
|
206
214
|
*/
|
|
207
|
-
export async function validateFile(filePath, config) {
|
|
215
|
+
export async function validateFile(filePath, config, options = {}) {
|
|
208
216
|
const resolved = resolveFileSpace(filePath, config);
|
|
209
217
|
if (!resolved) {
|
|
210
218
|
return {
|
|
@@ -214,7 +222,7 @@ export async function validateFile(filePath, config) {
|
|
|
214
222
|
};
|
|
215
223
|
}
|
|
216
224
|
const { space, label } = resolved;
|
|
217
|
-
const context =
|
|
225
|
+
const context = createSpaceContext(space.name, config, { configDir: options.configDir });
|
|
218
226
|
const { schema, schemaRefRegistry, schemaValidator } = context;
|
|
219
227
|
const metadata = schema.metadata;
|
|
220
228
|
const readResult = await readSpace(context);
|
|
@@ -280,12 +288,29 @@ export async function validateFile(filePath, config) {
|
|
|
280
288
|
}
|
|
281
289
|
}
|
|
282
290
|
}
|
|
291
|
+
// Unresolved content links for this node (warnings — do not affect errorCount)
|
|
292
|
+
const warnings = {};
|
|
293
|
+
const targetNode = nodes.find((n) => n.label === label);
|
|
294
|
+
if (targetNode) {
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
for (const link of targetNode.resolvedLinks) {
|
|
297
|
+
if (link.location === 'internal' && !seen.has(link.target)) {
|
|
298
|
+
seen.add(link.target);
|
|
299
|
+
warnings[`content-link:${link.target}`] = {
|
|
300
|
+
kind: 'content-link',
|
|
301
|
+
message: `Unresolved content link: ${link.target}`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
283
306
|
return {
|
|
284
307
|
file: isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath),
|
|
285
308
|
label,
|
|
286
309
|
space: space.name,
|
|
287
310
|
errors,
|
|
288
311
|
errorCount: Object.keys(errors).length,
|
|
312
|
+
warnings,
|
|
313
|
+
warningCount: Object.keys(warnings).length,
|
|
289
314
|
inSpace: true,
|
|
290
315
|
};
|
|
291
316
|
}
|
package/docs/concepts.md
CHANGED
|
@@ -254,6 +254,29 @@ Each entry is a `ResolvedParentRef` object:
|
|
|
254
254
|
|
|
255
255
|
The `source` label lets downstream consumers distinguish edge types without re-inspecting the schema. Validation routes `hierarchy` edges to structural checks (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
256
|
|
|
257
|
+
### Content links
|
|
258
|
+
|
|
259
|
+
**Content links** (`resolvedLinks` on `SpaceNode`) are all links extracted from a node's content at parse time — both from frontmatter non-edge fields and from the body text. They are distinct from graph edges: they are never used to infer structure, but are available to consumers for rendering, auditing, or navigation.
|
|
260
|
+
|
|
261
|
+
Each entry is a `ResolvedContentLink` object:
|
|
262
|
+
|
|
263
|
+
| Field | Type | Description |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| `text` | `string` | Display text of the link |
|
|
266
|
+
| `target` | `string` | Raw link target (URL or wikilink path) |
|
|
267
|
+
| `action` | `'link' \| 'embed'` | Whether the link navigates or transcludes |
|
|
268
|
+
| `anchor` | `string?` | Fragment component (heading or block ref) if present |
|
|
269
|
+
| `location` | `'node' \| 'internal' \| 'external' \| 'system' \| 'protocol'` | Resolved location classification |
|
|
270
|
+
|
|
271
|
+
**Location classification:**
|
|
272
|
+
- `node` — wikilink resolved to a known space node
|
|
273
|
+
- `internal` — wikilink or relative path to an in-source target that is not a space node
|
|
274
|
+
- `external` — `http://` or `https://` URL
|
|
275
|
+
- `system` — `file://` URL
|
|
276
|
+
- `protocol` — any other scheme (e.g. `obsidian://`)
|
|
277
|
+
|
|
278
|
+
**Sources:** Both wikilink syntax (`[[Target]]`, `![[Image]]`) and markdown link syntax (`[text](url)`, ``) are extracted. Bare URLs in frontmatter string fields are also captured. Graph edge fields (hierarchy `parent`, relationship fields) are excluded — those are graph edges, not content links.
|
|
279
|
+
|
|
257
280
|
### Anchor
|
|
258
281
|
|
|
259
282
|
An **anchor** is a block anchor (e.g. `^goal1`) appended to a heading in a `typed page`, using Obsidian block anchor syntax. Anchors serve two purposes:
|
package/docs/config.md
CHANGED
|
@@ -28,6 +28,8 @@ A space is a named directory or single file registered in the config. Example:
|
|
|
28
28
|
|
|
29
29
|
**`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
30
|
|
|
31
|
+
A config must define at least one of `spaces` or `includeSpacesFrom`. When using `includeSpacesFrom` alone, `spaces` can be omitted entirely.
|
|
32
|
+
|
|
31
33
|
## Plugins
|
|
32
34
|
|
|
33
35
|
Use `plugins` to load parse plugins that read spaces from non-markdown sources. The built-in markdown plugin is always available without any declaration. Plugins are tried in order; the first to return a result wins. The `plugins` field is a map of plugin name to plugin config, and can be declared at the top level (applies to all spaces) or per-space (overrides the top level):
|
package/package.json
CHANGED
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
},
|
|
208
208
|
"note": {
|
|
209
209
|
"type": "object",
|
|
210
|
-
"description": "Personal notes, journal entries,
|
|
210
|
+
"description": "Personal notes, journal entries, talk notes, or rough thoughts not drawn from an external URL.",
|
|
211
211
|
"allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
|
|
212
212
|
"properties": {
|
|
213
213
|
"type": { "const": "note" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|