@swarmvaultai/engine 0.6.4 → 0.6.6

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";
@@ -3869,8 +3870,31 @@ function interpreterFromShebang(content) {
3869
3870
  }
3870
3871
  return basename(parts[0] ?? "");
3871
3872
  }
3872
- function isShellInterpreter(value) {
3873
- return value === "sh" || value === "bash" || value === "zsh";
3873
+ function languageFromInterpreter(interpreter) {
3874
+ switch (interpreter) {
3875
+ case "sh":
3876
+ case "bash":
3877
+ case "zsh":
3878
+ case "dash":
3879
+ case "ksh":
3880
+ case "ash":
3881
+ return "bash";
3882
+ case "node":
3883
+ case "nodejs":
3884
+ return "javascript";
3885
+ case "python":
3886
+ case "python2":
3887
+ case "python3":
3888
+ return "python";
3889
+ case "ruby":
3890
+ return "ruby";
3891
+ case "php":
3892
+ return "php";
3893
+ case "lua":
3894
+ return "lua";
3895
+ default:
3896
+ return void 0;
3897
+ }
3874
3898
  }
3875
3899
  function formatDiagnosticCategory(category) {
3876
3900
  switch (category) {
@@ -4472,8 +4496,11 @@ function inferCodeLanguage(filePath, mimeType = "", options = {}) {
4472
4496
  if ([".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"].includes(extension)) {
4473
4497
  return "cpp";
4474
4498
  }
4475
- if (!extension && options.executable && isShellInterpreter(interpreterFromShebang(options.content))) {
4476
- return "bash";
4499
+ if (!extension && options.executable) {
4500
+ const fromShebang = languageFromInterpreter(interpreterFromShebang(options.content));
4501
+ if (fromShebang) {
4502
+ return fromShebang;
4503
+ }
4477
4504
  }
4478
4505
  return void 0;
4479
4506
  }
@@ -6583,7 +6610,7 @@ function inferKind(mimeType, filePath, detectionOptions = {}) {
6583
6610
  if (mimeType === "text/csv" || mimeType === "text/tab-separated-values" || filePath.toLowerCase().endsWith(".csv") || filePath.toLowerCase().endsWith(".tsv")) {
6584
6611
  return "csv";
6585
6612
  }
6586
- if (mimeType.startsWith("text/")) {
6613
+ if (mimeType.startsWith("text/") || isStructuredTextMime(mimeType) || isKnownTextPath(filePath)) {
6587
6614
  return "text";
6588
6615
  }
6589
6616
  if (mimeType === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || filePath.toLowerCase().endsWith(".xlsx")) {
@@ -6597,6 +6624,26 @@ function inferKind(mimeType, filePath, detectionOptions = {}) {
6597
6624
  }
6598
6625
  return "binary";
6599
6626
  }
6627
+ function isStructuredTextMime(mimeType) {
6628
+ switch (mimeType) {
6629
+ case "application/json":
6630
+ case "application/json5":
6631
+ case "application/ld+json":
6632
+ case "application/manifest+json":
6633
+ case "application/xml":
6634
+ case "application/toml":
6635
+ case "application/yaml":
6636
+ case "application/x-yaml":
6637
+ case "application/javascript":
6638
+ case "application/ecmascript":
6639
+ case "application/typescript":
6640
+ case "application/x-sh":
6641
+ case "application/x-shellscript":
6642
+ return true;
6643
+ default:
6644
+ return false;
6645
+ }
6646
+ }
6600
6647
  async function localCodeDetectionOptions(absolutePath, payloadBytes) {
6601
6648
  if (path12.extname(absolutePath)) {
6602
6649
  return {};
@@ -6645,7 +6692,129 @@ function guessMimeType(target) {
6645
6692
  if (isRstFilePath(target)) {
6646
6693
  return "text/x-rst";
6647
6694
  }
6648
- return mime.lookup(target) || "application/octet-stream";
6695
+ const extension = path12.extname(target).toLowerCase();
6696
+ if (extension === ".ts" || extension === ".tsx" || extension === ".mts" || extension === ".cts") {
6697
+ return "text/typescript";
6698
+ }
6699
+ const looked = mime.lookup(target);
6700
+ if (looked) {
6701
+ return looked;
6702
+ }
6703
+ if (isKnownTextPath(target)) {
6704
+ return "text/plain";
6705
+ }
6706
+ return "application/octet-stream";
6707
+ }
6708
+ var KNOWN_TEXT_DOTFILE_NAMES = /* @__PURE__ */ new Set([
6709
+ ".gitignore",
6710
+ ".gitattributes",
6711
+ ".gitkeep",
6712
+ ".gitmodules",
6713
+ ".editorconfig",
6714
+ ".npmrc",
6715
+ ".yarnrc",
6716
+ ".prettierignore",
6717
+ ".prettierrc",
6718
+ ".dockerignore",
6719
+ ".eslintignore",
6720
+ ".eslintrc",
6721
+ ".nvmrc",
6722
+ ".node-version",
6723
+ ".python-version",
6724
+ ".ruby-version",
6725
+ ".tool-versions"
6726
+ ]);
6727
+ var KNOWN_TEXT_BASENAMES = /* @__PURE__ */ new Set([
6728
+ "readme",
6729
+ "license",
6730
+ "licence",
6731
+ "copying",
6732
+ "unlicense",
6733
+ "notice",
6734
+ "authors",
6735
+ "contributors",
6736
+ "patents",
6737
+ "maintainers",
6738
+ "owners",
6739
+ "codeowners",
6740
+ "changelog",
6741
+ "changes",
6742
+ "history",
6743
+ "news",
6744
+ "todo",
6745
+ "install",
6746
+ "dockerfile",
6747
+ "containerfile",
6748
+ "makefile",
6749
+ "gnumakefile",
6750
+ "rakefile",
6751
+ "gemfile",
6752
+ "procfile",
6753
+ "jenkinsfile",
6754
+ "vagrantfile",
6755
+ "brewfile",
6756
+ "go.mod",
6757
+ "go.sum",
6758
+ "go.work",
6759
+ "go.work.sum",
6760
+ "cargo.lock",
6761
+ "pipfile",
6762
+ "pipfile.lock",
6763
+ "poetry.lock",
6764
+ "uv.lock",
6765
+ "py.typed",
6766
+ "package-lock.json",
6767
+ "yarn.lock",
6768
+ "pnpm-lock.yaml",
6769
+ "composer.lock",
6770
+ "requirements.txt"
6771
+ ]);
6772
+ var KNOWN_TEXT_BASENAME_PREFIXES = ["license", "licence", "copying", "unlicense", "readme", "changelog", "dockerfile", "containerfile"];
6773
+ var KNOWN_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
6774
+ ".toml",
6775
+ ".lock",
6776
+ ".tmpl",
6777
+ ".template",
6778
+ ".mustache",
6779
+ ".hbs",
6780
+ ".handlebars",
6781
+ ".ejs",
6782
+ ".njk",
6783
+ ".liquid",
6784
+ ".vim",
6785
+ ".typed",
6786
+ ".env",
6787
+ ".properties",
6788
+ ".ini",
6789
+ ".cfg",
6790
+ ".conf",
6791
+ ".config",
6792
+ ".bazel",
6793
+ ".bzl",
6794
+ ".bat",
6795
+ ".cmd"
6796
+ ]);
6797
+ function isKnownTextPath(target) {
6798
+ const basename = path12.basename(target).toLowerCase();
6799
+ if (KNOWN_TEXT_DOTFILE_NAMES.has(basename)) {
6800
+ return true;
6801
+ }
6802
+ if (basename === ".env" || basename.startsWith(".env.")) {
6803
+ return true;
6804
+ }
6805
+ if (KNOWN_TEXT_BASENAMES.has(basename)) {
6806
+ return true;
6807
+ }
6808
+ for (const prefix of KNOWN_TEXT_BASENAME_PREFIXES) {
6809
+ if (basename === prefix || basename.startsWith(`${prefix}-`) || basename.startsWith(`${prefix}.`)) {
6810
+ return true;
6811
+ }
6812
+ }
6813
+ const extension = path12.extname(target).toLowerCase();
6814
+ if (extension && KNOWN_TEXT_EXTENSIONS.has(extension)) {
6815
+ return true;
6816
+ }
6817
+ return false;
6649
6818
  }
6650
6819
  function sourceGroupIdFor(prepared) {
6651
6820
  const originKey = prepared.originType === "url" ? prepared.url ?? prepared.title : prepared.originalPath ?? prepared.title;
@@ -8564,7 +8733,7 @@ async function collectInboxAttachmentRefs(inputDir, files) {
8564
8733
  const sourceRefs = [];
8565
8734
  for (const ref of refs) {
8566
8735
  const resolved = path12.resolve(path12.dirname(absolutePath), ref);
8567
- if (!resolved.startsWith(inputDir) || !await fileExists(resolved)) {
8736
+ if (!isPathWithin(inputDir, resolved) || !await fileExists(resolved)) {
8568
8737
  continue;
8569
8738
  }
8570
8739
  sourceRefs.push({
@@ -9099,6 +9268,7 @@ import { z as z7 } from "zod";
9099
9268
 
9100
9269
  // src/analysis.ts
9101
9270
  import path14 from "path";
9271
+ import { fromMarkdown } from "mdast-util-from-markdown";
9102
9272
  import { z as z2 } from "zod";
9103
9273
  var ANALYSIS_FORMAT_VERSION = 7;
9104
9274
  var sourceAnalysisSchema = z2.object({
@@ -9158,6 +9328,12 @@ var STOPWORDS = /* @__PURE__ */ new Set([
9158
9328
  "would",
9159
9329
  "your"
9160
9330
  ]);
9331
+ var HEURISTIC_SECTION_SOURCE_KINDS = /* @__PURE__ */ new Map([
9332
+ ["transcript", "Transcript"],
9333
+ ["chat_export", "Messages"],
9334
+ ["email", "Message"],
9335
+ ["calendar", "Description"]
9336
+ ]);
9161
9337
  function extractTopTerms(text, count) {
9162
9338
  const frequency = /* @__PURE__ */ new Map();
9163
9339
  for (const token of text.toLowerCase().match(/[a-z][a-z0-9-]{3,}/g) ?? []) {
@@ -9184,18 +9360,112 @@ function detectPolarity(text) {
9184
9360
  }
9185
9361
  return "neutral";
9186
9362
  }
9187
- function deriveTitle(manifest, text) {
9188
- const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim();
9189
- return heading || manifest.title;
9363
+ function parseMarkdownNodes(text) {
9364
+ try {
9365
+ const root = fromMarkdown(text);
9366
+ return Array.isArray(root.children) ? root.children : [];
9367
+ } catch {
9368
+ return [];
9369
+ }
9370
+ }
9371
+ function markdownNodeText(node) {
9372
+ if (node.type === "text" || node.type === "inlineCode" || node.type === "code") {
9373
+ return normalizeWhitespace(node.value ?? "");
9374
+ }
9375
+ if (node.type === "image") {
9376
+ return normalizeWhitespace(node.alt ?? "");
9377
+ }
9378
+ if (node.type === "break" || node.type === "thematicBreak") {
9379
+ return " ";
9380
+ }
9381
+ return normalizeWhitespace((node.children ?? []).map((child) => markdownNodeText(child)).join(" "));
9382
+ }
9383
+ function markdownNodesText(nodes) {
9384
+ return normalizeWhitespace(nodes.map((node) => markdownNodeText(node)).join("\n"));
9385
+ }
9386
+ function stripLeadingTitleNodes(nodes, title) {
9387
+ const normalizedTitle = normalizeWhitespace(title);
9388
+ if (!normalizedTitle || !nodes.length) {
9389
+ return nodes;
9390
+ }
9391
+ for (let index = 0; index < nodes.length; index += 1) {
9392
+ const node = nodes[index];
9393
+ if (!node) {
9394
+ continue;
9395
+ }
9396
+ const nodeText2 = markdownNodeText(node);
9397
+ if (node.type === "heading" && node.depth === 1 && nodeText2 === normalizedTitle) {
9398
+ return nodes.slice(index + 1);
9399
+ }
9400
+ if (node.type === "paragraph" && nodeText2 === normalizedTitle) {
9401
+ return nodes.slice(index + 1);
9402
+ }
9403
+ return nodes;
9404
+ }
9405
+ return nodes;
9406
+ }
9407
+ function markdownSectionNodes(nodes, heading) {
9408
+ const normalizedHeading = normalizeWhitespace(heading);
9409
+ for (let index = 0; index < nodes.length; index += 1) {
9410
+ const node = nodes[index];
9411
+ if (node?.type !== "heading" || node.depth !== 2) {
9412
+ continue;
9413
+ }
9414
+ if (markdownNodeText(node) !== normalizedHeading) {
9415
+ continue;
9416
+ }
9417
+ const sectionNodes = [];
9418
+ for (let cursor = index + 1; cursor < nodes.length; cursor += 1) {
9419
+ const candidate = nodes[cursor];
9420
+ if (candidate?.type === "heading" && typeof candidate.depth === "number" && candidate.depth <= 2) {
9421
+ break;
9422
+ }
9423
+ if (candidate) {
9424
+ sectionNodes.push(candidate);
9425
+ }
9426
+ }
9427
+ return sectionNodes;
9428
+ }
9429
+ return [];
9430
+ }
9431
+ function textForHeuristicAnalysis(manifest, text) {
9432
+ const nodes = parseMarkdownNodes(text);
9433
+ if (!nodes.length) {
9434
+ return normalizeWhitespace(text);
9435
+ }
9436
+ const sectionHeading = HEURISTIC_SECTION_SOURCE_KINDS.get(manifest.sourceKind);
9437
+ const scopedNodes = sectionHeading ? markdownSectionNodes(nodes, sectionHeading) : nodes;
9438
+ const relevantNodes = scopedNodes.length ? scopedNodes : nodes;
9439
+ const contentNodes = stripLeadingTitleNodes(relevantNodes, manifest.title);
9440
+ const normalized = markdownNodesText(contentNodes.length ? contentNodes : relevantNodes);
9441
+ return normalized || normalizeWhitespace(text);
9442
+ }
9443
+ function normalizeAnalysisTitle(manifest, candidate) {
9444
+ if (manifest.sourceKind !== "code") {
9445
+ return manifest.title;
9446
+ }
9447
+ const normalized = normalizeWhitespace(candidate.replace(/^#+\s+/, ""));
9448
+ if (!normalized) {
9449
+ return manifest.title;
9450
+ }
9451
+ if (normalized.length > 140 || normalized.includes(" ## ")) {
9452
+ return manifest.title;
9453
+ }
9454
+ return normalized;
9455
+ }
9456
+ function normalizeSourceAnalysis(manifest, analysis) {
9457
+ const title = normalizeAnalysisTitle(manifest, analysis.title);
9458
+ return title === analysis.title ? analysis : { ...analysis, title };
9190
9459
  }
9191
9460
  function heuristicAnalysis(manifest, text, schemaHash) {
9192
- const normalized = normalizeWhitespace(text);
9461
+ const analysisText = textForHeuristicAnalysis(manifest, text);
9462
+ const normalized = normalizeWhitespace(analysisText);
9193
9463
  const concepts = extractTopTerms(normalized, 6).map((term) => ({
9194
9464
  id: `concept:${slugify(term)}`,
9195
9465
  name: term,
9196
9466
  description: `Frequently referenced concept in ${manifest.title}.`
9197
9467
  }));
9198
- const entities = extractEntities(text, 6).map((term) => ({
9468
+ const entities = extractEntities(analysisText, 6).map((term) => ({
9199
9469
  id: `entity:${slugify(term)}`,
9200
9470
  name: term,
9201
9471
  description: `Named entity mentioned in ${manifest.title}.`
@@ -9208,7 +9478,7 @@ function heuristicAnalysis(manifest, text, schemaHash) {
9208
9478
  semanticHash: manifest.semanticHash,
9209
9479
  extractionHash: manifest.extractionHash,
9210
9480
  schemaHash,
9211
- title: deriveTitle(manifest, text),
9481
+ title: manifest.title,
9212
9482
  summary: firstSentences(normalized, 3) || truncate(normalized, 280) || `Imported ${manifest.sourceKind} source.`,
9213
9483
  concepts,
9214
9484
  entities,
@@ -9333,7 +9603,11 @@ async function analyzeSource(manifest, extractedText, provider, paths, schema) {
9333
9603
  const cachePath = path14.join(paths.analysesDir, `${manifest.sourceId}.json`);
9334
9604
  const cached = await readJsonFile(cachePath);
9335
9605
  if (cached && cached.analysisVersion === ANALYSIS_FORMAT_VERSION && (cached.semanticHash ?? cached.sourceHash) === manifest.semanticHash && cached.extractionHash === manifest.extractionHash && cached.schemaHash === schema.hash) {
9336
- return cached;
9606
+ const normalizedCached = normalizeSourceAnalysis(manifest, cached);
9607
+ if (normalizedCached !== cached) {
9608
+ await writeJsonFile(cachePath, normalizedCached);
9609
+ }
9610
+ return normalizedCached;
9337
9611
  }
9338
9612
  const extraction = await readExtractionArtifact(paths.rootDir, manifest);
9339
9613
  const content = normalizeWhitespace(extractedText ?? "");
@@ -9398,8 +9672,9 @@ async function analyzeSource(manifest, extractedText, provider, paths, schema) {
9398
9672
  analysis = heuristicAnalysis(manifest, content, schema.hash);
9399
9673
  }
9400
9674
  }
9401
- await writeJsonFile(cachePath, analysis);
9402
- return analysis;
9675
+ const normalized = normalizeSourceAnalysis(manifest, analysis);
9676
+ await writeJsonFile(cachePath, normalized);
9677
+ return normalized;
9403
9678
  }
9404
9679
  function analysisSignature(analysis) {
9405
9680
  return sha256(JSON.stringify(analysis));
@@ -13486,7 +13761,7 @@ async function resolveImageGenerationProvider(rootDir) {
13486
13761
  if (!providerConfig) {
13487
13762
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
13488
13763
  }
13489
- const { createProvider: createProvider2 } = await import("./registry-TYROWPR5.js");
13764
+ const { createProvider: createProvider2 } = await import("./registry-NBLIJHZT.js");
13490
13765
  return createProvider2(preferredProviderId, providerConfig, rootDir);
13491
13766
  }
13492
13767
  async function generateOutputArtifacts(rootDir, input) {
@@ -17525,9 +17800,16 @@ async function listPages(rootDir) {
17525
17800
  return graph?.pages ?? [];
17526
17801
  }
17527
17802
  async function readPage(rootDir, relativePath) {
17803
+ if (!relativePath) {
17804
+ return null;
17805
+ }
17528
17806
  const { paths } = await loadVaultConfig(rootDir);
17529
17807
  const absolutePath = path23.resolve(paths.wikiDir, relativePath);
17530
- if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
17808
+ if (!isPathWithin(paths.wikiDir, absolutePath)) {
17809
+ return null;
17810
+ }
17811
+ const stats = await fs19.stat(absolutePath).catch(() => null);
17812
+ if (!stats?.isFile()) {
17531
17813
  return null;
17532
17814
  }
17533
17815
  const raw = await fs19.readFile(absolutePath, "utf8");
@@ -17556,6 +17838,28 @@ async function getWorkspaceInfo(rootDir) {
17556
17838
  pageCount: pages.length
17557
17839
  };
17558
17840
  }
17841
+ function extractClaimSectionLines(content) {
17842
+ const lines = content.split("\n");
17843
+ let inClaims = false;
17844
+ let found = false;
17845
+ const claimLines = [];
17846
+ for (const line of lines) {
17847
+ const trimmed = line.trimEnd();
17848
+ if (trimmed === "## Claims") {
17849
+ inClaims = true;
17850
+ found = true;
17851
+ continue;
17852
+ }
17853
+ if (inClaims) {
17854
+ if (/^#{1,2}\s/.test(trimmed)) {
17855
+ inClaims = false;
17856
+ continue;
17857
+ }
17858
+ claimLines.push(line);
17859
+ }
17860
+ }
17861
+ return found ? claimLines : null;
17862
+ }
17559
17863
  function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sourceProjects) {
17560
17864
  const manifestMap = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
17561
17865
  const pageMap2 = new Map(graph.pages.map((page) => [page.id, page]));
@@ -17601,8 +17905,9 @@ function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sour
17601
17905
  const absolutePath = path23.join(paths.wikiDir, page.path);
17602
17906
  if (await fileExists(absolutePath)) {
17603
17907
  const content = await fs19.readFile(absolutePath, "utf8");
17604
- if (content.includes("## Claims")) {
17605
- const uncited = content.split("\n").filter((line) => line.startsWith("- ") && !line.includes("[source:"));
17908
+ const claimLines = extractClaimSectionLines(content);
17909
+ if (claimLines !== null) {
17910
+ const uncited = claimLines.filter((line) => line.startsWith("- ") && !line.includes("[source:"));
17606
17911
  if (uncited.length) {
17607
17912
  findings.push({
17608
17913
  severity: "warning",
@@ -17717,7 +18022,7 @@ async function bootstrapDemo(rootDir, input) {
17717
18022
  }
17718
18023
 
17719
18024
  // src/mcp.ts
17720
- var SERVER_VERSION = "0.6.4";
18025
+ var SERVER_VERSION = "0.6.6";
17721
18026
  async function createMcpServer(rootDir) {
17722
18027
  const server = new McpServer({
17723
18028
  name: "swarmvault",
@@ -18052,7 +18357,7 @@ async function createMcpServer(rootDir) {
18052
18357
  const encodedPath = typeof variables.path === "string" ? variables.path : "";
18053
18358
  const relativePath = decodeURIComponent(encodedPath);
18054
18359
  const absolutePath = path24.resolve(paths.sessionsDir, relativePath);
18055
- if (!absolutePath.startsWith(paths.sessionsDir) || !await fileExists(absolutePath)) {
18360
+ if (!isPathWithin(paths.sessionsDir, absolutePath) || !await fileExists(absolutePath)) {
18056
18361
  return asTextResource(`swarmvault://sessions/${encodedPath}`, `Session not found: ${relativePath}`);
18057
18362
  }
18058
18363
  return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs20.readFile(absolutePath, "utf8"));
@@ -18391,6 +18696,15 @@ var DOCS_HINT_SEGMENTS = /* @__PURE__ */ new Set([
18391
18696
  function uniqueStrings4(values) {
18392
18697
  return uniqueBy(values.filter(Boolean), (value) => value);
18393
18698
  }
18699
+ function sourceOutputSchemaHash(schemas, projectIds) {
18700
+ if (!projectIds.length) {
18701
+ return schemas.effective.global.hash;
18702
+ }
18703
+ return composeVaultSchema(
18704
+ schemas.root,
18705
+ uniqueStrings4([...projectIds].sort((left, right) => left.localeCompare(right))).map((projectId) => schemas.projects[projectId]).filter((schema) => Boolean(schema?.hash))
18706
+ ).hash;
18707
+ }
18394
18708
  function normalizeManagedStatus(value) {
18395
18709
  return value === "missing" || value === "error" ? value : "ready";
18396
18710
  }
@@ -18974,6 +19288,7 @@ async function writeSourceBriefForScope(rootDir, source) {
18974
19288
  return null;
18975
19289
  }
18976
19290
  const graph = await readJsonFile(paths.graphPath);
19291
+ const schemas = await loadVaultSchemas(rootDir);
18977
19292
  const relatedPages = graph ? scopedSourcePages(graph, source.sourceIds) : [];
18978
19293
  const relatedPageIds = relatedPages.slice(0, 12).map((page) => page.id);
18979
19294
  const relatedNodeIds = graph ? scopedNodeIds(graph, source.sourceIds).slice(0, 20) : [];
@@ -18984,7 +19299,7 @@ async function writeSourceBriefForScope(rootDir, source) {
18984
19299
  question: `Brief ${source.title}`,
18985
19300
  answer: markdown,
18986
19301
  citations: source.sourceIds,
18987
- schemaHash: graph?.generatedAt ?? "",
19302
+ schemaHash: sourceOutputSchemaHash(schemas, projectIds),
18988
19303
  outputFormat: "report",
18989
19304
  relatedPageIds,
18990
19305
  relatedNodeIds,
@@ -19219,6 +19534,7 @@ async function buildSourceReviewStagedPage(rootDir, scope) {
19219
19534
  throw new Error(`Could not generate a source review for ${scope.id}.`);
19220
19535
  }
19221
19536
  const graph = await readJsonFile(paths.graphPath);
19537
+ const schemas = await loadVaultSchemas(rootDir);
19222
19538
  const scopeManifests = manifestsForScope(graph, scope);
19223
19539
  const relatedPages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
19224
19540
  const relatedPageIds = relatedPages.slice(0, 16).map((page) => page.id);
@@ -19230,7 +19546,7 @@ async function buildSourceReviewStagedPage(rootDir, scope) {
19230
19546
  question: `Review ${scope.title}`,
19231
19547
  answer: markdown,
19232
19548
  citations: scope.sourceIds,
19233
- schemaHash: graph?.generatedAt ?? "",
19549
+ schemaHash: sourceOutputSchemaHash(schemas, projectIds),
19234
19550
  outputFormat: "report",
19235
19551
  relatedPageIds,
19236
19552
  relatedNodeIds,
@@ -19464,6 +19780,7 @@ async function buildSourceGuideStagedPage(rootDir, scope) {
19464
19780
  throw new Error(`Could not generate a source guide for ${scope.id}.`);
19465
19781
  }
19466
19782
  const graph = await readJsonFile(paths.graphPath);
19783
+ const schemas = await loadVaultSchemas(rootDir);
19467
19784
  const scopeManifests = manifestsForScope(graph, scope);
19468
19785
  const relatedPages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
19469
19786
  const contradictions = findContradictionsForScope(scope, await readGraphReport(rootDir));
@@ -19477,7 +19794,7 @@ async function buildSourceGuideStagedPage(rootDir, scope) {
19477
19794
  question: `Guide ${scope.title}`,
19478
19795
  answer: markdown,
19479
19796
  citations: scope.sourceIds,
19480
- schemaHash: graph?.generatedAt ?? "",
19797
+ schemaHash: sourceOutputSchemaHash(schemas, projectIds),
19481
19798
  outputFormat: "report",
19482
19799
  relatedPageIds,
19483
19800
  relatedNodeIds,
@@ -19566,6 +19883,7 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
19566
19883
  await compileVault(rootDir, {});
19567
19884
  graph = await readJsonFile(paths.graphPath);
19568
19885
  }
19886
+ const schemas = await loadVaultSchemas(rootDir);
19569
19887
  const scopeManifests = manifestsForScope(graph, scope);
19570
19888
  const sourcePages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
19571
19889
  const analyses = await loadSourceAnalyses(rootDir, scope.sourceIds);
@@ -19631,7 +19949,7 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
19631
19949
  question: `Guided Session ${scope.title}`,
19632
19950
  answer: sessionMarkdown,
19633
19951
  citations: scope.sourceIds,
19634
- schemaHash: graph?.generatedAt ?? "",
19952
+ schemaHash: sourceOutputSchemaHash(schemas, projectIds),
19635
19953
  outputFormat: "report",
19636
19954
  relatedPageIds,
19637
19955
  relatedNodeIds,
@@ -19984,6 +20302,15 @@ async function resumeSourceSession(rootDir, id, options = {}) {
19984
20302
  function shouldCompile(changedSources, graphExists, compileRequested) {
19985
20303
  return compileRequested && (!graphExists || changedSources.length > 0);
19986
20304
  }
20305
+ async function shouldRefreshBriefForManagedSource(source, options) {
20306
+ if (options.compilePerformed || options.changed) {
20307
+ return true;
20308
+ }
20309
+ if (!source.briefPath) {
20310
+ return true;
20311
+ }
20312
+ return !await fileExists(source.briefPath);
20313
+ }
19987
20314
  async function listManagedSourceRecords(rootDir) {
19988
20315
  await ensureManagedSourcesArtifact(rootDir);
19989
20316
  return await loadManagedSources(rootDir);
@@ -20015,12 +20342,15 @@ async function addManagedSource(rootDir, input, options = {}) {
20015
20342
  }
20016
20343
  const graphExists = await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath));
20017
20344
  let compile;
20018
- if (shouldCompile([synced], graphExists, compileRequested)) {
20345
+ if (shouldCompile(synced.changed ? [synced] : [], graphExists, compileRequested)) {
20019
20346
  compile = await compileVault(rootDir, {});
20020
20347
  }
20021
20348
  let briefGenerated = false;
20022
20349
  let briefPath;
20023
- if (compileRequested && briefRequested && synced.status === "ready") {
20350
+ if (compileRequested && briefRequested && synced.status === "ready" && await shouldRefreshBriefForManagedSource(synced, {
20351
+ compilePerformed: Boolean(compile),
20352
+ changed: synced.changed
20353
+ })) {
20024
20354
  const briefs = await generateBriefsForSources(rootDir, [synced]);
20025
20355
  briefPath = briefs.get(synced.id);
20026
20356
  briefGenerated = Boolean(briefPath);
@@ -20664,10 +20994,21 @@ async function getWatchStatus(rootDir) {
20664
20994
 
20665
20995
  // src/viewer.ts
20666
20996
  var execFileAsync = promisify(execFile);
20997
+ async function isReadableFile(absolutePath) {
20998
+ try {
20999
+ const stats = await fs23.stat(absolutePath);
21000
+ return stats.isFile();
21001
+ } catch {
21002
+ return false;
21003
+ }
21004
+ }
20667
21005
  async function readViewerPage(rootDir, relativePath) {
21006
+ if (!relativePath) {
21007
+ return null;
21008
+ }
20668
21009
  const { paths } = await loadVaultConfig(rootDir);
20669
21010
  const absolutePath = path28.resolve(paths.wikiDir, relativePath);
20670
- if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
21011
+ if (!isPathWithin(paths.wikiDir, absolutePath) || !await isReadableFile(absolutePath)) {
20671
21012
  return null;
20672
21013
  }
20673
21014
  const raw = await fs23.readFile(absolutePath, "utf8");
@@ -20681,9 +21022,12 @@ async function readViewerPage(rootDir, relativePath) {
20681
21022
  };
20682
21023
  }
20683
21024
  async function readViewerAsset(rootDir, relativePath) {
21025
+ if (!relativePath) {
21026
+ return null;
21027
+ }
20684
21028
  const { paths } = await loadVaultConfig(rootDir);
20685
21029
  const absolutePath = path28.resolve(paths.wikiDir, relativePath);
20686
- if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
21030
+ if (!isPathWithin(paths.wikiDir, absolutePath) || !await isReadableFile(absolutePath)) {
20687
21031
  return null;
20688
21032
  }
20689
21033
  return {
@@ -20725,178 +21069,204 @@ async function startGraphServer(rootDir, port, options = {}) {
20725
21069
  await ensureViewerDist(paths.viewerDistDir);
20726
21070
  const server = http.createServer(async (request, response) => {
20727
21071
  const url = new URL(request.url ?? "/", `http://${request.headers.host ?? `localhost:${effectivePort}`}`);
20728
- if (url.pathname === "/api/graph") {
20729
- if (!await fileExists(paths.graphPath)) {
20730
- response.writeHead(404, { "content-type": "application/json" });
20731
- response.end(JSON.stringify({ error: "Graph artifact not found. Run `swarmvault compile` first." }));
21072
+ try {
21073
+ if (url.pathname === "/api/graph") {
21074
+ if (!await fileExists(paths.graphPath)) {
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 graph = await readJsonFile(paths.graphPath);
21080
+ if (!graph) {
21081
+ response.writeHead(404, { "content-type": "application/json" });
21082
+ response.end(JSON.stringify({ error: "Graph artifact not found. Run `swarmvault compile` first." }));
21083
+ return;
21084
+ }
21085
+ const reportPath = path28.join(paths.wikiDir, "graph", "report.json");
21086
+ const report = await readJsonFile(reportPath) ?? null;
21087
+ response.writeHead(200, { "content-type": "application/json" });
21088
+ response.end(JSON.stringify(buildViewerGraphArtifact(graph, { report, full: options.full ?? false })));
20732
21089
  return;
20733
21090
  }
20734
- const graph = await readJsonFile(paths.graphPath);
20735
- if (!graph) {
20736
- response.writeHead(404, { "content-type": "application/json" });
20737
- response.end(JSON.stringify({ error: "Graph artifact not found. Run `swarmvault compile` first." }));
21091
+ if (url.pathname === "/api/graph/query") {
21092
+ const question = url.searchParams.get("q") ?? "";
21093
+ const traversal = url.searchParams.get("traversal");
21094
+ const budget = Number.parseInt(url.searchParams.get("budget") ?? "12", 10);
21095
+ const result = await queryGraphVault(rootDir, question, {
21096
+ traversal: traversal === "dfs" ? "dfs" : "bfs",
21097
+ budget: Number.isFinite(budget) ? budget : 12
21098
+ });
21099
+ response.writeHead(200, { "content-type": "application/json" });
21100
+ response.end(JSON.stringify(result));
20738
21101
  return;
20739
21102
  }
20740
- const reportPath = path28.join(paths.wikiDir, "graph", "report.json");
20741
- const report = await readJsonFile(reportPath) ?? null;
20742
- response.writeHead(200, { "content-type": "application/json" });
20743
- response.end(JSON.stringify(buildViewerGraphArtifact(graph, { report, full: options.full ?? false })));
20744
- return;
20745
- }
20746
- if (url.pathname === "/api/graph/query") {
20747
- const question = url.searchParams.get("q") ?? "";
20748
- const traversal = url.searchParams.get("traversal");
20749
- const budget = Number.parseInt(url.searchParams.get("budget") ?? "12", 10);
20750
- response.writeHead(200, { "content-type": "application/json" });
20751
- response.end(
20752
- JSON.stringify(
20753
- await queryGraphVault(rootDir, question, {
20754
- traversal: traversal === "dfs" ? "dfs" : "bfs",
20755
- budget: Number.isFinite(budget) ? budget : 12
20756
- })
20757
- )
20758
- );
20759
- return;
20760
- }
20761
- if (url.pathname === "/api/graph/path") {
20762
- const from = url.searchParams.get("from") ?? "";
20763
- const to = url.searchParams.get("to") ?? "";
20764
- response.writeHead(200, { "content-type": "application/json" });
20765
- response.end(JSON.stringify(await pathGraphVault(rootDir, from, to)));
20766
- return;
20767
- }
20768
- if (url.pathname === "/api/graph/explain") {
20769
- const target2 = url.searchParams.get("target") ?? "";
20770
- response.writeHead(200, { "content-type": "application/json" });
20771
- response.end(JSON.stringify(await explainGraphVault(rootDir, target2)));
20772
- return;
20773
- }
20774
- if (url.pathname === "/api/search") {
20775
- if (!await fileExists(paths.searchDbPath)) {
20776
- response.writeHead(404, { "content-type": "application/json" });
20777
- response.end(JSON.stringify({ error: "Search index not found. Run `swarmvault compile` first." }));
21103
+ if (url.pathname === "/api/graph/path") {
21104
+ const from = url.searchParams.get("from") ?? "";
21105
+ const to = url.searchParams.get("to") ?? "";
21106
+ const result = await pathGraphVault(rootDir, from, to);
21107
+ response.writeHead(200, { "content-type": "application/json" });
21108
+ response.end(JSON.stringify(result));
20778
21109
  return;
20779
21110
  }
20780
- const query = url.searchParams.get("q") ?? "";
20781
- const limit = Number.parseInt(url.searchParams.get("limit") ?? "10", 10);
20782
- const kind = url.searchParams.get("kind") ?? "all";
20783
- const status = url.searchParams.get("status") ?? "all";
20784
- const project = url.searchParams.get("project") ?? "all";
20785
- const sourceType = url.searchParams.get("sourceType") ?? "all";
20786
- const sourceClass = url.searchParams.get("sourceClass") ?? "all";
20787
- const results = searchPages(paths.searchDbPath, query, {
20788
- limit: Number.isFinite(limit) ? limit : 10,
20789
- kind,
20790
- status,
20791
- project,
20792
- sourceType,
20793
- sourceClass
20794
- });
20795
- response.writeHead(200, { "content-type": "application/json" });
20796
- response.end(JSON.stringify(results));
20797
- return;
20798
- }
20799
- if (url.pathname === "/api/graph-report") {
20800
- const reportPath = path28.join(paths.wikiDir, "graph", "report.json");
20801
- if (!await fileExists(reportPath)) {
20802
- response.writeHead(404, { "content-type": "application/json" });
20803
- response.end(JSON.stringify({ error: "Graph report artifact not found. Run `swarmvault compile` first." }));
21111
+ if (url.pathname === "/api/graph/explain") {
21112
+ const target2 = url.searchParams.get("target") ?? "";
21113
+ if (!target2) {
21114
+ response.writeHead(400, { "content-type": "application/json" });
21115
+ response.end(JSON.stringify({ error: "Missing explain target." }));
21116
+ return;
21117
+ }
21118
+ try {
21119
+ const result = await explainGraphVault(rootDir, target2);
21120
+ response.writeHead(200, { "content-type": "application/json" });
21121
+ response.end(JSON.stringify(result));
21122
+ } catch (error) {
21123
+ response.writeHead(404, { "content-type": "application/json" });
21124
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : `Could not resolve graph target: ${target2}` }));
21125
+ }
20804
21126
  return;
20805
21127
  }
20806
- response.writeHead(200, { "content-type": "application/json" });
20807
- response.end(await fs23.readFile(reportPath, "utf8"));
20808
- return;
20809
- }
20810
- if (url.pathname === "/api/watch-status") {
20811
- response.writeHead(200, { "content-type": "application/json" });
20812
- response.end(JSON.stringify(await getWatchStatus(rootDir)));
20813
- return;
20814
- }
20815
- if (url.pathname === "/api/page") {
20816
- const relativePath2 = url.searchParams.get("path") ?? "";
20817
- const page = await readViewerPage(rootDir, relativePath2);
20818
- if (!page) {
20819
- response.writeHead(404, { "content-type": "application/json" });
20820
- response.end(JSON.stringify({ error: `Page not found: ${relativePath2}` }));
21128
+ if (url.pathname === "/api/search") {
21129
+ if (!await fileExists(paths.searchDbPath)) {
21130
+ response.writeHead(404, { "content-type": "application/json" });
21131
+ response.end(JSON.stringify({ error: "Search index not found. Run `swarmvault compile` first." }));
21132
+ return;
21133
+ }
21134
+ const query = url.searchParams.get("q") ?? "";
21135
+ const limit = Number.parseInt(url.searchParams.get("limit") ?? "10", 10);
21136
+ const kind = url.searchParams.get("kind") ?? "all";
21137
+ const status = url.searchParams.get("status") ?? "all";
21138
+ const project = url.searchParams.get("project") ?? "all";
21139
+ const sourceType = url.searchParams.get("sourceType") ?? "all";
21140
+ const sourceClass = url.searchParams.get("sourceClass") ?? "all";
21141
+ const results = searchPages(paths.searchDbPath, query, {
21142
+ limit: Number.isFinite(limit) ? limit : 10,
21143
+ kind,
21144
+ status,
21145
+ project,
21146
+ sourceType,
21147
+ sourceClass
21148
+ });
21149
+ response.writeHead(200, { "content-type": "application/json" });
21150
+ response.end(JSON.stringify(results));
20821
21151
  return;
20822
21152
  }
20823
- response.writeHead(200, { "content-type": "application/json" });
20824
- response.end(JSON.stringify(page));
20825
- return;
20826
- }
20827
- if (url.pathname === "/api/asset") {
20828
- const relativePath2 = url.searchParams.get("path") ?? "";
20829
- const asset = await readViewerAsset(rootDir, relativePath2);
20830
- if (!asset) {
20831
- response.writeHead(404, { "content-type": "application/json" });
20832
- response.end(JSON.stringify({ error: `Asset not found: ${relativePath2}` }));
21153
+ if (url.pathname === "/api/graph-report") {
21154
+ const reportPath = path28.join(paths.wikiDir, "graph", "report.json");
21155
+ if (!await fileExists(reportPath)) {
21156
+ response.writeHead(404, { "content-type": "application/json" });
21157
+ response.end(JSON.stringify({ error: "Graph report artifact not found. Run `swarmvault compile` first." }));
21158
+ return;
21159
+ }
21160
+ const body = await fs23.readFile(reportPath, "utf8");
21161
+ response.writeHead(200, { "content-type": "application/json" });
21162
+ response.end(body);
20833
21163
  return;
20834
21164
  }
20835
- response.writeHead(200, { "content-type": asset.mimeType });
20836
- response.end(asset.buffer);
20837
- return;
20838
- }
20839
- if (url.pathname === "/api/reviews" && request.method === "GET") {
20840
- response.writeHead(200, { "content-type": "application/json" });
20841
- response.end(JSON.stringify(await listApprovals(rootDir)));
20842
- return;
20843
- }
20844
- if (url.pathname === "/api/review" && request.method === "GET") {
20845
- const approvalId = url.searchParams.get("id") ?? "";
20846
- if (!approvalId) {
20847
- response.writeHead(400, { "content-type": "application/json" });
20848
- response.end(JSON.stringify({ error: "Missing approval id." }));
21165
+ if (url.pathname === "/api/watch-status") {
21166
+ const watchStatus = await getWatchStatus(rootDir);
21167
+ response.writeHead(200, { "content-type": "application/json" });
21168
+ response.end(JSON.stringify(watchStatus));
20849
21169
  return;
20850
21170
  }
20851
- response.writeHead(200, { "content-type": "application/json" });
20852
- response.end(JSON.stringify(await readApproval(rootDir, approvalId)));
20853
- return;
20854
- }
20855
- if (url.pathname === "/api/review" && request.method === "POST") {
20856
- const body = await readJsonBody(request);
20857
- const approvalId = typeof body.approvalId === "string" ? body.approvalId : "";
20858
- const targets = Array.isArray(body.targets) ? body.targets.filter((item) => typeof item === "string") : [];
20859
- const action = url.searchParams.get("action") ?? "";
20860
- if (!approvalId || action !== "accept" && action !== "reject") {
20861
- response.writeHead(400, { "content-type": "application/json" });
20862
- response.end(JSON.stringify({ error: "Missing approval id or invalid review action." }));
21171
+ if (url.pathname === "/api/page") {
21172
+ const relativePath2 = url.searchParams.get("path") ?? "";
21173
+ const page = await readViewerPage(rootDir, relativePath2);
21174
+ if (!page) {
21175
+ response.writeHead(404, { "content-type": "application/json" });
21176
+ response.end(JSON.stringify({ error: `Page not found: ${relativePath2}` }));
21177
+ return;
21178
+ }
21179
+ response.writeHead(200, { "content-type": "application/json" });
21180
+ response.end(JSON.stringify(page));
20863
21181
  return;
20864
21182
  }
20865
- const result = action === "accept" ? await acceptApproval(rootDir, approvalId, targets) : await rejectApproval(rootDir, approvalId, targets);
20866
- response.writeHead(200, { "content-type": "application/json" });
20867
- response.end(JSON.stringify(result));
20868
- return;
20869
- }
20870
- if (url.pathname === "/api/candidates" && request.method === "GET") {
20871
- response.writeHead(200, { "content-type": "application/json" });
20872
- response.end(JSON.stringify(await listCandidates(rootDir)));
20873
- return;
20874
- }
20875
- if (url.pathname === "/api/candidate" && request.method === "POST") {
20876
- const body = await readJsonBody(request);
20877
- const target2 = typeof body.target === "string" ? body.target : "";
20878
- const action = url.searchParams.get("action") ?? "";
20879
- if (!target2 || action !== "promote" && action !== "archive") {
20880
- response.writeHead(400, { "content-type": "application/json" });
20881
- response.end(JSON.stringify({ error: "Missing candidate target or invalid candidate action." }));
21183
+ if (url.pathname === "/api/asset") {
21184
+ const relativePath2 = url.searchParams.get("path") ?? "";
21185
+ const asset = await readViewerAsset(rootDir, relativePath2);
21186
+ if (!asset) {
21187
+ response.writeHead(404, { "content-type": "application/json" });
21188
+ response.end(JSON.stringify({ error: `Asset not found: ${relativePath2}` }));
21189
+ return;
21190
+ }
21191
+ response.writeHead(200, { "content-type": asset.mimeType });
21192
+ response.end(asset.buffer);
20882
21193
  return;
20883
21194
  }
20884
- const result = action === "promote" ? await promoteCandidate(rootDir, target2) : await archiveCandidate(rootDir, target2);
20885
- response.writeHead(200, { "content-type": "application/json" });
20886
- response.end(JSON.stringify(result));
20887
- return;
20888
- }
20889
- const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
20890
- const target = path28.join(paths.viewerDistDir, relativePath);
20891
- const fallback = path28.join(paths.viewerDistDir, "index.html");
20892
- const filePath = await fileExists(target) ? target : fallback;
20893
- if (!await fileExists(filePath)) {
20894
- response.writeHead(503, { "content-type": "text/plain" });
20895
- response.end("Viewer build not found. Run `pnpm build` first.");
20896
- return;
21195
+ if (url.pathname === "/api/reviews" && request.method === "GET") {
21196
+ const approvals = await listApprovals(rootDir);
21197
+ response.writeHead(200, { "content-type": "application/json" });
21198
+ response.end(JSON.stringify(approvals));
21199
+ return;
21200
+ }
21201
+ if (url.pathname === "/api/review" && request.method === "GET") {
21202
+ const approvalId = url.searchParams.get("id") ?? "";
21203
+ if (!approvalId) {
21204
+ response.writeHead(400, { "content-type": "application/json" });
21205
+ response.end(JSON.stringify({ error: "Missing approval id." }));
21206
+ return;
21207
+ }
21208
+ const approval = await readApproval(rootDir, approvalId);
21209
+ response.writeHead(200, { "content-type": "application/json" });
21210
+ response.end(JSON.stringify(approval));
21211
+ return;
21212
+ }
21213
+ if (url.pathname === "/api/review" && request.method === "POST") {
21214
+ const body = await readJsonBody(request);
21215
+ const approvalId = typeof body.approvalId === "string" ? body.approvalId : "";
21216
+ const targets = Array.isArray(body.targets) ? body.targets.filter((item) => typeof item === "string") : [];
21217
+ const action = url.searchParams.get("action") ?? "";
21218
+ if (!approvalId || action !== "accept" && action !== "reject") {
21219
+ response.writeHead(400, { "content-type": "application/json" });
21220
+ response.end(JSON.stringify({ error: "Missing approval id or invalid review action." }));
21221
+ return;
21222
+ }
21223
+ const result = action === "accept" ? await acceptApproval(rootDir, approvalId, targets) : await rejectApproval(rootDir, approvalId, targets);
21224
+ response.writeHead(200, { "content-type": "application/json" });
21225
+ response.end(JSON.stringify(result));
21226
+ return;
21227
+ }
21228
+ if (url.pathname === "/api/candidates" && request.method === "GET") {
21229
+ const candidates = await listCandidates(rootDir);
21230
+ response.writeHead(200, { "content-type": "application/json" });
21231
+ response.end(JSON.stringify(candidates));
21232
+ return;
21233
+ }
21234
+ if (url.pathname === "/api/candidate" && request.method === "POST") {
21235
+ const body = await readJsonBody(request);
21236
+ const target2 = typeof body.target === "string" ? body.target : "";
21237
+ const action = url.searchParams.get("action") ?? "";
21238
+ if (!target2 || action !== "promote" && action !== "archive") {
21239
+ response.writeHead(400, { "content-type": "application/json" });
21240
+ response.end(JSON.stringify({ error: "Missing candidate target or invalid candidate action." }));
21241
+ return;
21242
+ }
21243
+ const result = action === "promote" ? await promoteCandidate(rootDir, target2) : await archiveCandidate(rootDir, target2);
21244
+ response.writeHead(200, { "content-type": "application/json" });
21245
+ response.end(JSON.stringify(result));
21246
+ return;
21247
+ }
21248
+ const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
21249
+ const target = path28.join(paths.viewerDistDir, relativePath);
21250
+ const fallback = path28.join(paths.viewerDistDir, "index.html");
21251
+ const filePath = await fileExists(target) ? target : fallback;
21252
+ if (!await fileExists(filePath)) {
21253
+ response.writeHead(503, { "content-type": "text/plain" });
21254
+ response.end("Viewer build not found. Run `pnpm build` first.");
21255
+ return;
21256
+ }
21257
+ const staticBody = await fs23.readFile(filePath);
21258
+ response.writeHead(200, { "content-type": mime2.lookup(filePath) || "text/plain" });
21259
+ response.end(staticBody);
21260
+ } catch (error) {
21261
+ const message = error instanceof Error ? error.message : String(error);
21262
+ console.error(`[viewer] ${request.method ?? "GET"} ${url.pathname} failed: ${message}`);
21263
+ if (!response.headersSent) {
21264
+ response.writeHead(500, { "content-type": "application/json" });
21265
+ response.end(JSON.stringify({ error: message }));
21266
+ } else {
21267
+ response.end();
21268
+ }
20897
21269
  }
20898
- response.writeHead(200, { "content-type": mime2.lookup(filePath) || "text/plain" });
20899
- response.end(await fs23.readFile(filePath));
20900
21270
  });
20901
21271
  await new Promise((resolve) => {
20902
21272
  server.listen(effectivePort, resolve);