structured-context 0.9.1 → 0.9.3
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 +10 -6
- package/dist/commands/spaces.js +3 -2
- package/dist/commands/validate.js +15 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/plugin-api.d.ts +1 -1
- package/dist/plugins/markdown/read-space.js +35 -8
- package/dist/plugins/util.d.ts +3 -3
- package/dist/read/read-space.js +1 -1
- package/dist/schema/schema.js +44 -7
- package/dist/schemas/_knowledge_wiki.json +246 -0
- package/dist/schemas/{general.json → _strategy_general.json} +16 -15
- package/dist/schemas/knowledge_wiki.json +7 -237
- package/dist/schemas/strategy_general.json +19 -0
- package/dist/types.d.ts +14 -2
- package/docs/config.md +1 -1
- package/docs/schemas.md +8 -5
- package/package.json +1 -1
- package/schemas/_knowledge_wiki.json +246 -0
- package/schemas/{general.json → _strategy_general.json} +16 -15
- package/schemas/knowledge_wiki.json +7 -237
- package/schemas/strategy_general.json +19 -0
package/README.md
CHANGED
|
@@ -55,12 +55,13 @@ Install globally (recommended):
|
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
bun install -g structured-context
|
|
58
|
+
sctx --help
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
Or use directly via `bunx`:
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
|
-
bunx structured-context
|
|
64
|
+
bunx structured-context --help
|
|
64
65
|
```
|
|
65
66
|
|
|
66
67
|
## Setup for AI Agents
|
|
@@ -179,21 +180,24 @@ Bare wikilink items (`- [[Existing Node]]`) in any embedding section populate a
|
|
|
179
180
|
With `fieldOn: "parent"`, embedded child nodes (parsed from a matching heading's list or table) are appended as wikilinks to the parent's `field` array, rather than receiving a `parent` field. This matches schemas where the content model naturally lists children on the parent (e.g. `activity.tasks: ["[[Task A]]"]`).
|
|
180
181
|
|
|
181
182
|
Metadata is composable across `$ref` graphs:
|
|
182
|
-
-
|
|
183
|
+
- `hierarchy`: multiple schema files may define a hierarchy; **last one wins** (root schema overrides partials)
|
|
183
184
|
- `aliases` are shallow-merged (later wins)
|
|
184
185
|
- `rules` merge by `id`; conflicts error unless the later rule sets `override: true`
|
|
185
186
|
- `$metadata.rules` supports `$ref` imports for reusable rule packs
|
|
187
|
+
- `relationships` are collected from all files
|
|
186
188
|
|
|
187
|
-
If no
|
|
189
|
+
If no file defines `hierarchy`, hierarchy-specific checks are skipped.
|
|
188
190
|
|
|
189
191
|
**Customizing Schemas:**
|
|
190
|
-
- **Partial schemas**: Files starting with an underscore (like `_sctx_base.json`) are loaded and used to resolve references (using `$ref`).
|
|
192
|
+
- **Partial schemas**: Files starting with an underscore (like `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`) are loaded and used to resolve references (using `$ref`).
|
|
193
|
+
- **Partials as entity libraries**: Partials can define reusable entity types in `$defs` that composing schemas reference via `$ref`. Bundled partials like `_strategy_general` and `_knowledge_wiki` provide common entity sets for strategy and wiki content.
|
|
194
|
+
- **Partials can carry metadata**: Unlike plain JSON Schema, partials may include `$metadata` (hierarchy, aliases, relationships, rules). This makes them self-contained units that bundle both type definitions and behavioral metadata.
|
|
191
195
|
- **No-metadata partials**: If a partial has no `$metadata`, prefer `$schema: "http://json-schema.org/draft-07/schema#"` so it validates standalone as plain JSON Schema.
|
|
192
196
|
- **Loading priority**: Partial schemas are loaded from both the default schema directory and the directory of your specified target schema.
|
|
193
197
|
- **Transitive resolution**: `$ref` chains are resolved recursively across files/schemas (including nested `allOf` usage in partials).
|
|
194
198
|
- **Unique IDs**: To encourage clean namespacing, local partial schemas **must** have unique `$id`s that do not collide with the default schemas. If a collision is detected, validation will fail with an error.
|
|
195
199
|
|
|
196
|
-
Schema resolution order: space config `schema` > global config `schema
|
|
200
|
+
Schema resolution order: space config `schema` > global config `schema`. Schema resolution fails if none is configured.
|
|
197
201
|
|
|
198
202
|
**⚠️ Security Notice: Only use schemas and configuration files from trusted sources.**
|
|
199
203
|
|
|
@@ -322,7 +326,7 @@ Generates a Mermaid Entity Relationship Diagram from a schema:
|
|
|
322
326
|
|
|
323
327
|
Example:
|
|
324
328
|
```bash
|
|
325
|
-
sctx schemas show
|
|
329
|
+
sctx schemas show strategy_general --mermaid-erd
|
|
326
330
|
```
|
|
327
331
|
|
|
328
332
|
### Sync space to Miro
|
package/dist/commands/spaces.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basename, resolve } from 'node:path';
|
|
2
|
-
import { configPath, loadConfig
|
|
2
|
+
import { configPath, loadConfig } from '../config';
|
|
3
3
|
function renderConfigValue(v) {
|
|
4
4
|
if (typeof v === 'object' && v !== null)
|
|
5
5
|
return JSON.stringify(v);
|
|
@@ -12,7 +12,8 @@ export function listSpaces() {
|
|
|
12
12
|
for (const space of config.spaces) {
|
|
13
13
|
console.log(`${space.name}`);
|
|
14
14
|
console.log(` path: ${space.path}`);
|
|
15
|
-
|
|
15
|
+
const schema = space.schema ?? config.schema;
|
|
16
|
+
console.log(` schema: ${schema ? basename(schema) : '(none)'}`);
|
|
16
17
|
if (space.miroBoardId)
|
|
17
18
|
console.log(` miro: configured`);
|
|
18
19
|
const plugins = space.plugins ?? {};
|
|
@@ -97,7 +97,7 @@ export async function validate(context, options = {}) {
|
|
|
97
97
|
const { schema, schemaRefRegistry, schemaValidator } = context;
|
|
98
98
|
const metadata = schema.metadata;
|
|
99
99
|
const readResult = await readSpace(context);
|
|
100
|
-
const { nodes,
|
|
100
|
+
const { nodes, parseIssues } = readResult;
|
|
101
101
|
const result = {
|
|
102
102
|
validCount: 0,
|
|
103
103
|
nodeErrorCount: 0,
|
|
@@ -107,7 +107,7 @@ export async function validate(context, options = {}) {
|
|
|
107
107
|
ruleViolations: [],
|
|
108
108
|
hierarchyViolations: [],
|
|
109
109
|
orphans: [],
|
|
110
|
-
|
|
110
|
+
parseIssues,
|
|
111
111
|
};
|
|
112
112
|
for (const node of nodes) {
|
|
113
113
|
const valid = schemaValidator(node.schemaData);
|
|
@@ -180,7 +180,8 @@ export async function validate(context, options = {}) {
|
|
|
180
180
|
for (const v of result.hierarchyViolations) {
|
|
181
181
|
addError(v.file, `hierarchy:${v.description}`, 'hierarchy', v.description);
|
|
182
182
|
}
|
|
183
|
-
const
|
|
183
|
+
const parseErrorCount = result.parseIssues.filter((i) => i.severity === 'error').length;
|
|
184
|
+
const errorCount = Object.values(errorsByFile).reduce((sum, errs) => sum + Object.keys(errs).length, 0) + parseErrorCount;
|
|
184
185
|
console.log(JSON.stringify({
|
|
185
186
|
space: context.space.name,
|
|
186
187
|
valid: errorCount === 0,
|
|
@@ -188,7 +189,7 @@ export async function validate(context, options = {}) {
|
|
|
188
189
|
errorCount,
|
|
189
190
|
errors: errorsByFile,
|
|
190
191
|
orphanCount: result.orphans.length,
|
|
191
|
-
|
|
192
|
+
parseIssues: result.parseIssues,
|
|
192
193
|
}, null, 2));
|
|
193
194
|
return errorCount > 0 ? 1 : 0;
|
|
194
195
|
}
|
|
@@ -224,16 +225,19 @@ export async function validate(context, options = {}) {
|
|
|
224
225
|
console.log(fmt(' Rule violations', result.ruleViolations.length, true));
|
|
225
226
|
console.log(fmt(' Hierarchy violations', result.hierarchyViolations.length, true));
|
|
226
227
|
console.log(fmt(' Orphans (hierarchy nodes - no parent)', result.orphans.length, true, true));
|
|
227
|
-
|
|
228
|
+
const parseIssueErrorCount = result.parseIssues.filter((i) => i.severity === 'error').length;
|
|
229
|
+
console.log(fmt(' Excluded during parsing', result.parseIssues.length, true, parseIssueErrorCount === 0));
|
|
228
230
|
if (result.orphans.length > 0) {
|
|
229
231
|
console.log(`\nOrphans (hierarchy nodes - no parent):`);
|
|
230
232
|
for (const node of result.orphans)
|
|
231
233
|
console.log(` ${node.label}`);
|
|
232
234
|
}
|
|
233
|
-
if (result.
|
|
234
|
-
console.log(`\
|
|
235
|
-
for (const
|
|
236
|
-
|
|
235
|
+
if (result.parseIssues.length > 0) {
|
|
236
|
+
console.log(`\nExcluded during parsing:`);
|
|
237
|
+
for (const issue of result.parseIssues) {
|
|
238
|
+
const detail = issue.message ? ` - ${issue.message}` : '';
|
|
239
|
+
console.log(` ${issue.file}: ${issue.severity} - ${issue.type}${detail}`);
|
|
240
|
+
}
|
|
237
241
|
}
|
|
238
242
|
if (result.nodeErrors.length > 0) {
|
|
239
243
|
console.log(`\nSchema validation errors:`);
|
|
@@ -290,7 +294,8 @@ export async function validate(context, options = {}) {
|
|
|
290
294
|
result.refErrors.length > 0 ||
|
|
291
295
|
result.duplicateErrors.length > 0 ||
|
|
292
296
|
result.ruleViolations.length > 0 ||
|
|
293
|
-
result.hierarchyViolations.length > 0
|
|
297
|
+
result.hierarchyViolations.length > 0 ||
|
|
298
|
+
parseIssueErrorCount > 0) {
|
|
294
299
|
return 1;
|
|
295
300
|
}
|
|
296
301
|
return 0;
|
package/dist/config.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare function configPath(): string;
|
|
|
28
28
|
export declare function loadConfig(): Config;
|
|
29
29
|
/** Get the full space config entry by name. Throws if not found. */
|
|
30
30
|
export declare function getSpaceConfig(name: string, config: Config): SpaceConfig;
|
|
31
|
-
/** Resolve schema path: CLI arg > space-level config > global config
|
|
31
|
+
/** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
|
|
32
32
|
export declare function resolveSchema(config: Config, space?: SpaceConfig): string;
|
|
33
33
|
type StringFields<T> = {
|
|
34
34
|
[K in keyof T]: T[K] extends string | undefined ? K : never;
|
package/dist/config.js
CHANGED
|
@@ -170,9 +170,13 @@ export function getSpaceConfig(name, config) {
|
|
|
170
170
|
}
|
|
171
171
|
return space;
|
|
172
172
|
}
|
|
173
|
-
/** Resolve schema path: CLI arg > space-level config > global config
|
|
173
|
+
/** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
|
|
174
174
|
export function resolveSchema(config, space) {
|
|
175
|
-
|
|
175
|
+
const schema = space?.schema ?? config.schema;
|
|
176
|
+
if (!schema) {
|
|
177
|
+
throw new Error('No schema configured. Set "schema" in the space config or at the top level of the config file.');
|
|
178
|
+
}
|
|
179
|
+
return schema;
|
|
176
180
|
}
|
|
177
181
|
/** Update a string field on a space entry and persist config. */
|
|
178
182
|
export function updateSpaceField(spaceName, field, value) {
|
package/dist/plugin-api.d.ts
CHANGED
|
@@ -9,4 +9,4 @@ export type { AnySchemaObject, SchemaObject, ValidateFunction } from 'ajv';
|
|
|
9
9
|
export type { ParseHook, ParseResult, PluginContext, RenderFormat, RenderHook, RenderOptions, StructuredContextPlugin, TemplateSyncHook, TemplateSyncOptions, } from './plugins/util';
|
|
10
10
|
export type { SharedEmbeddingFields } from './schema/metadata-contract';
|
|
11
11
|
export type { SpaceGraph } from './space-graph';
|
|
12
|
-
export type { BaseNode, EdgeDefinition, HierarchyLevel, Relationship, SchemaMetadata, SchemaWithMetadata, SpaceContext, SpaceNode, UnresolvedRef, } from './types';
|
|
12
|
+
export type { BaseNode, EdgeDefinition, HierarchyLevel, ParseIssue, Relationship, SchemaMetadata, SchemaWithMetadata, SpaceContext, SpaceNode, UnresolvedRef, } from './types';
|
|
@@ -25,7 +25,13 @@ export function readSpaceOnAPage(context) {
|
|
|
25
25
|
pageType: 'space_on_a_page',
|
|
26
26
|
metadata,
|
|
27
27
|
});
|
|
28
|
-
|
|
28
|
+
const parseIssues = terminatedHeadings.map((heading) => ({
|
|
29
|
+
file: heading,
|
|
30
|
+
severity: 'warning',
|
|
31
|
+
type: 'terminated',
|
|
32
|
+
message: 'Ignored headings detected beyond end of hierarchy.',
|
|
33
|
+
}));
|
|
34
|
+
return { nodes, parseIssues, diagnostics: { kind: 'page', preambleNodeCount } };
|
|
29
35
|
}
|
|
30
36
|
export async function readSpaceDirectory(context, options) {
|
|
31
37
|
const { space, schema: { metadata }, } = context;
|
|
@@ -38,17 +44,33 @@ export async function readSpaceDirectory(context, options) {
|
|
|
38
44
|
const knownTypes = typeInferenceCfg?.mode !== 'off' ? extractSchemaTypeNames(context.schema, context.schemaRefRegistry) : undefined;
|
|
39
45
|
const files = await Array.fromAsync(new Glob('**/*.md').scan({ cwd: directory, followSymlinks: true }));
|
|
40
46
|
const nodes = [];
|
|
41
|
-
const
|
|
42
|
-
const nonSpace = [];
|
|
47
|
+
const parseIssues = [];
|
|
43
48
|
for (const file of files) {
|
|
44
49
|
const absoluteFilePath = resolve(directory, file);
|
|
45
50
|
if (absoluteTemplateDir && absoluteFilePath.startsWith(absoluteTemplateDir)) {
|
|
46
51
|
continue;
|
|
47
52
|
}
|
|
48
53
|
const content = readFileSync(join(directory, file), 'utf-8');
|
|
49
|
-
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = matter(content);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
// gray-matter caches parsed YAML and has a known bug where a caught exception
|
|
60
|
+
// corrupts its internal cache, causing subsequent parses to silently return {}.
|
|
61
|
+
// Clear the cache after any parse error to avoid stale state.
|
|
62
|
+
// See: https://github.com/jonschlinkert/gray-matter/issues/166
|
|
63
|
+
matter.clearCache();
|
|
64
|
+
parseIssues.push({
|
|
65
|
+
file,
|
|
66
|
+
severity: 'error',
|
|
67
|
+
type: 'parse',
|
|
68
|
+
message: err instanceof Error ? err.message : String(err),
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
50
72
|
if (!parsed.data || Object.keys(parsed.data).length === 0) {
|
|
51
|
-
|
|
73
|
+
parseIssues.push({ file, severity: 'warning', type: 'empty' });
|
|
52
74
|
continue;
|
|
53
75
|
}
|
|
54
76
|
const data = coerceDates(applyFieldMap(parsed.data, fieldMap));
|
|
@@ -56,7 +78,7 @@ export async function readSpaceDirectory(context, options) {
|
|
|
56
78
|
data.type = inferTypeFromPath(file, typeInferenceCfg, knownTypes, context.schema.metadata.typeAliases);
|
|
57
79
|
}
|
|
58
80
|
if (!data.type) {
|
|
59
|
-
|
|
81
|
+
parseIssues.push({ file, severity: 'warning', type: 'no-type' });
|
|
60
82
|
continue;
|
|
61
83
|
}
|
|
62
84
|
if (ON_A_PAGE_TYPES.includes(data.type) && !options?.includeOnAPageFiles) {
|
|
@@ -81,9 +103,14 @@ export async function readSpaceDirectory(context, options) {
|
|
|
81
103
|
});
|
|
82
104
|
nodes.push(...embedded);
|
|
83
105
|
for (const heading of terminatedHeadings) {
|
|
84
|
-
|
|
106
|
+
parseIssues.push({
|
|
107
|
+
file: `${file} > ${heading}`,
|
|
108
|
+
severity: 'warning',
|
|
109
|
+
type: 'terminated',
|
|
110
|
+
message: 'Ignored headings detected beyond end of hierarchy.',
|
|
111
|
+
});
|
|
85
112
|
}
|
|
86
113
|
}
|
|
87
114
|
}
|
|
88
|
-
return { nodes,
|
|
115
|
+
return { nodes, parseIssues, diagnostics: { kind: 'directory' } };
|
|
89
116
|
}
|
package/dist/plugins/util.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AnySchemaObject } from 'ajv';
|
|
2
2
|
import type { SpaceGraph } from '../space-graph';
|
|
3
|
-
import type { BaseNode, SpaceContext } from '../types';
|
|
3
|
+
import type { BaseNode, ParseIssue, SpaceContext } from '../types';
|
|
4
4
|
export declare const PLUGIN_PREFIX = "sctx-";
|
|
5
5
|
export declare const CONFIG_PLUGINS_DIR = "plugins";
|
|
6
6
|
/** Normalize a plugin name to its canonical prefixed form. */
|
|
@@ -11,8 +11,8 @@ export type PluginContext = SpaceContext & {
|
|
|
11
11
|
};
|
|
12
12
|
export type ParseResult = {
|
|
13
13
|
nodes: BaseNode[];
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/** Issues encountered while parsing files — excluded files with reason and severity. */
|
|
15
|
+
parseIssues: ParseIssue[];
|
|
16
16
|
/** Plugin diagnostics: keyed scalar or list values. */
|
|
17
17
|
diagnostics: Record<string, number | string | string[]>;
|
|
18
18
|
};
|
package/dist/read/read-space.js
CHANGED
package/dist/schema/schema.js
CHANGED
|
@@ -255,17 +255,14 @@ function areRulesEquivalent(left, right) {
|
|
|
255
255
|
}
|
|
256
256
|
function extractMetadata(schema, schemaRefRegistry) {
|
|
257
257
|
const metadataProviders = collectMetadataProviders(schema, schemaRefRegistry);
|
|
258
|
-
let hierarchyProvider;
|
|
259
258
|
let mergedHierarchy;
|
|
260
259
|
const mergedAliases = {};
|
|
261
260
|
const mergedRules = new Map();
|
|
262
261
|
const mergedRelationships = [];
|
|
263
262
|
for (const provider of metadataProviders) {
|
|
264
263
|
if (provider.metadata.hierarchy) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
hierarchyProvider = provider.schemaId;
|
|
264
|
+
// Later providers (including root schema) override earlier ones (e.g. partials).
|
|
265
|
+
// This allows partials to define a default hierarchy that composing schemas can override.
|
|
269
266
|
mergedHierarchy = provider.metadata.hierarchy;
|
|
270
267
|
}
|
|
271
268
|
if (provider.metadata.aliases) {
|
|
@@ -294,6 +291,14 @@ function extractMetadata(schema, schemaRefRegistry) {
|
|
|
294
291
|
}
|
|
295
292
|
}
|
|
296
293
|
}
|
|
294
|
+
// Collect valid type names from the schema's oneOf list for validation
|
|
295
|
+
const validTypeNames = extractSchemaTypeNames(schema, schemaRefRegistry);
|
|
296
|
+
// Validate and filter metadata references against valid type names
|
|
297
|
+
validateMetadataReferences({ mergedHierarchy, mergedAliases, mergedRules, mergedRelationships }, validTypeNames);
|
|
298
|
+
// Filter relationships to only include those where both parent and child types are valid
|
|
299
|
+
const filteredRelationships = mergedRelationships.filter((rel) => {
|
|
300
|
+
return validTypeNames.has(rel.parent) && validTypeNames.has(rel.type);
|
|
301
|
+
});
|
|
297
302
|
const levels = mergedHierarchy?.levels.map((entry) => {
|
|
298
303
|
if (typeof entry === 'string') {
|
|
299
304
|
return { type: entry, field: 'parent', fieldOn: 'child', multiple: false, selfRef: false };
|
|
@@ -321,8 +326,8 @@ function extractMetadata(schema, schemaRefRegistry) {
|
|
|
321
326
|
: undefined,
|
|
322
327
|
typeAliases: Object.keys(mergedAliases).length > 0 ? mergedAliases : undefined,
|
|
323
328
|
rules: mergedRules.size > 0 ? [...mergedRules.values()].map(({ rule }) => normalizeRule(rule)) : undefined,
|
|
324
|
-
relationships:
|
|
325
|
-
?
|
|
329
|
+
relationships: filteredRelationships.length > 0
|
|
330
|
+
? filteredRelationships.map((rel) => ({
|
|
326
331
|
...rel,
|
|
327
332
|
field: rel.field ?? 'parent',
|
|
328
333
|
fieldOn: rel.fieldOn === 'parent' ? 'parent' : 'child',
|
|
@@ -331,6 +336,38 @@ function extractMetadata(schema, schemaRefRegistry) {
|
|
|
331
336
|
: undefined,
|
|
332
337
|
};
|
|
333
338
|
}
|
|
339
|
+
function validateMetadataReferences(metadata, validTypes) {
|
|
340
|
+
// Skip validation if no types are defined (e.g., schemas without oneOf or placeholder schemas)
|
|
341
|
+
if (validTypes.size === 0) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const errors = [];
|
|
345
|
+
// Validate hierarchy level types
|
|
346
|
+
if (metadata.mergedHierarchy?.levels) {
|
|
347
|
+
for (const level of metadata.mergedHierarchy.levels) {
|
|
348
|
+
const typeName = typeof level === 'string' ? level : level.type;
|
|
349
|
+
if (!validTypes.has(typeName)) {
|
|
350
|
+
errors.push(`Hierarchy level "${typeName}" is not a valid type in the schema's oneOf list`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Validate alias targets (the values)
|
|
355
|
+
for (const [alias, target] of Object.entries(metadata.mergedAliases)) {
|
|
356
|
+
if (!validTypes.has(target)) {
|
|
357
|
+
errors.push(`Alias "${alias}" → "${target}" references type "${target}" which is not in the schema's oneOf list`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Note: relationships are filtered (not validated) - see extractMetadata
|
|
361
|
+
// Validate rule types
|
|
362
|
+
for (const { rule } of metadata.mergedRules.values()) {
|
|
363
|
+
if (rule.type && !validTypes.has(rule.type)) {
|
|
364
|
+
errors.push(`Rule "${rule.id}" has type "${rule.type}" which is not in the schema's oneOf list`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (errors.length > 0) {
|
|
368
|
+
throw new Error(`Schema metadata validation failed:\n - ${errors.join('\n - ')}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
334
371
|
export function extractEntityInfo(schema, schemaRefRegistry) {
|
|
335
372
|
if (!Array.isArray(schema.oneOf))
|
|
336
373
|
return [];
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/_structured_context_schema_meta.json",
|
|
3
|
+
"$id": "sctx://_knowledge_wiki",
|
|
4
|
+
// Shared entity definitions for knowledge wiki schemas.
|
|
5
|
+
// Provides aliases, relationships, rules, and entity type $defs.
|
|
6
|
+
// This partial has no hierarchy — wiki entities are flat with no required parent links.
|
|
7
|
+
"$metadata": {
|
|
8
|
+
"aliases": {
|
|
9
|
+
"source_summary": "source",
|
|
10
|
+
"study": "source",
|
|
11
|
+
"article": "source",
|
|
12
|
+
"paper": "source",
|
|
13
|
+
"research": "source",
|
|
14
|
+
"comparison": "synthesis",
|
|
15
|
+
"query": "synthesis"
|
|
16
|
+
},
|
|
17
|
+
"relationships": [
|
|
18
|
+
{
|
|
19
|
+
"parent": "source",
|
|
20
|
+
"type": "source",
|
|
21
|
+
"field": "sources",
|
|
22
|
+
"fieldOn": "child",
|
|
23
|
+
"multiple": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"parent": "source",
|
|
27
|
+
"type": "synthesis",
|
|
28
|
+
"field": "sources",
|
|
29
|
+
"fieldOn": "child",
|
|
30
|
+
"multiple": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"parent": "source",
|
|
34
|
+
"type": "entity",
|
|
35
|
+
"field": "sources",
|
|
36
|
+
"fieldOn": "child",
|
|
37
|
+
"multiple": true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"parent": "concept",
|
|
41
|
+
"type": "synthesis",
|
|
42
|
+
"field": "concepts",
|
|
43
|
+
"fieldOn": "child",
|
|
44
|
+
"multiple": true
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"rules": [
|
|
48
|
+
{
|
|
49
|
+
"id": "synthesis-references-sources",
|
|
50
|
+
"category": "coherence",
|
|
51
|
+
"description": "Syntheses should reference at least one contributing source in their sources array",
|
|
52
|
+
"type": "synthesis",
|
|
53
|
+
"check": "$exists(current.sources) and $count(current.sources) >= 1"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "concept-has-summary",
|
|
57
|
+
"category": "best-practice",
|
|
58
|
+
"description": "Concept pages should have a short summary for index discoverability",
|
|
59
|
+
"type": "concept",
|
|
60
|
+
"check": "$exists(current.summary) and $length(current.summary) > 0"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"$defs": {
|
|
65
|
+
"source": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"description": "A processed summary of a raw source (article, paper, book, video, thread, podcast, etc.). Captures key information from the source in structured form with provenance.",
|
|
68
|
+
"allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
|
|
69
|
+
"properties": {
|
|
70
|
+
"type": { "const": "source" },
|
|
71
|
+
"summary": {
|
|
72
|
+
"$ref": "sctx://_sctx_base#/$defs/summary"
|
|
73
|
+
},
|
|
74
|
+
"url": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "URL of the original source"
|
|
77
|
+
},
|
|
78
|
+
"local_copy": {
|
|
79
|
+
"$ref": "sctx://_sctx_base#/$defs/wikilink",
|
|
80
|
+
"description": "Wikilink to a local copy of the source (e.g. a saved PDF or clipped note)"
|
|
81
|
+
},
|
|
82
|
+
"author": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "Author or creator of the source"
|
|
85
|
+
},
|
|
86
|
+
"book": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"description": "Book title and author if the source is a book"
|
|
89
|
+
},
|
|
90
|
+
"published_date": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"format": "date",
|
|
93
|
+
"description": "Original publication date"
|
|
94
|
+
},
|
|
95
|
+
"source_type": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Kind of source material. Free-form — common values include article, paper, book, video, podcast, thread, substack, tweet, website. Not enum-constrained to allow evolving vocabulary."
|
|
98
|
+
},
|
|
99
|
+
"sources": {
|
|
100
|
+
"type": "array",
|
|
101
|
+
"items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
|
|
102
|
+
"description": "Wikilinks to other source pages that this one references or extends"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"required": ["type", "url"],
|
|
106
|
+
"additionalProperties": true,
|
|
107
|
+
"examples": [
|
|
108
|
+
{
|
|
109
|
+
"type": "source",
|
|
110
|
+
"author": "Joe Bloggs",
|
|
111
|
+
"url": "https://example.com/design-of-yesterday-things",
|
|
112
|
+
"source_type": "book",
|
|
113
|
+
"tags": ["design", "ux", "psychology"]
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
"concept": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"description": "A concept page — defines and frames a specific idea, mechanism, or domain term. May draw from multiple sources. The building blocks of the wiki's knowledge graph.",
|
|
120
|
+
"allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
|
|
121
|
+
"properties": {
|
|
122
|
+
"type": { "const": "concept" },
|
|
123
|
+
"summary": {
|
|
124
|
+
"$ref": "sctx://_sctx_base#/$defs/summary"
|
|
125
|
+
},
|
|
126
|
+
"sources": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
|
|
129
|
+
"description": "Wikilinks to source pages that contributed to this concept"
|
|
130
|
+
},
|
|
131
|
+
"related": {
|
|
132
|
+
"type": "array",
|
|
133
|
+
"items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
|
|
134
|
+
"description": "Wikilinks to related concept or synthesis pages"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"required": ["type"],
|
|
138
|
+
"additionalProperties": true,
|
|
139
|
+
"examples": [
|
|
140
|
+
{
|
|
141
|
+
"type": "concept",
|
|
142
|
+
"summary": "A property of an object that signals how it can be used, first described by Gibson and popularised in design by Norman",
|
|
143
|
+
"tags": ["design", "ux"]
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
"entity": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"description": "A concrete entity — a person, organisation, product, model, or other named thing. The building blocks of the wiki's knowledge graph alongside concepts. Entities are distinct from concepts: a concept frames an idea or mechanism; an entity is a specific, identifiable thing in the world.",
|
|
150
|
+
"allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
|
|
151
|
+
"properties": {
|
|
152
|
+
"type": { "const": "entity" },
|
|
153
|
+
"summary": {
|
|
154
|
+
"$ref": "sctx://_sctx_base#/$defs/summary"
|
|
155
|
+
},
|
|
156
|
+
"sources": {
|
|
157
|
+
"type": "array",
|
|
158
|
+
"items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
|
|
159
|
+
"description": "Wikilinks to source pages that contributed to this entity page"
|
|
160
|
+
},
|
|
161
|
+
"related": {
|
|
162
|
+
"type": "array",
|
|
163
|
+
"items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
|
|
164
|
+
"description": "Wikilinks to related entity, concept, or synthesis pages"
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"required": ["type"],
|
|
168
|
+
"additionalProperties": true,
|
|
169
|
+
"examples": [
|
|
170
|
+
{
|
|
171
|
+
"type": "entity",
|
|
172
|
+
"summary": "AI safety research lab founded by Dario Amodei and Daniela Amodei",
|
|
173
|
+
"tags": ["company", "ai", "safety"]
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
"synthesis": {
|
|
178
|
+
"type": "object",
|
|
179
|
+
"description": "A synthesis page — connects multiple concepts and sources into a coherent model, framework, or thesis. The highest-value pages in a knowledge wiki. Must reference contributing sources.",
|
|
180
|
+
"allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
|
|
181
|
+
"properties": {
|
|
182
|
+
"type": { "const": "synthesis" },
|
|
183
|
+
"summary": {
|
|
184
|
+
"$ref": "sctx://_sctx_base#/$defs/summary"
|
|
185
|
+
},
|
|
186
|
+
"sources": {
|
|
187
|
+
"type": "array",
|
|
188
|
+
"items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
|
|
189
|
+
"description": "Wikilinks to source pages this synthesis draws from"
|
|
190
|
+
},
|
|
191
|
+
"concepts": {
|
|
192
|
+
"type": "array",
|
|
193
|
+
"items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
|
|
194
|
+
"description": "Wikilinks to concept pages this synthesis connects"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"required": ["type"],
|
|
198
|
+
"additionalProperties": true,
|
|
199
|
+
"examples": [
|
|
200
|
+
{
|
|
201
|
+
"type": "synthesis",
|
|
202
|
+
"summary": "How Norman's affordance model and feedback principles combine to explain intuitive vs confusing interfaces",
|
|
203
|
+
"sources": ["[[The Design of Everyday Things - Don Norman]]"],
|
|
204
|
+
"concepts": ["[[Affordance]]", "[[Feedback loop]]"]
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
"note": {
|
|
209
|
+
"type": "object",
|
|
210
|
+
"description": "Personal notes, journal entries, appointments, experiential records. Your own data about your journey — not sourced from external material.",
|
|
211
|
+
"allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
|
|
212
|
+
"properties": {
|
|
213
|
+
"type": { "const": "note" },
|
|
214
|
+
"date": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"format": "date",
|
|
217
|
+
"description": "Date this entry was created or relates to"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
"required": ["type"],
|
|
221
|
+
"additionalProperties": true,
|
|
222
|
+
"examples": [
|
|
223
|
+
{
|
|
224
|
+
"type": "note",
|
|
225
|
+
"date": "2024-03-15",
|
|
226
|
+
"tags": ["reading-log"]
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
"index": {
|
|
231
|
+
"type": "object",
|
|
232
|
+
"description": "The wiki index — a catalog of all pages with links, summaries, and metadata. Updated by the agent on ingest. Used as the primary navigation entry point.",
|
|
233
|
+
"allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
|
|
234
|
+
"properties": {
|
|
235
|
+
"type": { "const": "index" }
|
|
236
|
+
},
|
|
237
|
+
"required": ["type"],
|
|
238
|
+
"additionalProperties": true,
|
|
239
|
+
"examples": [
|
|
240
|
+
{
|
|
241
|
+
"type": "index"
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|