@swarmvaultai/engine 0.7.30 → 0.7.31

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
@@ -22,7 +22,7 @@ import {
22
22
  uniqueBy,
23
23
  writeFileIfChanged,
24
24
  writeJsonFile
25
- } from "./chunk-N56FAH4N.js";
25
+ } from "./chunk-NECZ4MUE.js";
26
26
  import {
27
27
  estimatePageTokens,
28
28
  estimateTokens,
@@ -492,6 +492,100 @@ async function autoCommitWikiChanges(rootDir, operation, detail, options) {
492
492
  return message;
493
493
  }
494
494
 
495
+ // src/candidate-promotion.ts
496
+ var DEFAULT_PROMOTION_CONFIG = {
497
+ enabled: false,
498
+ minSources: 3,
499
+ minConfidence: 0.8,
500
+ minAgreement: 0.7,
501
+ minDegree: 2,
502
+ minAgeHours: 24,
503
+ maxPerRun: 25,
504
+ dryRun: false
505
+ };
506
+ function jaccard(left, right) {
507
+ if (left.length === 0 && right.length === 0) return 1;
508
+ const leftSet = new Set(left);
509
+ const rightSet = new Set(right);
510
+ const union = /* @__PURE__ */ new Set([...leftSet, ...rightSet]);
511
+ if (union.size === 0) return 1;
512
+ let intersection = 0;
513
+ for (const item of leftSet) {
514
+ if (rightSet.has(item)) intersection++;
515
+ }
516
+ return intersection / union.size;
517
+ }
518
+ function hoursSince(iso, now) {
519
+ const then = new Date(iso).getTime();
520
+ if (!Number.isFinite(then)) return 0;
521
+ return Math.max(0, (now - then) / (1e3 * 60 * 60));
522
+ }
523
+ function maxDegreeFor(graph, nodeIds) {
524
+ let best = 0;
525
+ const byId = new Map(graph.nodes.map((node) => [node.id, node]));
526
+ for (const nodeId of nodeIds) {
527
+ const node = byId.get(nodeId);
528
+ if (node && (node.degree ?? 0) > best) best = node.degree ?? 0;
529
+ }
530
+ return best;
531
+ }
532
+ function describeGate(result) {
533
+ const verb = result.passed ? ">=" : "<";
534
+ return `${result.gate} ${result.value.toFixed(2)} ${verb} ${result.threshold.toFixed(2)}`;
535
+ }
536
+ function evaluateCandidateForPromotion(page, graph, history, config, now = Date.now()) {
537
+ const historical = history?.[page.id];
538
+ const historicalSources = historical?.sourceIds ?? [];
539
+ const agreement = historicalSources.length ? jaccard(historicalSources, page.sourceIds) : 0;
540
+ const degree = maxDegreeFor(graph, page.nodeIds);
541
+ const ageHours = hoursSince(page.createdAt, now);
542
+ const gates = [
543
+ { gate: "sources", value: page.sourceIds.length, threshold: config.minSources, passed: page.sourceIds.length >= config.minSources },
544
+ { gate: "confidence", value: page.confidence, threshold: config.minConfidence, passed: page.confidence >= config.minConfidence },
545
+ { gate: "agreement", value: agreement, threshold: config.minAgreement, passed: agreement >= config.minAgreement },
546
+ { gate: "degree", value: degree, threshold: config.minDegree, passed: degree >= config.minDegree },
547
+ { gate: "age", value: ageHours, threshold: config.minAgeHours, passed: ageHours >= config.minAgeHours }
548
+ ];
549
+ const passedCount = gates.filter((gate) => gate.passed).length;
550
+ const promote = gates.every((gate) => gate.passed);
551
+ const score = passedCount / gates.length;
552
+ return {
553
+ pageId: page.id,
554
+ title: page.title,
555
+ kind: page.kind,
556
+ promote,
557
+ score,
558
+ gates,
559
+ reasons: gates.map(describeGate)
560
+ };
561
+ }
562
+ function sortDecisionsForPromotion(decisions) {
563
+ return [...decisions].sort((left, right) => {
564
+ if (left.promote !== right.promote) return left.promote ? -1 : 1;
565
+ if (right.score !== left.score) return right.score - left.score;
566
+ return left.pageId.localeCompare(right.pageId);
567
+ });
568
+ }
569
+ function renderPromotionSessionMarkdown(decisions, promotedPageIds, options) {
570
+ const lines = [];
571
+ lines.push(`# Auto-Promotion Run`);
572
+ lines.push("");
573
+ lines.push(`- started: ${options.startedAt}`);
574
+ lines.push(`- finished: ${options.finishedAt}`);
575
+ lines.push(`- mode: ${options.dryRun ? "dry-run" : "applied"}`);
576
+ lines.push(`- promoted: ${promotedPageIds.length}`);
577
+ lines.push(`- evaluated: ${decisions.length}`);
578
+ lines.push("");
579
+ lines.push(`| page | decision | score | reasons |`);
580
+ lines.push(`| --- | --- | --- | --- |`);
581
+ for (const decision of sortDecisionsForPromotion(decisions)) {
582
+ const decided = decision.promote ? promotedPageIds.includes(decision.pageId) ? "promoted" : "promote (dry-run)" : "skipped";
583
+ lines.push(`| ${decision.pageId} | ${decided} | ${decision.score.toFixed(2)} | ${decision.reasons.join("; ")} |`);
584
+ }
585
+ lines.push("");
586
+ return lines.join("\n");
587
+ }
588
+
495
589
  // src/graph-export.ts
496
590
  import { readFileSync } from "fs";
497
591
  import fs2 from "fs/promises";
@@ -7803,7 +7897,7 @@ function tsconfigPathAliasesForFile(repoRelativePath, config) {
7803
7897
  );
7804
7898
  const patternBase = pattern.replace("*", "");
7805
7899
  for (const candidate of [stripped, indexStripped]) {
7806
- if (candidate && candidate.startsWith(targetPrefix)) {
7900
+ if (candidate?.startsWith(targetPrefix)) {
7807
7901
  aliases.push(patternBase + candidate.slice(targetPrefix.length));
7808
7902
  }
7809
7903
  }
@@ -7816,11 +7910,11 @@ function tsconfigPathAliasesForFile(repoRelativePath, config) {
7816
7910
  }
7817
7911
  }
7818
7912
  if (config.baseUrl !== ".") {
7819
- const basePrefix = normalizeAlias(config.baseUrl) + "/";
7913
+ const basePrefix = `${normalizeAlias(config.baseUrl)}/`;
7820
7914
  if (stripped.startsWith(basePrefix)) {
7821
7915
  aliases.push(stripped.slice(basePrefix.length));
7822
7916
  }
7823
- if (indexStripped && indexStripped.startsWith(basePrefix)) {
7917
+ if (indexStripped?.startsWith(basePrefix)) {
7824
7918
  aliases.push(indexStripped.slice(basePrefix.length));
7825
7919
  }
7826
7920
  }
@@ -10995,6 +11089,37 @@ var MARKDOWN_SEMANTIC_FRONTMATTER_KEYS = [
10995
11089
  function uniqueStrings(values) {
10996
11090
  return [...new Set(values.filter(Boolean))];
10997
11091
  }
11092
+ function ingestRunStatePath(stateDir, runId) {
11093
+ return path12.join(stateDir, "ingest-runs", `${runId}.json`);
11094
+ }
11095
+ function buildIngestRunId() {
11096
+ return `ingest-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}`;
11097
+ }
11098
+ async function loadIngestRunState(stateDir, runId) {
11099
+ if (!runId) return null;
11100
+ const absolute = ingestRunStatePath(stateDir, runId);
11101
+ if (!await fileExists(absolute)) {
11102
+ throw new Error(`Ingest run state not found: ${runId}`);
11103
+ }
11104
+ const loaded = await readJsonFile(absolute);
11105
+ if (!loaded) {
11106
+ throw new Error(`Ingest run state is empty or unreadable: ${runId}`);
11107
+ }
11108
+ return loaded;
11109
+ }
11110
+ async function saveIngestRunState(stateDir, state) {
11111
+ const absolute = ingestRunStatePath(stateDir, state.runId);
11112
+ await ensureDir(path12.dirname(absolute));
11113
+ await writeJsonFile(absolute, state);
11114
+ return absolute;
11115
+ }
11116
+ async function clearIngestRunState(stateDir, runId) {
11117
+ const absolute = ingestRunStatePath(stateDir, runId);
11118
+ try {
11119
+ await fs11.rm(absolute, { force: true });
11120
+ } catch {
11121
+ }
11122
+ }
10998
11123
  function inferKind(mimeType, filePath, detectionOptions = {}) {
10999
11124
  if (inferCodeLanguage(filePath, mimeType, detectionOptions)) {
11000
11125
  return "code";
@@ -11447,7 +11572,8 @@ function normalizeIngestOptions(options) {
11447
11572
  exclude: (options?.exclude ?? []).map((pattern) => pattern.trim()).filter(Boolean),
11448
11573
  maxFiles: Math.max(1, Math.floor(options?.maxFiles ?? DEFAULT_MAX_DIRECTORY_FILES)),
11449
11574
  gitignore: options?.gitignore ?? true,
11450
- extractClasses: options?.extractClasses ?? ["first_party"]
11575
+ extractClasses: options?.extractClasses ?? ["first_party"],
11576
+ resume: options?.resume
11451
11577
  };
11452
11578
  }
11453
11579
  async function resolveRepoIngestOptions(rootDir, options) {
@@ -13489,37 +13615,73 @@ async function ingestDirectory(rootDir, inputDir, options) {
13489
13615
  skipped: result.skipped
13490
13616
  };
13491
13617
  }
13492
- const { files, skipped } = await collectDirectoryFiles(rootDir, absoluteInputDir, repoRoot, normalizedOptions);
13618
+ const collected = await collectDirectoryFiles(rootDir, absoluteInputDir, repoRoot, normalizedOptions);
13619
+ const skipped = collected.skipped;
13620
+ let files = collected.files;
13621
+ const resumeState = await loadIngestRunState(paths.stateDir, normalizedOptions.resume);
13622
+ if (resumeState) {
13623
+ const failedSet = new Set(resumeState.failed.map((entry) => entry.absolutePath));
13624
+ files = files.filter((absolutePath) => failedSet.has(absolutePath));
13625
+ }
13626
+ const runId = resumeState?.runId ?? buildIngestRunId();
13493
13627
  const imported = [];
13494
13628
  const updated = [];
13629
+ const failed = [];
13630
+ const failedRecords = [];
13495
13631
  const progress = createProgressReporter("ingest", files.length);
13496
13632
  for (const absolutePath of files) {
13633
+ const relativeForLog = toPosix(path12.relative(rootDir, absolutePath));
13497
13634
  const relativePath = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path12.relative(repoRoot, absolutePath));
13498
- const preparedInputs = await prepareFileInputs(
13499
- rootDir,
13500
- absolutePath,
13501
- repoRoot,
13502
- sourceClassForRelativePath(relativePath, normalizedOptions)
13503
- );
13504
- const result = await persistPreparedInputs(rootDir, absolutePath, preparedInputs, paths);
13505
- if (result.created.length) {
13506
- imported.push(...result.created);
13507
- }
13508
- if (result.updated.length) {
13509
- updated.push(...result.updated);
13635
+ let preparedInputs;
13636
+ try {
13637
+ preparedInputs = await prepareFileInputs(
13638
+ rootDir,
13639
+ absolutePath,
13640
+ repoRoot,
13641
+ sourceClassForRelativePath(relativePath, normalizedOptions)
13642
+ );
13643
+ } catch (error) {
13644
+ const message = error instanceof Error ? error.message : String(error);
13645
+ failed.push({ path: relativeForLog, error: message, stage: "prepare" });
13646
+ failedRecords.push({ absolutePath, path: relativeForLog, error: message, stage: "prepare" });
13647
+ progress.tick();
13648
+ continue;
13510
13649
  }
13511
- if (!result.created.length && !result.updated.length && !result.removed.length) {
13512
- skipped.push({ path: toPosix(path12.relative(rootDir, absolutePath)), reason: "duplicate_content" });
13650
+ try {
13651
+ const result = await persistPreparedInputs(rootDir, absolutePath, preparedInputs, paths);
13652
+ if (result.created.length) imported.push(...result.created);
13653
+ if (result.updated.length) updated.push(...result.updated);
13654
+ if (!result.created.length && !result.updated.length && !result.removed.length) {
13655
+ skipped.push({ path: relativeForLog, reason: "duplicate_content" });
13656
+ }
13657
+ } catch (error) {
13658
+ const message = error instanceof Error ? error.message : String(error);
13659
+ failed.push({ path: relativeForLog, error: message, stage: "persist" });
13660
+ failedRecords.push({ absolutePath, path: relativeForLog, error: message, stage: "persist" });
13513
13661
  }
13514
13662
  progress.tick();
13515
13663
  }
13516
- progress.finish(`imported=${imported.length}, updated=${updated.length}, skipped=${skipped.length}`);
13664
+ progress.finish(`imported=${imported.length}, updated=${updated.length}, skipped=${skipped.length}, failed=${failed.length}`);
13665
+ let statePath;
13666
+ if (failed.length) {
13667
+ statePath = await saveIngestRunState(paths.stateDir, {
13668
+ runId,
13669
+ inputDir: absoluteInputDir,
13670
+ repoRoot,
13671
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13672
+ failed: failedRecords
13673
+ });
13674
+ } else if (resumeState) {
13675
+ await clearIngestRunState(paths.stateDir, resumeState.runId);
13676
+ }
13517
13677
  await appendLogEntry(rootDir, "ingest_directory", toPosix(path12.relative(rootDir, absoluteInputDir)) || ".", [
13518
13678
  `repo_root=${toPosix(path12.relative(rootDir, repoRoot)) || "."}`,
13679
+ `run_id=${runId}`,
13519
13680
  `scanned=${files.length}`,
13520
13681
  `imported=${imported.length}`,
13521
13682
  `updated=${updated.length}`,
13522
- `skipped=${skipped.length}`
13683
+ `skipped=${skipped.length}`,
13684
+ `failed=${failed.length}`
13523
13685
  ]);
13524
13686
  return {
13525
13687
  inputDir: absoluteInputDir,
@@ -13527,7 +13689,10 @@ async function ingestDirectory(rootDir, inputDir, options) {
13527
13689
  scannedCount: files.length,
13528
13690
  imported,
13529
13691
  updated,
13530
- skipped
13692
+ skipped,
13693
+ failed,
13694
+ runId,
13695
+ statePath
13531
13696
  };
13532
13697
  }
13533
13698
  async function importInbox(rootDir, inputDir) {
@@ -13643,7 +13808,7 @@ async function readExtractionArtifact(rootDir, manifest) {
13643
13808
 
13644
13809
  // src/mcp.ts
13645
13810
  import fs20 from "fs/promises";
13646
- import path24 from "path";
13811
+ import path25 from "path";
13647
13812
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
13648
13813
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13649
13814
  import { z as z8 } from "zod";
@@ -18056,7 +18221,7 @@ async function resolveImageGenerationProvider(rootDir) {
18056
18221
  if (!providerConfig) {
18057
18222
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
18058
18223
  }
18059
- const { createProvider: createProvider2 } = await import("./registry-SYCRRA65.js");
18224
+ const { createProvider: createProvider2 } = await import("./registry-4C55ZCPL.js");
18060
18225
  return createProvider2(preferredProviderId, providerConfig, rootDir);
18061
18226
  }
18062
18227
  async function generateOutputArtifacts(rootDir, input) {
@@ -20877,10 +21042,9 @@ function updateCandidateHistory(compileState, page, deleted = false) {
20877
21042
  function sortGraphPages(pages) {
20878
21043
  return [...pages].sort((left, right) => left.path.localeCompare(right.path) || left.title.localeCompare(right.title));
20879
21044
  }
20880
- function computeUnifiedDiff(current, staged, label) {
21045
+ function diffLines(current, staged) {
20881
21046
  const currentLines = current.split("\n");
20882
21047
  const stagedLines = staged.split("\n");
20883
- const output = [`--- a/${label}`, `+++ b/${label}`];
20884
21048
  const n = currentLines.length;
20885
21049
  const m = stagedLines.length;
20886
21050
  const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
@@ -20889,23 +21053,107 @@ function computeUnifiedDiff(current, staged, label) {
20889
21053
  dp[i2][j2] = currentLines[i2] === stagedLines[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
20890
21054
  }
20891
21055
  }
21056
+ const lines = [];
20892
21057
  let i = 0;
20893
21058
  let j = 0;
20894
21059
  while (i < n || j < m) {
20895
21060
  if (i < n && j < m && currentLines[i] === stagedLines[j]) {
20896
- output.push(` ${currentLines[i]}`);
21061
+ lines.push({ type: "context", value: currentLines[i] });
20897
21062
  i++;
20898
21063
  j++;
20899
21064
  } else if (j < m && (i >= n || dp[i][j + 1] >= dp[i + 1][j])) {
20900
- output.push(`+${stagedLines[j]}`);
21065
+ lines.push({ type: "add", value: stagedLines[j] });
20901
21066
  j++;
20902
21067
  } else {
20903
- output.push(`-${currentLines[i]}`);
21068
+ lines.push({ type: "remove", value: currentLines[i] });
20904
21069
  i++;
20905
21070
  }
20906
21071
  }
21072
+ return lines;
21073
+ }
21074
+ function computeUnifiedDiff(current, staged, label) {
21075
+ const output = [`--- a/${label}`, `+++ b/${label}`];
21076
+ for (const line of diffLines(current, staged)) {
21077
+ const prefix = line.type === "add" ? "+" : line.type === "remove" ? "-" : " ";
21078
+ output.push(`${prefix}${line.value}`);
21079
+ }
20907
21080
  return output.join("\n");
20908
21081
  }
21082
+ var PROTECTED_FRONTMATTER_FIELDS = /* @__PURE__ */ new Set([
21083
+ "page_id",
21084
+ "source_ids",
21085
+ "node_ids",
21086
+ "freshness",
21087
+ "source_hashes",
21088
+ "source_semantic_hashes",
21089
+ "schema_hash"
21090
+ ]);
21091
+ function stableSerialize(value) {
21092
+ if (value === void 0) return "undefined";
21093
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
21094
+ if (Array.isArray(value)) return `[${value.map(stableSerialize).join(",")}]`;
21095
+ const keys = Object.keys(value).sort();
21096
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`).join(",")}}`;
21097
+ }
21098
+ function compareFrontmatter(currentData, stagedData) {
21099
+ const keys = /* @__PURE__ */ new Set([...Object.keys(currentData), ...Object.keys(stagedData)]);
21100
+ const changes = [];
21101
+ for (const key of keys) {
21102
+ const before = currentData[key];
21103
+ const after = stagedData[key];
21104
+ if (stableSerialize(before) === stableSerialize(after)) continue;
21105
+ changes.push({
21106
+ key,
21107
+ before,
21108
+ after,
21109
+ protected: PROTECTED_FRONTMATTER_FIELDS.has(key)
21110
+ });
21111
+ }
21112
+ return changes.sort((left, right) => left.key.localeCompare(right.key));
21113
+ }
21114
+ function computeStructuredDiff(current, staged, isBinaryAsset) {
21115
+ if (isBinaryAsset) {
21116
+ return {
21117
+ hunks: [],
21118
+ addedLines: 0,
21119
+ removedLines: 0,
21120
+ frontmatterChanges: []
21121
+ };
21122
+ }
21123
+ if (!current && !staged) return void 0;
21124
+ let currentData = {};
21125
+ let stagedData = {};
21126
+ let currentBody = current ?? "";
21127
+ let stagedBody = staged ?? "";
21128
+ if (current) {
21129
+ const parsed = matter10(current);
21130
+ currentData = parsed.data ?? {};
21131
+ currentBody = parsed.content;
21132
+ }
21133
+ if (staged) {
21134
+ const parsed = matter10(staged);
21135
+ stagedData = parsed.data ?? {};
21136
+ stagedBody = parsed.content;
21137
+ }
21138
+ const lines = diffLines(currentBody, stagedBody);
21139
+ const addedLines = lines.filter((line) => line.type === "add").length;
21140
+ const removedLines = lines.filter((line) => line.type === "remove").length;
21141
+ const hunks = lines.length ? [
21142
+ {
21143
+ oldStart: 1,
21144
+ oldLines: currentBody.split("\n").length,
21145
+ newStart: 1,
21146
+ newLines: stagedBody.split("\n").length,
21147
+ lines
21148
+ }
21149
+ ] : [];
21150
+ return {
21151
+ hunks,
21152
+ addedLines,
21153
+ removedLines,
21154
+ frontmatterChanges: compareFrontmatter(currentData, stagedData)
21155
+ };
21156
+ }
20909
21157
  function computeChangeSummary(current, staged, changeType) {
20910
21158
  if (changeType === "create") return "New page";
20911
21159
  if (changeType === "delete") return "Removed page";
@@ -20956,7 +21204,16 @@ async function readApproval(rootDir, approvalId, options) {
20956
21204
  stagedContent
20957
21205
  };
20958
21206
  detail.changeSummary = computeChangeSummary(detail.currentContent, detail.stagedContent, detail.changeType);
20959
- if (options?.diff && detail.currentContent && detail.stagedContent) {
21207
+ const isBinaryAsset = detail.kind === "output";
21208
+ const structured = computeStructuredDiff(detail.currentContent, detail.stagedContent, isBinaryAsset);
21209
+ if (structured) {
21210
+ detail.structuredDiff = structured;
21211
+ const protectedChanges = structured.frontmatterChanges.filter((change) => change.protected);
21212
+ if (protectedChanges.length) {
21213
+ detail.warnings = ["protected_frontmatter_changed"];
21214
+ }
21215
+ }
21216
+ if (options?.diff && detail.currentContent && detail.stagedContent && !isBinaryAsset) {
20960
21217
  detail.diff = computeUnifiedDiff(detail.currentContent, detail.stagedContent, detail.nextPath ?? detail.pageId);
20961
21218
  }
20962
21219
  return detail;
@@ -21167,6 +21424,86 @@ async function promoteCandidate(rootDir, target) {
21167
21424
  updatedAt: nextPage.updatedAt
21168
21425
  };
21169
21426
  }
21427
+ function resolvePromotionConfig(config) {
21428
+ const overrides = config.candidate?.autoPromote ?? {};
21429
+ return { ...DEFAULT_PROMOTION_CONFIG, ...overrides };
21430
+ }
21431
+ function promotionSessionTitle(promotionConfig) {
21432
+ return promotionConfig.dryRun ? "auto-promote-dry-run" : "auto-promote";
21433
+ }
21434
+ async function runAutoPromotion(rootDir, options = {}) {
21435
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
21436
+ const { config, paths } = await loadVaultConfig(rootDir);
21437
+ const base = resolvePromotionConfig(config);
21438
+ const promotionConfig = { ...base, dryRun: options.dryRun ?? base.dryRun };
21439
+ const graph = await readJsonFile(paths.graphPath);
21440
+ const compileState = await readJsonFile(paths.compileStatePath) ?? emptyCompileState();
21441
+ const candidates = (graph?.pages ?? []).filter(
21442
+ (page) => page.status === "candidate" && (page.kind === "concept" || page.kind === "entity")
21443
+ );
21444
+ const now = Date.now();
21445
+ const decisions = candidates.map(
21446
+ (page) => evaluateCandidateForPromotion(page, graph, compileState.candidateHistory, promotionConfig, now)
21447
+ );
21448
+ const sorted = sortDecisionsForPromotion(decisions);
21449
+ const acceptedIds = sorted.filter((decision) => decision.promote).slice(0, promotionConfig.maxPerRun).map((d) => d.pageId);
21450
+ const skippedIds = sorted.filter((decision) => !acceptedIds.includes(decision.pageId)).map((d) => d.pageId);
21451
+ const promotedPageIds = [];
21452
+ if (!promotionConfig.dryRun) {
21453
+ for (const pageId of acceptedIds) {
21454
+ try {
21455
+ await promoteCandidate(rootDir, pageId);
21456
+ promotedPageIds.push(pageId);
21457
+ } catch {
21458
+ }
21459
+ }
21460
+ }
21461
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
21462
+ const sessionBody = renderPromotionSessionMarkdown(decisions, promotedPageIds, {
21463
+ dryRun: promotionConfig.dryRun,
21464
+ startedAt,
21465
+ finishedAt
21466
+ });
21467
+ const { sessionPath } = await recordSession(rootDir, {
21468
+ operation: "candidate",
21469
+ title: promotionSessionTitle(promotionConfig),
21470
+ startedAt,
21471
+ finishedAt,
21472
+ success: true,
21473
+ relatedPageIds: decisions.map((decision) => decision.pageId),
21474
+ changedPages: promotedPageIds,
21475
+ lines: [
21476
+ `mode=${promotionConfig.dryRun ? "dry-run" : "applied"}`,
21477
+ `evaluated=${decisions.length}`,
21478
+ `promoted=${promotedPageIds.length}`,
21479
+ ...sessionBody.split("\n")
21480
+ ]
21481
+ });
21482
+ return {
21483
+ startedAt,
21484
+ finishedAt,
21485
+ dryRun: promotionConfig.dryRun,
21486
+ promotedPageIds,
21487
+ skippedPageIds: skippedIds,
21488
+ decisions: sorted,
21489
+ sessionPath
21490
+ };
21491
+ }
21492
+ async function previewCandidatePromotions(rootDir) {
21493
+ const { config, paths } = await loadVaultConfig(rootDir);
21494
+ const promotionConfig = resolvePromotionConfig(config);
21495
+ const graph = await readJsonFile(paths.graphPath);
21496
+ const compileState = await readJsonFile(paths.compileStatePath) ?? emptyCompileState();
21497
+ const candidates = (graph?.pages ?? []).filter(
21498
+ (page) => page.status === "candidate" && (page.kind === "concept" || page.kind === "entity")
21499
+ );
21500
+ const now = Date.now();
21501
+ return sortDecisionsForPromotion(
21502
+ candidates.map(
21503
+ (page) => evaluateCandidateForPromotion(page, graph, compileState.candidateHistory, promotionConfig, now)
21504
+ )
21505
+ );
21506
+ }
21170
21507
  async function archiveCandidate(rootDir, target) {
21171
21508
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
21172
21509
  const { paths } = await loadVaultConfig(rootDir);
@@ -21713,6 +22050,19 @@ async function compileVault(rootDir, options = {}) {
21713
22050
  pagesDropped: budgetResult.dropped.length
21714
22051
  };
21715
22052
  }
22053
+ let autoPromotionSummary;
22054
+ const promotionConfig = resolvePromotionConfig(config);
22055
+ const promotedFromAuto = [];
22056
+ if (promotionConfig.enabled && !options.approve) {
22057
+ const autoRun = await runAutoPromotion(rootDir, { dryRun: promotionConfig.dryRun });
22058
+ autoPromotionSummary = {
22059
+ evaluated: autoRun.decisions.length,
22060
+ promoted: autoRun.promotedPageIds.length,
22061
+ dryRun: autoRun.dryRun,
22062
+ sessionPath: autoRun.sessionPath
22063
+ };
22064
+ promotedFromAuto.push(...autoRun.promotedPageIds);
22065
+ }
21716
22066
  return {
21717
22067
  graphPath: paths.graphPath,
21718
22068
  pageCount: sync.allPages.length,
@@ -21723,8 +22073,9 @@ async function compileVault(rootDir, options = {}) {
21723
22073
  approvalDir: sync.approvalDir,
21724
22074
  postPassApprovalId,
21725
22075
  postPassApprovalDir,
21726
- promotedPageIds: sync.promotedPageIds,
22076
+ promotedPageIds: [...sync.promotedPageIds, ...promotedFromAuto],
21727
22077
  candidatePageCount: sync.candidatePageCount,
22078
+ autoPromotion: autoPromotionSummary,
21728
22079
  tokenStats
21729
22080
  };
21730
22081
  }
@@ -22475,128 +22826,619 @@ async function bootstrapDemo(rootDir, input) {
22475
22826
  };
22476
22827
  }
22477
22828
 
22478
- // src/mcp.ts
22479
- var SERVER_VERSION = "0.7.30";
22480
- async function createMcpServer(rootDir) {
22481
- const server = new McpServer({
22482
- name: "swarmvault",
22483
- version: SERVER_VERSION,
22484
- websiteUrl: "https://www.swarmvault.ai"
22485
- });
22486
- server.registerTool(
22487
- "workspace_info",
22488
- {
22489
- description: "Return the current SwarmVault workspace paths and high-level counts."
22490
- },
22491
- safeHandler(async () => {
22492
- const info = await getWorkspaceInfo(rootDir);
22493
- return asToolText(info);
22494
- })
22495
- );
22496
- server.registerTool(
22497
- "search_pages",
22498
- {
22499
- description: "Search compiled wiki pages using the local full-text index.",
22500
- inputSchema: {
22501
- query: z8.string().min(1).describe("Search query"),
22502
- limit: z8.number().int().min(1).max(25).optional().describe("Maximum number of results")
22503
- }
22504
- },
22505
- safeHandler(async ({ query, limit }) => {
22506
- const results = await searchVault(rootDir, query, limit ?? 5);
22507
- return asToolText(results);
22508
- })
22509
- );
22510
- server.registerTool(
22511
- "read_page",
22512
- {
22513
- description: "Read a generated wiki page by its path relative to wiki/.",
22514
- inputSchema: {
22515
- path: z8.string().min(1).describe("Path relative to wiki/, for example sources/example.md")
22516
- }
22517
- },
22518
- safeHandler(async ({ path: relativePath }) => {
22519
- const page = await readPage(rootDir, relativePath);
22520
- if (!page) {
22521
- return asToolError(`Page not found: ${relativePath}`);
22522
- }
22523
- return asToolText(page);
22524
- })
22525
- );
22526
- server.registerTool(
22527
- "list_sources",
22528
- {
22529
- description: "List source manifests in the current workspace.",
22530
- inputSchema: {
22531
- limit: z8.number().int().min(1).max(100).optional().describe("Maximum number of manifests to return")
22532
- }
22533
- },
22534
- safeHandler(async ({ limit }) => {
22535
- const manifests = await listManifests(rootDir);
22536
- return asToolText(limit ? manifests.slice(0, limit) : manifests);
22537
- })
22538
- );
22539
- server.registerTool(
22540
- "query_graph",
22541
- {
22542
- description: "Traverse the local graph from search seeds without calling a model provider.",
22543
- inputSchema: {
22544
- question: z8.string().min(1).describe("Question or graph search seed"),
22545
- traversal: z8.enum(["bfs", "dfs"]).optional().describe("Traversal strategy"),
22546
- budget: z8.number().int().min(3).max(50).optional().describe("Maximum nodes to summarize")
22547
- }
22548
- },
22549
- safeHandler(async ({ question, traversal, budget }) => {
22550
- const result = await queryGraphVault(rootDir, question, {
22551
- traversal,
22552
- budget
22553
- });
22554
- return asToolText(result);
22555
- })
22556
- );
22557
- server.registerTool(
22558
- "graph_report",
22559
- {
22560
- description: "Return the machine-readable graph report and trust artifact."
22561
- },
22562
- safeHandler(async () => {
22563
- return asToolText(await readGraphReport(rootDir) ?? { error: "Graph report not found. Run `swarmvault compile` first." });
22564
- })
22565
- );
22566
- server.registerTool(
22567
- "get_node",
22568
- {
22569
- description: "Explain a graph node, its page, community, neighbors, and group patterns.",
22570
- inputSchema: {
22571
- target: z8.string().min(1).describe("Node or page label/id")
22572
- }
22573
- },
22574
- safeHandler(async ({ target }) => {
22575
- return asToolText(await explainGraphVault(rootDir, target));
22576
- })
22577
- );
22578
- server.registerTool(
22579
- "get_hyperedges",
22580
- {
22581
- description: "List graph hyperedges, optionally filtered to a node or page target.",
22582
- inputSchema: {
22583
- target: z8.string().optional().describe("Optional node/page label or id to filter by"),
22584
- limit: z8.number().int().min(1).max(50).optional().describe("Maximum hyperedges to return")
22585
- }
22586
- },
22587
- safeHandler(async ({ target, limit }) => {
22588
- return asToolText(await listGraphHyperedges(rootDir, target, limit ?? 25));
22589
- })
22590
- );
22591
- server.registerTool(
22592
- "get_neighbors",
22593
- {
22594
- description: "Return the neighbors of a graph node or page target.",
22595
- inputSchema: {
22596
- target: z8.string().min(1).describe("Node or page label/id")
22597
- }
22598
- },
22599
- safeHandler(async ({ target }) => {
22829
+ // src/watch.ts
22830
+ import path24 from "path";
22831
+ import process3 from "process";
22832
+ import chokidar from "chokidar";
22833
+ var MAX_BACKOFF_MS = 3e4;
22834
+ var BACKOFF_THRESHOLD = 3;
22835
+ var CRITICAL_THRESHOLD = 10;
22836
+ var REPO_WATCH_IGNORES = /* @__PURE__ */ new Set([".git", ".venv"]);
22837
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
22838
+ ".ts",
22839
+ ".tsx",
22840
+ ".mts",
22841
+ ".cts",
22842
+ ".js",
22843
+ ".jsx",
22844
+ ".mjs",
22845
+ ".cjs",
22846
+ ".py",
22847
+ ".go",
22848
+ ".rs",
22849
+ ".java",
22850
+ ".kt",
22851
+ ".kts",
22852
+ ".scala",
22853
+ ".sc",
22854
+ ".dart",
22855
+ ".lua",
22856
+ ".zig",
22857
+ ".cs",
22858
+ ".php",
22859
+ ".rb",
22860
+ ".swift",
22861
+ ".c",
22862
+ ".h",
22863
+ ".cpp",
22864
+ ".cc",
22865
+ ".cxx",
22866
+ ".hpp",
22867
+ ".hxx",
22868
+ ".sh",
22869
+ ".bash",
22870
+ ".zsh",
22871
+ ".ps1",
22872
+ ".psm1",
22873
+ ".ex",
22874
+ ".exs",
22875
+ ".jl",
22876
+ ".r",
22877
+ ".R"
22878
+ ]);
22879
+ var FILE_CHANGE_RE = /^(?:add|change|unlink):(.+)$/;
22880
+ function isCodeOnlyChange(reasons) {
22881
+ for (const reason of reasons) {
22882
+ const match = reason.match(FILE_CHANGE_RE);
22883
+ if (!match) return false;
22884
+ const ext = path24.extname(match[1]).toLowerCase();
22885
+ if (!ext || !CODE_EXTENSIONS.has(ext)) return false;
22886
+ }
22887
+ return reasons.size > 0;
22888
+ }
22889
+ function hasNonCodeChanges(reasons) {
22890
+ for (const reason of reasons) {
22891
+ const match = reason.match(FILE_CHANGE_RE);
22892
+ if (!match) return true;
22893
+ const ext = path24.extname(match[1]).toLowerCase();
22894
+ if (!ext || !CODE_EXTENSIONS.has(ext)) return true;
22895
+ }
22896
+ return false;
22897
+ }
22898
+ function collectNonCodePaths(reasons) {
22899
+ const result = [];
22900
+ for (const reason of reasons) {
22901
+ const match = reason.match(FILE_CHANGE_RE);
22902
+ if (!match) {
22903
+ result.push(reason);
22904
+ continue;
22905
+ }
22906
+ const ext = path24.extname(match[1]).toLowerCase();
22907
+ if (!ext || !CODE_EXTENSIONS.has(ext)) result.push(match[1]);
22908
+ }
22909
+ return result;
22910
+ }
22911
+ function hasIgnoredRepoSegment(baseDir, targetPath) {
22912
+ const relativePath = path24.relative(baseDir, targetPath);
22913
+ if (!relativePath || relativePath.startsWith("..")) {
22914
+ return false;
22915
+ }
22916
+ return relativePath.split(path24.sep).some((segment) => REPO_WATCH_IGNORES.has(segment));
22917
+ }
22918
+ function workspaceIgnoreRoots(rootDir, paths) {
22919
+ return [
22920
+ paths.rawDir,
22921
+ paths.wikiDir,
22922
+ paths.stateDir,
22923
+ paths.agentDir,
22924
+ paths.inboxDir,
22925
+ path24.join(rootDir, ".claude"),
22926
+ path24.join(rootDir, ".cursor"),
22927
+ path24.join(rootDir, ".obsidian")
22928
+ ].map((candidate) => path24.resolve(candidate));
22929
+ }
22930
+ async function resolveWatchTargets(rootDir, paths, options) {
22931
+ const targets = /* @__PURE__ */ new Set([path24.resolve(paths.inboxDir)]);
22932
+ if (options.repo) {
22933
+ for (const repoRoot of await listTrackedRepoRoots(rootDir)) {
22934
+ targets.add(path24.resolve(repoRoot));
22935
+ }
22936
+ }
22937
+ return [...targets].sort((left, right) => left.localeCompare(right));
22938
+ }
22939
+ async function performWatchCycle(rootDir, paths, options, codeOnly = false) {
22940
+ const imported = await importInbox(rootDir, paths.inboxDir);
22941
+ const repoSync = options.repo ? await syncTrackedReposForWatch(rootDir) : null;
22942
+ const compile = await compileVault(rootDir, { codeOnly });
22943
+ const pendingSemanticRefresh = repoSync ? await mergePendingSemanticRefresh(rootDir, repoSync.pendingSemanticRefresh) : await readPendingSemanticRefresh(rootDir);
22944
+ const stalePagePaths = await markPagesStaleForSources(
22945
+ rootDir,
22946
+ pendingSemanticRefresh.map((entry) => entry.sourceId).filter((sourceId) => Boolean(sourceId))
22947
+ );
22948
+ const lintFindingCount = options.lint ? (await lintVault(rootDir)).length : void 0;
22949
+ return {
22950
+ watchedRepoRoots: repoSync?.repoRoots ?? [],
22951
+ importedCount: imported.imported.length,
22952
+ scannedCount: imported.scannedCount,
22953
+ attachmentCount: imported.attachmentCount,
22954
+ repoImportedCount: repoSync?.imported.length ?? 0,
22955
+ repoUpdatedCount: repoSync?.updated.length ?? 0,
22956
+ repoRemovedCount: repoSync?.removed.length ?? 0,
22957
+ repoScannedCount: repoSync?.scannedCount ?? 0,
22958
+ pendingSemanticRefreshCount: pendingSemanticRefresh.length,
22959
+ pendingSemanticRefreshPaths: pendingSemanticRefresh.map((entry) => entry.path),
22960
+ changedPages: [.../* @__PURE__ */ new Set([...compile.changedPages, ...stalePagePaths])],
22961
+ lintFindingCount
22962
+ };
22963
+ }
22964
+ async function runWatchCycle(rootDir, options = {}) {
22965
+ const { paths } = await initWorkspace(rootDir);
22966
+ const startedAt = /* @__PURE__ */ new Date();
22967
+ let success = true;
22968
+ let error;
22969
+ let result = {
22970
+ watchedRepoRoots: [],
22971
+ importedCount: 0,
22972
+ scannedCount: 0,
22973
+ attachmentCount: 0,
22974
+ repoImportedCount: 0,
22975
+ repoUpdatedCount: 0,
22976
+ repoRemovedCount: 0,
22977
+ repoScannedCount: 0,
22978
+ pendingSemanticRefreshCount: 0,
22979
+ pendingSemanticRefreshPaths: [],
22980
+ changedPages: []
22981
+ };
22982
+ try {
22983
+ result = await performWatchCycle(rootDir, paths, options, options.codeOnly ?? false);
22984
+ return result;
22985
+ } catch (caught) {
22986
+ success = false;
22987
+ error = caught instanceof Error ? caught.message : String(caught);
22988
+ throw caught;
22989
+ } finally {
22990
+ const finishedAt = /* @__PURE__ */ new Date();
22991
+ await recordSession(rootDir, {
22992
+ operation: "watch",
22993
+ title: `Watch cycle for ${paths.inboxDir}${options.repo ? " and tracked repos" : ""}`,
22994
+ startedAt: startedAt.toISOString(),
22995
+ finishedAt: finishedAt.toISOString(),
22996
+ success,
22997
+ error,
22998
+ changedPages: result.changedPages,
22999
+ lintFindingCount: result.lintFindingCount,
23000
+ lines: [
23001
+ "reasons=once",
23002
+ `imported=${result.importedCount}`,
23003
+ `scanned=${result.scannedCount}`,
23004
+ `attachments=${result.attachmentCount}`,
23005
+ `repo_scanned=${result.repoScannedCount}`,
23006
+ `repo_imported=${result.repoImportedCount}`,
23007
+ `repo_updated=${result.repoUpdatedCount}`,
23008
+ `repo_removed=${result.repoRemovedCount}`,
23009
+ `pending_semantic_refresh=${result.pendingSemanticRefreshCount}`,
23010
+ `lint=${result.lintFindingCount ?? 0}`
23011
+ ]
23012
+ });
23013
+ await appendWatchRun(rootDir, {
23014
+ startedAt: startedAt.toISOString(),
23015
+ finishedAt: finishedAt.toISOString(),
23016
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
23017
+ inputDir: paths.inboxDir,
23018
+ reasons: ["once"],
23019
+ importedCount: result.importedCount + result.repoImportedCount + result.repoUpdatedCount,
23020
+ scannedCount: result.scannedCount + result.repoScannedCount,
23021
+ attachmentCount: result.attachmentCount,
23022
+ changedPages: result.changedPages,
23023
+ repoImportedCount: result.repoImportedCount,
23024
+ repoUpdatedCount: result.repoUpdatedCount,
23025
+ repoRemovedCount: result.repoRemovedCount,
23026
+ repoScannedCount: result.repoScannedCount,
23027
+ pendingSemanticRefreshCount: result.pendingSemanticRefreshCount,
23028
+ pendingSemanticRefreshPaths: result.pendingSemanticRefreshPaths,
23029
+ lintFindingCount: result.lintFindingCount,
23030
+ success,
23031
+ error
23032
+ });
23033
+ await writeWatchStatusArtifact(rootDir, {
23034
+ generatedAt: finishedAt.toISOString(),
23035
+ watchedRepoRoots: result.watchedRepoRoots,
23036
+ lastRun: {
23037
+ startedAt: startedAt.toISOString(),
23038
+ finishedAt: finishedAt.toISOString(),
23039
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
23040
+ inputDir: paths.inboxDir,
23041
+ reasons: ["once"],
23042
+ importedCount: result.importedCount + result.repoImportedCount + result.repoUpdatedCount,
23043
+ scannedCount: result.scannedCount + result.repoScannedCount,
23044
+ attachmentCount: result.attachmentCount,
23045
+ changedPages: result.changedPages,
23046
+ repoImportedCount: result.repoImportedCount,
23047
+ repoUpdatedCount: result.repoUpdatedCount,
23048
+ repoRemovedCount: result.repoRemovedCount,
23049
+ repoScannedCount: result.repoScannedCount,
23050
+ pendingSemanticRefreshCount: result.pendingSemanticRefreshCount,
23051
+ pendingSemanticRefreshPaths: result.pendingSemanticRefreshPaths,
23052
+ lintFindingCount: result.lintFindingCount,
23053
+ success,
23054
+ error
23055
+ },
23056
+ pendingSemanticRefresh: await readPendingSemanticRefresh(rootDir)
23057
+ });
23058
+ }
23059
+ }
23060
+ async function watchVault(rootDir, options = {}) {
23061
+ const { paths } = await initWorkspace(rootDir);
23062
+ const baseDebounceMs = options.debounceMs ?? 900;
23063
+ const ignoredRoots = workspaceIgnoreRoots(rootDir, paths);
23064
+ const inboxWatchRoot = path24.resolve(paths.inboxDir);
23065
+ let watchTargets = await resolveWatchTargets(rootDir, paths, options);
23066
+ let timer;
23067
+ let running = false;
23068
+ let pending = false;
23069
+ let closed = false;
23070
+ let consecutiveFailures = 0;
23071
+ let currentDebounceMs = baseDebounceMs;
23072
+ const reasons = /* @__PURE__ */ new Set();
23073
+ let activeCycle = null;
23074
+ const watcher = chokidar.watch(watchTargets, {
23075
+ ignoreInitial: true,
23076
+ usePolling: true,
23077
+ interval: 100,
23078
+ ignored: (targetPath) => {
23079
+ const absolutePath = path24.resolve(targetPath);
23080
+ const primaryTarget = watchTargets.filter((watchTarget) => isPathWithin(watchTarget, absolutePath)).sort((left, right) => right.length - left.length)[0] ?? null;
23081
+ if (!primaryTarget) {
23082
+ return false;
23083
+ }
23084
+ if (primaryTarget !== inboxWatchRoot && ignoredRoots.some((ignoreRoot) => isPathWithin(ignoreRoot, absolutePath))) {
23085
+ return true;
23086
+ }
23087
+ return hasIgnoredRepoSegment(primaryTarget, absolutePath);
23088
+ },
23089
+ awaitWriteFinish: {
23090
+ stabilityThreshold: Math.max(250, Math.floor(baseDebounceMs / 2)),
23091
+ pollInterval: 100
23092
+ }
23093
+ });
23094
+ const syncWatchTargets = async () => {
23095
+ const nextTargets = await resolveWatchTargets(rootDir, paths, options);
23096
+ const nextSet = new Set(nextTargets);
23097
+ const currentSet = new Set(watchTargets);
23098
+ const toRemove = watchTargets.filter((target) => !nextSet.has(target));
23099
+ const toAdd = nextTargets.filter((target) => !currentSet.has(target));
23100
+ if (toRemove.length > 0) {
23101
+ await watcher.unwatch(toRemove);
23102
+ }
23103
+ if (toAdd.length > 0) {
23104
+ await watcher.add(toAdd);
23105
+ }
23106
+ watchTargets = nextTargets;
23107
+ };
23108
+ const schedule = (reason) => {
23109
+ if (closed) {
23110
+ return;
23111
+ }
23112
+ reasons.add(reason);
23113
+ pending = true;
23114
+ if (timer) {
23115
+ clearTimeout(timer);
23116
+ }
23117
+ timer = setTimeout(() => {
23118
+ const cycle = runCycle();
23119
+ activeCycle = cycle.finally(() => {
23120
+ if (activeCycle === cycle) {
23121
+ activeCycle = null;
23122
+ }
23123
+ });
23124
+ }, currentDebounceMs);
23125
+ };
23126
+ const runCycle = async () => {
23127
+ if (running || closed || !pending) {
23128
+ return;
23129
+ }
23130
+ pending = false;
23131
+ running = true;
23132
+ const startedAt = /* @__PURE__ */ new Date();
23133
+ const detectedCodeOnly = isCodeOnlyChange(reasons);
23134
+ const hasDeferredNonCode = !detectedCodeOnly && hasNonCodeChanges(reasons);
23135
+ const codeOnlyChange = options.codeOnly || detectedCodeOnly || hasDeferredNonCode;
23136
+ const runReasons = [...reasons];
23137
+ reasons.clear();
23138
+ if (hasDeferredNonCode) {
23139
+ const nonCodePaths = collectNonCodePaths(new Set(runReasons));
23140
+ process3.stderr.write(
23141
+ `[swarmvault watch] Non-code changes detected (${nonCodePaths.length} file(s)) \u2014 run \`swarmvault compile\` to include LLM re-analysis.
23142
+ `
23143
+ );
23144
+ } else if (codeOnlyChange) {
23145
+ process3.stderr.write("[swarmvault watch] Code-only changes detected \u2014 skipping LLM re-analysis.\n");
23146
+ }
23147
+ let importedCount = 0;
23148
+ let scannedCount = 0;
23149
+ let attachmentCount = 0;
23150
+ let repoImportedCount = 0;
23151
+ let repoUpdatedCount = 0;
23152
+ let repoRemovedCount = 0;
23153
+ let repoScannedCount = 0;
23154
+ let watchedRepoRoots = [];
23155
+ let pendingSemanticRefreshCount = 0;
23156
+ let pendingSemanticRefreshPaths = [];
23157
+ let changedPages = [];
23158
+ let lintFindingCount;
23159
+ let success = true;
23160
+ let error;
23161
+ try {
23162
+ const result = await performWatchCycle(rootDir, paths, options, codeOnlyChange);
23163
+ importedCount = result.importedCount;
23164
+ scannedCount = result.scannedCount;
23165
+ attachmentCount = result.attachmentCount;
23166
+ repoImportedCount = result.repoImportedCount;
23167
+ repoUpdatedCount = result.repoUpdatedCount;
23168
+ repoRemovedCount = result.repoRemovedCount;
23169
+ repoScannedCount = result.repoScannedCount;
23170
+ watchedRepoRoots = result.watchedRepoRoots;
23171
+ pendingSemanticRefreshCount = result.pendingSemanticRefreshCount;
23172
+ pendingSemanticRefreshPaths = result.pendingSemanticRefreshPaths;
23173
+ changedPages = result.changedPages;
23174
+ lintFindingCount = result.lintFindingCount;
23175
+ consecutiveFailures = 0;
23176
+ currentDebounceMs = baseDebounceMs;
23177
+ await syncWatchTargets();
23178
+ } catch (caught) {
23179
+ success = false;
23180
+ error = caught instanceof Error ? caught.message : String(caught);
23181
+ consecutiveFailures++;
23182
+ pending = true;
23183
+ if (consecutiveFailures >= CRITICAL_THRESHOLD) {
23184
+ process3.stderr.write(
23185
+ `[swarmvault watch] ${consecutiveFailures} consecutive failures. Check vault state. Continuing at max backoff.
23186
+ `
23187
+ );
23188
+ }
23189
+ if (consecutiveFailures >= BACKOFF_THRESHOLD) {
23190
+ const multiplier = 2 ** (consecutiveFailures - BACKOFF_THRESHOLD);
23191
+ currentDebounceMs = Math.min(baseDebounceMs * multiplier, MAX_BACKOFF_MS);
23192
+ }
23193
+ } finally {
23194
+ const finishedAt = /* @__PURE__ */ new Date();
23195
+ try {
23196
+ await recordSession(rootDir, {
23197
+ operation: "watch",
23198
+ title: `Watch cycle for ${paths.inboxDir}${options.repo ? " and tracked repos" : ""}`,
23199
+ startedAt: startedAt.toISOString(),
23200
+ finishedAt: finishedAt.toISOString(),
23201
+ success,
23202
+ error,
23203
+ changedPages,
23204
+ lintFindingCount,
23205
+ lines: [
23206
+ `reasons=${runReasons.join(",") || "none"}`,
23207
+ `code_only=${codeOnlyChange}`,
23208
+ `imported=${importedCount}`,
23209
+ `scanned=${scannedCount}`,
23210
+ `attachments=${attachmentCount}`,
23211
+ `repo_scanned=${repoScannedCount}`,
23212
+ `repo_imported=${repoImportedCount}`,
23213
+ `repo_updated=${repoUpdatedCount}`,
23214
+ `repo_removed=${repoRemovedCount}`,
23215
+ `lint=${lintFindingCount ?? 0}`
23216
+ ]
23217
+ });
23218
+ } catch {
23219
+ process3.stderr.write("[swarmvault watch] Failed to record session log.\n");
23220
+ }
23221
+ try {
23222
+ await appendWatchRun(rootDir, {
23223
+ startedAt: startedAt.toISOString(),
23224
+ finishedAt: finishedAt.toISOString(),
23225
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
23226
+ inputDir: paths.inboxDir,
23227
+ reasons: runReasons,
23228
+ importedCount: importedCount + repoImportedCount + repoUpdatedCount,
23229
+ scannedCount: scannedCount + repoScannedCount,
23230
+ attachmentCount,
23231
+ changedPages,
23232
+ repoImportedCount,
23233
+ repoUpdatedCount,
23234
+ repoRemovedCount,
23235
+ repoScannedCount,
23236
+ pendingSemanticRefreshCount,
23237
+ pendingSemanticRefreshPaths,
23238
+ lintFindingCount,
23239
+ success,
23240
+ error
23241
+ });
23242
+ } catch {
23243
+ process3.stderr.write("[swarmvault watch] Failed to append watch run.\n");
23244
+ }
23245
+ try {
23246
+ await writeWatchStatusArtifact(rootDir, {
23247
+ generatedAt: finishedAt.toISOString(),
23248
+ watchedRepoRoots,
23249
+ lastRun: {
23250
+ startedAt: startedAt.toISOString(),
23251
+ finishedAt: finishedAt.toISOString(),
23252
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
23253
+ inputDir: paths.inboxDir,
23254
+ reasons: runReasons,
23255
+ importedCount: importedCount + repoImportedCount + repoUpdatedCount,
23256
+ scannedCount: scannedCount + repoScannedCount,
23257
+ attachmentCount,
23258
+ changedPages,
23259
+ repoImportedCount,
23260
+ repoUpdatedCount,
23261
+ repoRemovedCount,
23262
+ repoScannedCount,
23263
+ pendingSemanticRefreshCount,
23264
+ pendingSemanticRefreshPaths,
23265
+ lintFindingCount,
23266
+ success,
23267
+ error
23268
+ },
23269
+ pendingSemanticRefresh: await readPendingSemanticRefresh(rootDir)
23270
+ });
23271
+ } catch {
23272
+ process3.stderr.write("[swarmvault watch] Failed to write watch status artifact.\n");
23273
+ }
23274
+ running = false;
23275
+ if (pending && !closed) {
23276
+ schedule("queued");
23277
+ }
23278
+ }
23279
+ };
23280
+ const reasonForPath = (targetPath) => {
23281
+ const baseDir = watchTargets.filter((watchTarget) => isPathWithin(watchTarget, path24.resolve(targetPath))).sort((left, right) => right.length - left.length)[0] ?? paths.inboxDir;
23282
+ return path24.relative(baseDir, targetPath) || ".";
23283
+ };
23284
+ watcher.on("add", (filePath) => schedule(`add:${reasonForPath(filePath)}`)).on("change", (filePath) => schedule(`change:${reasonForPath(filePath)}`)).on("unlink", (filePath) => schedule(`unlink:${reasonForPath(filePath)}`)).on("addDir", (dirPath) => schedule(`addDir:${reasonForPath(dirPath)}`)).on("unlinkDir", (dirPath) => schedule(`unlinkDir:${reasonForPath(dirPath)}`)).on("error", (caught) => schedule(`error:${caught instanceof Error ? caught.message : String(caught)}`));
23285
+ await new Promise((resolve, reject) => {
23286
+ const handleReady = () => {
23287
+ watcher.off("error", handleError);
23288
+ resolve();
23289
+ };
23290
+ const handleError = (caught) => {
23291
+ watcher.off("ready", handleReady);
23292
+ reject(caught);
23293
+ };
23294
+ watcher.once("ready", handleReady);
23295
+ watcher.once("error", handleError);
23296
+ });
23297
+ return {
23298
+ close: async () => {
23299
+ closed = true;
23300
+ if (timer) {
23301
+ clearTimeout(timer);
23302
+ }
23303
+ await watcher.close();
23304
+ await activeCycle;
23305
+ }
23306
+ };
23307
+ }
23308
+ async function getWatchStatus(rootDir) {
23309
+ const persisted = await readWatchStatusArtifact(rootDir);
23310
+ const watchedRepoRoots = await listTrackedRepoRoots(rootDir);
23311
+ const pendingSemanticRefresh = await readPendingSemanticRefresh(rootDir);
23312
+ return {
23313
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
23314
+ watchedRepoRoots,
23315
+ lastRun: persisted?.lastRun,
23316
+ pendingSemanticRefresh
23317
+ };
23318
+ }
23319
+
23320
+ // src/mcp.ts
23321
+ var SERVER_VERSION = "0.7.31";
23322
+ async function createMcpServer(rootDir) {
23323
+ const server = new McpServer({
23324
+ name: "swarmvault",
23325
+ version: SERVER_VERSION,
23326
+ websiteUrl: "https://www.swarmvault.ai"
23327
+ });
23328
+ server.registerTool(
23329
+ "workspace_info",
23330
+ {
23331
+ description: "Return the current SwarmVault workspace paths and high-level counts."
23332
+ },
23333
+ safeHandler(async () => {
23334
+ const info = await getWorkspaceInfo(rootDir);
23335
+ return asToolText(info);
23336
+ })
23337
+ );
23338
+ server.registerTool(
23339
+ "search_pages",
23340
+ {
23341
+ description: "Search compiled wiki pages using the local full-text index.",
23342
+ inputSchema: {
23343
+ query: z8.string().min(1).describe("Search query"),
23344
+ limit: z8.number().int().min(1).max(25).optional().describe("Maximum number of results")
23345
+ }
23346
+ },
23347
+ safeHandler(async ({ query, limit }) => {
23348
+ const results = await searchVault(rootDir, query, limit ?? 5);
23349
+ return asToolText(results);
23350
+ })
23351
+ );
23352
+ server.registerTool(
23353
+ "read_page",
23354
+ {
23355
+ description: "Read a generated wiki page by its path relative to wiki/.",
23356
+ inputSchema: {
23357
+ path: z8.string().min(1).describe("Path relative to wiki/, for example sources/example.md")
23358
+ }
23359
+ },
23360
+ safeHandler(async ({ path: relativePath }) => {
23361
+ const page = await readPage(rootDir, relativePath);
23362
+ if (!page) {
23363
+ return asToolError(`Page not found: ${relativePath}`);
23364
+ }
23365
+ return asToolText(page);
23366
+ })
23367
+ );
23368
+ server.registerTool(
23369
+ "list_sources",
23370
+ {
23371
+ description: "List source manifests in the current workspace.",
23372
+ inputSchema: {
23373
+ limit: z8.number().int().min(1).max(100).optional().describe("Maximum number of manifests to return")
23374
+ }
23375
+ },
23376
+ safeHandler(async ({ limit }) => {
23377
+ const manifests = await listManifests(rootDir);
23378
+ return asToolText(limit ? manifests.slice(0, limit) : manifests);
23379
+ })
23380
+ );
23381
+ server.registerTool(
23382
+ "query_graph",
23383
+ {
23384
+ description: "Traverse the local graph from search seeds without calling a model provider.",
23385
+ inputSchema: {
23386
+ question: z8.string().min(1).describe("Question or graph search seed"),
23387
+ traversal: z8.enum(["bfs", "dfs"]).optional().describe("Traversal strategy"),
23388
+ budget: z8.number().int().min(3).max(50).optional().describe("Maximum nodes to summarize")
23389
+ }
23390
+ },
23391
+ safeHandler(async ({ question, traversal, budget }) => {
23392
+ const result = await queryGraphVault(rootDir, question, {
23393
+ traversal,
23394
+ budget
23395
+ });
23396
+ return asToolText(result);
23397
+ })
23398
+ );
23399
+ server.registerTool(
23400
+ "graph_report",
23401
+ {
23402
+ description: "Return the machine-readable graph report and trust artifact."
23403
+ },
23404
+ safeHandler(async () => {
23405
+ return asToolText(await readGraphReport(rootDir) ?? { error: "Graph report not found. Run `swarmvault compile` first." });
23406
+ })
23407
+ );
23408
+ server.registerTool(
23409
+ "get_node",
23410
+ {
23411
+ description: "Explain a graph node, its page, community, neighbors, and group patterns.",
23412
+ inputSchema: {
23413
+ target: z8.string().min(1).describe("Node or page label/id")
23414
+ }
23415
+ },
23416
+ safeHandler(async ({ target }) => {
23417
+ return asToolText(await explainGraphVault(rootDir, target));
23418
+ })
23419
+ );
23420
+ server.registerTool(
23421
+ "get_hyperedges",
23422
+ {
23423
+ description: "List graph hyperedges, optionally filtered to a node or page target.",
23424
+ inputSchema: {
23425
+ target: z8.string().optional().describe("Optional node/page label or id to filter by"),
23426
+ limit: z8.number().int().min(1).max(50).optional().describe("Maximum hyperedges to return")
23427
+ }
23428
+ },
23429
+ safeHandler(async ({ target, limit }) => {
23430
+ return asToolText(await listGraphHyperedges(rootDir, target, limit ?? 25));
23431
+ })
23432
+ );
23433
+ server.registerTool(
23434
+ "get_neighbors",
23435
+ {
23436
+ description: "Return the neighbors of a graph node or page target.",
23437
+ inputSchema: {
23438
+ target: z8.string().min(1).describe("Node or page label/id")
23439
+ }
23440
+ },
23441
+ safeHandler(async ({ target }) => {
22600
23442
  const explanation = await explainGraphVault(rootDir, target);
22601
23443
  return asToolText(explanation.neighbors);
22602
23444
  })
@@ -22695,6 +23537,106 @@ async function createMcpServer(rootDir) {
22695
23537
  return asToolText(findings);
22696
23538
  })
22697
23539
  );
23540
+ server.registerTool(
23541
+ "list_approvals",
23542
+ {
23543
+ description: "List staged approval bundles awaiting review."
23544
+ },
23545
+ safeHandler(async () => {
23546
+ const approvals = await listApprovals(rootDir);
23547
+ return asToolText(approvals);
23548
+ })
23549
+ );
23550
+ server.registerTool(
23551
+ "read_approval",
23552
+ {
23553
+ description: "Read the details and structured diffs for an approval bundle.",
23554
+ inputSchema: {
23555
+ approvalId: z8.string().min(1).describe("Approval bundle id"),
23556
+ diff: z8.boolean().optional().describe("Include the textual unified diff alongside the structured diff")
23557
+ }
23558
+ },
23559
+ safeHandler(async ({ approvalId, diff }) => {
23560
+ const result = await readApproval(rootDir, approvalId, { diff: diff ?? true });
23561
+ return asToolText(result);
23562
+ })
23563
+ );
23564
+ server.registerTool(
23565
+ "promote_candidate",
23566
+ {
23567
+ description: "Promote a staged candidate into its active concept or entity page.",
23568
+ inputSchema: {
23569
+ target: z8.string().min(1).describe("Candidate page id or wiki/candidates path")
23570
+ }
23571
+ },
23572
+ safeHandler(async ({ target }) => {
23573
+ const result = await promoteCandidate(rootDir, target);
23574
+ return asToolText(result);
23575
+ })
23576
+ );
23577
+ server.registerTool(
23578
+ "archive_candidate",
23579
+ {
23580
+ description: "Archive a staged candidate without promoting it.",
23581
+ inputSchema: {
23582
+ target: z8.string().min(1).describe("Candidate page id or wiki/candidates path")
23583
+ }
23584
+ },
23585
+ safeHandler(async ({ target }) => {
23586
+ const result = await archiveCandidate(rootDir, target);
23587
+ return asToolText(result);
23588
+ })
23589
+ );
23590
+ server.registerTool(
23591
+ "preview_candidate_scores",
23592
+ {
23593
+ description: "Score staged candidates against the configured auto-promotion rules without promoting."
23594
+ },
23595
+ safeHandler(async () => {
23596
+ const decisions = await previewCandidatePromotions(rootDir);
23597
+ return asToolText(decisions);
23598
+ })
23599
+ );
23600
+ server.registerTool(
23601
+ "auto_promote_candidates",
23602
+ {
23603
+ description: "Apply configured auto-promotion rules to staged candidates. Requires candidate.autoPromote.enabled in config.",
23604
+ inputSchema: {
23605
+ dryRun: z8.boolean().optional().describe("Score candidates without moving files")
23606
+ }
23607
+ },
23608
+ safeHandler(async ({ dryRun }) => {
23609
+ const result = await runAutoPromotion(rootDir, { dryRun: dryRun ?? false });
23610
+ return asToolText(result);
23611
+ })
23612
+ );
23613
+ server.registerTool(
23614
+ "review_decision",
23615
+ {
23616
+ description: "Accept or reject approval bundle entries from a staged compile.",
23617
+ inputSchema: {
23618
+ approvalId: z8.string().min(1).describe("Approval bundle id as reported by list_approvals or read_approval"),
23619
+ decision: z8.enum(["accept", "reject"]).describe("Action to apply to the selected entries"),
23620
+ targets: z8.array(z8.string()).optional().describe("Specific entry page ids to act on (defaults to all pending)"),
23621
+ notes: z8.string().optional().describe("Free-form reviewer notes, surfaced in the session log")
23622
+ }
23623
+ },
23624
+ safeHandler(async ({ approvalId, decision, targets, notes }) => {
23625
+ const apply = decision === "accept" ? acceptApproval : rejectApproval;
23626
+ const result = await apply(rootDir, approvalId, targets ?? []);
23627
+ return asToolText({ ...result, notes });
23628
+ })
23629
+ );
23630
+ server.registerTool(
23631
+ "watch_status",
23632
+ {
23633
+ description: "Return the current watch-mode status: watched repos, last run summary, and pending semantic refreshes."
23634
+ },
23635
+ safeHandler(async () => {
23636
+ const status = await getWatchStatus(rootDir);
23637
+ return asToolText(status);
23638
+ })
23639
+ );
22698
23640
  server.registerResource(
22699
23641
  "swarmvault-config",
22700
23642
  "swarmvault://config",
@@ -22761,7 +23703,7 @@ async function createMcpServer(rootDir) {
22761
23703
  },
22762
23704
  async () => {
22763
23705
  const { paths } = await loadVaultConfig(rootDir);
22764
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path24.relative(paths.sessionsDir, filePath))).sort();
23706
+ const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path25.relative(paths.sessionsDir, filePath))).sort();
22765
23707
  return asTextResource("swarmvault://sessions", JSON.stringify(files, null, 2));
22766
23708
  }
22767
23709
  );
@@ -22794,7 +23736,7 @@ async function createMcpServer(rootDir) {
22794
23736
  return asTextResource(`swarmvault://pages/${encodedPath}`, `Page not found: ${relativePath}`);
22795
23737
  }
22796
23738
  const { paths } = await loadVaultConfig(rootDir);
22797
- const absolutePath = path24.resolve(paths.wikiDir, relativePath);
23739
+ const absolutePath = path25.resolve(paths.wikiDir, relativePath);
22798
23740
  return asTextResource(`swarmvault://pages/${encodedPath}`, await fs20.readFile(absolutePath, "utf8"));
22799
23741
  }
22800
23742
  );
@@ -22803,11 +23745,11 @@ async function createMcpServer(rootDir) {
22803
23745
  new ResourceTemplate("swarmvault://sessions/{path}", {
22804
23746
  list: async () => {
22805
23747
  const { paths } = await loadVaultConfig(rootDir);
22806
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path24.relative(paths.sessionsDir, filePath))).sort();
23748
+ const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path25.relative(paths.sessionsDir, filePath))).sort();
22807
23749
  return {
22808
23750
  resources: files.map((relativePath) => ({
22809
23751
  uri: `swarmvault://sessions/${encodeURIComponent(relativePath)}`,
22810
- name: path24.basename(relativePath, ".md"),
23752
+ name: path25.basename(relativePath, ".md"),
22811
23753
  title: relativePath,
22812
23754
  description: "SwarmVault session artifact",
22813
23755
  mimeType: "text/markdown"
@@ -22824,7 +23766,7 @@ async function createMcpServer(rootDir) {
22824
23766
  const { paths } = await loadVaultConfig(rootDir);
22825
23767
  const encodedPath = typeof variables.path === "string" ? variables.path : "";
22826
23768
  const relativePath = decodeURIComponent(encodedPath);
22827
- const absolutePath = path24.resolve(paths.sessionsDir, relativePath);
23769
+ const absolutePath = path25.resolve(paths.sessionsDir, relativePath);
22828
23770
  if (!isPathWithin(paths.sessionsDir, absolutePath) || !await fileExists(absolutePath)) {
22829
23771
  return asTextResource(`swarmvault://sessions/${encodedPath}`, `Session not found: ${relativePath}`);
22830
23772
  }
@@ -22888,12 +23830,12 @@ function asTextResource(uri, text) {
22888
23830
 
22889
23831
  // src/schedule.ts
22890
23832
  import fs21 from "fs/promises";
22891
- import path25 from "path";
23833
+ import path26 from "path";
22892
23834
  function scheduleStatePath(schedulesDir, jobId) {
22893
- return path25.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
23835
+ return path26.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
22894
23836
  }
22895
23837
  function scheduleLockPath(schedulesDir, jobId) {
22896
- return path25.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
23838
+ return path26.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
22897
23839
  }
22898
23840
  function parseEveryDuration(value) {
22899
23841
  const match = value.trim().match(/^(\d+)(m|h|d)$/i);
@@ -23160,7 +24102,7 @@ async function serveSchedules(rootDir, pollMs = 3e4) {
23160
24102
  // src/sources.ts
23161
24103
  import { spawn as spawn2 } from "child_process";
23162
24104
  import fs22 from "fs/promises";
23163
- import path26 from "path";
24105
+ import path27 from "path";
23164
24106
  import matter11 from "gray-matter";
23165
24107
  import { JSDOM as JSDOM3 } from "jsdom";
23166
24108
  var DEFAULT_CRAWL_MAX_PAGES = 12;
@@ -23206,24 +24148,24 @@ function emptyManagedSourceSyncCounts() {
23206
24148
  };
23207
24149
  }
23208
24150
  function withinRoot2(rootPath, targetPath) {
23209
- const relative = path26.relative(rootPath, targetPath);
23210
- return relative === "" || !relative.startsWith("..") && !path26.isAbsolute(relative);
24151
+ const relative = path27.relative(rootPath, targetPath);
24152
+ return relative === "" || !relative.startsWith("..") && !path27.isAbsolute(relative);
23211
24153
  }
23212
24154
  async function findNearestGitRoot3(startPath) {
23213
- let current = path26.resolve(startPath);
24155
+ let current = path27.resolve(startPath);
23214
24156
  try {
23215
24157
  const stat = await fs22.stat(current);
23216
24158
  if (!stat.isDirectory()) {
23217
- current = path26.dirname(current);
24159
+ current = path27.dirname(current);
23218
24160
  }
23219
24161
  } catch {
23220
- current = path26.dirname(current);
24162
+ current = path27.dirname(current);
23221
24163
  }
23222
24164
  while (true) {
23223
- if (await fileExists(path26.join(current, ".git"))) {
24165
+ if (await fileExists(path27.join(current, ".git"))) {
23224
24166
  return current;
23225
24167
  }
23226
- const parent = path26.dirname(current);
24168
+ const parent = path27.dirname(current);
23227
24169
  if (parent === current) {
23228
24170
  return null;
23229
24171
  }
@@ -23297,7 +24239,7 @@ function isAllowedDocsCandidate(candidate, startUrl) {
23297
24239
  if (candidate.origin !== startUrl.origin) {
23298
24240
  return false;
23299
24241
  }
23300
- const extension = path26.extname(candidate.pathname).toLowerCase();
24242
+ const extension = path27.extname(candidate.pathname).toLowerCase();
23301
24243
  if (extension && extension !== ".html" && extension !== ".htm" && extension !== ".md") {
23302
24244
  return false;
23303
24245
  }
@@ -23386,12 +24328,12 @@ function matchesManagedSourceSpec(existing, input) {
23386
24328
  return false;
23387
24329
  }
23388
24330
  if (input.kind === "directory" || input.kind === "file") {
23389
- return path26.resolve(existing.path ?? "") === path26.resolve(input.path);
24331
+ return path27.resolve(existing.path ?? "") === path27.resolve(input.path);
23390
24332
  }
23391
24333
  return (existing.url ?? "") === input.url;
23392
24334
  }
23393
24335
  async function resolveManagedSourceInput(rootDir, input) {
23394
- const absoluteInput = path26.resolve(rootDir, input);
24336
+ const absoluteInput = path27.resolve(rootDir, input);
23395
24337
  if (!(input.startsWith("http://") || input.startsWith("https://"))) {
23396
24338
  const stat = await fs22.stat(absoluteInput).catch(() => null);
23397
24339
  if (!stat) {
@@ -23401,7 +24343,7 @@ async function resolveManagedSourceInput(rootDir, input) {
23401
24343
  return {
23402
24344
  kind: "file",
23403
24345
  path: absoluteInput,
23404
- title: path26.basename(absoluteInput, path26.extname(absoluteInput)) || absoluteInput
24346
+ title: path27.basename(absoluteInput, path27.extname(absoluteInput)) || absoluteInput
23405
24347
  };
23406
24348
  }
23407
24349
  if (!stat.isDirectory()) {
@@ -23413,7 +24355,7 @@ async function resolveManagedSourceInput(rootDir, input) {
23413
24355
  kind: "directory",
23414
24356
  path: absoluteInput,
23415
24357
  repoRoot,
23416
- title: path26.basename(absoluteInput) || absoluteInput
24358
+ title: path27.basename(absoluteInput) || absoluteInput
23417
24359
  };
23418
24360
  }
23419
24361
  const github = normalizeGitHubRepoRootUrl(input);
@@ -23436,16 +24378,16 @@ async function resolveManagedSourceInput(rootDir, input) {
23436
24378
  };
23437
24379
  }
23438
24380
  function directorySourceIdsFor(manifests, inputPath) {
23439
- return manifests.filter((manifest) => manifest.originalPath && withinRoot2(path26.resolve(inputPath), path26.resolve(manifest.originalPath))).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
24381
+ return manifests.filter((manifest) => manifest.originalPath && withinRoot2(path27.resolve(inputPath), path27.resolve(manifest.originalPath))).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
23440
24382
  }
23441
24383
  function fileSourceIdsFor(manifests, inputPath) {
23442
- const absoluteInput = path26.resolve(inputPath);
23443
- return manifests.filter((manifest) => manifest.originalPath && path26.resolve(manifest.originalPath) === absoluteInput).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
24384
+ const absoluteInput = path27.resolve(inputPath);
24385
+ return manifests.filter((manifest) => manifest.originalPath && path27.resolve(manifest.originalPath) === absoluteInput).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
23444
24386
  }
23445
24387
  async function syncDirectorySource(rootDir, inputPath, repoRoot) {
23446
24388
  const manifestsBefore = await listManifests(rootDir);
23447
24389
  const previousInScope = manifestsBefore.filter(
23448
- (manifest) => manifest.originalPath && withinRoot2(path26.resolve(inputPath), path26.resolve(manifest.originalPath))
24390
+ (manifest) => manifest.originalPath && withinRoot2(path27.resolve(inputPath), path27.resolve(manifest.originalPath))
23449
24391
  );
23450
24392
  const result = await ingestDirectory(rootDir, inputPath, { repoRoot });
23451
24393
  const removed = [];
@@ -23453,7 +24395,7 @@ async function syncDirectorySource(rootDir, inputPath, repoRoot) {
23453
24395
  if (!manifest.originalPath) {
23454
24396
  continue;
23455
24397
  }
23456
- if (await fileExists(path26.resolve(manifest.originalPath))) {
24398
+ if (await fileExists(path27.resolve(manifest.originalPath))) {
23457
24399
  continue;
23458
24400
  }
23459
24401
  const removedManifest = await removeManifestBySourceId(rootDir, manifest.sourceId);
@@ -23463,7 +24405,7 @@ async function syncDirectorySource(rootDir, inputPath, repoRoot) {
23463
24405
  }
23464
24406
  const manifestsAfter = await listManifests(rootDir);
23465
24407
  return {
23466
- title: path26.basename(inputPath) || inputPath,
24408
+ title: path27.basename(inputPath) || inputPath,
23467
24409
  sourceIds: directorySourceIdsFor(manifestsAfter, inputPath),
23468
24410
  counts: {
23469
24411
  scannedCount: result.scannedCount,
@@ -23479,7 +24421,7 @@ async function syncFileSource(rootDir, inputPath) {
23479
24421
  const result = await ingestInputDetailed(rootDir, inputPath);
23480
24422
  const manifestsAfter = await listManifests(rootDir);
23481
24423
  return {
23482
- title: path26.basename(inputPath, path26.extname(inputPath)) || inputPath,
24424
+ title: path27.basename(inputPath, path27.extname(inputPath)) || inputPath,
23483
24425
  sourceIds: fileSourceIdsFor(manifestsAfter, inputPath),
23484
24426
  counts: {
23485
24427
  scannedCount: result.scannedCount,
@@ -23513,7 +24455,7 @@ async function runGitCommand(cwd, args) {
23513
24455
  }
23514
24456
  async function syncGitHubRepoSource(rootDir, entry) {
23515
24457
  const workingDir = await managedSourceWorkingDir(rootDir, entry.id);
23516
- const checkoutDir = path26.join(workingDir, "checkout");
24458
+ const checkoutDir = path27.join(workingDir, "checkout");
23517
24459
  await fs22.rm(checkoutDir, { recursive: true, force: true });
23518
24460
  await ensureDir(workingDir);
23519
24461
  if (!entry.url) {
@@ -23644,7 +24586,7 @@ function scopedNodeIds(graph, sourceIds) {
23644
24586
  async function loadSourceAnalyses(rootDir, sourceIds) {
23645
24587
  const { paths } = await loadVaultConfig(rootDir);
23646
24588
  const analyses = await Promise.all(
23647
- sourceIds.map(async (sourceId) => await readJsonFile(path26.join(paths.analysesDir, `${sourceId}.json`)))
24589
+ sourceIds.map(async (sourceId) => await readJsonFile(path27.join(paths.analysesDir, `${sourceId}.json`)))
23648
24590
  );
23649
24591
  return analyses.filter((analysis) => Boolean(analysis?.sourceId));
23650
24592
  }
@@ -23805,8 +24747,8 @@ async function writeSourceBriefForScope(rootDir, source) {
23805
24747
  confidence: 0.82
23806
24748
  }
23807
24749
  });
23808
- const absolutePath = path26.join(paths.wikiDir, output.page.path);
23809
- await ensureDir(path26.dirname(absolutePath));
24750
+ const absolutePath = path27.join(paths.wikiDir, output.page.path);
24751
+ await ensureDir(path27.dirname(absolutePath));
23810
24752
  await fs22.writeFile(absolutePath, output.content, "utf8");
23811
24753
  return absolutePath;
23812
24754
  }
@@ -24095,7 +25037,7 @@ function selectGuidedTargetPages(scope, sourcePages, questions) {
24095
25037
  return (matchedTargets.length ? matchedTargets : canonicalPages).slice(0, 6);
24096
25038
  }
24097
25039
  function insightRelativePathForTarget(page, scope) {
24098
- const basename = path26.basename(page.path);
25040
+ const basename = path27.basename(page.path);
24099
25041
  if (page.kind === "concept") {
24100
25042
  return `insights/concepts/${basename}`;
24101
25043
  }
@@ -24251,191 +25193,36 @@ Entities: ${analysis.entities.map((entity) => entity.name).join(", ") || "none"}
24251
25193
  "",
24252
25194
  "Analyses:",
24253
25195
  analysisContext || "No analysis context available.",
24254
- "",
24255
- "Deterministic fallback draft:",
24256
- fallback
24257
- ].join("\n")
24258
- });
24259
- return response.text?.trim() ? response.text.trim() : fallback;
24260
- } catch {
24261
- return fallback;
24262
- }
24263
- }
24264
- async function buildSourceGuideStagedPage(rootDir, scope) {
24265
- const { config, paths } = await loadVaultConfig(rootDir);
24266
- const markdown = await generateSourceGuideMarkdown(rootDir, scope);
24267
- if (!markdown) {
24268
- throw new Error(`Could not generate a source guide for ${scope.id}.`);
24269
- }
24270
- const graph = await readJsonFile(paths.graphPath);
24271
- const schemas = await loadVaultSchemas(rootDir);
24272
- const scopeManifests = manifestsForScope(graph, scope);
24273
- const relatedPages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
24274
- const contradictions = findContradictionsForScope(scope, await readGraphReport(rootDir));
24275
- const selectedTargets = selectGuidedTargetPages(scope, relatedPages, defaultGuidedSessionQuestions());
24276
- const relatedPageIds = relatedPages.slice(0, 18).map((page) => page.id);
24277
- const relatedNodeIds = graph ? scopedNodeIds(graph, scope.sourceIds).slice(0, 28) : [];
24278
- const projectIds = uniqueStrings4(relatedPages.flatMap((page) => page.projectIds));
24279
- const now = (/* @__PURE__ */ new Date()).toISOString();
24280
- const output = buildOutputPage({
24281
- title: `Source Guide: ${scope.title}`,
24282
- question: `Guide ${scope.title}`,
24283
- answer: markdown,
24284
- citations: scope.sourceIds,
24285
- schemaHash: sourceOutputSchemaHash(schemas, projectIds),
24286
- outputFormat: "report",
24287
- relatedPageIds,
24288
- relatedNodeIds,
24289
- relatedSourceIds: scope.sourceIds,
24290
- projectIds,
24291
- extraTags: ["source-guide", "guided-ingest"],
24292
- origin: "source_guide",
24293
- slug: `source-guides/${scope.id}`,
24294
- metadata: {
24295
- status: "draft",
24296
- createdAt: now,
24297
- updatedAt: now,
24298
- compiledFrom: scope.sourceIds,
24299
- managedBy: "system",
24300
- confidence: 0.8
24301
- },
24302
- frontmatter: {
24303
- profile_presets: config.profile.presets,
24304
- source_type: scopeSourceType(scope, scopeManifests),
24305
- occurred_at: scopeOccurredAt(scopeManifests),
24306
- participants: scopeParticipants(scopeManifests),
24307
- container_title: scopeContainerTitle(scopeManifests),
24308
- conversation_id: scopeConversationId(scopeManifests),
24309
- question_state: "answered",
24310
- canonical_targets: selectedTargets.map((page) => page.path),
24311
- evidence_state: contradictions.length ? "conflicting" : selectedTargets.some((page) => page.sourceIds.some((sourceId) => !scope.sourceIds.includes(sourceId))) ? "reinforcing" : selectedTargets.length ? "new" : "needs_judgment"
24312
- }
24313
- });
24314
- return { page: output.page, content: output.content };
24315
- }
24316
- async function stageSourceReviewForScope(rootDir, scope) {
24317
- const output = await buildSourceReviewStagedPage(rootDir, scope);
24318
- const approval = await stageGeneratedOutputPages(rootDir, [{ page: output.page, content: output.content, label: "source-review" }], {
24319
- bundleType: "source-review",
24320
- title: `Source Review: ${scope.title}`
24321
- });
24322
- return {
24323
- sourceId: scope.id,
24324
- pageId: output.page.id,
24325
- reviewPath: path26.join(approval.approvalDir, "wiki", output.page.path),
24326
- staged: true,
24327
- approvalId: approval.approvalId,
24328
- approvalDir: approval.approvalDir
24329
- };
24330
- }
24331
- function nextGuidedSourceSessionId(scope) {
24332
- return `source-session-${slugify(scope.id)}-${sha256(`${scope.id}:${(/* @__PURE__ */ new Date()).toISOString()}`).slice(0, 8)}`;
24333
- }
24334
- function shouldReuseGuidedSourceSession(session) {
24335
- return Boolean(session && session.status === "awaiting_input");
24336
- }
24337
- function questionAnswer(questions, id, fallback) {
24338
- return normalizeGuidedAnswerValue(questions.find((question) => question.id === id)?.answer) ?? fallback;
24339
- }
24340
- async function prepareGuidedSourceSession(rootDir, scope, answers) {
24341
- const existing = await findLatestGuidedSourceSessionByScope(rootDir, scope.id);
24342
- const now = (/* @__PURE__ */ new Date()).toISOString();
24343
- const session = shouldReuseGuidedSourceSession(existing) ? {
24344
- ...existing,
24345
- scopeTitle: scope.title,
24346
- sourceIds: scope.sourceIds,
24347
- kind: scope.kind,
24348
- questions: mergeGuidedSessionQuestions(existing.questions, answers),
24349
- updatedAt: now
24350
- } : {
24351
- sessionId: nextGuidedSourceSessionId(scope),
24352
- scopeId: scope.id,
24353
- scopeTitle: scope.title,
24354
- sourceIds: scope.sourceIds,
24355
- kind: scope.kind,
24356
- status: "awaiting_input",
24357
- createdAt: now,
24358
- updatedAt: now,
24359
- questions: mergeGuidedSessionQuestions(defaultGuidedSessionQuestions(), answers),
24360
- briefPath: scope.briefPath,
24361
- targetedPagePaths: [],
24362
- stagedUpdatePaths: []
24363
- };
24364
- const statePath = await guidedSourceSessionStatePath(rootDir, session.sessionId);
24365
- return { session, statePath };
24366
- }
24367
- async function buildSourceSessionSavedPage(rootDir, scope, session) {
24368
- const { config, paths } = await loadVaultConfig(rootDir);
24369
- let graph = await readJsonFile(paths.graphPath);
24370
- if (!graph) {
24371
- await compileVault(rootDir, {});
24372
- graph = await readJsonFile(paths.graphPath);
24373
- }
24374
- const schemas = await loadVaultSchemas(rootDir);
24375
- const scopeManifests = manifestsForScope(graph, scope);
24376
- const sourcePages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
24377
- const analyses = await loadSourceAnalyses(rootDir, scope.sourceIds);
24378
- const report = await readGraphReport(rootDir);
24379
- const contradictions = findContradictionsForScope(scope, report);
24380
- const relatedPageIds = uniqueStrings4([
24381
- ...sourcePages.slice(0, 18).map((page) => page.id),
24382
- ...session.targetedPagePaths.map((relativePath) => {
24383
- const page = graph?.pages.find((candidate) => candidate.path === relativePath);
24384
- return page?.id ?? "";
24385
- })
24386
- ]);
24387
- const relatedNodeIds = graph ? scopedNodeIds(graph, scope.sourceIds).slice(0, 28) : [];
24388
- const projectIds = uniqueStrings4(sourcePages.flatMap((page) => page.projectIds));
24389
- const evidenceState = contradictions.length > 0 ? "conflicting" : session.targetedPagePaths.some(
24390
- (targetPath) => sourcePages.some((page) => page.path === targetPath && page.sourceIds.some((sourceId) => !scope.sourceIds.includes(sourceId)))
24391
- ) ? "reinforcing" : session.targetedPagePaths.length ? "new" : "needs_judgment";
24392
- const relativeBriefPath = session.briefPath && path26.isAbsolute(session.briefPath) ? path26.relative(paths.wikiDir, session.briefPath) : session.briefPath;
24393
- const sessionMarkdown = [
24394
- `# Guided Session: ${scope.title}`,
24395
- "",
24396
- `Status: \`${session.status}\``,
24397
- `Session ID: \`${session.sessionId}\``,
24398
- ...session.approvalId ? [`Approval Bundle: \`${session.approvalId}\``] : [],
24399
- ...relativeBriefPath ? [`Brief: \`${relativeBriefPath}\``] : [],
24400
- "",
24401
- "## What This Source Is",
24402
- "",
24403
- ...analyses.length ? analyses.slice(0, 6).map((analysis) => `- ${analysis.title}: ${analysis.summary}`) : ["- Awaiting compile context."],
24404
- "",
24405
- "## Guided Questions",
24406
- "",
24407
- ...session.questions.flatMap((question) => [`### ${question.prompt}`, "", question.answer ?? "_Awaiting input._", ""]),
24408
- "## Proposed Wiki Targets",
24409
- "",
24410
- ...session.targetedPagePaths.length ? session.targetedPagePaths.map((targetPath) => `- [[${targetPath.replace(/\.md$/, "")}]]`) : ["- No canonical update targets selected yet."],
24411
- "",
24412
- "## Conflicts And Judgment Calls",
24413
- "",
24414
- ...contradictions.length ? contradictions.map((contradiction) => `- ${contradiction.claimA} / ${contradiction.claimB}`) : ["- No contradictions are currently flagged for this source scope."],
24415
- "",
24416
- "## Follow-up Questions",
24417
- "",
24418
- ...(() => {
24419
- const followups = questionAnswer(session.questions, "followups", "");
24420
- if (followups) {
24421
- return followups.split(/\n+/).map((line) => line.trim()).filter(Boolean).map((line) => `- ${line.replace(/^-+\s*/, "")}`);
24422
- }
24423
- const analysisQuestions = uniqueStrings4(analyses.flatMap((analysis) => analysis.questions)).slice(0, 6);
24424
- return analysisQuestions.length ? analysisQuestions.map((question) => `- ${question}`) : ["- No follow-up questions recorded yet."];
24425
- })(),
24426
- "",
24427
- "## Related Artifacts",
24428
- "",
24429
- `- [[outputs/source-briefs/${scope.id}|Source Brief]]`,
24430
- `- [[outputs/source-reviews/${scope.id}|Source Review]]`,
24431
- `- [[outputs/source-guides/${scope.id}|Source Guide]]`,
24432
- ""
24433
- ].join("\n");
25196
+ "",
25197
+ "Deterministic fallback draft:",
25198
+ fallback
25199
+ ].join("\n")
25200
+ });
25201
+ return response.text?.trim() ? response.text.trim() : fallback;
25202
+ } catch {
25203
+ return fallback;
25204
+ }
25205
+ }
25206
+ async function buildSourceGuideStagedPage(rootDir, scope) {
25207
+ const { config, paths } = await loadVaultConfig(rootDir);
25208
+ const markdown = await generateSourceGuideMarkdown(rootDir, scope);
25209
+ if (!markdown) {
25210
+ throw new Error(`Could not generate a source guide for ${scope.id}.`);
25211
+ }
25212
+ const graph = await readJsonFile(paths.graphPath);
25213
+ const schemas = await loadVaultSchemas(rootDir);
25214
+ const scopeManifests = manifestsForScope(graph, scope);
25215
+ const relatedPages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
25216
+ const contradictions = findContradictionsForScope(scope, await readGraphReport(rootDir));
25217
+ const selectedTargets = selectGuidedTargetPages(scope, relatedPages, defaultGuidedSessionQuestions());
25218
+ const relatedPageIds = relatedPages.slice(0, 18).map((page) => page.id);
25219
+ const relatedNodeIds = graph ? scopedNodeIds(graph, scope.sourceIds).slice(0, 28) : [];
25220
+ const projectIds = uniqueStrings4(relatedPages.flatMap((page) => page.projectIds));
24434
25221
  const now = (/* @__PURE__ */ new Date()).toISOString();
24435
25222
  const output = buildOutputPage({
24436
- title: `Guided Session: ${scope.title}`,
24437
- question: `Guided Session ${scope.title}`,
24438
- answer: sessionMarkdown,
25223
+ title: `Source Guide: ${scope.title}`,
25224
+ question: `Guide ${scope.title}`,
25225
+ answer: markdown,
24439
25226
  citations: scope.sourceIds,
24440
25227
  schemaHash: sourceOutputSchemaHash(schemas, projectIds),
24441
25228
  outputFormat: "report",
@@ -24443,1123 +25230,787 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
24443
25230
  relatedNodeIds,
24444
25231
  relatedSourceIds: scope.sourceIds,
24445
25232
  projectIds,
24446
- extraTags: ["source-session", "guided-session"],
24447
- origin: "source_session",
24448
- slug: `source-sessions/${scope.id}`,
25233
+ extraTags: ["source-guide", "guided-ingest"],
25234
+ origin: "source_guide",
25235
+ slug: `source-guides/${scope.id}`,
24449
25236
  metadata: {
24450
- status: "active",
25237
+ status: "draft",
24451
25238
  createdAt: now,
24452
25239
  updatedAt: now,
24453
25240
  compiledFrom: scope.sourceIds,
24454
25241
  managedBy: "system",
24455
- confidence: 0.81
24456
- },
24457
- frontmatter: {
24458
- profile_presets: config.profile.presets,
24459
- source_type: scopeSourceType(scope, scopeManifests),
24460
- occurred_at: scopeOccurredAt(scopeManifests),
24461
- participants: scopeParticipants(scopeManifests),
24462
- container_title: scopeContainerTitle(scopeManifests),
24463
- conversation_id: scopeConversationId(scopeManifests),
24464
- session_status: session.status,
24465
- question_state: questionStateForSession(session),
24466
- canonical_targets: session.targetedPagePaths,
24467
- evidence_state: evidenceState
24468
- }
24469
- });
24470
- return { page: output.page, content: output.content };
24471
- }
24472
- async function persistSourceSessionPage(rootDir, scope, session) {
24473
- const { paths } = await loadVaultConfig(rootDir);
24474
- const output = await buildSourceSessionSavedPage(rootDir, scope, session);
24475
- const absolutePath = path26.join(paths.wikiDir, output.page.path);
24476
- await ensureDir(path26.dirname(absolutePath));
24477
- await fs22.writeFile(absolutePath, output.content, "utf8");
24478
- return { pageId: output.page.id, sessionPath: absolutePath };
24479
- }
24480
- async function buildGuidedUpdatePages(rootDir, scope, session) {
24481
- const { config, paths } = await loadVaultConfig(rootDir);
24482
- let graph = await readJsonFile(paths.graphPath);
24483
- if (!graph) {
24484
- await compileVault(rootDir, {});
24485
- graph = await readJsonFile(paths.graphPath);
24486
- }
24487
- if (!graph) {
24488
- return [];
24489
- }
24490
- const sourcePages = scopedSourcePages(graph, scope.sourceIds);
24491
- const scopeManifests = manifestsForScope(graph, scope);
24492
- const analyses = await loadSourceAnalyses(rootDir, scope.sourceIds);
24493
- const report = await readGraphReport(rootDir);
24494
- const contradictions = findContradictionsForScope(scope, report);
24495
- const selectedTargets = selectGuidedTargetPages(scope, sourcePages, session.questions);
24496
- const useCanonicalTargets = config.profile.guidedSessionMode === "canonical_review" && selectedTargets.length > 0;
24497
- const targetPages = useCanonicalTargets ? selectedTargets : [selectedTargets[0] ?? null];
24498
- session.targetedPagePaths = uniqueStrings4(
24499
- useCanonicalTargets ? selectedTargets.map((page) => page.path) : selectedTargets.length ? selectedTargets.map((page) => page.path) : session.targetedPagePaths
24500
- );
24501
- return await Promise.all(
24502
- targetPages.map(async (targetPage) => {
24503
- const evidenceState = classifyGuidedEvidenceState(scope, targetPage, contradictions);
24504
- const relativePath = useCanonicalTargets && targetPage ? targetPage.path : targetPage ? insightRelativePathForTarget(targetPage, scope) : `insights/topics/${slugify(scope.title)}.md`;
24505
- const absolutePath = path26.join(paths.wikiDir, relativePath);
24506
- const existingContent = await fileExists(absolutePath) ? await fs22.readFile(absolutePath, "utf8") : "";
24507
- const parsed = existingContent ? matter11(existingContent) : { data: {}, content: "" };
24508
- const existingData = parsed.data;
24509
- const existingSourceIds = Array.isArray(existingData.source_ids) ? existingData.source_ids.filter((value) => typeof value === "string") : [];
24510
- const existingProjectIds = Array.isArray(existingData.project_ids) ? existingData.project_ids.filter((value) => typeof value === "string") : [];
24511
- const existingNodeIds = Array.isArray(existingData.node_ids) ? existingData.node_ids.filter((value) => typeof value === "string") : [];
24512
- const existingBacklinks = Array.isArray(existingData.backlinks) ? existingData.backlinks.filter((value) => typeof value === "string") : [];
24513
- const createdAt = typeof existingData.created_at === "string" && existingData.created_at.trim() ? existingData.created_at : (/* @__PURE__ */ new Date()).toISOString();
24514
- const title = typeof existingData.title === "string" && existingData.title.trim() || (useCanonicalTargets && targetPage ? targetPage.title : targetPage ? insightTitleForTarget(targetPage, scope) : `${scope.title} Notes`);
24515
- const baseBody = parsed.content.trim() ? parsed.content.trim() : [
24516
- `# ${title}`,
24517
- "",
24518
- useCanonicalTargets ? "Canonical page maintained by SwarmVault. Guided sessions stage replaceable update blocks here for approval." : "Human-curated insight page. Guided sessions stage replaceable update blocks here.",
24519
- ""
24520
- ].join("\n");
24521
- const importance = questionAnswer(
24522
- session.questions,
24523
- "importance",
24524
- "Capture the most important new ideas from this source before treating them as canonical."
24525
- );
24526
- const exclude = questionAnswer(
24527
- session.questions,
24528
- "exclude",
24529
- "Keep uncertain or incidental details provisional until they matter to the research thread."
24530
- );
24531
- const conflictNotes = questionAnswer(
24532
- session.questions,
24533
- "conflicts",
24534
- contradictions.length ? "Review the conflicting evidence before accepting any canonical summary changes." : "No explicit conflicts were called out."
24535
- );
24536
- const followups = questionAnswer(session.questions, "followups", "Track follow-up questions on the source session page.");
24537
- const updateBlock = [
24538
- `## Guided Session Update: ${scope.title}`,
24539
- "",
24540
- `Evidence State: \`${evidenceState}\``,
24541
- `Session: [[outputs/source-sessions/${scope.id}|Guided Session]]`,
24542
- `Source Guide: [[outputs/source-guides/${scope.id}|Source Guide]]`,
24543
- "",
24544
- "### What Matters Now",
24545
- "",
24546
- importance,
24547
- "",
24548
- "### Proposed Integration",
24549
- "",
24550
- targetPage ? `- Fold the strongest source-backed takeaways into [[${targetPage.path.replace(/\.md$/, "")}|${targetPage.title}]].` : `- Start a durable topic note for ${scope.title}.`,
24551
- ...analyses.slice(0, 5).map((analysis) => `- ${truncate(normalizeWhitespace(analysis.summary), 180)}`),
24552
- "",
24553
- "### Keep Provisional Or Out",
24554
- "",
24555
- exclude,
24556
- "",
24557
- "### Reinforcing Or Conflicting Notes",
24558
- "",
24559
- conflictNotes,
24560
- ...contradictions.length ? ["", ...contradictions.slice(0, 4).map((contradiction) => `- ${contradiction.claimA} / ${contradiction.claimB}`)] : [],
24561
- "",
24562
- "### Follow-up Questions",
24563
- "",
24564
- followups,
24565
- ""
24566
- ].join("\n");
24567
- const nextBody = replaceMarkedSection(baseBody, scope.id, updateBlock);
24568
- const content = matter11.stringify(
24569
- `${nextBody.trimEnd()}
24570
- `,
24571
- JSON.parse(
24572
- JSON.stringify({
24573
- ...existingData,
24574
- page_id: typeof existingData.page_id === "string" && existingData.page_id.trim() || (useCanonicalTargets && targetPage ? targetPage.id : `insight:${slugify(relativePath.replace(/\.md$/, ""))}`),
24575
- kind: useCanonicalTargets && targetPage ? targetPage.kind : "insight",
24576
- title,
24577
- tags: uniqueStrings4([
24578
- ...Array.isArray(existingData.tags) ? existingData.tags.filter((value) => typeof value === "string") : [],
24579
- ...useCanonicalTargets ? ["guided-session", `guided/${targetPage?.kind ?? "page"}`] : insightTagsForTarget(targetPage)
24580
- ]),
24581
- source_ids: uniqueStrings4([...existingSourceIds, ...scope.sourceIds]),
24582
- project_ids: uniqueStrings4([...existingProjectIds, ...targetPage?.projectIds ?? []]),
24583
- node_ids: uniqueStrings4([...existingNodeIds, ...targetPage?.nodeIds ?? []]),
24584
- freshness: "fresh",
24585
- status: existingData.status === "archived" ? "archived" : "active",
24586
- confidence: 0.83,
24587
- created_at: createdAt,
24588
- updated_at: (/* @__PURE__ */ new Date()).toISOString(),
24589
- compiled_from: uniqueStrings4([
24590
- ...Array.isArray(existingData.compiled_from) ? existingData.compiled_from.filter((value) => typeof value === "string") : [],
24591
- ...scope.sourceIds
24592
- ]),
24593
- managed_by: typeof existingData.managed_by === "string" && (existingData.managed_by === "human" || existingData.managed_by === "system") ? existingData.managed_by : useCanonicalTargets ? "system" : "human",
24594
- backlinks: uniqueStrings4([
24595
- ...existingBacklinks,
24596
- ...targetPage ? [targetPage.id] : [],
24597
- `output:source-sessions/${scope.id}`,
24598
- `output:source-guides/${scope.id}`
24599
- ]),
24600
- schema_hash: typeof existingData.schema_hash === "string" ? existingData.schema_hash : "",
24601
- source_hashes: existingData.source_hashes && typeof existingData.source_hashes === "object" ? existingData.source_hashes : {},
24602
- source_semantic_hashes: existingData.source_semantic_hashes && typeof existingData.source_semantic_hashes === "object" ? existingData.source_semantic_hashes : {},
24603
- profile_presets: config.profile.presets,
24604
- source_type: scopeSourceType(scope, scopeManifests),
24605
- occurred_at: scopeOccurredAt(scopeManifests),
24606
- participants: scopeParticipants(scopeManifests),
24607
- container_title: scopeContainerTitle(scopeManifests),
24608
- conversation_id: scopeConversationId(scopeManifests),
24609
- session_status: session.status,
24610
- question_state: questionStateForSession(session),
24611
- canonical_targets: useCanonicalTargets ? selectedTargets.map((page2) => page2.path) : [],
24612
- evidence_state: evidenceState
24613
- })
24614
- )
24615
- );
24616
- const page = parseStoredPage(relativePath, content, {
24617
- createdAt,
24618
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
24619
- });
24620
- if (!useCanonicalTargets && !selectedTargets.length) {
24621
- session.targetedPagePaths = uniqueStrings4([...session.targetedPagePaths, relativePath]);
24622
- }
24623
- return { page, content, label: "guided-update" };
24624
- })
24625
- );
24626
- }
24627
- async function stageSourceGuideForScope(rootDir, scope, options = {}) {
24628
- const { session, statePath } = await prepareGuidedSourceSession(rootDir, scope, options.answers);
24629
- const briefPath = scope.briefPath ?? session.briefPath ?? await writeSourceBriefForScope(rootDir, scope) ?? void 0;
24630
- session.briefPath = briefPath;
24631
- if (briefPath) {
24632
- await refreshVaultAfterOutputSave(rootDir);
24633
- }
24634
- if (answeredGuidedSessionQuestions(session.questions).length === 0) {
24635
- session.status = "awaiting_input";
24636
- const persisted2 = await persistSourceSessionPage(rootDir, scope, session);
24637
- session.sessionPath = persisted2.sessionPath;
24638
- await writeGuidedSourceSession(rootDir, session);
24639
- await refreshVaultAfterOutputSave(rootDir);
24640
- return {
24641
- sourceId: scope.id,
24642
- sessionId: session.sessionId,
24643
- sessionPath: persisted2.sessionPath,
24644
- sessionStatePath: statePath,
24645
- status: session.status,
24646
- questions: session.questions,
24647
- awaitingInput: true,
24648
- targetedPagePaths: session.targetedPagePaths,
24649
- stagedUpdatePaths: session.stagedUpdatePaths,
24650
- briefPath,
24651
- staged: false
24652
- };
24653
- }
24654
- session.status = "ready_to_stage";
24655
- await writeGuidedSourceSession(rootDir, session);
24656
- const reviewOutput = await buildSourceReviewStagedPage(rootDir, scope);
24657
- const guideOutput = await buildSourceGuideStagedPage(rootDir, {
24658
- ...scope,
24659
- briefPath
24660
- });
24661
- const guidedUpdates = await buildGuidedUpdatePages(rootDir, scope, session);
24662
- session.stagedUpdatePaths = guidedUpdates.map((item) => item.page.path);
24663
- const approval = await stageGeneratedOutputPages(
24664
- rootDir,
24665
- [
24666
- { page: reviewOutput.page, content: reviewOutput.content, label: "source-review" },
24667
- { page: guideOutput.page, content: guideOutput.content, label: "source-guide" },
24668
- ...guidedUpdates
24669
- ],
24670
- {
24671
- bundleType: "guided-session",
24672
- title: `Guided Session: ${scope.title}`,
24673
- sourceSessionId: session.sessionId
25242
+ confidence: 0.8
25243
+ },
25244
+ frontmatter: {
25245
+ profile_presets: config.profile.presets,
25246
+ source_type: scopeSourceType(scope, scopeManifests),
25247
+ occurred_at: scopeOccurredAt(scopeManifests),
25248
+ participants: scopeParticipants(scopeManifests),
25249
+ container_title: scopeContainerTitle(scopeManifests),
25250
+ conversation_id: scopeConversationId(scopeManifests),
25251
+ question_state: "answered",
25252
+ canonical_targets: selectedTargets.map((page) => page.path),
25253
+ evidence_state: contradictions.length ? "conflicting" : selectedTargets.some((page) => page.sourceIds.some((sourceId) => !scope.sourceIds.includes(sourceId))) ? "reinforcing" : selectedTargets.length ? "new" : "needs_judgment"
24674
25254
  }
24675
- );
24676
- session.status = "staged";
24677
- session.reviewPath = path26.join(approval.approvalDir, "wiki", reviewOutput.page.path);
24678
- session.guidePath = path26.join(approval.approvalDir, "wiki", guideOutput.page.path);
24679
- session.approvalId = approval.approvalId;
24680
- session.approvalDir = approval.approvalDir;
24681
- const persisted = await persistSourceSessionPage(rootDir, scope, session);
24682
- session.sessionPath = persisted.sessionPath;
24683
- await writeGuidedSourceSession(rootDir, session);
24684
- await refreshVaultAfterOutputSave(rootDir);
25255
+ });
25256
+ return { page: output.page, content: output.content };
25257
+ }
25258
+ async function stageSourceReviewForScope(rootDir, scope) {
25259
+ const output = await buildSourceReviewStagedPage(rootDir, scope);
25260
+ const approval = await stageGeneratedOutputPages(rootDir, [{ page: output.page, content: output.content, label: "source-review" }], {
25261
+ bundleType: "source-review",
25262
+ title: `Source Review: ${scope.title}`
25263
+ });
24685
25264
  return {
24686
25265
  sourceId: scope.id,
24687
- pageId: guideOutput.page.id,
24688
- guidePath: session.guidePath,
24689
- reviewPageId: reviewOutput.page.id,
24690
- reviewPath: session.reviewPath,
24691
- sessionId: session.sessionId,
24692
- sessionPath: persisted.sessionPath,
24693
- sessionStatePath: statePath,
24694
- status: session.status,
24695
- questions: session.questions,
24696
- targetedPagePaths: session.targetedPagePaths,
24697
- stagedUpdatePaths: session.stagedUpdatePaths,
24698
- briefPath,
25266
+ pageId: output.page.id,
25267
+ reviewPath: path27.join(approval.approvalDir, "wiki", output.page.path),
24699
25268
  staged: true,
24700
25269
  approvalId: approval.approvalId,
24701
25270
  approvalDir: approval.approvalDir
24702
25271
  };
24703
25272
  }
24704
- function scopeFromManagedSource(source) {
24705
- return {
24706
- id: source.id,
24707
- title: source.title,
24708
- sourceIds: source.sourceIds,
24709
- kind: source.kind,
24710
- briefPath: source.briefPath
24711
- };
24712
- }
24713
- function scopeFromManifest(manifest, manifests) {
24714
- const groupId = manifest.sourceGroupId ?? manifest.sourceId;
24715
- return {
24716
- id: groupId,
24717
- title: manifest.sourceGroupTitle ?? manifest.title,
24718
- sourceIds: manifest.sourceGroupId ? manifests.filter((candidate) => candidate.sourceGroupId === manifest.sourceGroupId).map((candidate) => candidate.sourceId) : [manifest.sourceId],
24719
- kind: manifest.sourceKind
24720
- };
24721
- }
24722
- async function resolveSourceScope(rootDir, id) {
24723
- const managedSources = await loadManagedSources(rootDir);
24724
- const managedSource = managedSources.find((source) => source.id === id);
24725
- if (managedSource) {
24726
- return scopeFromManagedSource(managedSource);
24727
- }
24728
- const latestSession = await findLatestGuidedSourceSessionByScope(rootDir, id);
24729
- if (latestSession) {
24730
- return {
24731
- id: latestSession.scopeId,
24732
- title: latestSession.scopeTitle,
24733
- sourceIds: latestSession.sourceIds
24734
- };
24735
- }
24736
- const manifests = await listManifests(rootDir);
24737
- const manifest = manifests.find((candidate) => candidate.sourceId === id) ?? manifests.find((candidate) => candidate.sourceGroupId === id);
24738
- if (!manifest) {
24739
- return null;
24740
- }
24741
- return scopeFromManifest(manifest, manifests);
24742
- }
24743
- async function reviewSourceScope(rootDir, scope) {
24744
- return await stageSourceReviewForScope(rootDir, scope);
24745
- }
24746
- async function guideSourceScope(rootDir, scope, options = {}) {
24747
- return await stageSourceGuideForScope(rootDir, scope, options);
24748
- }
24749
- async function reviewManagedSource(rootDir, id) {
24750
- const scope = await resolveSourceScope(rootDir, id);
24751
- if (!scope) {
24752
- throw new Error(`Managed source or source id not found: ${id}`);
24753
- }
24754
- if (!await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath))) {
24755
- await compileVault(rootDir, {});
24756
- }
24757
- return await stageSourceReviewForScope(rootDir, scope);
24758
- }
24759
- async function guideManagedSource(rootDir, id, options = {}) {
24760
- const scope = await resolveSourceScope(rootDir, id);
24761
- if (!scope) {
24762
- throw new Error(`Managed source or source id not found: ${id}`);
24763
- }
24764
- if (!await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath))) {
24765
- await compileVault(rootDir, {});
24766
- }
24767
- return await stageSourceGuideForScope(rootDir, scope, options);
24768
- }
24769
- async function resumeSourceSession(rootDir, id, options = {}) {
24770
- const existingSession = await readGuidedSourceSession(rootDir, id);
24771
- if (existingSession) {
24772
- return await stageSourceGuideForScope(
24773
- rootDir,
24774
- {
24775
- id: existingSession.scopeId,
24776
- title: existingSession.scopeTitle,
24777
- sourceIds: existingSession.sourceIds,
24778
- kind: existingSession.kind,
24779
- briefPath: existingSession.briefPath
24780
- },
24781
- options
24782
- );
24783
- }
24784
- const scope = await resolveSourceScope(rootDir, id);
24785
- if (!scope) {
24786
- throw new Error(`Managed source, source scope, or guided session not found: ${id}`);
24787
- }
24788
- return await stageSourceGuideForScope(rootDir, scope, options);
24789
- }
24790
- function shouldCompile(changedSources, graphExists, compileRequested) {
24791
- return compileRequested && (!graphExists || changedSources.length > 0);
25273
+ function nextGuidedSourceSessionId(scope) {
25274
+ return `source-session-${slugify(scope.id)}-${sha256(`${scope.id}:${(/* @__PURE__ */ new Date()).toISOString()}`).slice(0, 8)}`;
24792
25275
  }
24793
- async function shouldRefreshBriefForManagedSource(source, options) {
24794
- if (options.compilePerformed || options.changed) {
24795
- return true;
24796
- }
24797
- if (!source.briefPath) {
24798
- return true;
24799
- }
24800
- return !await fileExists(source.briefPath);
25276
+ function shouldReuseGuidedSourceSession(session) {
25277
+ return Boolean(session && session.status === "awaiting_input");
24801
25278
  }
24802
- async function listManagedSourceRecords(rootDir) {
24803
- await ensureManagedSourcesArtifact(rootDir);
24804
- return await loadManagedSources(rootDir);
25279
+ function questionAnswer(questions, id, fallback) {
25280
+ return normalizeGuidedAnswerValue(questions.find((question) => question.id === id)?.answer) ?? fallback;
24805
25281
  }
24806
- async function addManagedSource(rootDir, input, options = {}) {
24807
- const compileRequested = options.compile ?? true;
24808
- const guideRequested = options.guide ?? false;
24809
- const briefRequested = guideRequested ? true : options.brief ?? true;
24810
- const reviewRequested = guideRequested ? false : options.review ?? false;
24811
- const sources = await loadManagedSources(rootDir);
24812
- const resolved = await resolveManagedSourceInput(rootDir, input);
24813
- const existing = sources.find((candidate) => matchesManagedSourceSpec(candidate, resolved));
25282
+ async function prepareGuidedSourceSession(rootDir, scope, answers) {
25283
+ const existing = await findLatestGuidedSourceSessionByScope(rootDir, scope.id);
24814
25284
  const now = (/* @__PURE__ */ new Date()).toISOString();
24815
- const source = existing ?? {
24816
- id: resolved.kind === "directory" || resolved.kind === "file" ? stableManagedSourceId(resolved.kind, path26.resolve(resolved.path), resolved.title) : stableManagedSourceId(resolved.kind, resolved.url, resolved.title),
24817
- kind: resolved.kind,
24818
- title: resolved.title,
24819
- path: resolved.kind === "directory" || resolved.kind === "file" ? resolved.path : void 0,
24820
- repoRoot: resolved.kind === "directory" ? resolved.repoRoot : void 0,
24821
- url: resolved.kind === "directory" || resolved.kind === "file" ? void 0 : resolved.url,
25285
+ const session = shouldReuseGuidedSourceSession(existing) ? {
25286
+ ...existing,
25287
+ scopeTitle: scope.title,
25288
+ sourceIds: scope.sourceIds,
25289
+ kind: scope.kind,
25290
+ questions: mergeGuidedSessionQuestions(existing.questions, answers),
25291
+ updatedAt: now
25292
+ } : {
25293
+ sessionId: nextGuidedSourceSessionId(scope),
25294
+ scopeId: scope.id,
25295
+ scopeTitle: scope.title,
25296
+ sourceIds: scope.sourceIds,
25297
+ kind: scope.kind,
25298
+ status: "awaiting_input",
24822
25299
  createdAt: now,
24823
25300
  updatedAt: now,
24824
- status: "ready",
24825
- sourceIds: []
25301
+ questions: mergeGuidedSessionQuestions(defaultGuidedSessionQuestions(), answers),
25302
+ briefPath: scope.briefPath,
25303
+ targetedPagePaths: [],
25304
+ stagedUpdatePaths: []
24826
25305
  };
24827
- const synced = await syncManagedSource(rootDir, source, options);
24828
- if (synced.lastSyncStatus === "error") {
24829
- throw new Error(synced.lastError ?? `Failed to add managed source ${synced.id}.`);
24830
- }
24831
- const graphExists = await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath));
24832
- let compile;
24833
- if (shouldCompile(synced.changed ? [synced] : [], graphExists, compileRequested)) {
24834
- compile = await compileVault(rootDir, {});
24835
- }
24836
- let briefGenerated = false;
24837
- let briefPath;
24838
- if (compileRequested && briefRequested && synced.status === "ready" && await shouldRefreshBriefForManagedSource(synced, {
24839
- compilePerformed: Boolean(compile),
24840
- changed: synced.changed
24841
- })) {
24842
- const briefs = await generateBriefsForSources(rootDir, [synced]);
24843
- briefPath = briefs.get(synced.id);
24844
- briefGenerated = Boolean(briefPath);
25306
+ const statePath = await guidedSourceSessionStatePath(rootDir, session.sessionId);
25307
+ return { session, statePath };
25308
+ }
25309
+ async function buildSourceSessionSavedPage(rootDir, scope, session) {
25310
+ const { config, paths } = await loadVaultConfig(rootDir);
25311
+ let graph = await readJsonFile(paths.graphPath);
25312
+ if (!graph) {
25313
+ await compileVault(rootDir, {});
25314
+ graph = await readJsonFile(paths.graphPath);
24845
25315
  }
24846
- const nextSource = {
24847
- ...synced,
24848
- briefPath: briefPath ?? synced.briefPath,
24849
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
24850
- };
24851
- const nextSources = existing ? sources.map((candidate) => candidate.id === nextSource.id ? nextSource : candidate) : [...sources, nextSource];
24852
- await saveManagedSources(rootDir, nextSources);
24853
- const review = reviewRequested && nextSource.status === "ready" ? await stageSourceReviewForScope(rootDir, scopeFromManagedSource(nextSource)) : void 0;
24854
- const guide = guideRequested && nextSource.status === "ready" ? await stageSourceGuideForScope(
24855
- rootDir,
24856
- {
24857
- ...scopeFromManagedSource(nextSource),
24858
- briefPath: nextSource.briefPath
25316
+ const schemas = await loadVaultSchemas(rootDir);
25317
+ const scopeManifests = manifestsForScope(graph, scope);
25318
+ const sourcePages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
25319
+ const analyses = await loadSourceAnalyses(rootDir, scope.sourceIds);
25320
+ const report = await readGraphReport(rootDir);
25321
+ const contradictions = findContradictionsForScope(scope, report);
25322
+ const relatedPageIds = uniqueStrings4([
25323
+ ...sourcePages.slice(0, 18).map((page) => page.id),
25324
+ ...session.targetedPagePaths.map((relativePath) => {
25325
+ const page = graph?.pages.find((candidate) => candidate.path === relativePath);
25326
+ return page?.id ?? "";
25327
+ })
25328
+ ]);
25329
+ const relatedNodeIds = graph ? scopedNodeIds(graph, scope.sourceIds).slice(0, 28) : [];
25330
+ const projectIds = uniqueStrings4(sourcePages.flatMap((page) => page.projectIds));
25331
+ const evidenceState = contradictions.length > 0 ? "conflicting" : session.targetedPagePaths.some(
25332
+ (targetPath) => sourcePages.some((page) => page.path === targetPath && page.sourceIds.some((sourceId) => !scope.sourceIds.includes(sourceId)))
25333
+ ) ? "reinforcing" : session.targetedPagePaths.length ? "new" : "needs_judgment";
25334
+ const relativeBriefPath = session.briefPath && path27.isAbsolute(session.briefPath) ? path27.relative(paths.wikiDir, session.briefPath) : session.briefPath;
25335
+ const sessionMarkdown = [
25336
+ `# Guided Session: ${scope.title}`,
25337
+ "",
25338
+ `Status: \`${session.status}\``,
25339
+ `Session ID: \`${session.sessionId}\``,
25340
+ ...session.approvalId ? [`Approval Bundle: \`${session.approvalId}\``] : [],
25341
+ ...relativeBriefPath ? [`Brief: \`${relativeBriefPath}\``] : [],
25342
+ "",
25343
+ "## What This Source Is",
25344
+ "",
25345
+ ...analyses.length ? analyses.slice(0, 6).map((analysis) => `- ${analysis.title}: ${analysis.summary}`) : ["- Awaiting compile context."],
25346
+ "",
25347
+ "## Guided Questions",
25348
+ "",
25349
+ ...session.questions.flatMap((question) => [`### ${question.prompt}`, "", question.answer ?? "_Awaiting input._", ""]),
25350
+ "## Proposed Wiki Targets",
25351
+ "",
25352
+ ...session.targetedPagePaths.length ? session.targetedPagePaths.map((targetPath) => `- [[${targetPath.replace(/\.md$/, "")}]]`) : ["- No canonical update targets selected yet."],
25353
+ "",
25354
+ "## Conflicts And Judgment Calls",
25355
+ "",
25356
+ ...contradictions.length ? contradictions.map((contradiction) => `- ${contradiction.claimA} / ${contradiction.claimB}`) : ["- No contradictions are currently flagged for this source scope."],
25357
+ "",
25358
+ "## Follow-up Questions",
25359
+ "",
25360
+ ...(() => {
25361
+ const followups = questionAnswer(session.questions, "followups", "");
25362
+ if (followups) {
25363
+ return followups.split(/\n+/).map((line) => line.trim()).filter(Boolean).map((line) => `- ${line.replace(/^-+\s*/, "")}`);
25364
+ }
25365
+ const analysisQuestions = uniqueStrings4(analyses.flatMap((analysis) => analysis.questions)).slice(0, 6);
25366
+ return analysisQuestions.length ? analysisQuestions.map((question) => `- ${question}`) : ["- No follow-up questions recorded yet."];
25367
+ })(),
25368
+ "",
25369
+ "## Related Artifacts",
25370
+ "",
25371
+ `- [[outputs/source-briefs/${scope.id}|Source Brief]]`,
25372
+ `- [[outputs/source-reviews/${scope.id}|Source Review]]`,
25373
+ `- [[outputs/source-guides/${scope.id}|Source Guide]]`,
25374
+ ""
25375
+ ].join("\n");
25376
+ const now = (/* @__PURE__ */ new Date()).toISOString();
25377
+ const output = buildOutputPage({
25378
+ title: `Guided Session: ${scope.title}`,
25379
+ question: `Guided Session ${scope.title}`,
25380
+ answer: sessionMarkdown,
25381
+ citations: scope.sourceIds,
25382
+ schemaHash: sourceOutputSchemaHash(schemas, projectIds),
25383
+ outputFormat: "report",
25384
+ relatedPageIds,
25385
+ relatedNodeIds,
25386
+ relatedSourceIds: scope.sourceIds,
25387
+ projectIds,
25388
+ extraTags: ["source-session", "guided-session"],
25389
+ origin: "source_session",
25390
+ slug: `source-sessions/${scope.id}`,
25391
+ metadata: {
25392
+ status: "active",
25393
+ createdAt: now,
25394
+ updatedAt: now,
25395
+ compiledFrom: scope.sourceIds,
25396
+ managedBy: "system",
25397
+ confidence: 0.81
24859
25398
  },
24860
- { answers: options.guideAnswers }
24861
- ) : void 0;
24862
- return {
24863
- source: nextSource,
24864
- compile,
24865
- briefGenerated,
24866
- review,
24867
- guide
24868
- };
24869
- }
24870
- async function reloadManagedSources(rootDir, options = {}) {
24871
- const compileRequested = options.compile ?? true;
24872
- const guideRequested = options.guide ?? false;
24873
- const briefRequested = guideRequested ? true : options.brief ?? true;
24874
- const reviewRequested = guideRequested ? false : options.review ?? false;
24875
- const sources = await loadManagedSources(rootDir);
24876
- const selected = options.all || !options.id ? sources : sources.filter((source) => source.id === options.id);
24877
- if (!selected.length) {
24878
- throw new Error(options.id ? `Managed source not found: ${options.id}` : "No managed sources registered.");
24879
- }
24880
- const syncedSources = [];
24881
- const changedSources = [];
24882
- for (const source of selected) {
24883
- const synced = await syncManagedSource(rootDir, source, options);
24884
- syncedSources.push(synced);
24885
- if (synced.changed) {
24886
- changedSources.push(synced);
24887
- }
24888
- }
24889
- const graphExists = await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath));
24890
- let compile;
24891
- if (shouldCompile(changedSources, graphExists, compileRequested)) {
24892
- compile = await compileVault(rootDir, {});
24893
- }
24894
- const briefPaths = compileRequested && briefRequested ? await generateBriefsForSources(
24895
- rootDir,
24896
- syncedSources.filter((source) => source.status === "ready")
24897
- ) : /* @__PURE__ */ new Map();
24898
- const nextSources = sources.map((source) => {
24899
- const synced = syncedSources.find((candidate) => candidate.id === source.id);
24900
- if (!synced) {
24901
- return source;
25399
+ frontmatter: {
25400
+ profile_presets: config.profile.presets,
25401
+ source_type: scopeSourceType(scope, scopeManifests),
25402
+ occurred_at: scopeOccurredAt(scopeManifests),
25403
+ participants: scopeParticipants(scopeManifests),
25404
+ container_title: scopeContainerTitle(scopeManifests),
25405
+ conversation_id: scopeConversationId(scopeManifests),
25406
+ session_status: session.status,
25407
+ question_state: questionStateForSession(session),
25408
+ canonical_targets: session.targetedPagePaths,
25409
+ evidence_state: evidenceState
24902
25410
  }
24903
- return {
24904
- ...synced,
24905
- briefPath: briefPaths.get(synced.id) ?? synced.briefPath,
24906
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
24907
- };
24908
25411
  });
24909
- await saveManagedSources(rootDir, nextSources);
24910
- const reviews = reviewRequested ? await Promise.all(
24911
- nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)).filter((source) => source.status === "ready").map(async (source) => await stageSourceReviewForScope(rootDir, scopeFromManagedSource(source)))
24912
- ) : [];
24913
- const guides = guideRequested ? await Promise.all(
24914
- nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)).filter((source) => source.status === "ready").map(
24915
- async (source) => await stageSourceGuideForScope(
24916
- rootDir,
24917
- {
24918
- ...scopeFromManagedSource(source),
24919
- briefPath: source.briefPath
24920
- },
24921
- { answers: options.guideAnswers }
24922
- )
24923
- )
24924
- ) : [];
24925
- return {
24926
- sources: nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)),
24927
- compile,
24928
- briefPaths: [...briefPaths.values()],
24929
- reviews,
24930
- guides
24931
- };
25412
+ return { page: output.page, content: output.content };
24932
25413
  }
24933
- async function deleteManagedSource(rootDir, id) {
24934
- const sources = await loadManagedSources(rootDir);
24935
- const target = sources.find((source) => source.id === id);
24936
- if (!target) {
24937
- throw new Error(`Managed source not found: ${id}`);
25414
+ async function persistSourceSessionPage(rootDir, scope, session) {
25415
+ const { paths } = await loadVaultConfig(rootDir);
25416
+ const output = await buildSourceSessionSavedPage(rootDir, scope, session);
25417
+ const absolutePath = path27.join(paths.wikiDir, output.page.path);
25418
+ await ensureDir(path27.dirname(absolutePath));
25419
+ await fs22.writeFile(absolutePath, output.content, "utf8");
25420
+ return { pageId: output.page.id, sessionPath: absolutePath };
25421
+ }
25422
+ async function buildGuidedUpdatePages(rootDir, scope, session) {
25423
+ const { config, paths } = await loadVaultConfig(rootDir);
25424
+ let graph = await readJsonFile(paths.graphPath);
25425
+ if (!graph) {
25426
+ await compileVault(rootDir, {});
25427
+ graph = await readJsonFile(paths.graphPath);
24938
25428
  }
24939
- await saveManagedSources(
24940
- rootDir,
24941
- sources.filter((source) => source.id !== id)
25429
+ if (!graph) {
25430
+ return [];
25431
+ }
25432
+ const sourcePages = scopedSourcePages(graph, scope.sourceIds);
25433
+ const scopeManifests = manifestsForScope(graph, scope);
25434
+ const analyses = await loadSourceAnalyses(rootDir, scope.sourceIds);
25435
+ const report = await readGraphReport(rootDir);
25436
+ const contradictions = findContradictionsForScope(scope, report);
25437
+ const selectedTargets = selectGuidedTargetPages(scope, sourcePages, session.questions);
25438
+ const useCanonicalTargets = config.profile.guidedSessionMode === "canonical_review" && selectedTargets.length > 0;
25439
+ const targetPages = useCanonicalTargets ? selectedTargets : [selectedTargets[0] ?? null];
25440
+ session.targetedPagePaths = uniqueStrings4(
25441
+ useCanonicalTargets ? selectedTargets.map((page) => page.path) : selectedTargets.length ? selectedTargets.map((page) => page.path) : session.targetedPagePaths
25442
+ );
25443
+ return await Promise.all(
25444
+ targetPages.map(async (targetPage) => {
25445
+ const evidenceState = classifyGuidedEvidenceState(scope, targetPage, contradictions);
25446
+ const relativePath = useCanonicalTargets && targetPage ? targetPage.path : targetPage ? insightRelativePathForTarget(targetPage, scope) : `insights/topics/${slugify(scope.title)}.md`;
25447
+ const absolutePath = path27.join(paths.wikiDir, relativePath);
25448
+ const existingContent = await fileExists(absolutePath) ? await fs22.readFile(absolutePath, "utf8") : "";
25449
+ const parsed = existingContent ? matter11(existingContent) : { data: {}, content: "" };
25450
+ const existingData = parsed.data;
25451
+ const existingSourceIds = Array.isArray(existingData.source_ids) ? existingData.source_ids.filter((value) => typeof value === "string") : [];
25452
+ const existingProjectIds = Array.isArray(existingData.project_ids) ? existingData.project_ids.filter((value) => typeof value === "string") : [];
25453
+ const existingNodeIds = Array.isArray(existingData.node_ids) ? existingData.node_ids.filter((value) => typeof value === "string") : [];
25454
+ const existingBacklinks = Array.isArray(existingData.backlinks) ? existingData.backlinks.filter((value) => typeof value === "string") : [];
25455
+ const createdAt = typeof existingData.created_at === "string" && existingData.created_at.trim() ? existingData.created_at : (/* @__PURE__ */ new Date()).toISOString();
25456
+ const title = typeof existingData.title === "string" && existingData.title.trim() || (useCanonicalTargets && targetPage ? targetPage.title : targetPage ? insightTitleForTarget(targetPage, scope) : `${scope.title} Notes`);
25457
+ const baseBody = parsed.content.trim() ? parsed.content.trim() : [
25458
+ `# ${title}`,
25459
+ "",
25460
+ useCanonicalTargets ? "Canonical page maintained by SwarmVault. Guided sessions stage replaceable update blocks here for approval." : "Human-curated insight page. Guided sessions stage replaceable update blocks here.",
25461
+ ""
25462
+ ].join("\n");
25463
+ const importance = questionAnswer(
25464
+ session.questions,
25465
+ "importance",
25466
+ "Capture the most important new ideas from this source before treating them as canonical."
25467
+ );
25468
+ const exclude = questionAnswer(
25469
+ session.questions,
25470
+ "exclude",
25471
+ "Keep uncertain or incidental details provisional until they matter to the research thread."
25472
+ );
25473
+ const conflictNotes = questionAnswer(
25474
+ session.questions,
25475
+ "conflicts",
25476
+ contradictions.length ? "Review the conflicting evidence before accepting any canonical summary changes." : "No explicit conflicts were called out."
25477
+ );
25478
+ const followups = questionAnswer(session.questions, "followups", "Track follow-up questions on the source session page.");
25479
+ const updateBlock = [
25480
+ `## Guided Session Update: ${scope.title}`,
25481
+ "",
25482
+ `Evidence State: \`${evidenceState}\``,
25483
+ `Session: [[outputs/source-sessions/${scope.id}|Guided Session]]`,
25484
+ `Source Guide: [[outputs/source-guides/${scope.id}|Source Guide]]`,
25485
+ "",
25486
+ "### What Matters Now",
25487
+ "",
25488
+ importance,
25489
+ "",
25490
+ "### Proposed Integration",
25491
+ "",
25492
+ targetPage ? `- Fold the strongest source-backed takeaways into [[${targetPage.path.replace(/\.md$/, "")}|${targetPage.title}]].` : `- Start a durable topic note for ${scope.title}.`,
25493
+ ...analyses.slice(0, 5).map((analysis) => `- ${truncate(normalizeWhitespace(analysis.summary), 180)}`),
25494
+ "",
25495
+ "### Keep Provisional Or Out",
25496
+ "",
25497
+ exclude,
25498
+ "",
25499
+ "### Reinforcing Or Conflicting Notes",
25500
+ "",
25501
+ conflictNotes,
25502
+ ...contradictions.length ? ["", ...contradictions.slice(0, 4).map((contradiction) => `- ${contradiction.claimA} / ${contradiction.claimB}`)] : [],
25503
+ "",
25504
+ "### Follow-up Questions",
25505
+ "",
25506
+ followups,
25507
+ ""
25508
+ ].join("\n");
25509
+ const nextBody = replaceMarkedSection(baseBody, scope.id, updateBlock);
25510
+ const content = matter11.stringify(
25511
+ `${nextBody.trimEnd()}
25512
+ `,
25513
+ JSON.parse(
25514
+ JSON.stringify({
25515
+ ...existingData,
25516
+ page_id: typeof existingData.page_id === "string" && existingData.page_id.trim() || (useCanonicalTargets && targetPage ? targetPage.id : `insight:${slugify(relativePath.replace(/\.md$/, ""))}`),
25517
+ kind: useCanonicalTargets && targetPage ? targetPage.kind : "insight",
25518
+ title,
25519
+ tags: uniqueStrings4([
25520
+ ...Array.isArray(existingData.tags) ? existingData.tags.filter((value) => typeof value === "string") : [],
25521
+ ...useCanonicalTargets ? ["guided-session", `guided/${targetPage?.kind ?? "page"}`] : insightTagsForTarget(targetPage)
25522
+ ]),
25523
+ source_ids: uniqueStrings4([...existingSourceIds, ...scope.sourceIds]),
25524
+ project_ids: uniqueStrings4([...existingProjectIds, ...targetPage?.projectIds ?? []]),
25525
+ node_ids: uniqueStrings4([...existingNodeIds, ...targetPage?.nodeIds ?? []]),
25526
+ freshness: "fresh",
25527
+ status: existingData.status === "archived" ? "archived" : "active",
25528
+ confidence: 0.83,
25529
+ created_at: createdAt,
25530
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
25531
+ compiled_from: uniqueStrings4([
25532
+ ...Array.isArray(existingData.compiled_from) ? existingData.compiled_from.filter((value) => typeof value === "string") : [],
25533
+ ...scope.sourceIds
25534
+ ]),
25535
+ managed_by: typeof existingData.managed_by === "string" && (existingData.managed_by === "human" || existingData.managed_by === "system") ? existingData.managed_by : useCanonicalTargets ? "system" : "human",
25536
+ backlinks: uniqueStrings4([
25537
+ ...existingBacklinks,
25538
+ ...targetPage ? [targetPage.id] : [],
25539
+ `output:source-sessions/${scope.id}`,
25540
+ `output:source-guides/${scope.id}`
25541
+ ]),
25542
+ schema_hash: typeof existingData.schema_hash === "string" ? existingData.schema_hash : "",
25543
+ source_hashes: existingData.source_hashes && typeof existingData.source_hashes === "object" ? existingData.source_hashes : {},
25544
+ source_semantic_hashes: existingData.source_semantic_hashes && typeof existingData.source_semantic_hashes === "object" ? existingData.source_semantic_hashes : {},
25545
+ profile_presets: config.profile.presets,
25546
+ source_type: scopeSourceType(scope, scopeManifests),
25547
+ occurred_at: scopeOccurredAt(scopeManifests),
25548
+ participants: scopeParticipants(scopeManifests),
25549
+ container_title: scopeContainerTitle(scopeManifests),
25550
+ conversation_id: scopeConversationId(scopeManifests),
25551
+ session_status: session.status,
25552
+ question_state: questionStateForSession(session),
25553
+ canonical_targets: useCanonicalTargets ? selectedTargets.map((page2) => page2.path) : [],
25554
+ evidence_state: evidenceState
25555
+ })
25556
+ )
25557
+ );
25558
+ const page = parseStoredPage(relativePath, content, {
25559
+ createdAt,
25560
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
25561
+ });
25562
+ if (!useCanonicalTargets && !selectedTargets.length) {
25563
+ session.targetedPagePaths = uniqueStrings4([...session.targetedPagePaths, relativePath]);
25564
+ }
25565
+ return { page, content, label: "guided-update" };
25566
+ })
24942
25567
  );
24943
- const workingDir = await managedSourceWorkingDir(rootDir, id);
24944
- await fs22.rm(workingDir, { recursive: true, force: true });
24945
- return { removed: target };
24946
- }
24947
-
24948
- // src/viewer.ts
24949
- import { execFile as execFile2 } from "child_process";
24950
- import fs23 from "fs/promises";
24951
- import http from "http";
24952
- import path28 from "path";
24953
- import { promisify as promisify2 } from "util";
24954
- import matter12 from "gray-matter";
24955
- import mime2 from "mime-types";
24956
-
24957
- // src/graph-presentation.ts
24958
- var OVERVIEW_THRESHOLD = 5e3;
24959
- var OVERVIEW_NODE_BUDGET = 1500;
24960
- function nodePriority(node, pinnedNodeIds) {
24961
- return [pinnedNodeIds.has(node.id) ? 0 : 1, -(node.degree ?? 0), -(node.bridgeScore ?? 0), node.label, node.id];
24962
- }
24963
- function compareTuples(left, right) {
24964
- const length = Math.max(left.length, right.length);
24965
- for (let index = 0; index < length; index += 1) {
24966
- const leftValue = left[index];
24967
- const rightValue = right[index];
24968
- if (leftValue === rightValue) {
24969
- continue;
24970
- }
24971
- if (typeof leftValue === "number" && typeof rightValue === "number") {
24972
- return leftValue - rightValue;
24973
- }
24974
- return String(leftValue ?? "").localeCompare(String(rightValue ?? ""));
24975
- }
24976
- return 0;
24977
25568
  }
24978
- function survivingHyperedges(hyperedges, sampledNodeIds) {
24979
- return hyperedges.filter((hyperedge) => hyperedge.nodeIds.filter((nodeId) => sampledNodeIds.has(nodeId)).length >= 2);
24980
- }
24981
- function pinnedNodeIdsForReport(report) {
24982
- if (!report) {
24983
- return /* @__PURE__ */ new Set();
24984
- }
24985
- return /* @__PURE__ */ new Set([
24986
- ...report.godNodes.map((node) => node.nodeId),
24987
- ...report.bridgeNodes.map((node) => node.nodeId),
24988
- ...report.surprisingConnections.flatMap((connection) => [connection.sourceNodeId, connection.targetNodeId])
24989
- ]);
24990
- }
24991
- function sampleGraphNodes(graph, report, nodeBudget = OVERVIEW_NODE_BUDGET) {
24992
- const pinned = pinnedNodeIdsForReport(report);
24993
- const nodeById2 = new Map(graph.nodes.map((node) => [node.id, node]));
24994
- const selected = new Set([...pinned].filter((nodeId) => nodeById2.has(nodeId)));
24995
- const sortedCommunities2 = [...graph.communities ?? []].sort((left, right) => {
24996
- const leftNodes = left.nodeIds.map((nodeId) => nodeById2.get(nodeId)).filter((node) => Boolean(node));
24997
- const rightNodes = right.nodeIds.map((nodeId) => nodeById2.get(nodeId)).filter((node) => Boolean(node));
24998
- const leftFirstParty = leftNodes.filter((node) => node.sourceClass === "first_party").length;
24999
- const rightFirstParty = rightNodes.filter((node) => node.sourceClass === "first_party").length;
25000
- return compareTuples(
25001
- [-leftFirstParty, -leftNodes.length, left.label, left.id],
25002
- [-rightFirstParty, -rightNodes.length, right.label, right.id]
25003
- );
25004
- });
25005
- for (const community of sortedCommunities2) {
25006
- const communityNodes = community.nodeIds.map((nodeId) => nodeById2.get(nodeId)).filter((node) => Boolean(node)).sort((left, right) => compareTuples(nodePriority(left, pinned), nodePriority(right, pinned)));
25007
- for (const node of communityNodes) {
25008
- if (selected.size >= nodeBudget && !pinned.has(node.id)) {
25009
- break;
25010
- }
25011
- selected.add(node.id);
25012
- }
25013
- if (selected.size >= nodeBudget) {
25014
- break;
25015
- }
25016
- }
25017
- if (selected.size < nodeBudget) {
25018
- for (const node of [...graph.nodes].sort((left, right) => compareTuples(nodePriority(left, pinned), nodePriority(right, pinned)))) {
25019
- if (selected.size >= nodeBudget && !pinned.has(node.id)) {
25020
- break;
25021
- }
25022
- selected.add(node.id);
25023
- }
25569
+ async function stageSourceGuideForScope(rootDir, scope, options = {}) {
25570
+ const { session, statePath } = await prepareGuidedSourceSession(rootDir, scope, options.answers);
25571
+ const briefPath = scope.briefPath ?? session.briefPath ?? await writeSourceBriefForScope(rootDir, scope) ?? void 0;
25572
+ session.briefPath = briefPath;
25573
+ if (briefPath) {
25574
+ await refreshVaultAfterOutputSave(rootDir);
25024
25575
  }
25025
- return selected;
25026
- }
25027
- function buildViewerGraphArtifact(graph, options = {}) {
25028
- const threshold = options.threshold ?? OVERVIEW_THRESHOLD;
25029
- const nodeBudget = options.nodeBudget ?? OVERVIEW_NODE_BUDGET;
25030
- const totalCommunities = graph.communities?.length ?? 0;
25031
- if (options.full || graph.nodes.length <= threshold) {
25576
+ if (answeredGuidedSessionQuestions(session.questions).length === 0) {
25577
+ session.status = "awaiting_input";
25578
+ const persisted2 = await persistSourceSessionPage(rootDir, scope, session);
25579
+ session.sessionPath = persisted2.sessionPath;
25580
+ await writeGuidedSourceSession(rootDir, session);
25581
+ await refreshVaultAfterOutputSave(rootDir);
25032
25582
  return {
25033
- ...graph,
25034
- presentation: {
25035
- mode: "full",
25036
- threshold,
25037
- nodeBudget,
25038
- totalNodes: graph.nodes.length,
25039
- displayedNodes: graph.nodes.length,
25040
- totalEdges: graph.edges.length,
25041
- displayedEdges: graph.edges.length,
25042
- totalCommunities,
25043
- displayedCommunities: totalCommunities
25044
- }
25583
+ sourceId: scope.id,
25584
+ sessionId: session.sessionId,
25585
+ sessionPath: persisted2.sessionPath,
25586
+ sessionStatePath: statePath,
25587
+ status: session.status,
25588
+ questions: session.questions,
25589
+ awaitingInput: true,
25590
+ targetedPagePaths: session.targetedPagePaths,
25591
+ stagedUpdatePaths: session.stagedUpdatePaths,
25592
+ briefPath,
25593
+ staged: false
25045
25594
  };
25046
25595
  }
25047
- const sampledNodeIds = sampleGraphNodes(graph, options.report, nodeBudget);
25048
- const nodes = graph.nodes.filter((node) => sampledNodeIds.has(node.id));
25049
- const edges = graph.edges.filter((edge) => sampledNodeIds.has(edge.source) && sampledNodeIds.has(edge.target));
25050
- const hyperedges = survivingHyperedges(graph.hyperedges ?? [], sampledNodeIds);
25051
- const communities = (graph.communities ?? []).map((community) => ({
25052
- ...community,
25053
- nodeIds: community.nodeIds.filter((nodeId) => sampledNodeIds.has(nodeId))
25054
- })).filter((community) => community.nodeIds.length > 0);
25055
- return {
25056
- ...graph,
25057
- nodes,
25058
- edges,
25059
- hyperedges,
25060
- communities,
25061
- presentation: {
25062
- mode: "overview",
25063
- threshold,
25064
- nodeBudget,
25065
- totalNodes: graph.nodes.length,
25066
- displayedNodes: nodes.length,
25067
- totalEdges: graph.edges.length,
25068
- displayedEdges: edges.length,
25069
- totalCommunities,
25070
- displayedCommunities: communities.length
25071
- }
25072
- };
25073
- }
25074
-
25075
- // src/watch.ts
25076
- import path27 from "path";
25077
- import process3 from "process";
25078
- import chokidar from "chokidar";
25079
- var MAX_BACKOFF_MS = 3e4;
25080
- var BACKOFF_THRESHOLD = 3;
25081
- var CRITICAL_THRESHOLD = 10;
25082
- var REPO_WATCH_IGNORES = /* @__PURE__ */ new Set([".git", ".venv"]);
25083
- var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
25084
- ".ts",
25085
- ".tsx",
25086
- ".mts",
25087
- ".cts",
25088
- ".js",
25089
- ".jsx",
25090
- ".mjs",
25091
- ".cjs",
25092
- ".py",
25093
- ".go",
25094
- ".rs",
25095
- ".java",
25096
- ".kt",
25097
- ".kts",
25098
- ".scala",
25099
- ".sc",
25100
- ".dart",
25101
- ".lua",
25102
- ".zig",
25103
- ".cs",
25104
- ".php",
25105
- ".rb",
25106
- ".swift",
25107
- ".c",
25108
- ".h",
25109
- ".cpp",
25110
- ".cc",
25111
- ".cxx",
25112
- ".hpp",
25113
- ".hxx",
25114
- ".sh",
25115
- ".bash",
25116
- ".zsh",
25117
- ".ps1",
25118
- ".psm1",
25119
- ".ex",
25120
- ".exs",
25121
- ".jl",
25122
- ".r",
25123
- ".R"
25124
- ]);
25125
- var FILE_CHANGE_RE = /^(?:add|change|unlink):(.+)$/;
25126
- function isCodeOnlyChange(reasons) {
25127
- for (const reason of reasons) {
25128
- const match = reason.match(FILE_CHANGE_RE);
25129
- if (!match) return false;
25130
- const ext = path27.extname(match[1]).toLowerCase();
25131
- if (!ext || !CODE_EXTENSIONS.has(ext)) return false;
25132
- }
25133
- return reasons.size > 0;
25596
+ session.status = "ready_to_stage";
25597
+ await writeGuidedSourceSession(rootDir, session);
25598
+ const reviewOutput = await buildSourceReviewStagedPage(rootDir, scope);
25599
+ const guideOutput = await buildSourceGuideStagedPage(rootDir, {
25600
+ ...scope,
25601
+ briefPath
25602
+ });
25603
+ const guidedUpdates = await buildGuidedUpdatePages(rootDir, scope, session);
25604
+ session.stagedUpdatePaths = guidedUpdates.map((item) => item.page.path);
25605
+ const approval = await stageGeneratedOutputPages(
25606
+ rootDir,
25607
+ [
25608
+ { page: reviewOutput.page, content: reviewOutput.content, label: "source-review" },
25609
+ { page: guideOutput.page, content: guideOutput.content, label: "source-guide" },
25610
+ ...guidedUpdates
25611
+ ],
25612
+ {
25613
+ bundleType: "guided-session",
25614
+ title: `Guided Session: ${scope.title}`,
25615
+ sourceSessionId: session.sessionId
25616
+ }
25617
+ );
25618
+ session.status = "staged";
25619
+ session.reviewPath = path27.join(approval.approvalDir, "wiki", reviewOutput.page.path);
25620
+ session.guidePath = path27.join(approval.approvalDir, "wiki", guideOutput.page.path);
25621
+ session.approvalId = approval.approvalId;
25622
+ session.approvalDir = approval.approvalDir;
25623
+ const persisted = await persistSourceSessionPage(rootDir, scope, session);
25624
+ session.sessionPath = persisted.sessionPath;
25625
+ await writeGuidedSourceSession(rootDir, session);
25626
+ await refreshVaultAfterOutputSave(rootDir);
25627
+ return {
25628
+ sourceId: scope.id,
25629
+ pageId: guideOutput.page.id,
25630
+ guidePath: session.guidePath,
25631
+ reviewPageId: reviewOutput.page.id,
25632
+ reviewPath: session.reviewPath,
25633
+ sessionId: session.sessionId,
25634
+ sessionPath: persisted.sessionPath,
25635
+ sessionStatePath: statePath,
25636
+ status: session.status,
25637
+ questions: session.questions,
25638
+ targetedPagePaths: session.targetedPagePaths,
25639
+ stagedUpdatePaths: session.stagedUpdatePaths,
25640
+ briefPath,
25641
+ staged: true,
25642
+ approvalId: approval.approvalId,
25643
+ approvalDir: approval.approvalDir
25644
+ };
25134
25645
  }
25135
- function hasNonCodeChanges(reasons) {
25136
- for (const reason of reasons) {
25137
- const match = reason.match(FILE_CHANGE_RE);
25138
- if (!match) return true;
25139
- const ext = path27.extname(match[1]).toLowerCase();
25140
- if (!ext || !CODE_EXTENSIONS.has(ext)) return true;
25141
- }
25142
- return false;
25646
+ function scopeFromManagedSource(source) {
25647
+ return {
25648
+ id: source.id,
25649
+ title: source.title,
25650
+ sourceIds: source.sourceIds,
25651
+ kind: source.kind,
25652
+ briefPath: source.briefPath
25653
+ };
25143
25654
  }
25144
- function collectNonCodePaths(reasons) {
25145
- const result = [];
25146
- for (const reason of reasons) {
25147
- const match = reason.match(FILE_CHANGE_RE);
25148
- if (!match) {
25149
- result.push(reason);
25150
- continue;
25151
- }
25152
- const ext = path27.extname(match[1]).toLowerCase();
25153
- if (!ext || !CODE_EXTENSIONS.has(ext)) result.push(match[1]);
25154
- }
25155
- return result;
25655
+ function scopeFromManifest(manifest, manifests) {
25656
+ const groupId = manifest.sourceGroupId ?? manifest.sourceId;
25657
+ return {
25658
+ id: groupId,
25659
+ title: manifest.sourceGroupTitle ?? manifest.title,
25660
+ sourceIds: manifest.sourceGroupId ? manifests.filter((candidate) => candidate.sourceGroupId === manifest.sourceGroupId).map((candidate) => candidate.sourceId) : [manifest.sourceId],
25661
+ kind: manifest.sourceKind
25662
+ };
25156
25663
  }
25157
- function hasIgnoredRepoSegment(baseDir, targetPath) {
25158
- const relativePath = path27.relative(baseDir, targetPath);
25159
- if (!relativePath || relativePath.startsWith("..")) {
25160
- return false;
25664
+ async function resolveSourceScope(rootDir, id) {
25665
+ const managedSources = await loadManagedSources(rootDir);
25666
+ const managedSource = managedSources.find((source) => source.id === id);
25667
+ if (managedSource) {
25668
+ return scopeFromManagedSource(managedSource);
25669
+ }
25670
+ const latestSession = await findLatestGuidedSourceSessionByScope(rootDir, id);
25671
+ if (latestSession) {
25672
+ return {
25673
+ id: latestSession.scopeId,
25674
+ title: latestSession.scopeTitle,
25675
+ sourceIds: latestSession.sourceIds
25676
+ };
25677
+ }
25678
+ const manifests = await listManifests(rootDir);
25679
+ const manifest = manifests.find((candidate) => candidate.sourceId === id) ?? manifests.find((candidate) => candidate.sourceGroupId === id);
25680
+ if (!manifest) {
25681
+ return null;
25161
25682
  }
25162
- return relativePath.split(path27.sep).some((segment) => REPO_WATCH_IGNORES.has(segment));
25683
+ return scopeFromManifest(manifest, manifests);
25163
25684
  }
25164
- function workspaceIgnoreRoots(rootDir, paths) {
25165
- return [
25166
- paths.rawDir,
25167
- paths.wikiDir,
25168
- paths.stateDir,
25169
- paths.agentDir,
25170
- paths.inboxDir,
25171
- path27.join(rootDir, ".claude"),
25172
- path27.join(rootDir, ".cursor"),
25173
- path27.join(rootDir, ".obsidian")
25174
- ].map((candidate) => path27.resolve(candidate));
25685
+ async function reviewSourceScope(rootDir, scope) {
25686
+ return await stageSourceReviewForScope(rootDir, scope);
25175
25687
  }
25176
- async function resolveWatchTargets(rootDir, paths, options) {
25177
- const targets = /* @__PURE__ */ new Set([path27.resolve(paths.inboxDir)]);
25178
- if (options.repo) {
25179
- for (const repoRoot of await listTrackedRepoRoots(rootDir)) {
25180
- targets.add(path27.resolve(repoRoot));
25181
- }
25688
+ async function guideSourceScope(rootDir, scope, options = {}) {
25689
+ return await stageSourceGuideForScope(rootDir, scope, options);
25690
+ }
25691
+ async function reviewManagedSource(rootDir, id) {
25692
+ const scope = await resolveSourceScope(rootDir, id);
25693
+ if (!scope) {
25694
+ throw new Error(`Managed source or source id not found: ${id}`);
25182
25695
  }
25183
- return [...targets].sort((left, right) => left.localeCompare(right));
25696
+ if (!await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath))) {
25697
+ await compileVault(rootDir, {});
25698
+ }
25699
+ return await stageSourceReviewForScope(rootDir, scope);
25184
25700
  }
25185
- async function performWatchCycle(rootDir, paths, options, codeOnly = false) {
25186
- const imported = await importInbox(rootDir, paths.inboxDir);
25187
- const repoSync = options.repo ? await syncTrackedReposForWatch(rootDir) : null;
25188
- const compile = await compileVault(rootDir, { codeOnly });
25189
- const pendingSemanticRefresh = repoSync ? await mergePendingSemanticRefresh(rootDir, repoSync.pendingSemanticRefresh) : await readPendingSemanticRefresh(rootDir);
25190
- const stalePagePaths = await markPagesStaleForSources(
25191
- rootDir,
25192
- pendingSemanticRefresh.map((entry) => entry.sourceId).filter((sourceId) => Boolean(sourceId))
25193
- );
25194
- const lintFindingCount = options.lint ? (await lintVault(rootDir)).length : void 0;
25195
- return {
25196
- watchedRepoRoots: repoSync?.repoRoots ?? [],
25197
- importedCount: imported.imported.length,
25198
- scannedCount: imported.scannedCount,
25199
- attachmentCount: imported.attachmentCount,
25200
- repoImportedCount: repoSync?.imported.length ?? 0,
25201
- repoUpdatedCount: repoSync?.updated.length ?? 0,
25202
- repoRemovedCount: repoSync?.removed.length ?? 0,
25203
- repoScannedCount: repoSync?.scannedCount ?? 0,
25204
- pendingSemanticRefreshCount: pendingSemanticRefresh.length,
25205
- pendingSemanticRefreshPaths: pendingSemanticRefresh.map((entry) => entry.path),
25206
- changedPages: [.../* @__PURE__ */ new Set([...compile.changedPages, ...stalePagePaths])],
25207
- lintFindingCount
25208
- };
25701
+ async function guideManagedSource(rootDir, id, options = {}) {
25702
+ const scope = await resolveSourceScope(rootDir, id);
25703
+ if (!scope) {
25704
+ throw new Error(`Managed source or source id not found: ${id}`);
25705
+ }
25706
+ if (!await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath))) {
25707
+ await compileVault(rootDir, {});
25708
+ }
25709
+ return await stageSourceGuideForScope(rootDir, scope, options);
25209
25710
  }
25210
- async function runWatchCycle(rootDir, options = {}) {
25211
- const { paths } = await initWorkspace(rootDir);
25212
- const startedAt = /* @__PURE__ */ new Date();
25213
- let success = true;
25214
- let error;
25215
- let result = {
25216
- watchedRepoRoots: [],
25217
- importedCount: 0,
25218
- scannedCount: 0,
25219
- attachmentCount: 0,
25220
- repoImportedCount: 0,
25221
- repoUpdatedCount: 0,
25222
- repoRemovedCount: 0,
25223
- repoScannedCount: 0,
25224
- pendingSemanticRefreshCount: 0,
25225
- pendingSemanticRefreshPaths: [],
25226
- changedPages: []
25227
- };
25228
- try {
25229
- result = await performWatchCycle(rootDir, paths, options, options.codeOnly ?? false);
25230
- return result;
25231
- } catch (caught) {
25232
- success = false;
25233
- error = caught instanceof Error ? caught.message : String(caught);
25234
- throw caught;
25235
- } finally {
25236
- const finishedAt = /* @__PURE__ */ new Date();
25237
- await recordSession(rootDir, {
25238
- operation: "watch",
25239
- title: `Watch cycle for ${paths.inboxDir}${options.repo ? " and tracked repos" : ""}`,
25240
- startedAt: startedAt.toISOString(),
25241
- finishedAt: finishedAt.toISOString(),
25242
- success,
25243
- error,
25244
- changedPages: result.changedPages,
25245
- lintFindingCount: result.lintFindingCount,
25246
- lines: [
25247
- "reasons=once",
25248
- `imported=${result.importedCount}`,
25249
- `scanned=${result.scannedCount}`,
25250
- `attachments=${result.attachmentCount}`,
25251
- `repo_scanned=${result.repoScannedCount}`,
25252
- `repo_imported=${result.repoImportedCount}`,
25253
- `repo_updated=${result.repoUpdatedCount}`,
25254
- `repo_removed=${result.repoRemovedCount}`,
25255
- `pending_semantic_refresh=${result.pendingSemanticRefreshCount}`,
25256
- `lint=${result.lintFindingCount ?? 0}`
25257
- ]
25258
- });
25259
- await appendWatchRun(rootDir, {
25260
- startedAt: startedAt.toISOString(),
25261
- finishedAt: finishedAt.toISOString(),
25262
- durationMs: finishedAt.getTime() - startedAt.getTime(),
25263
- inputDir: paths.inboxDir,
25264
- reasons: ["once"],
25265
- importedCount: result.importedCount + result.repoImportedCount + result.repoUpdatedCount,
25266
- scannedCount: result.scannedCount + result.repoScannedCount,
25267
- attachmentCount: result.attachmentCount,
25268
- changedPages: result.changedPages,
25269
- repoImportedCount: result.repoImportedCount,
25270
- repoUpdatedCount: result.repoUpdatedCount,
25271
- repoRemovedCount: result.repoRemovedCount,
25272
- repoScannedCount: result.repoScannedCount,
25273
- pendingSemanticRefreshCount: result.pendingSemanticRefreshCount,
25274
- pendingSemanticRefreshPaths: result.pendingSemanticRefreshPaths,
25275
- lintFindingCount: result.lintFindingCount,
25276
- success,
25277
- error
25278
- });
25279
- await writeWatchStatusArtifact(rootDir, {
25280
- generatedAt: finishedAt.toISOString(),
25281
- watchedRepoRoots: result.watchedRepoRoots,
25282
- lastRun: {
25283
- startedAt: startedAt.toISOString(),
25284
- finishedAt: finishedAt.toISOString(),
25285
- durationMs: finishedAt.getTime() - startedAt.getTime(),
25286
- inputDir: paths.inboxDir,
25287
- reasons: ["once"],
25288
- importedCount: result.importedCount + result.repoImportedCount + result.repoUpdatedCount,
25289
- scannedCount: result.scannedCount + result.repoScannedCount,
25290
- attachmentCount: result.attachmentCount,
25291
- changedPages: result.changedPages,
25292
- repoImportedCount: result.repoImportedCount,
25293
- repoUpdatedCount: result.repoUpdatedCount,
25294
- repoRemovedCount: result.repoRemovedCount,
25295
- repoScannedCount: result.repoScannedCount,
25296
- pendingSemanticRefreshCount: result.pendingSemanticRefreshCount,
25297
- pendingSemanticRefreshPaths: result.pendingSemanticRefreshPaths,
25298
- lintFindingCount: result.lintFindingCount,
25299
- success,
25300
- error
25711
+ async function resumeSourceSession(rootDir, id, options = {}) {
25712
+ const existingSession = await readGuidedSourceSession(rootDir, id);
25713
+ if (existingSession) {
25714
+ return await stageSourceGuideForScope(
25715
+ rootDir,
25716
+ {
25717
+ id: existingSession.scopeId,
25718
+ title: existingSession.scopeTitle,
25719
+ sourceIds: existingSession.sourceIds,
25720
+ kind: existingSession.kind,
25721
+ briefPath: existingSession.briefPath
25301
25722
  },
25302
- pendingSemanticRefresh: await readPendingSemanticRefresh(rootDir)
25303
- });
25723
+ options
25724
+ );
25725
+ }
25726
+ const scope = await resolveSourceScope(rootDir, id);
25727
+ if (!scope) {
25728
+ throw new Error(`Managed source, source scope, or guided session not found: ${id}`);
25729
+ }
25730
+ return await stageSourceGuideForScope(rootDir, scope, options);
25731
+ }
25732
+ function shouldCompile(changedSources, graphExists, compileRequested) {
25733
+ return compileRequested && (!graphExists || changedSources.length > 0);
25734
+ }
25735
+ async function shouldRefreshBriefForManagedSource(source, options) {
25736
+ if (options.compilePerformed || options.changed) {
25737
+ return true;
25738
+ }
25739
+ if (!source.briefPath) {
25740
+ return true;
25304
25741
  }
25742
+ return !await fileExists(source.briefPath);
25305
25743
  }
25306
- async function watchVault(rootDir, options = {}) {
25307
- const { paths } = await initWorkspace(rootDir);
25308
- const baseDebounceMs = options.debounceMs ?? 900;
25309
- const ignoredRoots = workspaceIgnoreRoots(rootDir, paths);
25310
- const inboxWatchRoot = path27.resolve(paths.inboxDir);
25311
- let watchTargets = await resolveWatchTargets(rootDir, paths, options);
25312
- let timer;
25313
- let running = false;
25314
- let pending = false;
25315
- let closed = false;
25316
- let consecutiveFailures = 0;
25317
- let currentDebounceMs = baseDebounceMs;
25318
- const reasons = /* @__PURE__ */ new Set();
25319
- let activeCycle = null;
25320
- const watcher = chokidar.watch(watchTargets, {
25321
- ignoreInitial: true,
25322
- usePolling: true,
25323
- interval: 100,
25324
- ignored: (targetPath) => {
25325
- const absolutePath = path27.resolve(targetPath);
25326
- const primaryTarget = watchTargets.filter((watchTarget) => isPathWithin(watchTarget, absolutePath)).sort((left, right) => right.length - left.length)[0] ?? null;
25327
- if (!primaryTarget) {
25328
- return false;
25329
- }
25330
- if (primaryTarget !== inboxWatchRoot && ignoredRoots.some((ignoreRoot) => isPathWithin(ignoreRoot, absolutePath))) {
25331
- return true;
25332
- }
25333
- return hasIgnoredRepoSegment(primaryTarget, absolutePath);
25334
- },
25335
- awaitWriteFinish: {
25336
- stabilityThreshold: Math.max(250, Math.floor(baseDebounceMs / 2)),
25337
- pollInterval: 100
25338
- }
25339
- });
25340
- const syncWatchTargets = async () => {
25341
- const nextTargets = await resolveWatchTargets(rootDir, paths, options);
25342
- const nextSet = new Set(nextTargets);
25343
- const currentSet = new Set(watchTargets);
25344
- const toRemove = watchTargets.filter((target) => !nextSet.has(target));
25345
- const toAdd = nextTargets.filter((target) => !currentSet.has(target));
25346
- if (toRemove.length > 0) {
25347
- await watcher.unwatch(toRemove);
25348
- }
25349
- if (toAdd.length > 0) {
25350
- await watcher.add(toAdd);
25351
- }
25352
- watchTargets = nextTargets;
25353
- };
25354
- const schedule = (reason) => {
25355
- if (closed) {
25356
- return;
25357
- }
25358
- reasons.add(reason);
25359
- pending = true;
25360
- if (timer) {
25361
- clearTimeout(timer);
25362
- }
25363
- timer = setTimeout(() => {
25364
- const cycle = runCycle();
25365
- activeCycle = cycle.finally(() => {
25366
- if (activeCycle === cycle) {
25367
- activeCycle = null;
25368
- }
25369
- });
25370
- }, currentDebounceMs);
25744
+ async function listManagedSourceRecords(rootDir) {
25745
+ await ensureManagedSourcesArtifact(rootDir);
25746
+ return await loadManagedSources(rootDir);
25747
+ }
25748
+ async function addManagedSource(rootDir, input, options = {}) {
25749
+ const compileRequested = options.compile ?? true;
25750
+ const guideRequested = options.guide ?? false;
25751
+ const briefRequested = guideRequested ? true : options.brief ?? true;
25752
+ const reviewRequested = guideRequested ? false : options.review ?? false;
25753
+ const sources = await loadManagedSources(rootDir);
25754
+ const resolved = await resolveManagedSourceInput(rootDir, input);
25755
+ const existing = sources.find((candidate) => matchesManagedSourceSpec(candidate, resolved));
25756
+ const now = (/* @__PURE__ */ new Date()).toISOString();
25757
+ const source = existing ?? {
25758
+ id: resolved.kind === "directory" || resolved.kind === "file" ? stableManagedSourceId(resolved.kind, path27.resolve(resolved.path), resolved.title) : stableManagedSourceId(resolved.kind, resolved.url, resolved.title),
25759
+ kind: resolved.kind,
25760
+ title: resolved.title,
25761
+ path: resolved.kind === "directory" || resolved.kind === "file" ? resolved.path : void 0,
25762
+ repoRoot: resolved.kind === "directory" ? resolved.repoRoot : void 0,
25763
+ url: resolved.kind === "directory" || resolved.kind === "file" ? void 0 : resolved.url,
25764
+ createdAt: now,
25765
+ updatedAt: now,
25766
+ status: "ready",
25767
+ sourceIds: []
25371
25768
  };
25372
- const runCycle = async () => {
25373
- if (running || closed || !pending) {
25374
- return;
25375
- }
25376
- pending = false;
25377
- running = true;
25378
- const startedAt = /* @__PURE__ */ new Date();
25379
- const detectedCodeOnly = isCodeOnlyChange(reasons);
25380
- const hasDeferredNonCode = !detectedCodeOnly && hasNonCodeChanges(reasons);
25381
- const codeOnlyChange = options.codeOnly || detectedCodeOnly || hasDeferredNonCode;
25382
- const runReasons = [...reasons];
25383
- reasons.clear();
25384
- if (hasDeferredNonCode) {
25385
- const nonCodePaths = collectNonCodePaths(new Set(runReasons));
25386
- process3.stderr.write(
25387
- `[swarmvault watch] Non-code changes detected (${nonCodePaths.length} file(s)) \u2014 run \`swarmvault compile\` to include LLM re-analysis.
25388
- `
25389
- );
25390
- } else if (codeOnlyChange) {
25391
- process3.stderr.write("[swarmvault watch] Code-only changes detected \u2014 skipping LLM re-analysis.\n");
25392
- }
25393
- let importedCount = 0;
25394
- let scannedCount = 0;
25395
- let attachmentCount = 0;
25396
- let repoImportedCount = 0;
25397
- let repoUpdatedCount = 0;
25398
- let repoRemovedCount = 0;
25399
- let repoScannedCount = 0;
25400
- let watchedRepoRoots = [];
25401
- let pendingSemanticRefreshCount = 0;
25402
- let pendingSemanticRefreshPaths = [];
25403
- let changedPages = [];
25404
- let lintFindingCount;
25405
- let success = true;
25406
- let error;
25407
- try {
25408
- const result = await performWatchCycle(rootDir, paths, options, codeOnlyChange);
25409
- importedCount = result.importedCount;
25410
- scannedCount = result.scannedCount;
25411
- attachmentCount = result.attachmentCount;
25412
- repoImportedCount = result.repoImportedCount;
25413
- repoUpdatedCount = result.repoUpdatedCount;
25414
- repoRemovedCount = result.repoRemovedCount;
25415
- repoScannedCount = result.repoScannedCount;
25416
- watchedRepoRoots = result.watchedRepoRoots;
25417
- pendingSemanticRefreshCount = result.pendingSemanticRefreshCount;
25418
- pendingSemanticRefreshPaths = result.pendingSemanticRefreshPaths;
25419
- changedPages = result.changedPages;
25420
- lintFindingCount = result.lintFindingCount;
25421
- consecutiveFailures = 0;
25422
- currentDebounceMs = baseDebounceMs;
25423
- await syncWatchTargets();
25424
- } catch (caught) {
25425
- success = false;
25426
- error = caught instanceof Error ? caught.message : String(caught);
25427
- consecutiveFailures++;
25428
- pending = true;
25429
- if (consecutiveFailures >= CRITICAL_THRESHOLD) {
25430
- process3.stderr.write(
25431
- `[swarmvault watch] ${consecutiveFailures} consecutive failures. Check vault state. Continuing at max backoff.
25432
- `
25433
- );
25434
- }
25435
- if (consecutiveFailures >= BACKOFF_THRESHOLD) {
25436
- const multiplier = 2 ** (consecutiveFailures - BACKOFF_THRESHOLD);
25437
- currentDebounceMs = Math.min(baseDebounceMs * multiplier, MAX_BACKOFF_MS);
25438
- }
25439
- } finally {
25440
- const finishedAt = /* @__PURE__ */ new Date();
25441
- try {
25442
- await recordSession(rootDir, {
25443
- operation: "watch",
25444
- title: `Watch cycle for ${paths.inboxDir}${options.repo ? " and tracked repos" : ""}`,
25445
- startedAt: startedAt.toISOString(),
25446
- finishedAt: finishedAt.toISOString(),
25447
- success,
25448
- error,
25449
- changedPages,
25450
- lintFindingCount,
25451
- lines: [
25452
- `reasons=${runReasons.join(",") || "none"}`,
25453
- `code_only=${codeOnlyChange}`,
25454
- `imported=${importedCount}`,
25455
- `scanned=${scannedCount}`,
25456
- `attachments=${attachmentCount}`,
25457
- `repo_scanned=${repoScannedCount}`,
25458
- `repo_imported=${repoImportedCount}`,
25459
- `repo_updated=${repoUpdatedCount}`,
25460
- `repo_removed=${repoRemovedCount}`,
25461
- `lint=${lintFindingCount ?? 0}`
25462
- ]
25463
- });
25464
- } catch {
25465
- process3.stderr.write("[swarmvault watch] Failed to record session log.\n");
25466
- }
25467
- try {
25468
- await appendWatchRun(rootDir, {
25469
- startedAt: startedAt.toISOString(),
25470
- finishedAt: finishedAt.toISOString(),
25471
- durationMs: finishedAt.getTime() - startedAt.getTime(),
25472
- inputDir: paths.inboxDir,
25473
- reasons: runReasons,
25474
- importedCount: importedCount + repoImportedCount + repoUpdatedCount,
25475
- scannedCount: scannedCount + repoScannedCount,
25476
- attachmentCount,
25477
- changedPages,
25478
- repoImportedCount,
25479
- repoUpdatedCount,
25480
- repoRemovedCount,
25481
- repoScannedCount,
25482
- pendingSemanticRefreshCount,
25483
- pendingSemanticRefreshPaths,
25484
- lintFindingCount,
25485
- success,
25486
- error
25487
- });
25488
- } catch {
25489
- process3.stderr.write("[swarmvault watch] Failed to append watch run.\n");
25490
- }
25491
- try {
25492
- await writeWatchStatusArtifact(rootDir, {
25493
- generatedAt: finishedAt.toISOString(),
25494
- watchedRepoRoots,
25495
- lastRun: {
25496
- startedAt: startedAt.toISOString(),
25497
- finishedAt: finishedAt.toISOString(),
25498
- durationMs: finishedAt.getTime() - startedAt.getTime(),
25499
- inputDir: paths.inboxDir,
25500
- reasons: runReasons,
25501
- importedCount: importedCount + repoImportedCount + repoUpdatedCount,
25502
- scannedCount: scannedCount + repoScannedCount,
25503
- attachmentCount,
25504
- changedPages,
25505
- repoImportedCount,
25506
- repoUpdatedCount,
25507
- repoRemovedCount,
25508
- repoScannedCount,
25509
- pendingSemanticRefreshCount,
25510
- pendingSemanticRefreshPaths,
25511
- lintFindingCount,
25512
- success,
25513
- error
25514
- },
25515
- pendingSemanticRefresh: await readPendingSemanticRefresh(rootDir)
25516
- });
25517
- } catch {
25518
- process3.stderr.write("[swarmvault watch] Failed to write watch status artifact.\n");
25519
- }
25520
- running = false;
25521
- if (pending && !closed) {
25522
- schedule("queued");
25523
- }
25524
- }
25769
+ const synced = await syncManagedSource(rootDir, source, options);
25770
+ if (synced.lastSyncStatus === "error") {
25771
+ throw new Error(synced.lastError ?? `Failed to add managed source ${synced.id}.`);
25772
+ }
25773
+ const graphExists = await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath));
25774
+ let compile;
25775
+ if (shouldCompile(synced.changed ? [synced] : [], graphExists, compileRequested)) {
25776
+ compile = await compileVault(rootDir, {});
25777
+ }
25778
+ let briefGenerated = false;
25779
+ let briefPath;
25780
+ if (compileRequested && briefRequested && synced.status === "ready" && await shouldRefreshBriefForManagedSource(synced, {
25781
+ compilePerformed: Boolean(compile),
25782
+ changed: synced.changed
25783
+ })) {
25784
+ const briefs = await generateBriefsForSources(rootDir, [synced]);
25785
+ briefPath = briefs.get(synced.id);
25786
+ briefGenerated = Boolean(briefPath);
25787
+ }
25788
+ const nextSource = {
25789
+ ...synced,
25790
+ briefPath: briefPath ?? synced.briefPath,
25791
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
25525
25792
  };
25526
- const reasonForPath = (targetPath) => {
25527
- const baseDir = watchTargets.filter((watchTarget) => isPathWithin(watchTarget, path27.resolve(targetPath))).sort((left, right) => right.length - left.length)[0] ?? paths.inboxDir;
25528
- return path27.relative(baseDir, targetPath) || ".";
25793
+ const nextSources = existing ? sources.map((candidate) => candidate.id === nextSource.id ? nextSource : candidate) : [...sources, nextSource];
25794
+ await saveManagedSources(rootDir, nextSources);
25795
+ const review = reviewRequested && nextSource.status === "ready" ? await stageSourceReviewForScope(rootDir, scopeFromManagedSource(nextSource)) : void 0;
25796
+ const guide = guideRequested && nextSource.status === "ready" ? await stageSourceGuideForScope(
25797
+ rootDir,
25798
+ {
25799
+ ...scopeFromManagedSource(nextSource),
25800
+ briefPath: nextSource.briefPath
25801
+ },
25802
+ { answers: options.guideAnswers }
25803
+ ) : void 0;
25804
+ return {
25805
+ source: nextSource,
25806
+ compile,
25807
+ briefGenerated,
25808
+ review,
25809
+ guide
25529
25810
  };
25530
- watcher.on("add", (filePath) => schedule(`add:${reasonForPath(filePath)}`)).on("change", (filePath) => schedule(`change:${reasonForPath(filePath)}`)).on("unlink", (filePath) => schedule(`unlink:${reasonForPath(filePath)}`)).on("addDir", (dirPath) => schedule(`addDir:${reasonForPath(dirPath)}`)).on("unlinkDir", (dirPath) => schedule(`unlinkDir:${reasonForPath(dirPath)}`)).on("error", (caught) => schedule(`error:${caught instanceof Error ? caught.message : String(caught)}`));
25531
- await new Promise((resolve, reject) => {
25532
- const handleReady = () => {
25533
- watcher.off("error", handleError);
25534
- resolve();
25535
- };
25536
- const handleError = (caught) => {
25537
- watcher.off("ready", handleReady);
25538
- reject(caught);
25811
+ }
25812
+ async function reloadManagedSources(rootDir, options = {}) {
25813
+ const compileRequested = options.compile ?? true;
25814
+ const guideRequested = options.guide ?? false;
25815
+ const briefRequested = guideRequested ? true : options.brief ?? true;
25816
+ const reviewRequested = guideRequested ? false : options.review ?? false;
25817
+ const sources = await loadManagedSources(rootDir);
25818
+ const selected = options.all || !options.id ? sources : sources.filter((source) => source.id === options.id);
25819
+ if (!selected.length) {
25820
+ throw new Error(options.id ? `Managed source not found: ${options.id}` : "No managed sources registered.");
25821
+ }
25822
+ const syncedSources = [];
25823
+ const changedSources = [];
25824
+ for (const source of selected) {
25825
+ const synced = await syncManagedSource(rootDir, source, options);
25826
+ syncedSources.push(synced);
25827
+ if (synced.changed) {
25828
+ changedSources.push(synced);
25829
+ }
25830
+ }
25831
+ const graphExists = await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath));
25832
+ let compile;
25833
+ if (shouldCompile(changedSources, graphExists, compileRequested)) {
25834
+ compile = await compileVault(rootDir, {});
25835
+ }
25836
+ const briefPaths = compileRequested && briefRequested ? await generateBriefsForSources(
25837
+ rootDir,
25838
+ syncedSources.filter((source) => source.status === "ready")
25839
+ ) : /* @__PURE__ */ new Map();
25840
+ const nextSources = sources.map((source) => {
25841
+ const synced = syncedSources.find((candidate) => candidate.id === source.id);
25842
+ if (!synced) {
25843
+ return source;
25844
+ }
25845
+ return {
25846
+ ...synced,
25847
+ briefPath: briefPaths.get(synced.id) ?? synced.briefPath,
25848
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
25539
25849
  };
25540
- watcher.once("ready", handleReady);
25541
- watcher.once("error", handleError);
25542
25850
  });
25851
+ await saveManagedSources(rootDir, nextSources);
25852
+ const reviews = reviewRequested ? await Promise.all(
25853
+ nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)).filter((source) => source.status === "ready").map(async (source) => await stageSourceReviewForScope(rootDir, scopeFromManagedSource(source)))
25854
+ ) : [];
25855
+ const guides = guideRequested ? await Promise.all(
25856
+ nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)).filter((source) => source.status === "ready").map(
25857
+ async (source) => await stageSourceGuideForScope(
25858
+ rootDir,
25859
+ {
25860
+ ...scopeFromManagedSource(source),
25861
+ briefPath: source.briefPath
25862
+ },
25863
+ { answers: options.guideAnswers }
25864
+ )
25865
+ )
25866
+ ) : [];
25543
25867
  return {
25544
- close: async () => {
25545
- closed = true;
25546
- if (timer) {
25547
- clearTimeout(timer);
25868
+ sources: nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)),
25869
+ compile,
25870
+ briefPaths: [...briefPaths.values()],
25871
+ reviews,
25872
+ guides
25873
+ };
25874
+ }
25875
+ async function deleteManagedSource(rootDir, id) {
25876
+ const sources = await loadManagedSources(rootDir);
25877
+ const target = sources.find((source) => source.id === id);
25878
+ if (!target) {
25879
+ throw new Error(`Managed source not found: ${id}`);
25880
+ }
25881
+ await saveManagedSources(
25882
+ rootDir,
25883
+ sources.filter((source) => source.id !== id)
25884
+ );
25885
+ const workingDir = await managedSourceWorkingDir(rootDir, id);
25886
+ await fs22.rm(workingDir, { recursive: true, force: true });
25887
+ return { removed: target };
25888
+ }
25889
+
25890
+ // src/viewer.ts
25891
+ import { execFile as execFile2 } from "child_process";
25892
+ import fs23 from "fs/promises";
25893
+ import http from "http";
25894
+ import path28 from "path";
25895
+ import { promisify as promisify2 } from "util";
25896
+ import matter12 from "gray-matter";
25897
+ import mime2 from "mime-types";
25898
+
25899
+ // src/graph-presentation.ts
25900
+ var OVERVIEW_THRESHOLD = 5e3;
25901
+ var OVERVIEW_NODE_BUDGET = 1500;
25902
+ function nodePriority(node, pinnedNodeIds) {
25903
+ return [pinnedNodeIds.has(node.id) ? 0 : 1, -(node.degree ?? 0), -(node.bridgeScore ?? 0), node.label, node.id];
25904
+ }
25905
+ function compareTuples(left, right) {
25906
+ const length = Math.max(left.length, right.length);
25907
+ for (let index = 0; index < length; index += 1) {
25908
+ const leftValue = left[index];
25909
+ const rightValue = right[index];
25910
+ if (leftValue === rightValue) {
25911
+ continue;
25912
+ }
25913
+ if (typeof leftValue === "number" && typeof rightValue === "number") {
25914
+ return leftValue - rightValue;
25915
+ }
25916
+ return String(leftValue ?? "").localeCompare(String(rightValue ?? ""));
25917
+ }
25918
+ return 0;
25919
+ }
25920
+ function survivingHyperedges(hyperedges, sampledNodeIds) {
25921
+ return hyperedges.filter((hyperedge) => hyperedge.nodeIds.filter((nodeId) => sampledNodeIds.has(nodeId)).length >= 2);
25922
+ }
25923
+ function pinnedNodeIdsForReport(report) {
25924
+ if (!report) {
25925
+ return /* @__PURE__ */ new Set();
25926
+ }
25927
+ return /* @__PURE__ */ new Set([
25928
+ ...report.godNodes.map((node) => node.nodeId),
25929
+ ...report.bridgeNodes.map((node) => node.nodeId),
25930
+ ...report.surprisingConnections.flatMap((connection) => [connection.sourceNodeId, connection.targetNodeId])
25931
+ ]);
25932
+ }
25933
+ function sampleGraphNodes(graph, report, nodeBudget = OVERVIEW_NODE_BUDGET) {
25934
+ const pinned = pinnedNodeIdsForReport(report);
25935
+ const nodeById2 = new Map(graph.nodes.map((node) => [node.id, node]));
25936
+ const selected = new Set([...pinned].filter((nodeId) => nodeById2.has(nodeId)));
25937
+ const sortedCommunities2 = [...graph.communities ?? []].sort((left, right) => {
25938
+ const leftNodes = left.nodeIds.map((nodeId) => nodeById2.get(nodeId)).filter((node) => Boolean(node));
25939
+ const rightNodes = right.nodeIds.map((nodeId) => nodeById2.get(nodeId)).filter((node) => Boolean(node));
25940
+ const leftFirstParty = leftNodes.filter((node) => node.sourceClass === "first_party").length;
25941
+ const rightFirstParty = rightNodes.filter((node) => node.sourceClass === "first_party").length;
25942
+ return compareTuples(
25943
+ [-leftFirstParty, -leftNodes.length, left.label, left.id],
25944
+ [-rightFirstParty, -rightNodes.length, right.label, right.id]
25945
+ );
25946
+ });
25947
+ for (const community of sortedCommunities2) {
25948
+ const communityNodes = community.nodeIds.map((nodeId) => nodeById2.get(nodeId)).filter((node) => Boolean(node)).sort((left, right) => compareTuples(nodePriority(left, pinned), nodePriority(right, pinned)));
25949
+ for (const node of communityNodes) {
25950
+ if (selected.size >= nodeBudget && !pinned.has(node.id)) {
25951
+ break;
25548
25952
  }
25549
- await watcher.close();
25550
- await activeCycle;
25953
+ selected.add(node.id);
25551
25954
  }
25552
- };
25955
+ if (selected.size >= nodeBudget) {
25956
+ break;
25957
+ }
25958
+ }
25959
+ if (selected.size < nodeBudget) {
25960
+ for (const node of [...graph.nodes].sort((left, right) => compareTuples(nodePriority(left, pinned), nodePriority(right, pinned)))) {
25961
+ if (selected.size >= nodeBudget && !pinned.has(node.id)) {
25962
+ break;
25963
+ }
25964
+ selected.add(node.id);
25965
+ }
25966
+ }
25967
+ return selected;
25553
25968
  }
25554
- async function getWatchStatus(rootDir) {
25555
- const persisted = await readWatchStatusArtifact(rootDir);
25556
- const watchedRepoRoots = await listTrackedRepoRoots(rootDir);
25557
- const pendingSemanticRefresh = await readPendingSemanticRefresh(rootDir);
25969
+ function buildViewerGraphArtifact(graph, options = {}) {
25970
+ const threshold = options.threshold ?? OVERVIEW_THRESHOLD;
25971
+ const nodeBudget = options.nodeBudget ?? OVERVIEW_NODE_BUDGET;
25972
+ const totalCommunities = graph.communities?.length ?? 0;
25973
+ if (options.full || graph.nodes.length <= threshold) {
25974
+ return {
25975
+ ...graph,
25976
+ presentation: {
25977
+ mode: "full",
25978
+ threshold,
25979
+ nodeBudget,
25980
+ totalNodes: graph.nodes.length,
25981
+ displayedNodes: graph.nodes.length,
25982
+ totalEdges: graph.edges.length,
25983
+ displayedEdges: graph.edges.length,
25984
+ totalCommunities,
25985
+ displayedCommunities: totalCommunities
25986
+ }
25987
+ };
25988
+ }
25989
+ const sampledNodeIds = sampleGraphNodes(graph, options.report, nodeBudget);
25990
+ const nodes = graph.nodes.filter((node) => sampledNodeIds.has(node.id));
25991
+ const edges = graph.edges.filter((edge) => sampledNodeIds.has(edge.source) && sampledNodeIds.has(edge.target));
25992
+ const hyperedges = survivingHyperedges(graph.hyperedges ?? [], sampledNodeIds);
25993
+ const communities = (graph.communities ?? []).map((community) => ({
25994
+ ...community,
25995
+ nodeIds: community.nodeIds.filter((nodeId) => sampledNodeIds.has(nodeId))
25996
+ })).filter((community) => community.nodeIds.length > 0);
25558
25997
  return {
25559
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
25560
- watchedRepoRoots,
25561
- lastRun: persisted?.lastRun,
25562
- pendingSemanticRefresh
25998
+ ...graph,
25999
+ nodes,
26000
+ edges,
26001
+ hyperedges,
26002
+ communities,
26003
+ presentation: {
26004
+ mode: "overview",
26005
+ threshold,
26006
+ nodeBudget,
26007
+ totalNodes: graph.nodes.length,
26008
+ displayedNodes: nodes.length,
26009
+ totalEdges: graph.edges.length,
26010
+ displayedEdges: edges.length,
26011
+ totalCommunities,
26012
+ displayedCommunities: communities.length
26013
+ }
25563
26014
  };
25564
26015
  }
25565
26016
 
@@ -25771,7 +26222,7 @@ async function startGraphServer(rootDir, port, options = {}) {
25771
26222
  response.end(JSON.stringify({ error: "Missing approval id." }));
25772
26223
  return;
25773
26224
  }
25774
- const approval = await readApproval(rootDir, approvalId);
26225
+ const approval = await readApproval(rootDir, approvalId, { diff: true });
25775
26226
  response.writeHead(200, { "content-type": "application/json" });
25776
26227
  response.end(JSON.stringify(approval));
25777
26228
  return;
@@ -25962,6 +26413,7 @@ async function exportGraphHtml(rootDir, outputPath, options = {}) {
25962
26413
  return path28.resolve(outputPath);
25963
26414
  }
25964
26415
  export {
26416
+ DEFAULT_PROMOTION_CONFIG,
25965
26417
  acceptApproval,
25966
26418
  addInput,
25967
26419
  addManagedSource,
@@ -25981,6 +26433,7 @@ export {
25981
26433
  deleteManagedSource,
25982
26434
  estimatePageTokens,
25983
26435
  estimateTokens,
26436
+ evaluateCandidateForPromotion,
25984
26437
  explainGraphVault,
25985
26438
  exploreVault,
25986
26439
  exportGraphFormat,
@@ -26019,6 +26472,7 @@ export {
26019
26472
  loadVaultSchema,
26020
26473
  loadVaultSchemas,
26021
26474
  pathGraphVault,
26475
+ previewCandidatePromotions,
26022
26476
  promoteCandidate,
26023
26477
  pushGraphNeo4j,
26024
26478
  queryGraphVault,
@@ -26033,6 +26487,7 @@ export {
26033
26487
  resumeSourceSession,
26034
26488
  reviewManagedSource,
26035
26489
  reviewSourceScope,
26490
+ runAutoPromotion,
26036
26491
  runSchedule,
26037
26492
  runWatchCycle,
26038
26493
  searchVault,