@swarmvaultai/engine 0.1.3 → 0.1.4

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 CHANGED
@@ -22,12 +22,14 @@ import {
22
22
  compileVault,
23
23
  createMcpServer,
24
24
  defaultVaultConfig,
25
+ defaultVaultSchema,
25
26
  importInbox,
26
27
  ingestInput,
27
28
  initVault,
28
29
  installAgent,
29
30
  lintVault,
30
31
  loadVaultConfig,
32
+ loadVaultSchema,
31
33
  queryVault,
32
34
  searchVault,
33
35
  startGraphServer,
@@ -41,11 +43,13 @@ The engine also exports the main runtime types for providers, graph artifacts, p
41
43
  ## Example
42
44
 
43
45
  ```ts
44
- import { compileVault, importInbox, initVault, queryVault, watchVault } from "@swarmvaultai/engine";
46
+ import { compileVault, importInbox, initVault, loadVaultSchema, queryVault, watchVault } from "@swarmvaultai/engine";
45
47
 
46
48
  const rootDir = process.cwd();
47
49
 
48
50
  await initVault(rootDir);
51
+ const schema = await loadVaultSchema(rootDir);
52
+ console.log(schema.path);
49
53
  await importInbox(rootDir);
50
54
  await compileVault(rootDir);
51
55
 
@@ -55,6 +59,18 @@ console.log(result.answer);
55
59
  const watcher = await watchVault(rootDir, { lint: true });
56
60
  ```
57
61
 
62
+ ## Schema Layer
63
+
64
+ Each workspace carries a root markdown file named `swarmvault.schema.md`.
65
+
66
+ The engine treats that file as vault-specific operating guidance for compile and query work. In `v0.1.4`:
67
+
68
+ - `initVault()` creates the default schema file
69
+ - `loadVaultSchema()` resolves the canonical file and legacy `schema.md` fallback
70
+ - compile and query prompts include the schema content
71
+ - generated pages store `schema_hash`
72
+ - `lintVault()` marks generated pages stale when the schema changes
73
+
58
74
  ## Provider Model
59
75
 
60
76
  The engine supports:
@@ -88,8 +104,8 @@ This matters because many "OpenAI-compatible" backends only implement part of th
88
104
 
89
105
  ### Compile + Query
90
106
 
91
- - `compileVault(rootDir)` writes wiki pages, graph data, and search state
92
- - `queryVault(rootDir, question, save)` answers against the compiled vault
107
+ - `compileVault(rootDir)` writes wiki pages, graph data, and search state using the vault schema as guidance
108
+ - `queryVault(rootDir, question, save)` answers against the compiled vault using the same schema layer
93
109
  - `searchVault(rootDir, query, limit)` searches compiled pages directly
94
110
 
95
111
  ### Automation
@@ -102,12 +118,13 @@ This matters because many "OpenAI-compatible" backends only implement part of th
102
118
  - `createMcpServer(rootDir)` creates an MCP server instance
103
119
  - `startMcpServer(rootDir)` runs the MCP server over stdio
104
120
 
105
- The MCP surface includes tools for workspace info, page search, page reads, source listing, querying, ingestion, compile, and lint, along with resources for config, graph, manifests, and page content.
121
+ The MCP surface includes tools for workspace info, page search, page reads, source listing, querying, ingestion, compile, and lint, along with resources for config, graph, manifests, schema, and page content.
106
122
 
107
123
  ## Artifacts
108
124
 
109
125
  Running the engine produces a local workspace with these main areas:
110
126
 
127
+ - `swarmvault.schema.md`: vault-specific compile and query instructions
111
128
  - `inbox/`: capture staging area for markdown bundles and imported files
112
129
  - `raw/sources/`: immutable source copies
113
130
  - `raw/assets/`: copied attachments referenced by ingested markdown bundles
package/dist/index.d.ts CHANGED
@@ -84,6 +84,7 @@ interface VaultConfig {
84
84
  }
85
85
  interface ResolvedPaths {
86
86
  rootDir: string;
87
+ schemaPath: string;
87
88
  rawDir: string;
88
89
  rawSourcesDir: string;
89
90
  rawAssetsDir: string;
@@ -137,6 +138,7 @@ interface SourceClaim {
137
138
  interface SourceAnalysis {
138
139
  sourceId: string;
139
140
  sourceHash: string;
141
+ schemaHash: string;
140
142
  title: string;
141
143
  summary: string;
142
144
  concepts: AnalyzedTerm[];
@@ -173,6 +175,7 @@ interface GraphPage {
173
175
  freshness: Freshness;
174
176
  confidence: number;
175
177
  backlinks: string[];
178
+ schemaHash: string;
176
179
  sourceHashes: Record<string, string>;
177
180
  }
178
181
  interface GraphArtifact {
@@ -240,7 +243,8 @@ interface WatchController {
240
243
  }
241
244
 
242
245
  declare function defaultVaultConfig(): VaultConfig;
243
- declare function resolvePaths(rootDir: string, config?: VaultConfig, configPath?: string): ResolvedPaths;
246
+ declare function defaultVaultSchema(): string;
247
+ declare function resolvePaths(rootDir: string, config?: VaultConfig, configPath?: string, schemaPath?: string): ResolvedPaths;
244
248
  declare function loadVaultConfig(rootDir: string): Promise<{
245
249
  config: VaultConfig;
246
250
  paths: ResolvedPaths;
@@ -269,6 +273,7 @@ declare function readPage(rootDir: string, relativePath: string): Promise<{
269
273
  declare function getWorkspaceInfo(rootDir: string): Promise<{
270
274
  rootDir: string;
271
275
  configPath: string;
276
+ schemaPath: string;
272
277
  rawDir: string;
273
278
  wikiDir: string;
274
279
  stateDir: string;
@@ -286,6 +291,14 @@ declare function bootstrapDemo(rootDir: string, input?: string): Promise<{
286
291
  declare function installAgent(rootDir: string, agent: "codex" | "claude" | "cursor"): Promise<string>;
287
292
  declare function installConfiguredAgents(rootDir: string): Promise<string[]>;
288
293
 
294
+ interface VaultSchema {
295
+ path: string;
296
+ content: string;
297
+ hash: string;
298
+ isLegacyPath: boolean;
299
+ }
300
+ declare function loadVaultSchema(rootDir: string): Promise<VaultSchema>;
301
+
289
302
  declare function startGraphServer(rootDir: string, port?: number): Promise<{
290
303
  port: number;
291
304
  close: () => Promise<void>;
@@ -302,4 +315,4 @@ declare function createProvider(id: string, config: ProviderConfig, rootDir: str
302
315
  declare function getProviderForTask(rootDir: string, task: keyof Awaited<ReturnType<typeof loadVaultConfig>>["config"]["tasks"]): Promise<ProviderAdapter>;
303
316
  declare function assertProviderCapability(provider: ProviderAdapter, capability: ProviderCapability): void;
304
317
 
305
- export { type AnalyzedTerm, type ClaimStatus, type CompileResult, type Freshness, type GenerationAttachment, type GenerationRequest, type GenerationResponse, type GraphArtifact, type GraphEdge, type GraphNode, type GraphPage, type InboxImportResult, type InboxImportSkip, type LintFinding, type PageKind, type Polarity, type ProviderAdapter, type ProviderCapability, type ProviderConfig, type ProviderType, type QueryResult, type ResolvedPaths, type SearchResult, type SourceAnalysis, type SourceAttachment, type SourceClaim, type SourceManifest, type VaultConfig, type WatchController, type WatchOptions, type WatchRunRecord, assertProviderCapability, bootstrapDemo, compileVault, createMcpServer, createProvider, defaultVaultConfig, getProviderForTask, getWorkspaceInfo, importInbox, ingestInput, initVault, initWorkspace, installAgent, installConfiguredAgents, lintVault, listManifests, listPages, loadVaultConfig, providerCapabilitySchema, providerTypeSchema, queryVault, readExtractedText, readPage, resolvePaths, searchVault, startGraphServer, startMcpServer, watchVault };
318
+ export { type AnalyzedTerm, type ClaimStatus, type CompileResult, type Freshness, type GenerationAttachment, type GenerationRequest, type GenerationResponse, type GraphArtifact, type GraphEdge, type GraphNode, type GraphPage, type InboxImportResult, type InboxImportSkip, type LintFinding, type PageKind, type Polarity, type ProviderAdapter, type ProviderCapability, type ProviderConfig, type ProviderType, type QueryResult, type ResolvedPaths, type SearchResult, type SourceAnalysis, type SourceAttachment, type SourceClaim, type SourceManifest, type VaultConfig, type WatchController, type WatchOptions, type WatchRunRecord, assertProviderCapability, bootstrapDemo, compileVault, createMcpServer, createProvider, defaultVaultConfig, defaultVaultSchema, getProviderForTask, getWorkspaceInfo, importInbox, ingestInput, initVault, initWorkspace, installAgent, installConfiguredAgents, lintVault, listManifests, listPages, loadVaultConfig, loadVaultSchema, providerCapabilitySchema, providerTypeSchema, queryVault, readExtractedText, readPage, resolvePaths, searchVault, startGraphServer, startMcpServer, watchVault };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/config.ts
2
2
  import path2 from "path";
3
+ import fs2 from "fs/promises";
3
4
  import { fileURLToPath } from "url";
4
5
  import { z as z2 } from "zod";
5
6
 
@@ -134,6 +135,8 @@ var providerTypeSchema = z.enum([
134
135
  // src/config.ts
135
136
  var PRIMARY_CONFIG_FILENAME = "swarmvault.config.json";
136
137
  var LEGACY_CONFIG_FILENAME = "vault.config.json";
138
+ var PRIMARY_SCHEMA_FILENAME = "swarmvault.schema.md";
139
+ var LEGACY_SCHEMA_FILENAME = "schema.md";
137
140
  var moduleDir = path2.dirname(fileURLToPath(import.meta.url));
138
141
  var providerConfigSchema = z2.object({
139
142
  type: providerTypeSchema,
@@ -193,6 +196,52 @@ function defaultVaultConfig() {
193
196
  agents: ["codex", "claude", "cursor"]
194
197
  };
195
198
  }
199
+ function defaultVaultSchema() {
200
+ return [
201
+ "# SwarmVault Schema",
202
+ "",
203
+ "Edit this file to teach SwarmVault how this vault should be organized and maintained.",
204
+ "",
205
+ "## Vault Purpose",
206
+ "",
207
+ "- Describe the domain this vault covers.",
208
+ "- Note the intended audience and the kinds of questions the vault should answer well.",
209
+ "",
210
+ "## Naming Conventions",
211
+ "",
212
+ "- Prefer stable, descriptive page titles.",
213
+ "- Keep concept and entity names specific to the domain.",
214
+ "",
215
+ "## Page Structure Rules",
216
+ "",
217
+ "- Source pages should stay grounded in the original material.",
218
+ "- Concept and entity pages should aggregate source-backed claims instead of inventing new ones.",
219
+ "- Preserve contradictions instead of smoothing them away.",
220
+ "",
221
+ "## Categories",
222
+ "",
223
+ "- List domain-specific concept categories here.",
224
+ "- List important entity types here.",
225
+ "",
226
+ "## Relationship Types",
227
+ "",
228
+ "- Mentions",
229
+ "- Supports",
230
+ "- Contradicts",
231
+ "- Depends on",
232
+ "",
233
+ "## Grounding Rules",
234
+ "",
235
+ "- Prefer raw sources over summaries.",
236
+ "- Cite source ids whenever claims are stated.",
237
+ "- Do not treat the wiki as a source of truth when the raw material disagrees.",
238
+ "",
239
+ "## Exclusions",
240
+ "",
241
+ "- List topics, claims, or page types the compiler should avoid generating.",
242
+ ""
243
+ ].join("\n");
244
+ }
196
245
  async function findConfigPath(rootDir) {
197
246
  const primaryPath = path2.join(rootDir, PRIMARY_CONFIG_FILENAME);
198
247
  if (await fileExists(primaryPath)) {
@@ -204,7 +253,18 @@ async function findConfigPath(rootDir) {
204
253
  }
205
254
  return primaryPath;
206
255
  }
207
- function resolvePaths(rootDir, config, configPath = path2.join(rootDir, PRIMARY_CONFIG_FILENAME)) {
256
+ async function findSchemaPath(rootDir) {
257
+ const primaryPath = path2.join(rootDir, PRIMARY_SCHEMA_FILENAME);
258
+ if (await fileExists(primaryPath)) {
259
+ return primaryPath;
260
+ }
261
+ const legacyPath = path2.join(rootDir, LEGACY_SCHEMA_FILENAME);
262
+ if (await fileExists(legacyPath)) {
263
+ return legacyPath;
264
+ }
265
+ return primaryPath;
266
+ }
267
+ function resolvePaths(rootDir, config, configPath = path2.join(rootDir, PRIMARY_CONFIG_FILENAME), schemaPath = path2.join(rootDir, PRIMARY_SCHEMA_FILENAME)) {
208
268
  const effective = config ?? defaultVaultConfig();
209
269
  const rawDir = path2.resolve(rootDir, effective.workspace.rawDir);
210
270
  const rawSourcesDir = path2.join(rawDir, "sources");
@@ -215,6 +275,7 @@ function resolvePaths(rootDir, config, configPath = path2.join(rootDir, PRIMARY_
215
275
  const inboxDir = path2.resolve(rootDir, effective.workspace.inboxDir);
216
276
  return {
217
277
  rootDir,
278
+ schemaPath,
218
279
  rawDir,
219
280
  rawSourcesDir,
220
281
  rawAssetsDir,
@@ -235,17 +296,21 @@ function resolvePaths(rootDir, config, configPath = path2.join(rootDir, PRIMARY_
235
296
  }
236
297
  async function loadVaultConfig(rootDir) {
237
298
  const configPath = await findConfigPath(rootDir);
299
+ const schemaPath = await findSchemaPath(rootDir);
238
300
  const raw = await readJsonFile(configPath);
239
301
  const parsed = vaultConfigSchema.parse(raw ?? defaultVaultConfig());
240
302
  return {
241
303
  config: parsed,
242
- paths: resolvePaths(rootDir, parsed, configPath)
304
+ paths: resolvePaths(rootDir, parsed, configPath, schemaPath)
243
305
  };
244
306
  }
245
307
  async function initWorkspace(rootDir) {
246
308
  const configPath = await findConfigPath(rootDir);
309
+ const schemaPath = await findSchemaPath(rootDir);
247
310
  const config = await fileExists(configPath) ? (await loadVaultConfig(rootDir)).config : defaultVaultConfig();
248
- const paths = resolvePaths(rootDir, config, configPath);
311
+ const paths = resolvePaths(rootDir, config, configPath, schemaPath);
312
+ const primarySchemaPath = path2.join(rootDir, PRIMARY_SCHEMA_FILENAME);
313
+ const legacySchemaPath = path2.join(rootDir, LEGACY_SCHEMA_FILENAME);
249
314
  await Promise.all([
250
315
  ensureDir(paths.rawDir),
251
316
  ensureDir(paths.wikiDir),
@@ -261,11 +326,15 @@ async function initWorkspace(rootDir) {
261
326
  if (!await fileExists(configPath)) {
262
327
  await writeJsonFile(configPath, config);
263
328
  }
329
+ if (!await fileExists(primarySchemaPath) && !await fileExists(legacySchemaPath)) {
330
+ await ensureDir(path2.dirname(primarySchemaPath));
331
+ await fs2.writeFile(primarySchemaPath, defaultVaultSchema(), "utf8");
332
+ }
264
333
  return { config, paths };
265
334
  }
266
335
 
267
336
  // src/ingest.ts
268
- import fs3 from "fs/promises";
337
+ import fs4 from "fs/promises";
269
338
  import path4 from "path";
270
339
  import { JSDOM } from "jsdom";
271
340
  import TurndownService from "turndown";
@@ -273,7 +342,7 @@ import { Readability } from "@mozilla/readability";
273
342
  import mime from "mime-types";
274
343
 
275
344
  // src/logs.ts
276
- import fs2 from "fs/promises";
345
+ import fs3 from "fs/promises";
277
346
  import path3 from "path";
278
347
  async function appendLogEntry(rootDir, action, title, lines = []) {
279
348
  const { paths } = await initWorkspace(rootDir);
@@ -281,8 +350,8 @@ async function appendLogEntry(rootDir, action, title, lines = []) {
281
350
  const logPath = path3.join(paths.wikiDir, "log.md");
282
351
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
283
352
  const entry = [`## [${timestamp}] ${action} | ${title}`, ...lines.map((line) => `- ${line}`), ""].join("\n");
284
- const existing = await fileExists(logPath) ? await fs2.readFile(logPath, "utf8") : "# Log\n\n";
285
- await fs2.writeFile(logPath, `${existing}${entry}
353
+ const existing = await fileExists(logPath) ? await fs3.readFile(logPath, "utf8") : "# Log\n\n";
354
+ await fs3.writeFile(logPath, `${existing}${entry}
286
355
  `, "utf8");
287
356
  }
288
357
  async function appendWatchRun(rootDir, run) {
@@ -372,7 +441,7 @@ async function convertHtmlToMarkdown(html, url) {
372
441
  };
373
442
  }
374
443
  async function readManifestByHash(manifestsDir, contentHash) {
375
- const entries = await fs3.readdir(manifestsDir, { withFileTypes: true }).catch(() => []);
444
+ const entries = await fs4.readdir(manifestsDir, { withFileTypes: true }).catch(() => []);
376
445
  for (const entry of entries) {
377
446
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
378
447
  continue;
@@ -398,17 +467,17 @@ async function persistPreparedInput(rootDir, prepared, paths) {
398
467
  const now = (/* @__PURE__ */ new Date()).toISOString();
399
468
  const sourceId = `${slugify(prepared.title)}-${contentHash.slice(0, 8)}`;
400
469
  const storedPath = path4.join(paths.rawSourcesDir, `${sourceId}${prepared.storedExtension}`);
401
- await fs3.writeFile(storedPath, prepared.payloadBytes);
470
+ await fs4.writeFile(storedPath, prepared.payloadBytes);
402
471
  let extractedTextPath;
403
472
  if (prepared.extractedText) {
404
473
  extractedTextPath = path4.join(paths.extractsDir, `${sourceId}.md`);
405
- await fs3.writeFile(extractedTextPath, prepared.extractedText, "utf8");
474
+ await fs4.writeFile(extractedTextPath, prepared.extractedText, "utf8");
406
475
  }
407
476
  const manifestAttachments = [];
408
477
  for (const attachment of attachments) {
409
478
  const absoluteAttachmentPath = path4.join(paths.rawAssetsDir, sourceId, attachment.relativePath);
410
479
  await ensureDir(path4.dirname(absoluteAttachmentPath));
411
- await fs3.writeFile(absoluteAttachmentPath, attachment.bytes);
480
+ await fs4.writeFile(absoluteAttachmentPath, attachment.bytes);
412
481
  manifestAttachments.push({
413
482
  path: toPosix(path4.relative(rootDir, absoluteAttachmentPath)),
414
483
  mimeType: attachment.mimeType,
@@ -439,7 +508,7 @@ async function persistPreparedInput(rootDir, prepared, paths) {
439
508
  return { manifest, isNew: true };
440
509
  }
441
510
  async function prepareFileInput(rootDir, absoluteInput) {
442
- const payloadBytes = await fs3.readFile(absoluteInput);
511
+ const payloadBytes = await fs4.readFile(absoluteInput);
443
512
  const mimeType = guessMimeType(absoluteInput);
444
513
  const sourceKind = inferKind(mimeType, absoluteInput);
445
514
  const storedExtension = path4.extname(absoluteInput) || `.${mime.extension(mimeType) || "bin"}`;
@@ -509,7 +578,7 @@ async function collectInboxAttachmentRefs(inputDir, files) {
509
578
  if (sourceKind !== "markdown") {
510
579
  continue;
511
580
  }
512
- const content = await fs3.readFile(absolutePath, "utf8");
581
+ const content = await fs4.readFile(absolutePath, "utf8");
513
582
  const refs = extractMarkdownReferences(content);
514
583
  if (!refs.length) {
515
584
  continue;
@@ -550,12 +619,12 @@ function rewriteMarkdownReferences(content, replacements) {
550
619
  });
551
620
  }
552
621
  async function prepareInboxMarkdownInput(absolutePath, attachmentRefs) {
553
- const originalBytes = await fs3.readFile(absolutePath);
622
+ const originalBytes = await fs4.readFile(absolutePath);
554
623
  const originalText = originalBytes.toString("utf8");
555
624
  const title = titleFromText(path4.basename(absolutePath, path4.extname(absolutePath)), originalText);
556
625
  const attachments = [];
557
626
  for (const attachmentRef of attachmentRefs) {
558
- const bytes = await fs3.readFile(attachmentRef.absolutePath);
627
+ const bytes = await fs4.readFile(attachmentRef.absolutePath);
559
628
  attachments.push({
560
629
  relativePath: sanitizeAssetRelativePath(attachmentRef.relativeRef),
561
630
  mimeType: guessMimeType(attachmentRef.absolutePath),
@@ -652,7 +721,7 @@ async function listManifests(rootDir) {
652
721
  if (!await fileExists(paths.manifestsDir)) {
653
722
  return [];
654
723
  }
655
- const entries = await fs3.readdir(paths.manifestsDir);
724
+ const entries = await fs4.readdir(paths.manifestsDir);
656
725
  const manifests = await Promise.all(
657
726
  entries.filter((entry) => entry.endsWith(".json")).map((entry) => readJsonFile(path4.join(paths.manifestsDir, entry)))
658
727
  );
@@ -666,12 +735,12 @@ async function readExtractedText(rootDir, manifest) {
666
735
  if (!await fileExists(absolutePath)) {
667
736
  return void 0;
668
737
  }
669
- return fs3.readFile(absolutePath, "utf8");
738
+ return fs4.readFile(absolutePath, "utf8");
670
739
  }
671
740
 
672
741
  // src/vault.ts
673
- import fs7 from "fs/promises";
674
- import path9 from "path";
742
+ import fs9 from "fs/promises";
743
+ import path10 from "path";
675
744
  import matter3 from "gray-matter";
676
745
 
677
746
  // src/analysis.ts
@@ -758,7 +827,7 @@ function deriveTitle(manifest, text) {
758
827
  const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim();
759
828
  return heading || manifest.title;
760
829
  }
761
- function heuristicAnalysis(manifest, text) {
830
+ function heuristicAnalysis(manifest, text, schemaHash) {
762
831
  const normalized = normalizeWhitespace(text);
763
832
  const concepts = extractTopTerms(normalized, 6).map((term) => ({
764
833
  id: `concept:${slugify(term)}`,
@@ -774,6 +843,7 @@ function heuristicAnalysis(manifest, text) {
774
843
  return {
775
844
  sourceId: manifest.sourceId,
776
845
  sourceHash: manifest.contentHash,
846
+ schemaHash,
777
847
  title: deriveTitle(manifest, text),
778
848
  summary: firstSentences(normalized, 3) || truncate(normalized, 280) || `Imported ${manifest.sourceKind} source.`,
779
849
  concepts,
@@ -790,10 +860,19 @@ function heuristicAnalysis(manifest, text) {
790
860
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
791
861
  };
792
862
  }
793
- async function providerAnalysis(manifest, text, provider) {
863
+ async function providerAnalysis(manifest, text, provider, schema) {
794
864
  const parsed = await provider.generateStructured(
795
865
  {
796
- system: "You are compiling a durable markdown wiki and graph. Prefer grounded synthesis over creativity.",
866
+ system: [
867
+ "You are compiling a durable markdown wiki and graph. Prefer grounded synthesis over creativity.",
868
+ "",
869
+ "Follow the vault schema when choosing titles, categories, relationships, and summaries.",
870
+ "",
871
+ `Vault schema path: ${schema.path}`,
872
+ "",
873
+ "Vault schema instructions:",
874
+ truncate(schema.content, 6e3)
875
+ ].join("\n"),
797
876
  prompt: `Analyze the following source and return structured JSON.
798
877
 
799
878
  Source title: ${manifest.title}
@@ -808,6 +887,7 @@ ${truncate(text, 18e3)}`
808
887
  return {
809
888
  sourceId: manifest.sourceId,
810
889
  sourceHash: manifest.contentHash,
890
+ schemaHash: schema.hash,
811
891
  title: parsed.title,
812
892
  summary: parsed.summary,
813
893
  concepts: parsed.concepts.map((term) => ({
@@ -832,10 +912,10 @@ ${truncate(text, 18e3)}`
832
912
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
833
913
  };
834
914
  }
835
- async function analyzeSource(manifest, extractedText, provider, paths) {
915
+ async function analyzeSource(manifest, extractedText, provider, paths, schema) {
836
916
  const cachePath = path5.join(paths.analysesDir, `${manifest.sourceId}.json`);
837
917
  const cached = await readJsonFile(cachePath);
838
- if (cached && cached.sourceHash === manifest.contentHash) {
918
+ if (cached && cached.sourceHash === manifest.contentHash && cached.schemaHash === schema.hash) {
839
919
  return cached;
840
920
  }
841
921
  const content = normalizeWhitespace(extractedText ?? "");
@@ -844,6 +924,7 @@ async function analyzeSource(manifest, extractedText, provider, paths) {
844
924
  analysis = {
845
925
  sourceId: manifest.sourceId,
846
926
  sourceHash: manifest.contentHash,
927
+ schemaHash: schema.hash,
847
928
  title: manifest.title,
848
929
  summary: `Imported ${manifest.sourceKind} source. Text extraction is not yet available for this source.`,
849
930
  concepts: [],
@@ -853,12 +934,12 @@ async function analyzeSource(manifest, extractedText, provider, paths) {
853
934
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
854
935
  };
855
936
  } else if (provider.type === "heuristic") {
856
- analysis = heuristicAnalysis(manifest, content);
937
+ analysis = heuristicAnalysis(manifest, content, schema.hash);
857
938
  } else {
858
939
  try {
859
- analysis = await providerAnalysis(manifest, content, provider);
940
+ analysis = await providerAnalysis(manifest, content, provider, schema);
860
941
  } catch {
861
- analysis = heuristicAnalysis(manifest, content);
942
+ analysis = heuristicAnalysis(manifest, content, schema.hash);
862
943
  }
863
944
  }
864
945
  await writeJsonFile(cachePath, analysis);
@@ -869,7 +950,7 @@ function analysisSignature(analysis) {
869
950
  }
870
951
 
871
952
  // src/agents.ts
872
- import fs4 from "fs/promises";
953
+ import fs5 from "fs/promises";
873
954
  import path6 from "path";
874
955
  var managedStart = "<!-- swarmvault:managed:start -->";
875
956
  var managedEnd = "<!-- swarmvault:managed:end -->";
@@ -880,6 +961,7 @@ function buildManagedBlock(agent) {
880
961
  managedStart,
881
962
  `# SwarmVault Rules (${agent})`,
882
963
  "",
964
+ "- Read `swarmvault.schema.md` before compile or query style work. If only `schema.md` exists, treat it as the legacy schema path.",
883
965
  "- Treat `raw/` as immutable source input.",
884
966
  "- Treat `wiki/` as generated markdown owned by the agent and compiler workflow.",
885
967
  "- Read `wiki/index.md` before broad file searching when answering SwarmVault questions.",
@@ -895,10 +977,10 @@ function buildManagedBlock(agent) {
895
977
  return body;
896
978
  }
897
979
  async function upsertManagedBlock(filePath, block) {
898
- const existing = await fileExists(filePath) ? await fs4.readFile(filePath, "utf8") : "";
980
+ const existing = await fileExists(filePath) ? await fs5.readFile(filePath, "utf8") : "";
899
981
  if (!existing) {
900
982
  await ensureDir(path6.dirname(filePath));
901
- await fs4.writeFile(filePath, `${block}
983
+ await fs5.writeFile(filePath, `${block}
902
984
  `, "utf8");
903
985
  return;
904
986
  }
@@ -906,10 +988,10 @@ async function upsertManagedBlock(filePath, block) {
906
988
  const endIndex = existing.includes(managedEnd) ? existing.indexOf(managedEnd) : existing.indexOf(legacyManagedEnd);
907
989
  if (startIndex !== -1 && endIndex !== -1) {
908
990
  const next = `${existing.slice(0, startIndex)}${block}${existing.slice(endIndex + managedEnd.length)}`;
909
- await fs4.writeFile(filePath, next, "utf8");
991
+ await fs5.writeFile(filePath, next, "utf8");
910
992
  return;
911
993
  }
912
- await fs4.writeFile(filePath, `${existing.trimEnd()}
994
+ await fs5.writeFile(filePath, `${existing.trimEnd()}
913
995
 
914
996
  ${block}
915
997
  `, "utf8");
@@ -932,7 +1014,7 @@ async function installAgent(rootDir, agent) {
932
1014
  const rulesDir = path6.join(rootDir, ".cursor", "rules");
933
1015
  await ensureDir(rulesDir);
934
1016
  const target = path6.join(rulesDir, "swarmvault.mdc");
935
- await fs4.writeFile(target, `${block}
1017
+ await fs5.writeFile(target, `${block}
936
1018
  `, "utf8");
937
1019
  return target;
938
1020
  }
@@ -961,7 +1043,7 @@ function pagePathFor(kind, slug) {
961
1043
  return `${slug}.md`;
962
1044
  }
963
1045
  }
964
- function buildSourcePage(manifest, analysis) {
1046
+ function buildSourcePage(manifest, analysis, schemaHash) {
965
1047
  const relativePath = pagePathFor("source", manifest.sourceId);
966
1048
  const pageId = `source:${manifest.sourceId}`;
967
1049
  const nodeIds = [
@@ -984,6 +1066,7 @@ function buildSourcePage(manifest, analysis) {
984
1066
  confidence: 0.8,
985
1067
  updated_at: analysis.producedAt,
986
1068
  backlinks,
1069
+ schema_hash: schemaHash,
987
1070
  source_hashes: {
988
1071
  [manifest.sourceId]: manifest.contentHash
989
1072
  }
@@ -1026,12 +1109,13 @@ function buildSourcePage(manifest, analysis) {
1026
1109
  freshness: "fresh",
1027
1110
  confidence: 0.8,
1028
1111
  backlinks,
1112
+ schemaHash,
1029
1113
  sourceHashes: { [manifest.sourceId]: manifest.contentHash }
1030
1114
  },
1031
1115
  content: matter.stringify(body, frontmatter)
1032
1116
  };
1033
1117
  }
1034
- function buildAggregatePage(kind, name, descriptions, sourceAnalyses, sourceHashes) {
1118
+ function buildAggregatePage(kind, name, descriptions, sourceAnalyses, sourceHashes, schemaHash) {
1035
1119
  const slug = slugify(name);
1036
1120
  const relativePath = pagePathFor(kind, slug);
1037
1121
  const pageId = `${kind}:${slug}`;
@@ -1049,6 +1133,7 @@ function buildAggregatePage(kind, name, descriptions, sourceAnalyses, sourceHash
1049
1133
  confidence: 0.72,
1050
1134
  updated_at: (/* @__PURE__ */ new Date()).toISOString(),
1051
1135
  backlinks: otherPages,
1136
+ schema_hash: schemaHash,
1052
1137
  source_hashes: sourceHashes
1053
1138
  };
1054
1139
  const body = [
@@ -1080,12 +1165,13 @@ function buildAggregatePage(kind, name, descriptions, sourceAnalyses, sourceHash
1080
1165
  freshness: "fresh",
1081
1166
  confidence: 0.72,
1082
1167
  backlinks: otherPages,
1168
+ schemaHash,
1083
1169
  sourceHashes
1084
1170
  },
1085
1171
  content: matter.stringify(body, frontmatter)
1086
1172
  };
1087
1173
  }
1088
- function buildIndexPage(pages) {
1174
+ function buildIndexPage(pages, schemaHash) {
1089
1175
  const sources = pages.filter((page) => page.kind === "source");
1090
1176
  const concepts = pages.filter((page) => page.kind === "concept");
1091
1177
  const entities = pages.filter((page) => page.kind === "entity");
@@ -1102,6 +1188,7 @@ function buildIndexPage(pages) {
1102
1188
  "confidence: 1",
1103
1189
  `updated_at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1104
1190
  "backlinks: []",
1191
+ `schema_hash: ${schemaHash}`,
1105
1192
  "source_hashes: {}",
1106
1193
  "---",
1107
1194
  "",
@@ -1121,16 +1208,32 @@ function buildIndexPage(pages) {
1121
1208
  ""
1122
1209
  ].join("\n");
1123
1210
  }
1124
- function buildSectionIndex(kind, pages) {
1211
+ function buildSectionIndex(kind, pages, schemaHash) {
1125
1212
  const title = kind.charAt(0).toUpperCase() + kind.slice(1);
1126
- return [
1127
- `# ${title}`,
1128
- "",
1129
- ...pages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`),
1130
- ""
1131
- ].join("\n");
1213
+ return matter.stringify(
1214
+ [
1215
+ `# ${title}`,
1216
+ "",
1217
+ ...pages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`),
1218
+ ""
1219
+ ].join("\n"),
1220
+ {
1221
+ page_id: `${kind}:index`,
1222
+ kind: "index",
1223
+ title,
1224
+ tags: ["index", kind],
1225
+ source_ids: [],
1226
+ node_ids: [],
1227
+ freshness: "fresh",
1228
+ confidence: 1,
1229
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
1230
+ backlinks: [],
1231
+ schema_hash: schemaHash,
1232
+ source_hashes: {}
1233
+ }
1234
+ );
1132
1235
  }
1133
- function buildOutputPage(question, answer, citations) {
1236
+ function buildOutputPage(question, answer, citations, schemaHash) {
1134
1237
  const slug = slugify(question);
1135
1238
  const pageId = `output:${slug}`;
1136
1239
  const pathValue = pagePathFor("output", slug);
@@ -1145,6 +1248,7 @@ function buildOutputPage(question, answer, citations) {
1145
1248
  confidence: 0.74,
1146
1249
  updated_at: (/* @__PURE__ */ new Date()).toISOString(),
1147
1250
  backlinks: citations.map((sourceId) => `source:${sourceId}`),
1251
+ schema_hash: schemaHash,
1148
1252
  source_hashes: {}
1149
1253
  };
1150
1254
  return {
@@ -1158,6 +1262,7 @@ function buildOutputPage(question, answer, citations) {
1158
1262
  freshness: "fresh",
1159
1263
  confidence: 0.74,
1160
1264
  backlinks: citations.map((sourceId) => `source:${sourceId}`),
1265
+ schemaHash,
1161
1266
  sourceHashes: {}
1162
1267
  },
1163
1268
  content: matter.stringify(
@@ -1182,7 +1287,7 @@ import { pathToFileURL } from "url";
1182
1287
  import { z as z5 } from "zod";
1183
1288
 
1184
1289
  // src/providers/base.ts
1185
- import fs5 from "fs/promises";
1290
+ import fs6 from "fs/promises";
1186
1291
  import { z as z4 } from "zod";
1187
1292
  var BaseProviderAdapter = class {
1188
1293
  constructor(id, type, model, capabilities) {
@@ -1211,7 +1316,7 @@ ${schemaDescription}`
1211
1316
  return Promise.all(
1212
1317
  attachments.map(async (attachment) => ({
1213
1318
  mimeType: attachment.mimeType,
1214
- base64: await fs5.readFile(attachment.filePath, "base64")
1319
+ base64: await fs6.readFile(attachment.filePath, "base64")
1215
1320
  }))
1216
1321
  );
1217
1322
  }
@@ -1525,7 +1630,7 @@ function assertProviderCapability(provider, capability) {
1525
1630
  }
1526
1631
 
1527
1632
  // src/search.ts
1528
- import fs6 from "fs/promises";
1633
+ import fs7 from "fs/promises";
1529
1634
  import path8 from "path";
1530
1635
  import matter2 from "gray-matter";
1531
1636
  function getDatabaseSync() {
@@ -1563,7 +1668,7 @@ async function rebuildSearchIndex(dbPath, pages, wikiDir) {
1563
1668
  const insertPage = db.prepare("INSERT INTO pages (id, path, title, body) VALUES (?, ?, ?, ?)");
1564
1669
  for (const page of pages) {
1565
1670
  const absolutePath = path8.join(wikiDir, page.path);
1566
- const content = await fs6.readFile(absolutePath, "utf8");
1671
+ const content = await fs7.readFile(absolutePath, "utf8");
1567
1672
  const parsed = matter2(content);
1568
1673
  insertPage.run(page.id, page.path, page.title, parsed.content);
1569
1674
  }
@@ -1601,6 +1706,32 @@ function searchPages(dbPath, query, limit = 5) {
1601
1706
  }));
1602
1707
  }
1603
1708
 
1709
+ // src/schema.ts
1710
+ import fs8 from "fs/promises";
1711
+ import path9 from "path";
1712
+ async function loadVaultSchema(rootDir) {
1713
+ const { paths } = await loadVaultConfig(rootDir);
1714
+ const schemaPath = paths.schemaPath;
1715
+ const content = await fileExists(schemaPath) ? await fs8.readFile(schemaPath, "utf8") : defaultVaultSchema();
1716
+ const normalized = content.trim() ? content.trim() : defaultVaultSchema().trim();
1717
+ return {
1718
+ path: schemaPath,
1719
+ content: normalized,
1720
+ hash: sha256(normalized),
1721
+ isLegacyPath: path9.basename(schemaPath) === LEGACY_SCHEMA_FILENAME && path9.basename(schemaPath) !== PRIMARY_SCHEMA_FILENAME
1722
+ };
1723
+ }
1724
+ function buildSchemaPrompt(schema, instruction) {
1725
+ return [
1726
+ instruction,
1727
+ "",
1728
+ `Vault schema path: ${schema.path}`,
1729
+ "",
1730
+ "Vault schema instructions:",
1731
+ schema.content
1732
+ ].join("\n");
1733
+ }
1734
+
1604
1735
  // src/vault.ts
1605
1736
  function buildGraph(manifests, analyses, pages) {
1606
1737
  const sourceNodes = manifests.map((manifest) => ({
@@ -1682,8 +1813,8 @@ function buildGraph(manifests, analyses, pages) {
1682
1813
  pages
1683
1814
  };
1684
1815
  }
1685
- async function writePage(rootDir, relativePath, content, changedPages) {
1686
- const absolutePath = path9.resolve(rootDir, "wiki", relativePath);
1816
+ async function writePage(wikiDir, relativePath, content, changedPages) {
1817
+ const absolutePath = path10.resolve(wikiDir, relativePath);
1687
1818
  const changed = await writeFileIfChanged(absolutePath, content);
1688
1819
  if (changed) {
1689
1820
  changedPages.push(relativePath);
@@ -1714,50 +1845,52 @@ async function initVault(rootDir) {
1714
1845
  }
1715
1846
  async function compileVault(rootDir) {
1716
1847
  const { paths } = await initWorkspace(rootDir);
1848
+ const schema = await loadVaultSchema(rootDir);
1717
1849
  const provider = await getProviderForTask(rootDir, "compileProvider");
1718
1850
  const manifests = await listManifests(rootDir);
1719
1851
  const analyses = await Promise.all(
1720
- manifests.map(async (manifest) => analyzeSource(manifest, await readExtractedText(rootDir, manifest), provider, paths))
1852
+ manifests.map(async (manifest) => analyzeSource(manifest, await readExtractedText(rootDir, manifest), provider, paths, schema))
1721
1853
  );
1722
1854
  const changedPages = [];
1723
1855
  const pages = [];
1724
1856
  await Promise.all([
1725
- ensureDir(path9.join(paths.wikiDir, "sources")),
1726
- ensureDir(path9.join(paths.wikiDir, "concepts")),
1727
- ensureDir(path9.join(paths.wikiDir, "entities")),
1728
- ensureDir(path9.join(paths.wikiDir, "outputs"))
1857
+ ensureDir(path10.join(paths.wikiDir, "sources")),
1858
+ ensureDir(path10.join(paths.wikiDir, "concepts")),
1859
+ ensureDir(path10.join(paths.wikiDir, "entities")),
1860
+ ensureDir(path10.join(paths.wikiDir, "outputs"))
1729
1861
  ]);
1730
1862
  for (const manifest of manifests) {
1731
1863
  const analysis = analyses.find((item) => item.sourceId === manifest.sourceId);
1732
1864
  if (!analysis) {
1733
1865
  continue;
1734
1866
  }
1735
- const sourcePage = buildSourcePage(manifest, analysis);
1867
+ const sourcePage = buildSourcePage(manifest, analysis, schema.hash);
1736
1868
  pages.push(sourcePage.page);
1737
- await writePage(rootDir, sourcePage.page.path, sourcePage.content, changedPages);
1869
+ await writePage(paths.wikiDir, sourcePage.page.path, sourcePage.content, changedPages);
1738
1870
  }
1739
1871
  for (const aggregate of aggregateItems(analyses, "concepts")) {
1740
- const page = buildAggregatePage("concept", aggregate.name, aggregate.descriptions, aggregate.sourceAnalyses, aggregate.sourceHashes);
1872
+ const page = buildAggregatePage("concept", aggregate.name, aggregate.descriptions, aggregate.sourceAnalyses, aggregate.sourceHashes, schema.hash);
1741
1873
  pages.push(page.page);
1742
- await writePage(rootDir, page.page.path, page.content, changedPages);
1874
+ await writePage(paths.wikiDir, page.page.path, page.content, changedPages);
1743
1875
  }
1744
1876
  for (const aggregate of aggregateItems(analyses, "entities")) {
1745
- const page = buildAggregatePage("entity", aggregate.name, aggregate.descriptions, aggregate.sourceAnalyses, aggregate.sourceHashes);
1877
+ const page = buildAggregatePage("entity", aggregate.name, aggregate.descriptions, aggregate.sourceAnalyses, aggregate.sourceHashes, schema.hash);
1746
1878
  pages.push(page.page);
1747
- await writePage(rootDir, page.page.path, page.content, changedPages);
1879
+ await writePage(paths.wikiDir, page.page.path, page.content, changedPages);
1748
1880
  }
1749
1881
  const graph = buildGraph(manifests, analyses, pages);
1750
1882
  await writeJsonFile(paths.graphPath, graph);
1751
1883
  await writeJsonFile(paths.compileStatePath, {
1752
1884
  generatedAt: graph.generatedAt,
1885
+ schemaHash: schema.hash,
1753
1886
  analyses: Object.fromEntries(analyses.map((analysis) => [analysis.sourceId, analysisSignature(analysis)]))
1754
1887
  });
1755
- await writePage(rootDir, "index.md", buildIndexPage(pages), changedPages);
1756
- await writePage(rootDir, "sources/index.md", buildSectionIndex("sources", pages.filter((page) => page.kind === "source")), changedPages);
1757
- await writePage(rootDir, "concepts/index.md", buildSectionIndex("concepts", pages.filter((page) => page.kind === "concept")), changedPages);
1758
- await writePage(rootDir, "entities/index.md", buildSectionIndex("entities", pages.filter((page) => page.kind === "entity")), changedPages);
1888
+ await writePage(paths.wikiDir, "index.md", buildIndexPage(pages, schema.hash), changedPages);
1889
+ await writePage(paths.wikiDir, "sources/index.md", buildSectionIndex("sources", pages.filter((page) => page.kind === "source"), schema.hash), changedPages);
1890
+ await writePage(paths.wikiDir, "concepts/index.md", buildSectionIndex("concepts", pages.filter((page) => page.kind === "concept"), schema.hash), changedPages);
1891
+ await writePage(paths.wikiDir, "entities/index.md", buildSectionIndex("entities", pages.filter((page) => page.kind === "entity"), schema.hash), changedPages);
1759
1892
  await rebuildSearchIndex(paths.searchDbPath, pages, paths.wikiDir);
1760
- await appendLogEntry(rootDir, "compile", `Compiled ${manifests.length} source(s)`, [`provider=${provider.id}`, `pages=${pages.length}`]);
1893
+ await appendLogEntry(rootDir, "compile", `Compiled ${manifests.length} source(s)`, [`provider=${provider.id}`, `pages=${pages.length}`, `schema=${schema.hash.slice(0, 12)}`]);
1761
1894
  return {
1762
1895
  graphPath: paths.graphPath,
1763
1896
  pageCount: pages.length,
@@ -1767,6 +1900,7 @@ async function compileVault(rootDir) {
1767
1900
  }
1768
1901
  async function queryVault(rootDir, question, save = false) {
1769
1902
  const { paths } = await loadVaultConfig(rootDir);
1903
+ const schema = await loadVaultSchema(rootDir);
1770
1904
  const provider = await getProviderForTask(rootDir, "queryProvider");
1771
1905
  if (!await fileExists(paths.searchDbPath)) {
1772
1906
  await compileVault(rootDir);
@@ -1774,8 +1908,8 @@ async function queryVault(rootDir, question, save = false) {
1774
1908
  const searchResults = searchPages(paths.searchDbPath, question, 5);
1775
1909
  const excerpts = await Promise.all(
1776
1910
  searchResults.map(async (result) => {
1777
- const absolutePath = path9.join(paths.wikiDir, result.path);
1778
- const content = await fs7.readFile(absolutePath, "utf8");
1911
+ const absolutePath = path10.join(paths.wikiDir, result.path);
1912
+ const content = await fs9.readFile(absolutePath, "utf8");
1779
1913
  const parsed = matter3(content);
1780
1914
  return `# ${result.title}
1781
1915
  ${truncate(normalizeWhitespace(parsed.content), 1200)}`;
@@ -1793,7 +1927,7 @@ ${truncate(normalizeWhitespace(parsed.content), 1200)}`;
1793
1927
  ].join("\n");
1794
1928
  } else {
1795
1929
  const response = await provider.generateText({
1796
- system: "Answer using the provided SwarmVault excerpts. Cite source ids or page titles when possible.",
1930
+ system: buildSchemaPrompt(schema, "Answer using the provided SwarmVault excerpts. Cite source ids or page titles when possible."),
1797
1931
  prompt: `Question: ${question}
1798
1932
 
1799
1933
  Context:
@@ -1807,10 +1941,10 @@ ${excerpts.join("\n\n---\n\n")}`
1807
1941
  );
1808
1942
  let savedTo;
1809
1943
  if (save) {
1810
- const output = buildOutputPage(question, answer, citations);
1811
- const absolutePath = path9.join(paths.wikiDir, output.page.path);
1812
- await ensureDir(path9.dirname(absolutePath));
1813
- await fs7.writeFile(absolutePath, output.content, "utf8");
1944
+ const output = buildOutputPage(question, answer, citations, schema.hash);
1945
+ const absolutePath = path10.join(paths.wikiDir, output.page.path);
1946
+ await ensureDir(path10.dirname(absolutePath));
1947
+ await fs9.writeFile(absolutePath, output.content, "utf8");
1814
1948
  savedTo = absolutePath;
1815
1949
  }
1816
1950
  await appendLogEntry(rootDir, "query", question, [`citations=${citations.join(",") || "none"}`, `saved=${Boolean(savedTo)}`]);
@@ -1830,15 +1964,15 @@ async function listPages(rootDir) {
1830
1964
  }
1831
1965
  async function readPage(rootDir, relativePath) {
1832
1966
  const { paths } = await loadVaultConfig(rootDir);
1833
- const absolutePath = path9.resolve(paths.wikiDir, relativePath);
1967
+ const absolutePath = path10.resolve(paths.wikiDir, relativePath);
1834
1968
  if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
1835
1969
  return null;
1836
1970
  }
1837
- const raw = await fs7.readFile(absolutePath, "utf8");
1971
+ const raw = await fs9.readFile(absolutePath, "utf8");
1838
1972
  const parsed = matter3(raw);
1839
1973
  return {
1840
1974
  path: relativePath,
1841
- title: typeof parsed.data.title === "string" ? parsed.data.title : path9.basename(relativePath, path9.extname(relativePath)),
1975
+ title: typeof parsed.data.title === "string" ? parsed.data.title : path10.basename(relativePath, path10.extname(relativePath)),
1842
1976
  frontmatter: parsed.data,
1843
1977
  content: parsed.content
1844
1978
  };
@@ -1850,6 +1984,7 @@ async function getWorkspaceInfo(rootDir) {
1850
1984
  return {
1851
1985
  rootDir,
1852
1986
  configPath: paths.configPath,
1987
+ schemaPath: paths.schemaPath,
1853
1988
  rawDir: paths.rawDir,
1854
1989
  wikiDir: paths.wikiDir,
1855
1990
  stateDir: paths.stateDir,
@@ -1861,6 +1996,7 @@ async function getWorkspaceInfo(rootDir) {
1861
1996
  }
1862
1997
  async function lintVault(rootDir) {
1863
1998
  const { paths } = await loadVaultConfig(rootDir);
1999
+ const schema = await loadVaultSchema(rootDir);
1864
2000
  const manifests = await listManifests(rootDir);
1865
2001
  const graph = await readJsonFile(paths.graphPath);
1866
2002
  const findings = [];
@@ -1875,6 +2011,14 @@ async function lintVault(rootDir) {
1875
2011
  }
1876
2012
  const manifestMap = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
1877
2013
  for (const page of graph.pages) {
2014
+ if (page.schemaHash !== schema.hash) {
2015
+ findings.push({
2016
+ severity: "warning",
2017
+ code: "stale_page",
2018
+ message: `Page ${page.title} is stale because the vault schema changed.`,
2019
+ pagePath: path10.join(paths.wikiDir, page.path)
2020
+ });
2021
+ }
1878
2022
  for (const [sourceId, knownHash] of Object.entries(page.sourceHashes)) {
1879
2023
  const manifest = manifestMap.get(sourceId);
1880
2024
  if (manifest && manifest.contentHash !== knownHash) {
@@ -1882,7 +2026,7 @@ async function lintVault(rootDir) {
1882
2026
  severity: "warning",
1883
2027
  code: "stale_page",
1884
2028
  message: `Page ${page.title} is stale because source ${sourceId} changed.`,
1885
- pagePath: path9.join(paths.wikiDir, page.path)
2029
+ pagePath: path10.join(paths.wikiDir, page.path)
1886
2030
  });
1887
2031
  }
1888
2032
  }
@@ -1891,12 +2035,12 @@ async function lintVault(rootDir) {
1891
2035
  severity: "info",
1892
2036
  code: "orphan_page",
1893
2037
  message: `Page ${page.title} has no backlinks.`,
1894
- pagePath: path9.join(paths.wikiDir, page.path)
2038
+ pagePath: path10.join(paths.wikiDir, page.path)
1895
2039
  });
1896
2040
  }
1897
- const absolutePath = path9.join(paths.wikiDir, page.path);
2041
+ const absolutePath = path10.join(paths.wikiDir, page.path);
1898
2042
  if (await fileExists(absolutePath)) {
1899
- const content = await fs7.readFile(absolutePath, "utf8");
2043
+ const content = await fs9.readFile(absolutePath, "utf8");
1900
2044
  if (content.includes("## Claims")) {
1901
2045
  const uncited = content.split("\n").filter((line) => line.startsWith("- ") && !line.includes("[source:"));
1902
2046
  if (uncited.length) {
@@ -1927,9 +2071,9 @@ async function bootstrapDemo(rootDir, input) {
1927
2071
  }
1928
2072
 
1929
2073
  // src/viewer.ts
1930
- import fs8 from "fs/promises";
2074
+ import fs10 from "fs/promises";
1931
2075
  import http from "http";
1932
- import path10 from "path";
2076
+ import path11 from "path";
1933
2077
  import mime2 from "mime-types";
1934
2078
  async function startGraphServer(rootDir, port) {
1935
2079
  const { config, paths } = await loadVaultConfig(rootDir);
@@ -1943,12 +2087,12 @@ async function startGraphServer(rootDir, port) {
1943
2087
  return;
1944
2088
  }
1945
2089
  response.writeHead(200, { "content-type": "application/json" });
1946
- response.end(await fs8.readFile(paths.graphPath, "utf8"));
2090
+ response.end(await fs10.readFile(paths.graphPath, "utf8"));
1947
2091
  return;
1948
2092
  }
1949
2093
  const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
1950
- const target = path10.join(paths.viewerDistDir, relativePath);
1951
- const fallback = path10.join(paths.viewerDistDir, "index.html");
2094
+ const target = path11.join(paths.viewerDistDir, relativePath);
2095
+ const fallback = path11.join(paths.viewerDistDir, "index.html");
1952
2096
  const filePath = await fileExists(target) ? target : fallback;
1953
2097
  if (!await fileExists(filePath)) {
1954
2098
  response.writeHead(503, { "content-type": "text/plain" });
@@ -1956,7 +2100,7 @@ async function startGraphServer(rootDir, port) {
1956
2100
  return;
1957
2101
  }
1958
2102
  response.writeHead(200, { "content-type": mime2.lookup(filePath) || "text/plain" });
1959
- response.end(await fs8.readFile(filePath));
2103
+ response.end(await fs10.readFile(filePath));
1960
2104
  });
1961
2105
  await new Promise((resolve) => {
1962
2106
  server.listen(effectivePort, resolve);
@@ -1978,12 +2122,12 @@ async function startGraphServer(rootDir, port) {
1978
2122
  }
1979
2123
 
1980
2124
  // src/mcp.ts
1981
- import fs9 from "fs/promises";
1982
- import path11 from "path";
2125
+ import fs11 from "fs/promises";
2126
+ import path12 from "path";
1983
2127
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1984
2128
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1985
2129
  import { z as z6 } from "zod";
1986
- var SERVER_VERSION = "0.1.3";
2130
+ var SERVER_VERSION = "0.1.4";
1987
2131
  async function createMcpServer(rootDir) {
1988
2132
  const server = new McpServer({
1989
2133
  name: "swarmvault",
@@ -2086,6 +2230,14 @@ async function createMcpServer(rootDir) {
2086
2230
  const manifests = await listManifests(rootDir);
2087
2231
  return asTextResource("swarmvault://manifests", JSON.stringify(manifests, null, 2));
2088
2232
  });
2233
+ server.registerResource("swarmvault-schema", "swarmvault://schema", {
2234
+ title: "SwarmVault Schema",
2235
+ description: "The vault schema file that guides compile and query behavior.",
2236
+ mimeType: "text/markdown"
2237
+ }, async () => {
2238
+ const schema = await loadVaultSchema(rootDir);
2239
+ return asTextResource("swarmvault://schema", schema.content);
2240
+ });
2089
2241
  server.registerResource(
2090
2242
  "swarmvault-pages",
2091
2243
  new ResourceTemplate("swarmvault://pages/{path}", {
@@ -2115,8 +2267,8 @@ async function createMcpServer(rootDir) {
2115
2267
  return asTextResource(`swarmvault://pages/${encodedPath}`, `Page not found: ${relativePath}`);
2116
2268
  }
2117
2269
  const { paths } = await loadVaultConfig(rootDir);
2118
- const absolutePath = path11.resolve(paths.wikiDir, relativePath);
2119
- return asTextResource(`swarmvault://pages/${encodedPath}`, await fs9.readFile(absolutePath, "utf8"));
2270
+ const absolutePath = path12.resolve(paths.wikiDir, relativePath);
2271
+ return asTextResource(`swarmvault://pages/${encodedPath}`, await fs11.readFile(absolutePath, "utf8"));
2120
2272
  }
2121
2273
  );
2122
2274
  return server;
@@ -2164,7 +2316,7 @@ function asTextResource(uri, text) {
2164
2316
  }
2165
2317
 
2166
2318
  // src/watch.ts
2167
- import path12 from "path";
2319
+ import path13 from "path";
2168
2320
  import chokidar from "chokidar";
2169
2321
  async function watchVault(rootDir, options = {}) {
2170
2322
  const { paths } = await initWorkspace(rootDir);
@@ -2258,7 +2410,7 @@ async function watchVault(rootDir, options = {}) {
2258
2410
  };
2259
2411
  }
2260
2412
  function toWatchReason(baseDir, targetPath) {
2261
- return path12.relative(baseDir, targetPath) || ".";
2413
+ return path13.relative(baseDir, targetPath) || ".";
2262
2414
  }
2263
2415
  export {
2264
2416
  assertProviderCapability,
@@ -2267,6 +2419,7 @@ export {
2267
2419
  createMcpServer,
2268
2420
  createProvider,
2269
2421
  defaultVaultConfig,
2422
+ defaultVaultSchema,
2270
2423
  getProviderForTask,
2271
2424
  getWorkspaceInfo,
2272
2425
  importInbox,
@@ -2279,6 +2432,7 @@ export {
2279
2432
  listManifests,
2280
2433
  listPages,
2281
2434
  loadVaultConfig,
2435
+ loadVaultSchema,
2282
2436
  queryVault,
2283
2437
  readExtractedText,
2284
2438
  readPage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmvaultai/engine",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Core engine for SwarmVault: ingest, compile, query, lint, and provider abstractions.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",