@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 +21 -4
- package/dist/index.d.ts +15 -2
- package/dist/index.js +247 -93
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
285
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
738
|
+
return fs4.readFile(absolutePath, "utf8");
|
|
670
739
|
}
|
|
671
740
|
|
|
672
741
|
// src/vault.ts
|
|
673
|
-
import
|
|
674
|
-
import
|
|
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:
|
|
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
|
|
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
|
|
980
|
+
const existing = await fileExists(filePath) ? await fs5.readFile(filePath, "utf8") : "";
|
|
899
981
|
if (!existing) {
|
|
900
982
|
await ensureDir(path6.dirname(filePath));
|
|
901
|
-
await
|
|
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
|
|
991
|
+
await fs5.writeFile(filePath, next, "utf8");
|
|
910
992
|
return;
|
|
911
993
|
}
|
|
912
|
-
await
|
|
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
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
1686
|
-
const absolutePath =
|
|
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(
|
|
1726
|
-
ensureDir(
|
|
1727
|
-
ensureDir(
|
|
1728
|
-
ensureDir(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1756
|
-
await writePage(
|
|
1757
|
-
await writePage(
|
|
1758
|
-
await writePage(
|
|
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 =
|
|
1778
|
-
const content = await
|
|
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 =
|
|
1812
|
-
await ensureDir(
|
|
1813
|
-
await
|
|
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 =
|
|
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
|
|
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 :
|
|
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:
|
|
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:
|
|
2038
|
+
pagePath: path10.join(paths.wikiDir, page.path)
|
|
1895
2039
|
});
|
|
1896
2040
|
}
|
|
1897
|
-
const absolutePath =
|
|
2041
|
+
const absolutePath = path10.join(paths.wikiDir, page.path);
|
|
1898
2042
|
if (await fileExists(absolutePath)) {
|
|
1899
|
-
const content = await
|
|
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
|
|
2074
|
+
import fs10 from "fs/promises";
|
|
1931
2075
|
import http from "http";
|
|
1932
|
-
import
|
|
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
|
|
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 =
|
|
1951
|
-
const fallback =
|
|
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
|
|
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
|
|
1982
|
-
import
|
|
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.
|
|
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 =
|
|
2119
|
-
return asTextResource(`swarmvault://pages/${encodedPath}`, await
|
|
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
|
|
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
|
|
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,
|