structured-context 0.9.2 → 0.10.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 +8 -4
- package/dist/api.d.ts +15 -0
- package/dist/api.js +9 -0
- package/dist/commands/dump.js +1 -1
- package/dist/commands/validate-file.d.ts +5 -24
- package/dist/commands/validate-file.js +10 -104
- package/dist/commands/validate.d.ts +1 -12
- package/dist/commands/validate.js +6 -157
- package/dist/config.js +1 -1
- package/dist/index.js +2 -2
- package/dist/plugins/markdown/index.d.ts +42 -32
- package/dist/plugins/markdown/index.js +9 -8
- package/dist/plugins/markdown/parse-embedded.d.ts +1 -1
- package/dist/plugins/markdown/read-space.js +3 -3
- package/dist/plugins/markdown/template-sync.d.ts +1 -1
- package/dist/plugins/markdown/template-sync.js +2 -2
- package/dist/schema/schema.js +45 -8
- package/dist/schemas/_knowledge_wiki.json +246 -0
- package/dist/schemas/_strategy_general.json +262 -0
- package/dist/schemas/knowledge_wiki.json +7 -237
- package/dist/schemas/strategy_general.json +12 -254
- package/dist/types.d.ts +20 -2
- package/dist/validate.d.ts +50 -0
- package/dist/validate.js +291 -0
- package/docs/schemas.md +6 -3
- package/package.json +11 -5
- package/schemas/_knowledge_wiki.json +246 -0
- package/schemas/_strategy_general.json +262 -0
- package/schemas/knowledge_wiki.json +7 -237
- package/schemas/strategy_general.json +12 -254
- package/dist/plugin-api.d.ts +0 -12
- package/dist/plugin-api.js +0 -7
package/dist/validate.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
2
|
+
import { getSpaceConfigDir, resolveSchema } from './config';
|
|
3
|
+
import { readSpace } from './read/read-space';
|
|
4
|
+
import { extractEntityInfo, loadSchema } from './schema/schema';
|
|
5
|
+
import { validateGraph } from './schema/validate-graph';
|
|
6
|
+
import { validateRules } from './schema/validate-rules';
|
|
7
|
+
import { buildSpaceGraph } from './space-graph';
|
|
8
|
+
/**
|
|
9
|
+
* Format AJV errors for better readability.
|
|
10
|
+
* Groups related errors and extracts helpful context like allowed values.
|
|
11
|
+
*/
|
|
12
|
+
export function formatErrors(errors, schema, schemaRefRegistry, nodeData) {
|
|
13
|
+
const formatted = [];
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
// Group errors by instancePath
|
|
16
|
+
const byPath = new Map();
|
|
17
|
+
for (const err of errors) {
|
|
18
|
+
const path = err.instancePath || 'root';
|
|
19
|
+
if (!byPath.has(path)) {
|
|
20
|
+
byPath.set(path, []);
|
|
21
|
+
}
|
|
22
|
+
byPath.get(path).push(err);
|
|
23
|
+
}
|
|
24
|
+
for (const [path, pathErrors] of byPath) {
|
|
25
|
+
// Check if this is a oneOf failure at root - extract valid types from schema
|
|
26
|
+
const isRootOneOf = path === 'root' || path === '/type';
|
|
27
|
+
let hasOneOfContext = false;
|
|
28
|
+
if (isRootOneOf && pathErrors.length > 1) {
|
|
29
|
+
hasOneOfContext = Array.isArray(schema.oneOf);
|
|
30
|
+
if (hasOneOfContext) {
|
|
31
|
+
const entities = extractEntityInfo(schema, schemaRefRegistry);
|
|
32
|
+
const validTypes = entities.map((e) => e.type).sort();
|
|
33
|
+
if (validTypes.length > 0) {
|
|
34
|
+
const actualValue = nodeData.type;
|
|
35
|
+
// Only show type error if the actual type is NOT in the valid types list
|
|
36
|
+
if (actualValue !== undefined && !validTypes.includes(String(actualValue))) {
|
|
37
|
+
const message = `Invalid type "${actualValue}". Valid types are: ${validTypes.join(', ')}`;
|
|
38
|
+
const key = `type:${validTypes.join(',')}`;
|
|
39
|
+
if (!seen.has(key)) {
|
|
40
|
+
seen.add(key);
|
|
41
|
+
formatted.push({ message, dedupeKey: key });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Handle individual errors
|
|
48
|
+
for (const err of pathErrors) {
|
|
49
|
+
// Skip individual type const/enum errors when in oneOf context (we handle it above)
|
|
50
|
+
if (hasOneOfContext &&
|
|
51
|
+
(err.keyword === 'const' || err.keyword === 'enum') &&
|
|
52
|
+
(err.instancePath === '' || err.instancePath === '/type')) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const parts = err.instancePath.split('/').filter(Boolean);
|
|
56
|
+
const fieldName = parts.length > 0 ? parts[parts.length - 1] : 'root';
|
|
57
|
+
let message = err.message;
|
|
58
|
+
let key = `${err.instancePath}:${err.keyword}`;
|
|
59
|
+
// Enhance const errors
|
|
60
|
+
if (err.keyword === 'const' && err.params?.allowedValue !== undefined) {
|
|
61
|
+
const actual = err.data !== undefined ? `"${err.data}"` : 'missing value';
|
|
62
|
+
const expected = `"${err.params.allowedValue}"`;
|
|
63
|
+
message = `${fieldName}: expected ${expected}, got ${actual}`;
|
|
64
|
+
key = `${err.instancePath}:const:${err.params.allowedValue}`;
|
|
65
|
+
}
|
|
66
|
+
// Enhance enum errors
|
|
67
|
+
else if (err.keyword === 'enum' && err.params?.allowedValues && Array.isArray(err.params.allowedValues)) {
|
|
68
|
+
let actual = err.data !== undefined ? `"${err.data}"` : null;
|
|
69
|
+
// If err.data is undefined, try to get the value from nodeData using the field name
|
|
70
|
+
if (actual === null && fieldName !== 'root') {
|
|
71
|
+
const actualValue = nodeData[fieldName];
|
|
72
|
+
if (actualValue !== undefined) {
|
|
73
|
+
actual = `"${actualValue}"`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (actual === null) {
|
|
77
|
+
actual = 'missing value';
|
|
78
|
+
}
|
|
79
|
+
const allowed = err.params.allowedValues.map((v) => `"${v}"`).join(', ');
|
|
80
|
+
message = `${fieldName}: ${actual} is not valid. Allowed: ${allowed}`;
|
|
81
|
+
key = `${err.instancePath}:enum:${err.params.allowedValues.join(',')}`;
|
|
82
|
+
}
|
|
83
|
+
// Generic message with path
|
|
84
|
+
else {
|
|
85
|
+
message = `${fieldName}: ${err.message}`;
|
|
86
|
+
}
|
|
87
|
+
if (!seen.has(key)) {
|
|
88
|
+
seen.add(key);
|
|
89
|
+
formatted.push({ message, dedupeKey: key });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return formatted;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Run full validation on a space and return a structured result.
|
|
97
|
+
* Does not output to console or call process.exit.
|
|
98
|
+
*/
|
|
99
|
+
export async function validateSpace(context) {
|
|
100
|
+
const { schema, schemaRefRegistry, schemaValidator } = context;
|
|
101
|
+
const metadata = schema.metadata;
|
|
102
|
+
const readResult = await readSpace(context);
|
|
103
|
+
const { nodes, parseIssues } = readResult;
|
|
104
|
+
// Pre-extract valid types for early type validation
|
|
105
|
+
const validTypes = Array.isArray(schema.oneOf) ? extractEntityInfo(schema, schemaRefRegistry).map((e) => e.type) : [];
|
|
106
|
+
const result = {
|
|
107
|
+
validCount: 0,
|
|
108
|
+
nodeErrorCount: 0,
|
|
109
|
+
nodeErrors: [],
|
|
110
|
+
refErrors: [],
|
|
111
|
+
duplicateErrors: [],
|
|
112
|
+
ruleViolations: [],
|
|
113
|
+
hierarchyViolations: [],
|
|
114
|
+
orphans: [],
|
|
115
|
+
parseIssues,
|
|
116
|
+
};
|
|
117
|
+
for (const node of nodes) {
|
|
118
|
+
// Early type validation - check before full AJV validation to prevent cascading errors
|
|
119
|
+
const nodeType = node.schemaData.type;
|
|
120
|
+
if (nodeType !== undefined && validTypes.length > 0 && !validTypes.includes(nodeType)) {
|
|
121
|
+
result.nodeErrorCount++;
|
|
122
|
+
result.nodeErrors.push({
|
|
123
|
+
file: node.label,
|
|
124
|
+
errors: [
|
|
125
|
+
{
|
|
126
|
+
instancePath: '/type',
|
|
127
|
+
keyword: 'enum',
|
|
128
|
+
message: `Invalid type "${nodeType}". Valid types are: ${validTypes.sort().join(', ')}`,
|
|
129
|
+
params: { allowedValues: validTypes },
|
|
130
|
+
schemaPath: '#/oneOf',
|
|
131
|
+
data: nodeType,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
nodeData: node.schemaData,
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const valid = schemaValidator(node.schemaData);
|
|
139
|
+
if (valid) {
|
|
140
|
+
result.validCount++;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
result.nodeErrorCount++;
|
|
144
|
+
result.nodeErrors.push({
|
|
145
|
+
file: node.label,
|
|
146
|
+
errors: schemaValidator.errors || [],
|
|
147
|
+
nodeData: node.schemaData,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Detect duplicate node keys (titles)
|
|
152
|
+
const titleToFiles = new Map();
|
|
153
|
+
for (const node of nodes) {
|
|
154
|
+
const title = node.title;
|
|
155
|
+
if (!titleToFiles.has(title)) {
|
|
156
|
+
titleToFiles.set(title, []);
|
|
157
|
+
}
|
|
158
|
+
titleToFiles.get(title).push(node.label);
|
|
159
|
+
}
|
|
160
|
+
for (const [title, files] of titleToFiles) {
|
|
161
|
+
if (files.length > 1) {
|
|
162
|
+
result.duplicateErrors.push({ title, files });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Validate all hierarchy constraints (field references and structure)
|
|
166
|
+
const hierarchyValidation = validateGraph(nodes, metadata, readResult.unresolvedRefs);
|
|
167
|
+
result.refErrors.push(...hierarchyValidation.refErrors);
|
|
168
|
+
result.hierarchyViolations = [...hierarchyValidation.violations];
|
|
169
|
+
// Calculate orphan count (informational, not a validation error)
|
|
170
|
+
if (metadata.hierarchy) {
|
|
171
|
+
result.orphans = [...buildSpaceGraph(nodes, metadata.hierarchy.levels).orphans];
|
|
172
|
+
}
|
|
173
|
+
// Load and execute rules validation if schema defines rules
|
|
174
|
+
if (metadata.rules) {
|
|
175
|
+
result.ruleViolations = await validateRules(nodes, metadata.rules);
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
/** Find which space a file belongs to by checking directory containment. */
|
|
180
|
+
function resolveFileSpace(filePath, config) {
|
|
181
|
+
const absFile = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
|
|
182
|
+
for (const space of config.spaces) {
|
|
183
|
+
const absSpace = isAbsolute(space.path) ? space.path : resolve(process.cwd(), space.path);
|
|
184
|
+
// Trailing slash prevents prefix-match false positives (e.g. /foo matching /foobar/)
|
|
185
|
+
const spaceDir = absSpace.endsWith('/') ? absSpace : `${absSpace}/`;
|
|
186
|
+
if (absFile.startsWith(spaceDir) || absFile === absSpace) {
|
|
187
|
+
return { space, label: relative(absSpace, absFile) };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
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
|
+
/**
|
|
199
|
+
* Validate a single file within its space.
|
|
200
|
+
*
|
|
201
|
+
* Reads the full space (required for graph correctness) but filters all reported
|
|
202
|
+
* errors to only those attributable to the target file.
|
|
203
|
+
*
|
|
204
|
+
* Returns a result object — does not output to console or call process.exit.
|
|
205
|
+
* If the file is not in any configured space, returns a result with `inSpace: false`.
|
|
206
|
+
*/
|
|
207
|
+
export async function validateFile(filePath, config) {
|
|
208
|
+
const resolved = resolveFileSpace(filePath, config);
|
|
209
|
+
if (!resolved) {
|
|
210
|
+
return {
|
|
211
|
+
file: filePath,
|
|
212
|
+
inSpace: false,
|
|
213
|
+
message: 'File does not belong to any configured space.',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const { space, label } = resolved;
|
|
217
|
+
const context = buildContextForSpace(space, config);
|
|
218
|
+
const { schema, schemaRefRegistry, schemaValidator } = context;
|
|
219
|
+
const metadata = schema.metadata;
|
|
220
|
+
const readResult = await readSpace(context);
|
|
221
|
+
const { nodes } = readResult;
|
|
222
|
+
// Pre-extract valid types for early type validation
|
|
223
|
+
const validTypes = Array.isArray(schema.oneOf) ? extractEntityInfo(schema, schemaRefRegistry).map((e) => e.type) : [];
|
|
224
|
+
const errors = {};
|
|
225
|
+
// Schema validation errors for this node
|
|
226
|
+
for (const node of nodes) {
|
|
227
|
+
if (node.label !== label)
|
|
228
|
+
continue;
|
|
229
|
+
// Early type validation - check before full AJV validation to prevent cascading errors
|
|
230
|
+
const nodeType = node.schemaData.type;
|
|
231
|
+
if (nodeType !== undefined && validTypes.length > 0 && !validTypes.includes(nodeType)) {
|
|
232
|
+
errors[`schema:type:${validTypes.join(',')}`] = {
|
|
233
|
+
kind: 'schema',
|
|
234
|
+
message: `Invalid type "${nodeType}". Valid types are: ${validTypes.sort().join(', ')}`,
|
|
235
|
+
};
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const valid = schemaValidator(node.schemaData);
|
|
239
|
+
if (!valid) {
|
|
240
|
+
const formatted = formatErrors(schemaValidator.errors ?? [], schema, schemaRefRegistry, node.schemaData);
|
|
241
|
+
for (const { message, dedupeKey } of formatted) {
|
|
242
|
+
errors[`schema:${dedupeKey}`] = { kind: 'schema', message };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Duplicate key errors — include if this file is one of the duplicates
|
|
247
|
+
const titleToFiles = new Map();
|
|
248
|
+
for (const node of nodes) {
|
|
249
|
+
if (!titleToFiles.has(node.title))
|
|
250
|
+
titleToFiles.set(node.title, []);
|
|
251
|
+
titleToFiles.get(node.title).push(node.label);
|
|
252
|
+
}
|
|
253
|
+
for (const [title, files] of titleToFiles) {
|
|
254
|
+
if (files.length > 1 && files.includes(label)) {
|
|
255
|
+
const others = files.filter((f) => f !== label);
|
|
256
|
+
errors[`duplicate:${title}`] = {
|
|
257
|
+
kind: 'duplicate',
|
|
258
|
+
message: `Duplicate title "${title}" also exists in: ${others.join(', ')}`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Broken links and hierarchy violations from this file
|
|
263
|
+
const hierarchyValidation = validateGraph(nodes, metadata, readResult.unresolvedRefs);
|
|
264
|
+
for (const { file, parent, error } of hierarchyValidation.refErrors) {
|
|
265
|
+
if (file === label) {
|
|
266
|
+
errors[`broken-link:${parent}`] = { kind: 'broken-link', message: `${parent} → ${error}` };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (const v of hierarchyValidation.violations) {
|
|
270
|
+
if (v.file === label) {
|
|
271
|
+
errors[`hierarchy:${v.description}`] = { kind: 'hierarchy', message: v.description };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Rule violations for this node
|
|
275
|
+
if (metadata.rules) {
|
|
276
|
+
const ruleViolations = await validateRules(nodes, metadata.rules);
|
|
277
|
+
for (const v of ruleViolations) {
|
|
278
|
+
if (v.file === label) {
|
|
279
|
+
errors[`rule:${v.ruleId}`] = { kind: 'rule', message: `[${v.ruleId}] ${v.description}` };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
file: isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath),
|
|
285
|
+
label,
|
|
286
|
+
space: space.name,
|
|
287
|
+
errors,
|
|
288
|
+
errorCount: Object.keys(errors).length,
|
|
289
|
+
inSpace: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
package/docs/schemas.md
CHANGED
|
@@ -258,8 +258,9 @@ Metadata is composed across the `$ref` graph with deterministic behavior:
|
|
|
258
258
|
2. Apply root schema metadata last.
|
|
259
259
|
|
|
260
260
|
Merge rules:
|
|
261
|
-
- `hierarchy`:
|
|
262
|
-
- `aliases`: shallow merged; later
|
|
261
|
+
- `hierarchy`: may be defined in partials; **last one wins**. This allows partials to define a default hierarchy that composing schemas can override.
|
|
262
|
+
- `aliases`: shallow merged; later file wins per key.
|
|
263
|
+
- `relationships`: collected from all files; order preserved.
|
|
263
264
|
- `rules`: merged by `id`.
|
|
264
265
|
- Duplicate rule `id` with different payload errors by default.
|
|
265
266
|
- A later rule may replace an earlier one only with `"override": true`.
|
|
@@ -309,6 +310,8 @@ Imported rules are normalized into one executable flat list before validation.
|
|
|
309
310
|
- Local partial `$id` values must not collide with bundled IDs.
|
|
310
311
|
- `$ref` resolution is transitive across files.
|
|
311
312
|
- Partials with no `$metadata` should prefer `$schema: "http://json-schema.org/draft-07/schema#"` so they validate standalone as plain JSON Schema fragments.
|
|
313
|
+
- **Bundled partials as entity libraries**: `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`, and `_ost_strict.json` provide reusable entity definitions and metadata. Composing schemas can reference these via `$ref` rather than redefining common entity types.
|
|
314
|
+
- **Partials can carry metadata**: Partials may include `$metadata` (hierarchy, aliases, relationships, rules). This makes them self-contained units that bundle both type definitions and behavioral metadata.
|
|
312
315
|
|
|
313
316
|
## Editor expectations
|
|
314
317
|
|
|
@@ -328,7 +331,7 @@ For schemas migrating from older metadata structure:
|
|
|
328
331
|
3. Move `allowSkipLevels` under `hierarchy`.
|
|
329
332
|
4. Convert grouped rule containers to flat `rules[]` with per-rule `category`.
|
|
330
333
|
5. If duplicate rule IDs are intentional, mark later rules with `override: true`.
|
|
331
|
-
6. Re-run `
|
|
334
|
+
6. Re-run `sctx schemas show --space <name>` and `validate` to confirm merged metadata/rules.
|
|
332
335
|
|
|
333
336
|
## JSON5 support
|
|
334
337
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "structured-context",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Keep your content clean and your thinking clear with assistive tools for structured knowledge",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -18,9 +18,13 @@
|
|
|
18
18
|
],
|
|
19
19
|
"exports": {
|
|
20
20
|
".": "./dist/index.js",
|
|
21
|
-
"./
|
|
22
|
-
"types": "./dist/
|
|
23
|
-
"default": "./dist/
|
|
21
|
+
"./api": {
|
|
22
|
+
"types": "./dist/api.d.ts",
|
|
23
|
+
"default": "./dist/api.js"
|
|
24
|
+
},
|
|
25
|
+
"./types": {
|
|
26
|
+
"types": "./dist/types.d.ts",
|
|
27
|
+
"default": "./dist/types.js"
|
|
24
28
|
}
|
|
25
29
|
},
|
|
26
30
|
"scripts": {
|
|
@@ -57,8 +61,10 @@
|
|
|
57
61
|
"ajv": "^8.18.0",
|
|
58
62
|
"chokidar": "^5.0.0",
|
|
59
63
|
"commander": "^14.0.3",
|
|
64
|
+
"fast-glob": "^3.3.3",
|
|
60
65
|
"gray-matter": "^4.0.3",
|
|
61
66
|
"js-yaml": "^4.1.1",
|
|
67
|
+
"json5": "^2.2.3",
|
|
62
68
|
"jsonata": "^2.1.0",
|
|
63
69
|
"mdast-util-to-string": "^4.0.0",
|
|
64
70
|
"remark-gfm": "^4.0.1",
|
|
@@ -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
|
+
}
|