@swarmvaultai/engine 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  uniqueBy,
22
22
  writeFileIfChanged,
23
23
  writeJsonFile
24
- } from "./chunk-CWLDFLH2.js";
24
+ } from "./chunk-B3FC4J3P.js";
25
25
 
26
26
  // src/agents.ts
27
27
  import crypto from "crypto";
@@ -4504,6 +4504,7 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
4504
4504
  import fs7 from "fs/promises";
4505
4505
  import os from "os";
4506
4506
  import path7 from "path";
4507
+ import { Readable } from "stream";
4507
4508
  import { parse as parseCsvSync } from "csv-parse/sync";
4508
4509
  import { strFromU8, unzipSync } from "fflate";
4509
4510
  import { JSDOM } from "jsdom";
@@ -5171,6 +5172,508 @@ async function extractEpubChapters(input) {
5171
5172
  };
5172
5173
  }
5173
5174
  }
5175
+ function timestampFromMs(value) {
5176
+ const totalMs = Math.max(0, Math.floor(value));
5177
+ const totalSeconds = Math.floor(totalMs / 1e3);
5178
+ const hours = Math.floor(totalSeconds / 3600);
5179
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
5180
+ const seconds = totalSeconds % 60;
5181
+ const milliseconds = totalMs % 1e3;
5182
+ return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(
5183
+ milliseconds
5184
+ ).padStart(3, "0")}`;
5185
+ }
5186
+ function normalizeDelimitedList(values) {
5187
+ const unique = [...new Set(values.map((value) => normalizeWhitespace(value)).filter(Boolean))];
5188
+ return unique.length ? unique.join(", ") : void 0;
5189
+ }
5190
+ function normalizeIsoDate(value) {
5191
+ if (value instanceof Date && Number.isFinite(value.getTime())) {
5192
+ return value.toISOString();
5193
+ }
5194
+ if (typeof value === "string" && value.trim()) {
5195
+ const parsed = new Date(value);
5196
+ if (Number.isFinite(parsed.getTime())) {
5197
+ return parsed.toISOString();
5198
+ }
5199
+ }
5200
+ return void 0;
5201
+ }
5202
+ function addressNames(value) {
5203
+ if (!value || typeof value !== "object" || !("value" in value) || !Array.isArray(value.value)) {
5204
+ return [];
5205
+ }
5206
+ return value.value.map((entry) => normalizeWhitespace(entry.name ?? entry.address ?? "")).filter(Boolean);
5207
+ }
5208
+ function addressList(value) {
5209
+ return normalizeDelimitedList(addressNames(value));
5210
+ }
5211
+ function emailConversationId(parsed) {
5212
+ const asArray = (value) => Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
5213
+ return normalizeWhitespace(parsed.messageId ?? "") || normalizeWhitespace(asArray(parsed.inReplyTo)[0] ?? "") || normalizeWhitespace(asArray(parsed.references)[0] ?? "") || void 0;
5214
+ }
5215
+ function emailBodyMarkdown(parsed) {
5216
+ const text = normalizeDocumentText(parsed.text ?? "");
5217
+ if (text) {
5218
+ return text;
5219
+ }
5220
+ if (typeof parsed.html === "string" && parsed.html.trim()) {
5221
+ return normalizeDocumentText(htmlToMarkdown(parsed.html));
5222
+ }
5223
+ return "";
5224
+ }
5225
+ function normalizeParsedEmail(parsed, fallbackTitle) {
5226
+ const title = normalizeWhitespace(parsed.subject ?? "") || fallbackTitle;
5227
+ const sender = addressList(parsed.from);
5228
+ const recipients = addressList(parsed.to);
5229
+ const cc = addressList(parsed.cc);
5230
+ const occurredAt = normalizeIsoDate(parsed.date);
5231
+ const participants = normalizeDelimitedList([...addressNames(parsed.from), ...addressNames(parsed.to), ...addressNames(parsed.cc)]);
5232
+ const conversationId = emailConversationId(parsed);
5233
+ const body = emailBodyMarkdown(parsed);
5234
+ const attachmentCount = Array.isArray(parsed.attachments) ? parsed.attachments.length : 0;
5235
+ return {
5236
+ title,
5237
+ conversationId,
5238
+ metadata: {
5239
+ ...occurredAt ? { occurred_at: occurredAt } : {},
5240
+ ...sender ? { sender } : {},
5241
+ ...recipients ? { recipients } : {},
5242
+ ...cc ? { cc } : {},
5243
+ ...participants ? { participants } : {},
5244
+ ...conversationId ? { conversation_id: conversationId } : {},
5245
+ ...normalizeWhitespace(parsed.messageId ?? "") ? { message_id: normalizeWhitespace(parsed.messageId ?? "") } : {},
5246
+ ...attachmentCount ? { attachment_count: String(attachmentCount) } : {}
5247
+ },
5248
+ markdown: [
5249
+ `# ${title}`,
5250
+ "",
5251
+ ...occurredAt ? [`Date: ${occurredAt}`] : [],
5252
+ ...sender ? [`From: ${sender}`] : [],
5253
+ ...recipients ? [`To: ${recipients}`] : [],
5254
+ ...cc ? [`CC: ${cc}`] : [],
5255
+ ...conversationId ? [`Conversation ID: ${conversationId}`] : [],
5256
+ ...attachmentCount ? [`Attachments: ${attachmentCount}`] : [],
5257
+ "",
5258
+ "## Message",
5259
+ "",
5260
+ body || "No readable body content was extracted from this email.",
5261
+ ""
5262
+ ].join("\n")
5263
+ };
5264
+ }
5265
+ function calendarAttendees(value) {
5266
+ if (!value) {
5267
+ return [];
5268
+ }
5269
+ const attendees = Array.isArray(value) ? value : [value];
5270
+ return attendees.map((entry) => {
5271
+ if (!entry || typeof entry !== "object") {
5272
+ return "";
5273
+ }
5274
+ const item = entry;
5275
+ const name = normalizeWhitespace(String(item.params?.CN ?? ""));
5276
+ const address = normalizeWhitespace(String(item.val ?? item.value ?? ""));
5277
+ return name || address;
5278
+ }).filter(Boolean);
5279
+ }
5280
+ function slackFormatSpeakerId(input, usersById) {
5281
+ return usersById.get(input) ?? input;
5282
+ }
5283
+ function slackNormalizeText(text, usersById) {
5284
+ return normalizeWhitespace(
5285
+ text.replace(/<@([A-Z0-9]+)>/g, (_, userId) => `@${slackFormatSpeakerId(userId, usersById)}`).replace(/<#[A-Z0-9]+\|([^>]+)>/g, "#$1").replace(/<(https?:\/\/[^>|]+)\|([^>]+)>/g, "$2 ($1)").replace(/<(https?:\/\/[^>]+)>/g, "$1")
5286
+ );
5287
+ }
5288
+ function slackMessageTimestamp(ts2, fallbackDate) {
5289
+ const numeric = Number(ts2);
5290
+ if (Number.isFinite(numeric) && numeric > 0) {
5291
+ return new Date(numeric * 1e3).toISOString();
5292
+ }
5293
+ return (/* @__PURE__ */ new Date(`${fallbackDate}T00:00:00.000Z`)).toISOString();
5294
+ }
5295
+ async function loadZipMessageBuffers(bytes) {
5296
+ const { MboxStream } = await import("node-mbox");
5297
+ const stream = MboxStream(Readable.from([bytes]));
5298
+ return await new Promise((resolve, reject) => {
5299
+ const messages = [];
5300
+ stream.on("data", (message) => {
5301
+ messages.push(Buffer.isBuffer(message) ? message : Buffer.from(message));
5302
+ });
5303
+ stream.on("error", reject);
5304
+ stream.on("finish", () => resolve(messages));
5305
+ stream.on("end", () => resolve(messages));
5306
+ });
5307
+ }
5308
+ function archiveEntriesAsText(archive) {
5309
+ return new Map(
5310
+ Object.entries(archive).filter(([, value]) => value).map(([entryPath, value]) => [entryPath, strFromU8(value)])
5311
+ );
5312
+ }
5313
+ function looksLikeSlackEntries(entries) {
5314
+ const all = [...entries];
5315
+ const hasChannelsIndex = all.some(
5316
+ (entry) => entry === "channels.json" || entry === "groups.json" || entry === "dms.json" || entry === "mpims.json"
5317
+ );
5318
+ const hasChannelDayFiles = all.some((entry) => /^[^/]+\/\d{4}-\d{2}-\d{2}\.json$/i.test(entry));
5319
+ return hasChannelsIndex && hasChannelDayFiles;
5320
+ }
5321
+ function slackEntriesFromChannelIndex(raw, usersById) {
5322
+ const entries = /* @__PURE__ */ new Map();
5323
+ if (!Array.isArray(raw)) {
5324
+ return entries;
5325
+ }
5326
+ for (const item of raw) {
5327
+ if (!item || typeof item !== "object") {
5328
+ continue;
5329
+ }
5330
+ const value = item;
5331
+ const id = normalizeWhitespace(value.id ?? "");
5332
+ const title = normalizeWhitespace(value.name ?? "");
5333
+ if (!title) {
5334
+ continue;
5335
+ }
5336
+ const members = (Array.isArray(value.members) ? value.members : value.user ? [value.user] : []).map((member) => slackFormatSpeakerId(member, usersById)).filter(Boolean);
5337
+ entries.set(title, { id, title, members });
5338
+ }
5339
+ return entries;
5340
+ }
5341
+ async function extractTranscriptText(input) {
5342
+ try {
5343
+ const { parseSync } = await import("subtitle");
5344
+ const rawText = decodeTextBytes(input.bytes);
5345
+ const cues = parseSync(rawText).filter((node) => node.type === "cue" && node.data).map((node) => ({
5346
+ start: Math.max(0, node.data?.start ?? 0),
5347
+ end: Math.max(0, node.data?.end ?? 0),
5348
+ text: normalizeWhitespace((node.data?.text ?? "").replace(/\s*\n+\s*/g, " "))
5349
+ })).filter((cue) => cue.text);
5350
+ const title = input.fileName ? path7.basename(input.fileName, path7.extname(input.fileName)) : void 0;
5351
+ const extractedText = [
5352
+ title ? `# ${title}` : null,
5353
+ `Format: ${input.fileName?.toLowerCase().endsWith(".vtt") ? "WebVTT" : "SRT"}`,
5354
+ `Segments: ${cues.length}`,
5355
+ ...cues.length ? [`Start: ${timestampFromMs(cues[0].start)}`, `End: ${timestampFromMs(cues[cues.length - 1].end)}`] : [],
5356
+ "",
5357
+ "## Transcript",
5358
+ "",
5359
+ ...cues.length ? cues.map((cue) => `- [${timestampFromMs(cue.start)} - ${timestampFromMs(cue.end)}] ${cue.text}`) : ["- No transcript segments were extracted."],
5360
+ ""
5361
+ ].filter((item) => Boolean(item)).join("\n");
5362
+ return {
5363
+ title,
5364
+ extractedText,
5365
+ artifact: {
5366
+ ...extractionMetadata("transcript", input.mimeType, "transcript_text"),
5367
+ metadata: {
5368
+ format: input.fileName?.toLowerCase().endsWith(".vtt") ? "vtt" : "srt",
5369
+ segment_count: String(cues.length),
5370
+ ...cues.length ? { started_at: timestampFromMs(cues[0].start), ended_at: timestampFromMs(cues[cues.length - 1].end) } : {}
5371
+ }
5372
+ }
5373
+ };
5374
+ } catch (error) {
5375
+ return {
5376
+ artifact: {
5377
+ ...extractionMetadata("transcript", input.mimeType, "transcript_text"),
5378
+ warnings: [`Transcript extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5379
+ }
5380
+ };
5381
+ }
5382
+ }
5383
+ async function extractEmailText(input) {
5384
+ try {
5385
+ const { simpleParser } = await import("mailparser");
5386
+ const fallbackTitle = input.fileName ? path7.basename(input.fileName, path7.extname(input.fileName)) : "Email";
5387
+ const parsed = await simpleParser(input.bytes);
5388
+ const normalized = normalizeParsedEmail(parsed, fallbackTitle);
5389
+ return {
5390
+ title: normalized.title,
5391
+ extractedText: normalized.markdown,
5392
+ artifact: {
5393
+ ...extractionMetadata("email", input.mimeType, "email_text"),
5394
+ metadata: normalized.metadata
5395
+ }
5396
+ };
5397
+ } catch (error) {
5398
+ return {
5399
+ artifact: {
5400
+ ...extractionMetadata("email", input.mimeType, "email_text"),
5401
+ warnings: [`Email extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5402
+ }
5403
+ };
5404
+ }
5405
+ }
5406
+ async function extractMboxMessages(input) {
5407
+ try {
5408
+ const title = input.fileName ? path7.basename(input.fileName, path7.extname(input.fileName)) : "Mailbox";
5409
+ const { simpleParser } = await import("mailparser");
5410
+ const messages = await loadZipMessageBuffers(input.bytes);
5411
+ const extracted = [];
5412
+ for (let index = 0; index < messages.length; index += 1) {
5413
+ const parsed = await simpleParser(messages[index]);
5414
+ const normalized = normalizeParsedEmail(parsed, `Message ${index + 1}`);
5415
+ const conversationId = normalized.conversationId || `${index + 1}`;
5416
+ extracted.push({
5417
+ partKey: `${conversationId}-${index + 1}`,
5418
+ title: normalized.title,
5419
+ markdown: normalized.markdown,
5420
+ metadata: {
5421
+ ...normalized.metadata,
5422
+ container_title: title,
5423
+ mailbox_title: title,
5424
+ part_index: String(index + 1),
5425
+ part_count: String(messages.length)
5426
+ }
5427
+ });
5428
+ }
5429
+ return {
5430
+ title,
5431
+ messages: extracted,
5432
+ warnings: extracted.length ? void 0 : ["Mailbox extraction completed but found no readable messages."]
5433
+ };
5434
+ } catch (error) {
5435
+ return {
5436
+ messages: [],
5437
+ warnings: [`Mailbox extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5438
+ };
5439
+ }
5440
+ }
5441
+ async function extractCalendarEvents(input) {
5442
+ try {
5443
+ const ical = await import("node-ical");
5444
+ const calendarTitle = input.fileName ? path7.basename(input.fileName, path7.extname(input.fileName)) : "Calendar";
5445
+ const parsed = ical.default.sync.parseICS(decodeTextBytes(input.bytes));
5446
+ const events = [];
5447
+ for (const item of Object.values(parsed)) {
5448
+ if (!item || typeof item !== "object" || item.type !== "VEVENT") {
5449
+ continue;
5450
+ }
5451
+ const event = item;
5452
+ const title = normalizeWhitespace(event.summary ?? "") || "Calendar Event";
5453
+ const occurredAt = normalizeIsoDate(event.start);
5454
+ const endsAt = normalizeIsoDate(event.end);
5455
+ const organizer = event.organizer ? normalizeWhitespace(String(event.organizer.params?.CN ?? event.organizer.val ?? "")) : void 0;
5456
+ const attendees = calendarAttendees(event.attendees);
5457
+ const participants = normalizeDelimitedList([organizer ?? "", ...attendees]);
5458
+ const location = normalizeWhitespace(event.location ?? "") || void 0;
5459
+ const description = normalizeDocumentText(event.description ?? "");
5460
+ const conversationId = normalizeWhitespace(event.uid ?? "") || `${title}-${occurredAt ?? events.length + 1}`;
5461
+ events.push({
5462
+ partKey: conversationId,
5463
+ title,
5464
+ metadata: {
5465
+ container_title: calendarTitle,
5466
+ ...occurredAt ? { occurred_at: occurredAt } : {},
5467
+ ...endsAt ? { ends_at: endsAt } : {},
5468
+ ...organizer ? { organizer } : {},
5469
+ ...location ? { location } : {},
5470
+ ...participants ? { participants } : {},
5471
+ conversation_id: conversationId
5472
+ },
5473
+ markdown: [
5474
+ `# ${title}`,
5475
+ "",
5476
+ ...occurredAt ? [`Start: ${occurredAt}`] : [],
5477
+ ...endsAt ? [`End: ${endsAt}`] : [],
5478
+ ...organizer ? [`Organizer: ${organizer}`] : [],
5479
+ ...attendees.length ? [`Attendees: ${attendees.join(", ")}`] : [],
5480
+ ...location ? [`Location: ${location}`] : [],
5481
+ ...conversationId ? [`Event ID: ${conversationId}`] : [],
5482
+ "",
5483
+ "## Description",
5484
+ "",
5485
+ description || "No event description was provided.",
5486
+ ""
5487
+ ].join("\n")
5488
+ });
5489
+ }
5490
+ return {
5491
+ title: calendarTitle,
5492
+ events,
5493
+ warnings: events.length ? void 0 : ["Calendar extraction completed but found no VEVENT entries."]
5494
+ };
5495
+ } catch (error) {
5496
+ return {
5497
+ events: [],
5498
+ warnings: [`Calendar extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5499
+ };
5500
+ }
5501
+ }
5502
+ function parseSlackExportEntries(entries, exportTitle) {
5503
+ const usersById = /* @__PURE__ */ new Map();
5504
+ const rawUsers = entries.get("users.json");
5505
+ if (rawUsers) {
5506
+ const parsed = JSON.parse(rawUsers);
5507
+ for (const user of parsed) {
5508
+ const id = normalizeWhitespace(user.id ?? "");
5509
+ const name = normalizeWhitespace(user.profile?.display_name ?? user.real_name ?? user.profile?.real_name ?? user.name ?? "");
5510
+ if (id && name) {
5511
+ usersById.set(id, name);
5512
+ }
5513
+ }
5514
+ }
5515
+ const channelIndex = /* @__PURE__ */ new Map();
5516
+ for (const indexPath of ["channels.json", "groups.json", "dms.json", "mpims.json"]) {
5517
+ const rawIndex = entries.get(indexPath);
5518
+ if (!rawIndex) {
5519
+ continue;
5520
+ }
5521
+ const parsed = JSON.parse(rawIndex);
5522
+ for (const [key, value] of slackEntriesFromChannelIndex(parsed, usersById)) {
5523
+ channelIndex.set(key, value);
5524
+ }
5525
+ }
5526
+ const conversationPaths = [...entries.keys()].filter((entryPath) => /^[^/]+\/\d{4}-\d{2}-\d{2}\.json$/i.test(entryPath)).sort((left, right) => left.localeCompare(right));
5527
+ const conversations = [];
5528
+ for (const entryPath of conversationPaths) {
5529
+ const raw = entries.get(entryPath);
5530
+ if (!raw) {
5531
+ continue;
5532
+ }
5533
+ const messages = JSON.parse(raw);
5534
+ if (!Array.isArray(messages)) {
5535
+ continue;
5536
+ }
5537
+ const [channelName, dateFile] = entryPath.split("/");
5538
+ const date = dateFile?.replace(/\.json$/i, "") ?? "";
5539
+ const channel = channelIndex.get(channelName ?? "") ?? {
5540
+ id: channelName ?? "",
5541
+ title: channelName ?? "channel",
5542
+ members: []
5543
+ };
5544
+ const participants = new Set(channel.members);
5545
+ const lines = [];
5546
+ const threadIds = /* @__PURE__ */ new Set();
5547
+ const sortedMessages = [...messages].sort((left, right) => Number(left.ts ?? 0) - Number(right.ts ?? 0));
5548
+ let occurredAt;
5549
+ for (const message of sortedMessages) {
5550
+ const speaker = normalizeWhitespace(
5551
+ message.username ?? message.bot_profile?.name ?? (message.user ? slackFormatSpeakerId(message.user, usersById) : "")
5552
+ ) || "unknown";
5553
+ participants.add(speaker);
5554
+ const messageTime = slackMessageTimestamp(message.ts, date);
5555
+ occurredAt ??= messageTime;
5556
+ const normalizedText = slackNormalizeText(
5557
+ [
5558
+ message.text ?? "",
5559
+ ...Array.isArray(message.files) ? message.files.map((file) => normalizeWhitespace(file.title ?? file.name ?? "")).filter(Boolean).map((label) => `Attachment: ${label}`) : []
5560
+ ].join("\n"),
5561
+ usersById
5562
+ );
5563
+ if (message.thread_ts && message.thread_ts !== message.ts) {
5564
+ threadIds.add(message.thread_ts);
5565
+ }
5566
+ lines.push(
5567
+ `- [${messageTime}] ${speaker}${message.thread_ts ? ` {thread:${message.thread_ts}}` : ""}${message.ts ? ` {id:${message.ts}}` : ""}: ${normalizedText || normalizeWhitespace(message.subtype ?? "") || "[no text]"}`
5568
+ );
5569
+ }
5570
+ const participantsList = normalizeDelimitedList([...participants]);
5571
+ const conversationId = `${channel.id || channel.title}:${date}`;
5572
+ conversations.push({
5573
+ partKey: `${channel.title}-${date}`,
5574
+ title: `#${channel.title} - ${date}`,
5575
+ metadata: {
5576
+ workspace_title: exportTitle,
5577
+ channel: channel.title,
5578
+ ...channel.id ? { channel_id: channel.id } : {},
5579
+ ...occurredAt ? { occurred_at: occurredAt } : {},
5580
+ ...participantsList ? { participants: participantsList } : {},
5581
+ container_title: `${exportTitle} / #${channel.title}`,
5582
+ conversation_id: conversationId,
5583
+ date,
5584
+ message_count: String(sortedMessages.length),
5585
+ thread_count: String(threadIds.size)
5586
+ },
5587
+ markdown: [
5588
+ `# #${channel.title} - ${date}`,
5589
+ "",
5590
+ `Workspace: ${exportTitle}`,
5591
+ `Messages: ${sortedMessages.length}`,
5592
+ `Threads: ${threadIds.size}`,
5593
+ ...participantsList ? [`Participants: ${participantsList}`] : [],
5594
+ "",
5595
+ "## Messages",
5596
+ "",
5597
+ ...lines.length ? lines : ["- No messages were extracted."],
5598
+ ""
5599
+ ].join("\n")
5600
+ });
5601
+ }
5602
+ return {
5603
+ title: exportTitle,
5604
+ conversations,
5605
+ warnings: conversations.length ? void 0 : ["Slack export parsing completed but found no channel day files."]
5606
+ };
5607
+ }
5608
+ function isSlackExportArchive(bytes) {
5609
+ try {
5610
+ const archive = unzipSync(new Uint8Array(bytes));
5611
+ return looksLikeSlackEntries(Object.keys(archive));
5612
+ } catch {
5613
+ return false;
5614
+ }
5615
+ }
5616
+ async function isSlackExportDirectory(directoryPath) {
5617
+ const entries = await fs7.readdir(directoryPath).catch(() => []);
5618
+ if (!entries.length) {
5619
+ return false;
5620
+ }
5621
+ const fileSet = new Set(entries);
5622
+ const hasIndex = ["channels.json", "groups.json", "dms.json", "mpims.json"].some((name) => fileSet.has(name));
5623
+ if (!hasIndex) {
5624
+ return false;
5625
+ }
5626
+ for (const entry of entries) {
5627
+ const channelDir = path7.join(directoryPath, entry);
5628
+ const stat = await fs7.stat(channelDir).catch(() => null);
5629
+ if (!stat?.isDirectory()) {
5630
+ continue;
5631
+ }
5632
+ const channelEntries = await fs7.readdir(channelDir).catch(() => []);
5633
+ if (channelEntries.some((name) => /^\d{4}-\d{2}-\d{2}\.json$/i.test(name))) {
5634
+ return true;
5635
+ }
5636
+ }
5637
+ return false;
5638
+ }
5639
+ async function extractSlackExportArchive(input) {
5640
+ try {
5641
+ const archive = unzipSync(new Uint8Array(input.bytes));
5642
+ const title = input.fileName ? path7.basename(input.fileName, path7.extname(input.fileName)) : "Slack Export";
5643
+ return parseSlackExportEntries(archiveEntriesAsText(archive), title);
5644
+ } catch (error) {
5645
+ return {
5646
+ conversations: [],
5647
+ warnings: [`Slack export extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5648
+ };
5649
+ }
5650
+ }
5651
+ async function extractSlackExportDirectory(directoryPath) {
5652
+ const title = path7.basename(directoryPath) || "Slack Export";
5653
+ try {
5654
+ const entries = /* @__PURE__ */ new Map();
5655
+ const queue = [directoryPath];
5656
+ while (queue.length > 0) {
5657
+ const current = queue.shift();
5658
+ const children = await fs7.readdir(current, { withFileTypes: true });
5659
+ for (const child of children) {
5660
+ const absoluteChild = path7.join(current, child.name);
5661
+ if (child.isDirectory()) {
5662
+ queue.push(absoluteChild);
5663
+ continue;
5664
+ }
5665
+ const relativeChild = path7.posix.relative(directoryPath, absoluteChild.split(path7.sep).join(path7.posix.sep));
5666
+ entries.set(relativeChild, await fs7.readFile(absoluteChild, "utf8"));
5667
+ }
5668
+ }
5669
+ return parseSlackExportEntries(entries, title);
5670
+ } catch (error) {
5671
+ return {
5672
+ conversations: [],
5673
+ warnings: [`Slack export extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5674
+ };
5675
+ }
5676
+ }
5174
5677
 
5175
5678
  // src/logs.ts
5176
5679
  import fs8 from "fs/promises";
@@ -5600,6 +6103,9 @@ function inferKind(mimeType, filePath) {
5600
6103
  if (isRstFilePath(filePath)) {
5601
6104
  return "text";
5602
6105
  }
6106
+ if (isTranscriptFilePath(filePath) || mimeType === "application/x-subrip" || mimeType === "text/vtt") {
6107
+ return "transcript";
6108
+ }
5603
6109
  if (mimeType.includes("markdown")) {
5604
6110
  return "markdown";
5605
6111
  }
@@ -5612,6 +6118,12 @@ function inferKind(mimeType, filePath) {
5612
6118
  if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || filePath.toLowerCase().endsWith(".docx")) {
5613
6119
  return "docx";
5614
6120
  }
6121
+ if (isEmailFilePath(filePath) || mimeType === "message/rfc822" || mimeType === "application/mbox") {
6122
+ return "email";
6123
+ }
6124
+ if (isCalendarFilePath(filePath) || mimeType === "text/calendar") {
6125
+ return "calendar";
6126
+ }
5615
6127
  if (mimeType === "application/epub+zip" || filePath.toLowerCase().endsWith(".epub")) {
5616
6128
  return "epub";
5617
6129
  }
@@ -5636,6 +6148,17 @@ function isRstFilePath(filePath) {
5636
6148
  const extension = path12.extname(filePath).toLowerCase();
5637
6149
  return extension === ".rst" || extension === ".rest";
5638
6150
  }
6151
+ function isTranscriptFilePath(filePath) {
6152
+ const extension = path12.extname(filePath).toLowerCase();
6153
+ return extension === ".srt" || extension === ".vtt";
6154
+ }
6155
+ function isEmailFilePath(filePath) {
6156
+ const extension = path12.extname(filePath).toLowerCase();
6157
+ return extension === ".eml" || extension === ".mbox";
6158
+ }
6159
+ function isCalendarFilePath(filePath) {
6160
+ return path12.extname(filePath).toLowerCase() === ".ics";
6161
+ }
5639
6162
  function titleFromText(fallback, content, filePath) {
5640
6163
  if (filePath && isRstFilePath(filePath)) {
5641
6164
  const rstTitle = titleFromRst(fallback, content);
@@ -5656,6 +6179,53 @@ function sourceGroupIdFor(prepared) {
5656
6179
  const originKey = prepared.originType === "url" ? prepared.url ?? prepared.title : prepared.originalPath ?? prepared.title;
5657
6180
  return `${slugify(prepared.title)}-${sha256(originKey).slice(0, 8)}`;
5658
6181
  }
6182
+ function groupedPreparedInputsFor(input) {
6183
+ const groupId = sourceGroupIdFor({
6184
+ title: input.title,
6185
+ originType: input.originType,
6186
+ originalPath: input.originalPath,
6187
+ url: input.url
6188
+ });
6189
+ return input.parts.map(
6190
+ (part, index) => finalizePreparedInput({
6191
+ title: `${input.title} - ${part.title}`,
6192
+ originType: input.originType,
6193
+ sourceKind: input.sourceKind,
6194
+ sourceClass: input.sourceClass,
6195
+ originalPath: input.originalPath,
6196
+ repoRelativePath: input.repoRelativePath,
6197
+ url: input.url,
6198
+ mimeType: "text/markdown",
6199
+ storedExtension: input.storedExtension,
6200
+ payloadBytes: Buffer.from(part.markdown, "utf8"),
6201
+ extractedText: part.markdown,
6202
+ extractionArtifact: {
6203
+ extractor: `${input.sourceKind}_text`,
6204
+ sourceKind: input.sourceKind,
6205
+ mimeType: input.mimeType,
6206
+ producedAt: (/* @__PURE__ */ new Date()).toISOString(),
6207
+ metadata: {
6208
+ ...part.metadata,
6209
+ part_index: String(index + 1),
6210
+ part_count: String(input.parts.length)
6211
+ },
6212
+ warnings: input.warnings
6213
+ },
6214
+ sourceGroupId: groupId,
6215
+ sourceGroupTitle: input.title,
6216
+ sourcePartKey: part.partKey,
6217
+ partIndex: index + 1,
6218
+ partCount: input.parts.length,
6219
+ partTitle: part.title,
6220
+ details: {
6221
+ ...part.metadata,
6222
+ part_index: String(index + 1),
6223
+ part_count: String(input.parts.length)
6224
+ },
6225
+ logDetails: input.logDetails
6226
+ })
6227
+ );
6228
+ }
5659
6229
  function rstAdornmentLine(line) {
5660
6230
  const trimmed = line.trim();
5661
6231
  if (trimmed.length < 3) {
@@ -5886,6 +6456,13 @@ async function findNearestGitRoot2(startPath) {
5886
6456
  current = parent;
5887
6457
  }
5888
6458
  }
6459
+ async function detectScopedRepoRoot(rootDir, inputPath, fallbackRoot) {
6460
+ const detectedRepoRoot = await findNearestGitRoot2(inputPath);
6461
+ if (!detectedRepoRoot) {
6462
+ return fallbackRoot;
6463
+ }
6464
+ return withinRoot(rootDir, inputPath) && !withinRoot(rootDir, detectedRepoRoot) ? fallbackRoot : detectedRepoRoot;
6465
+ }
5889
6466
  function withinRoot(rootPath, targetPath) {
5890
6467
  const relative = path12.relative(rootPath, targetPath);
5891
6468
  return relative === "" || !relative.startsWith("..") && !path12.isAbsolute(relative);
@@ -6440,7 +7017,13 @@ async function collectDirectoryFiles(rootDir, inputDir, repoRoot, options) {
6440
7017
  continue;
6441
7018
  }
6442
7019
  const mimeType = guessMimeType(absolutePath);
6443
- const sourceKind = inferKind(mimeType, absolutePath);
7020
+ let sourceKind = inferKind(mimeType, absolutePath);
7021
+ if (sourceKind === "binary" && path12.extname(absolutePath).toLowerCase() === ".zip") {
7022
+ const bytes = await fs11.readFile(absolutePath);
7023
+ if (isSlackExportArchive(bytes)) {
7024
+ sourceKind = "chat_export";
7025
+ }
7026
+ }
6444
7027
  const sourceClass = sourceClassForRelativePath(relativePath, options);
6445
7028
  if (!supportedDirectoryKind(sourceKind)) {
6446
7029
  skipped.push({ path: toPosix(path12.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
@@ -6775,7 +7358,7 @@ function preparedMatchesManifest(manifest, prepared, contentHash) {
6775
7358
  return manifest.contentHash === contentHash && manifest.extractionHash === (prepared.extractionHash ?? buildExtractionHash(prepared.extractedText, prepared.extractionArtifact)) && manifest.semanticHash === (prepared.semanticHash ?? contentHash) && manifest.title === prepared.title && manifest.sourceKind === prepared.sourceKind && manifest.sourceType === prepared.sourceType && manifest.sourceClass === prepared.sourceClass && manifest.language === prepared.language && manifest.mimeType === prepared.mimeType && manifest.repoRelativePath === prepared.repoRelativePath && manifest.sourceGroupId === prepared.sourceGroupId && manifest.sourceGroupTitle === prepared.sourceGroupTitle && manifest.sourcePartKey === prepared.sourcePartKey && manifest.partIndex === prepared.partIndex && manifest.partCount === prepared.partCount && manifest.partTitle === prepared.partTitle && JSON.stringify(manifest.details ?? {}) === JSON.stringify(prepared.details ?? {});
6776
7359
  }
6777
7360
  function shouldDeferWatchSemanticRefresh(sourceKind) {
6778
- return sourceKind === "markdown" || sourceKind === "text" || sourceKind === "html" || sourceKind === "pdf" || sourceKind === "docx" || sourceKind === "epub" || sourceKind === "csv" || sourceKind === "xlsx" || sourceKind === "pptx" || sourceKind === "image";
7361
+ return sourceKind === "markdown" || sourceKind === "text" || sourceKind === "html" || sourceKind === "pdf" || sourceKind === "docx" || sourceKind === "epub" || sourceKind === "csv" || sourceKind === "xlsx" || sourceKind === "pptx" || sourceKind === "transcript" || sourceKind === "chat_export" || sourceKind === "email" || sourceKind === "calendar" || sourceKind === "image";
6779
7362
  }
6780
7363
  function pendingSemanticRefreshId(changeType, repoRoot, relativePath) {
6781
7364
  return `pending:${changeType}:${sha256(`${toPosix(repoRoot)}:${relativePath}`).slice(0, 12)}`;
@@ -7043,6 +7626,23 @@ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
7043
7626
  }
7044
7627
  async function prepareFileInputs(rootDir, absoluteInput, repoRoot, sourceClass) {
7045
7628
  const payloadBytes = await fs11.readFile(absoluteInput);
7629
+ if (path12.extname(absoluteInput).toLowerCase() === ".zip" && isSlackExportArchive(payloadBytes)) {
7630
+ const slackExport = await extractSlackExportArchive({ mimeType: "application/zip", bytes: payloadBytes, fileName: absoluteInput });
7631
+ if (slackExport.conversations.length) {
7632
+ return groupedPreparedInputsFor({
7633
+ title: slackExport.title?.trim() || path12.basename(absoluteInput, path12.extname(absoluteInput)),
7634
+ originType: "file",
7635
+ sourceKind: "chat_export",
7636
+ sourceClass,
7637
+ originalPath: toPosix(absoluteInput),
7638
+ repoRelativePath: repoRelativePathFor(absoluteInput, repoRoot),
7639
+ mimeType: "application/zip",
7640
+ storedExtension: ".md",
7641
+ warnings: slackExport.warnings,
7642
+ parts: slackExport.conversations
7643
+ });
7644
+ }
7645
+ }
7046
7646
  const mimeType = guessMimeType(absoluteInput);
7047
7647
  const sourceKind = inferKind(mimeType, absoluteInput);
7048
7648
  const language = inferCodeLanguage(absoluteInput, mimeType);
@@ -7072,6 +7672,68 @@ async function prepareFileInputs(rootDir, absoluteInput, repoRoot, sourceClass)
7072
7672
  title = extracted.artifact.metadata?.title?.trim() || title;
7073
7673
  extractedText = extracted.extractedText;
7074
7674
  extractionArtifact = extracted.artifact;
7675
+ } else if (sourceKind === "transcript") {
7676
+ title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7677
+ const extracted = await extractTranscriptText({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
7678
+ title = extracted.title?.trim() || title;
7679
+ extractedText = extracted.extractedText;
7680
+ extractionArtifact = extracted.artifact;
7681
+ } else if (sourceKind === "email" && path12.extname(absoluteInput).toLowerCase() === ".eml") {
7682
+ title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7683
+ const extracted = await extractEmailText({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
7684
+ title = extracted.title?.trim() || title;
7685
+ extractedText = extracted.extractedText;
7686
+ extractionArtifact = extracted.artifact;
7687
+ } else if (sourceKind === "email" && path12.extname(absoluteInput).toLowerCase() === ".mbox") {
7688
+ title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7689
+ const extracted = await extractMboxMessages({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
7690
+ title = extracted.title?.trim() || title;
7691
+ if (extracted.messages.length) {
7692
+ return groupedPreparedInputsFor({
7693
+ title,
7694
+ originType: "file",
7695
+ sourceKind: "email",
7696
+ sourceClass,
7697
+ originalPath: toPosix(absoluteInput),
7698
+ repoRelativePath: repoRelativePathFor(absoluteInput, repoRoot),
7699
+ mimeType,
7700
+ storedExtension: ".md",
7701
+ warnings: extracted.warnings,
7702
+ parts: extracted.messages
7703
+ });
7704
+ }
7705
+ extractionArtifact = {
7706
+ extractor: "email_text",
7707
+ sourceKind: "email",
7708
+ mimeType,
7709
+ producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7710
+ warnings: extracted.warnings ?? ["Mailbox extraction completed but produced no readable messages."]
7711
+ };
7712
+ } else if (sourceKind === "calendar") {
7713
+ title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7714
+ const extracted = await extractCalendarEvents({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
7715
+ title = extracted.title?.trim() || title;
7716
+ if (extracted.events.length) {
7717
+ return groupedPreparedInputsFor({
7718
+ title,
7719
+ originType: "file",
7720
+ sourceKind: "calendar",
7721
+ sourceClass,
7722
+ originalPath: toPosix(absoluteInput),
7723
+ repoRelativePath: repoRelativePathFor(absoluteInput, repoRoot),
7724
+ mimeType,
7725
+ storedExtension: ".md",
7726
+ warnings: extracted.warnings,
7727
+ parts: extracted.events
7728
+ });
7729
+ }
7730
+ extractionArtifact = {
7731
+ extractor: "calendar_text",
7732
+ sourceKind: "calendar",
7733
+ mimeType,
7734
+ producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7735
+ warnings: extracted.warnings ?? ["Calendar extraction completed but found no events."]
7736
+ };
7075
7737
  } else if (sourceKind === "csv") {
7076
7738
  title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7077
7739
  const extracted = await extractCsvText({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
@@ -7094,63 +7756,25 @@ async function prepareFileInputs(rootDir, absoluteInput, repoRoot, sourceClass)
7094
7756
  title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7095
7757
  const extracted = await extractEpubChapters({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
7096
7758
  title = extracted.title?.trim() || title;
7097
- const groupId = sourceGroupIdFor({
7098
- title,
7099
- originType: "file",
7100
- originalPath: toPosix(absoluteInput)
7101
- });
7102
7759
  if (extracted.chapters.length) {
7103
- return extracted.chapters.map(
7104
- (chapter, index) => finalizePreparedInput({
7105
- title: `${title} - ${chapter.title}`,
7106
- originType: "file",
7107
- sourceKind: "epub",
7108
- sourceClass,
7109
- originalPath: toPosix(absoluteInput),
7110
- repoRelativePath: repoRelativePathFor(absoluteInput, repoRoot),
7111
- mimeType: "text/markdown",
7112
- storedExtension: ".md",
7113
- payloadBytes: Buffer.from(chapter.markdown, "utf8"),
7114
- extractedText: chapter.markdown,
7115
- extractionArtifact: {
7116
- extractor: "epub_text",
7117
- sourceKind: "epub",
7118
- mimeType,
7119
- producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7120
- metadata: {
7121
- ...chapter.metadata,
7122
- chapter_index: String(index + 1),
7123
- chapter_count: String(extracted.chapters.length)
7124
- },
7125
- warnings: extracted.warnings
7126
- },
7127
- extractionHash: buildExtractionHash(chapter.markdown, {
7128
- extractor: "epub_text",
7129
- sourceKind: "epub",
7130
- mimeType,
7131
- producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7132
- metadata: {
7133
- ...chapter.metadata,
7134
- chapter_index: String(index + 1),
7135
- chapter_count: String(extracted.chapters.length)
7136
- },
7137
- warnings: extracted.warnings
7138
- }),
7139
- sourceGroupId: groupId,
7140
- sourceGroupTitle: title,
7141
- sourcePartKey: chapter.partKey,
7142
- partIndex: index + 1,
7143
- partCount: extracted.chapters.length,
7144
- partTitle: chapter.title,
7145
- details: {
7146
- book_title: title,
7147
- chapter_title: chapter.title,
7148
- chapter_index: String(index + 1),
7149
- chapter_count: String(extracted.chapters.length),
7760
+ return groupedPreparedInputsFor({
7761
+ title,
7762
+ originType: "file",
7763
+ sourceKind: "epub",
7764
+ sourceClass,
7765
+ originalPath: toPosix(absoluteInput),
7766
+ repoRelativePath: repoRelativePathFor(absoluteInput, repoRoot),
7767
+ mimeType,
7768
+ storedExtension: ".md",
7769
+ warnings: extracted.warnings,
7770
+ parts: extracted.chapters.map((chapter) => ({
7771
+ ...chapter,
7772
+ metadata: {
7773
+ ...chapter.metadata,
7150
7774
  ...extracted.author ? { author: extracted.author } : {}
7151
7775
  }
7152
- })
7153
- );
7776
+ }))
7777
+ });
7154
7778
  }
7155
7779
  extractedText = void 0;
7156
7780
  extractionArtifact = {
@@ -7208,6 +7832,25 @@ async function prepareUrlInputs(rootDir, input, options) {
7208
7832
  const finalUrl = normalizeOriginUrl(response.url || input);
7209
7833
  const inputUrl = new URL(finalUrl);
7210
7834
  const originalPayloadBytes = Buffer.from(await response.arrayBuffer());
7835
+ if (path12.extname(inputUrl.pathname).toLowerCase() === ".zip" && isSlackExportArchive(originalPayloadBytes)) {
7836
+ const slackExport = await extractSlackExportArchive({
7837
+ mimeType: "application/zip",
7838
+ bytes: originalPayloadBytes,
7839
+ fileName: inputUrl.pathname
7840
+ });
7841
+ if (slackExport.conversations.length) {
7842
+ return groupedPreparedInputsFor({
7843
+ title: slackExport.title?.trim() || inputUrl.hostname,
7844
+ originType: "url",
7845
+ sourceKind: "chat_export",
7846
+ url: finalUrl,
7847
+ mimeType: "application/zip",
7848
+ storedExtension: ".md",
7849
+ warnings: slackExport.warnings,
7850
+ parts: slackExport.conversations
7851
+ });
7852
+ }
7853
+ }
7211
7854
  let payloadBytes = originalPayloadBytes;
7212
7855
  let mimeType = resolveUrlMimeType(input, response);
7213
7856
  let sourceKind = inferKind(mimeType, inputUrl.pathname);
@@ -7294,6 +7937,60 @@ async function prepareUrlInputs(rootDir, input, options) {
7294
7937
  title = extracted.artifact.metadata?.title?.trim() || title;
7295
7938
  extractedText = extracted.extractedText;
7296
7939
  extractionArtifact = extracted.artifact;
7940
+ } else if (sourceKind === "transcript") {
7941
+ const extracted = await extractTranscriptText({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7942
+ title = extracted.title?.trim() || title;
7943
+ extractedText = extracted.extractedText;
7944
+ extractionArtifact = extracted.artifact;
7945
+ } else if (sourceKind === "email" && path12.extname(inputUrl.pathname).toLowerCase() === ".eml") {
7946
+ const extracted = await extractEmailText({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7947
+ title = extracted.title?.trim() || title;
7948
+ extractedText = extracted.extractedText;
7949
+ extractionArtifact = extracted.artifact;
7950
+ } else if (sourceKind === "email" && path12.extname(inputUrl.pathname).toLowerCase() === ".mbox") {
7951
+ const extracted = await extractMboxMessages({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7952
+ title = extracted.title?.trim() || title;
7953
+ if (extracted.messages.length) {
7954
+ return groupedPreparedInputsFor({
7955
+ title,
7956
+ originType: "url",
7957
+ sourceKind: "email",
7958
+ url: finalUrl,
7959
+ mimeType,
7960
+ storedExtension: ".md",
7961
+ warnings: extracted.warnings,
7962
+ parts: extracted.messages
7963
+ });
7964
+ }
7965
+ extractionArtifact = {
7966
+ extractor: "email_text",
7967
+ sourceKind: "email",
7968
+ mimeType,
7969
+ producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7970
+ warnings: extracted.warnings ?? ["Mailbox extraction completed but produced no readable messages."]
7971
+ };
7972
+ } else if (sourceKind === "calendar") {
7973
+ const extracted = await extractCalendarEvents({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7974
+ title = extracted.title?.trim() || title;
7975
+ if (extracted.events.length) {
7976
+ return groupedPreparedInputsFor({
7977
+ title,
7978
+ originType: "url",
7979
+ sourceKind: "calendar",
7980
+ url: finalUrl,
7981
+ mimeType,
7982
+ storedExtension: ".md",
7983
+ warnings: extracted.warnings,
7984
+ parts: extracted.events
7985
+ });
7986
+ }
7987
+ extractionArtifact = {
7988
+ extractor: "calendar_text",
7989
+ sourceKind: "calendar",
7990
+ mimeType,
7991
+ producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7992
+ warnings: extracted.warnings ?? ["Calendar extraction completed but found no events."]
7993
+ };
7297
7994
  } else if (sourceKind === "csv") {
7298
7995
  const extracted = await extractCsvText({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7299
7996
  title = extracted.title?.trim() || title;
@@ -7312,62 +8009,24 @@ async function prepareUrlInputs(rootDir, input, options) {
7312
8009
  } else if (sourceKind === "epub") {
7313
8010
  const extracted = await extractEpubChapters({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7314
8011
  title = extracted.title?.trim() || title;
7315
- const groupId = sourceGroupIdFor({
7316
- title,
7317
- originType: "url",
7318
- url: finalUrl
7319
- });
7320
8012
  if (extracted.chapters.length) {
7321
- return extracted.chapters.map(
7322
- (chapter, index) => finalizePreparedInput({
7323
- title: `${title} - ${chapter.title}`,
7324
- originType: "url",
7325
- sourceKind: "epub",
7326
- url: finalUrl,
7327
- mimeType: "text/markdown",
7328
- storedExtension: ".md",
7329
- payloadBytes: Buffer.from(chapter.markdown, "utf8"),
7330
- extractedText: chapter.markdown,
7331
- extractionArtifact: {
7332
- extractor: "epub_text",
7333
- sourceKind: "epub",
7334
- mimeType,
7335
- producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7336
- metadata: {
7337
- ...chapter.metadata,
7338
- chapter_index: String(index + 1),
7339
- chapter_count: String(extracted.chapters.length)
7340
- },
7341
- warnings: extracted.warnings
7342
- },
7343
- extractionHash: buildExtractionHash(chapter.markdown, {
7344
- extractor: "epub_text",
7345
- sourceKind: "epub",
7346
- mimeType,
7347
- producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7348
- metadata: {
7349
- ...chapter.metadata,
7350
- chapter_index: String(index + 1),
7351
- chapter_count: String(extracted.chapters.length)
7352
- },
7353
- warnings: extracted.warnings
7354
- }),
7355
- sourceGroupId: groupId,
7356
- sourceGroupTitle: title,
7357
- sourcePartKey: chapter.partKey,
7358
- partIndex: index + 1,
7359
- partCount: extracted.chapters.length,
7360
- partTitle: chapter.title,
7361
- details: {
7362
- book_title: title,
7363
- chapter_title: chapter.title,
7364
- chapter_index: String(index + 1),
7365
- chapter_count: String(extracted.chapters.length),
8013
+ return groupedPreparedInputsFor({
8014
+ title,
8015
+ originType: "url",
8016
+ sourceKind: "epub",
8017
+ url: finalUrl,
8018
+ mimeType,
8019
+ storedExtension: ".md",
8020
+ warnings: extracted.warnings,
8021
+ parts: extracted.chapters.map((chapter) => ({
8022
+ ...chapter,
8023
+ metadata: {
8024
+ ...chapter.metadata,
7366
8025
  ...extracted.author ? { author: extracted.author } : {}
7367
- },
7368
- logDetails
7369
- })
7370
- );
8026
+ }
8027
+ })),
8028
+ logDetails
8029
+ });
7371
8030
  }
7372
8031
  extractionArtifact = {
7373
8032
  extractor: "epub_text",
@@ -7544,13 +8203,28 @@ async function prepareInboxHtmlInput(absolutePath, attachmentRefs) {
7544
8203
  };
7545
8204
  }
7546
8205
  function isSupportedInboxKind(sourceKind) {
7547
- return ["markdown", "text", "html", "pdf", "docx", "epub", "csv", "xlsx", "pptx", "image"].includes(sourceKind);
8206
+ return [
8207
+ "markdown",
8208
+ "text",
8209
+ "html",
8210
+ "pdf",
8211
+ "docx",
8212
+ "epub",
8213
+ "csv",
8214
+ "xlsx",
8215
+ "pptx",
8216
+ "transcript",
8217
+ "chat_export",
8218
+ "email",
8219
+ "calendar",
8220
+ "image"
8221
+ ].includes(sourceKind);
7548
8222
  }
7549
8223
  async function ingestInputDetailed(rootDir, input, options) {
7550
8224
  const { paths } = await initWorkspace(rootDir);
7551
8225
  const normalizedOptions = normalizeIngestOptions(options);
7552
8226
  const absoluteInput = path12.resolve(rootDir, input);
7553
- const repoRoot = isHttpUrl(input) || normalizedOptions.repoRoot ? normalizedOptions.repoRoot : await findNearestGitRoot2(absoluteInput).then((value) => value ?? path12.dirname(absoluteInput));
8227
+ const repoRoot = isHttpUrl(input) || normalizedOptions.repoRoot ? normalizedOptions.repoRoot : await detectScopedRepoRoot(rootDir, absoluteInput, path12.dirname(absoluteInput));
7554
8228
  const prepared = isHttpUrl(input) ? await prepareUrlInputs(rootDir, input, normalizedOptions) : await prepareFileInputs(rootDir, absoluteInput, repoRoot);
7555
8229
  return await persistPreparedInputs(rootDir, input, prepared, paths);
7556
8230
  }
@@ -7648,10 +8322,39 @@ async function ingestDirectory(rootDir, inputDir, options) {
7648
8322
  const { paths } = await initWorkspace(rootDir);
7649
8323
  const normalizedOptions = await resolveRepoIngestOptions(rootDir, options);
7650
8324
  const absoluteInputDir = path12.resolve(rootDir, inputDir);
7651
- const repoRoot = normalizedOptions.repoRoot ?? await findNearestGitRoot2(absoluteInputDir) ?? absoluteInputDir;
8325
+ const repoRoot = normalizedOptions.repoRoot ?? await detectScopedRepoRoot(rootDir, absoluteInputDir, absoluteInputDir);
7652
8326
  if (!await fileExists(absoluteInputDir)) {
7653
8327
  throw new Error(`Directory not found: ${absoluteInputDir}`);
7654
8328
  }
8329
+ if (await isSlackExportDirectory(absoluteInputDir)) {
8330
+ const extracted = await extractSlackExportDirectory(absoluteInputDir);
8331
+ const preparedInputs = groupedPreparedInputsFor({
8332
+ title: extracted.title?.trim() || path12.basename(absoluteInputDir),
8333
+ originType: "file",
8334
+ sourceKind: "chat_export",
8335
+ originalPath: toPosix(absoluteInputDir),
8336
+ mimeType: "application/json",
8337
+ storedExtension: ".md",
8338
+ warnings: extracted.warnings,
8339
+ parts: extracted.conversations
8340
+ });
8341
+ const result = await persistPreparedInputs(rootDir, absoluteInputDir, preparedInputs, paths);
8342
+ await appendLogEntry(rootDir, "ingest_directory", toPosix(path12.relative(rootDir, absoluteInputDir)) || ".", [
8343
+ `repo_root=${toPosix(path12.relative(rootDir, repoRoot)) || "."}`,
8344
+ `scanned=${preparedInputs.length}`,
8345
+ `imported=${result.created.length}`,
8346
+ `updated=${result.updated.length}`,
8347
+ `skipped=${result.skipped.length}`
8348
+ ]);
8349
+ return {
8350
+ inputDir: absoluteInputDir,
8351
+ repoRoot,
8352
+ scannedCount: preparedInputs.length,
8353
+ imported: result.created,
8354
+ updated: result.updated,
8355
+ skipped: result.skipped
8356
+ };
8357
+ }
7655
8358
  const { files, skipped } = await collectDirectoryFiles(rootDir, absoluteInputDir, repoRoot, normalizedOptions);
7656
8359
  const imported = [];
7657
8360
  const updated = [];
@@ -7716,7 +8419,13 @@ async function importInbox(rootDir, inputDir) {
7716
8419
  continue;
7717
8420
  }
7718
8421
  const mimeType = guessMimeType(absolutePath);
7719
- const sourceKind = inferKind(mimeType, absolutePath);
8422
+ let sourceKind = inferKind(mimeType, absolutePath);
8423
+ if (sourceKind === "binary" && path12.extname(absolutePath).toLowerCase() === ".zip") {
8424
+ const bytes = await fs11.readFile(absolutePath);
8425
+ if (isSlackExportArchive(bytes)) {
8426
+ sourceKind = "chat_export";
8427
+ }
8428
+ }
7720
8429
  if (!isSupportedInboxKind(sourceKind)) {
7721
8430
  skipped.push({ path: toPosix(path12.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
7722
8431
  continue;
@@ -9945,6 +10654,19 @@ function relatedOutputsSection(relatedOutputs) {
9945
10654
  }
9946
10655
  return ["## Related Outputs", "", ...relatedOutputs.map((page) => `- ${pageLink(page)}`), ""];
9947
10656
  }
10657
+ function detailValue(manifest, key) {
10658
+ const value = manifest.details?.[key];
10659
+ const normalized = typeof value === "string" ? value.trim() : "";
10660
+ return normalized || void 0;
10661
+ }
10662
+ function detailList(manifest, key) {
10663
+ const value = detailValue(manifest, key);
10664
+ if (!value) {
10665
+ return void 0;
10666
+ }
10667
+ const items = value.split(",").map((item) => item.trim()).filter(Boolean);
10668
+ return items.length ? items : void 0;
10669
+ }
9948
10670
  function buildSourcePage(manifest, analysis, schemaHash, metadata, relatedOutputs = [], modulePage, decorations) {
9949
10671
  const relativePath = pagePathFor("source", manifest.sourceId);
9950
10672
  const pageId = `source:${manifest.sourceId}`;
@@ -9968,6 +10690,10 @@ function buildSourcePage(manifest, analysis, schemaHash, metadata, relatedOutput
9968
10690
  title: analysis.title,
9969
10691
  ...manifest.sourceType ? { source_type: manifest.sourceType } : {},
9970
10692
  ...manifest.sourceClass ? { source_class: manifest.sourceClass } : {},
10693
+ ...detailValue(manifest, "occurred_at") ? { occurred_at: detailValue(manifest, "occurred_at") } : {},
10694
+ ...detailList(manifest, "participants") ? { participants: detailList(manifest, "participants") } : {},
10695
+ ...detailValue(manifest, "container_title") ? { container_title: detailValue(manifest, "container_title") } : {},
10696
+ ...detailValue(manifest, "conversation_id") ? { conversation_id: detailValue(manifest, "conversation_id") } : {},
9971
10697
  tags: decoratedTags(analysis.code ? ["source", "code"] : ["source"], decorations),
9972
10698
  source_ids: [manifest.sourceId],
9973
10699
  project_ids: decorations?.projectIds ?? [],
@@ -10300,6 +11026,9 @@ function buildIndexPage(pages, schemaHash, metadata, projectPages = []) {
10300
11026
  const outputs = pages.filter((page) => page.kind === "output");
10301
11027
  const insights = pages.filter((page) => page.kind === "insight");
10302
11028
  const graphPages = pages.filter((page) => page.kind === "graph_report" || page.kind === "community_summary");
11029
+ const dashboards = pages.filter(
11030
+ (page) => page.kind === "index" && page.path.startsWith("dashboards/") && page.path !== "dashboards/index.md"
11031
+ );
10303
11032
  return [
10304
11033
  "---",
10305
11034
  "page_id: index",
@@ -10345,6 +11074,10 @@ function buildIndexPage(pages, schemaHash, metadata, projectPages = []) {
10345
11074
  "",
10346
11075
  ...outputs.length ? outputs.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No saved outputs yet."],
10347
11076
  "",
11077
+ "## Dashboards",
11078
+ "",
11079
+ ...dashboards.length ? dashboards.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No dashboards yet."],
11080
+ "",
10348
11081
  "## Graph",
10349
11082
  "",
10350
11083
  ...graphPages.length ? graphPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No graph reports yet."],
@@ -11821,15 +12554,37 @@ async function rebuildSearchIndex(dbPath, pages, wikiDir) {
11821
12554
  const insertPage = db.prepare(
11822
12555
  "INSERT INTO pages (id, path, title, body, kind, status, source_type, source_class, project_ids, project_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
11823
12556
  );
12557
+ const rootDir = path21.dirname(wikiDir);
11824
12558
  for (const page of pages) {
11825
12559
  const absolutePath = path21.join(wikiDir, page.path);
11826
12560
  const content = await fs17.readFile(absolutePath, "utf8");
11827
12561
  const parsed = matter8(content);
12562
+ let body = parsed.content;
12563
+ const primarySourceId = Array.isArray(parsed.data.source_ids) && typeof parsed.data.source_ids[0] === "string" ? parsed.data.source_ids[0] : page.sourceIds[0];
12564
+ if ((page.kind === "source" || page.kind === "module") && primarySourceId) {
12565
+ try {
12566
+ const manifest = JSON.parse(
12567
+ await fs17.readFile(path21.join(rootDir, "state", "manifests", `${primarySourceId}.json`), "utf8")
12568
+ );
12569
+ const excerptPath = manifest.extractedTextPath ?? manifest.storedPath;
12570
+ if (excerptPath) {
12571
+ const excerpt = await fs17.readFile(path21.join(rootDir, excerptPath), "utf8");
12572
+ if (excerpt.trim()) {
12573
+ body = `${body}
12574
+
12575
+ ## Source Excerpt
12576
+
12577
+ ${excerpt.trim()}`.trim();
12578
+ }
12579
+ }
12580
+ } catch {
12581
+ }
12582
+ }
11828
12583
  insertPage.run(
11829
12584
  page.id,
11830
12585
  page.path,
11831
12586
  page.title,
11832
- parsed.content,
12587
+ body,
11833
12588
  page.kind,
11834
12589
  page.status,
11835
12590
  typeof parsed.data.source_type === "string" ? parsed.data.source_type : "",
@@ -11890,7 +12645,25 @@ function searchPages(dbPath, query, limitOrOptions = 5) {
11890
12645
  FROM page_search
11891
12646
  JOIN pages ON pages.rowid = page_search.rowid
11892
12647
  WHERE ${clauses.join(" AND ")}
11893
- ORDER BY rank
12648
+ ORDER BY
12649
+ CASE pages.status
12650
+ WHEN 'active' THEN 0
12651
+ WHEN 'draft' THEN 1
12652
+ WHEN 'candidate' THEN 2
12653
+ ELSE 3
12654
+ END,
12655
+ CASE pages.kind
12656
+ WHEN 'source' THEN 0
12657
+ WHEN 'module' THEN 1
12658
+ WHEN 'output' THEN 2
12659
+ WHEN 'insight' THEN 3
12660
+ WHEN 'graph_report' THEN 4
12661
+ WHEN 'community_summary' THEN 5
12662
+ WHEN 'concept' THEN 6
12663
+ WHEN 'entity' THEN 7
12664
+ ELSE 8
12665
+ END,
12666
+ rank
11894
12667
  LIMIT ?
11895
12668
  `);
11896
12669
  params.push(options.limit ?? 5);
@@ -12112,7 +12885,7 @@ async function resolveImageGenerationProvider(rootDir) {
12112
12885
  if (!providerConfig) {
12113
12886
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
12114
12887
  }
12115
- const { createProvider: createProvider2 } = await import("./registry-2REAPKPO.js");
12888
+ const { createProvider: createProvider2 } = await import("./registry-KVJAO5DF.js");
12116
12889
  return createProvider2(preferredProviderId, providerConfig, rootDir);
12117
12890
  }
12118
12891
  async function generateOutputArtifacts(rootDir, input) {
@@ -12490,6 +13263,8 @@ function approvalSummary(manifest) {
12490
13263
  return {
12491
13264
  approvalId: manifest.approvalId,
12492
13265
  createdAt: manifest.createdAt,
13266
+ bundleType: manifest.bundleType,
13267
+ title: manifest.title,
12493
13268
  entryCount: manifest.entries.length,
12494
13269
  pendingCount: manifest.entries.filter((entry) => entry.status === "pending").length,
12495
13270
  acceptedCount: manifest.entries.filter((entry) => entry.status === "accepted").length,
@@ -12587,6 +13362,425 @@ async function buildManagedContent(absolutePath, defaults, build) {
12587
13362
  }
12588
13363
  return content;
12589
13364
  }
13365
+ function manifestDetailValue(manifest, key) {
13366
+ const value = manifest.details?.[key];
13367
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
13368
+ }
13369
+ async function loadAnalysesBySourceIds(paths, sourceIds) {
13370
+ const analyses = await Promise.all(
13371
+ sourceIds.map(async (sourceId) => await readJsonFile(path22.join(paths.analysesDir, `${sourceId}.json`)))
13372
+ );
13373
+ return analyses.filter((analysis) => Boolean(analysis?.sourceId));
13374
+ }
13375
+ async function buildDashboardRecords(paths, graph, schemaHash, report) {
13376
+ const sourcePages = graph.pages.filter((page) => page.kind === "source");
13377
+ const reviewPages = graph.pages.filter((page) => page.kind === "output" && page.path.startsWith("outputs/source-reviews/"));
13378
+ const briefPages = graph.pages.filter((page) => page.kind === "output" && page.path.startsWith("outputs/source-briefs/"));
13379
+ const guidePages = graph.pages.filter((page) => page.kind === "output" && page.path.startsWith("outputs/source-guides/"));
13380
+ const conceptPages = graph.pages.filter((page) => page.kind === "concept" && page.status !== "candidate").slice(0, 16);
13381
+ const entityPages = graph.pages.filter((page) => page.kind === "entity" && page.status !== "candidate").slice(0, 16);
13382
+ const manifests = graph.sources;
13383
+ const manifestBySourceId = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
13384
+ const timelineManifests = manifests.filter((manifest) => manifestDetailValue(manifest, "occurred_at")).sort((left, right) => (manifestDetailValue(right, "occurred_at") ?? "").localeCompare(manifestDetailValue(left, "occurred_at") ?? "")).slice(0, 25);
13385
+ const recentSourcePages = [...sourcePages].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, 20);
13386
+ const analyses = await loadAnalysesBySourceIds(paths, uniqueStrings3(sourcePages.flatMap((page) => page.sourceIds)));
13387
+ const openQuestions = uniqueStrings3(
13388
+ analyses.flatMap((analysis) => analysis.questions.map((question) => `${analysis.title}: ${question}`))
13389
+ ).slice(0, 20);
13390
+ const stagedGuideBundles = (await Promise.all(
13391
+ (await fs18.readdir(paths.approvalsDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map(async (entry) => await readJsonFile(approvalManifestPath(paths, entry.name)))
13392
+ )).filter((manifest) => Boolean(manifest)).filter((manifest) => manifest.bundleType === "guided_source").sort((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, 12);
13393
+ const dashboards = [
13394
+ {
13395
+ relativePath: "dashboards/index.md",
13396
+ title: "Dashboards",
13397
+ content: (metadata) => matter9.stringify(
13398
+ [
13399
+ "# Dashboards",
13400
+ "",
13401
+ "- [[dashboards/recent-sources|Recent Sources]]",
13402
+ "- [[dashboards/reading-log|Reading Log]]",
13403
+ "- [[dashboards/timeline|Timeline]]",
13404
+ "- [[dashboards/source-guides|Source Guides]]",
13405
+ "- [[dashboards/research-map|Research Map]]",
13406
+ "- [[dashboards/contradictions|Contradictions]]",
13407
+ "- [[dashboards/open-questions|Open Questions]]",
13408
+ "",
13409
+ "```dataview",
13410
+ "TABLE file.mtime AS updated",
13411
+ 'FROM "dashboards"',
13412
+ 'WHERE file.name != "index"',
13413
+ "SORT file.mtime desc",
13414
+ "```",
13415
+ ""
13416
+ ].join("\n"),
13417
+ {
13418
+ page_id: "dashboards:index",
13419
+ kind: "index",
13420
+ title: "Dashboards",
13421
+ tags: ["index", "dashboards"],
13422
+ source_ids: [],
13423
+ project_ids: [],
13424
+ node_ids: [],
13425
+ freshness: "fresh",
13426
+ status: metadata.status,
13427
+ confidence: 1,
13428
+ created_at: metadata.createdAt,
13429
+ updated_at: metadata.updatedAt,
13430
+ compiled_from: metadata.compiledFrom,
13431
+ managed_by: metadata.managedBy,
13432
+ backlinks: [],
13433
+ schema_hash: schemaHash,
13434
+ source_hashes: {},
13435
+ source_semantic_hashes: {}
13436
+ }
13437
+ )
13438
+ },
13439
+ {
13440
+ relativePath: "dashboards/recent-sources.md",
13441
+ title: "Recent Sources",
13442
+ content: (metadata) => matter9.stringify(
13443
+ [
13444
+ "# Recent Sources",
13445
+ "",
13446
+ ...recentSourcePages.length ? recentSourcePages.map((page) => `- ${page.updatedAt}: [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No source pages yet."],
13447
+ "",
13448
+ "```dataview",
13449
+ "TABLE source_type, occurred_at, participants",
13450
+ 'FROM "sources"',
13451
+ "SORT updated_at desc",
13452
+ "LIMIT 25",
13453
+ "```",
13454
+ ""
13455
+ ].join("\n"),
13456
+ {
13457
+ page_id: "dashboards:recent-sources",
13458
+ kind: "index",
13459
+ title: "Recent Sources",
13460
+ tags: ["index", "dashboard", "recent-sources"],
13461
+ source_ids: recentSourcePages.flatMap((page) => page.sourceIds),
13462
+ project_ids: [],
13463
+ node_ids: [],
13464
+ freshness: "fresh",
13465
+ status: metadata.status,
13466
+ confidence: 1,
13467
+ created_at: metadata.createdAt,
13468
+ updated_at: metadata.updatedAt,
13469
+ compiled_from: recentSourcePages.flatMap((page) => page.sourceIds),
13470
+ managed_by: metadata.managedBy,
13471
+ backlinks: [],
13472
+ schema_hash: schemaHash,
13473
+ source_hashes: {},
13474
+ source_semantic_hashes: {}
13475
+ }
13476
+ )
13477
+ },
13478
+ {
13479
+ relativePath: "dashboards/reading-log.md",
13480
+ title: "Reading Log",
13481
+ content: (metadata) => matter9.stringify(
13482
+ [
13483
+ "# Reading Log",
13484
+ "",
13485
+ ...timelineManifests.length ? timelineManifests.map((manifest) => {
13486
+ const occurredAt = manifestDetailValue(manifest, "occurred_at") ?? manifest.updatedAt;
13487
+ const participants = manifestDetailValue(manifest, "participants");
13488
+ return `- ${occurredAt}: ${manifest.title}${participants ? ` (${participants})` : ""}`;
13489
+ }) : recentSourcePages.map((page) => `- ${page.updatedAt}: [[${page.path.replace(/\.md$/, "")}|${page.title}]]`),
13490
+ "",
13491
+ "```dataview",
13492
+ "TABLE occurred_at, source_type, participants, container_title",
13493
+ 'FROM "sources"',
13494
+ "SORT occurred_at desc",
13495
+ "LIMIT 25",
13496
+ "```",
13497
+ ""
13498
+ ].join("\n"),
13499
+ {
13500
+ page_id: "dashboards:reading-log",
13501
+ kind: "index",
13502
+ title: "Reading Log",
13503
+ tags: ["index", "dashboard", "reading-log"],
13504
+ source_ids: timelineManifests.map((manifest) => manifest.sourceId),
13505
+ project_ids: [],
13506
+ node_ids: [],
13507
+ freshness: "fresh",
13508
+ status: metadata.status,
13509
+ confidence: 1,
13510
+ created_at: metadata.createdAt,
13511
+ updated_at: metadata.updatedAt,
13512
+ compiled_from: timelineManifests.map((manifest) => manifest.sourceId),
13513
+ managed_by: metadata.managedBy,
13514
+ backlinks: [],
13515
+ schema_hash: schemaHash,
13516
+ source_hashes: {},
13517
+ source_semantic_hashes: {}
13518
+ }
13519
+ )
13520
+ },
13521
+ {
13522
+ relativePath: "dashboards/timeline.md",
13523
+ title: "Timeline",
13524
+ content: (metadata) => matter9.stringify(
13525
+ [
13526
+ "# Timeline",
13527
+ "",
13528
+ ...timelineManifests.length ? timelineManifests.map((manifest) => {
13529
+ const occurredAt = manifestDetailValue(manifest, "occurred_at") ?? manifest.updatedAt;
13530
+ const sourcePage = sourcePages.find((page) => page.sourceIds.includes(manifest.sourceId));
13531
+ return `- ${occurredAt}: ${sourcePage ? `[[${sourcePage.path.replace(/\.md$/, "")}|${sourcePage.title}]]` : manifest.title}`;
13532
+ }) : ["- No timeline-aware sources yet."],
13533
+ "",
13534
+ "```dataview",
13535
+ "TABLE occurred_at, participants, container_title",
13536
+ 'FROM "sources"',
13537
+ "WHERE occurred_at",
13538
+ "SORT occurred_at desc",
13539
+ "```",
13540
+ ""
13541
+ ].join("\n"),
13542
+ {
13543
+ page_id: "dashboards:timeline",
13544
+ kind: "index",
13545
+ title: "Timeline",
13546
+ tags: ["index", "dashboard", "timeline"],
13547
+ source_ids: timelineManifests.map((manifest) => manifest.sourceId),
13548
+ project_ids: [],
13549
+ node_ids: [],
13550
+ freshness: "fresh",
13551
+ status: metadata.status,
13552
+ confidence: 1,
13553
+ created_at: metadata.createdAt,
13554
+ updated_at: metadata.updatedAt,
13555
+ compiled_from: timelineManifests.map((manifest) => manifest.sourceId),
13556
+ managed_by: metadata.managedBy,
13557
+ backlinks: [],
13558
+ schema_hash: schemaHash,
13559
+ source_hashes: {},
13560
+ source_semantic_hashes: {}
13561
+ }
13562
+ )
13563
+ },
13564
+ {
13565
+ relativePath: "dashboards/source-guides.md",
13566
+ title: "Source Guides",
13567
+ content: (metadata) => matter9.stringify(
13568
+ [
13569
+ "# Source Guides",
13570
+ "",
13571
+ ...guidePages.length ? guidePages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No accepted source guides yet."],
13572
+ "",
13573
+ "## Pending Guided Bundles",
13574
+ "",
13575
+ ...stagedGuideBundles.length ? stagedGuideBundles.map(
13576
+ (bundle) => `- ${bundle.createdAt}: \`${bundle.approvalId}\`${bundle.title ? ` ${bundle.title}` : ""} (${bundle.entries.length} staged entr${bundle.entries.length === 1 ? "y" : "ies"})`
13577
+ ) : ["- No staged guided bundles right now."],
13578
+ "",
13579
+ "```dataview",
13580
+ 'LIST FROM "outputs/source-guides"',
13581
+ "SORT file.mtime desc",
13582
+ "```",
13583
+ ""
13584
+ ].join("\n"),
13585
+ {
13586
+ page_id: "dashboards:source-guides",
13587
+ kind: "index",
13588
+ title: "Source Guides",
13589
+ tags: ["index", "dashboard", "source-guides"],
13590
+ source_ids: uniqueStrings3([
13591
+ ...guidePages.flatMap((page) => page.sourceIds),
13592
+ ...stagedGuideBundles.flatMap((bundle) => bundle.entries.flatMap((entry) => entry.sourceIds))
13593
+ ]),
13594
+ project_ids: [],
13595
+ node_ids: [],
13596
+ freshness: "fresh",
13597
+ status: metadata.status,
13598
+ confidence: 1,
13599
+ created_at: metadata.createdAt,
13600
+ updated_at: metadata.updatedAt,
13601
+ compiled_from: uniqueStrings3([
13602
+ ...guidePages.flatMap((page) => page.sourceIds),
13603
+ ...stagedGuideBundles.flatMap((bundle) => bundle.entries.flatMap((entry) => entry.sourceIds))
13604
+ ]),
13605
+ managed_by: metadata.managedBy,
13606
+ backlinks: [],
13607
+ schema_hash: schemaHash,
13608
+ source_hashes: {},
13609
+ source_semantic_hashes: {}
13610
+ }
13611
+ )
13612
+ },
13613
+ {
13614
+ relativePath: "dashboards/research-map.md",
13615
+ title: "Research Map",
13616
+ content: (metadata) => matter9.stringify(
13617
+ [
13618
+ "# Research Map",
13619
+ "",
13620
+ "## Canonical Concept Pages",
13621
+ "",
13622
+ ...conceptPages.length ? conceptPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No concept pages yet."],
13623
+ "",
13624
+ "## Canonical Entity Pages",
13625
+ "",
13626
+ ...entityPages.length ? entityPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No entity pages yet."],
13627
+ "",
13628
+ "## Recently Guided Sources",
13629
+ "",
13630
+ ...guidePages.length ? guidePages.slice(0, 8).map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No accepted source guides yet."],
13631
+ ...report?.suggestedQuestions?.length ? ["", "## Suggested Questions", "", ...report.suggestedQuestions.slice(0, 8).map((question) => `- ${question}`)] : [],
13632
+ "",
13633
+ "```dataview",
13634
+ 'TABLE file.folder, file.mtime FROM "concepts" OR "entities"',
13635
+ "SORT file.mtime desc",
13636
+ "LIMIT 30",
13637
+ "```",
13638
+ ""
13639
+ ].join("\n"),
13640
+ {
13641
+ page_id: "dashboards:research-map",
13642
+ kind: "index",
13643
+ title: "Research Map",
13644
+ tags: ["index", "dashboard", "research-map"],
13645
+ source_ids: uniqueStrings3([
13646
+ ...conceptPages.flatMap((page) => page.sourceIds),
13647
+ ...entityPages.flatMap((page) => page.sourceIds),
13648
+ ...guidePages.flatMap((page) => page.sourceIds)
13649
+ ]),
13650
+ project_ids: [],
13651
+ node_ids: [],
13652
+ freshness: "fresh",
13653
+ status: metadata.status,
13654
+ confidence: 1,
13655
+ created_at: metadata.createdAt,
13656
+ updated_at: metadata.updatedAt,
13657
+ compiled_from: uniqueStrings3([
13658
+ ...conceptPages.flatMap((page) => page.sourceIds),
13659
+ ...entityPages.flatMap((page) => page.sourceIds),
13660
+ ...guidePages.flatMap((page) => page.sourceIds)
13661
+ ]),
13662
+ managed_by: metadata.managedBy,
13663
+ backlinks: [],
13664
+ schema_hash: schemaHash,
13665
+ source_hashes: {},
13666
+ source_semantic_hashes: {}
13667
+ }
13668
+ )
13669
+ },
13670
+ {
13671
+ relativePath: "dashboards/contradictions.md",
13672
+ title: "Contradictions",
13673
+ content: (metadata) => matter9.stringify(
13674
+ [
13675
+ "# Contradictions",
13676
+ "",
13677
+ ...report?.contradictions.length ? report.contradictions.map((contradiction) => {
13678
+ const left = manifestBySourceId.get(contradiction.sourceIdA)?.title ?? contradiction.sourceIdA;
13679
+ const right = manifestBySourceId.get(contradiction.sourceIdB)?.title ?? contradiction.sourceIdB;
13680
+ return `- ${left} / ${right}: ${contradiction.claimA} <> ${contradiction.claimB}`;
13681
+ }) : ["- No contradictions are currently flagged."],
13682
+ "",
13683
+ ...reviewPages.length || briefPages.length || guidePages.length ? [
13684
+ "## Related Reviews",
13685
+ "",
13686
+ ...[...guidePages, ...reviewPages, ...briefPages].slice(0, 12).map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`),
13687
+ ""
13688
+ ] : [],
13689
+ "```dataview",
13690
+ 'LIST FROM "outputs/source-reviews" OR "outputs/source-guides"',
13691
+ "SORT file.mtime desc",
13692
+ "```",
13693
+ ""
13694
+ ].join("\n"),
13695
+ {
13696
+ page_id: "dashboards:contradictions",
13697
+ kind: "index",
13698
+ title: "Contradictions",
13699
+ tags: ["index", "dashboard", "contradictions"],
13700
+ source_ids: report?.contradictions.flatMap((item) => [item.sourceIdA, item.sourceIdB]) ?? [],
13701
+ project_ids: [],
13702
+ node_ids: [],
13703
+ freshness: "fresh",
13704
+ status: metadata.status,
13705
+ confidence: 1,
13706
+ created_at: metadata.createdAt,
13707
+ updated_at: metadata.updatedAt,
13708
+ compiled_from: report?.contradictions.flatMap((item) => [item.sourceIdA, item.sourceIdB]) ?? [],
13709
+ managed_by: metadata.managedBy,
13710
+ backlinks: [],
13711
+ schema_hash: schemaHash,
13712
+ source_hashes: {},
13713
+ source_semantic_hashes: {}
13714
+ }
13715
+ )
13716
+ },
13717
+ {
13718
+ relativePath: "dashboards/open-questions.md",
13719
+ title: "Open Questions",
13720
+ content: (metadata) => matter9.stringify(
13721
+ [
13722
+ "# Open Questions",
13723
+ "",
13724
+ ...openQuestions.length ? openQuestions.map((question) => `- ${question}`) : ["- No open questions are currently extracted."],
13725
+ "",
13726
+ "```dataview",
13727
+ 'LIST FROM "outputs/source-briefs" OR "outputs/source-reviews" OR "outputs/source-guides"',
13728
+ "SORT file.mtime desc",
13729
+ "```",
13730
+ ""
13731
+ ].join("\n"),
13732
+ {
13733
+ page_id: "dashboards:open-questions",
13734
+ kind: "index",
13735
+ title: "Open Questions",
13736
+ tags: ["index", "dashboard", "open-questions"],
13737
+ source_ids: analyses.map((analysis) => analysis.sourceId),
13738
+ project_ids: [],
13739
+ node_ids: [],
13740
+ freshness: "fresh",
13741
+ status: metadata.status,
13742
+ confidence: 1,
13743
+ created_at: metadata.createdAt,
13744
+ updated_at: metadata.updatedAt,
13745
+ compiled_from: analyses.map((analysis) => analysis.sourceId),
13746
+ managed_by: metadata.managedBy,
13747
+ backlinks: [],
13748
+ schema_hash: schemaHash,
13749
+ source_hashes: {},
13750
+ source_semantic_hashes: {}
13751
+ }
13752
+ )
13753
+ }
13754
+ ];
13755
+ const records = [];
13756
+ for (const dashboard of dashboards) {
13757
+ const absolutePath = path22.join(paths.wikiDir, dashboard.relativePath);
13758
+ const compiledFrom = dashboard.relativePath === "dashboards/recent-sources.md" ? recentSourcePages.flatMap((page) => page.sourceIds) : [];
13759
+ const content = await buildManagedContent(
13760
+ absolutePath,
13761
+ {
13762
+ managedBy: "system",
13763
+ compiledFrom
13764
+ },
13765
+ dashboard.content
13766
+ );
13767
+ records.push({
13768
+ page: emptyGraphPage({
13769
+ id: `dashboard:${dashboard.relativePath.replace(/\.md$/, "")}`,
13770
+ path: dashboard.relativePath,
13771
+ title: dashboard.title,
13772
+ kind: "index",
13773
+ sourceIds: compiledFrom,
13774
+ nodeIds: [],
13775
+ schemaHash,
13776
+ sourceHashes: {},
13777
+ confidence: 1
13778
+ }),
13779
+ content
13780
+ });
13781
+ }
13782
+ return records;
13783
+ }
12590
13784
  function indexCompiledFrom(pages) {
12591
13785
  return uniqueStrings3(pages.flatMap((page) => page.sourceIds));
12592
13786
  }
@@ -13304,7 +14498,7 @@ async function writeApprovalManifest(paths, manifest) {
13304
14498
  await fs18.writeFile(approvalManifestPath(paths, manifest.approvalId), `${JSON.stringify(manifest, null, 2)}
13305
14499
  `, "utf8");
13306
14500
  }
13307
- async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousGraph, graph) {
14501
+ async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousGraph, graph, labelsByPath = /* @__PURE__ */ new Map()) {
13308
14502
  const previousPagesById = new Map((previousGraph?.pages ?? []).map((page) => [page.id, page]));
13309
14503
  const previousPagesByPath = new Map((previousGraph?.pages ?? []).map((page) => [page.path, page]));
13310
14504
  const nextPagesByPath = new Map(graph.pages.map((page) => [page.path, page]));
@@ -13326,7 +14520,8 @@ async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousG
13326
14520
  status: "pending",
13327
14521
  sourceIds: nextPage.sourceIds,
13328
14522
  nextPath: nextPage.path,
13329
- previousPath: previousPage.path
14523
+ previousPath: previousPage.path,
14524
+ label: labelsByPath.get(nextPage.path) ?? labelsByPath.get(previousPage.path)
13330
14525
  });
13331
14526
  handledDeletedPaths.add(previousPage.path);
13332
14527
  continue;
@@ -13339,7 +14534,8 @@ async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousG
13339
14534
  status: "pending",
13340
14535
  sourceIds: nextPage.sourceIds,
13341
14536
  nextPath: nextPage.path,
13342
- previousPath: previousPage?.path
14537
+ previousPath: previousPage?.path,
14538
+ label: labelsByPath.get(nextPage.path) ?? (previousPage?.path ? labelsByPath.get(previousPage.path) : void 0)
13343
14539
  });
13344
14540
  }
13345
14541
  for (const deletedPath of deletedPaths.sort((left, right) => left.localeCompare(right))) {
@@ -13354,7 +14550,8 @@ async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousG
13354
14550
  changeType: "delete",
13355
14551
  status: "pending",
13356
14552
  sourceIds: previousPage?.sourceIds ?? [],
13357
- previousPath: deletedPath
14553
+ previousPath: deletedPath,
14554
+ label: labelsByPath.get(deletedPath)
13358
14555
  });
13359
14556
  }
13360
14557
  return uniqueBy(entries, (entry) => `${entry.pageId}:${entry.changeType}:${entry.nextPath ?? ""}:${entry.previousPath ?? ""}`);
@@ -13374,6 +14571,8 @@ async function stageApprovalBundle(paths, changedFiles, deletedPaths, previousGr
13374
14571
  await writeApprovalManifest(paths, {
13375
14572
  approvalId,
13376
14573
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
14574
+ bundleType: "compile",
14575
+ title: "Compile Approval",
13377
14576
  entries: await buildApprovalEntries(paths, changedFiles, deletedPaths, previousGraph, graph)
13378
14577
  });
13379
14578
  return { approvalId, approvalDir };
@@ -13612,8 +14811,19 @@ async function syncVaultArtifacts(rootDir, input) {
13612
14811
  input.previousState?.generatedAt,
13613
14812
  contradictions
13614
14813
  );
13615
- records.push(...graphOrientation.records);
13616
- const allPages = [...basePages, ...graphOrientation.records.map((record) => record.page)];
14814
+ const preliminaryPages = [...basePages, ...graphOrientation.records.map((record) => record.page)];
14815
+ const dashboardRecords = await buildDashboardRecords(
14816
+ paths,
14817
+ {
14818
+ ...baseGraph,
14819
+ sources: input.manifests,
14820
+ pages: preliminaryPages
14821
+ },
14822
+ globalSchemaHash,
14823
+ graphOrientation.report
14824
+ );
14825
+ records.push(...graphOrientation.records, ...dashboardRecords);
14826
+ const allPages = uniqueBy([...preliminaryPages, ...dashboardRecords.map((record) => record.page)], (page) => page.id);
13617
14827
  const graph = {
13618
14828
  ...baseGraph,
13619
14829
  pages: allPages
@@ -13717,6 +14927,11 @@ async function syncVaultArtifacts(rootDir, input) {
13717
14927
  ["concepts/index.md", "concepts", activeConceptPages],
13718
14928
  ["entities/index.md", "entities", activeEntityPages],
13719
14929
  ["outputs/index.md", "outputs", allPages.filter((page) => page.kind === "output")],
14930
+ [
14931
+ "dashboards/index.md",
14932
+ "dashboards",
14933
+ allPages.filter((page) => page.kind === "index" && page.path.startsWith("dashboards/") && page.path !== "dashboards/index.md")
14934
+ ],
13720
14935
  ["candidates/index.md", "candidates", candidatePages],
13721
14936
  ["graph/index.md", "graph", allPages.filter((page) => page.kind === "graph_report" || page.kind === "community_summary")]
13722
14937
  ]) {
@@ -13817,17 +15032,40 @@ async function refreshIndexesAndSearch(rootDir, pages) {
13817
15032
  const compileState = await readJsonFile(paths.compileStatePath);
13818
15033
  const globalSchemaHash = schemas.effective.global.hash;
13819
15034
  const currentGraph = await readJsonFile(paths.graphPath);
13820
- const basePages = pages.filter((page) => page.kind !== "graph_report" && page.kind !== "community_summary");
15035
+ const orientationPages = uniqueBy(
15036
+ pages.filter((page) => page.kind !== "graph_report" && page.kind !== "community_summary"),
15037
+ (page) => page.id
15038
+ );
15039
+ const basePages = uniqueBy(
15040
+ pages.filter(
15041
+ (page) => page.kind !== "graph_report" && page.kind !== "community_summary" && !(page.kind === "index" && page.path.startsWith("dashboards/"))
15042
+ ),
15043
+ (page) => page.id
15044
+ );
13821
15045
  const graphOrientation = currentGraph ? await buildGraphOrientationPages(
13822
15046
  {
13823
15047
  ...currentGraph,
13824
- pages: basePages
15048
+ pages: orientationPages
13825
15049
  },
13826
15050
  paths,
13827
15051
  globalSchemaHash,
13828
15052
  compileState?.generatedAt
13829
15053
  ) : { records: [], report: null };
13830
- const pagesWithGraph = sortGraphPages([...basePages, ...graphOrientation.records.map((record) => record.page)]);
15054
+ const dashboardRecords = currentGraph ? await buildDashboardRecords(
15055
+ paths,
15056
+ {
15057
+ ...currentGraph,
15058
+ pages: [...basePages, ...graphOrientation.records.map((record) => record.page)]
15059
+ },
15060
+ globalSchemaHash,
15061
+ graphOrientation.report
15062
+ ) : [];
15063
+ const pagesWithGraph = sortGraphPages(
15064
+ uniqueBy(
15065
+ [...basePages, ...graphOrientation.records.map((record) => record.page), ...dashboardRecords.map((record) => record.page)],
15066
+ (page) => page.id
15067
+ )
15068
+ );
13831
15069
  if (currentGraph) {
13832
15070
  await writeJsonFile(paths.graphPath, {
13833
15071
  ...currentGraph,
@@ -13855,6 +15093,7 @@ async function refreshIndexesAndSearch(rootDir, pages) {
13855
15093
  ensureDir(path22.join(paths.wikiDir, "concepts")),
13856
15094
  ensureDir(path22.join(paths.wikiDir, "entities")),
13857
15095
  ensureDir(path22.join(paths.wikiDir, "outputs")),
15096
+ ensureDir(path22.join(paths.wikiDir, "dashboards")),
13858
15097
  ensureDir(path22.join(paths.wikiDir, "graph")),
13859
15098
  ensureDir(path22.join(paths.wikiDir, "graph", "communities")),
13860
15099
  ensureDir(path22.join(paths.wikiDir, "projects")),
@@ -13917,6 +15156,11 @@ async function refreshIndexesAndSearch(rootDir, pages) {
13917
15156
  ["concepts/index.md", "concepts", pagesWithGraph.filter((page) => page.kind === "concept" && page.status !== "candidate")],
13918
15157
  ["entities/index.md", "entities", pagesWithGraph.filter((page) => page.kind === "entity" && page.status !== "candidate")],
13919
15158
  ["outputs/index.md", "outputs", pagesWithGraph.filter((page) => page.kind === "output")],
15159
+ [
15160
+ "dashboards/index.md",
15161
+ "dashboards",
15162
+ pagesWithGraph.filter((page) => page.kind === "index" && page.path.startsWith("dashboards/") && page.path !== "dashboards/index.md")
15163
+ ],
13920
15164
  ["candidates/index.md", "candidates", pagesWithGraph.filter((page) => page.status === "candidate")],
13921
15165
  ["graph/index.md", "graph", pagesWithGraph.filter((page) => page.kind === "graph_report" || page.kind === "community_summary")]
13922
15166
  ]) {
@@ -13936,6 +15180,9 @@ async function refreshIndexesAndSearch(rootDir, pages) {
13936
15180
  for (const record of graphOrientation.records) {
13937
15181
  await writeFileIfChanged(path22.join(paths.wikiDir, record.page.path), record.content);
13938
15182
  }
15183
+ for (const record of dashboardRecords) {
15184
+ await writeFileIfChanged(path22.join(paths.wikiDir, record.page.path), record.content);
15185
+ }
13939
15186
  if (graphOrientation.report) {
13940
15187
  await writeJsonFile(path22.join(paths.wikiDir, "graph", "report.json"), graphOrientation.report);
13941
15188
  }
@@ -13952,6 +15199,11 @@ async function refreshIndexesAndSearch(rootDir, pages) {
13952
15199
  await Promise.all(
13953
15200
  existingGraphPages.filter((relativePath) => !allowedGraphPages.has(relativePath)).map((relativePath) => fs18.rm(path22.join(paths.wikiDir, relativePath), { force: true }))
13954
15201
  );
15202
+ const existingDashboardPages = (await listFilesRecursive(path22.join(paths.wikiDir, "dashboards")).catch(() => [])).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path22.relative(paths.wikiDir, absolutePath)));
15203
+ const allowedDashboardPages = /* @__PURE__ */ new Set(["dashboards/index.md", ...dashboardRecords.map((record) => record.page.path)]);
15204
+ await Promise.all(
15205
+ existingDashboardPages.filter((relativePath) => !allowedDashboardPages.has(relativePath)).map((relativePath) => fs18.rm(path22.join(paths.wikiDir, relativePath), { force: true }))
15206
+ );
13955
15207
  await rebuildSearchIndex(paths.searchDbPath, pagesWithGraph, paths.wikiDir);
13956
15208
  }
13957
15209
  async function prepareOutputPageSave(rootDir, input) {
@@ -14036,7 +15288,7 @@ async function persistExploreHub(rootDir, input) {
14036
15288
  }
14037
15289
  return { page: prepared.page, savedPath: prepared.savedPath, outputAssets: prepared.outputAssets };
14038
15290
  }
14039
- async function stageOutputApprovalBundle(rootDir, stagedPages) {
15291
+ async function stageOutputApprovalBundle(rootDir, stagedPages, options = {}) {
14040
15292
  const { paths } = await loadVaultConfig(rootDir);
14041
15293
  const previousGraph = await readJsonFile(paths.graphPath);
14042
15294
  const changedFiles = stagedPages.flatMap((item) => [
@@ -14047,6 +15299,7 @@ async function stageOutputApprovalBundle(rootDir, stagedPages) {
14047
15299
  binary: typeof assetFile.content !== "string"
14048
15300
  }))
14049
15301
  ]);
15302
+ const labelsByPath = new Map(stagedPages.filter((item) => item.label).map((item) => [item.page.path, item.label]));
14050
15303
  const approvalId = `schedule-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
14051
15304
  const approvalDir = path22.join(paths.approvalsDir, approvalId);
14052
15305
  await ensureDir(approvalDir);
@@ -14077,16 +15330,22 @@ async function stageOutputApprovalBundle(rootDir, stagedPages) {
14077
15330
  await writeApprovalManifest(paths, {
14078
15331
  approvalId,
14079
15332
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
15333
+ bundleType: options.bundleType ?? "generated_output",
15334
+ title: options.title,
14080
15335
  entries: await buildApprovalEntries(
14081
15336
  paths,
14082
15337
  stagedPages.map((item) => ({ relativePath: item.page.path, content: item.content })),
14083
15338
  [],
14084
15339
  previousGraph ?? null,
14085
- graph
15340
+ graph,
15341
+ labelsByPath
14086
15342
  )
14087
15343
  });
14088
15344
  return { approvalId, approvalDir };
14089
15345
  }
15346
+ async function stageGeneratedOutputPages(rootDir, stagedPages, options = {}) {
15347
+ return await stageOutputApprovalBundle(rootDir, stagedPages, options);
15348
+ }
14090
15349
  async function executeQuery(rootDir, question, format) {
14091
15350
  const { paths } = await loadVaultConfig(rootDir);
14092
15351
  const schemas = await loadVaultSchemas(rootDir);
@@ -14658,14 +15917,24 @@ async function ensureObsidianWorkspace(rootDir) {
14658
15917
  ]);
14659
15918
  }
14660
15919
  async function initVault(rootDir, options = {}) {
14661
- const { paths } = await initWorkspace(rootDir);
15920
+ const profile = options.profile ?? "default";
15921
+ const { paths } = await initWorkspace(rootDir, { profile });
14662
15922
  await installConfiguredAgents(rootDir);
14663
15923
  const insightsIndexPath = path22.join(paths.wikiDir, "insights", "index.md");
14664
15924
  const now = (/* @__PURE__ */ new Date()).toISOString();
14665
15925
  await writeFileIfChanged(
14666
15926
  insightsIndexPath,
14667
15927
  matter9.stringify(
14668
- [
15928
+ (profile === "personal-research" ? [
15929
+ "# Insights",
15930
+ "",
15931
+ "Human-authored research notes live here.",
15932
+ "",
15933
+ "- Use this folder for thesis notes, reading reflections, synthesis drafts, and decisions you want to keep explicitly human-authored.",
15934
+ "- Guided ingest can propose updates elsewhere, but SwarmVault does not rewrite files inside `wiki/insights/` after initialization.",
15935
+ "- Treat these pages as the human judgment layer for your vault.",
15936
+ ""
15937
+ ] : [
14669
15938
  "# Insights",
14670
15939
  "",
14671
15940
  "Human-authored notes live here.",
@@ -14673,7 +15942,7 @@ async function initVault(rootDir, options = {}) {
14673
15942
  "- SwarmVault can read these pages during compile and query.",
14674
15943
  "- SwarmVault does not rewrite files inside `wiki/insights/` after initialization.",
14675
15944
  ""
14676
- ].join("\n"),
15945
+ ]).join("\n"),
14677
15946
  {
14678
15947
  page_id: "insights:index",
14679
15948
  kind: "index",
@@ -14745,6 +16014,42 @@ async function initVault(rootDir, options = {}) {
14745
16014
  if (options.obsidian) {
14746
16015
  await ensureObsidianWorkspace(rootDir);
14747
16016
  }
16017
+ if (profile === "personal-research") {
16018
+ await writeFileIfChanged(
16019
+ path22.join(paths.wikiDir, "insights", "research-playbook.md"),
16020
+ matter9.stringify(
16021
+ [
16022
+ "# Personal Research Playbook",
16023
+ "",
16024
+ "- Add one source at a time with `swarmvault ingest <input> --guide` or `swarmvault source add <input> --guide`.",
16025
+ "- Review `wiki/outputs/source-briefs/`, `wiki/outputs/source-reviews/`, and `wiki/outputs/source-guides/` before accepting staged updates.",
16026
+ "- Keep unresolved questions visible in `wiki/dashboards/open-questions.md`.",
16027
+ "- Use `swarmvault review list` and `swarmvault review show --diff` to decide what becomes canonical.",
16028
+ ""
16029
+ ].join("\n"),
16030
+ {
16031
+ page_id: "insights:research-playbook",
16032
+ kind: "insight",
16033
+ title: "Personal Research Playbook",
16034
+ tags: ["insight", "research", "playbook"],
16035
+ source_ids: [],
16036
+ project_ids: [],
16037
+ node_ids: [],
16038
+ freshness: "fresh",
16039
+ status: "active",
16040
+ confidence: 1,
16041
+ created_at: now,
16042
+ updated_at: now,
16043
+ compiled_from: [],
16044
+ managed_by: "human",
16045
+ backlinks: [],
16046
+ schema_hash: "",
16047
+ source_hashes: {},
16048
+ source_semantic_hashes: {}
16049
+ }
16050
+ )
16051
+ );
16052
+ }
14748
16053
  }
14749
16054
  async function runConfiguredBenchmark(rootDir, config) {
14750
16055
  if (config.benchmark?.enabled === false) {
@@ -15428,7 +16733,17 @@ async function benchmarkVault(rootDir, options = {}) {
15428
16733
  });
15429
16734
  await writeJsonFile(paths.benchmarkPath, artifact);
15430
16735
  await refreshIndexesAndSearch(rootDir, graph.pages);
15431
- return artifact;
16736
+ const refreshedGraph = await readJsonFile(paths.graphPath) ?? graph;
16737
+ const refreshedHash = graphHash(refreshedGraph);
16738
+ if (artifact.graphHash === refreshedHash) {
16739
+ return artifact;
16740
+ }
16741
+ const refreshedArtifact = {
16742
+ ...artifact,
16743
+ graphHash: refreshedHash
16744
+ };
16745
+ await writeJsonFile(paths.benchmarkPath, refreshedArtifact);
16746
+ return refreshedArtifact;
15432
16747
  }
15433
16748
  async function pathGraphVault(rootDir, from, to) {
15434
16749
  const graph = await ensureCompiledGraph(rootDir);
@@ -15648,7 +16963,7 @@ async function bootstrapDemo(rootDir, input) {
15648
16963
  }
15649
16964
 
15650
16965
  // src/mcp.ts
15651
- var SERVER_VERSION = "0.3.0";
16966
+ var SERVER_VERSION = "0.5.0";
15652
16967
  async function createMcpServer(rootDir) {
15653
16968
  const server = new McpServer({
15654
16969
  name: "swarmvault",
@@ -16504,7 +17819,7 @@ function matchesManagedSourceSpec(existing, input) {
16504
17819
  if (existing.kind !== input.kind) {
16505
17820
  return false;
16506
17821
  }
16507
- if (input.kind === "directory") {
17822
+ if (input.kind === "directory" || input.kind === "file") {
16508
17823
  return path25.resolve(existing.path ?? "") === path25.resolve(input.path);
16509
17824
  }
16510
17825
  return (existing.url ?? "") === input.url;
@@ -16516,10 +17831,15 @@ async function resolveManagedSourceInput(rootDir, input) {
16516
17831
  if (!stat) {
16517
17832
  throw new Error(`Source not found: ${input}`);
16518
17833
  }
17834
+ if (stat.isFile()) {
17835
+ return {
17836
+ kind: "file",
17837
+ path: absoluteInput,
17838
+ title: path25.basename(absoluteInput, path25.extname(absoluteInput)) || absoluteInput
17839
+ };
17840
+ }
16519
17841
  if (!stat.isDirectory()) {
16520
- throw new Error(
16521
- "`swarmvault source add` supports directories, public GitHub repo root URLs, and docs hubs. Use `swarmvault ingest` for single files."
16522
- );
17842
+ throw new Error("`swarmvault source add` supports local files, directories, public GitHub repo root URLs, and docs hubs.");
16523
17843
  }
16524
17844
  const detectedRepoRoot = await findNearestGitRoot3(absoluteInput);
16525
17845
  const repoRoot = detectedRepoRoot && !(withinRoot2(rootDir, absoluteInput) && !withinRoot2(rootDir, detectedRepoRoot)) ? detectedRepoRoot : absoluteInput;
@@ -16552,6 +17872,10 @@ async function resolveManagedSourceInput(rootDir, input) {
16552
17872
  function directorySourceIdsFor(manifests, inputPath) {
16553
17873
  return manifests.filter((manifest) => manifest.originalPath && withinRoot2(path25.resolve(inputPath), path25.resolve(manifest.originalPath))).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
16554
17874
  }
17875
+ function fileSourceIdsFor(manifests, inputPath) {
17876
+ const absoluteInput = path25.resolve(inputPath);
17877
+ return manifests.filter((manifest) => manifest.originalPath && path25.resolve(manifest.originalPath) === absoluteInput).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
17878
+ }
16555
17879
  async function syncDirectorySource(rootDir, inputPath, repoRoot) {
16556
17880
  const manifestsBefore = await listManifests(rootDir);
16557
17881
  const previousInScope = manifestsBefore.filter(
@@ -16585,6 +17909,22 @@ async function syncDirectorySource(rootDir, inputPath, repoRoot) {
16585
17909
  changed: result.imported.length + result.updated.length + removed.length > 0
16586
17910
  };
16587
17911
  }
17912
+ async function syncFileSource(rootDir, inputPath) {
17913
+ const result = await ingestInputDetailed(rootDir, inputPath);
17914
+ const manifestsAfter = await listManifests(rootDir);
17915
+ return {
17916
+ title: path25.basename(inputPath, path25.extname(inputPath)) || inputPath,
17917
+ sourceIds: fileSourceIdsFor(manifestsAfter, inputPath),
17918
+ counts: {
17919
+ scannedCount: result.scannedCount,
17920
+ importedCount: result.created.length,
17921
+ updatedCount: result.updated.length,
17922
+ removedCount: result.removed.length,
17923
+ skippedCount: result.skipped.length
17924
+ },
17925
+ changed: result.created.length + result.updated.length + result.removed.length > 0
17926
+ };
17927
+ }
16588
17928
  async function runGitCommand(cwd, args) {
16589
17929
  await new Promise((resolve, reject) => {
16590
17930
  const child = spawn2("git", args, {
@@ -16679,6 +18019,22 @@ async function syncManagedSource(rootDir, entry, options) {
16679
18019
  };
16680
18020
  }
16681
18021
  sync = await syncDirectorySource(rootDir, entry.path, entry.repoRoot);
18022
+ } else if (entry.kind === "file") {
18023
+ if (!entry.path) {
18024
+ throw new Error(`Managed source ${entry.id} is missing its file path.`);
18025
+ }
18026
+ if (!await fileExists(entry.path)) {
18027
+ return {
18028
+ ...entry,
18029
+ status: "missing",
18030
+ updatedAt: now,
18031
+ lastSyncAt: now,
18032
+ lastSyncStatus: "error",
18033
+ lastError: `File not found: ${entry.path}`,
18034
+ changed: false
18035
+ };
18036
+ }
18037
+ sync = await syncFileSource(rootDir, entry.path);
16682
18038
  } else if (entry.kind === "github_repo") {
16683
18039
  sync = await syncGitHubRepoSource(rootDir, entry);
16684
18040
  } else {
@@ -16786,7 +18142,7 @@ function renderDeterministicSourceBrief(input) {
16786
18142
  ""
16787
18143
  ].join("\n");
16788
18144
  }
16789
- async function generateSourceBriefMarkdown(rootDir, source) {
18145
+ async function generateSourceBriefMarkdownForScope(rootDir, source) {
16790
18146
  const { paths } = await loadVaultConfig(rootDir);
16791
18147
  const graph = await readJsonFile(paths.graphPath);
16792
18148
  if (!graph) {
@@ -16823,7 +18179,7 @@ Entities: ${analysis.entities.map((entity) => entity.name).join(", ") || "none"}
16823
18179
  ),
16824
18180
  prompt: [
16825
18181
  `Source title: ${source.title}`,
16826
- `Source kind: ${source.kind}`,
18182
+ `Source kind: ${source.kind ?? "source"}`,
16827
18183
  `Tracked source ids: ${source.sourceIds.join(", ") || "none"}`,
16828
18184
  "",
16829
18185
  "Pages:",
@@ -16841,12 +18197,12 @@ Entities: ${analysis.entities.map((entity) => entity.name).join(", ") || "none"}
16841
18197
  return fallback;
16842
18198
  }
16843
18199
  }
16844
- async function writeSourceBrief(rootDir, source) {
18200
+ async function writeSourceBriefForScope(rootDir, source) {
16845
18201
  if (!source.sourceIds.length) {
16846
18202
  return null;
16847
18203
  }
16848
18204
  const { paths } = await loadVaultConfig(rootDir);
16849
- const markdown = await generateSourceBriefMarkdown(rootDir, source);
18205
+ const markdown = await generateSourceBriefMarkdownForScope(rootDir, source);
16850
18206
  if (!markdown) {
16851
18207
  return null;
16852
18208
  }
@@ -16884,6 +18240,9 @@ async function writeSourceBrief(rootDir, source) {
16884
18240
  await fs21.writeFile(absolutePath, output.content, "utf8");
16885
18241
  return absolutePath;
16886
18242
  }
18243
+ async function writeSourceBrief(rootDir, source) {
18244
+ return await writeSourceBriefForScope(rootDir, scopeFromManagedSource(source));
18245
+ }
16887
18246
  async function generateBriefsForSources(rootDir, sources) {
16888
18247
  const briefPaths = /* @__PURE__ */ new Map();
16889
18248
  for (const source of sources) {
@@ -16897,6 +18256,410 @@ async function generateBriefsForSources(rootDir, sources) {
16897
18256
  }
16898
18257
  return briefPaths;
16899
18258
  }
18259
+ function renderDeterministicSourceReview(input) {
18260
+ const canonicalPages = input.sourcePages.filter((page) => page.kind === "source" || page.kind === "concept" || page.kind === "entity").slice(0, 10);
18261
+ const modulePages = input.sourcePages.filter((page) => page.kind === "module").slice(0, 8);
18262
+ const questions = uniqueStrings4(input.analyses.flatMap((analysis) => analysis.questions)).slice(0, 8);
18263
+ const concepts = uniqueStrings4(input.analyses.flatMap((analysis) => analysis.concepts.map((concept) => concept.name))).slice(0, 8);
18264
+ const entities = uniqueStrings4(input.analyses.flatMap((analysis) => analysis.entities.map((entity) => entity.name))).slice(0, 8);
18265
+ const contradictions = input.report?.contradictions.filter(
18266
+ (contradiction) => input.scope.sourceIds.includes(contradiction.sourceIdA) || input.scope.sourceIds.includes(contradiction.sourceIdB)
18267
+ ) ?? [];
18268
+ return [
18269
+ `# Source Review: ${input.scope.title}`,
18270
+ "",
18271
+ "## What This Source Contains",
18272
+ "",
18273
+ ...input.analyses.length ? input.analyses.map((analysis) => `- ${analysis.title}: ${analysis.summary}`) : ["- This source has not been analyzed yet. Compile the vault before trusting downstream pages."],
18274
+ "",
18275
+ "## Likely Canonical Pages To Update",
18276
+ "",
18277
+ ...canonicalPages.length ? canonicalPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No canonical source, concept, or entity pages are linked to this source yet."],
18278
+ "",
18279
+ "## Important Topics And Entities",
18280
+ "",
18281
+ ...concepts.length ? [`Concepts: ${concepts.join(", ")}`] : ["Concepts: none detected."],
18282
+ ...entities.length ? [`Entities: ${entities.join(", ")}`] : ["Entities: none detected."],
18283
+ ...modulePages.length ? ["", ...modulePages.map((page) => `- Module: [[${page.path.replace(/\.md$/, "")}|${page.title}]]`)] : [],
18284
+ "",
18285
+ "## Contradictions To Inspect",
18286
+ "",
18287
+ ...contradictions.length ? contradictions.map((contradiction) => `- ${contradiction.claimA} / ${contradiction.claimB}`) : ["- No contradictions are currently flagged for this source scope."],
18288
+ "",
18289
+ "## Open Questions",
18290
+ "",
18291
+ ...questions.length ? questions.map((question) => `- ${question}`) : ["- No extracted open questions yet."],
18292
+ "",
18293
+ "## Suggested Next Steps",
18294
+ "",
18295
+ ...canonicalPages.length ? canonicalPages.slice(0, 5).map((page) => `- Review [[${page.path.replace(/\.md$/, "")}|${page.title}]] for canonical updates.`) : ["- Review the source page and decide which canonical pages should exist."],
18296
+ ""
18297
+ ].join("\n");
18298
+ }
18299
+ async function generateSourceReviewMarkdown(rootDir, scope) {
18300
+ const { paths } = await loadVaultConfig(rootDir);
18301
+ let graph = await readJsonFile(paths.graphPath);
18302
+ if (!graph) {
18303
+ await compileVault(rootDir, {});
18304
+ graph = await readJsonFile(paths.graphPath);
18305
+ }
18306
+ if (!graph) {
18307
+ return null;
18308
+ }
18309
+ const sourcePages = scopedSourcePages(graph, scope.sourceIds);
18310
+ const analyses = await loadSourceAnalyses(rootDir, scope.sourceIds);
18311
+ const report = await readGraphReport(rootDir);
18312
+ const fallback = renderDeterministicSourceReview({
18313
+ scope,
18314
+ sourcePages,
18315
+ graph,
18316
+ analyses,
18317
+ report
18318
+ });
18319
+ const provider = await getProviderForTask(rootDir, "queryProvider");
18320
+ if (provider.type === "heuristic") {
18321
+ return fallback;
18322
+ }
18323
+ try {
18324
+ const schemas = await loadVaultSchemas(rootDir);
18325
+ const pageContext = sourcePages.slice(0, 12).map((page) => `- ${page.title} (${page.kind}) -> ${page.path}`).join("\n");
18326
+ const analysisContext = analyses.slice(0, 8).map(
18327
+ (analysis) => `# ${analysis.title}
18328
+ Summary: ${analysis.summary}
18329
+ Questions: ${analysis.questions.join(" | ") || "none"}
18330
+ Concepts: ${analysis.concepts.map((concept) => concept.name).join(", ") || "none"}
18331
+ Entities: ${analysis.entities.map((entity) => entity.name).join(", ") || "none"}`
18332
+ ).join("\n\n---\n\n");
18333
+ const response = await provider.generateText({
18334
+ system: buildSchemaPrompt(
18335
+ schemas.effective.global,
18336
+ "Write a concise markdown source review with sections: What This Source Contains, Likely Canonical Pages To Update, Important Topics And Entities, Contradictions To Inspect, Open Questions, Suggested Next Steps. Focus on helping a human decide what to keep, update, or question in the wiki."
18337
+ ),
18338
+ prompt: [
18339
+ `Source scope: ${scope.title}`,
18340
+ `Scope id: ${scope.id}`,
18341
+ `Tracked source ids: ${scope.sourceIds.join(", ") || "none"}`,
18342
+ "",
18343
+ "Pages:",
18344
+ pageContext || "- none",
18345
+ "",
18346
+ "Analyses:",
18347
+ analysisContext || "No analysis context available.",
18348
+ "",
18349
+ "Deterministic fallback draft:",
18350
+ fallback
18351
+ ].join("\n")
18352
+ });
18353
+ return response.text?.trim() ? response.text.trim() : fallback;
18354
+ } catch {
18355
+ return fallback;
18356
+ }
18357
+ }
18358
+ async function buildSourceReviewStagedPage(rootDir, scope) {
18359
+ const { paths } = await loadVaultConfig(rootDir);
18360
+ const markdown = await generateSourceReviewMarkdown(rootDir, scope);
18361
+ if (!markdown) {
18362
+ throw new Error(`Could not generate a source review for ${scope.id}.`);
18363
+ }
18364
+ const graph = await readJsonFile(paths.graphPath);
18365
+ const relatedPages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
18366
+ const relatedPageIds = relatedPages.slice(0, 16).map((page) => page.id);
18367
+ const relatedNodeIds = graph ? scopedNodeIds(graph, scope.sourceIds).slice(0, 24) : [];
18368
+ const projectIds = uniqueStrings4(relatedPages.flatMap((page) => page.projectIds));
18369
+ const now = (/* @__PURE__ */ new Date()).toISOString();
18370
+ const output = buildOutputPage({
18371
+ title: `Source Review: ${scope.title}`,
18372
+ question: `Review ${scope.title}`,
18373
+ answer: markdown,
18374
+ citations: scope.sourceIds,
18375
+ schemaHash: graph?.generatedAt ?? "",
18376
+ outputFormat: "report",
18377
+ relatedPageIds,
18378
+ relatedNodeIds,
18379
+ relatedSourceIds: scope.sourceIds,
18380
+ projectIds,
18381
+ extraTags: ["source-review"],
18382
+ origin: "query",
18383
+ slug: `source-reviews/${scope.id}`,
18384
+ metadata: {
18385
+ status: "draft",
18386
+ createdAt: now,
18387
+ updatedAt: now,
18388
+ compiledFrom: scope.sourceIds,
18389
+ managedBy: "system",
18390
+ confidence: 0.79
18391
+ }
18392
+ });
18393
+ return { page: output.page, content: output.content };
18394
+ }
18395
+ function classifySourceGuidePageBuckets(sourcePages, scopeSourceIds) {
18396
+ const scopeSet = new Set(scopeSourceIds);
18397
+ const canonicalPages = sourcePages.filter((page) => page.kind === "source" || page.kind === "concept" || page.kind === "entity").slice(0, 12);
18398
+ const newPages = canonicalPages.filter((page) => page.sourceIds.every((sourceId) => scopeSet.has(sourceId))).slice(0, 6);
18399
+ const reinforcingPages = canonicalPages.filter((page) => page.sourceIds.some((sourceId) => !scopeSet.has(sourceId))).slice(0, 6);
18400
+ return { canonicalPages, newPages, reinforcingPages };
18401
+ }
18402
+ function renderDeterministicSourceGuide(input) {
18403
+ const { canonicalPages, newPages, reinforcingPages } = classifySourceGuidePageBuckets(input.sourcePages, input.scope.sourceIds);
18404
+ const modulePages = input.sourcePages.filter((page) => page.kind === "module").slice(0, 6);
18405
+ const takeaways = uniqueStrings4(
18406
+ input.analyses.flatMap((analysis) => [
18407
+ analysis.summary,
18408
+ ...analysis.concepts.map((concept) => concept.description),
18409
+ ...analysis.entities.map((entity) => entity.description)
18410
+ ]).filter(Boolean).map((value) => normalizeWhitespace(value))
18411
+ ).slice(0, 7).map((value) => truncate(value, 180));
18412
+ const questions = uniqueStrings4(input.analyses.flatMap((analysis) => analysis.questions)).slice(0, 6);
18413
+ const contradictions = input.report?.contradictions.filter(
18414
+ (contradiction) => input.scope.sourceIds.includes(contradiction.sourceIdA) || input.scope.sourceIds.includes(contradiction.sourceIdB)
18415
+ ) ?? [];
18416
+ return [
18417
+ `# Source Guide: ${input.scope.title}`,
18418
+ "",
18419
+ "## What This Source Is",
18420
+ "",
18421
+ takeaways.length ? takeaways[0] : `${input.scope.title} has been compiled into the vault and is ready for guided review.`,
18422
+ "",
18423
+ "## Key Takeaways",
18424
+ "",
18425
+ ...takeaways.length ? takeaways.map((takeaway) => `- ${takeaway}`) : ["- No takeaways are available until the source is compiled."],
18426
+ "",
18427
+ "## Proposed Canonical Pages To Update",
18428
+ "",
18429
+ ...canonicalPages.length ? canonicalPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No likely canonical pages were identified yet."],
18430
+ "",
18431
+ "## New, Reinforcing, And Conflicting Claims",
18432
+ "",
18433
+ ...newPages.length ? ["New or source-local pages:", ...newPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`), ""] : [],
18434
+ ...reinforcingPages.length ? ["Reinforcing existing pages:", ...reinforcingPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`), ""] : [],
18435
+ ...contradictions.length ? ["Conflicts to judge:", ...contradictions.map((contradiction) => `- ${contradiction.claimA} / ${contradiction.claimB}`), ""] : ["Conflicts to judge:", "- No contradictions are currently flagged for this source scope.", ""],
18436
+ "## What Should Probably Stay Out For Now",
18437
+ "",
18438
+ ...modulePages.length ? ["- Avoid promoting narrow implementation details unless they matter to your thesis or recurring questions."] : ["- Avoid promoting incidental details that are not yet supported by multiple sources or clear research goals."],
18439
+ ...contradictions.length ? ["- Keep contested claims provisional until you review the conflicting evidence side by side."] : [],
18440
+ "",
18441
+ "## Needs Human Judgment",
18442
+ "",
18443
+ ...questions.length ? questions.map((question) => `- ${question}`) : ["- Decide which proposed canonical pages deserve durable summary updates."],
18444
+ "",
18445
+ "## Suggested Follow-up Questions",
18446
+ "",
18447
+ ...questions.length ? questions.map((question) => `- ${question}`) : ["- What changed in your understanding after reading this source?"],
18448
+ ""
18449
+ ].join("\n");
18450
+ }
18451
+ async function generateSourceGuideMarkdown(rootDir, scope) {
18452
+ const { paths } = await loadVaultConfig(rootDir);
18453
+ let graph = await readJsonFile(paths.graphPath);
18454
+ if (!graph) {
18455
+ await compileVault(rootDir, {});
18456
+ graph = await readJsonFile(paths.graphPath);
18457
+ }
18458
+ if (!graph) {
18459
+ return null;
18460
+ }
18461
+ const sourcePages = scopedSourcePages(graph, scope.sourceIds);
18462
+ const analyses = await loadSourceAnalyses(rootDir, scope.sourceIds);
18463
+ const report = await readGraphReport(rootDir);
18464
+ const fallback = renderDeterministicSourceGuide({
18465
+ scope,
18466
+ sourcePages,
18467
+ analyses,
18468
+ report
18469
+ });
18470
+ const provider = await getProviderForTask(rootDir, "queryProvider");
18471
+ if (provider.type === "heuristic") {
18472
+ return fallback;
18473
+ }
18474
+ try {
18475
+ const schemas = await loadVaultSchemas(rootDir);
18476
+ const { canonicalPages, newPages, reinforcingPages } = classifySourceGuidePageBuckets(sourcePages, scope.sourceIds);
18477
+ const pageContext = sourcePages.slice(0, 12).map((page) => `- ${page.title} (${page.kind}) -> ${page.path}`).join("\n");
18478
+ const analysisContext = analyses.slice(0, 8).map(
18479
+ (analysis) => `# ${analysis.title}
18480
+ Summary: ${analysis.summary}
18481
+ Questions: ${analysis.questions.join(" | ") || "none"}
18482
+ Concepts: ${analysis.concepts.map((concept) => concept.name).join(", ") || "none"}
18483
+ Entities: ${analysis.entities.map((entity) => entity.name).join(", ") || "none"}`
18484
+ ).join("\n\n---\n\n");
18485
+ const response = await provider.generateText({
18486
+ system: buildSchemaPrompt(
18487
+ schemas.effective.global,
18488
+ "Write a concise markdown source guide with sections: What This Source Is, Key Takeaways, Proposed Canonical Pages To Update, New Reinforcing And Conflicting Claims, What Should Probably Stay Out For Now, Needs Human Judgment, Suggested Follow-up Questions. Focus on helping a human integrate one source into an evolving research wiki."
18489
+ ),
18490
+ prompt: [
18491
+ `Source scope: ${scope.title}`,
18492
+ `Scope id: ${scope.id}`,
18493
+ `Tracked source ids: ${scope.sourceIds.join(", ") || "none"}`,
18494
+ `Current brief path: ${scope.briefPath ?? "none"}`,
18495
+ "",
18496
+ "Likely canonical pages:",
18497
+ canonicalPages.length ? canonicalPages.map((page) => `- ${page.title} -> ${page.path}`).join("\n") : "- none",
18498
+ "",
18499
+ "Likely source-local pages:",
18500
+ newPages.length ? newPages.map((page) => `- ${page.title} -> ${page.path}`).join("\n") : "- none",
18501
+ "",
18502
+ "Likely reinforcing pages:",
18503
+ reinforcingPages.length ? reinforcingPages.map((page) => `- ${page.title} -> ${page.path}`).join("\n") : "- none",
18504
+ "",
18505
+ "Pages:",
18506
+ pageContext || "- none",
18507
+ "",
18508
+ "Analyses:",
18509
+ analysisContext || "No analysis context available.",
18510
+ "",
18511
+ "Deterministic fallback draft:",
18512
+ fallback
18513
+ ].join("\n")
18514
+ });
18515
+ return response.text?.trim() ? response.text.trim() : fallback;
18516
+ } catch {
18517
+ return fallback;
18518
+ }
18519
+ }
18520
+ async function buildSourceGuideStagedPage(rootDir, scope) {
18521
+ const { paths } = await loadVaultConfig(rootDir);
18522
+ const markdown = await generateSourceGuideMarkdown(rootDir, scope);
18523
+ if (!markdown) {
18524
+ throw new Error(`Could not generate a source guide for ${scope.id}.`);
18525
+ }
18526
+ const graph = await readJsonFile(paths.graphPath);
18527
+ const relatedPages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
18528
+ const relatedPageIds = relatedPages.slice(0, 18).map((page) => page.id);
18529
+ const relatedNodeIds = graph ? scopedNodeIds(graph, scope.sourceIds).slice(0, 28) : [];
18530
+ const projectIds = uniqueStrings4(relatedPages.flatMap((page) => page.projectIds));
18531
+ const now = (/* @__PURE__ */ new Date()).toISOString();
18532
+ const output = buildOutputPage({
18533
+ title: `Source Guide: ${scope.title}`,
18534
+ question: `Guide ${scope.title}`,
18535
+ answer: markdown,
18536
+ citations: scope.sourceIds,
18537
+ schemaHash: graph?.generatedAt ?? "",
18538
+ outputFormat: "report",
18539
+ relatedPageIds,
18540
+ relatedNodeIds,
18541
+ relatedSourceIds: scope.sourceIds,
18542
+ projectIds,
18543
+ extraTags: ["source-guide", "guided-ingest"],
18544
+ origin: "query",
18545
+ slug: `source-guides/${scope.id}`,
18546
+ metadata: {
18547
+ status: "draft",
18548
+ createdAt: now,
18549
+ updatedAt: now,
18550
+ compiledFrom: scope.sourceIds,
18551
+ managedBy: "system",
18552
+ confidence: 0.8
18553
+ }
18554
+ });
18555
+ return { page: output.page, content: output.content };
18556
+ }
18557
+ async function stageSourceReviewForScope(rootDir, scope) {
18558
+ const output = await buildSourceReviewStagedPage(rootDir, scope);
18559
+ const approval = await stageGeneratedOutputPages(rootDir, [{ page: output.page, content: output.content, label: "source-review" }], {
18560
+ bundleType: "source_review",
18561
+ title: `Source Review: ${scope.title}`
18562
+ });
18563
+ return {
18564
+ sourceId: scope.id,
18565
+ pageId: output.page.id,
18566
+ reviewPath: path25.join(approval.approvalDir, "wiki", output.page.path),
18567
+ staged: true,
18568
+ approvalId: approval.approvalId,
18569
+ approvalDir: approval.approvalDir
18570
+ };
18571
+ }
18572
+ async function stageSourceGuideForScope(rootDir, scope) {
18573
+ const briefPath = scope.briefPath ?? await writeSourceBriefForScope(rootDir, scope) ?? void 0;
18574
+ if (briefPath) {
18575
+ await refreshVaultAfterOutputSave(rootDir);
18576
+ }
18577
+ const reviewOutput = await buildSourceReviewStagedPage(rootDir, scope);
18578
+ const guideOutput = await buildSourceGuideStagedPage(rootDir, {
18579
+ ...scope,
18580
+ briefPath
18581
+ });
18582
+ const approval = await stageGeneratedOutputPages(
18583
+ rootDir,
18584
+ [
18585
+ { page: reviewOutput.page, content: reviewOutput.content, label: "source-review" },
18586
+ { page: guideOutput.page, content: guideOutput.content, label: "source-guide" }
18587
+ ],
18588
+ {
18589
+ bundleType: "guided_source",
18590
+ title: `Guided Source: ${scope.title}`
18591
+ }
18592
+ );
18593
+ await refreshVaultAfterOutputSave(rootDir);
18594
+ return {
18595
+ sourceId: scope.id,
18596
+ pageId: guideOutput.page.id,
18597
+ guidePath: path25.join(approval.approvalDir, "wiki", guideOutput.page.path),
18598
+ reviewPageId: reviewOutput.page.id,
18599
+ reviewPath: path25.join(approval.approvalDir, "wiki", reviewOutput.page.path),
18600
+ briefPath,
18601
+ staged: true,
18602
+ approvalId: approval.approvalId,
18603
+ approvalDir: approval.approvalDir
18604
+ };
18605
+ }
18606
+ function scopeFromManagedSource(source) {
18607
+ return {
18608
+ id: source.id,
18609
+ title: source.title,
18610
+ sourceIds: source.sourceIds,
18611
+ kind: source.kind,
18612
+ briefPath: source.briefPath
18613
+ };
18614
+ }
18615
+ async function reviewSourceScope(rootDir, scope) {
18616
+ return await stageSourceReviewForScope(rootDir, scope);
18617
+ }
18618
+ async function guideSourceScope(rootDir, scope) {
18619
+ return await stageSourceGuideForScope(rootDir, scope);
18620
+ }
18621
+ async function reviewManagedSource(rootDir, id) {
18622
+ const managedSources = await loadManagedSources(rootDir);
18623
+ const managedSource = managedSources.find((source) => source.id === id);
18624
+ if (managedSource) {
18625
+ if (!await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath))) {
18626
+ await compileVault(rootDir, {});
18627
+ }
18628
+ return await stageSourceReviewForScope(rootDir, scopeFromManagedSource(managedSource));
18629
+ }
18630
+ const manifests = await listManifests(rootDir);
18631
+ const manifest = manifests.find((candidate) => candidate.sourceId === id);
18632
+ if (!manifest) {
18633
+ throw new Error(`Managed source or source id not found: ${id}`);
18634
+ }
18635
+ return await stageSourceReviewForScope(rootDir, {
18636
+ id: manifest.sourceGroupId ?? manifest.sourceId,
18637
+ title: manifest.sourceGroupTitle ?? manifest.title,
18638
+ sourceIds: manifest.sourceGroupId ? manifests.filter((candidate) => candidate.sourceGroupId === manifest.sourceGroupId).map((candidate) => candidate.sourceId) : [manifest.sourceId],
18639
+ kind: manifest.sourceKind
18640
+ });
18641
+ }
18642
+ async function guideManagedSource(rootDir, id) {
18643
+ const managedSources = await loadManagedSources(rootDir);
18644
+ const managedSource = managedSources.find((source) => source.id === id);
18645
+ if (managedSource) {
18646
+ if (!await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath))) {
18647
+ await compileVault(rootDir, {});
18648
+ }
18649
+ return await stageSourceGuideForScope(rootDir, scopeFromManagedSource(managedSource));
18650
+ }
18651
+ const manifests = await listManifests(rootDir);
18652
+ const manifest = manifests.find((candidate) => candidate.sourceId === id);
18653
+ if (!manifest) {
18654
+ throw new Error(`Managed source or source id not found: ${id}`);
18655
+ }
18656
+ return await stageSourceGuideForScope(rootDir, {
18657
+ id: manifest.sourceGroupId ?? manifest.sourceId,
18658
+ title: manifest.sourceGroupTitle ?? manifest.title,
18659
+ sourceIds: manifest.sourceGroupId ? manifests.filter((candidate) => candidate.sourceGroupId === manifest.sourceGroupId).map((candidate) => candidate.sourceId) : [manifest.sourceId],
18660
+ kind: manifest.sourceKind
18661
+ });
18662
+ }
16900
18663
  function shouldCompile(changedSources, graphExists, compileRequested) {
16901
18664
  return compileRequested && (!graphExists || changedSources.length > 0);
16902
18665
  }
@@ -16906,18 +18669,20 @@ async function listManagedSourceRecords(rootDir) {
16906
18669
  }
16907
18670
  async function addManagedSource(rootDir, input, options = {}) {
16908
18671
  const compileRequested = options.compile ?? true;
16909
- const briefRequested = options.brief ?? true;
18672
+ const guideRequested = options.guide ?? false;
18673
+ const briefRequested = guideRequested ? true : options.brief ?? true;
18674
+ const reviewRequested = guideRequested ? false : options.review ?? false;
16910
18675
  const sources = await loadManagedSources(rootDir);
16911
18676
  const resolved = await resolveManagedSourceInput(rootDir, input);
16912
18677
  const existing = sources.find((candidate) => matchesManagedSourceSpec(candidate, resolved));
16913
18678
  const now = (/* @__PURE__ */ new Date()).toISOString();
16914
18679
  const source = existing ?? {
16915
- id: resolved.kind === "directory" ? stableManagedSourceId("directory", path25.resolve(resolved.path), resolved.title) : stableManagedSourceId(resolved.kind, resolved.url, resolved.title),
18680
+ id: resolved.kind === "directory" || resolved.kind === "file" ? stableManagedSourceId(resolved.kind, path25.resolve(resolved.path), resolved.title) : stableManagedSourceId(resolved.kind, resolved.url, resolved.title),
16916
18681
  kind: resolved.kind,
16917
18682
  title: resolved.title,
16918
- path: resolved.kind === "directory" ? resolved.path : void 0,
18683
+ path: resolved.kind === "directory" || resolved.kind === "file" ? resolved.path : void 0,
16919
18684
  repoRoot: resolved.kind === "directory" ? resolved.repoRoot : void 0,
16920
- url: resolved.kind === "directory" ? void 0 : resolved.url,
18685
+ url: resolved.kind === "directory" || resolved.kind === "file" ? void 0 : resolved.url,
16921
18686
  createdAt: now,
16922
18687
  updatedAt: now,
16923
18688
  status: "ready",
@@ -16946,15 +18711,24 @@ async function addManagedSource(rootDir, input, options = {}) {
16946
18711
  };
16947
18712
  const nextSources = existing ? sources.map((candidate) => candidate.id === nextSource.id ? nextSource : candidate) : [...sources, nextSource];
16948
18713
  await saveManagedSources(rootDir, nextSources);
18714
+ const review = reviewRequested && nextSource.status === "ready" ? await stageSourceReviewForScope(rootDir, scopeFromManagedSource(nextSource)) : void 0;
18715
+ const guide = guideRequested && nextSource.status === "ready" ? await stageSourceGuideForScope(rootDir, {
18716
+ ...scopeFromManagedSource(nextSource),
18717
+ briefPath: nextSource.briefPath
18718
+ }) : void 0;
16949
18719
  return {
16950
18720
  source: nextSource,
16951
18721
  compile,
16952
- briefGenerated
18722
+ briefGenerated,
18723
+ review,
18724
+ guide
16953
18725
  };
16954
18726
  }
16955
18727
  async function reloadManagedSources(rootDir, options = {}) {
16956
18728
  const compileRequested = options.compile ?? true;
16957
- const briefRequested = options.brief ?? true;
18729
+ const guideRequested = options.guide ?? false;
18730
+ const briefRequested = guideRequested ? true : options.brief ?? true;
18731
+ const reviewRequested = guideRequested ? false : options.review ?? false;
16958
18732
  const sources = await loadManagedSources(rootDir);
16959
18733
  const selected = options.all || !options.id ? sources : sources.filter((source) => source.id === options.id);
16960
18734
  if (!selected.length) {
@@ -16990,10 +18764,23 @@ async function reloadManagedSources(rootDir, options = {}) {
16990
18764
  };
16991
18765
  });
16992
18766
  await saveManagedSources(rootDir, nextSources);
18767
+ const reviews = reviewRequested ? await Promise.all(
18768
+ nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)).filter((source) => source.status === "ready").map(async (source) => await stageSourceReviewForScope(rootDir, scopeFromManagedSource(source)))
18769
+ ) : [];
18770
+ const guides = guideRequested ? await Promise.all(
18771
+ nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)).filter((source) => source.status === "ready").map(
18772
+ async (source) => await stageSourceGuideForScope(rootDir, {
18773
+ ...scopeFromManagedSource(source),
18774
+ briefPath: source.briefPath
18775
+ })
18776
+ )
18777
+ ) : [];
16993
18778
  return {
16994
18779
  sources: nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)),
16995
18780
  compile,
16996
- briefPaths: [...briefPaths.values()]
18781
+ briefPaths: [...briefPaths.values()],
18782
+ reviews,
18783
+ guides
16997
18784
  };
16998
18785
  }
16999
18786
  async function deleteManagedSource(rootDir, id) {
@@ -17894,6 +19681,8 @@ export {
17894
19681
  getWatchStatus,
17895
19682
  getWebSearchAdapterForTask,
17896
19683
  getWorkspaceInfo,
19684
+ guideManagedSource,
19685
+ guideSourceScope,
17897
19686
  importInbox,
17898
19687
  ingestDirectory,
17899
19688
  ingestInput,
@@ -17928,10 +19717,13 @@ export {
17928
19717
  rejectApproval,
17929
19718
  reloadManagedSources,
17930
19719
  resolvePaths,
19720
+ reviewManagedSource,
19721
+ reviewSourceScope,
17931
19722
  runSchedule,
17932
19723
  runWatchCycle,
17933
19724
  searchVault,
17934
19725
  serveSchedules,
19726
+ stageGeneratedOutputPages,
17935
19727
  startGraphServer,
17936
19728
  startMcpServer,
17937
19729
  syncTrackedRepos,