@swarmvaultai/engine 0.6.5 → 0.6.7

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
@@ -9,6 +9,7 @@ import {
9
9
  firstSentences,
10
10
  getProviderForTask,
11
11
  initWorkspace,
12
+ isPathWithin,
12
13
  listFilesRecursive,
13
14
  loadVaultConfig,
14
15
  normalizeWhitespace,
@@ -21,7 +22,7 @@ import {
21
22
  uniqueBy,
22
23
  writeFileIfChanged,
23
24
  writeJsonFile
24
- } from "./chunk-OK5752AP.js";
25
+ } from "./chunk-HRRPWXRZ.js";
25
26
 
26
27
  // src/agents.ts
27
28
  import crypto from "crypto";
@@ -63,6 +64,13 @@ function buildManagedBlock(target) {
63
64
  ""
64
65
  ].join("\n");
65
66
  }
67
+ function buildCursorRule() {
68
+ const frontmatter = YAML.stringify({
69
+ description: "SwarmVault graph-first repository instructions.",
70
+ alwaysApply: true
71
+ }).trimEnd();
72
+ return ["---", frontmatter, "---", "", buildManagedBlock("cursor").trimEnd(), ""].join("\n");
73
+ }
66
74
  function supportsAgentHook(agent) {
67
75
  return agent === "claude" || agent === "opencode" || agent === "gemini" || agent === "copilot";
68
76
  }
@@ -689,8 +697,7 @@ async function installAgent(rootDir, agent, options = {}) {
689
697
  await upsertManagedBlock(target, buildManagedBlock("gemini"));
690
698
  break;
691
699
  case "cursor":
692
- await writeOwnedFile(target, `${buildManagedBlock("cursor")}
693
- `);
700
+ await writeOwnedFile(target, buildCursorRule());
694
701
  break;
695
702
  case "aider":
696
703
  await upsertManagedBlock(target, buildManagedBlock("aider"));
@@ -1342,10 +1349,13 @@ function buildBenchmarkArtifact(input) {
1342
1349
  const corpusTokens = Math.max(1, Math.round(input.corpusWords * (100 / 75)));
1343
1350
  const perQuestion = input.perQuestion.filter((entry) => entry.queryTokens > 0).map((entry) => ({
1344
1351
  ...entry,
1345
- reduction: Number(Math.max(0, 1 - entry.queryTokens / Math.max(1, corpusTokens)).toFixed(3))
1352
+ // Honest reduction: negative values mean graph context is larger than the
1353
+ // full corpus, which is the truth on very small vaults. Clamping to zero
1354
+ // hid that signal.
1355
+ reduction: Number((1 - entry.queryTokens / Math.max(1, corpusTokens)).toFixed(3))
1346
1356
  }));
1347
1357
  const avgQueryTokens = perQuestion.length ? Math.max(1, Math.round(perQuestion.reduce((total, entry) => total + entry.queryTokens, 0) / perQuestion.length)) : 0;
1348
- const reductionRatio = avgQueryTokens ? Number(Math.max(0, 1 - avgQueryTokens / Math.max(1, corpusTokens)).toFixed(3)) : 0;
1358
+ const reductionRatio = avgQueryTokens ? Number((1 - avgQueryTokens / Math.max(1, corpusTokens)).toFixed(3)) : 0;
1349
1359
  const uniqueVisitedNodes = new Set(perQuestion.flatMap((entry) => entry.visitedNodeIds)).size;
1350
1360
  const summary = {
1351
1361
  questionCount: input.questions.length,
@@ -1727,6 +1737,7 @@ import { pathToFileURL } from "url";
1727
1737
  import { Readability } from "@mozilla/readability";
1728
1738
  import matter3 from "gray-matter";
1729
1739
  import ignore from "ignore";
1740
+ import { isText } from "istextorbinary";
1730
1741
  import { JSDOM as JSDOM2 } from "jsdom";
1731
1742
  import mime from "mime-types";
1732
1743
  import TurndownService2 from "turndown";
@@ -2446,14 +2457,18 @@ function rubyStringContent(node) {
2446
2457
  const contentNode = node.descendantsOfType(["string_content", "simple_symbol", "bare_string"]).find((item) => item !== null) ?? null;
2447
2458
  return contentNode?.text.trim() || void 0;
2448
2459
  }
2460
+ function normalizePowerShellDotSourceSpecifier(raw) {
2461
+ const unquoted = raw.replace(/^['"]+|['"]+$/g, "").trim();
2462
+ return unquoted.replace(/^\$PSScriptRoot(?:[\\/]+|$)/i, "./");
2463
+ }
2449
2464
  function parsePowerShellImport(commandNode) {
2450
2465
  const commandName = commandNode.descendantsOfType(["command_name", "command_name_expr"]).find((item) => item !== null)?.text.trim();
2451
2466
  const genericTokens = commandNode.descendantsOfType("generic_token").filter((item) => item !== null).map((item) => item.text.trim());
2452
2467
  if (commandNode.namedChildren.some((child) => child?.type === "command_invokation_operator")) {
2453
- const specifier = commandName?.trim();
2454
- if (specifier) {
2468
+ const raw = commandName?.trim();
2469
+ if (raw) {
2455
2470
  return {
2456
- specifier,
2471
+ specifier: normalizePowerShellDotSourceSpecifier(raw),
2457
2472
  importedSymbols: [],
2458
2473
  isExternal: false,
2459
2474
  reExport: false
@@ -2934,6 +2949,21 @@ function rustCodeAnalysis(manifest, rootNode, diagnostics) {
2934
2949
  imports.push(parseRustUse(child.text));
2935
2950
  continue;
2936
2951
  }
2952
+ if (child.type === "mod_item") {
2953
+ const hasInlineBody = child.namedChildren.some((item) => item?.type === "declaration_list");
2954
+ if (!hasInlineBody) {
2955
+ const modName = extractIdentifier(child.childForFieldName("name") ?? findNamedChild(child, "identifier"));
2956
+ if (modName) {
2957
+ imports.push({
2958
+ specifier: `self::${modName}`,
2959
+ importedSymbols: [],
2960
+ isExternal: false,
2961
+ reExport: false
2962
+ });
2963
+ }
2964
+ }
2965
+ continue;
2966
+ }
2937
2967
  const name = child.type === "function_item" ? extractIdentifier(child.childForFieldName("name")) : extractIdentifier(child.childForFieldName("name"));
2938
2968
  if (child.type === "impl_item") {
2939
2969
  const traitName = normalizeSymbolReference(nodeText(child.childForFieldName("trait")));
@@ -3517,8 +3547,9 @@ function rubyCodeAnalysis(manifest, rootNode, diagnostics) {
3517
3547
  if (callee === "require" || callee === "require_relative") {
3518
3548
  const specifier = rubyStringContent(child.childForFieldName("arguments") ?? child.namedChildren.at(1) ?? null);
3519
3549
  if (specifier) {
3550
+ const normalizedSpecifier = callee === "require_relative" && !specifier.startsWith(".") && !specifier.startsWith("/") ? `./${specifier}` : specifier;
3520
3551
  imports.push({
3521
- specifier,
3552
+ specifier: normalizedSpecifier,
3522
3553
  importedSymbols: [],
3523
3554
  isExternal: callee === "require" && !specifier.startsWith("."),
3524
3555
  reExport: false
@@ -3765,7 +3796,7 @@ async function analyzeTreeSitterCode(manifest, content, language) {
3765
3796
  };
3766
3797
  }
3767
3798
  try {
3768
- const diagnostics = diagnosticsFromTree(tree.rootNode);
3799
+ const diagnostics = language === "lua" ? [] : diagnosticsFromTree(tree.rootNode);
3769
3800
  const rationales = extractTreeSitterRationales(manifest, language, tree.rootNode);
3770
3801
  switch (language) {
3771
3802
  case "bash":
@@ -3869,8 +3900,31 @@ function interpreterFromShebang(content) {
3869
3900
  }
3870
3901
  return basename(parts[0] ?? "");
3871
3902
  }
3872
- function isShellInterpreter(value) {
3873
- return value === "sh" || value === "bash" || value === "zsh";
3903
+ function languageFromInterpreter(interpreter) {
3904
+ switch (interpreter) {
3905
+ case "sh":
3906
+ case "bash":
3907
+ case "zsh":
3908
+ case "dash":
3909
+ case "ksh":
3910
+ case "ash":
3911
+ return "bash";
3912
+ case "node":
3913
+ case "nodejs":
3914
+ return "javascript";
3915
+ case "python":
3916
+ case "python2":
3917
+ case "python3":
3918
+ return "python";
3919
+ case "ruby":
3920
+ return "ruby";
3921
+ case "php":
3922
+ return "php";
3923
+ case "lua":
3924
+ return "lua";
3925
+ default:
3926
+ return void 0;
3927
+ }
3874
3928
  }
3875
3929
  function formatDiagnosticCategory(category) {
3876
3930
  switch (category) {
@@ -4472,8 +4526,11 @@ function inferCodeLanguage(filePath, mimeType = "", options = {}) {
4472
4526
  if ([".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"].includes(extension)) {
4473
4527
  return "cpp";
4474
4528
  }
4475
- if (!extension && options.executable && isShellInterpreter(interpreterFromShebang(options.content))) {
4476
- return "bash";
4529
+ if (!extension && options.executable) {
4530
+ const fromShebang = languageFromInterpreter(interpreterFromShebang(options.content));
4531
+ if (fromShebang) {
4532
+ return fromShebang;
4533
+ }
4477
4534
  }
4478
4535
  return void 0;
4479
4536
  }
@@ -4482,7 +4539,13 @@ function modulePageTitle(manifest) {
4482
4539
  }
4483
4540
  function importResolutionCandidates(basePath, specifier, extensions) {
4484
4541
  const resolved = path6.posix.normalize(path6.posix.join(path6.posix.dirname(basePath), specifier));
4485
- if (path6.posix.extname(resolved)) {
4542
+ const resolvedExt = path6.posix.extname(resolved);
4543
+ if (resolvedExt) {
4544
+ if (extensions.includes(resolvedExt)) {
4545
+ const resolvedBase = resolved.slice(0, -resolvedExt.length);
4546
+ const candidates = [resolved, ...extensions.map((extension) => `${resolvedBase}${extension}`)];
4547
+ return uniqueBy(candidates, (candidate) => candidate);
4548
+ }
4486
4549
  return [resolved];
4487
4550
  }
4488
4551
  const direct = extensions.map((extension) => path6.posix.normalize(`${resolved}${extension}`));
@@ -4577,7 +4640,9 @@ async function readNearestGoModulePath(startPath, cache) {
4577
4640
  }
4578
4641
  function rustModuleAlias(repoRelativePath) {
4579
4642
  const withoutExt = stripCodeExtension2(normalizeAlias(repoRelativePath));
4580
- const trimmed = withoutExt.replace(/^src\//, "").replace(/\/mod$/i, "");
4643
+ const srcIdx = withoutExt.lastIndexOf("/src/");
4644
+ const withinCrate = srcIdx >= 0 ? withoutExt.slice(srcIdx + "/src/".length) : withoutExt.replace(/^src\//, "");
4645
+ const trimmed = withinCrate.replace(/\/mod$/i, "");
4581
4646
  if (!trimmed || trimmed === "lib" || trimmed === "main") {
4582
4647
  return "crate";
4583
4648
  }
@@ -4792,7 +4857,7 @@ function resolveRustAliases(manifest, specifier) {
4792
4857
  if (!currentAlias) {
4793
4858
  return [];
4794
4859
  }
4795
- const currentParts = currentAlias.replace(/^crate::?/, "").split("::").filter(Boolean);
4860
+ const currentParts = currentAlias.replace(/^crate(?:::)?/, "").split("::").filter(Boolean);
4796
4861
  if (specifier.startsWith("self::")) {
4797
4862
  return [`crate${currentParts.length ? `::${currentParts.join("::")}` : ""}::${specifier.slice("self::".length)}`];
4798
4863
  }
@@ -6583,7 +6648,7 @@ function inferKind(mimeType, filePath, detectionOptions = {}) {
6583
6648
  if (mimeType === "text/csv" || mimeType === "text/tab-separated-values" || filePath.toLowerCase().endsWith(".csv") || filePath.toLowerCase().endsWith(".tsv")) {
6584
6649
  return "csv";
6585
6650
  }
6586
- if (mimeType.startsWith("text/")) {
6651
+ if (mimeType.startsWith("text/") || isStructuredTextMime(mimeType)) {
6587
6652
  return "text";
6588
6653
  }
6589
6654
  if (mimeType === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || filePath.toLowerCase().endsWith(".xlsx")) {
@@ -6597,6 +6662,26 @@ function inferKind(mimeType, filePath, detectionOptions = {}) {
6597
6662
  }
6598
6663
  return "binary";
6599
6664
  }
6665
+ function isStructuredTextMime(mimeType) {
6666
+ switch (mimeType) {
6667
+ case "application/json":
6668
+ case "application/json5":
6669
+ case "application/ld+json":
6670
+ case "application/manifest+json":
6671
+ case "application/xml":
6672
+ case "application/toml":
6673
+ case "application/yaml":
6674
+ case "application/x-yaml":
6675
+ case "application/javascript":
6676
+ case "application/ecmascript":
6677
+ case "application/typescript":
6678
+ case "application/x-sh":
6679
+ case "application/x-shellscript":
6680
+ return true;
6681
+ default:
6682
+ return false;
6683
+ }
6684
+ }
6600
6685
  async function localCodeDetectionOptions(absolutePath, payloadBytes) {
6601
6686
  if (path12.extname(absolutePath)) {
6602
6687
  return {};
@@ -6645,8 +6730,35 @@ function guessMimeType(target) {
6645
6730
  if (isRstFilePath(target)) {
6646
6731
  return "text/x-rst";
6647
6732
  }
6733
+ const extension = path12.extname(target).toLowerCase();
6734
+ if (extension === ".ts" || extension === ".tsx" || extension === ".mts" || extension === ".cts") {
6735
+ return "text/typescript";
6736
+ }
6648
6737
  return mime.lookup(target) || "application/octet-stream";
6649
6738
  }
6739
+ function refineBinaryKindWithBytes(absolutePath, currentKind, bytes) {
6740
+ if (currentKind !== "binary") {
6741
+ return currentKind;
6742
+ }
6743
+ const sniffSlice = bytes.length > 4096 ? bytes.subarray(0, 4096) : bytes;
6744
+ return isText(absolutePath, sniffSlice) ? "text" : currentKind;
6745
+ }
6746
+ async function refineBinaryKindWithContentSniff(absolutePath, currentKind) {
6747
+ if (currentKind !== "binary") {
6748
+ return currentKind;
6749
+ }
6750
+ let handle;
6751
+ try {
6752
+ handle = await fs11.open(absolutePath, "r");
6753
+ const chunk = Buffer.alloc(4096);
6754
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, 0);
6755
+ return refineBinaryKindWithBytes(absolutePath, currentKind, chunk.subarray(0, bytesRead));
6756
+ } catch {
6757
+ return currentKind;
6758
+ } finally {
6759
+ await handle?.close().catch(() => void 0);
6760
+ }
6761
+ }
6650
6762
  function sourceGroupIdFor(prepared) {
6651
6763
  const originKey = prepared.originType === "url" ? prepared.url ?? prepared.title : prepared.originalPath ?? prepared.title;
6652
6764
  return `${slugify(prepared.title)}-${sha256(originKey).slice(0, 8)}`;
@@ -7497,6 +7609,7 @@ async function collectDirectoryFiles(rootDir, inputDir, repoRoot, options) {
7497
7609
  sourceKind = "chat_export";
7498
7610
  }
7499
7611
  }
7612
+ sourceKind = await refineBinaryKindWithContentSniff(absolutePath, sourceKind);
7500
7613
  const sourceClass = sourceClassForRelativePath(relativePath, options);
7501
7614
  if (!supportedDirectoryKind(sourceKind)) {
7502
7615
  skipped.push({ path: toPosix(path12.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
@@ -8118,7 +8231,7 @@ async function prepareFileInputs(rootDir, absoluteInput, repoRoot, sourceClass)
8118
8231
  }
8119
8232
  const mimeType = guessMimeType(absoluteInput);
8120
8233
  const detectionOptions = await localCodeDetectionOptions(absoluteInput, payloadBytes);
8121
- const sourceKind = inferKind(mimeType, absoluteInput, detectionOptions);
8234
+ const sourceKind = refineBinaryKindWithBytes(absoluteInput, inferKind(mimeType, absoluteInput, detectionOptions), payloadBytes);
8122
8235
  const language = inferCodeLanguage(absoluteInput, mimeType, detectionOptions);
8123
8236
  const storedExtension = path12.extname(absoluteInput) || `.${mime.extension(mimeType) || "bin"}`;
8124
8237
  let title;
@@ -8564,7 +8677,7 @@ async function collectInboxAttachmentRefs(inputDir, files) {
8564
8677
  const sourceRefs = [];
8565
8678
  for (const ref of refs) {
8566
8679
  const resolved = path12.resolve(path12.dirname(absolutePath), ref);
8567
- if (!resolved.startsWith(inputDir) || !await fileExists(resolved)) {
8680
+ if (!isPathWithin(inputDir, resolved) || !await fileExists(resolved)) {
8568
8681
  continue;
8569
8682
  }
8570
8683
  sourceRefs.push({
@@ -8902,6 +9015,7 @@ async function importInbox(rootDir, inputDir) {
8902
9015
  sourceKind = "chat_export";
8903
9016
  }
8904
9017
  }
9018
+ sourceKind = await refineBinaryKindWithContentSniff(absolutePath, sourceKind);
8905
9019
  if (!isSupportedInboxKind(sourceKind)) {
8906
9020
  skipped.push({ path: toPosix(path12.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
8907
9021
  continue;
@@ -9099,6 +9213,7 @@ import { z as z7 } from "zod";
9099
9213
 
9100
9214
  // src/analysis.ts
9101
9215
  import path14 from "path";
9216
+ import nlp from "compromise";
9102
9217
  import { fromMarkdown } from "mdast-util-from-markdown";
9103
9218
  import { z as z2 } from "zod";
9104
9219
  var ANALYSIS_FORMAT_VERSION = 7;
@@ -9176,11 +9291,31 @@ function extractTopTerms(text, count) {
9176
9291
  return [...frequency.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])).slice(0, count).map(([token]) => token);
9177
9292
  }
9178
9293
  function extractEntities(text, count) {
9179
- const matches = text.match(/\b[A-Z][A-Za-z0-9-]+(?:\s+[A-Z][A-Za-z0-9-]+){0,2}\b/g) ?? [];
9180
- return uniqueBy(
9181
- matches.map((value) => normalizeWhitespace(value)),
9182
- (value) => value.toLowerCase()
9183
- ).slice(0, count);
9294
+ const candidates = [];
9295
+ try {
9296
+ const doc = nlp(text);
9297
+ const segments = [
9298
+ doc.match("#ProperNoun+").out("array"),
9299
+ doc.people().out("array"),
9300
+ doc.places().out("array"),
9301
+ doc.organizations().out("array"),
9302
+ doc.topics().out("array")
9303
+ ];
9304
+ for (const segment of segments) {
9305
+ for (const term of segment) {
9306
+ const normalized = normalizeWhitespace(term);
9307
+ if (normalized) {
9308
+ candidates.push(normalized);
9309
+ }
9310
+ }
9311
+ }
9312
+ } catch {
9313
+ }
9314
+ if (candidates.length === 0) {
9315
+ const matches = text.match(/\b[A-Z][A-Za-z0-9-]+(?:\s+[A-Z][A-Za-z0-9-]+){0,2}\b/g) ?? [];
9316
+ candidates.push(...matches.map((value) => normalizeWhitespace(value)));
9317
+ }
9318
+ return uniqueBy(candidates, (value) => value.toLowerCase()).slice(0, count);
9184
9319
  }
9185
9320
  function detectPolarity(text) {
9186
9321
  if (/\b(no|not|never|cannot|can't|won't|without)\b/i.test(text)) {
@@ -10899,15 +11034,37 @@ function graphAdjacency(graph) {
10899
11034
  }
10900
11035
  return adjacency;
10901
11036
  }
11037
+ var NODE_TYPE_PRIORITY = {
11038
+ concept: 6,
11039
+ entity: 5,
11040
+ source: 4,
11041
+ module: 3,
11042
+ symbol: 2,
11043
+ rationale: 1
11044
+ };
11045
+ function nodeTypePriority(type) {
11046
+ return NODE_TYPE_PRIORITY[type] ?? 0;
11047
+ }
11048
+ function compareLabelCandidates(left, right) {
11049
+ const priorityDelta = nodeTypePriority(right.type) - nodeTypePriority(left.type);
11050
+ if (priorityDelta !== 0) {
11051
+ return priorityDelta;
11052
+ }
11053
+ const degreeDelta = (right.degree ?? 0) - (left.degree ?? 0);
11054
+ if (degreeDelta !== 0) {
11055
+ return degreeDelta;
11056
+ }
11057
+ return left.id.localeCompare(right.id);
11058
+ }
10902
11059
  function resolveNode(graph, target) {
10903
11060
  const normalized = normalizeTarget(target);
10904
11061
  const byId = nodeById(graph);
10905
11062
  if (byId.has(target)) {
10906
11063
  return byId.get(target);
10907
11064
  }
10908
- const exact = graph.nodes.find((node) => normalizeTarget(node.label) === normalized || normalizeTarget(node.id) === normalized);
10909
- if (exact) {
10910
- return exact;
11065
+ const labelMatches = graph.nodes.filter((node) => normalizeTarget(node.label) === normalized || normalizeTarget(node.id) === normalized);
11066
+ if (labelMatches.length) {
11067
+ return labelMatches.sort(compareLabelCandidates)[0];
10911
11068
  }
10912
11069
  const pages = graph.pages.map((page) => ({
10913
11070
  page,
@@ -10916,7 +11073,7 @@ function resolveNode(graph, target) {
10916
11073
  if (pages[0]) {
10917
11074
  return primaryNodeForPage(graph, pages[0].page);
10918
11075
  }
10919
- return graph.nodes.map((node) => ({ node, score: Math.max(scoreMatch(target, node.label), scoreMatch(target, node.id)) })).filter((item) => item.score > 0).sort((left, right) => right.score - left.score || left.node.label.localeCompare(right.node.label))[0]?.node;
11076
+ return graph.nodes.map((node) => ({ node, score: Math.max(scoreMatch(target, node.label), scoreMatch(target, node.id)) })).filter((item) => item.score > 0).sort((left, right) => right.score - left.score || compareLabelCandidates(left.node, right.node))[0]?.node;
10920
11077
  }
10921
11078
  function communityLabel(graph, communityId) {
10922
11079
  if (!communityId) {
@@ -13592,7 +13749,7 @@ async function resolveImageGenerationProvider(rootDir) {
13592
13749
  if (!providerConfig) {
13593
13750
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
13594
13751
  }
13595
- const { createProvider: createProvider2 } = await import("./registry-TYROWPR5.js");
13752
+ const { createProvider: createProvider2 } = await import("./registry-NBLIJHZT.js");
13596
13753
  return createProvider2(preferredProviderId, providerConfig, rootDir);
13597
13754
  }
13598
13755
  async function generateOutputArtifacts(rootDir, input) {
@@ -17631,9 +17788,16 @@ async function listPages(rootDir) {
17631
17788
  return graph?.pages ?? [];
17632
17789
  }
17633
17790
  async function readPage(rootDir, relativePath) {
17791
+ if (!relativePath) {
17792
+ return null;
17793
+ }
17634
17794
  const { paths } = await loadVaultConfig(rootDir);
17635
17795
  const absolutePath = path23.resolve(paths.wikiDir, relativePath);
17636
- if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
17796
+ if (!isPathWithin(paths.wikiDir, absolutePath)) {
17797
+ return null;
17798
+ }
17799
+ const stats = await fs19.stat(absolutePath).catch(() => null);
17800
+ if (!stats?.isFile()) {
17637
17801
  return null;
17638
17802
  }
17639
17803
  const raw = await fs19.readFile(absolutePath, "utf8");
@@ -17662,6 +17826,32 @@ async function getWorkspaceInfo(rootDir) {
17662
17826
  pageCount: pages.length
17663
17827
  };
17664
17828
  }
17829
+ function extractClaimSectionLines(content) {
17830
+ const lines = content.split("\n");
17831
+ let inClaims = false;
17832
+ let found = false;
17833
+ const claimLines = [];
17834
+ for (const line of lines) {
17835
+ const trimmed = line.trimEnd();
17836
+ if (trimmed === "## Claims") {
17837
+ inClaims = true;
17838
+ found = true;
17839
+ continue;
17840
+ }
17841
+ if (inClaims) {
17842
+ if (/^#{1,2}\s/.test(trimmed)) {
17843
+ inClaims = false;
17844
+ continue;
17845
+ }
17846
+ claimLines.push(line);
17847
+ }
17848
+ }
17849
+ return found ? claimLines : null;
17850
+ }
17851
+ function isClaimPlaceholderBullet(line) {
17852
+ const trimmed = line.trim();
17853
+ return /^-\s+No\s+claims\s+extracted\.?$/i.test(trimmed);
17854
+ }
17665
17855
  function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sourceProjects) {
17666
17856
  const manifestMap = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
17667
17857
  const pageMap2 = new Map(graph.pages.map((page) => [page.id, page]));
@@ -17707,8 +17897,11 @@ function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sour
17707
17897
  const absolutePath = path23.join(paths.wikiDir, page.path);
17708
17898
  if (await fileExists(absolutePath)) {
17709
17899
  const content = await fs19.readFile(absolutePath, "utf8");
17710
- if (content.includes("## Claims")) {
17711
- const uncited = content.split("\n").filter((line) => line.startsWith("- ") && !line.includes("[source:"));
17900
+ const claimLines = extractClaimSectionLines(content);
17901
+ if (claimLines !== null) {
17902
+ const uncited = claimLines.filter(
17903
+ (line) => line.startsWith("- ") && !line.includes("[source:") && !isClaimPlaceholderBullet(line)
17904
+ );
17712
17905
  if (uncited.length) {
17713
17906
  findings.push({
17714
17907
  severity: "warning",
@@ -17823,7 +18016,7 @@ async function bootstrapDemo(rootDir, input) {
17823
18016
  }
17824
18017
 
17825
18018
  // src/mcp.ts
17826
- var SERVER_VERSION = "0.6.5";
18019
+ var SERVER_VERSION = "0.6.7";
17827
18020
  async function createMcpServer(rootDir) {
17828
18021
  const server = new McpServer({
17829
18022
  name: "swarmvault",
@@ -18158,7 +18351,7 @@ async function createMcpServer(rootDir) {
18158
18351
  const encodedPath = typeof variables.path === "string" ? variables.path : "";
18159
18352
  const relativePath = decodeURIComponent(encodedPath);
18160
18353
  const absolutePath = path24.resolve(paths.sessionsDir, relativePath);
18161
- if (!absolutePath.startsWith(paths.sessionsDir) || !await fileExists(absolutePath)) {
18354
+ if (!isPathWithin(paths.sessionsDir, absolutePath) || !await fileExists(absolutePath)) {
18162
18355
  return asTextResource(`swarmvault://sessions/${encodedPath}`, `Session not found: ${relativePath}`);
18163
18356
  }
18164
18357
  return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs20.readFile(absolutePath, "utf8"));
@@ -18497,6 +18690,15 @@ var DOCS_HINT_SEGMENTS = /* @__PURE__ */ new Set([
18497
18690
  function uniqueStrings4(values) {
18498
18691
  return uniqueBy(values.filter(Boolean), (value) => value);
18499
18692
  }
18693
+ function sourceOutputSchemaHash(schemas, projectIds) {
18694
+ if (!projectIds.length) {
18695
+ return schemas.effective.global.hash;
18696
+ }
18697
+ return composeVaultSchema(
18698
+ schemas.root,
18699
+ uniqueStrings4([...projectIds].sort((left, right) => left.localeCompare(right))).map((projectId) => schemas.projects[projectId]).filter((schema) => Boolean(schema?.hash))
18700
+ ).hash;
18701
+ }
18500
18702
  function normalizeManagedStatus(value) {
18501
18703
  return value === "missing" || value === "error" ? value : "ready";
18502
18704
  }
@@ -19080,6 +19282,7 @@ async function writeSourceBriefForScope(rootDir, source) {
19080
19282
  return null;
19081
19283
  }
19082
19284
  const graph = await readJsonFile(paths.graphPath);
19285
+ const schemas = await loadVaultSchemas(rootDir);
19083
19286
  const relatedPages = graph ? scopedSourcePages(graph, source.sourceIds) : [];
19084
19287
  const relatedPageIds = relatedPages.slice(0, 12).map((page) => page.id);
19085
19288
  const relatedNodeIds = graph ? scopedNodeIds(graph, source.sourceIds).slice(0, 20) : [];
@@ -19090,7 +19293,7 @@ async function writeSourceBriefForScope(rootDir, source) {
19090
19293
  question: `Brief ${source.title}`,
19091
19294
  answer: markdown,
19092
19295
  citations: source.sourceIds,
19093
- schemaHash: graph?.generatedAt ?? "",
19296
+ schemaHash: sourceOutputSchemaHash(schemas, projectIds),
19094
19297
  outputFormat: "report",
19095
19298
  relatedPageIds,
19096
19299
  relatedNodeIds,
@@ -19325,6 +19528,7 @@ async function buildSourceReviewStagedPage(rootDir, scope) {
19325
19528
  throw new Error(`Could not generate a source review for ${scope.id}.`);
19326
19529
  }
19327
19530
  const graph = await readJsonFile(paths.graphPath);
19531
+ const schemas = await loadVaultSchemas(rootDir);
19328
19532
  const scopeManifests = manifestsForScope(graph, scope);
19329
19533
  const relatedPages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
19330
19534
  const relatedPageIds = relatedPages.slice(0, 16).map((page) => page.id);
@@ -19336,7 +19540,7 @@ async function buildSourceReviewStagedPage(rootDir, scope) {
19336
19540
  question: `Review ${scope.title}`,
19337
19541
  answer: markdown,
19338
19542
  citations: scope.sourceIds,
19339
- schemaHash: graph?.generatedAt ?? "",
19543
+ schemaHash: sourceOutputSchemaHash(schemas, projectIds),
19340
19544
  outputFormat: "report",
19341
19545
  relatedPageIds,
19342
19546
  relatedNodeIds,
@@ -19570,6 +19774,7 @@ async function buildSourceGuideStagedPage(rootDir, scope) {
19570
19774
  throw new Error(`Could not generate a source guide for ${scope.id}.`);
19571
19775
  }
19572
19776
  const graph = await readJsonFile(paths.graphPath);
19777
+ const schemas = await loadVaultSchemas(rootDir);
19573
19778
  const scopeManifests = manifestsForScope(graph, scope);
19574
19779
  const relatedPages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
19575
19780
  const contradictions = findContradictionsForScope(scope, await readGraphReport(rootDir));
@@ -19583,7 +19788,7 @@ async function buildSourceGuideStagedPage(rootDir, scope) {
19583
19788
  question: `Guide ${scope.title}`,
19584
19789
  answer: markdown,
19585
19790
  citations: scope.sourceIds,
19586
- schemaHash: graph?.generatedAt ?? "",
19791
+ schemaHash: sourceOutputSchemaHash(schemas, projectIds),
19587
19792
  outputFormat: "report",
19588
19793
  relatedPageIds,
19589
19794
  relatedNodeIds,
@@ -19672,6 +19877,7 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
19672
19877
  await compileVault(rootDir, {});
19673
19878
  graph = await readJsonFile(paths.graphPath);
19674
19879
  }
19880
+ const schemas = await loadVaultSchemas(rootDir);
19675
19881
  const scopeManifests = manifestsForScope(graph, scope);
19676
19882
  const sourcePages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
19677
19883
  const analyses = await loadSourceAnalyses(rootDir, scope.sourceIds);
@@ -19737,7 +19943,7 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
19737
19943
  question: `Guided Session ${scope.title}`,
19738
19944
  answer: sessionMarkdown,
19739
19945
  citations: scope.sourceIds,
19740
- schemaHash: graph?.generatedAt ?? "",
19946
+ schemaHash: sourceOutputSchemaHash(schemas, projectIds),
19741
19947
  outputFormat: "report",
19742
19948
  relatedPageIds,
19743
19949
  relatedNodeIds,
@@ -20782,10 +20988,21 @@ async function getWatchStatus(rootDir) {
20782
20988
 
20783
20989
  // src/viewer.ts
20784
20990
  var execFileAsync = promisify(execFile);
20991
+ async function isReadableFile(absolutePath) {
20992
+ try {
20993
+ const stats = await fs23.stat(absolutePath);
20994
+ return stats.isFile();
20995
+ } catch {
20996
+ return false;
20997
+ }
20998
+ }
20785
20999
  async function readViewerPage(rootDir, relativePath) {
21000
+ if (!relativePath) {
21001
+ return null;
21002
+ }
20786
21003
  const { paths } = await loadVaultConfig(rootDir);
20787
21004
  const absolutePath = path28.resolve(paths.wikiDir, relativePath);
20788
- if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
21005
+ if (!isPathWithin(paths.wikiDir, absolutePath) || !await isReadableFile(absolutePath)) {
20789
21006
  return null;
20790
21007
  }
20791
21008
  const raw = await fs23.readFile(absolutePath, "utf8");
@@ -20799,9 +21016,12 @@ async function readViewerPage(rootDir, relativePath) {
20799
21016
  };
20800
21017
  }
20801
21018
  async function readViewerAsset(rootDir, relativePath) {
21019
+ if (!relativePath) {
21020
+ return null;
21021
+ }
20802
21022
  const { paths } = await loadVaultConfig(rootDir);
20803
21023
  const absolutePath = path28.resolve(paths.wikiDir, relativePath);
20804
- if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
21024
+ if (!isPathWithin(paths.wikiDir, absolutePath) || !await isReadableFile(absolutePath)) {
20805
21025
  return null;
20806
21026
  }
20807
21027
  return {
@@ -20843,178 +21063,200 @@ async function startGraphServer(rootDir, port, options = {}) {
20843
21063
  await ensureViewerDist(paths.viewerDistDir);
20844
21064
  const server = http.createServer(async (request, response) => {
20845
21065
  const url = new URL(request.url ?? "/", `http://${request.headers.host ?? `localhost:${effectivePort}`}`);
20846
- if (url.pathname === "/api/graph") {
20847
- if (!await fileExists(paths.graphPath)) {
20848
- response.writeHead(404, { "content-type": "application/json" });
20849
- response.end(JSON.stringify({ error: "Graph artifact not found. Run `swarmvault compile` first." }));
21066
+ try {
21067
+ if (url.pathname === "/api/graph") {
21068
+ if (!await fileExists(paths.graphPath)) {
21069
+ response.writeHead(404, { "content-type": "application/json" });
21070
+ response.end(JSON.stringify({ error: "Graph artifact not found. Run `swarmvault compile` first." }));
21071
+ return;
21072
+ }
21073
+ const graph = await readJsonFile(paths.graphPath);
21074
+ if (!graph) {
21075
+ response.writeHead(404, { "content-type": "application/json" });
21076
+ response.end(JSON.stringify({ error: "Graph artifact not found. Run `swarmvault compile` first." }));
21077
+ return;
21078
+ }
21079
+ const reportPath = path28.join(paths.wikiDir, "graph", "report.json");
21080
+ const report = await readJsonFile(reportPath) ?? null;
21081
+ response.writeHead(200, { "content-type": "application/json" });
21082
+ response.end(JSON.stringify(buildViewerGraphArtifact(graph, { report, full: options.full ?? false })));
20850
21083
  return;
20851
21084
  }
20852
- const graph = await readJsonFile(paths.graphPath);
20853
- if (!graph) {
20854
- response.writeHead(404, { "content-type": "application/json" });
20855
- response.end(JSON.stringify({ error: "Graph artifact not found. Run `swarmvault compile` first." }));
21085
+ if (url.pathname === "/api/graph/query") {
21086
+ const question = url.searchParams.get("q") ?? "";
21087
+ const traversal = url.searchParams.get("traversal");
21088
+ const budget = Number.parseInt(url.searchParams.get("budget") ?? "12", 10);
21089
+ const result = await queryGraphVault(rootDir, question, {
21090
+ traversal: traversal === "dfs" ? "dfs" : "bfs",
21091
+ budget: Number.isFinite(budget) ? budget : 12
21092
+ });
21093
+ response.writeHead(200, { "content-type": "application/json" });
21094
+ response.end(JSON.stringify(result));
20856
21095
  return;
20857
21096
  }
20858
- const reportPath = path28.join(paths.wikiDir, "graph", "report.json");
20859
- const report = await readJsonFile(reportPath) ?? null;
20860
- response.writeHead(200, { "content-type": "application/json" });
20861
- response.end(JSON.stringify(buildViewerGraphArtifact(graph, { report, full: options.full ?? false })));
20862
- return;
20863
- }
20864
- if (url.pathname === "/api/graph/query") {
20865
- const question = url.searchParams.get("q") ?? "";
20866
- const traversal = url.searchParams.get("traversal");
20867
- const budget = Number.parseInt(url.searchParams.get("budget") ?? "12", 10);
20868
- response.writeHead(200, { "content-type": "application/json" });
20869
- response.end(
20870
- JSON.stringify(
20871
- await queryGraphVault(rootDir, question, {
20872
- traversal: traversal === "dfs" ? "dfs" : "bfs",
20873
- budget: Number.isFinite(budget) ? budget : 12
20874
- })
20875
- )
20876
- );
20877
- return;
20878
- }
20879
- if (url.pathname === "/api/graph/path") {
20880
- const from = url.searchParams.get("from") ?? "";
20881
- const to = url.searchParams.get("to") ?? "";
20882
- response.writeHead(200, { "content-type": "application/json" });
20883
- response.end(JSON.stringify(await pathGraphVault(rootDir, from, to)));
20884
- return;
20885
- }
20886
- if (url.pathname === "/api/graph/explain") {
20887
- const target2 = url.searchParams.get("target") ?? "";
20888
- response.writeHead(200, { "content-type": "application/json" });
20889
- response.end(JSON.stringify(await explainGraphVault(rootDir, target2)));
20890
- return;
20891
- }
20892
- if (url.pathname === "/api/search") {
20893
- if (!await fileExists(paths.searchDbPath)) {
20894
- response.writeHead(404, { "content-type": "application/json" });
20895
- response.end(JSON.stringify({ error: "Search index not found. Run `swarmvault compile` first." }));
21097
+ if (url.pathname === "/api/graph/path") {
21098
+ const from = url.searchParams.get("from") ?? "";
21099
+ const to = url.searchParams.get("to") ?? "";
21100
+ const result = await pathGraphVault(rootDir, from, to);
21101
+ response.writeHead(200, { "content-type": "application/json" });
21102
+ response.end(JSON.stringify(result));
20896
21103
  return;
20897
21104
  }
20898
- const query = url.searchParams.get("q") ?? "";
20899
- const limit = Number.parseInt(url.searchParams.get("limit") ?? "10", 10);
20900
- const kind = url.searchParams.get("kind") ?? "all";
20901
- const status = url.searchParams.get("status") ?? "all";
20902
- const project = url.searchParams.get("project") ?? "all";
20903
- const sourceType = url.searchParams.get("sourceType") ?? "all";
20904
- const sourceClass = url.searchParams.get("sourceClass") ?? "all";
20905
- const results = searchPages(paths.searchDbPath, query, {
20906
- limit: Number.isFinite(limit) ? limit : 10,
20907
- kind,
20908
- status,
20909
- project,
20910
- sourceType,
20911
- sourceClass
20912
- });
20913
- response.writeHead(200, { "content-type": "application/json" });
20914
- response.end(JSON.stringify(results));
20915
- return;
20916
- }
20917
- if (url.pathname === "/api/graph-report") {
20918
- const reportPath = path28.join(paths.wikiDir, "graph", "report.json");
20919
- if (!await fileExists(reportPath)) {
20920
- response.writeHead(404, { "content-type": "application/json" });
20921
- response.end(JSON.stringify({ error: "Graph report artifact not found. Run `swarmvault compile` first." }));
21105
+ if (url.pathname === "/api/graph/explain") {
21106
+ const target2 = url.searchParams.get("target") ?? "";
21107
+ if (!target2) {
21108
+ response.writeHead(400, { "content-type": "application/json" });
21109
+ response.end(JSON.stringify({ error: "Missing explain target." }));
21110
+ return;
21111
+ }
21112
+ const result = await explainGraphVault(rootDir, target2);
21113
+ response.writeHead(200, { "content-type": "application/json" });
21114
+ response.end(JSON.stringify(result));
20922
21115
  return;
20923
21116
  }
20924
- response.writeHead(200, { "content-type": "application/json" });
20925
- response.end(await fs23.readFile(reportPath, "utf8"));
20926
- return;
20927
- }
20928
- if (url.pathname === "/api/watch-status") {
20929
- response.writeHead(200, { "content-type": "application/json" });
20930
- response.end(JSON.stringify(await getWatchStatus(rootDir)));
20931
- return;
20932
- }
20933
- if (url.pathname === "/api/page") {
20934
- const relativePath2 = url.searchParams.get("path") ?? "";
20935
- const page = await readViewerPage(rootDir, relativePath2);
20936
- if (!page) {
20937
- response.writeHead(404, { "content-type": "application/json" });
20938
- response.end(JSON.stringify({ error: `Page not found: ${relativePath2}` }));
21117
+ if (url.pathname === "/api/search") {
21118
+ if (!await fileExists(paths.searchDbPath)) {
21119
+ response.writeHead(404, { "content-type": "application/json" });
21120
+ response.end(JSON.stringify({ error: "Search index not found. Run `swarmvault compile` first." }));
21121
+ return;
21122
+ }
21123
+ const query = url.searchParams.get("q") ?? "";
21124
+ const limit = Number.parseInt(url.searchParams.get("limit") ?? "10", 10);
21125
+ const kind = url.searchParams.get("kind") ?? "all";
21126
+ const status = url.searchParams.get("status") ?? "all";
21127
+ const project = url.searchParams.get("project") ?? "all";
21128
+ const sourceType = url.searchParams.get("sourceType") ?? "all";
21129
+ const sourceClass = url.searchParams.get("sourceClass") ?? "all";
21130
+ const results = searchPages(paths.searchDbPath, query, {
21131
+ limit: Number.isFinite(limit) ? limit : 10,
21132
+ kind,
21133
+ status,
21134
+ project,
21135
+ sourceType,
21136
+ sourceClass
21137
+ });
21138
+ response.writeHead(200, { "content-type": "application/json" });
21139
+ response.end(JSON.stringify(results));
20939
21140
  return;
20940
21141
  }
20941
- response.writeHead(200, { "content-type": "application/json" });
20942
- response.end(JSON.stringify(page));
20943
- return;
20944
- }
20945
- if (url.pathname === "/api/asset") {
20946
- const relativePath2 = url.searchParams.get("path") ?? "";
20947
- const asset = await readViewerAsset(rootDir, relativePath2);
20948
- if (!asset) {
20949
- response.writeHead(404, { "content-type": "application/json" });
20950
- response.end(JSON.stringify({ error: `Asset not found: ${relativePath2}` }));
21142
+ if (url.pathname === "/api/graph-report") {
21143
+ const reportPath = path28.join(paths.wikiDir, "graph", "report.json");
21144
+ if (!await fileExists(reportPath)) {
21145
+ response.writeHead(404, { "content-type": "application/json" });
21146
+ response.end(JSON.stringify({ error: "Graph report artifact not found. Run `swarmvault compile` first." }));
21147
+ return;
21148
+ }
21149
+ const body = await fs23.readFile(reportPath, "utf8");
21150
+ response.writeHead(200, { "content-type": "application/json" });
21151
+ response.end(body);
20951
21152
  return;
20952
21153
  }
20953
- response.writeHead(200, { "content-type": asset.mimeType });
20954
- response.end(asset.buffer);
20955
- return;
20956
- }
20957
- if (url.pathname === "/api/reviews" && request.method === "GET") {
20958
- response.writeHead(200, { "content-type": "application/json" });
20959
- response.end(JSON.stringify(await listApprovals(rootDir)));
20960
- return;
20961
- }
20962
- if (url.pathname === "/api/review" && request.method === "GET") {
20963
- const approvalId = url.searchParams.get("id") ?? "";
20964
- if (!approvalId) {
20965
- response.writeHead(400, { "content-type": "application/json" });
20966
- response.end(JSON.stringify({ error: "Missing approval id." }));
21154
+ if (url.pathname === "/api/watch-status") {
21155
+ const watchStatus = await getWatchStatus(rootDir);
21156
+ response.writeHead(200, { "content-type": "application/json" });
21157
+ response.end(JSON.stringify(watchStatus));
20967
21158
  return;
20968
21159
  }
20969
- response.writeHead(200, { "content-type": "application/json" });
20970
- response.end(JSON.stringify(await readApproval(rootDir, approvalId)));
20971
- return;
20972
- }
20973
- if (url.pathname === "/api/review" && request.method === "POST") {
20974
- const body = await readJsonBody(request);
20975
- const approvalId = typeof body.approvalId === "string" ? body.approvalId : "";
20976
- const targets = Array.isArray(body.targets) ? body.targets.filter((item) => typeof item === "string") : [];
20977
- const action = url.searchParams.get("action") ?? "";
20978
- if (!approvalId || action !== "accept" && action !== "reject") {
20979
- response.writeHead(400, { "content-type": "application/json" });
20980
- response.end(JSON.stringify({ error: "Missing approval id or invalid review action." }));
21160
+ if (url.pathname === "/api/page") {
21161
+ const relativePath2 = url.searchParams.get("path") ?? "";
21162
+ const page = await readViewerPage(rootDir, relativePath2);
21163
+ if (!page) {
21164
+ response.writeHead(404, { "content-type": "application/json" });
21165
+ response.end(JSON.stringify({ error: `Page not found: ${relativePath2}` }));
21166
+ return;
21167
+ }
21168
+ response.writeHead(200, { "content-type": "application/json" });
21169
+ response.end(JSON.stringify(page));
20981
21170
  return;
20982
21171
  }
20983
- const result = action === "accept" ? await acceptApproval(rootDir, approvalId, targets) : await rejectApproval(rootDir, approvalId, targets);
20984
- response.writeHead(200, { "content-type": "application/json" });
20985
- response.end(JSON.stringify(result));
20986
- return;
20987
- }
20988
- if (url.pathname === "/api/candidates" && request.method === "GET") {
20989
- response.writeHead(200, { "content-type": "application/json" });
20990
- response.end(JSON.stringify(await listCandidates(rootDir)));
20991
- return;
20992
- }
20993
- if (url.pathname === "/api/candidate" && request.method === "POST") {
20994
- const body = await readJsonBody(request);
20995
- const target2 = typeof body.target === "string" ? body.target : "";
20996
- const action = url.searchParams.get("action") ?? "";
20997
- if (!target2 || action !== "promote" && action !== "archive") {
20998
- response.writeHead(400, { "content-type": "application/json" });
20999
- response.end(JSON.stringify({ error: "Missing candidate target or invalid candidate action." }));
21172
+ if (url.pathname === "/api/asset") {
21173
+ const relativePath2 = url.searchParams.get("path") ?? "";
21174
+ const asset = await readViewerAsset(rootDir, relativePath2);
21175
+ if (!asset) {
21176
+ response.writeHead(404, { "content-type": "application/json" });
21177
+ response.end(JSON.stringify({ error: `Asset not found: ${relativePath2}` }));
21178
+ return;
21179
+ }
21180
+ response.writeHead(200, { "content-type": asset.mimeType });
21181
+ response.end(asset.buffer);
21000
21182
  return;
21001
21183
  }
21002
- const result = action === "promote" ? await promoteCandidate(rootDir, target2) : await archiveCandidate(rootDir, target2);
21003
- response.writeHead(200, { "content-type": "application/json" });
21004
- response.end(JSON.stringify(result));
21005
- return;
21006
- }
21007
- const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
21008
- const target = path28.join(paths.viewerDistDir, relativePath);
21009
- const fallback = path28.join(paths.viewerDistDir, "index.html");
21010
- const filePath = await fileExists(target) ? target : fallback;
21011
- if (!await fileExists(filePath)) {
21012
- response.writeHead(503, { "content-type": "text/plain" });
21013
- response.end("Viewer build not found. Run `pnpm build` first.");
21014
- return;
21184
+ if (url.pathname === "/api/reviews" && request.method === "GET") {
21185
+ const approvals = await listApprovals(rootDir);
21186
+ response.writeHead(200, { "content-type": "application/json" });
21187
+ response.end(JSON.stringify(approvals));
21188
+ return;
21189
+ }
21190
+ if (url.pathname === "/api/review" && request.method === "GET") {
21191
+ const approvalId = url.searchParams.get("id") ?? "";
21192
+ if (!approvalId) {
21193
+ response.writeHead(400, { "content-type": "application/json" });
21194
+ response.end(JSON.stringify({ error: "Missing approval id." }));
21195
+ return;
21196
+ }
21197
+ const approval = await readApproval(rootDir, approvalId);
21198
+ response.writeHead(200, { "content-type": "application/json" });
21199
+ response.end(JSON.stringify(approval));
21200
+ return;
21201
+ }
21202
+ if (url.pathname === "/api/review" && request.method === "POST") {
21203
+ const body = await readJsonBody(request);
21204
+ const approvalId = typeof body.approvalId === "string" ? body.approvalId : "";
21205
+ const targets = Array.isArray(body.targets) ? body.targets.filter((item) => typeof item === "string") : [];
21206
+ const action = url.searchParams.get("action") ?? "";
21207
+ if (!approvalId || action !== "accept" && action !== "reject") {
21208
+ response.writeHead(400, { "content-type": "application/json" });
21209
+ response.end(JSON.stringify({ error: "Missing approval id or invalid review action." }));
21210
+ return;
21211
+ }
21212
+ const result = action === "accept" ? await acceptApproval(rootDir, approvalId, targets) : await rejectApproval(rootDir, approvalId, targets);
21213
+ response.writeHead(200, { "content-type": "application/json" });
21214
+ response.end(JSON.stringify(result));
21215
+ return;
21216
+ }
21217
+ if (url.pathname === "/api/candidates" && request.method === "GET") {
21218
+ const candidates = await listCandidates(rootDir);
21219
+ response.writeHead(200, { "content-type": "application/json" });
21220
+ response.end(JSON.stringify(candidates));
21221
+ return;
21222
+ }
21223
+ if (url.pathname === "/api/candidate" && request.method === "POST") {
21224
+ const body = await readJsonBody(request);
21225
+ const target2 = typeof body.target === "string" ? body.target : "";
21226
+ const action = url.searchParams.get("action") ?? "";
21227
+ if (!target2 || action !== "promote" && action !== "archive") {
21228
+ response.writeHead(400, { "content-type": "application/json" });
21229
+ response.end(JSON.stringify({ error: "Missing candidate target or invalid candidate action." }));
21230
+ return;
21231
+ }
21232
+ const result = action === "promote" ? await promoteCandidate(rootDir, target2) : await archiveCandidate(rootDir, target2);
21233
+ response.writeHead(200, { "content-type": "application/json" });
21234
+ response.end(JSON.stringify(result));
21235
+ return;
21236
+ }
21237
+ const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
21238
+ const target = path28.join(paths.viewerDistDir, relativePath);
21239
+ const fallback = path28.join(paths.viewerDistDir, "index.html");
21240
+ const filePath = await fileExists(target) ? target : fallback;
21241
+ if (!await fileExists(filePath)) {
21242
+ response.writeHead(503, { "content-type": "text/plain" });
21243
+ response.end("Viewer build not found. Run `pnpm build` first.");
21244
+ return;
21245
+ }
21246
+ const staticBody = await fs23.readFile(filePath);
21247
+ response.writeHead(200, { "content-type": mime2.lookup(filePath) || "text/plain" });
21248
+ response.end(staticBody);
21249
+ } catch (error) {
21250
+ const message = error instanceof Error ? error.message : String(error);
21251
+ console.error(`[viewer] ${request.method ?? "GET"} ${url.pathname} failed: ${message}`);
21252
+ if (!response.headersSent) {
21253
+ const status = /not found|could not resolve|cannot resolve/i.test(message) ? 404 : 500;
21254
+ response.writeHead(status, { "content-type": "application/json" });
21255
+ response.end(JSON.stringify({ error: message }));
21256
+ } else {
21257
+ response.end();
21258
+ }
21015
21259
  }
21016
- response.writeHead(200, { "content-type": mime2.lookup(filePath) || "text/plain" });
21017
- response.end(await fs23.readFile(filePath));
21018
21260
  });
21019
21261
  await new Promise((resolve) => {
21020
21262
  server.listen(effectivePort, resolve);